From 40bbf46e0507368fd9c2f08d2bc5175fa9dab6b1 Mon Sep 17 00:00:00 2001 From: Vishwas Babu A J Date: Tue, 20 Aug 2013 17:00:56 +0530 Subject: [PATCH] initial work on transfers --- .../V104__permissions_for_transfers.sql | 11 + .../commands/domain/CommandWrapper.java | 8 + .../service/CommandWrapperBuilder.java | 18 ++ .../SynchronousCommandProcessingService.java | 6 +- .../core/data/DataValidatorBuilder.java | 1 + .../domain/OfficeRepositoryWrapper.java | 45 +++ .../domain/CalendarInstanceRepository.java | 10 +- .../client/api/ClientsApiResource.java | 3 + .../portfolio/client/domain/Client.java | 8 + .../ClientHasBeenClosedException.java | 11 + ...WritePlatformServiceJpaRepositoryImpl.java | 5 + .../group/api/GroupsApiResource.java | 4 + .../portfolio/loanaccount/domain/Loan.java | 46 ++-- .../loanaccount/domain/LoanRepository.java | 16 +- .../loanaccount/domain/LoanStatus.java | 4 + .../transfer/api/TransferApiConstants.java | 34 +++ .../transfer/data/TransfersDataValidator.java | 111 ++++++++ .../TransferNotSupportedException.java | 51 ++++ ...erClientBetweenBranchesCommandHandler.java | 32 +++ ...ferClientsBetweenGroupsCommandHandler.java | 32 +++ .../service/TransferWritePlatformService.java | 17 ++ ...WritePlatformServiceJpaRepositoryImpl.java | 260 ++++++++++++++++++ 22 files changed, 705 insertions(+), 28 deletions(-) create mode 100644 mifosng-db/migrations/core_db/V104__permissions_for_transfers.sql create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/organisation/office/domain/OfficeRepositoryWrapper.java create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/exception/ClientHasBeenClosedException.java create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/portfolio/transfer/api/TransferApiConstants.java create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/portfolio/transfer/data/TransfersDataValidator.java create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/portfolio/transfer/exception/TransferNotSupportedException.java create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/portfolio/transfer/handler/TransferClientBetweenBranchesCommandHandler.java create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/portfolio/transfer/handler/TransferClientsBetweenGroupsCommandHandler.java create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/portfolio/transfer/service/TransferWritePlatformService.java create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/portfolio/transfer/service/TransferWritePlatformServiceJpaRepositoryImpl.java diff --git a/mifosng-db/migrations/core_db/V104__permissions_for_transfers.sql b/mifosng-db/migrations/core_db/V104__permissions_for_transfers.sql new file mode 100644 index 000000000..485010005 --- /dev/null +++ b/mifosng-db/migrations/core_db/V104__permissions_for_transfers.sql @@ -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); \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/commands/domain/CommandWrapper.java b/mifosng-provider/src/main/java/org/mifosplatform/commands/domain/CommandWrapper.java index 3a809e0c9..90ff1c732 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/commands/domain/CommandWrapper.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/commands/domain/CommandWrapper.java @@ -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"); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/commands/service/CommandWrapperBuilder.java b/mifosng-provider/src/main/java/org/mifosplatform/commands/service/CommandWrapperBuilder.java index caa102376..49d7838ad 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/commands/service/CommandWrapperBuilder.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/commands/service/CommandWrapperBuilder.java @@ -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"; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/commands/service/SynchronousCommandProcessingService.java b/mifosng-provider/src/main/java/org/mifosplatform/commands/service/SynchronousCommandProcessingService.java index a70f704d1..b8822b31c 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/commands/service/SynchronousCommandProcessingService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/commands/service/SynchronousCommandProcessingService.java @@ -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()) { diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/data/DataValidatorBuilder.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/data/DataValidatorBuilder.java index a6f66c1f9..0e9c76e28 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/data/DataValidatorBuilder.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/data/DataValidatorBuilder.java @@ -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) diff --git a/mifosng-provider/src/main/java/org/mifosplatform/organisation/office/domain/OfficeRepositoryWrapper.java b/mifosng-provider/src/main/java/org/mifosplatform/organisation/office/domain/OfficeRepositoryWrapper.java new file mode 100644 index 000000000..7c660cb0c --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/organisation/office/domain/OfficeRepositoryWrapper.java @@ -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; + +/** + *

+ * Wrapper for {@link OfficeRepository} that adds NULL checking and Error + * handling capabilities + *

+ */ +@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); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/calendar/domain/CalendarInstanceRepository.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/calendar/domain/CalendarInstanceRepository.java index fe7fd2553..1a3a3b4c9 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/calendar/domain/CalendarInstanceRepository.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/calendar/domain/CalendarInstanceRepository.java @@ -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 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 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 findCalendarInstancesForActiveLoansByGroupIdAndClientId(@Param("clientId") Long clientId, + @Param("groupId") Long groupId); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/api/ClientsApiResource.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/api/ClientsApiResource.java index a760e9e6d..4fc537a27 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/api/ClientsApiResource.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/api/ClientsApiResource.java @@ -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", diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/domain/Client.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/domain/Client.java index 085ea3d0d..aaf9fca4b 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/domain/Client.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/domain/Client.java @@ -429,6 +429,10 @@ public final class Client extends AbstractPersistable { 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 { public void assignStaff(final Staff staff) { this.staff = staff; } + + public Set getGroups(){ + return this.groups; + } public void close(final Long closureReasonId, final Date closureDate) { diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/exception/ClientHasBeenClosedException.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/exception/ClientHasBeenClosedException.java new file mode 100644 index 000000000..465421c46 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/exception/ClientHasBeenClosedException.java @@ -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"); + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/service/ClientWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/service/ClientWritePlatformServiceJpaRepositoryImpl.java index bd7270965..a97f2bbec 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/service/ClientWritePlatformServiceJpaRepositoryImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/service/ClientWritePlatformServiceJpaRepositoryImpl.java @@ -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); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/group/api/GroupsApiResource.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/group/api/GroupsApiResource.java index 33119a77b..c46c238a1 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/group/api/GroupsApiResource.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/group/api/GroupsApiResource.java @@ -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" }); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/Loan.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/Loan.java index dda71b4bc..b88a003ce 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/Loan.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/Loan.java @@ -1113,12 +1113,12 @@ public class Loan extends AbstractPersistable { 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 { 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 { 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 { * details/state. */ private void regenerateRepaymentSchedule(final LoanScheduleGeneratorFactory loanScheduleFactory, - final ApplicationCurrency applicationCurrency, final LocalDate calculatedRepaymentsStartingFromDate, final boolean isHolidayEnabled, - final List holidays, final WorkingDays workingDays) { + final ApplicationCurrency applicationCurrency, final LocalDate calculatedRepaymentsStartingFromDate, + final boolean isHolidayEnabled, final List 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 { 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 { 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 { 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 { public void updateLoanRepaymentScheduleDates(final LocalDate meetingStartDate, final String recuringRule, final boolean isHolidayEnabled, final List 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 { 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 { 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 { 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 { } - public void updateGroup(Group group) { - this.group = group; - } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanRepository.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanRepository.java index a084795b4..07854f45b 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanRepository.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanRepository.java @@ -52,6 +52,8 @@ public interface LoanRepository extends JpaRepository, 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 getGroupLoansDisbursedAfter(@Param("disbursementDate") Date disbursementDate, @Param("groupId") Long groupId, @Param("loanType") Integer loanType); @@ -101,15 +103,21 @@ public interface LoanRepository extends JpaRepository, JpaSpecificat List getLoansDisbursedAfter(@Param("disbursalDate") Date disbursalDate); @Query("from Loan loan where loan.client.office.id IN :officeIds and loan.loanStatus IN :loanStatuses") - List findByClientOfficeIdsAndLoanStatus(@Param("officeIds") Collection officeIds, @Param("loanStatuses") Collection loanStatuses); - - @Query("from Loan loan where loan.group.office.id IN :officeIds and loan.loanStatus IN :loanStatuses") - List findByGroupOfficeIdsAndLoanStatus(@Param("officeIds") Collection officeIds, @Param("loanStatuses") Collection loanStatuses); + List findByClientOfficeIdsAndLoanStatus(@Param("officeIds") Collection officeIds, + @Param("loanStatuses") Collection loanStatuses); + @Query("from Loan loan where loan.group.office.id IN :officeIds and loan.loanStatus IN :loanStatuses") + List findByGroupOfficeIdsAndLoanStatus(@Param("officeIds") Collection officeIds, + @Param("loanStatuses") Collection loanStatuses); + + /*** FIXME: Add more appropriate names for the query ***/ @Query(FIND_ACTIVE_LOANS_PRODUCT_IDS_BY_CLIENT) List findActiveLoansLoanProductIdsByClient(@Param("clientId") Long clientId, @Param("loanStatus") Integer loanStatus); @Query(FIND_ACTIVE_LOANS_PRODUCT_IDS_BY_GROUP) List findActiveLoansLoanProductIdsByGroup(@Param("groupId") Long groupId, @Param("loanStatus") Integer loanStatus); + @Query(DOES_CLIENT_HAVE_ACTIVE_LOANS) + boolean doesClientHaveActiveLoans(@Param("clientId") Long clientId); + } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanStatus.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanStatus.java index abf9b99a9..958a551f2 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanStatus.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanStatus.java @@ -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(); + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/transfer/api/TransferApiConstants.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/transfer/api/TransferApiConstants.java new file mode 100644 index 000000000..9b6a6221d --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/transfer/api/TransferApiConstants.java @@ -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 TRANSFER_CLIENTS_BETWEEN_GROUPS_DATA_PARAMETERS = new HashSet(Arrays.asList(localeParamName, + dateFormatParamName, destinationGroupIdParamName, clients, inheritDestinationGroupLoanOfficer, newStaffIdParamName, + transferActiveLoans)); + + public static final Set TRANSFER_CLIENT_BETWEEN_BRANCHES_DATA_PARAMETERS = new HashSet(Arrays.asList(localeParamName, + dateFormatParamName, destinationOfficeIdParamName, newStaffIdParamName, transferActiveLoans)); + +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/transfer/data/TransfersDataValidator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/transfer/data/TransfersDataValidator.java new file mode 100644 index 000000000..a71646c74 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/transfer/data/TransfersDataValidator.java @@ -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 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>() {}.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, + TransferApiConstants.TRANSFER_CLIENTS_BETWEEN_GROUPS_DATA_PARAMETERS); + final JsonElement element = this.fromApiJsonHelper.parse(json); + + final List dataValidationErrors = new ArrayList(); + + 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>() {}.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, + TransferApiConstants.TRANSFER_CLIENT_BETWEEN_BRANCHES_DATA_PARAMETERS); + final JsonElement element = this.fromApiJsonHelper.parse(json); + + final List dataValidationErrors = new ArrayList(); + + 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); + } + +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/transfer/exception/TransferNotSupportedException.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/transfer/exception/TransferNotSupportedException.java new file mode 100644 index 000000000..a4959234d --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/transfer/exception/TransferNotSupportedException.java @@ -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); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/transfer/handler/TransferClientBetweenBranchesCommandHandler.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/transfer/handler/TransferClientBetweenBranchesCommandHandler.java new file mode 100644 index 000000000..5d2edea84 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/transfer/handler/TransferClientBetweenBranchesCommandHandler.java @@ -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); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/transfer/handler/TransferClientsBetweenGroupsCommandHandler.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/transfer/handler/TransferClientsBetweenGroupsCommandHandler.java new file mode 100644 index 000000000..f8e79ace7 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/transfer/handler/TransferClientsBetweenGroupsCommandHandler.java @@ -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); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/transfer/service/TransferWritePlatformService.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/transfer/service/TransferWritePlatformService.java new file mode 100644 index 000000000..6e11e6d46 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/transfer/service/TransferWritePlatformService.java @@ -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); + +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/transfer/service/TransferWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/transfer/service/TransferWritePlatformServiceJpaRepositoryImpl.java new file mode 100644 index 000000000..584bae5fb --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/transfer/service/TransferWritePlatformServiceJpaRepositoryImpl.java @@ -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 clients = this.assembleListOfClients(jsonCommand); + + for (Client client : clients) { + transferClientBetweenGroups(sourceGroup, client, destinationGroup); + } + + return new CommandProcessingResultBuilder() // + .withEntityId(sourceGroupId) // + .build(); + } + + /**** + * Variables that would make sense
+ *
    + *
  • inheritDestinationGroupLoanOfficer: Default False
  • + *
  • newStaffId: Optional field with Id of new Loan Officer to be linked + * to this client and all his JLG loans for this group
  • + *
  • 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
  • + *
  • + *
  • + *
  • + *
  • + *
  • + *
  • + *
  • + *
+ * ***/ + @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
+ **/ + 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 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 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
+ * 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 assembleListOfClients(final JsonCommand command) { + + final List clients = new ArrayList(); + + 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; + } + +} \ No newline at end of file