ensure actions on loans with dates that violate domain rules throws exception that is handled gracefully.

This commit is contained in:
Keith Woodlock
2012-06-01 01:17:21 +01:00
parent 4579d66cdd
commit 3afd2a6f74
6 changed files with 91 additions and 115 deletions
@@ -0,0 +1,11 @@
package org.mifosng.platform.exceptions;
/**
* {@link AbstractPlatformDomainRuleException} thrown an action to transition a loan from one state to another violates a domain rule.
*/
public class InvalidLoanStateTransitionException extends AbstractPlatformDomainRuleException {
public InvalidLoanStateTransitionException(final String action, String postFix, String defaultUserMessage, Object... defaultUserMessageArgs) {
super("error.msg.loan." + action + postFix, defaultUserMessage, defaultUserMessageArgs);
}
}
@@ -1,9 +0,0 @@
package org.mifosng.platform.exceptions;
public class InvalidSignupException extends RuntimeException {
public InvalidSignupException(final Throwable e) {
super(e);
}
}
@@ -1,15 +0,0 @@
package org.mifosng.platform.loan.domain;
public class InvalidLoanTimelineDate extends RuntimeException {
private final String errorCode;
public InvalidLoanTimelineDate(final String message, final String errorCode) {
super(message);
this.errorCode = errorCode;
}
public String getErrorCode() {
return this.errorCode;
}
}
@@ -33,6 +33,7 @@ import org.mifosng.platform.api.data.MoneyData;
import org.mifosng.platform.client.domain.Client; import org.mifosng.platform.client.domain.Client;
import org.mifosng.platform.currency.domain.MonetaryCurrency; import org.mifosng.platform.currency.domain.MonetaryCurrency;
import org.mifosng.platform.currency.domain.Money; import org.mifosng.platform.currency.domain.Money;
import org.mifosng.platform.exceptions.InvalidLoanStateTransitionException;
import org.mifosng.platform.infrastructure.AbstractAuditableCustom; import org.mifosng.platform.infrastructure.AbstractAuditableCustom;
import org.mifosng.platform.loanproduct.service.LoanEnumerations; import org.mifosng.platform.loanproduct.service.LoanEnumerations;
import org.mifosng.platform.organisation.domain.Organisation; import org.mifosng.platform.organisation.domain.Organisation;
@@ -72,6 +73,7 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
@Column(name = "rejectedon_date") @Column(name = "rejectedon_date")
private Date rejectedOnDate; private Date rejectedOnDate;
@SuppressWarnings("unused")
@Temporal(TemporalType.TIMESTAMP) @Temporal(TemporalType.TIMESTAMP)
@Column(name = "withdrawnon_date") @Column(name = "withdrawnon_date")
private Date withdrawnOnDate; private Date withdrawnOnDate;
@@ -199,16 +201,14 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
this.interestChargedFromDate = interestChargedFromDate.toDateMidnight().toDate(); this.interestChargedFromDate = interestChargedFromDate.toDateMidnight().toDate();
} }
if (new LocalDate(this.submittedOnDate).isAfter(new LocalDate())) { if (submittedOn.isAfter(new LocalDate())) {
throw new InvalidLoanTimelineDate( final String errorMessage = "The date on which a loan is submitted cannot be in the future.";
"The loan application submit date cannot be in the future.", throw new InvalidLoanStateTransitionException("submittal", "cannot.be.a.future.date", errorMessage, submittedOn);
"invalid.submit.date.as.date.is.in.future");
} }
if (new LocalDate(this.submittedOnDate).isAfter(new LocalDate(
this.expectedDisbursedOnDate))) { if (submittedOn.isAfter(getExpectedDisbursedOnLocalDate())) {
throw new InvalidLoanTimelineDate( final String errorMessage = "The date on which a loan is submitted cannot be after its expected disbursement date: " + getExpectedDisbursedOnLocalDate().toString();
"The loan application submit date cannot be after its expected disbursement date.", throw new InvalidLoanStateTransitionException("submittal", "cannot.be.after.expected.disbursement.date", errorMessage, submittedOn, getExpectedDisbursedOnLocalDate());
"invalid.submit.date.as.date.is.after.expected.disbursement.date");
} }
} }
@@ -218,16 +218,14 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
this.rejectedOnDate = rejectedOn.toDateTimeAtCurrentTime().toDate(); this.rejectedOnDate = rejectedOn.toDateTimeAtCurrentTime().toDate();
this.closedOnDate = rejectedOn.toDateTimeAtCurrentTime().toDate(); this.closedOnDate = rejectedOn.toDateTimeAtCurrentTime().toDate();
if (new LocalDate(this.rejectedOnDate).isBefore(new LocalDate(
this.submittedOnDate))) { if (rejectedOn.isBefore(getSubmittedOnDate())) {
throw new InvalidLoanTimelineDate( final String errorMessage = "The date on which a loan is rejected cannot be before its submittal date: " + getSubmittedOnDate().toString();
"The loan rejection date cannot be before its submittal date.", throw new InvalidLoanStateTransitionException("reject", "cannot.be.before.submittal.date", errorMessage, rejectedOn, getSubmittedOnDate());
"invalid.rejection.date.as.date.is.before.submittal.date");
} }
if (new LocalDate(this.rejectedOnDate).isAfter(new LocalDate())) { if (rejectedOn.isAfter(new LocalDate())) {
throw new InvalidLoanTimelineDate( final String errorMessage = "The date on which a loan is rejected cannot be in the future.";
"The loan application rejection date cannot be in the future.", throw new InvalidLoanStateTransitionException("reject", "cannot.be.a.future.date", errorMessage, rejectedOn);
"invalid.rejection.date.as.date.is.in.future");
} }
} }
@@ -237,32 +235,29 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
this.withdrawnOnDate = withdrawnOn.toDateTimeAtCurrentTime().toDate(); this.withdrawnOnDate = withdrawnOn.toDateTimeAtCurrentTime().toDate();
this.closedOnDate = withdrawnOn.toDateTimeAtCurrentTime().toDate(); this.closedOnDate = withdrawnOn.toDateTimeAtCurrentTime().toDate();
if (new LocalDate(this.withdrawnOnDate).isBefore(new LocalDate(
this.submittedOnDate))) { if (withdrawnOn.isBefore(getSubmittedOnDate())) {
throw new InvalidLoanTimelineDate( final String errorMessage = "The date on which a loan is withdrawn cannot be before its submittal date: " + getSubmittedOnDate().toString();
"The date of when loan is withdrawn cannot be before its submittal date.", throw new InvalidLoanStateTransitionException("reject", "cannot.be.before.submittal.date", errorMessage, withdrawnOn, getSubmittedOnDate());
"invalid.withdrawal.date.as.date.is.before.submittal.date");
} }
if (new LocalDate(this.withdrawnOnDate).isAfter(new LocalDate())) { if (withdrawnOn.isAfter(new LocalDate())) {
throw new InvalidLoanTimelineDate( final String errorMessage = "The date on which a loan is withdrawn cannot be in the future.";
"The loan application withdraw date cannot be in the future.", throw new InvalidLoanStateTransitionException("reject", "cannot.be.a.future.date", errorMessage, withdrawnOn);
"invalid.withdrawal.date.as.date.is.in.future");
} }
} }
public void approve(final LocalDate approvedOn, LoanLifecycleStateMachine loanLifecycleStateMachine) { public void approve(final LocalDate approvedOn, LoanLifecycleStateMachine loanLifecycleStateMachine) {
this.loanStatus = loanLifecycleStateMachine.transition(LoanEvent.LOAN_APPROVED, this.loanStatus); this.loanStatus = loanLifecycleStateMachine.transition(LoanEvent.LOAN_APPROVED, this.loanStatus);
this.approvedOnDate = approvedOn.toDateTimeAtCurrentTime().toDate(); this.approvedOnDate = approvedOn.toDateTimeAtCurrentTime().toDate();
if (new LocalDate(this.approvedOnDate).isBefore(new LocalDate(
this.submittedOnDate))) { LocalDate submittalDate = new LocalDate(this.submittedOnDate);
throw new InvalidLoanTimelineDate( if (approvedOn.isBefore(submittalDate)) {
"The date of when loan is approved cannot be before its submittal date.", final String errorMessage = "The date on which a loan is approved cannot be before its submittal date: " + submittalDate.toString();
"invalid.approval.date.as.date.is.before.submittal.date"); throw new InvalidLoanStateTransitionException("approval", "cannot.be.before.submittal.date", errorMessage, getApprovedOnDate(), submittalDate);
} }
if (new LocalDate(this.approvedOnDate).isAfter(new LocalDate())) { if (approvedOn.isAfter(new LocalDate())) {
throw new InvalidLoanTimelineDate( final String errorMessage = "The date on which a loan is approved cannot be in the future.";
"The date of when loan is approved cannot be in the future.", throw new InvalidLoanStateTransitionException("approval", "cannot.be.a.future.date", errorMessage, getApprovedOnDate());
"invalid.approval.date.as.date.is.in.future");
} }
} }
@@ -273,7 +268,6 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
public void disburseWithModifiedRepaymentSchedule( public void disburseWithModifiedRepaymentSchedule(
final LocalDate disbursedOn, final LocalDate disbursedOn,
final String comment,
final List<LoanRepaymentScheduleInstallment> modifiedLoanRepaymentSchedule, LoanLifecycleStateMachine loanLifecycleStateMachine) { final List<LoanRepaymentScheduleInstallment> modifiedLoanRepaymentSchedule, LoanLifecycleStateMachine loanLifecycleStateMachine) {
this.repaymentScheduleInstallments.clear(); this.repaymentScheduleInstallments.clear();
for (LoanRepaymentScheduleInstallment modifiedInstallment : modifiedLoanRepaymentSchedule) { for (LoanRepaymentScheduleInstallment modifiedInstallment : modifiedLoanRepaymentSchedule) {
@@ -296,23 +290,21 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
loanTransaction.updateLoan(this); loanTransaction.updateLoan(this);
loanTransaction.updateOrganisation(organisation); loanTransaction.updateOrganisation(organisation);
this.loanTransactions.add(loanTransaction); this.loanTransactions.add(loanTransaction);
if (disbursedOn.isBefore(new LocalDate(this.approvedOnDate))) { if (disbursedOn.isBefore(getApprovedOnDate())) {
throw new InvalidLoanTimelineDate( final String errorMessage = "The date on which a loan is disbursed cannot be before its approval date: " + getApprovedOnDate().toString();
"The date of when loan is disbursed cannot be before its approval date.", throw new InvalidLoanStateTransitionException("disbursal", "cannot.be.before.approval.date", errorMessage, disbursedOn, getApprovedOnDate());
"invalid.disbursal.as.disbursement.date.is.before.approved.date");
} }
if (disbursedOn.isAfter(new LocalDate())) { if (disbursedOn.isAfter(new LocalDate())) {
throw new InvalidLoanTimelineDate( final String errorMessage = "The date on which a loan is disbursed cannot be in the future.";
"The date of when loan is disbursed cannot be in the future.", throw new InvalidLoanStateTransitionException("disbursal", "cannot.be.a.future.date", errorMessage, disbursedOn);
"invalid.disbursal.as.disbursement.date.is.in.future");
} }
LocalDate firstRepaymentDueDate = this.repaymentScheduleInstallments
.get(0).getDueDate(); LocalDate firstRepaymentDueDate = this.repaymentScheduleInstallments.get(0).getDueDate();
if (disbursedOn.isAfter(firstRepaymentDueDate)) { if (disbursedOn.isAfter(firstRepaymentDueDate)) {
throw new InvalidLoanTimelineDate( final String errorMessage = "The date on which a loan is disbursed cannot be after the first expected repayment date: " + firstRepaymentDueDate.toString();
"The date of when loan is disbursed cannot be after the first expected repayment.", throw new InvalidLoanStateTransitionException("disbursal", "cannot.be.after.first.repayment.due.date", errorMessage, disbursedOn, firstRepaymentDueDate);
"invalid.disbursal.as.disbursement.date.is.after.first.repayment.due.date");
} }
} }
@@ -328,18 +320,17 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
loanTransaction.updateOrganisation(organisation); loanTransaction.updateOrganisation(organisation);
this.loanTransactions.add(loanTransaction); this.loanTransactions.add(loanTransaction);
if (loanTransaction.getTransactionDate().isBefore( LocalDate loanTransactionDate = loanTransaction.getTransactionDate();
this.getDisbursementDate())) { if (loanTransactionDate.isBefore(this.getDisbursementDate())) {
throw new InvalidLoanTimelineDate( final String errorMessage = "The transaction date cannot be before the loan disbursement date: " + getApprovedOnDate().toString();
"The repayment date cannot be before the loan disbursement date.", throw new InvalidLoanStateTransitionException("waive", "cannot.be.before.disbursement.date", errorMessage, loanTransactionDate, this.getDisbursementDate());
"invalid.repayment.date.as.date.is.before.disbursement.date");
} }
if (loanTransaction.getTransactionDate().isAfter(new LocalDate())) {
throw new InvalidLoanTimelineDate( if (loanTransactionDate.isAfter(new LocalDate())) {
"The repayment date cannot be in the future.", final String errorMessage = "The transaction date cannot be in the future.";
"invalid.repayment.date.as.date.is.in.future"); throw new InvalidLoanStateTransitionException("waive", "cannot.be.a.futre.date", errorMessage, loanTransactionDate);
} }
if (this.isRepaidInFull()) { if (this.isRepaidInFull()) {
this.loanStatus = loanLifecycleStateMachine.transition(LoanEvent.REPAID_IN_FULL, this.loanStatus); this.loanStatus = loanLifecycleStateMachine.transition(LoanEvent.REPAID_IN_FULL, this.loanStatus);
this.closedOnDate = loanTransaction.getTransactionDate().toDate(); this.closedOnDate = loanTransaction.getTransactionDate().toDate();
@@ -368,18 +359,18 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
throw new IllegalArgumentException( throw new IllegalArgumentException(
"Only repayment transactions can be passed to makeRepayment."); "Only repayment transactions can be passed to makeRepayment.");
} }
LocalDate loanTransactionDate = loanTransaction.getTransactionDate();
if (loanTransactionDate.isBefore(this.getDisbursementDate())) {
final String errorMessage = "The transaction date cannot be before the loan disbursement date: " + getApprovedOnDate().toString();
throw new InvalidLoanStateTransitionException("repayment", "cannot.be.before.disbursement.date", errorMessage, loanTransactionDate, this.getDisbursementDate());
}
if (loanTransactionDate.isAfter(new LocalDate())) {
final String errorMessage = "The transaction date cannot be in the future.";
throw new InvalidLoanStateTransitionException("repayment", "cannot.be.a.futre.date", errorMessage, loanTransactionDate);
}
if (loanTransaction.getTransactionDate().isBefore(
this.getDisbursementDate())) {
throw new InvalidLoanTimelineDate(
"The repayment date cannot be before the loan disbursement date.",
"invalid.repayment.date.as.date.is.before.disbursement.date");
}
if (loanTransaction.getTransactionDate().isAfter(new LocalDate())) {
throw new InvalidLoanTimelineDate(
"The repayment date cannot be in the future.",
"invalid.repayment.date.as.date.is.in.future");
}
if (this.isRepaidInFull()) { if (this.isRepaidInFull()) {
this.loanStatus = loanLifecycleStateMachine.transition(LoanEvent.REPAID_IN_FULL, this.loanStatus); this.loanStatus = loanLifecycleStateMachine.transition(LoanEvent.REPAID_IN_FULL, this.loanStatus);
this.closedOnDate = loanTransaction.getTransactionDate().toDate(); this.closedOnDate = loanTransaction.getTransactionDate().toDate();
@@ -529,16 +520,15 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
this.loanStatus = loanLifecycleStateMachine.transition(LoanEvent.LOAN_WRITE_OFF, this.loanStatus); this.loanStatus = loanLifecycleStateMachine.transition(LoanEvent.LOAN_WRITE_OFF, this.loanStatus);
this.closedOnDate = writtenOffOn.toDate(); this.closedOnDate = writtenOffOn.toDate();
this.writtenOffOnDate = writtenOffOn.toDate(); this.writtenOffOnDate = writtenOffOn.toDate();
if (new LocalDate(this.writtenOffOnDate).isBefore(this
.getDisbursementDate())) { LocalDate writtenOffOnLocalDate = new LocalDate(writtenOffOnDate);
throw new InvalidLoanTimelineDate( if (writtenOffOnLocalDate.isBefore(this.getDisbursementDate())) {
"The date the loan is written off cannot be before the loan disbursement date.", final String errorMessage = "The date on which a loan is withdrawn cannot be before the loan disbursement date: " + getDisbursementDate().toString();
"invalid.writeoff.date.as.date.is.before.disbursement.date"); throw new InvalidLoanStateTransitionException("writeoff", "cannot.be.before.submittal.date", errorMessage, writtenOffOnLocalDate, getDisbursementDate());
} }
if (new LocalDate(this.writtenOffOnDate).isAfter(new LocalDate())) { if (writtenOffOnLocalDate.isAfter(new LocalDate())) {
throw new InvalidLoanTimelineDate( final String errorMessage = "The date on which a loan is written off cannot be in the future.";
"The date the loan is written off cannot be in the future.", throw new InvalidLoanStateTransitionException("writeoff", "cannot.be.a.future.date", errorMessage, writtenOffOnLocalDate);
"invalid.writeoff.date.as.date.is.in.future");
} }
} }
@@ -546,16 +536,15 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
this.loanStatus = loanLifecycleStateMachine.transition(LoanEvent.LOAN_RESCHEDULE, this.loanStatus); this.loanStatus = loanLifecycleStateMachine.transition(LoanEvent.LOAN_RESCHEDULE, this.loanStatus);
this.closedOnDate = rescheduledOn.toDate(); this.closedOnDate = rescheduledOn.toDate();
this.rescheduledOnDate = rescheduledOn.toDate(); this.rescheduledOnDate = rescheduledOn.toDate();
if (new LocalDate(this.rescheduledOnDate).isBefore(this
.getDisbursementDate())) { LocalDate rescheduledOnLocalDate = new LocalDate(rescheduledOnDate);
throw new InvalidLoanTimelineDate( if (rescheduledOnLocalDate.isBefore(this.getDisbursementDate())) {
"The date the loan is rescheduled cannot be before the loan disbursement date.", final String errorMessage = "The date on which a loan is rescheduled cannot be before the loan disbursement date: " + getDisbursementDate().toString();
"invalid.reschedule.date.as.date.is.before.disbursement.date"); throw new InvalidLoanStateTransitionException("writeoff", "cannot.be.before.submittal.date", errorMessage, rescheduledOnLocalDate, getDisbursementDate());
} }
if (new LocalDate(this.rescheduledOnDate).isAfter(new LocalDate())) { if (rescheduledOnLocalDate.isAfter(new LocalDate())) {
throw new InvalidLoanTimelineDate( final String errorMessage = "The date on which a loan is rescheduled cannot be in the future.";
"The date the loan is rescheduled cannot be in the future.", throw new InvalidLoanStateTransitionException("writeoff", "cannot.be.a.future.date", errorMessage, rescheduledOnLocalDate);
"invalid.reschedule.date.as.date.is.in.future");
} }
} }
@@ -24,7 +24,7 @@ public class LoanTransactionCommandValidator {
baseDataValidator.reset().parameter("loanid").value(command.getLoanId()).notNull().greaterThanZero(); baseDataValidator.reset().parameter("loanid").value(command.getLoanId()).notNull().greaterThanZero();
baseDataValidator.reset().parameter("transactionDate").value(command.getTransactionLocalDate()).notNull(); baseDataValidator.reset().parameter("transactionDate").value(command.getTransactionLocalDate()).notNull();
baseDataValidator.reset().parameter("transactionAmount").value(command.getTransactionAmount()).notNull().positiveAmount(); baseDataValidator.reset().parameter("transactionAmount").value(command.getTransactionAmountValue()).notNull().positiveAmount();
baseDataValidator.reset().parameter("note").value(command.getNote()).notExceedingLengthOf(1000); baseDataValidator.reset().parameter("note").value(command.getNote()).notExceedingLengthOf(1000);
if (!dataValidationErrors.isEmpty()) { if (!dataValidationErrors.isEmpty()) {
@@ -312,7 +312,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
interest.getAmount()); interest.getAmount());
modifiedLoanRepaymentSchedule.add(installment); modifiedLoanRepaymentSchedule.add(installment);
} }
loan.disburseWithModifiedRepaymentSchedule(actualDisbursementDate, noteText, modifiedLoanRepaymentSchedule, defaultLoanLifecycleStateMachine()); loan.disburseWithModifiedRepaymentSchedule(actualDisbursementDate, modifiedLoanRepaymentSchedule, defaultLoanLifecycleStateMachine());
} else { } else {
loan.disburse(actualDisbursementDate, defaultLoanLifecycleStateMachine()); loan.disburse(actualDisbursementDate, defaultLoanLifecycleStateMachine());
} }