blob: 325416dc8aa94f47d946bbf4862af9fd25a71f43 [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.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.apache.fineract.infrastructure.codes.domain.CodeValue;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.security.service.RandomPasswordGenerator;
import org.apache.fineract.organisation.office.domain.Office;
import org.apache.fineract.organisation.staff.domain.Staff;
import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.portfolio.group.api.GroupingTypesApiConstants;
import org.apache.fineract.portfolio.group.exception.ClientExistInGroupException;
import org.apache.fineract.portfolio.group.exception.ClientNotInGroupException;
import org.apache.fineract.portfolio.group.exception.GroupExistsInCenterException;
import org.apache.fineract.portfolio.group.exception.GroupNotExistsInCenterException;
import org.apache.fineract.portfolio.group.exception.InvalidGroupStateTransitionException;
import org.apache.fineract.useradministration.domain.AppUser;
import org.joda.time.LocalDate;
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;
}
}