From 6a137535a88cdfcf6c89e4c86a839b09144e7714 Mon Sep 17 00:00:00 2001 From: nidhiii128 Date: Mon, 22 Jun 2026 14:18:19 +0000 Subject: [PATCH] FINERACT-2291: Migrate Group activate, close, assignStaff, unassignStaff endpoints to typed command framework --- .../group/api/GroupsApiResource.java | 89 ++++++--- .../group/command/GroupActivateCommand.java | 29 +++ .../command/GroupAssignStaffCommand.java | 29 +++ .../group/command/GroupCloseCommand.java | 29 +++ .../command/GroupUnassignStaffCommand.java | 29 +++ .../group/data/GroupActivateRequest.java | 42 ++++ .../group/data/GroupActivateResponse.java | 36 ++++ .../group/data/GroupAssignStaffRequest.java | 41 ++++ .../group/data/GroupAssignStaffResponse.java | 38 ++++ .../group/data/GroupCloseRequest.java | 45 +++++ .../group/data/GroupCloseResponse.java | 35 ++++ .../group/data/GroupUnassignStaffRequest.java | 38 ++++ .../data/GroupUnassignStaffResponse.java | 38 ++++ .../handler/GroupActivateCommandHandler.java | 51 +++++ .../GroupAssignStaffCommandHandler.java | 51 +++++ .../handler/GroupCloseCommandHandler.java | 51 +++++ .../GroupUnassignStaffCommandHandler.java | 51 +++++ .../GroupingTypesWritePlatformService.java | 16 ++ ...WritePlatformServiceJpaRepositoryImpl.java | 183 ++++++++++++++++++ .../src/main/resources/application.properties | 17 ++ .../GroupActivateCommandHandlerTest.java | 60 ++++++ .../GroupAssignStaffCommandHandlerTest.java | 61 ++++++ .../handler/GroupCloseCommandHandlerTest.java | 59 ++++++ .../GroupUnassignStaffCommandHandlerTest.java | 60 ++++++ .../resources/ValidationMessages.properties | 10 + 25 files changed, 1162 insertions(+), 26 deletions(-) create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/group/command/GroupActivateCommand.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/group/command/GroupAssignStaffCommand.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/group/command/GroupCloseCommand.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/group/command/GroupUnassignStaffCommand.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/GroupActivateRequest.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/GroupActivateResponse.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/GroupAssignStaffRequest.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/GroupAssignStaffResponse.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/GroupCloseRequest.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/GroupCloseResponse.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/GroupUnassignStaffRequest.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/GroupUnassignStaffResponse.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/group/handler/GroupActivateCommandHandler.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/group/handler/GroupAssignStaffCommandHandler.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/group/handler/GroupCloseCommandHandler.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/group/handler/GroupUnassignStaffCommandHandler.java create mode 100644 fineract-provider/src/test/java/org/apache/fineract/portfolio/group/handler/GroupActivateCommandHandlerTest.java create mode 100644 fineract-provider/src/test/java/org/apache/fineract/portfolio/group/handler/GroupAssignStaffCommandHandlerTest.java create mode 100644 fineract-provider/src/test/java/org/apache/fineract/portfolio/group/handler/GroupCloseCommandHandlerTest.java create mode 100644 fineract-provider/src/test/java/org/apache/fineract/portfolio/group/handler/GroupUnassignStaffCommandHandlerTest.java diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/api/GroupsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/api/GroupsApiResource.java index 72bbbcca8a5..4b109a9d143 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/api/GroupsApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/api/GroupsApiResource.java @@ -18,6 +18,7 @@ */ package org.apache.fineract.portfolio.group.api; +import com.fasterxml.jackson.databind.ObjectMapper; import com.google.gson.JsonElement; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -50,6 +51,7 @@ import java.util.Set; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.StringUtils; +import org.apache.fineract.command.core.CommandDispatcher; import org.apache.fineract.commands.domain.CommandWrapper; import org.apache.fineract.commands.service.CommandWrapperBuilder; import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService; @@ -84,8 +86,20 @@ import org.apache.fineract.portfolio.client.service.ClientReadPlatformService; import org.apache.fineract.portfolio.collectionsheet.data.JLGCollectionSheetData; import org.apache.fineract.portfolio.collectionsheet.service.CollectionSheetReadPlatformService; +import org.apache.fineract.portfolio.group.command.GroupActivateCommand; +import org.apache.fineract.portfolio.group.command.GroupAssignStaffCommand; +import org.apache.fineract.portfolio.group.command.GroupCloseCommand; +import org.apache.fineract.portfolio.group.command.GroupUnassignStaffCommand; +import org.apache.fineract.portfolio.group.data.GroupActivateRequest; +import org.apache.fineract.portfolio.group.data.GroupActivateResponse; +import org.apache.fineract.portfolio.group.data.GroupAssignStaffRequest; +import org.apache.fineract.portfolio.group.data.GroupAssignStaffResponse; +import org.apache.fineract.portfolio.group.data.GroupCloseRequest; +import org.apache.fineract.portfolio.group.data.GroupCloseResponse; import org.apache.fineract.portfolio.group.data.GroupGeneralData; import org.apache.fineract.portfolio.group.data.GroupRoleData; +import org.apache.fineract.portfolio.group.data.GroupUnassignStaffRequest; +import org.apache.fineract.portfolio.group.data.GroupUnassignStaffResponse; import org.apache.fineract.portfolio.group.service.CenterReadPlatformService; import org.apache.fineract.portfolio.group.service.GroupReadPlatformService; import org.apache.fineract.portfolio.group.service.GroupRolesReadPlatformService; @@ -131,6 +145,8 @@ public class GroupsApiResource { private final GLIMAccountInfoReadPlatformService glimAccountInfoReadPlatformService; private final GSIMReadPlatformService gsimReadPlatformService; private final SqlValidator sqlValidator; + private final CommandDispatcher commandDispatcher; + private final ObjectMapper objectMapper; @GET @Path("template") @@ -339,17 +355,15 @@ public String create(@Parameter(hidden = true) final String apiRequestBodyAsJson @Operation(summary = "Unassign a Staff", operationId = "unassignLoanOfficerGroup", description = "Allows you to unassign the Staff.\n\n" + "Mandatory Fields: staffId") @RequestBody(required = true, content = @Content(schema = @Schema(implementation = GroupsApiResourceSwagger.PostGroupsGroupIdCommandUnassignStaffRequest.class))) - @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = GroupsApiResourceSwagger.PostGroupsGroupIdCommandUnassignStaffResponse.class))) public String unassignLoanOfficer(@PathParam("groupId") @Parameter(description = "groupId") final Long groupId, - @Parameter(hidden = true) final String apiRequestBodyAsJson) { - - final CommandWrapper commandRequest = new CommandWrapperBuilder() // - .unassignGroupStaff(groupId) // - .withJson(apiRequestBodyAsJson) // - .build(); // - final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(commandRequest); - return toApiJsonSerializer.serialize(result); - + @Parameter(hidden = true) final String apiRequestBodyAsJson) throws java.io.IOException { + + final GroupUnassignStaffRequest request = objectMapper.readValue(apiRequestBodyAsJson, GroupUnassignStaffRequest.class); + request.setGroupId(groupId); + final var command = new GroupUnassignStaffCommand(); + command.setPayload(request); + final java.util.function.Supplier response = commandDispatcher.dispatch(command); + return toApiJsonSerializer.serialize(response.get()); } @PUT @@ -420,69 +434,92 @@ public String delete(@PathParam("groupId") @Parameter(description = "groupId") f + "Allows you to update the member Role.\n\n" + "Mandatory Fields: role\n\n" + "Showing request/response for Transfer Clients across groups") @RequestBody(required = true, content = @Content(schema = @Schema(implementation = GroupsApiResourceSwagger.PostGroupsGroupIdRequest.class))) - @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = GroupsApiResourceSwagger.PostGroupsGroupIdResponse.class))) public String activateOrGenerateCollectionSheet(@PathParam("groupId") @Parameter(description = "groupId") final Long groupId, @QueryParam("command") @Parameter(description = "command") final String commandParam, @QueryParam("roleId") @Parameter(description = "roleId") final Long roleId, - @Parameter(hidden = true) final String apiRequestBodyAsJson, @Context final UriInfo uriInfo) { - final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(apiRequestBodyAsJson); + @Parameter(hidden = true) final String apiRequestBodyAsJson, @Context final UriInfo uriInfo) throws java.io.IOException { + final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(apiRequestBodyAsJson); CommandProcessingResult result = null; + if (is(commandParam, "activate")) { - final CommandWrapper commandRequest = builder.activateGroup(groupId).build(); - result = commandsSourceWritePlatformService.logCommandSource(commandRequest); - return toApiJsonSerializer.serialize(result); + final GroupActivateRequest request = objectMapper.readValue(apiRequestBodyAsJson, GroupActivateRequest.class); + request.setGroupId(groupId); + final var command = new GroupActivateCommand(); + command.setPayload(request); + final java.util.function.Supplier response = commandDispatcher.dispatch(command); + return toApiJsonSerializer.serialize(response.get()); + } else if (is(commandParam, "associateClients")) { final CommandWrapper commandRequest = builder.associateClientsToGroup(groupId).build(); result = commandsSourceWritePlatformService.logCommandSource(commandRequest); return toApiJsonSerializer.serialize(result); + } else if (is(commandParam, "disassociateClients")) { final CommandWrapper commandRequest = builder.disassociateClientsFromGroup(groupId).build(); result = commandsSourceWritePlatformService.logCommandSource(commandRequest); return toApiJsonSerializer.serialize(result); + } else if (is(commandParam, "generateCollectionSheet")) { final JsonElement parsedQuery = fromJsonHelper.parse(apiRequestBodyAsJson); final JsonQuery query = JsonQuery.from(apiRequestBodyAsJson, parsedQuery, fromJsonHelper); final JLGCollectionSheetData collectionSheet = collectionSheetReadPlatformService.generateGroupCollectionSheet(groupId, query); final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters()); return toApiJsonSerializer.serialize(settings, collectionSheet, GroupingTypesApiConstants.COLLECTIONSHEET_DATA_PARAMETERS); + } else if (is(commandParam, "saveCollectionSheet")) { final CommandWrapper commandRequest = builder.saveGroupCollectionSheet(groupId).build(); result = commandsSourceWritePlatformService.logCommandSource(commandRequest); return toApiJsonSerializer.serialize(result); + } else if (is(commandParam, "unassignStaff")) { - final CommandWrapper commandRequest = builder.unassignGroupStaff(groupId).build(); - result = commandsSourceWritePlatformService.logCommandSource(commandRequest); - return toApiJsonSerializer.serialize(result); + final GroupUnassignStaffRequest request = objectMapper.readValue(apiRequestBodyAsJson, GroupUnassignStaffRequest.class); + request.setGroupId(groupId); + final var command = new GroupUnassignStaffCommand(); + command.setPayload(request); + final java.util.function.Supplier response = commandDispatcher.dispatch(command); + return toApiJsonSerializer.serialize(response.get()); + } else if (is(commandParam, "assignStaff")) { - final CommandWrapper commandRequest = builder.assignGroupStaff(groupId).build(); - result = commandsSourceWritePlatformService.logCommandSource(commandRequest); - return toApiJsonSerializer.serialize(result); + final GroupAssignStaffRequest request = objectMapper.readValue(apiRequestBodyAsJson, GroupAssignStaffRequest.class); + request.setGroupId(groupId); + final var command = new GroupAssignStaffCommand(); + command.setPayload(request); + final java.util.function.Supplier response = commandDispatcher.dispatch(command); + return toApiJsonSerializer.serialize(response.get()); + } else if (is(commandParam, "assignRole")) { final CommandWrapper commandRequest = builder.assignRole(groupId).build(); result = commandsSourceWritePlatformService.logCommandSource(commandRequest); return toApiJsonSerializer.serialize(result); + } else if (is(commandParam, "unassignRole")) { final CommandWrapper commandRequest = builder.unassignRole(groupId, roleId).build(); result = commandsSourceWritePlatformService.logCommandSource(commandRequest); return toApiJsonSerializer.serialize(result); + } else if (is(commandParam, "updateRole")) { final CommandWrapper commandRequest = builder.updateRole(groupId, roleId).build(); result = commandsSourceWritePlatformService.logCommandSource(commandRequest); return toApiJsonSerializer.serialize(result); + } else if (is(commandParam, "transferClients")) { final CommandWrapper commandRequest = builder.transferClientsBetweenGroups(groupId).build(); result = commandsSourceWritePlatformService.logCommandSource(commandRequest); return toApiJsonSerializer.serialize(result); + } else if (is(commandParam, "close")) { - final CommandWrapper commandRequest = builder.closeGroup(groupId).build(); - result = commandsSourceWritePlatformService.logCommandSource(commandRequest); - return toApiJsonSerializer.serialize(result); + final GroupCloseRequest request = objectMapper.readValue(apiRequestBodyAsJson, GroupCloseRequest.class); + request.setGroupId(groupId); + final var command = new GroupCloseCommand(); + command.setPayload(request); + final java.util.function.Supplier response = commandDispatcher.dispatch(command); + return toApiJsonSerializer.serialize(response.get()); + } else { throw new UnrecognizedQueryParamException("command", commandParam, new Object[] { "activate", "generateCollectionSheet", "saveCollectionSheet", "unassignStaff", "assignRole", "unassignRole", "updateassignRole" }); } - } private boolean is(final String commandParam, final String commandValue) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/command/GroupActivateCommand.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/command/GroupActivateCommand.java new file mode 100644 index 00000000000..4b6f8a4c854 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/command/GroupActivateCommand.java @@ -0,0 +1,29 @@ +/** + * 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.command; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.apache.fineract.command.core.Command; +import org.apache.fineract.portfolio.group.data.GroupActivateRequest; + +@Data +@EqualsAndHashCode(callSuper = true) +public class GroupActivateCommand extends Command {} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/command/GroupAssignStaffCommand.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/command/GroupAssignStaffCommand.java new file mode 100644 index 00000000000..d56a24d0524 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/command/GroupAssignStaffCommand.java @@ -0,0 +1,29 @@ +/** + * 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.command; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.apache.fineract.command.core.Command; +import org.apache.fineract.portfolio.group.data.GroupAssignStaffRequest; + +@Data +@EqualsAndHashCode(callSuper = true) +public class GroupAssignStaffCommand extends Command {} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/command/GroupCloseCommand.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/command/GroupCloseCommand.java new file mode 100644 index 00000000000..bc954da4add --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/command/GroupCloseCommand.java @@ -0,0 +1,29 @@ +/** + * 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.command; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.apache.fineract.command.core.Command; +import org.apache.fineract.portfolio.group.data.GroupCloseRequest; + +@Data +@EqualsAndHashCode(callSuper = true) +public class GroupCloseCommand extends Command {} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/command/GroupUnassignStaffCommand.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/command/GroupUnassignStaffCommand.java new file mode 100644 index 00000000000..92d675c0c9e --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/command/GroupUnassignStaffCommand.java @@ -0,0 +1,29 @@ +/** + * 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.command; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.apache.fineract.command.core.Command; +import org.apache.fineract.portfolio.group.data.GroupUnassignStaffRequest; + +@Data +@EqualsAndHashCode(callSuper = true) +public class GroupUnassignStaffCommand extends Command {} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/GroupActivateRequest.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/GroupActivateRequest.java new file mode 100644 index 00000000000..9a3868381f7 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/GroupActivateRequest.java @@ -0,0 +1,42 @@ +/** + * 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.data; + +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class GroupActivateRequest { + + @NotNull + private Long groupId; + + @NotNull + private String activationDate; + + private String locale; + private String dateFormat; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/GroupActivateResponse.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/GroupActivateResponse.java new file mode 100644 index 00000000000..61162763dd6 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/GroupActivateResponse.java @@ -0,0 +1,36 @@ +/** + * 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.data; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class GroupActivateResponse { + + private Long resourceId; + private Long officeId; + private Long groupId; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/GroupAssignStaffRequest.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/GroupAssignStaffRequest.java new file mode 100644 index 00000000000..4029ca4bbae --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/GroupAssignStaffRequest.java @@ -0,0 +1,41 @@ +/** + * 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.data; + +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class GroupAssignStaffRequest { + + @NotNull + private Long groupId; + + @NotNull + private Long staffId; + + private Boolean inheritStaffForClientAccounts; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/GroupAssignStaffResponse.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/GroupAssignStaffResponse.java new file mode 100644 index 00000000000..3930034db83 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/GroupAssignStaffResponse.java @@ -0,0 +1,38 @@ +/** + * 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.data; + +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class GroupAssignStaffResponse { + + private Long resourceId; + private Long officeId; + private Long groupId; + private Map changes; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/GroupCloseRequest.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/GroupCloseRequest.java new file mode 100644 index 00000000000..00141409eb8 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/GroupCloseRequest.java @@ -0,0 +1,45 @@ +/** + * 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.data; + +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class GroupCloseRequest { + + @NotNull + private Long groupId; + + @NotNull + private String closureDate; + + @NotNull + private Long closureReasonId; + + private String locale; + private String dateFormat; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/GroupCloseResponse.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/GroupCloseResponse.java new file mode 100644 index 00000000000..e0d3ad00947 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/GroupCloseResponse.java @@ -0,0 +1,35 @@ +/** + * 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.data; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class GroupCloseResponse { + + private Long resourceId; + private Long groupId; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/GroupUnassignStaffRequest.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/GroupUnassignStaffRequest.java new file mode 100644 index 00000000000..b87ea12a05e --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/GroupUnassignStaffRequest.java @@ -0,0 +1,38 @@ +/** + * 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.data; + +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class GroupUnassignStaffRequest { + + @NotNull + private Long groupId; + + private Long staffId; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/GroupUnassignStaffResponse.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/GroupUnassignStaffResponse.java new file mode 100644 index 00000000000..ff7260ed0c5 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/GroupUnassignStaffResponse.java @@ -0,0 +1,38 @@ +/** + * 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.data; + +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class GroupUnassignStaffResponse { + + private Long resourceId; + private Long officeId; + private Long groupId; + private Map changes; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/handler/GroupActivateCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/handler/GroupActivateCommandHandler.java new file mode 100644 index 00000000000..0c600e81cb3 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/handler/GroupActivateCommandHandler.java @@ -0,0 +1,51 @@ +/** + * 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.handler; + +import io.github.resilience4j.retry.annotation.Retry; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.command.core.Command; +import org.apache.fineract.command.core.CommandHandler; +import org.apache.fineract.portfolio.group.data.GroupActivateRequest; +import org.apache.fineract.portfolio.group.data.GroupActivateResponse; +import org.apache.fineract.portfolio.group.service.GroupingTypesWritePlatformService; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Component +@RequiredArgsConstructor +public class GroupActivateCommandHandler implements CommandHandler { + + private final GroupingTypesWritePlatformService writePlatformService; + + @Retry(name = "commandGroupActivate", fallbackMethod = "fallback") + @Override + @Transactional + public GroupActivateResponse handle(Command command) { + return writePlatformService.activateGroup(command.getPayload()); + } + + @Override + public GroupActivateResponse fallback(Command command, Throwable t) { + return CommandHandler.super.fallback(command, t); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/handler/GroupAssignStaffCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/handler/GroupAssignStaffCommandHandler.java new file mode 100644 index 00000000000..9d721a32ff1 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/handler/GroupAssignStaffCommandHandler.java @@ -0,0 +1,51 @@ +/** + * 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.handler; + +import io.github.resilience4j.retry.annotation.Retry; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.command.core.Command; +import org.apache.fineract.command.core.CommandHandler; +import org.apache.fineract.portfolio.group.data.GroupAssignStaffRequest; +import org.apache.fineract.portfolio.group.data.GroupAssignStaffResponse; +import org.apache.fineract.portfolio.group.service.GroupingTypesWritePlatformService; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Component +@RequiredArgsConstructor +public class GroupAssignStaffCommandHandler implements CommandHandler { + + private final GroupingTypesWritePlatformService writePlatformService; + + @Retry(name = "commandGroupAssignStaff", fallbackMethod = "fallback") + @Override + @Transactional + public GroupAssignStaffResponse handle(Command command) { + return writePlatformService.assignGroupStaff(command.getPayload()); + } + + @Override + public GroupAssignStaffResponse fallback(Command command, Throwable t) { + return CommandHandler.super.fallback(command, t); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/handler/GroupCloseCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/handler/GroupCloseCommandHandler.java new file mode 100644 index 00000000000..a9a25f2c649 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/handler/GroupCloseCommandHandler.java @@ -0,0 +1,51 @@ +/** + * 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.handler; + +import io.github.resilience4j.retry.annotation.Retry; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.command.core.Command; +import org.apache.fineract.command.core.CommandHandler; +import org.apache.fineract.portfolio.group.data.GroupCloseRequest; +import org.apache.fineract.portfolio.group.data.GroupCloseResponse; +import org.apache.fineract.portfolio.group.service.GroupingTypesWritePlatformService; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Component +@RequiredArgsConstructor +public class GroupCloseCommandHandler implements CommandHandler { + + private final GroupingTypesWritePlatformService writePlatformService; + + @Retry(name = "commandGroupClose", fallbackMethod = "fallback") + @Override + @Transactional + public GroupCloseResponse handle(Command command) { + return writePlatformService.closeGroup(command.getPayload()); + } + + @Override + public GroupCloseResponse fallback(Command command, Throwable t) { + return CommandHandler.super.fallback(command, t); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/handler/GroupUnassignStaffCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/handler/GroupUnassignStaffCommandHandler.java new file mode 100644 index 00000000000..e60ccbee9a3 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/handler/GroupUnassignStaffCommandHandler.java @@ -0,0 +1,51 @@ +/** + * 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.handler; + +import io.github.resilience4j.retry.annotation.Retry; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.command.core.Command; +import org.apache.fineract.command.core.CommandHandler; +import org.apache.fineract.portfolio.group.data.GroupUnassignStaffRequest; +import org.apache.fineract.portfolio.group.data.GroupUnassignStaffResponse; +import org.apache.fineract.portfolio.group.service.GroupingTypesWritePlatformService; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Component +@RequiredArgsConstructor +public class GroupUnassignStaffCommandHandler implements CommandHandler { + + private final GroupingTypesWritePlatformService writePlatformService; + + @Retry(name = "commandGroupUnassignStaff", fallbackMethod = "fallback") + @Override + @Transactional + public GroupUnassignStaffResponse handle(Command command) { + return writePlatformService.unassignGroupStaff(command.getPayload()); + } + + @Override + public GroupUnassignStaffResponse fallback(Command command, Throwable t) { + return CommandHandler.super.fallback(command, t); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/service/GroupingTypesWritePlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/service/GroupingTypesWritePlatformService.java index df8931f3e9e..894a38b32cf 100755 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/service/GroupingTypesWritePlatformService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/service/GroupingTypesWritePlatformService.java @@ -20,9 +20,25 @@ import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.portfolio.group.data.GroupActivateRequest; +import org.apache.fineract.portfolio.group.data.GroupActivateResponse; +import org.apache.fineract.portfolio.group.data.GroupAssignStaffRequest; +import org.apache.fineract.portfolio.group.data.GroupAssignStaffResponse; +import org.apache.fineract.portfolio.group.data.GroupCloseRequest; +import org.apache.fineract.portfolio.group.data.GroupCloseResponse; +import org.apache.fineract.portfolio.group.data.GroupUnassignStaffRequest; +import org.apache.fineract.portfolio.group.data.GroupUnassignStaffResponse; public interface GroupingTypesWritePlatformService { + GroupActivateResponse activateGroup(GroupActivateRequest request); + + GroupCloseResponse closeGroup(GroupCloseRequest request); + + GroupAssignStaffResponse assignGroupStaff(GroupAssignStaffRequest request); + + GroupUnassignStaffResponse unassignGroupStaff(GroupUnassignStaffRequest request); + CommandProcessingResult createCenter(JsonCommand command); CommandProcessingResult updateCenter(Long entityId, JsonCommand command); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/service/GroupingTypesWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/service/GroupingTypesWritePlatformServiceJpaRepositoryImpl.java index 830412c52a6..c65d5ab1f78 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/service/GroupingTypesWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/service/GroupingTypesWritePlatformServiceJpaRepositoryImpl.java @@ -69,6 +69,14 @@ 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.data.GroupActivateRequest; +import org.apache.fineract.portfolio.group.data.GroupActivateResponse; +import org.apache.fineract.portfolio.group.data.GroupAssignStaffRequest; +import org.apache.fineract.portfolio.group.data.GroupAssignStaffResponse; +import org.apache.fineract.portfolio.group.data.GroupCloseRequest; +import org.apache.fineract.portfolio.group.data.GroupCloseResponse; +import org.apache.fineract.portfolio.group.data.GroupUnassignStaffRequest; +import org.apache.fineract.portfolio.group.data.GroupUnassignStaffResponse; import org.apache.fineract.portfolio.group.domain.Group; import org.apache.fineract.portfolio.group.domain.GroupLevel; import org.apache.fineract.portfolio.group.domain.GroupLevelRepository; @@ -231,6 +239,181 @@ private CommandProcessingResult createGroupingType(final JsonCommand command, fi } } + @Transactional + @Override + public GroupActivateResponse activateGroup(final GroupActivateRequest request) { + try { + final AppUser currentUser = this.context.authenticatedUser(); + currentUser.validateHasPermissionTo("ACTIVATE_GROUP"); + final Group group = this.groupRepository.findOneWithNotFoundDetection(request.getGroupId()); + + if (group.isGroup()) { + validateGroupRulesBeforeActivation(group); + } + + final LocalDate activationDate = parseDate(request.getActivationDate(), request.getDateFormat(), request.getLocale()); + validateOfficeOpeningDateisAfterGroupOrCenterOpeningDate(group.getOffice(), group.getGroupLevel(), activationDate); + + group.activate(currentUser, activationDate); + this.groupRepository.saveAndFlush(group); + + return GroupActivateResponse.builder().resourceId(group.getId()).officeId(group.officeId()).groupId(group.getId()).build(); + + } catch (final JpaSystemException | DataIntegrityViolationException dve) { + handleGroupDataIntegrityIssuesTyped(null, null, dve.getMostSpecificCause(), dve, GroupTypes.GROUP); + return GroupActivateResponse.builder().build(); + } catch (final PersistenceException dve) { + final Throwable throwable = ExceptionUtils.getRootCause(dve.getCause()); + handleGroupDataIntegrityIssuesTyped(null, null, throwable, dve, GroupTypes.GROUP); + return GroupActivateResponse.builder().build(); + } + } + + @Transactional + @Override + public GroupCloseResponse closeGroup(final GroupCloseRequest request) { + final AppUser currentUser = this.context.authenticatedUser(); + currentUser.validateHasPermissionTo("CLOSE_GROUP"); + final Group group = this.groupRepository.findOneWithNotFoundDetection(request.getGroupId()); + + final LocalDate closureDate = parseDate(request.getClosureDate(), request.getDateFormat(), request.getLocale()); + + final CodeValue closureReason = this.codeValueRepository + .findOneByCodeNameAndIdWithNotFoundDetection(GroupingTypesApiConstants.GROUP_CLOSURE_REASON, request.getClosureReasonId()); + + 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(request.getGroupId(), EntityTables.GROUP.getName(), + StatusEnum.CLOSE.getValue(), EntityTables.GROUP.getForeignKeyColumnNameOnDatatable(), null); + + group.close(currentUser, closureReason, closureDate); + this.groupRepository.saveAndFlush(group); + + return GroupCloseResponse.builder().resourceId(group.getId()).groupId(group.getId()).build(); + } + + @Transactional + @Override + public GroupAssignStaffResponse assignGroupStaff(final GroupAssignStaffRequest request) { + this.context.authenticatedUser().validateHasPermissionTo("ASSIGNSTAFF_GROUP"); + + final Map actualChanges = new LinkedHashMap<>(5); + + final Group groupForUpdate = this.groupRepository.findOneWithNotFoundDetection(request.getGroupId()); + + final Staff staff = this.staffRepository.findByOfficeHierarchyWithNotFoundDetection(request.getStaffId(), + groupForUpdate.getOffice().getHierarchy()); + groupForUpdate.updateStaff(staff); + + if (Boolean.TRUE.equals(request.getInheritStaffForClientAccounts())) { + LocalDate loanOfficerReassignmentDate = DateUtils.getBusinessLocalDate(); + Set 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()) { + loanOfficerService.reassignLoanOfficer(loan, 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, request.getStaffId()); + + return GroupAssignStaffResponse.builder().resourceId(groupForUpdate.getId()).officeId(groupForUpdate.officeId()) + .groupId(request.getGroupId()).changes(actualChanges).build(); + } + + @Transactional + @Override + public GroupUnassignStaffResponse unassignGroupStaff(final GroupUnassignStaffRequest request) { + this.context.authenticatedUser().validateHasPermissionTo("UNASSIGNSTAFF_GROUP"); + + final Map actualChanges = new LinkedHashMap<>(9); + + final Group groupForUpdate = this.groupRepository.findOneWithNotFoundDetection(request.getGroupId()); + final Staff presentStaff = groupForUpdate.getStaff(); + if (presentStaff == null) { + throw new GroupHasNoStaffException(request.getGroupId()); + } + + final Long presentStaffId = presentStaff.getId(); + if (request.getStaffId() != null && request.getStaffId().equals(presentStaffId)) { + groupForUpdate.unassignStaff(); + } + + this.groupRepository.saveAndFlush(groupForUpdate); + + actualChanges.put(GroupingTypesApiConstants.staffIdParamName, null); + + return GroupUnassignStaffResponse.builder().resourceId(groupForUpdate.getId()).officeId(groupForUpdate.officeId()) + .groupId(request.getGroupId()).changes(actualChanges).build(); + } + + private LocalDate parseDate(final String value, final String dateFormat, final String locale) { + if (value == null || value.isBlank()) { + return null; + } + final String fmt = (dateFormat == null || dateFormat.isBlank()) ? "dd MMMM yyyy" : dateFormat; + final java.util.Locale loc = (locale == null || locale.isBlank()) ? java.util.Locale.ENGLISH + : java.util.Locale.forLanguageTag(locale); + try { + return LocalDate.parse(value, java.time.format.DateTimeFormatter.ofPattern(fmt, loc)); + } catch (java.time.format.DateTimeParseException ex) { + throw ErrorHandler.getMappable(ex, "error.msg.invalid.date.format", "Invalid date `" + value + "` with format `" + fmt + "`"); + } + } + + private void handleGroupDataIntegrityIssuesTyped(final String name, final String externalId, 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; + } + + if (realCause.getMessage().contains("'external_id'")) { + final String errorMessageForUser = levelName + " with externalId `" + externalId + "` already exists."; + final String errorMessageForMachine = "error.msg." + levelName.toLowerCase() + ".duplicate.externalId"; + throw new PlatformDataIntegrityException(errorMessageForMachine, errorMessageForUser, + GroupingTypesApiConstants.externalIdParamName, externalId); + } else if (realCause.getMessage().contains("'name'")) { + final String errorMessageForUser = levelName + " with name `" + name + "` already exists."; + final String 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."); + } + private void generateAccountNumber(Group newGroup) { EntityAccountType entityAccountType = null; AccountNumberFormat accountNumberFormat = null; diff --git a/fineract-provider/src/main/resources/application.properties b/fineract-provider/src/main/resources/application.properties index 9523aeb8c91..c90f351acbd 100644 --- a/fineract-provider/src/main/resources/application.properties +++ b/fineract-provider/src/main/resources/application.properties @@ -848,6 +848,23 @@ resilience4j.retry.instances.commandStore.enable-exponential-backoff=${FINERACT_ resilience4j.retry.instances.commandStore.exponential-backoff-multiplier=${FINERACT_COMMAND_STORE_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2} resilience4j.retry.instances.commandStore.retryExceptions=${FINERACT_COMMAND_STORE_RETRY_EXCEPTIONS:org.springframework.dao.ConcurrencyFailureException,org.eclipse.persistence.exceptions.OptimisticLockException,jakarta.persistence.OptimisticLockException,org.springframework.orm.jpa.JpaOptimisticLockingFailureException} +# group +resilience4j.retry.instances.commandGroupActivate.max-attempts=3 +resilience4j.retry.instances.commandGroupActivate.wait-duration=500ms +resilience4j.retry.instances.commandGroupActivate.retry-exceptions=org.springframework.dao.OptimisticLockingFailureException + +resilience4j.retry.instances.commandGroupClose.max-attempts=3 +resilience4j.retry.instances.commandGroupClose.wait-duration=500ms +resilience4j.retry.instances.commandGroupClose.retry-exceptions=org.springframework.dao.OptimisticLockingFailureException + +resilience4j.retry.instances.commandGroupAssignStaff.max-attempts=3 +resilience4j.retry.instances.commandGroupAssignStaff.wait-duration=500ms +resilience4j.retry.instances.commandGroupAssignStaff.retry-exceptions=org.springframework.dao.OptimisticLockingFailureException + +resilience4j.retry.instances.commandGroupUnassignStaff.max-attempts=3 +resilience4j.retry.instances.commandGroupUnassignStaff.wait-duration=500ms +resilience4j.retry.instances.commandGroupUnassignStaff.retry-exceptions=org.springframework.dao.OptimisticLockingFailureException + # command async (WIP) # fineract.command.async.enabled=${FINERACT_COMMAND_ASYNC_ENABLED:false} diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/group/handler/GroupActivateCommandHandlerTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/group/handler/GroupActivateCommandHandlerTest.java new file mode 100644 index 00000000000..6f2b641a9a3 --- /dev/null +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/group/handler/GroupActivateCommandHandlerTest.java @@ -0,0 +1,60 @@ +/** + * 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.handler; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +import org.apache.fineract.portfolio.group.command.GroupActivateCommand; +import org.apache.fineract.portfolio.group.data.GroupActivateRequest; +import org.apache.fineract.portfolio.group.data.GroupActivateResponse; +import org.apache.fineract.portfolio.group.service.GroupingTypesWritePlatformService; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class GroupActivateCommandHandlerTest { + + @Mock + private GroupingTypesWritePlatformService service; + + @InjectMocks + private GroupActivateCommandHandler handler; + + @Test + void shouldActivateGroup() { + GroupActivateRequest request = GroupActivateRequest.builder().groupId(1L).activationDate("01 January 2026").locale("en") + .dateFormat("dd MMMM yyyy").build(); + GroupActivateResponse expected = GroupActivateResponse.builder().resourceId(1L).groupId(1L).officeId(2L).build(); + + when(service.activateGroup(any(GroupActivateRequest.class))).thenReturn(expected); + + GroupActivateCommand command = new GroupActivateCommand(); + command.setPayload(request); + GroupActivateResponse result = handler.handle(command); + + assertThat(result.getGroupId()).isEqualTo(1L); + assertThat(result.getResourceId()).isEqualTo(1L); + } +} diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/group/handler/GroupAssignStaffCommandHandlerTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/group/handler/GroupAssignStaffCommandHandlerTest.java new file mode 100644 index 00000000000..1cb3e70f851 --- /dev/null +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/group/handler/GroupAssignStaffCommandHandlerTest.java @@ -0,0 +1,61 @@ +/** + * 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.handler; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +import java.util.HashMap; +import org.apache.fineract.portfolio.group.command.GroupAssignStaffCommand; +import org.apache.fineract.portfolio.group.data.GroupAssignStaffRequest; +import org.apache.fineract.portfolio.group.data.GroupAssignStaffResponse; +import org.apache.fineract.portfolio.group.service.GroupingTypesWritePlatformService; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class GroupAssignStaffCommandHandlerTest { + + @Mock + private GroupingTypesWritePlatformService service; + + @InjectMocks + private GroupAssignStaffCommandHandler handler; + + @Test + void shouldAssignStaff() { + GroupAssignStaffRequest request = GroupAssignStaffRequest.builder().groupId(1L).staffId(2L).inheritStaffForClientAccounts(true) + .build(); + GroupAssignStaffResponse expected = GroupAssignStaffResponse.builder().resourceId(1L).groupId(1L).officeId(3L) + .changes(new HashMap<>()).build(); + + when(service.assignGroupStaff(any(GroupAssignStaffRequest.class))).thenReturn(expected); + + GroupAssignStaffCommand command = new GroupAssignStaffCommand(); + command.setPayload(request); + GroupAssignStaffResponse result = handler.handle(command); + + assertThat(result.getGroupId()).isEqualTo(1L); + } +} diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/group/handler/GroupCloseCommandHandlerTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/group/handler/GroupCloseCommandHandlerTest.java new file mode 100644 index 00000000000..fb85a3209e1 --- /dev/null +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/group/handler/GroupCloseCommandHandlerTest.java @@ -0,0 +1,59 @@ +/** + * 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.handler; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +import org.apache.fineract.portfolio.group.command.GroupCloseCommand; +import org.apache.fineract.portfolio.group.data.GroupCloseRequest; +import org.apache.fineract.portfolio.group.data.GroupCloseResponse; +import org.apache.fineract.portfolio.group.service.GroupingTypesWritePlatformService; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class GroupCloseCommandHandlerTest { + + @Mock + private GroupingTypesWritePlatformService service; + + @InjectMocks + private GroupCloseCommandHandler handler; + + @Test + void shouldCloseGroup() { + GroupCloseRequest request = GroupCloseRequest.builder().groupId(1L).closureDate("01 January 2026").closureReasonId(5L).locale("en") + .dateFormat("dd MMMM yyyy").build(); + GroupCloseResponse expected = GroupCloseResponse.builder().resourceId(1L).groupId(1L).build(); + + when(service.closeGroup(any(GroupCloseRequest.class))).thenReturn(expected); + + GroupCloseCommand command = new GroupCloseCommand(); + command.setPayload(request); + GroupCloseResponse result = handler.handle(command); + + assertThat(result.getGroupId()).isEqualTo(1L); + } +} diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/group/handler/GroupUnassignStaffCommandHandlerTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/group/handler/GroupUnassignStaffCommandHandlerTest.java new file mode 100644 index 00000000000..3885784a129 --- /dev/null +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/group/handler/GroupUnassignStaffCommandHandlerTest.java @@ -0,0 +1,60 @@ +/** + * 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.handler; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +import java.util.HashMap; +import org.apache.fineract.portfolio.group.command.GroupUnassignStaffCommand; +import org.apache.fineract.portfolio.group.data.GroupUnassignStaffRequest; +import org.apache.fineract.portfolio.group.data.GroupUnassignStaffResponse; +import org.apache.fineract.portfolio.group.service.GroupingTypesWritePlatformService; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class GroupUnassignStaffCommandHandlerTest { + + @Mock + private GroupingTypesWritePlatformService service; + + @InjectMocks + private GroupUnassignStaffCommandHandler handler; + + @Test + void shouldUnassignStaff() { + GroupUnassignStaffRequest request = GroupUnassignStaffRequest.builder().groupId(1L).staffId(2L).build(); + GroupUnassignStaffResponse expected = GroupUnassignStaffResponse.builder().resourceId(1L).groupId(1L).officeId(3L) + .changes(new HashMap<>()).build(); + + when(service.unassignGroupStaff(any(GroupUnassignStaffRequest.class))).thenReturn(expected); + + GroupUnassignStaffCommand command = new GroupUnassignStaffCommand(); + command.setPayload(request); + GroupUnassignStaffResponse result = handler.handle(command); + + assertThat(result.getGroupId()).isEqualTo(1L); + } +} diff --git a/fineract-validation/src/main/resources/ValidationMessages.properties b/fineract-validation/src/main/resources/ValidationMessages.properties index c5fd63504a6..bb9d8cfc8db 100644 --- a/fineract-validation/src/main/resources/ValidationMessages.properties +++ b/fineract-validation/src/main/resources/ValidationMessages.properties @@ -135,3 +135,13 @@ org.apache.fineract.portfolio.meeting.date-format.not-null=The parameter 'dateFo org.apache.fineract.portfolio.meeting.locale.not-null=The parameter 'locale' is mandatory org.apache.fineract.portfolio.meeting.attendance.client-id.not-null=The parameter 'clientId' is mandatory org.apache.fineract.portfolio.meeting.attendance.attendance-type.not-null=The parameter 'attendanceType' is mandatory + +# group +GroupActivateRequest.activationDate.NotNull=activationDate is required +GroupCloseRequest.closureDate.NotNull=closureDate is required +GroupCloseRequest.closureReasonId.NotNull=closureReasonId is required +GroupAssignStaffRequest.staffId.NotNull=staffId is required +GroupActivateRequest.groupId.NotNull=groupId is required +GroupCloseRequest.groupId.NotNull=groupId is required +GroupAssignStaffRequest.groupId.NotNull=groupId is required +GroupUnassignStaffRequest.groupId.NotNull=groupId is required