From f69df1466e2f4193b5c276f519fec3ec041fcf90 Mon Sep 17 00:00:00 2001 From: nidhiii128 Date: Fri, 19 Jun 2026 21:22:37 +0000 Subject: [PATCH 1/2] FINERACT-2291: New command processing - create group --- .../service/CommandWrapperBuilder.java | 7 - .../group/GroupImportHandler.java | 72 ++++++--- .../core/config/SecurityConfig.java | 4 + ...tyDatatableChecksWritePlatformService.java | 3 + ...tatableChecksWritePlatformServiceImpl.java | 37 +++++ .../group/GroupsCreateBusinessEvent.java | 18 ++- .../NotificationDomainServiceImpl.java | 7 +- .../group/api/GroupsApiResource.java | 19 ++- .../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} | 32 ++-- .../GroupingTypesWritePlatformService.java | 4 +- ...WritePlatformServiceJpaRepositoryImpl.java | 143 +++++++++++++++--- .../src/main/resources/application.properties | 8 + .../GroupCreateCommandHandlerTest.java | 70 +++++++++ .../resources/ValidationMessages.properties | 12 ++ 18 files changed, 564 insertions(+), 71 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} (52%) create mode 100644 fineract-provider/src/test/java/org/apache/fineract/portfolio/group/handler/GroupCreateCommandHandlerTest.java diff --git a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java index 35f5ec13ac7..cd85459190f 100644 --- a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java +++ b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java @@ -2245,13 +2245,6 @@ public CommandWrapperBuilder deleteCalendar(final String supportedEntityType, fi return this; } - public CommandWrapperBuilder createGroup() { - this.actionName = ACTION_CREATE; - this.entityName = ENTITY_GROUP; - this.href = "/groups/template"; - return this; - } - public CommandWrapperBuilder updateGroup(final Long groupId) { this.actionName = ACTION_UPDATE; this.entityName = ENTITY_GROUP; diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/importhandler/group/GroupImportHandler.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/importhandler/group/GroupImportHandler.java index 227da531c7a..db0d754963f 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/importhandler/group/GroupImportHandler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/importhandler/group/GroupImportHandler.java @@ -18,14 +18,15 @@ */ package org.apache.fineract.infrastructure.bulkimport.importhandler.group; -import com.google.common.reflect.TypeToken; import com.google.gson.GsonBuilder; -import java.lang.reflect.Type; import java.time.LocalDate; +import java.time.format.DateTimeFormatter; import java.util.ArrayList; -import java.util.Collection; +import java.util.HashSet; import java.util.List; import java.util.Objects; +import java.util.Set; +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.IdempotencyKeyGenerator; @@ -35,14 +36,17 @@ import org.apache.fineract.infrastructure.bulkimport.data.Count; import org.apache.fineract.infrastructure.bulkimport.importhandler.ImportHandler; import org.apache.fineract.infrastructure.bulkimport.importhandler.ImportHandlerUtils; -import org.apache.fineract.infrastructure.bulkimport.importhandler.helper.ClientIdSerializer; import org.apache.fineract.infrastructure.bulkimport.importhandler.helper.DateSerializer; import org.apache.fineract.infrastructure.bulkimport.importhandler.helper.EnumOptionDataValueSerializer; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; import org.apache.fineract.infrastructure.core.data.EnumOptionData; import org.apache.fineract.infrastructure.core.serialization.GoogleGsonSerializerHelper; import org.apache.fineract.portfolio.calendar.data.CalendarData; import org.apache.fineract.portfolio.client.data.ClientData; +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.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.IndexedColors; @@ -61,12 +65,14 @@ public class GroupImportHandler implements ImportHandler { private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; private final IdempotencyKeyGenerator idempotencyKeyGenerator; + private final CommandDispatcher dispatcher; @Autowired public GroupImportHandler(final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService, - IdempotencyKeyGenerator idempotencyKeyGenerator) { + final IdempotencyKeyGenerator idempotencyKeyGenerator, final CommandDispatcher dispatcher) { this.commandsSourceWritePlatformService = commandsSourceWritePlatformService; this.idempotencyKeyGenerator = idempotencyKeyGenerator; + this.dispatcher = dispatcher; } @Override @@ -271,18 +277,51 @@ private Integer importGroupMeeting(final List meetings, CommandPro } private CommandProcessingResult importGroup(final List groups, final int rowIndex, final String dateFormat) { - GsonBuilder gsonBuilder = GoogleGsonSerializerHelper.createGsonBuilder(); - gsonBuilder.registerTypeAdapter(LocalDate.class, new DateSerializer(dateFormat, groups.get(rowIndex).getLocale())); - Type clientCollectionType = new TypeToken>() { + GroupGeneralData groupData = groups.get(rowIndex); - }.getType(); - gsonBuilder.registerTypeAdapter(clientCollectionType, new ClientIdSerializer()); - String payload = gsonBuilder.create().toJson(groups.get(rowIndex)); - final CommandWrapper commandRequest = new CommandWrapperBuilder() // - .createGroup() // - .withJson(payload) // - .build(); // - return commandsSourceWritePlatformService.logCommandSource(commandRequest); + Set clientMemberIds = new HashSet<>(); + if (groupData.getClientMembers() != null) { + for (ClientData client : groupData.getClientMembers()) { + if (client.getId() != null) { + clientMemberIds.add(client.getId()); + } + } + } + + String activationDateStr = formatDate(groupData.getActivationDate(), dateFormat); + String submittedOnDateStr = formatDate(groupData.getSubmittedOnDate(), dateFormat); + + GroupCreateRequest request = GroupCreateRequest.builder() // + .name(groupData.getName()) // + .officeId(groupData.getOfficeId()) // + .staffId(groupData.getStaffId()) // + .centerId(groupData.getCenterId()) // + .externalId(groupData.getExternalId()) // + .active(groupData.getActive()) // + .activationDate(activationDateStr) // + .submittedOnDate(submittedOnDateStr) // + .clientMembers(clientMemberIds.isEmpty() ? null : clientMemberIds) // + .locale(groupData.getLocale()) // + .dateFormat(dateFormat) // + .build(); + + GroupCreateCommand command = new GroupCreateCommand(); + command.setPayload(request); + GroupCreateResponse response = dispatcher.dispatch(command).get(); + + return new CommandProcessingResultBuilder() // + .withOfficeId(response.getOfficeId()) // + .withGroupId(response.getGroupId()) // + .withEntityId(response.getResourceId()) // + .build(); + } + + private String formatDate(final LocalDate date, final String dateFormat) { + if (date == null) { + return null; + } + String pattern = (dateFormat != null && !dateFormat.isBlank()) ? dateFormat : "yyyy-MM-dd"; + return date.format(DateTimeFormatter.ofPattern(pattern)); } private int getProgressLevel(String status) { @@ -293,5 +332,4 @@ private int getProgressLevel(String status) { } return 0; } - } 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 8fb1cc9a895..8c4177d0c10 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 @@ -386,6 +386,10 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/standinginstructionrunhistory")) .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_READ, "READ_STANDINGINSTRUCTION") + // group + .requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/groups")) + .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "CREATE_GROUP") + .requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/twofactor/validate")).fullyAuthenticated() .requestMatchers(API_MATCHER.matcher("/api/*/twofactor")).fullyAuthenticated() .requestMatchers(API_MATCHER.matcher("/api/**")).access(allOfRequestManagers(authorizationManagers)); diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/EntityDatatableChecksWritePlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/EntityDatatableChecksWritePlatformService.java index d690214c753..420b84dfdf0 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/EntityDatatableChecksWritePlatformService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/EntityDatatableChecksWritePlatformService.java @@ -34,4 +34,7 @@ public interface EntityDatatableChecksWritePlatformService { boolean saveDatatables(Integer status, String entity, Long entityId, Long productId, JsonArray data); + boolean saveDatatables(Integer status, String entity, Long entityId, Long productId, + java.util.List datatableEntries); + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/EntityDatatableChecksWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/EntityDatatableChecksWritePlatformServiceImpl.java index 9902463031a..e6897d9ccec 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/EntityDatatableChecksWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/EntityDatatableChecksWritePlatformServiceImpl.java @@ -234,6 +234,43 @@ public boolean saveDatatables(final Integer status, final String entity, final L return isMakerCheckerEnabled; } + @Transactional + @Override + public boolean saveDatatables(final Integer status, final String entity, final Long entityId, final Long productId, + final java.util.List datatableEntries) { + if (datatableEntries == null || datatableEntries.isEmpty()) { + return false; + } + final AppUser user = this.context.authenticatedUser(); + boolean isMakerCheckerEnabled = false; + for (org.apache.fineract.portfolio.group.data.DatatableEntry entry : datatableEntries) { + final String datatableName = entry.getRegisteredTableName(); + if (datatableName == null || entry.getData() == null) { + final ApiParameterError error = ApiParameterError.generalError( + "registeredTableName.and.data.parameters.must.be.present.in.each.list.items.in.datatables", + "registeredTableName and data parameters must be present in each list items in datatables"); + List errors = new ArrayList<>(); + errors.add(error); + throw new PlatformApiDataValidationException(errors); + } + final String taskPermissionName = "CREATE_" + datatableName; + user.validateHasPermissionTo(taskPermissionName); + if (this.configurationDomainService.isMakerCheckerEnabledForTask(taskPermissionName)) { + isMakerCheckerEnabled = true; + } + try { + final String dataAsJson = new com.google.gson.Gson().toJson(entry.getData()); + datatableWriteService.createNewDatatableEntry(datatableName, entityId, dataAsJson); + } catch (PlatformApiDataValidationException e) { + for (ApiParameterError error : e.getErrors()) { + error.setParameterName("datatables." + datatableName + "." + error.getParameterName()); + } + throw e; + } + } + return isMakerCheckerEnabled; + } + @Transactional @Override public CommandProcessingResult deleteCheck(final Long entityDatatableCheckId) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/domain/group/GroupsCreateBusinessEvent.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/domain/group/GroupsCreateBusinessEvent.java index d81a3520360..815679275c9 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/domain/group/GroupsCreateBusinessEvent.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/domain/group/GroupsCreateBusinessEvent.java @@ -18,18 +18,30 @@ */ package org.apache.fineract.infrastructure.event.business.domain.group; -import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.event.business.domain.AbstractBusinessEvent; +import org.apache.fineract.portfolio.group.domain.Group; -public class GroupsCreateBusinessEvent extends GroupsBusinessEvent { +public class GroupsCreateBusinessEvent extends AbstractBusinessEvent { + private static final String CATEGORY = "Group"; private static final String TYPE = "GroupsCreateBusinessEvent"; - public GroupsCreateBusinessEvent(CommandProcessingResult value) { + public GroupsCreateBusinessEvent(Group value) { super(value); } + @Override + public String getCategory() { + return CATEGORY; + } + @Override public String getType() { return TYPE; } + + @Override + public Long getAggregateRootId() { + return get().getId(); + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/notification/service/NotificationDomainServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/notification/service/NotificationDomainServiceImpl.java index c10f8717f9c..ec289194e23 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/notification/service/NotificationDomainServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/notification/service/NotificationDomainServiceImpl.java @@ -46,6 +46,7 @@ import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.portfolio.client.domain.Client; +import org.apache.fineract.portfolio.group.domain.Group; import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct; @@ -120,9 +121,9 @@ private final class GroupCreatedListener implements BusinessEventListenerdispatch(command).get(); } @POST @@ -348,7 +348,6 @@ public String create(@Parameter(hidden = true) final String apiRequestBodyAsJson @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) // 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 52% 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 62a8e5fb144..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,27 +18,33 @@ */ package org.apache.fineract.portfolio.group.handler; +import io.github.resilience4j.retry.annotation.Retry; import lombok.RequiredArgsConstructor; -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 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.stereotype.Service; +import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; -@Service -@CommandType(entity = "GROUP", action = "CREATE") +@Slf4j +@Component @RequiredArgsConstructor -public class CreateGroupCommandHandler implements NewCommandSourceHandler { +public class GroupCreateCommandHandler implements CommandHandler { - private final GroupingTypesWritePlatformService groupWritePlatformService; + private final GroupingTypesWritePlatformService groupingTypesWritePlatformService; - @Transactional + @Retry(name = "commandGroupCreate", fallbackMethod = "fallback") @Override - public CommandProcessingResult processCommand(final JsonCommand command) { + @Transactional + public GroupCreateResponse handle(Command command) { + return groupingTypesWritePlatformService.createGroup(command.getPayload()); + } - final Long centerId = command.longValueOfParameterNamed("centerId"); - return this.groupWritePlatformService.createGroup(centerId, command); + @Override + 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..3849ee6fc4e 100644 --- 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 { @@ -27,7 +29,7 @@ public interface GroupingTypesWritePlatformService { CommandProcessingResult updateCenter(Long entityId, JsonCommand command); - CommandProcessingResult createGroup(Long centerId, JsonCommand command); + GroupCreateResponse createGroup(GroupCreateRequest request); CommandProcessingResult activateGroupOrCenter(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..86082f93b54 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; @@ -260,23 +263,6 @@ public CommandProcessingResult createCenter(final JsonCommand command) { return commandProcessingResult; } - @Transactional - @Override - public CommandProcessingResult createGroup(final Long centerId, final JsonCommand command) { - - if (centerId != null) { - this.fromApiJsonDeserializer.validateForCreateCenterGroup(command); - } else { - this.fromApiJsonDeserializer.validateForCreateGroup(command); - } - - CommandProcessingResult commandProcessingResult = createGroupingType(command, GroupTypes.GROUP, centerId); - - businessEventNotifierService.notifyPostBusinessEvent(new GroupsCreateBusinessEvent(commandProcessingResult)); - - return commandProcessingResult; - } - @Transactional @Override public CommandProcessingResult activateGroupOrCenter(final Long groupId, final JsonCommand command) { @@ -944,4 +930,127 @@ else if (ceneterCalendar != null && groupCalendar != null) { } } } + + @Transactional + @Override + public GroupCreateResponse createGroup(final GroupCreateRequest request) { + try { + 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, null, null); + + if (newGroup.isActive()) { + this.groupRepository.saveAndFlush(newGroup); + if (newGroup.isGroup()) { + validateGroupRulesBeforeActivation(newGroup); + } + } + + this.groupRepository.save(newGroup); + generateAccountNumber(newGroup); + newGroup.generateHierarchy(); + this.groupRepository.saveAndFlush(newGroup); + newGroup.captureStaffHistoryDuringCenterCreation(staff, activationDate); + + if (request.getDatatables() != null && !request.getDatatables().isEmpty()) { + this.entityDatatableChecksWritePlatformService.saveDatatables(StatusEnum.CREATE.getValue(), EntityTables.GROUP.getName(), + newGroup.getId(), null, request.getDatatables()); + } + + this.entityDatatableChecksWritePlatformService.runTheCheck(newGroup.getId(), EntityTables.GROUP.getName(), + StatusEnum.CREATE.getValue(), EntityTables.GROUP.getForeignKeyColumnNameOnDatatable(), null); + + businessEventNotifierService.notifyPostBusinessEvent(new GroupsCreateBusinessEvent(newGroup)); + + 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 4872a4358b6..62c68d4be12 100644 --- a/fineract-provider/src/main/resources/application.properties +++ b/fineract-provider/src/main/resources/application.properties @@ -869,6 +869,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. From 9f929fac776709afc2685dd616d598050cc59f4a Mon Sep 17 00:00:00 2001 From: nidhiii128 Date: Mon, 22 Jun 2026 11:05:03 +0000 Subject: [PATCH 2/2] FINERACT-2291: Migrate Update and Delete --- .../service/CommandWrapperBuilder.java | 9 -- .../core/config/SecurityConfig.java | 5 ++ .../group/api/GroupsApiResource.java | 53 ++++++----- .../group/command/GroupCloseCommand.java | 28 ++++++ .../group/command/GroupDeleteCommand.java | 28 ++++++ .../group/command/GroupUpdateCommand.java | 28 ++++++ .../group/data/GroupCloseRequest.java | 54 +++++++++++ .../group/data/GroupCloseResponse.java | 40 +++++++++ .../group/data/GroupDeleteRequest.java | 40 +++++++++ .../group/data/GroupDeleteResponse.java | 40 +++++++++ .../group/data/GroupUpdateRequest.java | 79 ++++++++++++++++ .../group/data/GroupUpdateResponse.java | 42 +++++++++ .../handler/DeleteCenterCommandHandler.java | 13 ++- ...ler.java => GroupCloseCommandHandler.java} | 27 +++--- ...er.java => GroupDeleteCommandHandler.java} | 31 ++++--- ...er.java => GroupUpdateCommandHandler.java} | 30 ++++--- .../GroupingTypesWritePlatformService.java | 18 ++-- ...WritePlatformServiceJpaRepositoryImpl.java | 89 ++++++++++++++----- .../src/main/resources/application.properties | 23 +++++ .../GroupDeleteCommandHandlerTest.java | 63 +++++++++++++ .../GroupUpdateCommandHandlerTest.java | 66 ++++++++++++++ .../resources/ValidationMessages.properties | 11 ++- .../feign/helpers/FeignGroupHelper.java | 24 ++--- 23 files changed, 735 insertions(+), 106 deletions(-) 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/GroupDeleteCommand.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/group/command/GroupUpdateCommand.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/GroupDeleteRequest.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/GroupDeleteResponse.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/GroupUpdateRequest.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/GroupUpdateResponse.java rename fineract-provider/src/main/java/org/apache/fineract/portfolio/group/handler/{CloseGroupCommandHandler.java => GroupCloseCommandHandler.java} (57%) rename fineract-provider/src/main/java/org/apache/fineract/portfolio/group/handler/{UpdateGroupCommandHandler.java => GroupDeleteCommandHandler.java} (52%) rename fineract-provider/src/main/java/org/apache/fineract/portfolio/group/handler/{DeleteGroupCommandHandler.java => GroupUpdateCommandHandler.java} (52%) create mode 100644 fineract-provider/src/test/java/org/apache/fineract/portfolio/group/handler/GroupDeleteCommandHandlerTest.java create mode 100644 fineract-provider/src/test/java/org/apache/fineract/portfolio/group/handler/GroupUpdateCommandHandlerTest.java diff --git a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java index cd85459190f..7c4c40f94de 100644 --- a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java +++ b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java @@ -2333,15 +2333,6 @@ public CommandWrapperBuilder assignGroupStaff(final Long groupId) { return this; } - public CommandWrapperBuilder closeGroup(final Long groupId) { - this.actionName = ACTION_CLOSE; - this.entityName = ENTITY_GROUP; - this.entityId = groupId; - this.groupId = groupId; - this.href = "/groups/" + groupId + "?command=close"; - return this; - } - public CommandWrapperBuilder createCollateral(final Long loanId) { this.actionName = ACTION_CREATE; this.entityName = ENTITY_COLLATERAL; 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 8c4177d0c10..0cd3661c111 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 @@ -370,6 +370,7 @@ 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") + // template .requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/templates/*")) .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_READ, "READ_TEMPLATE") @@ -389,6 +390,10 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { // group .requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/groups")) .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "CREATE_GROUP") + .requestMatchers(API_MATCHER.matcher(HttpMethod.PUT, "/api/*/groups/*")) + .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "UPDATE_GROUP") + .requestMatchers(API_MATCHER.matcher(HttpMethod.DELETE, "/api/*/groups/*")) + .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "DELETE_GROUP") .requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/twofactor/validate")).fullyAuthenticated() .requestMatchers(API_MATCHER.matcher("/api/*/twofactor")).fullyAuthenticated() 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 0855c1dda00..2bbdb998083 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 @@ -86,11 +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.GroupCloseCommand; import org.apache.fineract.portfolio.group.command.GroupCreateCommand; +import org.apache.fineract.portfolio.group.command.GroupDeleteCommand; +import org.apache.fineract.portfolio.group.command.GroupUpdateCommand; +import org.apache.fineract.portfolio.group.data.GroupCloseRequest; +import org.apache.fineract.portfolio.group.data.GroupCloseResponse; import org.apache.fineract.portfolio.group.data.GroupCreateRequest; import org.apache.fineract.portfolio.group.data.GroupCreateResponse; +import org.apache.fineract.portfolio.group.data.GroupDeleteRequest; +import org.apache.fineract.portfolio.group.data.GroupDeleteResponse; import org.apache.fineract.portfolio.group.data.GroupGeneralData; import org.apache.fineract.portfolio.group.data.GroupRoleData; +import org.apache.fineract.portfolio.group.data.GroupUpdateRequest; +import org.apache.fineract.portfolio.group.data.GroupUpdateResponse; import org.apache.fineract.portfolio.group.service.CenterReadPlatformService; import org.apache.fineract.portfolio.group.service.GroupReadPlatformService; import org.apache.fineract.portfolio.group.service.GroupRolesReadPlatformService; @@ -363,17 +372,11 @@ public String unassignLoanOfficer(@PathParam("groupId") @Parameter(description = @Produces({ MediaType.APPLICATION_JSON }) @Operation(summary = "Update a Group", operationId = "updateGroup", description = "Updates a Group") @AlternativeOperationId("update_13") - @RequestBody(required = true, content = @Content(schema = @Schema(implementation = GroupsApiResourceSwagger.PutGroupsGroupIdRequest.class))) - @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = GroupsApiResourceSwagger.PutGroupsGroupIdResponse.class))) - public String update(@PathParam("groupId") @Parameter(description = "groupId") final Long groupId, - @Parameter(hidden = true) final String apiRequestBodyAsJson) { - - final CommandWrapper commandRequest = new CommandWrapperBuilder() // - .updateGroup(groupId) // - .withJson(apiRequestBodyAsJson) // - .build(); // - final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(commandRequest); - return toApiJsonSerializer.serialize(result); + public GroupUpdateResponse update(@PathParam("groupId") final Long groupId, GroupUpdateRequest request) { + request.setGroupId(groupId); + var command = new GroupUpdateCommand(); + command.setPayload(request); + return dispatcher.dispatch(command).get(); } @DELETE @@ -381,14 +384,24 @@ public String update(@PathParam("groupId") @Parameter(description = "groupId") f @Produces({ MediaType.APPLICATION_JSON }) @Operation(summary = "Delete a Group", operationId = "deleteGroup", description = "A group can be deleted if it is in pending state and has no associations - clients, loans or savings") @AlternativeOperationId("delete_11") - @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = GroupsApiResourceSwagger.DeleteGroupsGroupIdResponse.class))) - public String delete(@PathParam("groupId") @Parameter(description = "groupId") final Long groupId) { + public GroupDeleteResponse delete(@PathParam("groupId") final Long groupId) { + final GroupDeleteRequest request = GroupDeleteRequest.builder().groupId(groupId).build(); + var command = new GroupDeleteCommand(); + command.setPayload(request); + return dispatcher.dispatch(command).get(); + } - final CommandWrapper commandRequest = new CommandWrapperBuilder() // - .deleteGroup(groupId) // - .build(); // - final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(commandRequest); - return toApiJsonSerializer.serialize(result); + @POST + @Path("{groupId}/close") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Close a Group", operationId = "closeGroup", description = "Close a Group that is not in pending state and has no non-closed associations") + @AlternativeOperationId("closeGroup_typed") + public GroupCloseResponse close(@PathParam("groupId") final Long groupId, GroupCloseRequest request) { + request.setGroupId(groupId); + final GroupCloseCommand command = new GroupCloseCommand(); + command.setPayload(request); + return dispatcher.dispatch(command).get(); } @POST @@ -482,10 +495,6 @@ public String activateOrGenerateCollectionSheet(@PathParam("groupId") @Parameter 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); } else { throw new UnrecognizedQueryParamException("command", commandParam, new Object[] { "activate", "generateCollectionSheet", "saveCollectionSheet", "unassignStaff", "assignRole", "unassignRole", "updateassignRole" }); 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..c67d7cce45c --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/command/GroupCloseCommand.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.GroupCloseRequest; + +@Data +@EqualsAndHashCode(callSuper = true) +public class GroupCloseCommand extends Command {} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/command/GroupDeleteCommand.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/command/GroupDeleteCommand.java new file mode 100644 index 00000000000..b3c2fdd8259 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/command/GroupDeleteCommand.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.GroupDeleteRequest; + +@Data +@EqualsAndHashCode(callSuper = true) +public class GroupDeleteCommand extends Command {} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/command/GroupUpdateCommand.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/command/GroupUpdateCommand.java new file mode 100644 index 00000000000..819ad04e7d3 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/command/GroupUpdateCommand.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.GroupUpdateRequest; + +@Data +@EqualsAndHashCode(callSuper = true) +public class GroupUpdateCommand extends Command {} 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..08a1586d8da --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/GroupCloseRequest.java @@ -0,0 +1,54 @@ +/** + * 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.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +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 GroupCloseRequest implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @JsonIgnore + private Long groupId; + + @NotBlank(message = "{org.apache.fineract.portfolio.group.close.assertion.closure-date-required}") + private String closureDate; + + @NotNull(message = "{org.apache.fineract.portfolio.group.close.assertion.closure-reason-id-required}") + @Positive(message = "{org.apache.fineract.portfolio.group.close.assertion.closure-reason-id-positive}") + 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..6e257c3a441 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/GroupCloseResponse.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 GroupCloseResponse implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private Long resourceId; + + private Long groupId; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/GroupDeleteRequest.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/GroupDeleteRequest.java new file mode 100644 index 00000000000..ac465aea6c2 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/GroupDeleteRequest.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 jakarta.validation.constraints.NotNull; +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 GroupDeleteRequest implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @NotNull(message = "{org.apache.fineract.portfolio.group.delete.assertion.group-id-required}") + private Long groupId; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/GroupDeleteResponse.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/GroupDeleteResponse.java new file mode 100644 index 00000000000..54caaee0abf --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/GroupDeleteResponse.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 GroupDeleteResponse 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/data/GroupUpdateRequest.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/GroupUpdateRequest.java new file mode 100644 index 00000000000..3b69e970585 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/GroupUpdateRequest.java @@ -0,0 +1,79 @@ +/** + * 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.constraints.AssertTrue; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.Size; +import java.io.Serial; +import java.io.Serializable; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.FieldNameConstants; + +@Builder +@Data +@NoArgsConstructor +@AllArgsConstructor +@FieldNameConstants +public class GroupUpdateRequest implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private Long groupId; + + @NotBlank(message = "{org.apache.fineract.portfolio.group.update.assertion.name-required}") + @Size(max = 100, message = "{org.apache.fineract.portfolio.group.update.assertion.name-max-length}") + private String name; + + @Size(max = 100, message = "{org.apache.fineract.portfolio.group.update.assertion.external-id-max-length}") + private String externalId; + + @Positive(message = "{org.apache.fineract.portfolio.group.update.assertion.office-id-positive}") + private Long officeId; + + @Positive(message = "{org.apache.fineract.portfolio.group.update.assertion.staff-id-positive}") + private Long staffId; + + private Long centerId; + + private Boolean active; + + private String activationDate; + + private String submittedOnDate; + + private String locale; + + private String dateFormat; + + @JsonIgnore + @AssertTrue(message = "{org.apache.fineract.portfolio.group.update.assertion.activation-date-required-when-active-or-present}") + public boolean isActivationDateValid() { + 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/GroupUpdateResponse.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/GroupUpdateResponse.java new file mode 100644 index 00000000000..ec936d52ee1 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/GroupUpdateResponse.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 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 GroupUpdateResponse implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + 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/DeleteCenterCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/handler/DeleteCenterCommandHandler.java index e6915363456..e0d91e9a54c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/handler/DeleteCenterCommandHandler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/handler/DeleteCenterCommandHandler.java @@ -23,6 +23,9 @@ import org.apache.fineract.commands.handler.NewCommandSourceHandler; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; +import org.apache.fineract.portfolio.group.data.GroupDeleteRequest; +import org.apache.fineract.portfolio.group.data.GroupDeleteResponse; import org.apache.fineract.portfolio.group.service.GroupingTypesWritePlatformService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -37,6 +40,14 @@ public class DeleteCenterCommandHandler implements NewCommandSourceHandler { @Transactional @Override public CommandProcessingResult processCommand(final JsonCommand command) { - return this.groupWritePlatformService.deleteGroup(command.entityId()); + + final GroupDeleteResponse response = this.groupWritePlatformService + .deleteGroup(GroupDeleteRequest.builder().groupId(command.entityId()).build()); + + return new CommandProcessingResultBuilder() // + .withCommandId(command.commandId()) // + .withEntityId(response.getGroupId()) // + .withOfficeId(response.getOfficeId()) // + .build(); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/handler/CloseGroupCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/handler/GroupCloseCommandHandler.java similarity index 57% rename from fineract-provider/src/main/java/org/apache/fineract/portfolio/group/handler/CloseGroupCommandHandler.java rename to fineract-provider/src/main/java/org/apache/fineract/portfolio/group/handler/GroupCloseCommandHandler.java index ad5885e56d8..a5886f4a446 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/handler/CloseGroupCommandHandler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/handler/GroupCloseCommandHandler.java @@ -18,26 +18,33 @@ */ package org.apache.fineract.portfolio.group.handler; +import io.github.resilience4j.retry.annotation.Retry; import lombok.RequiredArgsConstructor; -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 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.Service; +import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; -@Service -@CommandType(entity = "GROUP", action = "CLOSE") +@Slf4j +@Component @RequiredArgsConstructor -public class CloseGroupCommandHandler implements NewCommandSourceHandler { +public class GroupCloseCommandHandler implements CommandHandler { private final GroupingTypesWritePlatformService groupingTypesWritePlatformService; + @Retry(name = "commandGroupClose", fallbackMethod = "fallback") @Override @Transactional - public CommandProcessingResult processCommand(final JsonCommand command) { - return this.groupingTypesWritePlatformService.closeGroup(command.entityId(), command); + public GroupCloseResponse handle(Command command) { + return groupingTypesWritePlatformService.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/UpdateGroupCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/handler/GroupDeleteCommandHandler.java similarity index 52% rename from fineract-provider/src/main/java/org/apache/fineract/portfolio/group/handler/UpdateGroupCommandHandler.java rename to fineract-provider/src/main/java/org/apache/fineract/portfolio/group/handler/GroupDeleteCommandHandler.java index ac2b1b9f8ec..6522af0bcc2 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/handler/UpdateGroupCommandHandler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/handler/GroupDeleteCommandHandler.java @@ -18,26 +18,33 @@ */ package org.apache.fineract.portfolio.group.handler; +import io.github.resilience4j.retry.annotation.Retry; import lombok.RequiredArgsConstructor; -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 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.GroupDeleteRequest; +import org.apache.fineract.portfolio.group.data.GroupDeleteResponse; import org.apache.fineract.portfolio.group.service.GroupingTypesWritePlatformService; -import org.springframework.stereotype.Service; +import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; -@Service -@CommandType(entity = "GROUP", action = "UPDATE") +@Slf4j +@Component @RequiredArgsConstructor -public class UpdateGroupCommandHandler implements NewCommandSourceHandler { +public class GroupDeleteCommandHandler implements CommandHandler { - private final GroupingTypesWritePlatformService groupWritePlatformService; + private final GroupingTypesWritePlatformService groupingTypesWritePlatformService; - @Transactional + @Retry(name = "commandGroupDelete", fallbackMethod = "fallback") @Override - public CommandProcessingResult processCommand(final JsonCommand command) { + @Transactional + public GroupDeleteResponse handle(Command command) { + return groupingTypesWritePlatformService.deleteGroup(command.getPayload()); + } - return this.groupWritePlatformService.updateGroup(command.entityId(), command); + @Override + public GroupDeleteResponse 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/DeleteGroupCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/handler/GroupUpdateCommandHandler.java similarity index 52% rename from fineract-provider/src/main/java/org/apache/fineract/portfolio/group/handler/DeleteGroupCommandHandler.java rename to fineract-provider/src/main/java/org/apache/fineract/portfolio/group/handler/GroupUpdateCommandHandler.java index 4a30787a43f..c466720ca42 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/handler/DeleteGroupCommandHandler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/handler/GroupUpdateCommandHandler.java @@ -18,25 +18,33 @@ */ package org.apache.fineract.portfolio.group.handler; +import io.github.resilience4j.retry.annotation.Retry; import lombok.RequiredArgsConstructor; -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 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.GroupUpdateRequest; +import org.apache.fineract.portfolio.group.data.GroupUpdateResponse; import org.apache.fineract.portfolio.group.service.GroupingTypesWritePlatformService; -import org.springframework.stereotype.Service; +import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; -@Service -@CommandType(entity = "GROUP", action = "DELETE") +@Slf4j +@Component @RequiredArgsConstructor -public class DeleteGroupCommandHandler implements NewCommandSourceHandler { +public class GroupUpdateCommandHandler implements CommandHandler { - private final GroupingTypesWritePlatformService groupWritePlatformService; + private final GroupingTypesWritePlatformService groupingTypesWritePlatformService; + @Retry(name = "commandGroupUpdate", fallbackMethod = "fallback") + @Override @Transactional + public GroupUpdateResponse handle(Command command) { + return groupingTypesWritePlatformService.updateGroup(command.getPayload()); + } + @Override - public CommandProcessingResult processCommand(final JsonCommand command) { - return this.groupWritePlatformService.deleteGroup(command.entityId()); + public GroupUpdateResponse 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 3849ee6fc4e..425367d9b1f 100644 --- 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,8 +20,14 @@ import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.portfolio.group.data.GroupCloseRequest; +import org.apache.fineract.portfolio.group.data.GroupCloseResponse; import org.apache.fineract.portfolio.group.data.GroupCreateRequest; import org.apache.fineract.portfolio.group.data.GroupCreateResponse; +import org.apache.fineract.portfolio.group.data.GroupDeleteRequest; +import org.apache.fineract.portfolio.group.data.GroupDeleteResponse; +import org.apache.fineract.portfolio.group.data.GroupUpdateRequest; +import org.apache.fineract.portfolio.group.data.GroupUpdateResponse; public interface GroupingTypesWritePlatformService { @@ -33,12 +39,6 @@ public interface GroupingTypesWritePlatformService { CommandProcessingResult activateGroupOrCenter(Long entityId, JsonCommand command); - CommandProcessingResult updateGroup(Long groupId, JsonCommand command); - - CommandProcessingResult deleteGroup(Long groupId); - - CommandProcessingResult closeGroup(Long groupId, JsonCommand command); - CommandProcessingResult closeCenter(Long centerId, JsonCommand command); CommandProcessingResult unassignGroupOrCenterStaff(Long groupId, JsonCommand command); @@ -52,4 +52,10 @@ public interface GroupingTypesWritePlatformService { CommandProcessingResult associateGroupsToCenter(Long centerId, JsonCommand command); CommandProcessingResult disassociateGroupsToCenter(Long centerId, JsonCommand command); + + GroupUpdateResponse updateGroup(GroupUpdateRequest request); + + GroupDeleteResponse deleteGroup(GroupDeleteRequest request); + + GroupCloseResponse closeGroup(GroupCloseRequest request); } 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 86082f93b54..04798e94e33 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 @@ -27,6 +27,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -70,8 +71,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.GroupCloseRequest; +import org.apache.fineract.portfolio.group.data.GroupCloseResponse; import org.apache.fineract.portfolio.group.data.GroupCreateRequest; import org.apache.fineract.portfolio.group.data.GroupCreateResponse; +import org.apache.fineract.portfolio.group.data.GroupDeleteRequest; +import org.apache.fineract.portfolio.group.data.GroupDeleteResponse; +import org.apache.fineract.portfolio.group.data.GroupUpdateRequest; +import org.apache.fineract.portfolio.group.data.GroupUpdateResponse; import org.apache.fineract.portfolio.group.domain.Group; import org.apache.fineract.portfolio.group.domain.GroupLevel; import org.apache.fineract.portfolio.group.domain.GroupLevelRepository; @@ -331,13 +338,51 @@ public CommandProcessingResult updateCenter(final Long centerId, final JsonComma return updateGroupingType(centerId, command, GroupTypes.CENTER); } - @Transactional @Override - public CommandProcessingResult updateGroup(final Long groupId, final JsonCommand command) { + @Transactional + public GroupUpdateResponse updateGroup(final GroupUpdateRequest request) { + + final Group group = this.groupRepository.findOneWithNotFoundDetection(request.getGroupId()); + + final String groupHierarchy = group.getOffice().getHierarchy(); + + this.context.validateAccessRights(groupHierarchy); + + final Map changes = new HashMap<>(); + + if (StringUtils.isNotBlank(request.getName()) && !Objects.equals(request.getName(), group.getName())) { + + group.setName(request.getName()); + changes.put(GroupUpdateRequest.Fields.name, request.getName()); + } + + if (!Objects.equals(request.getExternalId(), group.getExternalId())) { + + group.setExternalId(request.getExternalId()); + changes.put(GroupUpdateRequest.Fields.externalId, request.getExternalId()); + } + + if (request.getStaffId() != null) { + + final Staff newStaff = this.staffRepository.findByOfficeHierarchyWithNotFoundDetection(request.getStaffId(), groupHierarchy); + + group.updateStaff(newStaff); + + changes.put(GroupUpdateRequest.Fields.staffId, request.getStaffId()); + } + + if (request.getCenterId() != null) { - this.fromApiJsonDeserializer.validateForUpdateGroup(command, groupId); + final Group parentGroup = this.groupRepository.findOneWithNotFoundDetection(request.getCenterId()); - return updateGroupingType(groupId, command, GroupTypes.GROUP); + group.setParent(parentGroup); + group.generateHierarchy(); + + changes.put(GroupUpdateRequest.Fields.centerId, request.getCenterId()); + } + + return GroupUpdateResponse.builder().resourceId(group.getId()).groupId(group.getId()).officeId(group.officeId()).changes(changes) + .build(); } private CommandProcessingResult updateGroupingType(final Long groupId, final JsonCommand command, final GroupTypes groupingType) { @@ -533,9 +578,9 @@ public CommandProcessingResult assignGroupOrCenterStaff(final Long groupId, fina @Transactional @Override - public CommandProcessingResult deleteGroup(final Long groupId) { + public GroupDeleteResponse deleteGroup(final GroupDeleteRequest request) { try { - + final Long groupId = request.getGroupId(); final Group groupForDelete = this.groupRepository.findOneWithNotFoundDetection(groupId); if (groupForDelete.isNotPending()) { @@ -545,15 +590,19 @@ public CommandProcessingResult deleteGroup(final Long groupId) { final List relatedNotes = this.noteRepository.findByGroup(groupForDelete); this.noteRepository.deleteAllInBatch(relatedNotes); + final Long officeId = groupForDelete.officeId(); + final Long deletedGroupId = groupForDelete.getId(); + this.groupRepository.delete(groupForDelete); this.groupRepository.flush(); - return new CommandProcessingResultBuilder() // - .withOfficeId(groupForDelete.getId()) // - .withGroupId(groupForDelete.officeId()) // - .withEntityId(groupForDelete.getId()) // + + return GroupDeleteResponse.builder() // + .resourceId(deletedGroupId) // + .officeId(officeId) // + .groupId(deletedGroupId) // .build(); } catch (final JpaSystemException | DataIntegrityViolationException dve) { - Throwable throwable = ExceptionUtils.getRootCause(dve.getCause()); + final Throwable throwable = ExceptionUtils.getRootCause(dve.getCause()); log.error("Error occured.", throwable); throw ErrorHandler.getMappable(dve, "error.msg.group.unknown.data.integrity.issue", "Unknown data integrity issue with resource."); @@ -561,16 +610,15 @@ public CommandProcessingResult deleteGroup(final Long groupId) { } @Override - public CommandProcessingResult closeGroup(final Long groupId, final JsonCommand command) { - this.fromApiJsonDeserializer.validateForGroupClose(command); + @Transactional + public GroupCloseResponse closeGroup(final GroupCloseRequest request) { + final Long groupId = request.getGroupId(); final Group group = this.groupRepository.findOneWithNotFoundDetection(groupId); - final LocalDate closureDate = command.localDateValueOfParameterNamed(GroupingTypesApiConstants.closureDateParamName); - final Long closureReasonId = command.longValueOfParameterNamed(GroupingTypesApiConstants.closureReasonIdParamName); - final AppUser currentUser = this.context.authenticatedUser(); + final LocalDate closureDate = parseDate(request.getClosureDate(), request.getDateFormat(), request.getLocale()); final CodeValue closureReason = this.codeValueRepository - .findOneByCodeNameAndIdWithNotFoundDetection(GroupingTypesApiConstants.GROUP_CLOSURE_REASON, closureReasonId); + .findOneByCodeNameAndIdWithNotFoundDetection(GroupingTypesApiConstants.GROUP_CLOSURE_REASON, request.getClosureReasonId()); if (group.hasActiveClients()) { final String errorMessage = group.getGroupLevel().getLevelName() @@ -584,14 +632,11 @@ public CommandProcessingResult closeGroup(final Long groupId, final JsonCommand entityDatatableChecksWritePlatformService.runTheCheck(groupId, EntityTables.GROUP.getName(), StatusEnum.CLOSE.getValue(), EntityTables.GROUP.getForeignKeyColumnNameOnDatatable(), null); - group.close(currentUser, closureReason, closureDate); + group.close(null, closureReason, closureDate); this.groupRepository.saveAndFlush(group); - return new CommandProcessingResultBuilder() // - .withGroupId(groupId) // - .withEntityId(groupId) // - .build(); + return GroupCloseResponse.builder().resourceId(groupId).groupId(groupId).build(); } private void validateLoansAndSavingsForGroupOrCenterClose(final Group groupOrCenter, final LocalDate closureDate) { diff --git a/fineract-provider/src/main/resources/application.properties b/fineract-provider/src/main/resources/application.properties index 62c68d4be12..99e9d5fc025 100644 --- a/fineract-provider/src/main/resources/application.properties +++ b/fineract-provider/src/main/resources/application.properties @@ -888,5 +888,28 @@ resilience4j.retry.instances.commandGroupCreate.retryExceptions=${FINERACT_COMMA # fineract.command.disruptor.ring-buffer-size=${FINERACT_COMMAND_RING_BUFFER_SIZE:1024} # fineract.command.disruptor.producer-type=${FINERACT_COMMAND_PRODUCER_TYPE:single} +# group update + +resilience4j.retry.instances.commandGroupUpdate.max-attempts=${FINERACT_COMMAND_GROUP_UPDATE_RETRY_MAX_ATTEMPTS:3} +resilience4j.retry.instances.commandGroupUpdate.wait-duration=${FINERACT_COMMAND_GROUP_UPDATE_RETRY_WAIT_DURATION:1s} +resilience4j.retry.instances.commandGroupUpdate.enable-exponential-backoff=${FINERACT_COMMAND_GROUP_UPDATE_RETRY_ENABLE_EXPONENTIAL_BACKOFF:true} +resilience4j.retry.instances.commandGroupUpdate.exponential-backoff-multiplier=${FINERACT_COMMAND_GROUP_UPDATE_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2} +resilience4j.retry.instances.commandGroupUpdate.retryExceptions=${FINERACT_COMMAND_GROUP_UPDATE_RETRY_EXCEPTIONS:org.springframework.dao.ConcurrencyFailureException,org.eclipse.persistence.exceptions.OptimisticLockException,jakarta.persistence.OptimisticLockException,org.springframework.orm.jpa.JpaOptimisticLockingFailureException} + +# group delete + +resilience4j.retry.instances.commandGroupDelete.max-attempts=${FINERACT_COMMAND_GROUP_DELETE_RETRY_MAX_ATTEMPTS:3} +resilience4j.retry.instances.commandGroupDelete.wait-duration=${FINERACT_COMMAND_GROUP_DELETE_RETRY_WAIT_DURATION:1s} +resilience4j.retry.instances.commandGroupDelete.enable-exponential-backoff=${FINERACT_COMMAND_GROUP_DELETE_RETRY_ENABLE_EXPONENTIAL_BACKOFF:true} +resilience4j.retry.instances.commandGroupDelete.exponential-backoff-multiplier=${FINERACT_COMMAND_GROUP_DELETE_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2} +resilience4j.retry.instances.commandGroupDelete.retryExceptions=${FINERACT_COMMAND_GROUP_DELETE_RETRY_EXCEPTIONS:org.springframework.dao.ConcurrencyFailureException,org.eclipse.persistence.exceptions.OptimisticLockException,jakarta.persistence.OptimisticLockException,org.springframework.orm.jpa.JpaOptimisticLockingFailureException} + +# group close +resilience4j.retry.instances.commandGroupClose.max-attempts=${FINERACT_COMMAND_GROUP_CLOSE_RETRY_MAX_ATTEMPTS:3} +resilience4j.retry.instances.commandGroupClose.wait-duration=${FINERACT_COMMAND_GROUP_CLOSE_RETRY_WAIT_DURATION:1s} +resilience4j.retry.instances.commandGroupClose.enable-exponential-backoff=${FINERACT_COMMAND_GROUP_CLOSE_RETRY_ENABLE_EXPONENTIAL_BACKOFF:true} +resilience4j.retry.instances.commandGroupClose.exponential-backoff-multiplier=${FINERACT_COMMAND_GROUP_CLOSE_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2} +resilience4j.retry.instances.commandGroupClose.retryExceptions=${FINERACT_COMMAND_GROUP_CLOSE_RETRY_EXCEPTIONS:org.springframework.dao.ConcurrencyFailureException,org.eclipse.persistence.exceptions.OptimisticLockException,jakarta.persistence.OptimisticLockException,org.springframework.orm.jpa.JpaOptimisticLockingFailureException} + # Fineract Default Config values fineract.defaults.office-id=${FINERACT_CONFIG_DEFAULT_OFFICE_ID:1} diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/group/handler/GroupDeleteCommandHandlerTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/group/handler/GroupDeleteCommandHandlerTest.java new file mode 100644 index 00000000000..eadeef1411a --- /dev/null +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/group/handler/GroupDeleteCommandHandlerTest.java @@ -0,0 +1,63 @@ +/** + * 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.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.apache.fineract.portfolio.group.command.GroupDeleteCommand; +import org.apache.fineract.portfolio.group.data.GroupDeleteRequest; +import org.apache.fineract.portfolio.group.data.GroupDeleteResponse; +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 GroupDeleteCommandHandlerTest { + + @Mock + private GroupingTypesWritePlatformService groupingTypesWritePlatformService; + + @InjectMocks + private GroupDeleteCommandHandler underTest; + + @Test + void handle_delegatesToServiceAndReturnsResponse() { + GroupDeleteRequest request = GroupDeleteRequest.builder().groupId(42L).build(); + + GroupDeleteResponse expectedResponse = GroupDeleteResponse.builder() // + .resourceId(42L) // + .groupId(42L) // + .build(); + + when(groupingTypesWritePlatformService.deleteGroup(request)).thenReturn(expectedResponse); + + GroupDeleteCommand command = new GroupDeleteCommand(); + command.setPayload(request); + + GroupDeleteResponse response = underTest.handle(command); + + verify(groupingTypesWritePlatformService).deleteGroup(request); + assertThat(response.getResourceId()).isEqualTo(42L); + } +} diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/group/handler/GroupUpdateCommandHandlerTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/group/handler/GroupUpdateCommandHandlerTest.java new file mode 100644 index 00000000000..f5a149c19be --- /dev/null +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/group/handler/GroupUpdateCommandHandlerTest.java @@ -0,0 +1,66 @@ +/** + * 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.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.apache.fineract.portfolio.group.command.GroupUpdateCommand; +import org.apache.fineract.portfolio.group.data.GroupUpdateRequest; +import org.apache.fineract.portfolio.group.data.GroupUpdateResponse; +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 GroupUpdateCommandHandlerTest { + + @Mock + private GroupingTypesWritePlatformService groupingTypesWritePlatformService; + + @InjectMocks + private GroupUpdateCommandHandler underTest; + + @Test + void handle_delegatesToServiceAndReturnsResponse() { + GroupUpdateRequest request = GroupUpdateRequest.builder() // + .groupId(42L) // + .name("Updated Group") // + .build(); + + GroupUpdateResponse expectedResponse = GroupUpdateResponse.builder() // + .resourceId(42L) // + .groupId(42L) // + .build(); + + when(groupingTypesWritePlatformService.updateGroup(request)).thenReturn(expectedResponse); + + GroupUpdateCommand command = new GroupUpdateCommand(); + command.setPayload(request); + + GroupUpdateResponse response = underTest.handle(command); + + verify(groupingTypesWritePlatformService).updateGroup(request); + assertThat(response.getResourceId()).isEqualTo(42L); + } +} diff --git a/fineract-validation/src/main/resources/ValidationMessages.properties b/fineract-validation/src/main/resources/ValidationMessages.properties index e81f694abe7..6a4f6aa0655 100644 --- a/fineract-validation/src/main/resources/ValidationMessages.properties +++ b/fineract-validation/src/main/resources/ValidationMessages.properties @@ -137,7 +137,6 @@ org.apache.fineract.portfolio.meeting.attendance.client-id.not-null=The paramete 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. @@ -147,3 +146,13 @@ org.apache.fineract.portfolio.group.create.assertion.staff-id-positive=Staff ID 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. +org.apache.fineract.portfolio.group.update.assertion.name-required=Group name is required. +org.apache.fineract.portfolio.group.update.assertion.name-max-length=Group name cannot exceed 100 characters. +org.apache.fineract.portfolio.group.update.assertion.external-id-max-length=External ID cannot exceed 100 characters. +org.apache.fineract.portfolio.group.update.assertion.office-id-positive=Office ID must be greater than zero. +org.apache.fineract.portfolio.group.update.assertion.staff-id-positive=Staff ID must be greater than zero. +org.apache.fineract.portfolio.group.update.assertion.activation-date-required-when-active-or-present=Activation date is required when the group is being activated. +org.apache.fineract.portfolio.group.delete.assertion.group-id-required=Group ID is required. +org.apache.fineract.portfolio.group.close.assertion.closure-date-required=Closure date is required. +org.apache.fineract.portfolio.group.close.assertion.closure-reason-id-required=Closure reason ID is required. +org.apache.fineract.portfolio.group.close.assertion.closure-reason-id-positive=Closure reason ID must be greater than zero. diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/helpers/FeignGroupHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/helpers/FeignGroupHelper.java index f31ac104c97..e8d198765d2 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/helpers/FeignGroupHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/helpers/FeignGroupHelper.java @@ -25,16 +25,16 @@ import java.util.Set; import java.util.UUID; import org.apache.fineract.client.feign.FineractFeignClient; -import org.apache.fineract.client.models.DeleteGroupsGroupIdResponse; import org.apache.fineract.client.models.GetGroupsGroupIdClientMembers; import org.apache.fineract.client.models.GetGroupsGroupIdResponse; import org.apache.fineract.client.models.GetGroupsPageItems; +import org.apache.fineract.client.models.GroupCreateResponse; +import org.apache.fineract.client.models.GroupDeleteResponse; +import org.apache.fineract.client.models.GroupUpdateRequest; +import org.apache.fineract.client.models.GroupUpdateResponse; import org.apache.fineract.client.models.PostGroupsGroupIdChanges; import org.apache.fineract.client.models.PostGroupsGroupIdRequest; import org.apache.fineract.client.models.PostGroupsRequest; -import org.apache.fineract.client.models.PostGroupsResponse; -import org.apache.fineract.client.models.PutGroupsGroupIdRequest; -import org.apache.fineract.client.models.PutGroupsGroupIdResponse; import org.apache.fineract.integrationtests.client.feign.modules.LoanTestData; import org.apache.fineract.integrationtests.common.Utils; @@ -60,12 +60,12 @@ public FeignGroupHelper(FineractFeignClient fineractClient) { } /** Creates a group in {@code pending} status (active=false) in the default office. */ - public PostGroupsResponse createGroup() { + public GroupCreateResponse createGroup() { return createGroup(DEFAULT_OFFICE_ID); } /** Creates a group in {@code pending} status (active=false). */ - public PostGroupsResponse createGroup(Long officeId) { + public GroupCreateResponse createGroup(Long officeId) { PostGroupsRequest request = new PostGroupsRequest()// .officeId(officeId)// .name(Utils.uniqueRandomStringGenerator("Group_Name_", 5))// @@ -78,11 +78,11 @@ public PostGroupsResponse createGroup(Long officeId) { } /** Creates an {@code active} group in the default office. */ - public PostGroupsResponse createActiveGroup() { + public GroupCreateResponse createActiveGroup() { return createActiveGroup(DEFAULT_OFFICE_ID, DEFAULT_ACTIVATION_DATE); } - public PostGroupsResponse createActiveGroup(Long officeId, String activationDate) { + public GroupCreateResponse createActiveGroup(Long officeId, String activationDate) { PostGroupsRequest request = new PostGroupsRequest()// .officeId(officeId)// .name(Utils.uniqueRandomStringGenerator("Group_Name_", 5))// @@ -94,7 +94,7 @@ public PostGroupsResponse createActiveGroup(Long officeId, String activationDate return createGroup(request); } - public PostGroupsResponse createGroup(PostGroupsRequest request) { + public GroupCreateResponse createGroup(PostGroupsRequest request) { return ok(() -> fineractClient.groups().createGroup(request)); } @@ -109,12 +109,12 @@ public GetGroupsGroupIdResponse retrieveGroupWithAssociations(Long groupId, Stri return ok(() -> fineractClient.groups().retrieveOneGroup(groupId, params)); } - public PutGroupsGroupIdResponse updateGroup(Long groupId, String name) { - PutGroupsGroupIdRequest request = new PutGroupsGroupIdRequest().name(name); + public GroupUpdateResponse updateGroup(Long groupId, String name) { + GroupUpdateRequest request = new GroupUpdateRequest().name(name); return ok(() -> fineractClient.groups().updateGroup(groupId, request)); } - public DeleteGroupsGroupIdResponse deleteGroup(Long groupId) { + public GroupDeleteResponse deleteGroup(Long groupId) { return ok(() -> fineractClient.groups().deleteGroup(groupId)); }