From 839a93cea3ff60408f5b2749fc105557a0e51efa Mon Sep 17 00:00:00 2001 From: nidhiii128 Date: Fri, 19 Jun 2026 21:22:37 +0000 Subject: [PATCH] FINERACT-2291: New command processing - create group --- .../core/config/SecurityConfig.java | 3 + .../group/api/GroupsApiResource.java | 21 +-- .../group/command/GroupCreateCommand.java | 28 ++++ .../portfolio/group/data/DatatableEntry.java | 43 +++++ .../group/data/GroupCreateRequest.java | 88 +++++++++++ .../group/data/GroupCreateResponse.java | 40 +++++ ...er.java => GroupCreateCommandHandler.java} | 38 ++--- .../GroupingTypesWritePlatformService.java | 4 + ...WritePlatformServiceJpaRepositoryImpl.java | 147 ++++++++++++++++++ .../src/main/resources/application.properties | 8 + .../GroupCreateCommandHandlerTest.java | 70 +++++++++ .../resources/ValidationMessages.properties | 12 ++ 12 files changed, 474 insertions(+), 28 deletions(-) create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/group/command/GroupCreateCommand.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/DatatableEntry.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/GroupCreateRequest.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/GroupCreateResponse.java rename fineract-provider/src/main/java/org/apache/fineract/portfolio/group/handler/{CreateGroupCommandHandler.java => GroupCreateCommandHandler.java} (50%) create mode 100644 fineract-provider/src/test/java/org/apache/fineract/portfolio/group/handler/GroupCreateCommandHandlerTest.java diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/SecurityConfig.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/SecurityConfig.java index 27a328d7b8c..e1e1f6cdcd0 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/SecurityConfig.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/SecurityConfig.java @@ -397,6 +397,9 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "UPDATE_HOOK") .requestMatchers(API_MATCHER.matcher(HttpMethod.DELETE, "/api/*/hooks/*")) .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "DELETE_HOOK") + // group + .requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/groups")) + .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "CREATE_GROUP") // template .requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/templates/*")) .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_READ, "READ_TEMPLATE") 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..0b606bef4d0 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 @@ -50,6 +50,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,6 +85,9 @@ 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.GroupCreateCommand; +import org.apache.fineract.portfolio.group.data.GroupCreateRequest; +import org.apache.fineract.portfolio.group.data.GroupCreateResponse; import org.apache.fineract.portfolio.group.data.GroupGeneralData; import org.apache.fineract.portfolio.group.data.GroupRoleData; import org.apache.fineract.portfolio.group.service.CenterReadPlatformService; @@ -131,6 +135,7 @@ public class GroupsApiResource { private final GLIMAccountInfoReadPlatformService glimAccountInfoReadPlatformService; private final GSIMReadPlatformService gsimReadPlatformService; private final SqlValidator sqlValidator; + private final CommandDispatcher dispatcher; @GET @Path("template") @@ -321,15 +326,11 @@ public String retrieveOne(@Context final UriInfo uriInfo, @PathParam("groupId") + "Mandatory Fields: name, officeId, active, activationDate (if active=true)\n\n" + "Optional Fields: externalId, staffId, clientMembers") @RequestBody(required = true, content = @Content(schema = @Schema(implementation = GroupsApiResourceSwagger.PostGroupsRequest.class))) - @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = GroupsApiResourceSwagger.PostGroupsResponse.class))) - public String create(@Parameter(hidden = true) final String apiRequestBodyAsJson) { - - final CommandWrapper commandRequest = new CommandWrapperBuilder() // - .createGroup() // - .withJson(apiRequestBodyAsJson) // - .build(); // - final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(commandRequest); - return toApiJsonSerializer.serialize(result); + @ApiResponse(responseCode = "200", description = "OK") + public GroupCreateResponse create(GroupCreateRequest request) { + var command = new GroupCreateCommand(); + command.setPayload(request); + return dispatcher.dispatch(command).get(); } @POST @@ -339,7 +340,7 @@ 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))) + @ApiResponse(responseCode = "200", description = "OK") public String unassignLoanOfficer(@PathParam("groupId") @Parameter(description = "groupId") final Long groupId, @Parameter(hidden = true) final String apiRequestBodyAsJson) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/command/GroupCreateCommand.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/command/GroupCreateCommand.java new file mode 100644 index 00000000000..886ccee8e28 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/command/GroupCreateCommand.java @@ -0,0 +1,28 @@ +/** + * 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.GroupCreateRequest; + +@Data +@EqualsAndHashCode(callSuper = true) +public class GroupCreateCommand extends Command {} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/DatatableEntry.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/DatatableEntry.java new file mode 100644 index 00000000000..af3989249f1 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/DatatableEntry.java @@ -0,0 +1,43 @@ +/** + * 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.NotBlank; +import java.io.Serial; +import java.io.Serializable; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Builder +@Data +@NoArgsConstructor +@AllArgsConstructor +public class DatatableEntry implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @NotBlank(message = "{org.apache.fineract.portfolio.group.create.assertion.datatable-registered-table-name-required}") + private String registeredTableName; + + private Map data; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/GroupCreateRequest.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/GroupCreateRequest.java new file mode 100644 index 00000000000..8760c1b830a --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/GroupCreateRequest.java @@ -0,0 +1,88 @@ +/** + * 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 com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.validation.Valid; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.Size; +import java.io.Serial; +import java.io.Serializable; +import java.util.List; +import java.util.Set; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.FieldNameConstants; + +@Builder +@Data +@NoArgsConstructor +@AllArgsConstructor +@FieldNameConstants +public class GroupCreateRequest implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @NotBlank(message = "{org.apache.fineract.portfolio.group.create.assertion.name-required}") + @Size(max = 100, message = "{org.apache.fineract.portfolio.group.create.assertion.name-max-length}") + private String name; + + @Size(max = 100, message = "{org.apache.fineract.portfolio.group.create.assertion.external-id-max-length}") + private String externalId; + + private Long centerId; + + @NotNull(message = "{org.apache.fineract.portfolio.group.create.assertion.office-id-required}") + @Positive(message = "{org.apache.fineract.portfolio.group.create.assertion.office-id-positive}") + private Long officeId; + + @Positive(message = "{org.apache.fineract.portfolio.group.create.assertion.staff-id-positive}") + private Long staffId; + + @NotNull(message = "{org.apache.fineract.portfolio.group.create.assertion.active-required}") + private Boolean active; + + private String activationDate; + + private String submittedOnDate; + + private Set clientMembers; + + private String locale; + + private String dateFormat; + + @Valid + private List datatables; + + @JsonIgnore + @AssertTrue(message = "{org.apache.fineract.portfolio.group.create.assertion.activation-date-required-when-active}") + public boolean isActivationDateValidWhenActive() { + if (Boolean.TRUE.equals(active)) { + return activationDate != null && !activationDate.isBlank(); + } + return true; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/GroupCreateResponse.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/GroupCreateResponse.java new file mode 100644 index 00000000000..a19620a505f --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/GroupCreateResponse.java @@ -0,0 +1,40 @@ +/** + * 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.io.Serial; +import java.io.Serializable; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Builder +@Data +@NoArgsConstructor +@AllArgsConstructor +public class GroupCreateResponse implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private Long resourceId; + private Long officeId; + private Long groupId; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/handler/CreateGroupCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/handler/GroupCreateCommandHandler.java similarity index 50% rename from fineract-provider/src/main/java/org/apache/fineract/portfolio/group/handler/CreateGroupCommandHandler.java rename to fineract-provider/src/main/java/org/apache/fineract/portfolio/group/handler/GroupCreateCommandHandler.java index 9cc8400fbbb..1a809c26325 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/handler/CreateGroupCommandHandler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/handler/GroupCreateCommandHandler.java @@ -18,31 +18,33 @@ */ package org.apache.fineract.portfolio.group.handler; -import org.apache.fineract.commands.annotation.CommandType; -import org.apache.fineract.commands.handler.NewCommandSourceHandler; -import org.apache.fineract.infrastructure.core.api.JsonCommand; -import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +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.GroupCreateRequest; +import org.apache.fineract.portfolio.group.data.GroupCreateResponse; import org.apache.fineract.portfolio.group.service.GroupingTypesWritePlatformService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; +import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; -@Service -@CommandType(entity = "GROUP", action = "CREATE") -public class CreateGroupCommandHandler implements NewCommandSourceHandler { +@Slf4j +@Component +@RequiredArgsConstructor +public class GroupCreateCommandHandler implements CommandHandler { - private final GroupingTypesWritePlatformService groupWritePlatformService; + private final GroupingTypesWritePlatformService groupingTypesWritePlatformService; - @Autowired - public CreateGroupCommandHandler(final GroupingTypesWritePlatformService groupWritePlatformService) { - this.groupWritePlatformService = groupWritePlatformService; + @Retry(name = "commandGroupCreate", fallbackMethod = "fallback") + @Override + @Transactional + public GroupCreateResponse handle(Command command) { + return groupingTypesWritePlatformService.createGroup(command.getPayload()); } - @Transactional @Override - public CommandProcessingResult processCommand(final JsonCommand command) { - - final Long centerId = command.longValueOfParameterNamed("centerId"); - return this.groupWritePlatformService.createGroup(centerId, command); + public GroupCreateResponse 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..160391b442e 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,6 +20,8 @@ import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.portfolio.group.data.GroupCreateRequest; +import org.apache.fineract.portfolio.group.data.GroupCreateResponse; public interface GroupingTypesWritePlatformService { @@ -29,6 +31,8 @@ public interface GroupingTypesWritePlatformService { CommandProcessingResult createGroup(Long centerId, JsonCommand command); + GroupCreateResponse createGroup(GroupCreateRequest request); + CommandProcessingResult activateGroupOrCenter(Long entityId, JsonCommand command); CommandProcessingResult updateGroup(Long groupId, 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..5f015fa4717 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 @@ -21,6 +21,7 @@ import jakarta.persistence.PersistenceException; import java.time.LocalDate; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; @@ -69,6 +70,8 @@ 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.GroupCreateRequest; +import org.apache.fineract.portfolio.group.data.GroupCreateResponse; import org.apache.fineract.portfolio.group.domain.Group; import org.apache.fineract.portfolio.group.domain.GroupLevel; import org.apache.fineract.portfolio.group.domain.GroupLevelRepository; @@ -944,4 +947,148 @@ else if (ceneterCalendar != null && groupCalendar != null) { } } } + + @Transactional + @Override + public GroupCreateResponse createGroup(final GroupCreateRequest request) { + try { + final AppUser currentUser = this.context.authenticatedUser(); + final Long centerId = request.getCenterId(); + + Long officeId; + Group parentGroup = null; + if (centerId == null) { + officeId = request.getOfficeId(); + } else { + parentGroup = this.groupRepository.findOneWithNotFoundDetection(centerId); + officeId = parentGroup.officeId(); + } + + final Office groupOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(officeId); + final LocalDate activationDate = parseDate(request.getActivationDate(), request.getDateFormat(), request.getLocale()); + final GroupLevel groupLevel = this.groupLevelRepository.findById(GroupTypes.GROUP.getId()).orElse(null); + + validateOfficeOpeningDateisAfterGroupOrCenterOpeningDate(groupOffice, groupLevel, activationDate); + + Staff staff = null; + final Long staffId = request.getStaffId(); + if (staffId != null) { + staff = this.staffRepository.findByOfficeHierarchyWithNotFoundDetection(staffId, groupOffice.getHierarchy()); + } + + final Set clientMembers = assembleSetOfClients(officeId, request.getClientMembers()); + final Set groupMembers = Collections.emptySet(); + + final boolean active = Boolean.TRUE.equals(request.getActive()); + + LocalDate submittedOnDate = DateUtils.getBusinessLocalDate(); + if (active && activationDate != null && DateUtils.isAfter(submittedOnDate, activationDate)) { + submittedOnDate = activationDate; + } + if (request.getSubmittedOnDate() != null && !request.getSubmittedOnDate().isBlank()) { + submittedOnDate = parseDate(request.getSubmittedOnDate(), request.getDateFormat(), request.getLocale()); + } + + final Group newGroup = Group.newGroup(groupOffice, staff, parentGroup, groupLevel, request.getName(), request.getExternalId(), + active, activationDate, clientMembers, groupMembers, submittedOnDate, currentUser, null); + + boolean rollbackTransaction = false; + if (newGroup.isActive()) { + this.groupRepository.saveAndFlush(newGroup); + if (newGroup.isGroup()) { + validateGroupRulesBeforeActivation(newGroup); + } + final CommandWrapper commandWrapper = new CommandWrapperBuilder().activateGroup(null).build(); + rollbackTransaction = this.commandProcessingService.validateRollbackCommand(commandWrapper, currentUser); + } + + if (newGroup.hasActiveClients()) { + final CommandWrapper commandWrapper = new CommandWrapperBuilder().associateClientsToGroup(newGroup.getId()).build(); + rollbackTransaction = this.commandProcessingService.validateRollbackCommand(commandWrapper, currentUser); + } + + this.groupRepository.save(newGroup); + generateAccountNumber(newGroup); + newGroup.generateHierarchy(); + this.groupRepository.saveAndFlush(newGroup); + newGroup.captureStaffHistoryDuringCenterCreation(staff, activationDate); + + if (request.getDatatables() != null && !request.getDatatables().isEmpty()) { + final com.google.gson.JsonArray datatablesArray = new com.google.gson.JsonArray(); + for (final org.apache.fineract.portfolio.group.data.DatatableEntry entry : request.getDatatables()) { + final com.google.gson.JsonObject entryJson = new com.google.gson.JsonObject(); + entryJson.addProperty("registeredTableName", entry.getRegisteredTableName()); + if (entry.getData() != null) { + entryJson.add("data", new com.google.gson.Gson().toJsonTree(entry.getData())); + } + datatablesArray.add(entryJson); + } + this.entityDatatableChecksWritePlatformService.saveDatatables(StatusEnum.CREATE.getValue(), EntityTables.GROUP.getName(), + newGroup.getId(), null, datatablesArray); + } + + this.entityDatatableChecksWritePlatformService.runTheCheck(newGroup.getId(), EntityTables.GROUP.getName(), + StatusEnum.CREATE.getValue(), EntityTables.GROUP.getForeignKeyColumnNameOnDatatable(), null); + + final CommandProcessingResult result = new CommandProcessingResultBuilder().withOfficeId(groupOffice.getId()) + .withGroupId(newGroup.getId()).withEntityId(newGroup.getId()).setRollbackTransaction(rollbackTransaction).build(); + + businessEventNotifierService.notifyPostBusinessEvent(new GroupsCreateBusinessEvent(result)); + + return GroupCreateResponse.builder().resourceId(newGroup.getId()).officeId(groupOffice.getId()).groupId(newGroup.getId()) + .build(); + + } catch (final JpaSystemException | DataIntegrityViolationException dve) { + handleGroupDataIntegrityIssuesTyped(request.getName(), request.getExternalId(), dve.getMostSpecificCause(), dve, + GroupTypes.GROUP); + return GroupCreateResponse.builder().build(); + } catch (final PersistenceException dve) { + final Throwable throwable = ExceptionUtils.getRootCause(dve.getCause()); + handleGroupDataIntegrityIssuesTyped(request.getName(), request.getExternalId(), throwable, dve, GroupTypes.GROUP); + return GroupCreateResponse.builder().build(); + } + } + + private Set assembleSetOfClients(final Long groupOfficeId, final Set clientMemberIds) { + final Set clientMembers = new HashSet<>(); + if (clientMemberIds == null || clientMemberIds.isEmpty()) { + return clientMembers; + } + for (final Long clientId : clientMemberIds) { + final Client client = this.clientRepositoryWrapper.findOneWithNotFoundDetection(clientId); + if (!client.isOfficeIdentifiedBy(groupOfficeId)) { + final String errorMessage = "Client with identifier " + clientId + " must have the same office as group."; + throw new InvalidOfficeException("client", "attach.to.group", errorMessage, clientId.toString(), groupOfficeId); + } + clientMembers.add(client); + } + return clientMembers; + } + + private void handleGroupDataIntegrityIssuesTyped(final String name, final String externalId, final Throwable realCause, + final Exception dve, final GroupTypes groupingType) { + final String resource = groupingType.equals(GroupTypes.CENTER) ? "center" : "group"; + if (realCause != null && realCause.getMessage() != null) { + if (realCause.getMessage().contains("external_id")) { + throw new PlatformDataIntegrityException("error.msg." + resource + ".duplicate.externalId", + "Group with externalId `" + externalId + "` already exists", "externalId", externalId); + } else if (realCause.getMessage().contains("name")) { + throw new PlatformDataIntegrityException("error.msg." + resource + ".duplicate.name", + "Group with name `" + name + "` already exists", "name", name); + } + } + log.error("Error occured.", dve); + throw new PlatformDataIntegrityException("error.msg." + resource + ".unknown.data.integrity.issue", + "Unknown data integrity issue with resource: " + (realCause != null ? realCause.getMessage() : dve.getMessage())); + } + + private LocalDate parseDate(final String value, final String dateFormat, final String locale) { + if (value == null || value.isBlank()) { + return null; + } + final String pattern = (dateFormat != null && !dateFormat.isBlank()) ? dateFormat : "yyyy-MM-dd"; + final java.util.Locale resolvedLocale = (locale != null && !locale.isBlank()) ? java.util.Locale.forLanguageTag(locale) + : java.util.Locale.ENGLISH; + return LocalDate.parse(value, java.time.format.DateTimeFormatter.ofPattern(pattern, resolvedLocale)); + } } diff --git a/fineract-provider/src/main/resources/application.properties b/fineract-provider/src/main/resources/application.properties index 9523aeb8c91..ce0ab6a4efc 100644 --- a/fineract-provider/src/main/resources/application.properties +++ b/fineract-provider/src/main/resources/application.properties @@ -848,6 +848,14 @@ 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 create + +resilience4j.retry.instances.commandGroupCreate.max-attempts=${FINERACT_COMMAND_GROUP_CREATE_RETRY_MAX_ATTEMPTS:3} +resilience4j.retry.instances.commandGroupCreate.wait-duration=${FINERACT_COMMAND_GROUP_CREATE_RETRY_WAIT_DURATION:1s} +resilience4j.retry.instances.commandGroupCreate.enable-exponential-backoff=${FINERACT_COMMAND_GROUP_CREATE_RETRY_ENABLE_EXPONENTIAL_BACKOFF:true} +resilience4j.retry.instances.commandGroupCreate.exponential-backoff-multiplier=${FINERACT_COMMAND_GROUP_CREATE_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2} +resilience4j.retry.instances.commandGroupCreate.retryExceptions=${FINERACT_COMMAND_GROUP_CREATE_RETRY_EXCEPTIONS:org.springframework.dao.ConcurrencyFailureException,org.eclipse.persistence.exceptions.OptimisticLockException,jakarta.persistence.OptimisticLockException,org.springframework.orm.jpa.JpaOptimisticLockingFailureException} + # 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/GroupCreateCommandHandlerTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/group/handler/GroupCreateCommandHandlerTest.java new file mode 100644 index 00000000000..86cdbb87f8c --- /dev/null +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/group/handler/GroupCreateCommandHandlerTest.java @@ -0,0 +1,70 @@ +/** + * 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.verify; +import static org.mockito.Mockito.when; + +import org.apache.fineract.portfolio.group.command.GroupCreateCommand; +import org.apache.fineract.portfolio.group.data.GroupCreateRequest; +import org.apache.fineract.portfolio.group.data.GroupCreateResponse; +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 GroupCreateCommandHandlerTest { + + @Mock + private GroupingTypesWritePlatformService groupingTypesWritePlatformService; + + @InjectMocks + private GroupCreateCommandHandler underTest; + + @Test + void handle_delegatesToServiceAndReturnsResponse() { + GroupCreateRequest request = GroupCreateRequest.builder() // + .name("Test Group") // + .officeId(1L) // + .active(false) // + .build(); + + GroupCreateResponse expectedResponse = GroupCreateResponse.builder() // + .resourceId(42L) // + .officeId(1L) // + .groupId(42L) // + .build(); + + when(groupingTypesWritePlatformService.createGroup(any(GroupCreateRequest.class))).thenReturn(expectedResponse); + + GroupCreateCommand command = new GroupCreateCommand(); + command.setPayload(request); + + GroupCreateResponse response = underTest.handle(command); + + verify(groupingTypesWritePlatformService).createGroup(request); + assertThat(response.getResourceId()).isEqualTo(42L); + assertThat(response.getOfficeId()).isEqualTo(1L); + } +} diff --git a/fineract-validation/src/main/resources/ValidationMessages.properties b/fineract-validation/src/main/resources/ValidationMessages.properties index c5fd63504a6..e81f694abe7 100644 --- a/fineract-validation/src/main/resources/ValidationMessages.properties +++ b/fineract-validation/src/main/resources/ValidationMessages.properties @@ -135,3 +135,15 @@ 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 + +org.apache.fineract.portfolio.group.create.assertion.name-required=Group name is required. +org.apache.fineract.portfolio.group.create.assertion.name-max-length=Group name cannot exceed 100 characters. +org.apache.fineract.portfolio.group.create.assertion.external-id-max-length=External ID cannot exceed 100 characters. +org.apache.fineract.portfolio.group.create.assertion.office-id-required=Office ID is required. +org.apache.fineract.portfolio.group.create.assertion.office-id-positive=Office ID must be greater than zero. +org.apache.fineract.portfolio.group.create.assertion.staff-id-positive=Staff ID must be greater than zero. +org.apache.fineract.portfolio.group.create.assertion.active-required=Active flag is required. +org.apache.fineract.portfolio.group.create.assertion.activation-date-required-when-active=Activation date is required when the group is created as active. +org.apache.fineract.portfolio.group.create.assertion.datatable-registered-table-name-required=Datatable registered table name is required.