/** * 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.group.domain; import java.util.ArrayList; import java.util.Date; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import javax.persistence.CascadeType; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.JoinColumn; import javax.persistence.JoinTable; import javax.persistence.ManyToMany; import javax.persistence.ManyToOne; import javax.persistence.OneToMany; import javax.persistence.Table; import javax.persistence.Temporal; import javax.persistence.TemporalType; import javax.persistence.Transient; import org.apache.commons.lang.ObjectUtils; import org.apache.commons.lang.StringUtils; import org.joda.time.LocalDate; import org.mifosplatform.infrastructure.codes.domain.CodeValue; import org.mifosplatform.infrastructure.core.api.JsonCommand; import org.mifosplatform.infrastructure.core.data.ApiParameterError; import org.mifosplatform.infrastructure.core.exception.GeneralPlatformDomainRuleException; import org.mifosplatform.infrastructure.core.exception.PlatformApiDataValidationException; import org.mifosplatform.infrastructure.core.service.DateUtils; import org.mifosplatform.infrastructure.security.service.RandomPasswordGenerator; import org.mifosplatform.organisation.office.domain.Office; import org.mifosplatform.organisation.staff.domain.Staff; import org.mifosplatform.portfolio.client.domain.Client; import org.mifosplatform.portfolio.group.api.GroupingTypesApiConstants; import org.mifosplatform.portfolio.group.exception.ClientExistInGroupException; import org.mifosplatform.portfolio.group.exception.ClientNotInGroupException; import org.mifosplatform.portfolio.group.exception.GroupExistsInCenterException; import org.mifosplatform.portfolio.group.exception.GroupNotExistsInCenterException; import org.mifosplatform.portfolio.group.exception.InvalidGroupStateTransitionException; import org.mifosplatform.useradministration.domain.AppUser; import org.springframework.data.jpa.domain.AbstractPersistable; @Entity @Table(name = "m_group") public final class Group extends AbstractPersistable<Long> { @Column(name = "external_id", length = 100, unique = true) private String externalId; /** * A value from {@link GroupingTypeStatus}. */ @Column(name = "status_enum", nullable = false) private Integer status; @Column(name = "activation_date", nullable = true) @Temporal(TemporalType.DATE) private Date activationDate; @ManyToOne(optional = true) @JoinColumn(name = "activatedon_userid", nullable = true) private AppUser activatedBy; @ManyToOne @JoinColumn(name = "office_id", nullable = false) private Office office; @ManyToOne @JoinColumn(name = "staff_id", nullable = true) private Staff staff; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "parent_id") private Group parent; @ManyToOne @JoinColumn(name = "level_id", nullable = false) private GroupLevel groupLevel; @Column(name = "display_name", length = 100, unique = true) private String name; @Column(name = "hierarchy", length = 100) private String hierarchy; @OneToMany(fetch = FetchType.EAGER) @JoinColumn(name = "parent_id") private final List<Group> groupMembers = new LinkedList<>(); @ManyToMany @JoinTable(name = "m_group_client", joinColumns = @JoinColumn(name = "group_id"), inverseJoinColumns = @JoinColumn(name = "client_id")) private Set<Client> clientMembers = new HashSet<>(); @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "closure_reason_cv_id", nullable = true) private CodeValue closureReason; @Column(name = "closedon_date", nullable = true) @Temporal(TemporalType.DATE) private Date closureDate; @ManyToOne(optional = true) @JoinColumn(name = "closedon_userid", nullable = true) private AppUser closedBy; @Column(name = "submittedon_date", nullable = true) @Temporal(TemporalType.DATE) private Date submittedOnDate; @ManyToOne(optional = true) @JoinColumn(name = "submittedon_userid", nullable = true) private AppUser submittedBy; @OneToMany(cascade = CascadeType.ALL, mappedBy = "center", orphanRemoval = true) private Set<StaffAssignmentHistory> staffHistory; @Column(name = "account_no", length = 20, unique = true, nullable = false) private String accountNumber; @Transient private boolean accountNumberRequiresAutoGeneration = false; // JPA default constructor for entity protected Group() { this.name = null; this.externalId = null; this.clientMembers = new HashSet<>(); } public static Group newGroup(final Office office, final Staff staff, final Group parent, final GroupLevel groupLevel, final String name, final String externalId, final boolean active, final LocalDate activationDate, final Set<Client> clientMembers, final Set<Group> groupMembers, final LocalDate submittedOnDate, final AppUser currentUser, final String accountNo) { // By default new group is created in PENDING status, unless explicitly // status is set to active GroupingTypeStatus status = GroupingTypeStatus.PENDING; LocalDate groupActivationDate = null; if (active) { status = GroupingTypeStatus.ACTIVE; groupActivationDate = activationDate; } return new Group(office, staff, parent, groupLevel, name, externalId, status, groupActivationDate, clientMembers, groupMembers, submittedOnDate, currentUser, accountNo); } private Group(final Office office, final Staff staff, final Group parent, final GroupLevel groupLevel, final String name, final String externalId, final GroupingTypeStatus status, final LocalDate activationDate, final Set<Client> clientMembers, final Set<Group> groupMembers, final LocalDate submittedOnDate, final AppUser currentUser, final String accountNo) { final List<ApiParameterError> dataValidationErrors = new ArrayList<>(); this.office = office; this.staff = staff; this.groupLevel = groupLevel; this.parent = parent; if (parent != null) { this.parent.addChild(this); } if (StringUtils.isBlank(accountNo)) { this.accountNumber = new RandomPasswordGenerator(19).generate(); this.accountNumberRequiresAutoGeneration = true; } else { this.accountNumber = accountNo; } if (StringUtils.isNotBlank(name)) { this.name = name.trim(); } else { this.name = null; } if (StringUtils.isNotBlank(externalId)) { this.externalId = externalId.trim(); } else { this.externalId = null; } if (groupMembers != null) { this.groupMembers.addAll(groupMembers); } this.submittedOnDate = submittedOnDate.toDate(); this.submittedBy = currentUser; this.staffHistory = null; associateClients(clientMembers); /* * Always keep status change at the bottom, as status change rule * depends on the attribute's value */ setStatus(activationDate, currentUser, status, dataValidationErrors); throwExceptionIfErrors(dataValidationErrors); } private void setStatus(final LocalDate activationDate, final AppUser loginUser, final GroupingTypeStatus status, final List<ApiParameterError> dataValidationErrors) { if (status.isActive()) { activate(loginUser, activationDate, dataValidationErrors); } else { this.status = status.getValue(); } } private void activate(final AppUser currentUser, final LocalDate activationLocalDate, final List<ApiParameterError> dataValidationErrors) { validateStatusNotEqualToActiveAndLogError(dataValidationErrors); if (dataValidationErrors.isEmpty()) { this.status = GroupingTypeStatus.ACTIVE.getValue(); setActivationDate(activationLocalDate.toDate(), currentUser, dataValidationErrors); } } public void activate(final AppUser currentUser, final LocalDate activationLocalDate) { final List<ApiParameterError> dataValidationErrors = new ArrayList<>(); activate(currentUser, activationLocalDate, dataValidationErrors); if (this.isCenter() && this.hasStaff()) { Staff staff = this.getStaff(); this.reassignStaff(staff, activationLocalDate); } throwExceptionIfErrors(dataValidationErrors); } private void setActivationDate(final Date activationDate, final AppUser loginUser, final List<ApiParameterError> dataValidationErrors) { if (activationDate != null) { this.activationDate = activationDate; this.activatedBy = loginUser; } validateActivationDate(dataValidationErrors); } public boolean isActivatedAfter(final LocalDate submittedOn) { return getActivationLocalDate().isAfter(submittedOn); } public boolean isNotActive() { return !isActive(); } public boolean isActive() { return this.status != null ? GroupingTypeStatus.fromInt(this.status).isActive() : false; } private boolean isDateInTheFuture(final LocalDate localDate) { return localDate.isAfter(DateUtils.getLocalDateOfTenant()); } public boolean isNotPending() { return !isPending(); } public boolean isPending() { return GroupingTypeStatus.fromInt(this.status).isPending(); } public Map<String, Object> update(final JsonCommand command) { final Map<String, Object> actualChanges = new LinkedHashMap<>(9); if (command.isChangeInIntegerParameterNamed(GroupingTypesApiConstants.statusParamName, this.status)) { final Integer newValue = command.integerValueOfParameterNamed(GroupingTypesApiConstants.statusParamName); actualChanges.put(GroupingTypesApiConstants.statusParamName, GroupingTypeEnumerations.status(newValue)); this.status = GroupingTypeStatus.fromInt(newValue).getValue(); } if (command.isChangeInStringParameterNamed(GroupingTypesApiConstants.externalIdParamName, this.externalId)) { final String newValue = command.stringValueOfParameterNamed(GroupingTypesApiConstants.externalIdParamName); actualChanges.put(GroupingTypesApiConstants.externalIdParamName, newValue); this.externalId = StringUtils.defaultIfEmpty(newValue, null); } if (command.isChangeInLongParameterNamed(GroupingTypesApiConstants.officeIdParamName, this.office.getId())) { final Long newValue = command.longValueOfParameterNamed(GroupingTypesApiConstants.officeIdParamName); actualChanges.put(GroupingTypesApiConstants.officeIdParamName, newValue); } if (command.isChangeInLongParameterNamed(GroupingTypesApiConstants.staffIdParamName, staffId())) { final Long newValue = command.longValueOfParameterNamed(GroupingTypesApiConstants.staffIdParamName); actualChanges.put(GroupingTypesApiConstants.staffIdParamName, newValue); } if (command.isChangeInStringParameterNamed(GroupingTypesApiConstants.nameParamName, this.name)) { final String newValue = command.stringValueOfParameterNamed(GroupingTypesApiConstants.nameParamName); actualChanges.put(GroupingTypesApiConstants.nameParamName, newValue); this.name = StringUtils.defaultIfEmpty(newValue, null); } final String dateFormatAsInput = command.dateFormat(); final String localeAsInput = command.locale(); if (command.isChangeInLocalDateParameterNamed(GroupingTypesApiConstants.activationDateParamName, getActivationLocalDate())) { final String valueAsInput = command.stringValueOfParameterNamed(GroupingTypesApiConstants.activationDateParamName); actualChanges.put(GroupingTypesApiConstants.activationDateParamName, valueAsInput); actualChanges.put(GroupingTypesApiConstants.dateFormatParamName, dateFormatAsInput); actualChanges.put(GroupingTypesApiConstants.localeParamName, localeAsInput); final LocalDate newValue = command.localDateValueOfParameterNamed(GroupingTypesApiConstants.activationDateParamName); this.activationDate = newValue.toDate(); } if (command.isChangeInStringParameterNamed(GroupingTypesApiConstants.accountNoParamName, this.accountNumber)) { final String newValue = command.stringValueOfParameterNamed(GroupingTypesApiConstants.accountNoParamName); actualChanges.put(GroupingTypesApiConstants.accountNoParamName, newValue); this.accountNumber = StringUtils.defaultIfEmpty(newValue, null); } return actualChanges; } public LocalDate getSubmittedOnDate() { return (LocalDate) ObjectUtils.defaultIfNull(new LocalDate(this.submittedOnDate), null); } public LocalDate getActivationLocalDate() { LocalDate activationLocalDate = null; if (this.activationDate != null) { activationLocalDate = new LocalDate(this.activationDate); } return activationLocalDate; } public List<String> associateClients(final Set<Client> clientMembersSet) { final List<String> differences = new ArrayList<>(); for (final Client client : clientMembersSet) { if (hasClientAsMember(client)) { throw new ClientExistInGroupException(client.getId(), getId()); } this.clientMembers.add(client); differences.add(client.getId().toString()); } return differences; } public List<String> disassociateClients(final Set<Client> clientMembersSet) { final List<String> differences = new ArrayList<>(); for (final Client client : clientMembersSet) { if (hasClientAsMember(client)) { this.clientMembers.remove(client); differences.add(client.getId().toString()); } else { throw new ClientNotInGroupException(client.getId(), getId()); } } return differences; } public boolean hasClientAsMember(final Client client) { return this.clientMembers.contains(client); } public void generateHierarchy() { if (this.parent != null) { this.hierarchy = this.parent.hierarchyOf(getId()); } else { this.hierarchy = "." + getId() + "."; for (Group group : this.groupMembers) { group.setParent(this); group.generateHierarchy(); } } } public void resetHierarchy() { this.hierarchy = "." + this.getId(); } private String hierarchyOf(final Long id) { return this.hierarchy + id.toString() + "."; } public boolean isOfficeIdentifiedBy(final Long officeId) { return this.office.identifiedBy(officeId); } public Long officeId() { return this.office.getId(); } private Long staffId() { Long staffId = null; if (this.staff != null) { staffId = this.staff.getId(); } return staffId; } private void addChild(final Group group) { this.groupMembers.add(group); } public void updateStaff(final Staff staff) { if (this.isCenter() && this.isActive()) { LocalDate updatedDate = DateUtils.getLocalDateOfTenant(); reassignStaff(staff, updatedDate); } this.staff = staff; } public void unassignStaff() { if (this.isCenter() && this.isActive()) { LocalDate dateOfStaffUnassigned = DateUtils.getLocalDateOfTenant(); removeStaff(dateOfStaffUnassigned); } this.staff = null; } public GroupLevel getGroupLevel() { return this.groupLevel; } public Staff getStaff() { return this.staff; } public void setStaff(final Staff staff) { this.staff = staff; } public Group getParent() { return this.parent; } public void setParent(final Group parent) { this.parent = parent; } public Office getOffice() { return this.office; } public boolean isCenter() { return this.groupLevel.isCenter(); } public boolean isGroup() { return this.groupLevel.isGroup(); } public boolean isTransferInProgress() { return GroupingTypeStatus.fromInt(this.status).isTransferInProgress(); } public boolean isTransferOnHold() { return GroupingTypeStatus.fromInt(this.status).isTransferOnHold(); } public boolean isTransferInProgressOrOnHold() { return isTransferInProgress() || isTransferOnHold(); } public boolean isChildClient(final Long clientId) { if (clientId != null && this.clientMembers != null && !this.clientMembers.isEmpty()) { for (final Client client : this.clientMembers) { if (client.getId().equals(clientId)) { return true; } } } return false; } public boolean isChildGroup() { return this.parent == null ? false : true; } public boolean isClosed() { return GroupingTypeStatus.fromInt(this.status).isClosed(); } public void close(final AppUser currentUser, final CodeValue closureReason, final LocalDate closureDate) { if (isClosed()) { final String errorMessage = "Group with identifier " + getId() + " is alread closed."; throw new InvalidGroupStateTransitionException(this.groupLevel.getLevelName(), "close", "already.closed", errorMessage, getId()); } if (isNotPending() && getActivationLocalDate().isAfter(closureDate)) { final String errorMessage = "The Group closure Date " + closureDate + " cannot be before the group Activation Date " + getActivationLocalDate() + "."; throw new InvalidGroupStateTransitionException(this.groupLevel.getLevelName(), "close", "date.cannot.before.group.actvation.date", errorMessage, closureDate, getActivationLocalDate()); } this.closureReason = closureReason; this.closureDate = closureDate.toDate(); this.status = GroupingTypeStatus.CLOSED.getValue(); this.closedBy = currentUser; } public boolean hasActiveClients() { for (final Client client : this.clientMembers) { if (!client.isClosed()) { return true; } } return false; } public boolean hasActiveGroups() { for (final Group group : this.groupMembers) { if (!group.isClosed()) { return true; } } return false; } public boolean hasGroupAsMember(final Group group) { return this.groupMembers.contains(group); } public boolean hasStaff() { if (this.staff != null) { return true; } return false; } public List<String> associateGroups(final Set<Group> groupMembersSet) { final List<String> differences = new ArrayList<>(); for (final Group group : groupMembersSet) { if (group.isCenter()) { final String defaultUserMessage = "Center can not assigned as a child"; throw new GeneralPlatformDomainRuleException("error.msg.center.cannot.be.assigned.as.child", defaultUserMessage, group.getId()); } if (hasGroupAsMember(group)) { throw new GroupExistsInCenterException(getId(), group.getId()); } if (group.isChildGroup()) { final String defaultUserMessage = "Group is already associated with a center"; throw new GeneralPlatformDomainRuleException("error.msg.group.already.associated.with.center", defaultUserMessage, group .getParent().getId(), group.getId()); } this.groupMembers.add(group); differences.add(group.getId().toString()); group.setParent(this); group.generateHierarchy(); } return differences; } public List<String> disassociateGroups(Set<Group> groupMembersSet) { final List<String> differences = new ArrayList<>(); for (final Group group : groupMembersSet) { if (hasGroupAsMember(group)) { this.groupMembers.remove(group); differences.add(group.getId().toString()); group.resetHierarchy(); } else { throw new GroupNotExistsInCenterException(group.getId(), getId()); } } return differences; } public Boolean isGroupsClientCountWithinMinMaxRange(Integer minClients, Integer maxClients) { if (maxClients == null && minClients == null) { return true; } // set minClients or maxClients to 0 if null if (minClients == null) { minClients = 0; } if (maxClients == null) { maxClients = Integer.MAX_VALUE; } Set<Client> activeClientMembers = getActiveClientMembers(); if (activeClientMembers.size() >= minClients && activeClientMembers.size() <= maxClients) { return true; } return false; } public Boolean isGroupsClientCountWithinMaxRange(Integer maxClients) { Set<Client> activeClientMembers = getActiveClientMembers(); if (maxClients == null) { return true; } else if (activeClientMembers.size() <= maxClients) { return true; } else { return false; } } public Set<Client> getActiveClientMembers() { Set<Client> activeClientMembers = new HashSet<>(); for (Client client : this.clientMembers) { if (client.isActive()) { activeClientMembers.add(client); } } return activeClientMembers; } private void validateActivationDate(final List<ApiParameterError> dataValidationErrors) { if (getSubmittedOnDate() != null && isDateInTheFuture(getSubmittedOnDate())) { final String defaultUserMessage = "Submitted on date cannot be in the future."; final String globalisationMessageCode = "error.msg.group.submittedOnDate.in.the.future"; final ApiParameterError error = ApiParameterError.parameterError(globalisationMessageCode, defaultUserMessage, GroupingTypesApiConstants.submittedOnDateParamName, this.submittedOnDate); dataValidationErrors.add(error); } if (getActivationLocalDate() != null && getSubmittedOnDate() != null && getSubmittedOnDate().isAfter(getActivationLocalDate())) { final String defaultUserMessage = "Submitted on date cannot be after the activation date"; final ApiParameterError error = ApiParameterError.parameterError("error.msg.group.submittedOnDate.after.activation.date", defaultUserMessage, GroupingTypesApiConstants.submittedOnDateParamName, this.submittedOnDate); dataValidationErrors.add(error); } if (getActivationLocalDate() != null && isDateInTheFuture(getActivationLocalDate())) { final String defaultUserMessage = "Activation date cannot be in the future."; final ApiParameterError error = ApiParameterError.parameterError("error.msg.group.activationDate.in.the.future", defaultUserMessage, GroupingTypesApiConstants.activationDateParamName, getActivationLocalDate()); dataValidationErrors.add(error); } if (getActivationLocalDate() != null) { if (this.office.isOpeningDateAfter(getActivationLocalDate())) { final String defaultUserMessage = "Activation date cannot be a date before the office opening date."; final ApiParameterError error = ApiParameterError.parameterError( "error.msg.group.activationDate.cannot.be.before.office.activation.date", defaultUserMessage, GroupingTypesApiConstants.activationDateParamName, getActivationLocalDate()); dataValidationErrors.add(error); } } } private void validateStatusNotEqualToActiveAndLogError(final List<ApiParameterError> dataValidationErrors) { if (isActive()) { final String defaultUserMessage = "Cannot activate group. Group is already active."; final String globalisationMessageCode = "error.msg.group.already.active"; final ApiParameterError error = ApiParameterError.parameterError(globalisationMessageCode, defaultUserMessage, GroupingTypesApiConstants.activeParamName, true); dataValidationErrors.add(error); } } private void throwExceptionIfErrors(final List<ApiParameterError> dataValidationErrors) { if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } } public Set<Client> getClientMembers() { return this.clientMembers; } // StaffAssignmentHistory[during center creation] public void captureStaffHistoryDuringCenterCreation(final Staff newStaff, final LocalDate assignmentDate) { if (this.isCenter() && this.isActive() && staff != null) { this.staff = newStaff; final StaffAssignmentHistory staffAssignmentHistory = StaffAssignmentHistory.createNew(this, this.staff, assignmentDate); if (staffAssignmentHistory != null) { staffHistory = new HashSet<>(); this.staffHistory.add(staffAssignmentHistory); } } } // StaffAssignmentHistory[assign staff] public void reassignStaff(final Staff newStaff, final LocalDate assignmentDate) { this.staff = newStaff; final StaffAssignmentHistory staffAssignmentHistory = StaffAssignmentHistory.createNew(this, this.staff, assignmentDate); this.staffHistory.add(staffAssignmentHistory); } // StaffAssignmentHistory[unassign staff] public void removeStaff(final LocalDate unassignDate) { final StaffAssignmentHistory latestHistoryRecord = findLatestIncompleteHistoryRecord(); if (latestHistoryRecord != null) { latestHistoryRecord.updateEndDate(unassignDate); } } private StaffAssignmentHistory findLatestIncompleteHistoryRecord() { StaffAssignmentHistory latestRecordWithNoEndDate = null; for (final StaffAssignmentHistory historyRecord : this.staffHistory) { if (historyRecord.isCurrentRecord()) { latestRecordWithNoEndDate = historyRecord; break; } } return latestRecordWithNoEndDate; } public boolean isAccountNumberRequiresAutoGeneration() { return this.accountNumberRequiresAutoGeneration; } public void setAccountNumberRequiresAutoGeneration(final boolean accountNumberRequiresAutoGeneration) { this.accountNumberRequiresAutoGeneration = accountNumberRequiresAutoGeneration; } public void updateAccountNo(final String accountIdentifier) { this.accountNumber = accountIdentifier; this.accountNumberRequiresAutoGeneration = false; } }