blob: cda35f7ccdfc691d20f7061a25637a5d4a25168b [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.group.service;
import jakarta.persistence.PersistenceException;
import java.time.LocalDate;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.fineract.commands.domain.CommandWrapper;
import org.apache.fineract.commands.service.CommandProcessingService;
import org.apache.fineract.commands.service.CommandWrapperBuilder;
import org.apache.fineract.infrastructure.accountnumberformat.domain.AccountNumberFormat;
import org.apache.fineract.infrastructure.accountnumberformat.domain.AccountNumberFormatRepositoryWrapper;
import org.apache.fineract.infrastructure.accountnumberformat.domain.EntityAccountType;
import org.apache.fineract.infrastructure.codes.domain.CodeValue;
import org.apache.fineract.infrastructure.codes.domain.CodeValueRepositoryWrapper;
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
import org.apache.fineract.infrastructure.core.exception.ErrorHandler;
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.dataqueries.data.EntityTables;
import org.apache.fineract.infrastructure.dataqueries.data.StatusEnum;
import org.apache.fineract.infrastructure.dataqueries.service.EntityDatatableChecksWritePlatformService;
import org.apache.fineract.infrastructure.event.business.domain.group.CentersCreateBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.group.GroupsCreateBusinessEvent;
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.organisation.office.domain.Office;
import org.apache.fineract.organisation.office.domain.OfficeRepositoryWrapper;
import org.apache.fineract.organisation.office.exception.InvalidOfficeException;
import org.apache.fineract.organisation.staff.domain.Staff;
import org.apache.fineract.organisation.staff.domain.StaffRepositoryWrapper;
import org.apache.fineract.portfolio.account.service.AccountNumberGenerator;
import org.apache.fineract.portfolio.calendar.domain.Calendar;
import org.apache.fineract.portfolio.calendar.domain.CalendarEntityType;
import org.apache.fineract.portfolio.calendar.domain.CalendarInstance;
import org.apache.fineract.portfolio.calendar.domain.CalendarInstanceRepository;
import org.apache.fineract.portfolio.calendar.domain.CalendarType;
import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper;
import org.apache.fineract.portfolio.client.service.LoanStatusMapper;
import org.apache.fineract.portfolio.group.api.GroupingTypesApiConstants;
import org.apache.fineract.portfolio.group.domain.Group;
import org.apache.fineract.portfolio.group.domain.GroupLevel;
import org.apache.fineract.portfolio.group.domain.GroupLevelRepository;
import org.apache.fineract.portfolio.group.domain.GroupRepositoryWrapper;
import org.apache.fineract.portfolio.group.domain.GroupTypes;
import org.apache.fineract.portfolio.group.exception.GroupAccountExistsException;
import org.apache.fineract.portfolio.group.exception.GroupHasNoStaffException;
import org.apache.fineract.portfolio.group.exception.GroupMemberCountNotInPermissibleRangeException;
import org.apache.fineract.portfolio.group.exception.GroupMustBePendingToBeDeletedException;
import org.apache.fineract.portfolio.group.exception.InvalidGroupLevelException;
import org.apache.fineract.portfolio.group.exception.InvalidGroupStateTransitionException;
import org.apache.fineract.portfolio.group.serialization.GroupingTypesDataValidator;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
import org.apache.fineract.portfolio.note.domain.Note;
import org.apache.fineract.portfolio.note.domain.NoteRepository;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepositoryWrapper;
import org.apache.fineract.useradministration.domain.AppUser;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.orm.jpa.JpaSystemException;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
@Slf4j
@RequiredArgsConstructor
public class GroupingTypesWritePlatformServiceJpaRepositoryImpl implements GroupingTypesWritePlatformService {
private final PlatformSecurityContext context;
private final GroupRepositoryWrapper groupRepository;
private final ClientRepositoryWrapper clientRepositoryWrapper;
private final OfficeRepositoryWrapper officeRepositoryWrapper;
private final StaffRepositoryWrapper staffRepository;
private final NoteRepository noteRepository;
private final GroupLevelRepository groupLevelRepository;
private final GroupingTypesDataValidator fromApiJsonDeserializer;
private final LoanRepositoryWrapper loanRepositoryWrapper;
private final CodeValueRepositoryWrapper codeValueRepository;
private final CommandProcessingService commandProcessingService;
private final CalendarInstanceRepository calendarInstanceRepository;
private final ConfigurationDomainService configurationDomainService;
private final SavingsAccountRepositoryWrapper savingsAccountRepositoryWrapper;
private final AccountNumberFormatRepositoryWrapper accountNumberFormatRepository;
private final AccountNumberGenerator accountNumberGenerator;
private final EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService;
private final BusinessEventNotifierService businessEventNotifierService;
private CommandProcessingResult createGroupingType(final JsonCommand command, final GroupTypes groupingType, final Long centerId) {
try {
final String accountNo = command.stringValueOfParameterNamed(GroupingTypesApiConstants.accountNoParamName);
final String name = command.stringValueOfParameterNamed(GroupingTypesApiConstants.nameParamName);
final String externalId = command.stringValueOfParameterNamed(GroupingTypesApiConstants.externalIdParamName);
final AppUser currentUser = this.context.authenticatedUser();
Long officeId = null;
Group parentGroup = null;
if (centerId == null) {
officeId = command.longValueOfParameterNamed(GroupingTypesApiConstants.officeIdParamName);
} else {
parentGroup = this.groupRepository.findOneWithNotFoundDetection(centerId);
officeId = parentGroup.officeId();
}
final Office groupOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(officeId);
final LocalDate activationDate = command.localDateValueOfParameterNamed(GroupingTypesApiConstants.activationDateParamName);
final GroupLevel groupLevel = this.groupLevelRepository.findById(groupingType.getId()).orElse(null);
validateOfficeOpeningDateisAfterGroupOrCenterOpeningDate(groupOffice, groupLevel, activationDate);
Staff staff = null;
final Long staffId = command.longValueOfParameterNamed(GroupingTypesApiConstants.staffIdParamName);
if (staffId != null) {
staff = this.staffRepository.findByOfficeHierarchyWithNotFoundDetection(staffId, groupOffice.getHierarchy());
}
final Set<Client> clientMembers = assembleSetOfClients(officeId, command);
final Set<Group> groupMembers = assembleSetOfChildGroups(officeId, command);
final boolean active = command.booleanPrimitiveValueOfParameterNamed(GroupingTypesApiConstants.activeParamName);
LocalDate submittedOnDate = DateUtils.getBusinessLocalDate();
if (active && DateUtils.isAfter(submittedOnDate, activationDate)) {
submittedOnDate = activationDate;
}
if (command.hasParameter(GroupingTypesApiConstants.submittedOnDateParamName)) {
submittedOnDate = command.localDateValueOfParameterNamed(GroupingTypesApiConstants.submittedOnDateParamName);
}
final Group newGroup = Group.newGroup(groupOffice, staff, parentGroup, groupLevel, name, externalId, active, activationDate,
clientMembers, groupMembers, submittedOnDate, currentUser, accountNo);
boolean rollbackTransaction = false;
if (newGroup.isActive()) {
this.groupRepository.saveAndFlush(newGroup);
// validate Group creation rules for Group
if (newGroup.isGroup()) {
validateGroupRulesBeforeActivation(newGroup);
}
if (newGroup.isCenter()) {
final CommandWrapper commandWrapper = new CommandWrapperBuilder().activateCenter(null).build();
rollbackTransaction = this.commandProcessingService.validateRollbackCommand(commandWrapper, currentUser);
} else {
final CommandWrapper commandWrapper = new CommandWrapperBuilder().activateGroup(null).build();
rollbackTransaction = this.commandProcessingService.validateRollbackCommand(commandWrapper, currentUser);
}
}
if (!newGroup.isCenter() && newGroup.hasActiveClients()) {
final CommandWrapper commandWrapper = new CommandWrapperBuilder().associateClientsToGroup(newGroup.getId()).build();
rollbackTransaction = this.commandProcessingService.validateRollbackCommand(commandWrapper, currentUser);
}
// pre-save to generate id for use in group hierarchy
this.groupRepository.save(newGroup);
/*
* Generate hierarchy for a new center/group and all the child groups if they exist
*/
newGroup.generateHierarchy();
/* Generate account number if required */
generateAccountNumberIfRequired(newGroup);
this.groupRepository.saveAndFlush(newGroup);
newGroup.captureStaffHistoryDuringCenterCreation(staff, activationDate);
if (newGroup.isGroup()) {
if (command.parameterExists(GroupingTypesApiConstants.datatables)) {
this.entityDatatableChecksWritePlatformService.saveDatatables(StatusEnum.CREATE.getCode().longValue(),
EntityTables.GROUP.getName(), newGroup.getId(), null,
command.arrayOfParameterNamed(GroupingTypesApiConstants.datatables));
}
this.entityDatatableChecksWritePlatformService.runTheCheck(newGroup.getId(), EntityTables.GROUP.getName(),
StatusEnum.CREATE.getCode(), EntityTables.GROUP.getForeignKeyColumnNameOnDatatable(), null);
}
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withOfficeId(groupOffice.getId()) //
.withGroupId(newGroup.getId()) //
.withEntityId(newGroup.getId()) //
.setRollbackTransaction(rollbackTransaction)//
.build();
} catch (final JpaSystemException | DataIntegrityViolationException dve) {
handleGroupDataIntegrityIssues(command, dve.getMostSpecificCause(), dve, groupingType);
return CommandProcessingResult.empty();
} catch (final PersistenceException dve) {
Throwable throwable = ExceptionUtils.getRootCause(dve.getCause());
handleGroupDataIntegrityIssues(command, throwable, dve, groupingType);
return CommandProcessingResult.empty();
}
}
private void generateAccountNumberIfRequired(Group newGroup) {
if (newGroup.isAccountNumberRequiresAutoGeneration()) {
EntityAccountType entityAccountType = null;
AccountNumberFormat accountNumberFormat = null;
if (newGroup.isCenter()) {
entityAccountType = EntityAccountType.CENTER;
accountNumberFormat = this.accountNumberFormatRepository.findByAccountType(entityAccountType);
newGroup.updateAccountNo(this.accountNumberGenerator.generateCenterAccountNumber(newGroup, accountNumberFormat));
} else {
entityAccountType = EntityAccountType.GROUP;
accountNumberFormat = this.accountNumberFormatRepository.findByAccountType(entityAccountType);
newGroup.updateAccountNo(this.accountNumberGenerator.generateGroupAccountNumber(newGroup, accountNumberFormat));
}
}
}
@Transactional
@Override
public CommandProcessingResult createCenter(final JsonCommand command) {
this.fromApiJsonDeserializer.validateForCreateCenter(command);
final Long centerId = null;
CommandProcessingResult commandProcessingResult = createGroupingType(command, GroupTypes.CENTER, centerId);
businessEventNotifierService.notifyPostBusinessEvent(new CentersCreateBusinessEvent(commandProcessingResult));
return commandProcessingResult;
}
@Transactional
@Override
public CommandProcessingResult createGroup(final Long centerId, final JsonCommand command) {
if (centerId != null) {
this.fromApiJsonDeserializer.validateForCreateCenterGroup(command);
} else {
this.fromApiJsonDeserializer.validateForCreateGroup(command);
}
CommandProcessingResult commandProcessingResult = createGroupingType(command, GroupTypes.GROUP, centerId);
businessEventNotifierService.notifyPostBusinessEvent(new GroupsCreateBusinessEvent(commandProcessingResult));
return commandProcessingResult;
}
@Transactional
@Override
public CommandProcessingResult activateGroupOrCenter(final Long groupId, final JsonCommand command) {
try {
this.fromApiJsonDeserializer.validateForActivation(command, GroupingTypesApiConstants.GROUP_RESOURCE_NAME);
final AppUser currentUser = this.context.authenticatedUser();
final Group group = this.groupRepository.findOneWithNotFoundDetection(groupId);
if (group.isGroup()) {
validateGroupRulesBeforeActivation(group);
}
final LocalDate activationDate = command.localDateValueOfParameterNamed("activationDate");
validateOfficeOpeningDateisAfterGroupOrCenterOpeningDate(group.getOffice(), group.getGroupLevel(), activationDate);
group.activate(currentUser, activationDate);
this.groupRepository.saveAndFlush(group);
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withOfficeId(group.officeId()) //
.withGroupId(groupId) //
.withEntityId(groupId) //
.build();
} catch (final JpaSystemException | DataIntegrityViolationException dve) {
handleGroupDataIntegrityIssues(command, dve.getMostSpecificCause(), dve, GroupTypes.GROUP);
return CommandProcessingResult.empty();
} catch (final PersistenceException dve) {
Throwable throwable = ExceptionUtils.getRootCause(dve.getCause());
handleGroupDataIntegrityIssues(command, throwable, dve, GroupTypes.GROUP);
return CommandProcessingResult.empty();
}
}
private void validateGroupRulesBeforeActivation(final Group group) {
Integer minClients = configurationDomainService.retrieveMinAllowedClientsInGroup();
Integer maxClients = configurationDomainService.retrieveMaxAllowedClientsInGroup();
boolean isGroupClientCountValid = group.isGroupsClientCountWithinMinMaxRange(minClients, maxClients);
if (!isGroupClientCountValid) {
throw new GroupMemberCountNotInPermissibleRangeException(group.getId(), minClients, maxClients);
}
entityDatatableChecksWritePlatformService.runTheCheck(group.getId(), EntityTables.GROUP.getName(), StatusEnum.ACTIVATE.getCode(),
EntityTables.GROUP.getForeignKeyColumnNameOnDatatable(), null);
}
public void validateGroupRulesBeforeClientAssociation(final Group group) {
Integer minClients = configurationDomainService.retrieveMinAllowedClientsInGroup();
Integer maxClients = configurationDomainService.retrieveMaxAllowedClientsInGroup();
boolean isGroupClientCountValid = group.isGroupsClientCountWithinMaxRange(maxClients);
if (!isGroupClientCountValid) {
throw new GroupMemberCountNotInPermissibleRangeException(group.getId(), minClients, maxClients);
}
}
@Transactional
@Override
public CommandProcessingResult updateCenter(final Long centerId, final JsonCommand command) {
this.fromApiJsonDeserializer.validateForUpdateCenter(command, centerId);
return updateGroupingType(centerId, command, GroupTypes.CENTER);
}
@Transactional
@Override
public CommandProcessingResult updateGroup(final Long groupId, final JsonCommand command) {
this.fromApiJsonDeserializer.validateForUpdateGroup(command, groupId);
return updateGroupingType(groupId, command, GroupTypes.GROUP);
}
private CommandProcessingResult updateGroupingType(final Long groupId, final JsonCommand command, final GroupTypes groupingType) {
try {
this.context.authenticatedUser();
final Group groupForUpdate = this.groupRepository.findOneWithNotFoundDetection(groupId);
final Long officeId = groupForUpdate.officeId();
final Office groupOffice = groupForUpdate.getOffice();
final String groupHierarchy = groupOffice.getHierarchy();
this.context.validateAccessRights(groupHierarchy);
final LocalDate activationDate = command.localDateValueOfParameterNamed(GroupingTypesApiConstants.activationDateParamName);
validateOfficeOpeningDateisAfterGroupOrCenterOpeningDate(groupOffice, groupForUpdate.getGroupLevel(), activationDate);
final Map<String, Object> actualChanges = groupForUpdate.update(command);
if (actualChanges.containsKey(GroupingTypesApiConstants.staffIdParamName)) {
final Long newValue = command.longValueOfParameterNamed(GroupingTypesApiConstants.staffIdParamName);
Staff newStaff = null;
if (newValue != null) {
newStaff = this.staffRepository.findByOfficeHierarchyWithNotFoundDetection(newValue, groupHierarchy);
}
groupForUpdate.updateStaff(newStaff);
}
final GroupLevel groupLevel = this.groupLevelRepository.findById(groupForUpdate.getGroupLevel().getId()).orElse(null);
/*
* Ignoring parentId param, if group for update is super parent. TODO Need to check: Ignoring is correct or
* need throw unsupported param
*/
if (!groupLevel.isSuperParent()) {
Long parentId = null;
final Group presentParentGroup = groupForUpdate.getParent();
if (presentParentGroup != null) {
parentId = presentParentGroup.getId();
}
if (command.isChangeInLongParameterNamed(GroupingTypesApiConstants.centerIdParamName, parentId)) {
final Long newValue = command.longValueOfParameterNamed(GroupingTypesApiConstants.centerIdParamName);
actualChanges.put(GroupingTypesApiConstants.centerIdParamName, newValue);
Group newParentGroup = null;
if (newValue != null) {
newParentGroup = this.groupRepository.findOneWithNotFoundDetection(newValue);
if (!newParentGroup.isOfficeIdentifiedBy(officeId)) {
final String errorMessage = "Group and parent group must have the same office";
throw new InvalidOfficeException("group", "attach.to.parent.group", errorMessage);
}
/*
* If Group is not super parent then validate group level's parent level is same as group
* parent's level this check makes sure new group is added at immediate next level in hierarchy
*/
if (!groupForUpdate.getGroupLevel().isIdentifiedByParentId(newParentGroup.getGroupLevel().getId())) {
final String errorMessage = "Parent group's level is not equal to child level's parent level ";
throw new InvalidGroupLevelException("add", "invalid.level", errorMessage);
}
}
groupForUpdate.setParent(newParentGroup);
// Parent has changed, re-generate 'Hierarchy' as parent is
// changed
groupForUpdate.generateHierarchy();
}
}
/*
* final Set<Client> clientMembers = assembleSetOfClients(officeId, command); List<String> changes =
* groupForUpdate.updateClientMembersIfDifferent(clientMembers); if (!changes.isEmpty()) {
* actualChanges.put(GroupingTypesApiConstants .clientMembersParamName, changes); }
*/
this.groupRepository.saveAndFlush(groupForUpdate);
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withOfficeId(groupForUpdate.officeId()) //
.withGroupId(groupForUpdate.getId()) //
.withEntityId(groupForUpdate.getId()) //
.with(actualChanges) //
.build();
} catch (final JpaSystemException | DataIntegrityViolationException dve) {
handleGroupDataIntegrityIssues(command, dve.getMostSpecificCause(), dve, groupingType);
return CommandProcessingResult.empty();
} catch (final PersistenceException dve) {
Throwable throwable = ExceptionUtils.getRootCause(dve.getCause());
handleGroupDataIntegrityIssues(command, throwable, dve, groupingType);
return CommandProcessingResult.empty();
}
}
@Transactional
@Override
public CommandProcessingResult unassignGroupOrCenterStaff(final Long grouptId, final JsonCommand command) {
this.context.authenticatedUser();
final Map<String, Object> actualChanges = new LinkedHashMap<>(9);
this.fromApiJsonDeserializer.validateForUnassignStaff(command.json());
final Group groupForUpdate = this.groupRepository.findOneWithNotFoundDetection(grouptId);
final Staff presentStaff = groupForUpdate.getStaff();
Long presentStaffId = null;
if (presentStaff == null) {
throw new GroupHasNoStaffException(grouptId);
}
presentStaffId = presentStaff.getId();
final String staffIdParamName = "staffId";
if (!command.isChangeInLongParameterNamed(staffIdParamName, presentStaffId)) {
groupForUpdate.unassignStaff();
}
this.groupRepository.saveAndFlush(groupForUpdate);
actualChanges.put(staffIdParamName, null);
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withOfficeId(groupForUpdate.getId()) //
.withGroupId(groupForUpdate.officeId()) //
.withEntityId(groupForUpdate.getId()) //
.with(actualChanges) //
.build();
}
@Override
public CommandProcessingResult assignGroupOrCenterStaff(final Long groupId, final JsonCommand command) {
this.context.authenticatedUser();
final Map<String, Object> actualChanges = new LinkedHashMap<>(5);
this.fromApiJsonDeserializer.validateForAssignStaff(command.json());
final Group groupForUpdate = this.groupRepository.findOneWithNotFoundDetection(groupId);
Staff staff = null;
final Long staffId = command.longValueOfParameterNamed(GroupingTypesApiConstants.staffIdParamName);
final boolean inheritStaffForClientAccounts = command
.booleanPrimitiveValueOfParameterNamed(GroupingTypesApiConstants.inheritStaffForClientAccounts);
staff = this.staffRepository.findByOfficeHierarchyWithNotFoundDetection(staffId, groupForUpdate.getOffice().getHierarchy());
groupForUpdate.updateStaff(staff);
if (inheritStaffForClientAccounts) {
LocalDate loanOfficerReassignmentDate = DateUtils.getBusinessLocalDate();
/*
* update loan officer for client and update loan officer for clients loans and savings
*/
Set<Client> clients = groupForUpdate.getClientMembers();
if (clients != null) {
for (Client client : clients) {
client.updateStaff(staff);
if (this.loanRepositoryWrapper.doNonClosedLoanAccountsExistForClient(client.getId())) {
for (final Loan loan : this.loanRepositoryWrapper.findLoanByClientId(client.getId())) {
if (loan.isDisbursed() && !loan.isClosed()) {
loan.reassignLoanOfficer(staff, loanOfficerReassignmentDate);
}
}
}
if (this.savingsAccountRepositoryWrapper.doNonClosedSavingAccountsExistForClient(client.getId())) {
for (final SavingsAccount savingsAccount : this.savingsAccountRepositoryWrapper
.findSavingAccountByClientId(client.getId())) {
if (!savingsAccount.isClosed()) {
savingsAccount.reassignSavingsOfficer(staff, loanOfficerReassignmentDate);
}
}
}
}
}
}
this.groupRepository.saveAndFlush(groupForUpdate);
actualChanges.put(GroupingTypesApiConstants.staffIdParamName, staffId);
return new CommandProcessingResultBuilder() //
.withOfficeId(groupForUpdate.officeId()) //
.withEntityId(groupForUpdate.getId()) //
.withGroupId(groupId) //
.with(actualChanges) //
.build();
}
@Transactional
@Override
public CommandProcessingResult deleteGroup(final Long groupId) {
try {
final Group groupForDelete = this.groupRepository.findOneWithNotFoundDetection(groupId);
if (groupForDelete.isNotPending()) {
throw new GroupMustBePendingToBeDeletedException(groupId);
}
final List<Note> relatedNotes = this.noteRepository.findByGroup(groupForDelete);
this.noteRepository.deleteAllInBatch(relatedNotes);
this.groupRepository.delete(groupForDelete);
this.groupRepository.flush();
return new CommandProcessingResultBuilder() //
.withOfficeId(groupForDelete.getId()) //
.withGroupId(groupForDelete.officeId()) //
.withEntityId(groupForDelete.getId()) //
.build();
} catch (final JpaSystemException | DataIntegrityViolationException dve) {
Throwable throwable = ExceptionUtils.getRootCause(dve.getCause());
log.error("Error occured.", throwable);
throw ErrorHandler.getMappable(dve, "error.msg.group.unknown.data.integrity.issue",
"Unknown data integrity issue with resource.");
}
}
@Override
public CommandProcessingResult closeGroup(final Long groupId, final JsonCommand command) {
this.fromApiJsonDeserializer.validateForGroupClose(command);
final Group group = this.groupRepository.findOneWithNotFoundDetection(groupId);
final LocalDate closureDate = command.localDateValueOfParameterNamed(GroupingTypesApiConstants.closureDateParamName);
final Long closureReasonId = command.longValueOfParameterNamed(GroupingTypesApiConstants.closureReasonIdParamName);
final AppUser currentUser = this.context.authenticatedUser();
final CodeValue closureReason = this.codeValueRepository
.findOneByCodeNameAndIdWithNotFoundDetection(GroupingTypesApiConstants.GROUP_CLOSURE_REASON, closureReasonId);
if (group.hasActiveClients()) {
final String errorMessage = group.getGroupLevel().getLevelName()
+ " cannot be closed because of active clients associated with it.";
throw new InvalidGroupStateTransitionException(group.getGroupLevel().getLevelName(), "close", "active.clients.exist",
errorMessage);
}
validateLoansAndSavingsForGroupOrCenterClose(group, closureDate);
entityDatatableChecksWritePlatformService.runTheCheck(groupId, EntityTables.GROUP.getName(), StatusEnum.CLOSE.getCode(),
EntityTables.GROUP.getForeignKeyColumnNameOnDatatable(), null);
group.close(currentUser, closureReason, closureDate);
this.groupRepository.saveAndFlush(group);
return new CommandProcessingResultBuilder() //
.withGroupId(groupId) //
.withEntityId(groupId) //
.build();
}
private void validateLoansAndSavingsForGroupOrCenterClose(final Group groupOrCenter, final LocalDate closureDate) {
final Collection<Loan> groupLoans = this.loanRepositoryWrapper.findByGroupId(groupOrCenter.getId());
for (final Loan loan : groupLoans) {
final LoanStatusMapper loanStatus = new LoanStatusMapper(loan.getStatus().getValue());
if (loanStatus.isOpen()) {
final String errorMessage = groupOrCenter.getGroupLevel().getLevelName() + " cannot be closed because of non-closed loans.";
throw new InvalidGroupStateTransitionException(groupOrCenter.getGroupLevel().getLevelName(), "close", "loan.not.closed",
errorMessage);
} else if (loanStatus.isClosed() && DateUtils.isAfter(loan.getClosedOnDate(), closureDate)) {
final String errorMessage = groupOrCenter.getGroupLevel().getLevelName()
+ "closureDate cannot be before the loan closedOnDate.";
throw new InvalidGroupStateTransitionException(groupOrCenter.getGroupLevel().getLevelName(), "close",
"date.cannot.before.loan.closed.date", errorMessage, closureDate, loan.getClosedOnDate());
} else if (loanStatus.isPendingApproval()) {
final String errorMessage = groupOrCenter.getGroupLevel().getLevelName() + " cannot be closed because of non-closed loans.";
throw new InvalidGroupStateTransitionException(groupOrCenter.getGroupLevel().getLevelName(), "close", "loan.not.closed",
errorMessage);
} else if (loanStatus.isAwaitingDisbursal()) {
final String errorMessage = "Group cannot be closed because of non-closed loans.";
throw new InvalidGroupStateTransitionException(groupOrCenter.getGroupLevel().getLevelName(), "close", "loan.not.closed",
errorMessage);
}
}
final List<SavingsAccount> groupSavingAccounts = this.savingsAccountRepositoryWrapper.findByGroupId(groupOrCenter.getId());
for (final SavingsAccount saving : groupSavingAccounts) {
if (saving.isActive() || saving.isSubmittedAndPendingApproval() || saving.isApproved()) {
final String errorMessage = groupOrCenter.getGroupLevel().getLevelName()
+ " cannot be closed with active savings accounts associated.";
throw new InvalidGroupStateTransitionException(groupOrCenter.getGroupLevel().getLevelName(), "close",
"savings.account.not.closed", errorMessage);
} else if (saving.isClosed() && DateUtils.isAfter(saving.getClosedOnDate(), closureDate)) {
final String errorMessage = groupOrCenter.getGroupLevel().getLevelName()
+ " closureDate cannot be before the loan closedOnDate.";
throw new InvalidGroupStateTransitionException(groupOrCenter.getGroupLevel().getLevelName(), "close",
"date.cannot.before.loan.closed.date", errorMessage, closureDate, saving.getClosedOnDate());
}
}
}
@Override
public CommandProcessingResult closeCenter(final Long centerId, final JsonCommand command) {
this.fromApiJsonDeserializer.validateForCenterClose(command);
final Group center = this.groupRepository.findOneWithNotFoundDetection(centerId);
final LocalDate closureDate = command.localDateValueOfParameterNamed(GroupingTypesApiConstants.closureDateParamName);
final Long closureReasonId = command.longValueOfParameterNamed(GroupingTypesApiConstants.closureReasonIdParamName);
final CodeValue closureReason = this.codeValueRepository
.findOneByCodeNameAndIdWithNotFoundDetection(GroupingTypesApiConstants.CENTER_CLOSURE_REASON, closureReasonId);
final AppUser currentUser = this.context.authenticatedUser();
if (center.hasActiveGroups()) {
final String errorMessage = center.getGroupLevel().getLevelName()
+ " cannot be closed because of active groups associated with it.";
throw new InvalidGroupStateTransitionException(center.getGroupLevel().getLevelName(), "close", "active.groups.exist",
errorMessage);
}
validateLoansAndSavingsForGroupOrCenterClose(center, closureDate);
entityDatatableChecksWritePlatformService.runTheCheck(centerId, EntityTables.GROUP.getName(), StatusEnum.ACTIVATE.getCode(),
EntityTables.GROUP.getForeignKeyColumnNameOnDatatable(), null);
center.close(currentUser, closureReason, closureDate);
this.groupRepository.saveAndFlush(center);
return new CommandProcessingResultBuilder() //
.withEntityId(centerId) //
.build();
}
private Set<Client> assembleSetOfClients(final Long groupOfficeId, final JsonCommand command) {
final Set<Client> clientMembers = new HashSet<>();
final String[] clientMembersArray = command.arrayValueOfParameterNamed(GroupingTypesApiConstants.clientMembersParamName);
if (!ObjectUtils.isEmpty(clientMembersArray)) {
for (final String clientId : clientMembersArray) {
final Long id = Long.valueOf(clientId);
final Client client = this.clientRepositoryWrapper.findOneWithNotFoundDetection(id);
if (!client.isOfficeIdentifiedBy(groupOfficeId)) {
final String errorMessage = "Client with identifier " + clientId + " must have the same office as group.";
throw new InvalidOfficeException("client", "attach.to.group", errorMessage, clientId, groupOfficeId);
}
clientMembers.add(client);
}
}
return clientMembers;
}
private Set<Group> assembleSetOfChildGroups(final Long officeId, final JsonCommand command) {
final Set<Group> childGroups = new HashSet<>();
final String[] childGroupsArray = command.arrayValueOfParameterNamed(GroupingTypesApiConstants.groupMembersParamName);
if (!ObjectUtils.isEmpty(childGroupsArray)) {
for (final String groupId : childGroupsArray) {
final Long id = Long.valueOf(groupId);
final Group group = this.groupRepository.findOneWithNotFoundDetection(id);
if (!group.isOfficeIdentifiedBy(officeId)) {
final String errorMessage = "Group and child groups must have the same office.";
throw new InvalidOfficeException("group", "attach.to.parent.group", errorMessage);
}
childGroups.add(group);
}
}
return childGroups;
}
/*
* Guaranteed to throw an exception no matter what the data integrity issue is.
*/
private void handleGroupDataIntegrityIssues(final JsonCommand command, final Throwable realCause, final Exception dve,
final GroupTypes groupLevel) {
String levelName = "Invalid";
switch (groupLevel) {
case CENTER:
levelName = "Center";
break;
case GROUP:
levelName = "Group";
break;
case INVALID:
break;
}
String errorMessageForUser = null;
String errorMessageForMachine = null;
if (realCause.getMessage().contains("'external_id'")) {
final String externalId = command.stringValueOfParameterNamed(GroupingTypesApiConstants.externalIdParamName);
errorMessageForUser = levelName + " with externalId `" + externalId + "` already exists.";
errorMessageForMachine = "error.msg." + levelName.toLowerCase() + ".duplicate.externalId";
throw new PlatformDataIntegrityException(errorMessageForMachine, errorMessageForUser,
GroupingTypesApiConstants.externalIdParamName, externalId);
} else if (realCause.getMessage().contains("'name'")) {
final String name = command.stringValueOfParameterNamed(GroupingTypesApiConstants.nameParamName);
errorMessageForUser = levelName + " with name `" + name + "` already exists.";
errorMessageForMachine = "error.msg." + levelName.toLowerCase() + ".duplicate.name";
throw new PlatformDataIntegrityException(errorMessageForMachine, errorMessageForUser, GroupingTypesApiConstants.nameParamName,
name);
}
log.error("Error occured.", dve);
throw ErrorHandler.getMappable(dve, "error.msg.group.unknown.data.integrity.issue", "Unknown data integrity issue with resource.");
}
@Override
public CommandProcessingResult associateClientsToGroup(final Long groupId, final JsonCommand command) {
this.fromApiJsonDeserializer.validateForAssociateClients(command.json());
final Group groupForUpdate = this.groupRepository.findOneWithNotFoundDetection(groupId);
final Set<Client> clientMembers = assembleSetOfClients(groupForUpdate.officeId(), command);
final Map<String, Object> actualChanges = new HashMap<>();
final List<String> changes = groupForUpdate.associateClients(clientMembers);
if (groupForUpdate.isGroup()) {
validateGroupRulesBeforeClientAssociation(groupForUpdate);
}
if (!changes.isEmpty()) {
actualChanges.put(GroupingTypesApiConstants.clientMembersParamName, changes);
}
this.groupRepository.saveAndFlush(groupForUpdate);
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withOfficeId(groupForUpdate.officeId()) //
.withGroupId(groupForUpdate.getId()) //
.withEntityId(groupForUpdate.getId()) //
.with(actualChanges) //
.build();
}
@Transactional
@Override
public CommandProcessingResult disassociateClientsFromGroup(final Long groupId, final JsonCommand command) {
this.fromApiJsonDeserializer.validateForDisassociateClients(command.json());
final Group groupForUpdate = this.groupRepository.findOneWithNotFoundDetection(groupId);
final Set<Client> clientMembers = assembleSetOfClients(groupForUpdate.officeId(), command);
// check if any client has got group loans
checkForActiveJLGLoans(groupForUpdate.getId(), clientMembers);
validateForJLGSavings(groupForUpdate.getId(), clientMembers);
final Map<String, Object> actualChanges = new HashMap<>();
final List<String> changes = groupForUpdate.disassociateClients(clientMembers);
if (!changes.isEmpty()) {
actualChanges.put(GroupingTypesApiConstants.clientMembersParamName, changes);
}
this.groupRepository.saveAndFlush(groupForUpdate);
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withOfficeId(groupForUpdate.officeId()) //
.withGroupId(groupForUpdate.getId()) //
.withEntityId(groupForUpdate.getId()) //
.with(actualChanges) //
.build();
}
@Transactional
@Override
public CommandProcessingResult associateGroupsToCenter(final Long centerId, final JsonCommand command) {
this.fromApiJsonDeserializer.validateForAssociateGroups(command.json());
final Group centerForUpdate = this.groupRepository.findOneWithNotFoundDetection(centerId);
final Set<Group> groupMembers = assembleSetOfChildGroups(centerForUpdate.officeId(), command);
checkGroupMembersMeetingSyncWithCenterMeeting(centerId, groupMembers);
final Map<String, Object> actualChanges = new HashMap<>();
final List<String> changes = centerForUpdate.associateGroups(groupMembers);
if (!changes.isEmpty()) {
actualChanges.put(GroupingTypesApiConstants.groupMembersParamName, changes);
}
this.groupRepository.saveAndFlush(centerForUpdate);
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withOfficeId(centerForUpdate.officeId()) //
.withGroupId(centerForUpdate.getId()) //
.withEntityId(centerForUpdate.getId()) //
.with(actualChanges) //
.build();
}
@Transactional
@Override
public CommandProcessingResult disassociateGroupsToCenter(final Long centerId, final JsonCommand command) {
this.fromApiJsonDeserializer.validateForDisassociateGroups(command.json());
final Group centerForUpdate = this.groupRepository.findOneWithNotFoundDetection(centerId);
final Set<Group> groupMembers = assembleSetOfChildGroups(centerForUpdate.officeId(), command);
final Map<String, Object> actualChanges = new HashMap<>();
final List<String> changes = centerForUpdate.disassociateGroups(groupMembers);
if (!changes.isEmpty()) {
actualChanges.put(GroupingTypesApiConstants.clientMembersParamName, changes);
}
this.groupRepository.saveAndFlush(centerForUpdate);
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withOfficeId(centerForUpdate.officeId()) //
.withGroupId(centerForUpdate.getId()) //
.withEntityId(centerForUpdate.getId()) //
.with(actualChanges) //
.build();
}
@Transactional
private void checkForActiveJLGLoans(final Long groupId, final Set<Client> clientMembers) {
for (final Client client : clientMembers) {
final Collection<Loan> loans = this.loanRepositoryWrapper.findActiveLoansByLoanIdAndGroupId(client.getId(), groupId);
if (!CollectionUtils.isEmpty(loans)) {
final String defaultUserMessage = "Client with identifier " + client.getId()
+ " cannot be disassociated it has group loans.";
throw new GroupAccountExistsException("disassociate", "client.has.group.loan", defaultUserMessage, client.getId(), groupId);
}
}
}
@Transactional
private void validateForJLGSavings(final Long groupId, final Set<Client> clientMembers) {
for (final Client client : clientMembers) {
final Collection<SavingsAccount> savings = this.savingsAccountRepositoryWrapper.findByClientIdAndGroupId(client.getId(),
groupId);
if (!CollectionUtils.isEmpty(savings)) {
final String defaultUserMessage = "Client with identifier " + client.getId()
+ " cannot be disassociated it has group savings.";
throw new GroupAccountExistsException("disassociate", "client.has.group.saving", defaultUserMessage, client.getId(),
groupId);
}
}
}
public void validateOfficeOpeningDateisAfterGroupOrCenterOpeningDate(final Office groupOffice, final GroupLevel groupLevel,
final LocalDate activationDate) {
if (activationDate != null && DateUtils.isAfter(groupOffice.getOpeningLocalDate(), activationDate)) {
final String levelName = groupLevel.getLevelName();
final String errorMessage = levelName + " activation date should be greater than or equal to the parent Office's creation date "
+ activationDate.toString();
throw new InvalidGroupStateTransitionException(levelName.toLowerCase(), "activate.date",
"cannot.be.before.office.activation.date", errorMessage, activationDate, groupOffice.getOpeningLocalDate());
}
}
private void checkGroupMembersMeetingSyncWithCenterMeeting(final Long centerId, final Set<Group> groupMembers) {
// Get parent(center) calendar
Calendar ceneterCalendar = null;
final CalendarInstance parentCalendarInstance = this.calendarInstanceRepository.findByEntityIdAndEntityTypeIdAndCalendarTypeId(
centerId, CalendarEntityType.CENTERS.getValue(), CalendarType.COLLECTION.getValue());
if (parentCalendarInstance != null) {
ceneterCalendar = parentCalendarInstance.getCalendar();
}
for (final Group group : groupMembers) {
// Get child(group) calendar
Calendar groupCalendar = null;
final CalendarInstance groupCalendarInstance = this.calendarInstanceRepository.findByEntityIdAndEntityTypeIdAndCalendarTypeId(
group.getId(), CalendarEntityType.GROUPS.getValue(), CalendarType.COLLECTION.getValue());
if (groupCalendarInstance != null) {
groupCalendar = groupCalendarInstance.getCalendar();
}
// Group shouldn't have a meeting when no meeting attached for center
if (ceneterCalendar == null && groupCalendar != null) {
throw new GeneralPlatformDomainRuleException(
"error.msg.center.associating.group.not.allowed.with.meeting.attached.to.group",
"Group with id " + group.getId() + " is already associated with meeting", group.getId());
}
// Group meeting recurrence should match with center meeting recurrence
else if (ceneterCalendar != null && groupCalendar != null) {
if (!ceneterCalendar.getRecurrence().equalsIgnoreCase(groupCalendar.getRecurrence())) {
throw new GeneralPlatformDomainRuleException("error.msg.center.associating.group.not.allowed.with.different.meeting",
"Group with id " + group.getId() + " meeting recurrence doesnot matched with center meeting recurrence",
group.getId());
}
}
}
}
}