initial work on transfers

This commit is contained in:
Vishwas Babu A J
2013-08-20 17:16:59 +05:30
parent fd2cdefb95
commit 40bbf46e05
22 changed files with 705 additions and 28 deletions
@@ -0,0 +1,11 @@
INSERT INTO `m_permission` (`grouping`, `code`, `entity_name`, `action_name`, `can_maker_checker`)
VALUES ('portfolio_group', 'TRANSFERCLIENTS_GROUP', 'GROUP', 'TRANSFERCLIENTS', 0);
INSERT INTO `m_permission` (`grouping`, `code`, `entity_name`, `action_name`, `can_maker_checker`)
VALUES ('portfolio_group', 'TRANSFERCLIENTS_GROUP_CHECKER', 'GROUP', 'TRANSFERCLIENTS', 0);
INSERT INTO `m_permission` (`grouping`, `code`, `entity_name`, `action_name`, `can_maker_checker`)
VALUES ('portfolio', 'TRANSFER_CLIENT', 'CLIENT', 'TRANSFER', 0);
INSERT INTO `m_permission` (`grouping`, `code`, `entity_name`, `action_name`, `can_maker_checker`)
VALUES ('portfolio', 'TRANSFER_CLIENT_CHECKER', 'CLIENT', 'TRANSFER', 0);
@@ -551,6 +551,10 @@ public class CommandWrapper {
public boolean isUpdateGroupRole() {
return this.entityName.equalsIgnoreCase("GROUP") && this.actionName.equalsIgnoreCase("UPDATEROLE");
}
public boolean isTransferClientsBetweenGroups() {
return this.entityName.equalsIgnoreCase("GROUP") && this.actionName.equalsIgnoreCase("TRANSFERCLIENTS");
}
public boolean isClientAssignStaff() {
return this.actionName.equalsIgnoreCase("ASSIGNSTAFF") && this.entityName.equalsIgnoreCase("CLIENT");
@@ -559,6 +563,10 @@ public class CommandWrapper {
public boolean isClientClose() {
return this.actionName.equalsIgnoreCase("CLOSE") && this.entityName.equalsIgnoreCase("CLIENT");
}
public boolean isClientTransfer() {
return this.actionName.equalsIgnoreCase("TRANSFER") && this.entityName.equalsIgnoreCase("CLIENT");
}
public boolean isProductMixResource() {
return this.entityName.equalsIgnoreCase("PRODUCTMIX");
@@ -351,6 +351,15 @@ public class CommandWrapperBuilder {
this.href = "/clients/" + clientId + "?command=close&template=true";
return this;
}
public CommandWrapperBuilder transferClient(final Long clientId) {
this.actionName = "TRANSFER";
this.entityName = "CLIENT";
this.entityId = clientId;
this.clientId = clientId;
this.href = "/clientId/" + clientId + "?command=transfer";
return this;
}
public CommandWrapperBuilder updateClient(final Long clientId) {
this.actionName = "UPDATE";
@@ -1000,6 +1009,15 @@ public class CommandWrapperBuilder {
this.href = "/groups/" + groupId + "?command=disassociateClients";
return this;
}
public CommandWrapperBuilder transferClientsBetweenGroups(final Long sourceGroupId) {
this.actionName = "TRANSFERCLIENTS";
this.entityName = "GROUP";
this.entityId = sourceGroupId;
this.groupId = sourceGroupId;
this.href = "/groups/" + sourceGroupId + "?command=transferClients";
return this;
}
public CommandWrapperBuilder unassignGroupStaff(final Long groupId) {
this.actionName = "UNASSIGNSTAFF";
@@ -170,6 +170,8 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
handler = this.applicationContext.getBean("assignClientStaffCommandHandler", NewCommandSourceHandler.class);
} else if (wrapper.isClientClose()) {
handler = this.applicationContext.getBean("closeClientCommandHandler", NewCommandSourceHandler.class);
} else if (wrapper.isClientTransfer()) {
handler = this.applicationContext.getBean("transferClientBetweenBranchesCommandHandler", NewCommandSourceHandler.class);
} else {
throw new UnsupportedCommandException(wrapper.commandName());
}
@@ -452,7 +454,9 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
handler = this.applicationContext.getBean("updateGroupRoleCommandHandler", NewCommandSourceHandler.class);
} else if (wrapper.isAssignStaff()) {
handler = this.applicationContext.getBean("assignGroupStaffCommandHandler", NewCommandSourceHandler.class);
}else {
} else if (wrapper.isTransferClientsBetweenGroups()) {
handler = this.applicationContext.getBean("transferClientsBetweenGroupsCommandHandler", NewCommandSourceHandler.class);
} else {
throw new UnsupportedCommandException(wrapper.commandName());
}
} else if (wrapper.isCenterResource()) {
@@ -128,6 +128,7 @@ public class DataValidatorBuilder {
return this;
}
/***FIXME: Vishwas, why does this method have a parameter? Seems wrong***/
public DataValidatorBuilder trueOrFalseRequired(final boolean trueOfFalseFieldProvided) {
if (!trueOfFalseFieldProvided && !ignoreNullValue) {
StringBuilder validationErrorCode = new StringBuilder("validation.msg.").append(resource).append(".").append(parameter)
@@ -0,0 +1,45 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.mifosplatform.organisation.office.domain;
import org.mifosplatform.organisation.office.exception.OfficeNotFoundException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* <p>
* Wrapper for {@link OfficeRepository} that adds NULL checking and Error
* handling capabilities
* </p>
*/
@Service
public class OfficeRepositoryWrapper {
private final OfficeRepository repository;
@Autowired
public OfficeRepositoryWrapper(final OfficeRepository repository) {
this.repository = repository;
}
public Office findOneWithNotFoundDetection(final Long id) {
final Office office = this.repository.findOne(id);
if (office == null) { throw new OfficeNotFoundException(id); }
return office;
}
public void save(final Office entity) {
this.repository.save(entity);
}
public void saveAndFlush(final Office entity) {
this.repository.saveAndFlush(entity);
}
public void delete(final Office entity) {
this.repository.delete(entity);
}
}
@@ -6,6 +6,7 @@
package org.mifosplatform.portfolio.calendar.domain;
import java.util.Collection;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
@@ -18,8 +19,15 @@ public interface CalendarInstanceRepository extends JpaRepository<CalendarInstan
Collection<CalendarInstance> findByEntityIdAndEntityTypeId(Long entityId, Integer entityTypeId);
CalendarInstance findByEntityIdAndEntityTypeIdAndCalendarTypeId(Long entityId, Integer entityTypeId, Integer calendarTypeId);
@Query("from CalendarInstance ci where ci.entityId = :loanId and ci.entityTypeId = :entityTypeId")
CalendarInstance findCalendarInstaneByLoanId(@Param("loanId") Long loanId, @Param("entityTypeId") Integer entityTypeId);
Collection<CalendarInstance> findByCalendarIdAndEntityTypeId(Long calendarId, Integer entityTypeId);
/**Should use in clause, can I do it without creating a new class?**/
@Query("from CalendarInstance ci where ci.entityId in (select id from Loan loan where loan.client.id = :clientId and loan.group.id=:groupId and (loan.loanStatus = 100 or loan.loanStatus = 200 or loan.loanStatus = 300)) and ci.entityTypeId = 3")
List<CalendarInstance> findCalendarInstancesForActiveLoansByGroupIdAndClientId(@Param("clientId") Long clientId,
@Param("groupId") Long groupId);
}
@@ -207,6 +207,9 @@ public class ClientsApiResource {
} else if (is(commandParam, "close")) {
commandRequest = builder.closeClient(clientId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
}else if (is(commandParam, "transfer")) {
commandRequest = builder.transferClient(clientId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
}
if (result == null) { throw new UnrecognizedQueryParamException("command", commandParam, new Object[] { "activate",
@@ -429,6 +429,10 @@ public final class Client extends AbstractPersistable<Long> {
public Office getOffice() {
return this.office;
}
public void updateOffice(final Office office) {
this.office = office;
}
private Long staffId() {
Long staffId = null;
@@ -453,6 +457,10 @@ public final class Client extends AbstractPersistable<Long> {
public void assignStaff(final Staff staff) {
this.staff = staff;
}
public Set<Group> getGroups(){
return this.groups;
}
public void close(final Long closureReasonId, final Date closureDate) {
@@ -0,0 +1,11 @@
package org.mifosplatform.portfolio.client.exception;
import org.mifosplatform.infrastructure.core.exception.AbstractPlatformDomainRuleException;
public class ClientHasBeenClosedException extends AbstractPlatformDomainRuleException {
public ClientHasBeenClosedException(final Long clientId) {
super("error.msg.client.closed", "Client with identifier " + clientId + " has already been closed");
}
}
@@ -306,6 +306,11 @@ public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWriteP
final Long staffId = command.longValueOfParameterNamed(ClientApiConstants.staffIdParamName);
if (staffId != null) {
staff = this.staffRepository.findByOfficeHierarchyWithNotFoundDetection(staffId, clientForUpdate.getOffice().getHierarchy());
/**
* TODO Vishwas: We maintain history of chage of loan officer w.r.t
* loan in a history table, should we do the same for a client?
* Especially useful when the change happens due to a transfer etc
**/
clientForUpdate.assignStaff(staff);
}
@@ -329,6 +329,10 @@ public class GroupsApiResource {
final CommandWrapper commandRequest = builder.updateRole(groupId, roleId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
return this.toApiJsonSerializer.serialize(result);
} else if (is(commandParam, "transferClients")) {
final CommandWrapper commandRequest = builder.transferClientsBetweenGroups(groupId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
return this.toApiJsonSerializer.serialize(result);
} else {
throw new UnrecognizedQueryParamException("command", commandParam, new Object[] { "activate", "generateCollectionSheet",
"saveCollectionSheet", "unassignStaff", "assignRole", "unassignRole", "updateassignRole" });
@@ -1113,12 +1113,12 @@ public class Loan extends AbstractPersistable<Long> {
for (final LoanCharge loanCharge : setOfLoanCharges()) {
validateChargeHasValidSpecifiedDateIfApplicable(loanCharge, getDisbursementDate(), getLastRepaymentPeriodDueDate());
}
//validate if disbursement date is a holiday or a non-working day
// validate if disbursement date is a holiday or a non-working day
validateDisbursementDateIsOnNonWorkingDay(workingDays, allowTransactionsOnNonWorkingDay);
validateDisbursementDateIsOnHoliday(allowTransactionsOnHoliday, holidays);
}
private LocalDate determineExpectedMaturityDate() {
final int numberOfInstallments = this.repaymentScheduleInstallments.size();
return this.repaymentScheduleInstallments.get(numberOfInstallments - 1).getDueDate();
@@ -1323,7 +1323,7 @@ public class Loan extends AbstractPersistable<Long> {
final LocalDate actualDisbursementDate = command.localDateValueOfParameterNamed("actualDisbursementDate");
final String txnExternalId = command.stringValueOfParameterNamedAllowingNull("externalId");
this.actualDisbursementDate = actualDisbursementDate.toDate();
this.disbursedBy = currentUser;
updateLoanScheduleDependentDerivedFields();
@@ -1332,14 +1332,15 @@ public class Loan extends AbstractPersistable<Long> {
actualChanges.put("dateFormat", command.dateFormat());
actualChanges.put("actualDisbursementDate", command.stringValueOfParameterNamed("actualDisbursementDate"));
//validate if disbursement date is a holiday or a non-working day
// validate if disbursement date is a holiday or a non-working day
validateDisbursementDateIsOnNonWorkingDay(workingDays, allowTransactionsOnNonWorkingDay);
validateDisbursementDateIsOnHoliday(allowTransactionsOnHoliday, holidays);
handleDisbursementTransaction(paymentDetail, actualDisbursementDate, txnExternalId);
if (isRepaymentScheduleRegenerationRequiredForDisbursement(actualDisbursementDate)) {
regenerateRepaymentSchedule(loanScheduleFactory, currency, calculatedRepaymentsStartingFromDate, isHolidayEnabled, holidays, workingDays);
regenerateRepaymentSchedule(loanScheduleFactory, currency, calculatedRepaymentsStartingFromDate, isHolidayEnabled,
holidays, workingDays);
updateLoanRepaymentPeriodsDerivedFields(actualDisbursementDate);
} else {
updateLoanRepaymentPeriodsDerivedFields(actualDisbursementDate);
@@ -1366,8 +1367,8 @@ public class Loan extends AbstractPersistable<Long> {
* details/state.
*/
private void regenerateRepaymentSchedule(final LoanScheduleGeneratorFactory loanScheduleFactory,
final ApplicationCurrency applicationCurrency, final LocalDate calculatedRepaymentsStartingFromDate, final boolean isHolidayEnabled,
final List<Holiday> holidays, final WorkingDays workingDays) {
final ApplicationCurrency applicationCurrency, final LocalDate calculatedRepaymentsStartingFromDate,
final boolean isHolidayEnabled, final List<Holiday> holidays, final WorkingDays workingDays) {
final InterestMethod interestMethod = this.loanRepaymentScheduleDetail.getInterestMethod();
final LoanScheduleGenerator loanScheduleGenerator = loanScheduleFactory.create(interestMethod);
@@ -1386,7 +1387,8 @@ public class Loan extends AbstractPersistable<Long> {
updateLoanSchedule(loanSchedule);
}
private LoanTransaction handleDisbursementTransaction(final PaymentDetail paymentDetail, final LocalDate disbursedOn, final String txnExternalId) {
private LoanTransaction handleDisbursementTransaction(final PaymentDetail paymentDetail, final LocalDate disbursedOn,
final String txnExternalId) {
// track disbursement transaction
final LoanTransaction disbursementTransaction = LoanTransaction.disbursement(this.loanRepaymentScheduleDetail.getPrincipal(),
paymentDetail, disbursedOn, txnExternalId);
@@ -1511,7 +1513,7 @@ public class Loan extends AbstractPersistable<Long> {
validateRepaymentDateIsOnHoliday(repaymentTransaction.getTransactionDate(), allowTransactionsOnHoliday, holidays);
validateRepaymentDateIsOnNonWorkingDay(repaymentTransaction.getTransactionDate(), workingDays, allowTransactionsOnNonWorkingDay);
existingTransactionIds.addAll(findExistingTransactionIds());
existingReversedTransactionIds.addAll(findExistingReversedTransactionIds());
@@ -1875,7 +1877,7 @@ public class Loan extends AbstractPersistable<Long> {
final LocalDate closureDate = command.localDateValueOfParameterNamed("transactionDate");
final String txnExternalId = command.stringValueOfParameterNamedAllowingNull("externalId");
this.closedOnDate = closureDate.toDate();
changes.put("closedOnDate", command.stringValueOfParameterNamed("transactionDate"));
@@ -2387,7 +2389,6 @@ public class Loan extends AbstractPersistable<Long> {
public void updateLoanRepaymentScheduleDates(final LocalDate meetingStartDate, final String recuringRule,
final boolean isHolidayEnabled, final List<Holiday> holidays, final WorkingDays workingDays) {
//first repayment's from date is same as disbursement date.
LocalDate tmpFromDate = this.getDisbursementDate();
final PeriodFrequencyType repaymentPeriodFrequencyType = this.loanRepaymentScheduleDetail.getRepaymentPeriodFrequencyType();
@@ -2483,9 +2484,10 @@ public class Loan extends AbstractPersistable<Long> {
private void validateDisbursementDateIsOnNonWorkingDay(final WorkingDays workingDays, final boolean allowTransactionsOnNonWorkingDay) {
if (!allowTransactionsOnNonWorkingDay) {
if(!WorkingDaysUtil.isWorkingDay(workingDays, getDisbursementDate())){
if (!WorkingDaysUtil.isWorkingDay(workingDays, getDisbursementDate())) {
final String errorMessage = "Expected disbursement date cannot be on a non working day";
throw new LoanApplicationDateException("disbursement.date.on.non.working.day", errorMessage, getExpectedDisbursedOnLocalDate());
throw new LoanApplicationDateException("disbursement.date.on.non.working.day", errorMessage,
getExpectedDisbursedOnLocalDate());
}
}
}
@@ -2494,12 +2496,11 @@ public class Loan extends AbstractPersistable<Long> {
if (!allowTransactionsOnHoliday) {
if (HolidayUtil.isHoliday(getDisbursementDate(), holidays)) {
final String errorMessage = "Expected disbursement date cannot be on a holiday";
throw new LoanApplicationDateException("disbursement.date.on.holiday", errorMessage,
getExpectedDisbursedOnLocalDate());
throw new LoanApplicationDateException("disbursement.date.on.holiday", errorMessage, getExpectedDisbursedOnLocalDate());
}
}
}
private void validateRepaymentDateIsOnNonWorkingDay(final LocalDate repaymentDate, final WorkingDays workingDays,
final boolean allowTransactionsOnNonWorkingDay) {
if (!allowTransactionsOnNonWorkingDay) {
@@ -2518,12 +2519,16 @@ public class Loan extends AbstractPersistable<Long> {
throw new LoanApplicationDateException("repayment.date.on.holiday", errorMessage, repaymentDate);
}
}
}
}
public Group group() {
return this.group;
}
public void updateGroup(Group newGroup) {
this.group = newGroup;
}
public Integer getCurrentLoanCounter() {
return this.loanCounter;
}
@@ -2564,8 +2569,5 @@ public class Loan extends AbstractPersistable<Long> {
}
public void updateGroup(Group group) {
this.group = group;
}
}
@@ -52,6 +52,8 @@ public interface LoanRepository extends JpaRepository<Loan, Long>, JpaSpecificat
public static final String FIND_ACTIVE_LOANS_PRODUCT_IDS_BY_GROUP = "Select loan.loanProduct.id from Loan loan where "
+ "loan.group.id = :groupId and loan.loanStatus = :loanStatus and loan.client.id is NULL group by loan.loanProduct.id";
public static final String DOES_CLIENT_HAVE_ACTIVE_LOANS = "select case when (count (loan) > 0) then true else false end from Loan loan where loan.client.id = :clientId and loan.loanStatus in (100,200,300)";
@Query(FIND_GROUP_LOANS_DISBURSED_AFTER)
List<Loan> getGroupLoansDisbursedAfter(@Param("disbursementDate") Date disbursementDate, @Param("groupId") Long groupId,
@Param("loanType") Integer loanType);
@@ -101,15 +103,21 @@ public interface LoanRepository extends JpaRepository<Loan, Long>, JpaSpecificat
List<Long> getLoansDisbursedAfter(@Param("disbursalDate") Date disbursalDate);
@Query("from Loan loan where loan.client.office.id IN :officeIds and loan.loanStatus IN :loanStatuses")
List<Loan> findByClientOfficeIdsAndLoanStatus(@Param("officeIds") Collection<Long> officeIds, @Param("loanStatuses") Collection<Integer> loanStatuses);
@Query("from Loan loan where loan.group.office.id IN :officeIds and loan.loanStatus IN :loanStatuses")
List<Loan> findByGroupOfficeIdsAndLoanStatus(@Param("officeIds") Collection<Long> officeIds, @Param("loanStatuses") Collection<Integer> loanStatuses);
List<Loan> findByClientOfficeIdsAndLoanStatus(@Param("officeIds") Collection<Long> officeIds,
@Param("loanStatuses") Collection<Integer> loanStatuses);
@Query("from Loan loan where loan.group.office.id IN :officeIds and loan.loanStatus IN :loanStatuses")
List<Loan> findByGroupOfficeIdsAndLoanStatus(@Param("officeIds") Collection<Long> officeIds,
@Param("loanStatuses") Collection<Integer> loanStatuses);
/*** FIXME: Add more appropriate names for the query ***/
@Query(FIND_ACTIVE_LOANS_PRODUCT_IDS_BY_CLIENT)
List<Long> findActiveLoansLoanProductIdsByClient(@Param("clientId") Long clientId, @Param("loanStatus") Integer loanStatus);
@Query(FIND_ACTIVE_LOANS_PRODUCT_IDS_BY_GROUP)
List<Long> findActiveLoansLoanProductIdsByGroup(@Param("groupId") Long groupId, @Param("loanStatus") Integer loanStatus);
@Query(DOES_CLIENT_HAVE_ACTIVE_LOANS)
boolean doesClientHaveActiveLoans(@Param("clientId") Long clientId);
}
@@ -111,4 +111,8 @@ public enum LoanStatus {
public boolean isRejected() {
return this.value.equals(LoanStatus.REJECTED.getValue());
}
public boolean isActiveOrAwaitingApprovalOrDisbursal() {
return this.isApproved() || this.isSubmittedAndPendingApproval() || this.isActive();
}
}
@@ -0,0 +1,34 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.mifosplatform.portfolio.transfer.api;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
public class TransferApiConstants {
// general
public static final String localeParamName = "locale";
public static final String dateFormatParamName = "dateFormat";
// request parameters
public static final String idParamName = "id";
public static final String destinationGroupIdParamName = "destinationGroupId";
public static final String clients = "clients";
public static final String inheritDestinationGroupLoanOfficer = "inheritDestinationGroupLoanOfficer";
public static final String newStaffIdParamName = "newStaffId";
public static final String transferActiveLoans = "transferActiveLoans";
public static final String destinationOfficeIdParamName = "destinationOfficeIdParamName";
public static final Set<String> TRANSFER_CLIENTS_BETWEEN_GROUPS_DATA_PARAMETERS = new HashSet<String>(Arrays.asList(localeParamName,
dateFormatParamName, destinationGroupIdParamName, clients, inheritDestinationGroupLoanOfficer, newStaffIdParamName,
transferActiveLoans));
public static final Set<String> TRANSFER_CLIENT_BETWEEN_BRANCHES_DATA_PARAMETERS = new HashSet<String>(Arrays.asList(localeParamName,
dateFormatParamName, destinationOfficeIdParamName, newStaffIdParamName, transferActiveLoans));
}
@@ -0,0 +1,111 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.mifosplatform.portfolio.transfer.data;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang.StringUtils;
import org.mifosplatform.infrastructure.core.data.ApiParameterError;
import org.mifosplatform.infrastructure.core.data.DataValidatorBuilder;
import org.mifosplatform.infrastructure.core.exception.InvalidJsonException;
import org.mifosplatform.infrastructure.core.exception.PlatformApiDataValidationException;
import org.mifosplatform.infrastructure.core.serialization.FromJsonHelper;
import org.mifosplatform.portfolio.client.api.ClientApiConstants;
import org.mifosplatform.portfolio.group.api.GroupingTypesApiConstants;
import org.mifosplatform.portfolio.transfer.api.TransferApiConstants;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.google.gson.JsonElement;
import com.google.gson.reflect.TypeToken;
@Component
public final class TransfersDataValidator {
private final FromJsonHelper fromApiJsonHelper;
@Autowired
public TransfersDataValidator(final FromJsonHelper fromApiJsonHelper) {
this.fromApiJsonHelper = fromApiJsonHelper;
}
private void throwExceptionIfValidationWarningsExist(final List<ApiParameterError> dataValidationErrors) {
if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); }
}
public void validateForClientsTransferBetweenGroups(final String json) {
if (StringUtils.isBlank(json)) { throw new InvalidJsonException(); }
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json,
TransferApiConstants.TRANSFER_CLIENTS_BETWEEN_GROUPS_DATA_PARAMETERS);
final JsonElement element = this.fromApiJsonHelper.parse(json);
final List<ApiParameterError> dataValidationErrors = new ArrayList<ApiParameterError>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(GroupingTypesApiConstants.GROUP_RESOURCE_NAME);
final Long destinationGroupId = this.fromApiJsonHelper.extractLongNamed(TransferApiConstants.destinationGroupIdParamName, element);
baseDataValidator.reset().parameter(TransferApiConstants.destinationGroupIdParamName).value(destinationGroupId).notNull()
.integerGreaterThanZero();
if (this.fromApiJsonHelper.parameterExists(TransferApiConstants.newStaffIdParamName, element)) {
final Long newStaffId = this.fromApiJsonHelper.extractLongNamed(TransferApiConstants.newStaffIdParamName, element);
baseDataValidator.reset().parameter(TransferApiConstants.newStaffIdParamName).value(newStaffId).notNull()
.integerGreaterThanZero();
}
if (this.fromApiJsonHelper.parameterExists(TransferApiConstants.transferActiveLoans, element)) {
final Boolean inheritDestinationGroupLoanOfficer = this.fromApiJsonHelper.extractBooleanNamed(
TransferApiConstants.transferActiveLoans, element);
baseDataValidator.reset().parameter(TransferApiConstants.transferActiveLoans).value(inheritDestinationGroupLoanOfficer)
.notNull();
}
if (this.fromApiJsonHelper.parameterExists(TransferApiConstants.inheritDestinationGroupLoanOfficer, element)) {
final Boolean inheritDestinationGroupLoanOfficer = this.fromApiJsonHelper.extractBooleanNamed(
TransferApiConstants.inheritDestinationGroupLoanOfficer, element);
baseDataValidator.reset().parameter(TransferApiConstants.inheritDestinationGroupLoanOfficer)
.value(inheritDestinationGroupLoanOfficer).notNull();
}
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
public void validateForClientTransferBetweenBranches(final String json) {
if (StringUtils.isBlank(json)) { throw new InvalidJsonException(); }
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json,
TransferApiConstants.TRANSFER_CLIENT_BETWEEN_BRANCHES_DATA_PARAMETERS);
final JsonElement element = this.fromApiJsonHelper.parse(json);
final List<ApiParameterError> dataValidationErrors = new ArrayList<ApiParameterError>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(ClientApiConstants.CLIENT_RESOURCE_NAME);
final Long destinationOfficeId = this.fromApiJsonHelper
.extractLongNamed(TransferApiConstants.destinationOfficeIdParamName, element);
baseDataValidator.reset().parameter(TransferApiConstants.destinationOfficeIdParamName).value(destinationOfficeId).notNull()
.integerGreaterThanZero();
if (this.fromApiJsonHelper.parameterExists(TransferApiConstants.newStaffIdParamName, element)) {
final Long newStaffId = this.fromApiJsonHelper.extractLongNamed(TransferApiConstants.newStaffIdParamName, element);
baseDataValidator.reset().parameter(TransferApiConstants.newStaffIdParamName).value(newStaffId).notNull()
.integerGreaterThanZero();
}
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
}
@@ -0,0 +1,51 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.mifosplatform.portfolio.transfer.exception;
import org.mifosplatform.infrastructure.core.exception.AbstractPlatformDomainRuleException;
/**
* {@link AbstractPlatformDomainRuleException} thrown when not supported loan
* template type is sent.
*/
public class TransferNotSupportedException extends AbstractPlatformDomainRuleException {
/**
* Default Constructor (cannot transfer entities with active loan/savings
* accounts)
*/
public TransferNotSupportedException() {
super("error.msg.entity.transfers.with.active.accounts", "Cannot transfer Clients/Groups having active loan or savings accounts");
}
/**
* Overloaded Constructor (Cannot transfer Clients with active accounts
* between groups with different meeting frequency)
*
* @param clientId
* @param sourceGroupId
* @param destinationGroupId
*/
public TransferNotSupportedException(Long clientId, Long sourceGroupId, Long destinationGroupId) {
super("error.msg.client.transfers.with.active.accounts.between.groups.with.different.meeting.frequency",
"Cannot transfer Clients with active accounts between groups with different meeting frequency", clientId, sourceGroupId,
destinationGroupId);
}
/**
* Overloaded Constructor (Cannot transfer Clients with active (JLG)
* accounts to a group with no meetings defined)
*
* @param clientId
* @param sourceGroupId
* @param destinationGroupId
*/
public TransferNotSupportedException(Long clientId, Long sourceGroupId, Long destinationGroupId, boolean destinationGroupMeetingAbsent) {
super("error.msg.client.transfers.with.active.accounts.to.group.with.no.meeting.frequency",
"Cannot transfer Client with active accounts to a groups with no meeting frequency", clientId, sourceGroupId,
destinationGroupId,destinationGroupMeetingAbsent);
}
}
@@ -0,0 +1,32 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.mifosplatform.portfolio.transfer.handler;
import org.mifosplatform.commands.handler.NewCommandSourceHandler;
import org.mifosplatform.infrastructure.core.api.JsonCommand;
import org.mifosplatform.infrastructure.core.data.CommandProcessingResult;
import org.mifosplatform.portfolio.transfer.service.TransferWritePlatformService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class TransferClientBetweenBranchesCommandHandler implements NewCommandSourceHandler {
private final TransferWritePlatformService writePlatformService;
@Autowired
public TransferClientBetweenBranchesCommandHandler(final TransferWritePlatformService writePlatformService) {
this.writePlatformService = writePlatformService;
}
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.transferClientBetweenBranches(command.entityId(), command);
}
}
@@ -0,0 +1,32 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.mifosplatform.portfolio.transfer.handler;
import org.mifosplatform.commands.handler.NewCommandSourceHandler;
import org.mifosplatform.infrastructure.core.api.JsonCommand;
import org.mifosplatform.infrastructure.core.data.CommandProcessingResult;
import org.mifosplatform.portfolio.transfer.service.TransferWritePlatformService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class TransferClientsBetweenGroupsCommandHandler implements NewCommandSourceHandler {
private final TransferWritePlatformService writePlatformService;
@Autowired
public TransferClientsBetweenGroupsCommandHandler(final TransferWritePlatformService writePlatformService) {
this.writePlatformService = writePlatformService;
}
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.transferClientsBetweenGroups(command.entityId(), command);
}
}
@@ -0,0 +1,17 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.mifosplatform.portfolio.transfer.service;
import org.mifosplatform.infrastructure.core.api.JsonCommand;
import org.mifosplatform.infrastructure.core.data.CommandProcessingResult;
public interface TransferWritePlatformService {
CommandProcessingResult transferClientsBetweenGroups(final Long sourceGroupId, final JsonCommand jsonCommand);
CommandProcessingResult transferClientBetweenBranches(final Long clientId, final JsonCommand jsonCommand);
}
@@ -0,0 +1,260 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.mifosplatform.portfolio.transfer.service;
import java.util.ArrayList;
import java.util.List;
import org.mifosplatform.infrastructure.core.api.JsonCommand;
import org.mifosplatform.infrastructure.core.data.CommandProcessingResult;
import org.mifosplatform.infrastructure.core.data.CommandProcessingResultBuilder;
import org.mifosplatform.infrastructure.core.service.DateUtils;
import org.mifosplatform.organisation.office.domain.Office;
import org.mifosplatform.organisation.office.domain.OfficeRepositoryWrapper;
import org.mifosplatform.organisation.staff.domain.Staff;
import org.mifosplatform.portfolio.calendar.domain.Calendar;
import org.mifosplatform.portfolio.calendar.domain.CalendarEntityType;
import org.mifosplatform.portfolio.calendar.domain.CalendarInstance;
import org.mifosplatform.portfolio.calendar.domain.CalendarInstanceRepository;
import org.mifosplatform.portfolio.calendar.domain.CalendarType;
import org.mifosplatform.portfolio.calendar.service.CalendarUtils;
import org.mifosplatform.portfolio.client.domain.Client;
import org.mifosplatform.portfolio.client.domain.ClientRepositoryWrapper;
import org.mifosplatform.portfolio.client.exception.ClientHasBeenClosedException;
import org.mifosplatform.portfolio.group.domain.Group;
import org.mifosplatform.portfolio.group.domain.GroupRepositoryWrapper;
import org.mifosplatform.portfolio.group.exception.ClientNotInGroupException;
import org.mifosplatform.portfolio.loanaccount.domain.Loan;
import org.mifosplatform.portfolio.loanaccount.domain.LoanRepository;
import org.mifosplatform.portfolio.loanaccount.service.LoanWritePlatformService;
import org.mifosplatform.portfolio.transfer.api.TransferApiConstants;
import org.mifosplatform.portfolio.transfer.data.TransfersDataValidator;
import org.mifosplatform.portfolio.transfer.exception.TransferNotSupportedException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
@Service
public class TransferWritePlatformServiceJpaRepositoryImpl implements TransferWritePlatformService {
private final ClientRepositoryWrapper clientRepository;
private final OfficeRepositoryWrapper officeRepository;
private final CalendarInstanceRepository calendarInstanceRepository;
private final GroupRepositoryWrapper groupRepository;
private final LoanWritePlatformService loanWritePlatformService;
private final LoanRepository loanRepository;
private final TransfersDataValidator transfersDataValidator;
@Autowired
public TransferWritePlatformServiceJpaRepositoryImpl(final ClientRepositoryWrapper clientRepository,
final OfficeRepositoryWrapper officeRepository, final CalendarInstanceRepository calendarInstanceRepository,
final LoanWritePlatformService loanWritePlatformService, final GroupRepositoryWrapper groupRepository,
final LoanRepository loanRepository, final TransfersDataValidator transfersDataValidator) {
this.clientRepository = clientRepository;
this.officeRepository = officeRepository;
this.calendarInstanceRepository = calendarInstanceRepository;
this.loanWritePlatformService = loanWritePlatformService;
this.groupRepository = groupRepository;
this.loanRepository = loanRepository;
this.transfersDataValidator = transfersDataValidator;
}
@Override
@Transactional
public CommandProcessingResult transferClientsBetweenGroups(Long sourceGroupId, JsonCommand jsonCommand) {
this.transfersDataValidator.validateForClientsTransferBetweenGroups(jsonCommand.json());
final Group sourceGroup = this.groupRepository.findOneWithNotFoundDetection(sourceGroupId);
final Long destinationGroupId = jsonCommand.longValueOfParameterNamed(TransferApiConstants.destinationGroupIdParamName);
final Group destinationGroup = this.groupRepository.findOneWithNotFoundDetection(destinationGroupId);
final List<Client> clients = this.assembleListOfClients(jsonCommand);
for (Client client : clients) {
transferClientBetweenGroups(sourceGroup, client, destinationGroup);
}
return new CommandProcessingResultBuilder() //
.withEntityId(sourceGroupId) //
.build();
}
/****
* Variables that would make sense <br/>
* <ul>
* <li>inheritDestinationGroupLoanOfficer: Default False</li>
* <li>newStaffId: Optional field with Id of new Loan Officer to be linked
* to this client and all his JLG loans for this group</li>
* <li>transferActiveLoans:Default False, unless overridden, this client
* cannot be transferred between groups within the same branch if he has
* active JLG loans and across branches if he has any active loan</li>
* <li></li>
* <li></li>
* <li></li>
* <li></li>
* <li></li>
* <li></li>
* <li></li>
* </ul>
* ***/
@Transactional
public void transferClientBetweenGroups(final Group sourceGroup, final Client client, final Group destinationGroup) {
// next I shall validate that the client is present in this group
if (!sourceGroup.hasClientAsMember(client)) { throw new ClientNotInGroupException(client.getId(), sourceGroup.getId()); }
// Is client active?
if (client.isNotActive()) { throw new ClientHasBeenClosedException(client.getId()); }
/*** Code for intra-branch transfers ***/
if (client.getOffice().getId() == destinationGroup.getOffice().getId()) {
/**
* TODO: for now we need to ensure that only one collection sheet
* calendar can be linked with a center or group entity <br/>
**/
final CalendarInstance sourceGroupCalendarInstance = this.calendarInstanceRepository
.findByEntityIdAndEntityTypeIdAndCalendarTypeId(sourceGroup.getId(), CalendarEntityType.GROUPS.getValue(),
CalendarType.COLLECTION.getValue());
// get all customer loans synced with this group calendar Instance
final List<CalendarInstance> activeLoanCalendarInstances = this.calendarInstanceRepository
.findCalendarInstancesForActiveLoansByGroupIdAndClientId(sourceGroup.getId(), client.getId());
/**
* if a calendar is present in the source group along with loans
* synced with it, we should ensure that the destination group also
* has a collection calendar
**/
if (sourceGroupCalendarInstance != null && !activeLoanCalendarInstances.isEmpty()) {
// get the destination calendar
final CalendarInstance destinationGroupCalendarInstance = this.calendarInstanceRepository
.findByEntityIdAndEntityTypeIdAndCalendarTypeId(destinationGroup.getId(), CalendarEntityType.GROUPS.getValue(),
CalendarType.COLLECTION.getValue());
if (destinationGroupCalendarInstance == null) { throw new TransferNotSupportedException(client.getId(),
sourceGroup.getId(), destinationGroup.getId(), false);
}
Calendar sourceGroupCalendar = destinationGroupCalendarInstance.getCalendar();
Calendar destinationGroupCalendar = destinationGroupCalendarInstance.getCalendar();
/***
* Ensure that the recurrence pattern are same for collection
* meeting in both the source and the destination calendar
***/
if (!(CalendarUtils.isFrequencySame(sourceGroupCalendar.getRecurrence(), destinationGroupCalendar.getRecurrence()) && CalendarUtils
.isIntervalSame(sourceGroupCalendar.getRecurrence(), destinationGroupCalendar.getRecurrence()))) { throw new TransferNotSupportedException(
client.getId(), sourceGroup.getId(), destinationGroup.getId()); }
/** map all JLG loans for this client to the destinationGroup **/
for (CalendarInstance calendarInstance : activeLoanCalendarInstances) {
calendarInstance.updateCalendar(destinationGroupCalendar);
}
// reschedule all JLG Loans to follow new Calendar
this.loanWritePlatformService.applyMeetingDateChanges(destinationGroupCalendar, activeLoanCalendarInstances);
}
} else {
Office destinationOffice = destinationGroup.getOffice();
transferClientToNewBranch(client, destinationOffice);
}
/**
* Now Change the loan officer for this client and all his active JLG
* loans
**/
Staff destinationGroupLoanOfficer = destinationGroup.getStaff();
if (destinationGroupLoanOfficer != null) {
client.updateStaff(destinationGroupLoanOfficer);
}
client.getGroups().add(destinationGroup);
this.clientRepository.saveAndFlush(client);
/**
* Active JLG loans are now linked to the new Group and Loan officer
**/
List<Loan> allClientJLGLoans = loanRepository.findByClientIdAndGroupId(client.getId(), sourceGroup.getId());
for (Loan loan : allClientJLGLoans) {
if (loan.status().isActiveOrAwaitingApprovalOrDisbursal()) {
loan.updateGroup(destinationGroup);
if (destinationGroupLoanOfficer != null) {
loan.reassignLoanOfficer(destinationGroupLoanOfficer, DateUtils.getLocalDateOfTenant());
}
this.loanRepository.saveAndFlush(loan);
}
}
// change client group membership
client.getGroups().remove(sourceGroup);
}
/**
* This API is meant for transferring clients between branches mainly by
* Organizations following an Individual lending Model <br/>
* If the Client is linked to any Groups, we can optionally choose to have
* all the linkages broken and all JLG Loans are converted into Individual
* Loans
*
* @param clientId
* @param destinationOfficeId
* @param jsonCommand
* @return
**/
@Transactional
@Override
public CommandProcessingResult transferClientBetweenBranches(final Long clientId, final JsonCommand jsonCommand) {
// validation
this.transfersDataValidator.validateForClientsTransferBetweenGroups(jsonCommand.json());
final Long destinationOfficeId = jsonCommand.longValueOfParameterNamed(TransferApiConstants.destinationOfficeIdParamName);
final Office office = this.officeRepository.findOneWithNotFoundDetection(destinationOfficeId);
final Client client = this.clientRepository.findOneWithNotFoundDetection(clientId);
/*** Remove all group associations for this client ***/
client.getGroups().clear();
transferClientToNewBranch(client, office);
this.clientRepository.save(client);
return new CommandProcessingResultBuilder() //
.withClientId(clientId) //
.withEntityId(clientId) //
.build();
}
private void transferClientToNewBranch(final Client client, final Office destinationOffice) {
/**
* For now, do not allow Clients with active loans to be transferred
* between branches
**/
if (loanRepository.doesClientHaveActiveLoans(client.getId())) { throw new TransferNotSupportedException(); }
// set the new office for the client
client.updateOffice(destinationOffice);
}
private List<Client> assembleListOfClients(final JsonCommand command) {
final List<Client> clients = new ArrayList<Client>();
if (command.parameterExists(TransferApiConstants.clients)) {
JsonArray clientsArray = command.arrayOfParameterNamed(TransferApiConstants.clients);
if (clientsArray != null) {
for (int i = 0; i < clientsArray.size(); i++) {
final JsonObject jsonObject = clientsArray.get(i).getAsJsonObject();
if (jsonObject.has(TransferApiConstants.idParamName)) {
final Long id = jsonObject.get(TransferApiConstants.idParamName).getAsLong();
final Client client = this.clientRepository.findOneWithNotFoundDetection(id);
clients.add(client);
}
}
}
}
return clients;
}
}