support for MIFOSX-73: Loan Officer reassignment History.

This commit is contained in:
Michal Dudzinski
2012-11-12 16:53:46 +01:00
parent 2c92635ced
commit 6e12e9e62a
11 changed files with 184 additions and 17 deletions
@@ -0,0 +1,14 @@
CREATE TABLE `m_loan_officer_assignment_history` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`loan_id` bigint(20) NOT NULL ,
`loan_officer_id` bigint(20) DEFAULT NULL,
`start_date` date NOT NULL,
`end_date` date DEFAULT NULL,
`createdby_id` bigint(20) DEFAULT NULL,
`created_date` datetime DEFAULT NULL,
`lastmodified_date` datetime DEFAULT NULL,
`lastmodifiedby_id` bigint(20) DEFAULT NULL,
PRIMARY KEY (`id`),
CONSTRAINT `fk_m_loan_officer_assignment_history_0001` FOREIGN KEY (`loan_id`) REFERENCES `m_loan` (`id`),
CONSTRAINT `fk_m_loan_officer_assignment_history_0002` FOREIGN KEY (`loan_officer_id`) REFERENCES `m_staff` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
@@ -18,6 +18,7 @@ import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo; import javax.ws.rs.core.UriInfo;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;
import org.joda.time.LocalDate;
import org.mifosng.platform.api.commands.BulkLoanReassignmentCommand; import org.mifosng.platform.api.commands.BulkLoanReassignmentCommand;
import org.mifosng.platform.api.commands.StaffCommand; import org.mifosng.platform.api.commands.StaffCommand;
import org.mifosng.platform.api.data.EntityIdentifier; import org.mifosng.platform.api.data.EntityIdentifier;
@@ -179,8 +180,8 @@ public class StaffApiResource {
staffAccountSummaryCollectionData = this.readPlatformService.retrieveLoanOfficerAccountSummary(loanOfficerId); staffAccountSummaryCollectionData = this.readPlatformService.retrieveLoanOfficerAccountSummary(loanOfficerId);
} }
final LoanReassignmentData loanReassignmentData = new LoanReassignmentData(officeId, loanOfficerId, offices, final LoanReassignmentData loanReassignmentData = new LoanReassignmentData(officeId, loanOfficerId,
loanOfficers, staffAccountSummaryCollectionData); new LocalDate(), offices, loanOfficers, staffAccountSummaryCollectionData);
return this.apiJsonSerializerService.serializeLoanReassignmentDataToJson(prettyPrint, responseParameters, loanReassignmentData); return this.apiJsonSerializerService.serializeLoanReassignmentDataToJson(prettyPrint, responseParameters, loanReassignmentData);
} }
@@ -1,5 +1,7 @@
package org.mifosng.platform.api.commands; package org.mifosng.platform.api.commands;
import org.joda.time.LocalDate;
/** /**
* Immutable data object for reassigning loan officers on loans in bulk. * Immutable data object for reassigning loan officers on loans in bulk.
*/ */
@@ -7,13 +9,15 @@ public class BulkLoanReassignmentCommand {
private final Long fromLoanOfficerId; private final Long fromLoanOfficerId;
private final Long toLoanOfficerId; private final Long toLoanOfficerId;
private final LocalDate assignmentDate;
private final String[] loans; private final String[] loans;
public BulkLoanReassignmentCommand(Long fromLoanOfficerId, public BulkLoanReassignmentCommand(Long fromLoanOfficerId,
Long toLoanOfficerId, String[] loans) { Long toLoanOfficerId, LocalDate assignmentDate, String[] loans) {
this.fromLoanOfficerId = fromLoanOfficerId; this.fromLoanOfficerId = fromLoanOfficerId;
this.toLoanOfficerId = toLoanOfficerId; this.toLoanOfficerId = toLoanOfficerId;
this.assignmentDate = assignmentDate;
this.loans = loans; this.loans = loans;
} }
@@ -25,7 +29,11 @@ public class BulkLoanReassignmentCommand {
return toLoanOfficerId; return toLoanOfficerId;
} }
public String[] getLoans() { public LocalDate getAssignmentDate() {
return assignmentDate;
}
public String[] getLoans() {
return loans; return loans;
} }
} }
@@ -1,5 +1,7 @@
package org.mifosng.platform.api.data; package org.mifosng.platform.api.data;
import org.joda.time.LocalDate;
import java.util.Collection; import java.util.Collection;
/** /**
@@ -11,6 +13,8 @@ public class LoanReassignmentData {
private final Long officeId; private final Long officeId;
@SuppressWarnings("unused") @SuppressWarnings("unused")
private final Long fromLoanOfficerId; private final Long fromLoanOfficerId;
@SuppressWarnings("unused")
private final LocalDate assignmentDate;
//template //template
@SuppressWarnings("unused") @SuppressWarnings("unused")
@@ -23,11 +27,13 @@ public class LoanReassignmentData {
public LoanReassignmentData( public LoanReassignmentData(
final Long officeId, final Long officeId,
final Long fromLoanOfficerId, final Long fromLoanOfficerId,
final LocalDate assignmentDate,
final Collection<OfficeLookup> officeOptions, final Collection<OfficeLookup> officeOptions,
final Collection<StaffData> loanOfficerOptions, final Collection<StaffData> loanOfficerOptions,
final StaffAccountSummaryCollectionData accountSummaryCollection) { final StaffAccountSummaryCollectionData accountSummaryCollection) {
this.officeId = officeId; this.officeId = officeId;
this.fromLoanOfficerId = fromLoanOfficerId; this.fromLoanOfficerId = fromLoanOfficerId;
this.assignmentDate = assignmentDate;
this.officeOptions = officeOptions; this.officeOptions = officeOptions;
this.loanOfficerOptions = loanOfficerOptions; this.loanOfficerOptions = loanOfficerOptions;
this.accountSummaryCollection = accountSummaryCollection; this.accountSummaryCollection = accountSummaryCollection;
@@ -194,7 +194,8 @@ public class GoogleGsonPortfolioApiJsonSerializerService implements PortfolioApi
Arrays.asList("id", "type", "date", "currency", "amount")); Arrays.asList("id", "type", "date", "currency", "amount"));
private static final Set<String> LOAN_REASSIGNMENT_DATA_PARAMETERS = new HashSet<String>( private static final Set<String> LOAN_REASSIGNMENT_DATA_PARAMETERS = new HashSet<String>(
Arrays.asList("officeId", "fromLoanOfficerId", "officeOptions", "loanOfficerOptions", "accountSummaryCollection") Arrays.asList("officeId", "fromLoanOfficerId", "assignmentDate",
"officeOptions", "loanOfficerOptions", "accountSummaryCollection")
); );
private static final Set<String> CHARGES_DATA_PARAMETERS = new HashSet<String>( private static final Set<String> CHARGES_DATA_PARAMETERS = new HashSet<String>(
@@ -167,7 +167,7 @@ public class PortfolioApiDataConversionServiceImpl implements PortfolioApiDataCo
Map<String, String> requestMap = gsonConverter.fromJson(json, typeOfMap); Map<String, String> requestMap = gsonConverter.fromJson(json, typeOfMap);
Set<String> supportedParams = new HashSet<String>( Set<String> supportedParams = new HashSet<String>(
Arrays.asList("fromLoanOfficerId", "toLoanOfficerId","loans") Arrays.asList("fromLoanOfficerId", "toLoanOfficerId", "assignmentDate", "locale", "dateFormat", "loans")
); );
checkForUnsupportedParameters(requestMap, supportedParams); checkForUnsupportedParameters(requestMap, supportedParams);
@@ -176,6 +176,7 @@ public class PortfolioApiDataConversionServiceImpl implements PortfolioApiDataCo
Long fromLoanOfficerId = extractLongParameter("fromLoanOfficerId", requestMap, modifiedParameters); Long fromLoanOfficerId = extractLongParameter("fromLoanOfficerId", requestMap, modifiedParameters);
Long toLoanOfficerId = extractLongParameter("toLoanOfficerId", requestMap, modifiedParameters); Long toLoanOfficerId = extractLongParameter("toLoanOfficerId", requestMap, modifiedParameters);
LocalDate assignmentDate = extractLocalDateParameter("assignmentDate", requestMap, modifiedParameters);
// check array // check array
JsonParser parser = new JsonParser(); JsonParser parser = new JsonParser();
@@ -195,7 +196,7 @@ public class PortfolioApiDataConversionServiceImpl implements PortfolioApiDataCo
} }
// //
return new BulkLoanReassignmentCommand(fromLoanOfficerId, toLoanOfficerId, loans); return new BulkLoanReassignmentCommand(fromLoanOfficerId, toLoanOfficerId, assignmentDate, loans);
} }
@Override @Override
@@ -1,11 +1,18 @@
package org.mifosng.platform.exceptions; package org.mifosng.platform.exceptions;
import org.joda.time.LocalDate;
public class LoanOfficerAssignmentException extends public class LoanOfficerAssignmentException extends
AbstractPlatformDomainRuleException { AbstractPlatformDomainRuleException {
public LoanOfficerAssignmentException(final Long loanId, final Long loanOfficerId) { public LoanOfficerAssignmentException(final Long loanId, final Long fromLoanOfficerId) {
super("error.msg.loan.not.assigned.to.loan.officer", "Loan with identifier " + loanId + super("error.msg.loan.not.assigned.to.loan.officer", "Loan with identifier " + loanId +
" is not assigned to Loan Officer with identifier " + loanOfficerId + ".", loanId); " is not assigned to Loan Officer with identifier " + fromLoanOfficerId + ".", loanId);
}
public LoanOfficerAssignmentException(final Long loanId, final LocalDate date){
super("error.msg.loan.assignment.date.is.before.last.assignment.date", "Loan with identifier " + loanId +
" was already assigned before date " + date.toString());
} }
} }
@@ -36,6 +36,7 @@ import org.mifosng.platform.currency.domain.Money;
import org.mifosng.platform.exceptions.InvalidLoanStateTransitionException; import org.mifosng.platform.exceptions.InvalidLoanStateTransitionException;
import org.mifosng.platform.exceptions.InvalidLoanTransactionTypeException; import org.mifosng.platform.exceptions.InvalidLoanTransactionTypeException;
import org.mifosng.platform.exceptions.LoanChargeCannotBeAddedException; import org.mifosng.platform.exceptions.LoanChargeCannotBeAddedException;
import org.mifosng.platform.exceptions.LoanOfficerAssignmentException;
import org.mifosng.platform.fund.domain.Fund; import org.mifosng.platform.fund.domain.Fund;
import org.mifosng.platform.group.domain.Group; import org.mifosng.platform.group.domain.Group;
import org.mifosng.platform.infrastructure.AbstractAuditableCustom; import org.mifosng.platform.infrastructure.AbstractAuditableCustom;
@@ -146,6 +147,10 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
@OneToMany(cascade = CascadeType.ALL, mappedBy = "loan", orphanRemoval = true) @OneToMany(cascade = CascadeType.ALL, mappedBy = "loan", orphanRemoval = true)
private Set<LoanCharge> charges; private Set<LoanCharge> charges;
@LazyCollection(LazyCollectionOption.FALSE)
@OneToMany(cascade = CascadeType.ALL, mappedBy = "loan", orphanRemoval = true)
private Set<LoanOfficerAssignmentHistory> loanOfficerAssignmentHistorySet;
// see // see
// http://stackoverflow.com/questions/4334970/hibernate-cannot-simultaneously-fetch-multiple-bags // http://stackoverflow.com/questions/4334970/hibernate-cannot-simultaneously-fetch-multiple-bags
@LazyCollection(LazyCollectionOption.FALSE) @LazyCollection(LazyCollectionOption.FALSE)
@@ -183,6 +188,7 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
this.loanProduct = null; this.loanProduct = null;
this.loanRepaymentScheduleDetail = null; this.loanRepaymentScheduleDetail = null;
this.charges = null; this.charges = null;
this.loanOfficerAssignmentHistorySet = null;
} }
private Loan( private Loan(
@@ -208,6 +214,7 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
} else { } else {
this.charges = null; this.charges = null;
} }
this.loanOfficerAssignmentHistorySet = null;
} }
private Loan( private Loan(
@@ -233,6 +240,7 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
} else { } else {
this.charges = null; this.charges = null;
} }
this.loanOfficerAssignmentHistorySet = null;
} }
private BigDecimal deriveSumTotalOfChargesDueAtDisbursement() { private BigDecimal deriveSumTotalOfChargesDueAtDisbursement() {
@@ -434,7 +442,7 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
return this.loanProduct; return this.loanProduct;
} }
public LoanProductRelatedDetail repaymentScheduleDetail() { public LoanProductRelatedDetail repaymentScheduleDetail() {
return this.loanRepaymentScheduleDetail; return this.loanRepaymentScheduleDetail;
} }
@@ -683,6 +691,13 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
"cannot.be.a.future.date", errorMessage, "cannot.be.a.future.date", errorMessage,
getApprovedOnDate()); getApprovedOnDate());
} }
if (this.loanofficer != null){
LoanOfficerAssignmentHistory loanOfficerAssignmentHistory =
LoanOfficerAssignmentHistory.createNew(this, this.loanofficer, approvedOn);
this.loanOfficerAssignmentHistorySet.add(loanOfficerAssignmentHistory);
}
} }
public void undoApproval(LoanLifecycleStateMachine loanLifecycleStateMachine) { public void undoApproval(LoanLifecycleStateMachine loanLifecycleStateMachine) {
@@ -691,6 +706,8 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
this.loanStatus = statusEnum.getValue(); this.loanStatus = statusEnum.getValue();
this.approvedOnDate = null; this.approvedOnDate = null;
this.loanOfficerAssignmentHistorySet.clear();
} }
public void disburseWithModifiedRepaymentSchedule( public void disburseWithModifiedRepaymentSchedule(
@@ -1584,7 +1601,31 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
return this.loanRepaymentScheduleDetail.getCurrency(); return this.loanRepaymentScheduleDetail.getCurrency();
} }
public void updateLoanofficer(final Staff newLoanOfficer) { public void updateLoanOfficer(final Staff newLoanOfficer, final LocalDate assignmentDate) {
this.loanofficer = newLoanOfficer; boolean createNewLoanOfficerAssignmentHistoryRecord = true;
for (LoanOfficerAssignmentHistory lastLoanOfficerAssignmentHistoryRecord : this.loanOfficerAssignmentHistorySet){
if (lastLoanOfficerAssignmentHistoryRecord.getEndDate() == null){
if (this.loanofficer.identifiedBy(newLoanOfficer)){
lastLoanOfficerAssignmentHistoryRecord.updateStartDate(assignmentDate);
createNewLoanOfficerAssignmentHistoryRecord = false;
} else if (lastLoanOfficerAssignmentHistoryRecord.getStartDate().isEqual(assignmentDate)){
lastLoanOfficerAssignmentHistoryRecord.updateLoanOfficer(newLoanOfficer);
createNewLoanOfficerAssignmentHistoryRecord = false;
} else if (assignmentDate.isBefore(lastLoanOfficerAssignmentHistoryRecord.getStartDate())){
throw new LoanOfficerAssignmentException(this.getId(), assignmentDate);
} else {
lastLoanOfficerAssignmentHistoryRecord.updateEndDate(assignmentDate);
}
break;
}
}
if (createNewLoanOfficerAssignmentHistoryRecord){
LoanOfficerAssignmentHistory loanOfficerAssignmentHistory =
LoanOfficerAssignmentHistory.createNew(this, this.loanofficer, assignmentDate);
this.loanOfficerAssignmentHistorySet.add(loanOfficerAssignmentHistory);
}
this.loanofficer = newLoanOfficer;
} }
} }
@@ -0,0 +1,86 @@
package org.mifosng.platform.loan.domain;
import org.joda.time.LocalDate;
import org.mifosng.platform.infrastructure.AbstractAuditableCustom;
import org.mifosng.platform.staff.domain.Staff;
import org.mifosng.platform.user.domain.AppUser;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import java.util.Date;
@Entity
@Table(name = "m_loan_officer_assignment_history")
public class LoanOfficerAssignmentHistory extends AbstractAuditableCustom<AppUser, Long> {
@SuppressWarnings("unused")
@ManyToOne
@JoinColumn(name = "loan_id", nullable = false)
private Loan loan;
@SuppressWarnings("unused")
@ManyToOne
@JoinColumn(name = "loan_officer_id", nullable = true)
private Staff loanOfficer;
@Temporal(TemporalType.DATE)
@Column(name = "start_date")
private Date startDate;
@SuppressWarnings("unused")
@Temporal(TemporalType.DATE)
@Column(name = "end_date")
private Date endDate;
public static LoanOfficerAssignmentHistory createNew(Loan loan, Staff loanOfficer,
LocalDate startDate){
return new LoanOfficerAssignmentHistory(loan, loanOfficer, startDate.toDate(), null);
}
@SuppressWarnings("unused")
protected LoanOfficerAssignmentHistory() {
this.loan = null;
this.loanOfficer = null;
this.startDate = null;
this.endDate = null;
}
private LoanOfficerAssignmentHistory(Loan loan, Staff loanOfficer,
Date startDate, Date endDate) {
this.loan = loan;
this.loanOfficer = loanOfficer;
this.startDate = startDate;
this.endDate = endDate;
}
public Loan getLoan() {
return loan;
}
public LocalDate getStartDate() {
return new LocalDate(startDate);
}
public Date getEndDate() {
return endDate;
}
public void updateLoanOfficer(Staff loanOfficer) {
this.loanOfficer = loanOfficer;
}
public void updateStartDate(LocalDate startDate){
this.startDate = startDate.toDate();
}
public void updateEndDate(LocalDate endDate){
this.endDate = endDate.toDate();
}
}
@@ -754,7 +754,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
throw new LoanOfficerAssignmentException(loan.getId(), fromLoanOfficer.getId()); throw new LoanOfficerAssignmentException(loan.getId(), fromLoanOfficer.getId());
} }
loan.updateLoanofficer(toLoanOfficer); loan.updateLoanOfficer(toLoanOfficer, command.getAssignmentDate());
this.loanRepository.save(loan); this.loanRepository.save(loan);
} }
@@ -26,6 +26,8 @@ public class BulkLoanReassignmentCommandValidator {
baseDataValidator.reset().parameter("toLoanOfficerId").value(command.getToLoanOfficerId()).notNull(). baseDataValidator.reset().parameter("toLoanOfficerId").value(command.getToLoanOfficerId()).notNull().
integerGreaterThanZero().notSameAsParameter("fromLoanOfficerId", command.getFromLoanOfficerId()); integerGreaterThanZero().notSameAsParameter("fromLoanOfficerId", command.getFromLoanOfficerId());
baseDataValidator.reset().parameter("assignmentDate").value(command.getAssignmentDate()).notNull();
baseDataValidator.reset().parameter("loans").value(command.getLoans()).arrayNotEmpty(); baseDataValidator.reset().parameter("loans").value(command.getLoans()).arrayNotEmpty();
if (!dataValidationErrors.isEmpty()) { if (!dataValidationErrors.isEmpty()) {