From cf085c6a8da2bdc33a4826f37900f0ae370216af Mon Sep 17 00:00:00 2001 From: Nayan Date: Tue, 14 Oct 2014 09:55:02 +0530 Subject: [PATCH] Added min/max number of clients per group rule for activate/activating group --- .../portfolio/client/domain/Client.java | 17 +- .../portfolio/group/domain/Group.java | 354 +++++++++++------- .../portfolio/group/domain/GroupLevel.java | 20 +- ...WritePlatformServiceJpaRepositoryImpl.java | 7 +- .../V207__min_max_clients_per_group.sql | 3 + 5 files changed, 259 insertions(+), 142 deletions(-) create mode 100644 mifosng-provider/src/main/resources/sql/migrations/core_db/V207__min_max_clients_per_group.sql 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 5fb4c3cdf..abb9d364a 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 @@ -298,9 +298,12 @@ public final class Client extends AbstractPersistable { final List dataValidationErrors = new ArrayList<>(); validateNameParts(dataValidationErrors); validateActivationDate(dataValidationErrors); + validateClientsGroupRules(dataValidationErrors); + if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } + } - + public boolean isAccountNumberRequiresAutoGeneration() { return this.accountNumberRequiresAutoGeneration; } @@ -568,6 +571,18 @@ public final class Client extends AbstractPersistable { } } } + + /* + * To become a part of a group, group may have set of criteria to be met + * before client can become member of it. This method is placeholder for + * such validation. + */ + + private void validateClientsGroupRules(final List dataValidationErrors) { + for (Group group : this.groups) { + group.validateGroupHasMoreThanRequiredNumberOfClientsAngLogError(dataValidationErrors); + } + } private void deriveDisplayName() { diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/group/domain/Group.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/group/domain/Group.java index 8d80b0279..c92a7dafb 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/group/domain/Group.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/group/domain/Group.java @@ -6,10 +6,8 @@ package org.mifosplatform.portfolio.group.domain; import java.util.ArrayList; -import java.util.Arrays; import java.util.Date; import java.util.HashSet; -import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; @@ -31,7 +29,6 @@ import javax.persistence.TemporalType; import org.apache.commons.lang.ObjectUtils; import org.apache.commons.lang.StringUtils; import org.joda.time.LocalDate; -import org.joda.time.format.DateTimeFormatter; import org.mifosplatform.infrastructure.codes.domain.CodeValue; import org.mifosplatform.infrastructure.core.api.JsonCommand; import org.mifosplatform.infrastructure.core.data.ApiParameterError; @@ -40,7 +37,6 @@ import org.mifosplatform.infrastructure.core.exception.PlatformApiDataValidation import org.mifosplatform.infrastructure.core.service.DateUtils; import org.mifosplatform.organisation.office.domain.Office; import org.mifosplatform.organisation.staff.domain.Staff; -import org.mifosplatform.portfolio.client.api.ClientApiConstants; import org.mifosplatform.portfolio.client.domain.Client; import org.mifosplatform.portfolio.group.api.GroupingTypesApiConstants; import org.mifosplatform.portfolio.group.exception.ClientExistInGroupException; @@ -51,8 +47,6 @@ import org.mifosplatform.portfolio.group.exception.InvalidGroupStateTransitionEx import org.mifosplatform.useradministration.domain.AppUser; import org.springframework.data.jpa.domain.AbstractPersistable; -import com.google.common.collect.Sets; - @Entity @Table(name = "m_group") public final class Group extends AbstractPersistable { @@ -102,7 +96,7 @@ public final class Group extends AbstractPersistable { @ManyToMany @JoinTable(name = "m_group_client", joinColumns = @JoinColumn(name = "group_id"), inverseJoinColumns = @JoinColumn(name = "client_id")) - private Set clientMembers; + private Set clientMembers = new HashSet<>(); @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "closure_reason_cv_id", nullable = true) @@ -123,17 +117,19 @@ public final class Group extends AbstractPersistable { @ManyToOne(optional = true) @JoinColumn(name = "submittedon_userid", nullable = true) private AppUser submittedBy; - - public Group() { + + // 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 clientMembers, final Set groupMembers, final LocalDate submittedOnDate, final AppUser currentUser) { - + + // 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) { @@ -148,6 +144,9 @@ public final class Group extends AbstractPersistable { 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 clientMembers, final Set groupMembers, final LocalDate submittedOnDate, final AppUser currentUser) { + + final List dataValidationErrors = new ArrayList<>(); + this.office = office; this.staff = staff; this.groupLevel = groupLevel; @@ -157,12 +156,6 @@ public final class Group extends AbstractPersistable { this.parent.addChild(this); } - this.status = status.getValue(); - if (activationDate != null) { - this.activationDate = activationDate.toDate(); - this.activatedBy = currentUser; - } - if (StringUtils.isNotBlank(name)) { this.name = name.trim(); } else { @@ -173,9 +166,7 @@ public final class Group extends AbstractPersistable { } else { this.externalId = null; } - if (clientMembers != null) { - this.clientMembers = clientMembers; - } + if (groupMembers != null) { this.groupMembers.addAll(groupMembers); } @@ -183,72 +174,59 @@ public final class Group extends AbstractPersistable { this.submittedOnDate = submittedOnDate.toDate(); this.submittedBy = currentUser; - validate(); + 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 dataValidationErrors) { + + if (status.isActive()) { + activate(loginUser, activationDate, dataValidationErrors); + } + else{ + this.status = status.getValue(); + } + } - private void validate() { + private void activate(final AppUser currentUser, final LocalDate activationLocalDate, final List dataValidationErrors) { + + validateStatusNotEqualToActiveAndLogError(dataValidationErrors); + validateRequiredNumberOfClientPerGroupAngLogError(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 dataValidationErrors = new ArrayList<>(); + activate(currentUser, activationLocalDate, dataValidationErrors); + throwExceptionIfErrors(dataValidationErrors); + + } + + private void setActivationDate(final Date activationDate, final AppUser loginUser, final List dataValidationErrors){ + + if (activationDate != null) { + this.activationDate = activationDate; + this.activatedBy = loginUser; + } + validateActivationDate(dataValidationErrors); - if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } - } - - private void validateActivationDate(final List dataValidationErrors) { - - if (getSubmittedOnDate() != null && isDateInTheFuture(getSubmittedOnDate())) { - - final String defaultUserMessage = "Submitted on date cannot be in the future."; - final ApiParameterError error = ApiParameterError.parameterError("error.msg.group.submittedOnDate.in.the.future", - defaultUserMessage, ClientApiConstants.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); - } - } - } - - public void activate(final AppUser currentUser, final DateTimeFormatter formatter, final LocalDate activationLocalDate) { - if (isActive()) { - final String defaultUserMessage = "Cannot activate group. Group is already active."; - final ApiParameterError error = ApiParameterError.parameterError("error.msg.group.already.active", defaultUserMessage, - "activationDate", activationLocalDate.toString(formatter)); - - final List dataValidationErrors = new ArrayList<>(); - dataValidationErrors.add(error); - - throw new PlatformApiDataValidationException(dataValidationErrors); - } - - this.activationDate = activationLocalDate.toDate(); - this.status = GroupingTypeStatus.ACTIVE.getValue(); - this.activatedBy = currentUser; - - validate(); + } public boolean isActivatedAfter(final LocalDate submittedOn) { @@ -260,7 +238,7 @@ public final class Group extends AbstractPersistable { } public boolean isActive() { - return GroupingTypeStatus.fromInt(this.status).isActive(); + return this.status != null ? GroupingTypeStatus.fromInt(this.status).isActive(): false; } private boolean isDateInTheFuture(final LocalDate localDate) { @@ -334,10 +312,47 @@ public final class Group extends AbstractPersistable { return activationLocalDate; } - public void addClientMember(final Client member) { - this.clientMembers.add(member); + public List associateClients(final Set clientMembersSet) { + + final List dataValidationErrors = new ArrayList<>(); + final List newlyAssociatedClients = associateClients(clientMembersSet, dataValidationErrors); + throwExceptionIfErrors(dataValidationErrors); + return newlyAssociatedClients; + } + public List associateClients(final Set clientMembersSet, final List dataValidationErrors) { + final List 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()); + } + + /* + * Minimum and maximum number of client rule is applicable only for active group + */ + if (isActive()) { + validateGroupHasMoreThanRequiredNumberOfClientsAngLogError(dataValidationErrors); + } + + return differences; + } + + public List disassociateClients(final Set clientMembersSet) { + final List 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); } @@ -382,54 +397,6 @@ public final class Group extends AbstractPersistable { this.staff = null; } - public List updateClientMembersIfDifferent(final Set clientMembersSet) { - List differences = new ArrayList<>(); - if (!clientMembersSet.equals(this.clientMembers)) { - final Set diffClients = Sets.symmetricDifference(clientMembersSet, this.clientMembers); - final String[] diffClientsIds = getClientIds(diffClients); - if (diffClientsIds != null) { - differences = Arrays.asList(diffClientsIds); - } - this.clientMembers = clientMembersSet; - } - - return differences; - } - - public List associateClients(final Set clientMembersSet) { - final List 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 disassociateClients(final Set clientMembersSet) { - final List 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; - } - - private String[] getClientIds(final Set clients) { - final String[] clientIds = new String[clients.size()]; - final Iterator it = clients.iterator(); - for (int i = 0; it.hasNext(); i++) { - clientIds[i] = it.next().getId().toString(); - } - return clientIds; - } - public GroupLevel getGroupLevel() { return this.groupLevel; } @@ -567,4 +534,123 @@ public final class Group extends AbstractPersistable { return differences; } + + public void validateRequiredNumberOfClientPerGroupAngLogError(final List dataValidationErrors) { + + if (!hasGroupRequiredNumberOfClients()) { + + // This validation not strictly related to a parameter, that is the + // reason parameterName is set null + + final String parameterName = null; + final String globalisationMessageCode = "error.msg.group.activation.without.required.number.of.members"; + final String defaultUserMessage = "Group should have number of members between " + this.groupLevel.getMinClients() + " and " + + this.groupLevel.getMaxClients(); + final ApiParameterError error = ApiParameterError.parameterError(globalisationMessageCode, defaultUserMessage, parameterName); + + dataValidationErrors.add(error); + + } + } + + private Boolean hasGroupRequiredNumberOfClients() { + + /* + * If maximum clients required per group is set to zero or less then zero + * then irrespective of minimum required number, below condition returns true + * that means this is rule is not validated + */ + + if (this.clientMembers.size() >= this.groupLevel.getMinClients() + && (this.clientMembers.size() < this.groupLevel.getMaxClients() || this.groupLevel.getMaxClients() <= 0)) { return true; } + return false; + + } + + public void validateGroupHasMoreThanRequiredNumberOfClientsAngLogError(final List dataValidationErrors) { + + if (hasGroupMoreThanRequiredNumberOfClients()) { + + // This validation not strictly related to a parameter, that is the + // reason parameterName is set null + + final String parameterName = null; + final String globalisationMessageCode = "error.msg.group.can.not.have.more.than.required.number.of.members"; + final String defaultUserMessage = "Group should have number of members between " + this.groupLevel.getMinClients() + " and " + + this.groupLevel.getMaxClients(); + final ApiParameterError error = ApiParameterError.parameterError(globalisationMessageCode, defaultUserMessage, parameterName); + + dataValidationErrors.add(error); + + } + } + + private Boolean hasGroupMoreThanRequiredNumberOfClients(){ + /* + * If maximum clients required per group is set to zero or less then zero + * then below condition returns always false + * that means this is rule is not validated and group can have unlimited number of clients. + */ + + if (this.clientMembers.size() > this.groupLevel.getMaxClients() && this.groupLevel.getMaxClients() > 0) { return true; } + return false; + + } + + private void validateActivationDate(final List 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 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 dataValidationErrors) { + if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } + } + } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/group/domain/GroupLevel.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/group/domain/GroupLevel.java index 64b9e54de..280246aa5 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/group/domain/GroupLevel.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/group/domain/GroupLevel.java @@ -30,6 +30,12 @@ public class GroupLevel extends AbstractPersistable { @Column(name = "can_have_clients", nullable = false) private boolean canHaveClients = false; + @Column(name = "min_clients", nullable = false) + private final Long minClients; + + @Column(name = "max_clients", nullable = false) + private final Long maxClients; + public GroupLevel() { this.parentId = null; @@ -37,17 +43,21 @@ public class GroupLevel extends AbstractPersistable { this.levelName = null; this.recursable = false; this.canHaveClients = false; + this.minClients = null; + this.maxClients = null; } public GroupLevel(final Long parentId, final boolean isSuperParent, final String levelName, final boolean recursable, - final boolean canHaveClients) { + final boolean canHaveClients, final Long minClients, final Long maxClients) { this.superParent = isSuperParent; this.parentId = parentId; this.levelName = levelName; this.recursable = recursable; this.canHaveClients = canHaveClients; + this.minClients = minClients; + this.maxClients = maxClients; } @@ -78,4 +88,12 @@ public class GroupLevel extends AbstractPersistable { public boolean isCenter() { return this.levelName.equalsIgnoreCase("Center"); } + + public Long getMinClients(){ + return this.minClients; + } + + public Long getMaxClients(){ + return this.maxClients; + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/group/service/GroupingTypesWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/group/service/GroupingTypesWritePlatformServiceJpaRepositoryImpl.java index 08ad471dd..f013a56c1 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/group/service/GroupingTypesWritePlatformServiceJpaRepositoryImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/group/service/GroupingTypesWritePlatformServiceJpaRepositoryImpl.java @@ -10,13 +10,10 @@ import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.Set; import org.joda.time.LocalDate; -import org.joda.time.format.DateTimeFormat; -import org.joda.time.format.DateTimeFormatter; import org.mifosplatform.commands.domain.CommandWrapper; import org.mifosplatform.commands.service.CommandProcessingService; import org.mifosplatform.commands.service.CommandWrapperBuilder; @@ -229,12 +226,10 @@ public class GroupingTypesWritePlatformServiceJpaRepositoryImpl implements Group final Group group = this.groupRepository.findOneWithNotFoundDetection(groupId); - final Locale locale = command.extractLocale(); - final DateTimeFormatter fmt = DateTimeFormat.forPattern(command.dateFormat()).withLocale(locale); final LocalDate activationDate = command.localDateValueOfParameterNamed("activationDate"); validateOfficeOpeningDateisAfterGroupOrCenterOpeningDate(group.getOffice(), group.getGroupLevel(), activationDate); - group.activate(currentUser, fmt, activationDate); + group.activate(currentUser, activationDate); this.groupRepository.saveAndFlush(group); diff --git a/mifosng-provider/src/main/resources/sql/migrations/core_db/V207__min_max_clients_per_group.sql b/mifosng-provider/src/main/resources/sql/migrations/core_db/V207__min_max_clients_per_group.sql new file mode 100644 index 000000000..cd21a1160 --- /dev/null +++ b/mifosng-provider/src/main/resources/sql/migrations/core_db/V207__min_max_clients_per_group.sql @@ -0,0 +1,3 @@ +ALTER TABLE `m_group_level` + ADD COLUMN `min_clients` INT(11) NOT NULL DEFAULT '0' AFTER `can_have_clients`, + ADD COLUMN `max_clients` INT(11) NOT NULL DEFAULT '0' AFTER `min_clients`; \ No newline at end of file