FINERACT-2239: New command processing - resource notes

This commit is contained in:
Aleksandar Vidakovic
2026-02-11 00:15:27 +01:00
committed by Aleksandar Vidakovic
parent 28f63cc20e
commit 02fb166b5b
39 changed files with 763 additions and 882 deletions
@@ -157,6 +157,50 @@ public class SecurityConfig {
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_READ, "READ_CURRENCY")
.requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/currencies"))
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "UPDATE_CURRENCY")
// notes: read
.requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/clients/*/notes"))
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_READ, "READ_CLIENTNOTE")
.requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/loans/*/notes"))
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_READ, "READ_LOANNOTE")
.requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/loanTransactions/*/notes"))
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_READ, "READ_LOANTRANSACTIONNOTE")
.requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/savings/*/notes"))
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_READ, "READ_SAVINGNOTE")
.requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/groups/*/notes"))
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_READ, "READ_GROUPNOTE")
// notes: create
.requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/clients/*/notes"))
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "CREATE_CLIENTNOTE")
.requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/loans/*/notes"))
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "CREATE_LOANNOTE")
.requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/loanTransactions/*/notes"))
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "CREATE_LOANTRANSACTIONNOTE")
.requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/savings/*/notes"))
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "CREATE_SAVINGNOTE")
.requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/groups/*/notes"))
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "CREATE_GROUPNOTE")
// notes: update
.requestMatchers(API_MATCHER.matcher(HttpMethod.PUT, "/api/*/clients/*/notes"))
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "UPDATE_CLIENTNOTE")
.requestMatchers(API_MATCHER.matcher(HttpMethod.PUT, "/api/*/loans/*/notes"))
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "UPDATE_LOANNOTE")
.requestMatchers(API_MATCHER.matcher(HttpMethod.PUT, "/api/*/loanTransactions/*/notes"))
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "UPDATE_LOANTRANSACTIONNOTE")
.requestMatchers(API_MATCHER.matcher(HttpMethod.PUT, "/api/*/savings/*/notes"))
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "UPDATE_SAVINGNOTE")
.requestMatchers(API_MATCHER.matcher(HttpMethod.PUT, "/api/*/groups/*/notes"))
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "UPDATE_GROUPNOTE")
// notes: delete
.requestMatchers(API_MATCHER.matcher(HttpMethod.DELETE, "/api/*/clients/*/notes"))
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "DELETE_CLIENTNOTE")
.requestMatchers(API_MATCHER.matcher(HttpMethod.DELETE, "/api/*/loans/*/notes"))
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "DELETE_LOANNOTE")
.requestMatchers(API_MATCHER.matcher(HttpMethod.DELETE, "/api/*/loanTransactions/*/notes"))
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "DELETE_LOANTRANSACTIONNOTE")
.requestMatchers(API_MATCHER.matcher(HttpMethod.DELETE, "/api/*/savings/*/notes"))
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "DELETE_SAVINGNOTE")
.requestMatchers(API_MATCHER.matcher(HttpMethod.DELETE, "/api/*/groups/*/notes"))
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "DELETE_GROUPNOTE")
.requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/twofactor/validate")).fullyAuthenticated()
.requestMatchers(API_MATCHER.matcher("/api/*/twofactor")).fullyAuthenticated()
@@ -175,7 +175,6 @@ import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRepository;
import org.apache.fineract.portfolio.loanproduct.service.LoanDropdownReadPlatformService;
import org.apache.fineract.portfolio.loanproduct.service.LoanProductReadPlatformService;
import org.apache.fineract.portfolio.note.domain.NoteRepository;
import org.apache.fineract.portfolio.note.service.NoteWritePlatformService;
import org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService;
import org.apache.fineract.portfolio.paymenttype.service.PaymentTypeReadPlatformService;
import org.apache.fineract.portfolio.rate.service.RateAssembler;
@@ -184,6 +183,7 @@ import org.apache.fineract.portfolio.repaymentwithpostdatedchecks.service.Repaym
import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepositoryWrapper;
import org.apache.fineract.portfolio.savings.service.GSIMReadPlatformService;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
@@ -397,12 +397,12 @@ public class LoanAccountConfiguration {
public BuyDownFeePlatformService buyDownFeePlatformService(ProgressiveLoanTransactionValidator loanTransactionValidator,
LoanAssembler loanAssembler, LoanTransactionRepository loanTransactionRepository,
PaymentDetailWritePlatformService paymentDetailWritePlatformService, LoanJournalEntryPoster loanJournalEntryPoster,
NoteWritePlatformService noteWritePlatformService, ExternalIdFactory externalIdFactory,
LoanBuyDownFeeBalanceRepository loanBuyDownFeeBalanceRepository, BusinessEventNotifierService businessEventNotifierService,
CodeValueRepository codeValueRepository) {
ExternalIdFactory externalIdFactory, LoanBuyDownFeeBalanceRepository loanBuyDownFeeBalanceRepository,
BusinessEventNotifierService businessEventNotifierService, CodeValueRepository codeValueRepository,
ApplicationEventPublisher eventPublisher) {
return new BuyDownFeeWritePlatformServiceImpl(loanTransactionValidator, loanAssembler, loanTransactionRepository,
paymentDetailWritePlatformService, loanJournalEntryPoster, noteWritePlatformService, externalIdFactory,
loanBuyDownFeeBalanceRepository, businessEventNotifierService, codeValueRepository);
paymentDetailWritePlatformService, loanJournalEntryPoster, externalIdFactory, loanBuyDownFeeBalanceRepository,
businessEventNotifierService, codeValueRepository, eventPublisher);
}
@Bean
@@ -19,13 +19,8 @@
package org.apache.fineract.portfolio.note.api;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
@@ -35,201 +30,160 @@ import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Supplier;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.domain.CommandWrapper;
import org.apache.fineract.commands.service.CommandWrapperBuilder;
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.command.core.CommandPipeline;
import org.apache.fineract.portfolio.note.command.NoteCreateCommand;
import org.apache.fineract.portfolio.note.command.NoteDeleteCommand;
import org.apache.fineract.portfolio.note.command.NoteUpdateCommand;
import org.apache.fineract.portfolio.note.data.NoteCreateRequest;
import org.apache.fineract.portfolio.note.data.NoteCreateResponse;
import org.apache.fineract.portfolio.note.data.NoteData;
import org.apache.fineract.portfolio.note.data.NoteRequest;
import org.apache.fineract.portfolio.note.data.NoteDeleteRequest;
import org.apache.fineract.portfolio.note.data.NoteDeleteResponse;
import org.apache.fineract.portfolio.note.data.NoteUpdateRequest;
import org.apache.fineract.portfolio.note.data.NoteUpdateResponse;
import org.apache.fineract.portfolio.note.domain.NoteType;
import org.apache.fineract.portfolio.note.exception.NoteResourceNotSupportedException;
import org.apache.fineract.portfolio.note.service.NoteReadPlatformService;
import org.springframework.stereotype.Component;
@Path("/v1/{resourceType}/{resourceId}/notes")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Component
@Tag(name = "Notes", description = "Notes API allows to enter notes for supported resources.")
@Tag(name = "Notes", description = """
Notes API allows to enter notes for supported resources.
""")
@RequiredArgsConstructor
public class NotesApiResource {
public static final String CLIENTNOTE = "CLIENTNOTE";
public static final String LOANNOTE = "LOANNOTE";
public static final String LOANTRANSACTIONNOTE = "LOANTRANSACTIONNOTE";
public static final String SAVINGNOTE = "SAVINGNOTE";
public static final String GROUPNOTE = "GROUPNOTE";
public static final String INVALIDNOTE = "INVALIDNOTE";
private static final Set<String> NOTE_DATA_PARAMETERS = new HashSet<>(
Arrays.asList("id", "resourceId", "clientId", "groupId", "loanId", "loanTransactionId", "depositAccountId", "savingAccountId",
"noteType", "note", "createdById", "createdByUsername", "createdOn", "updatedById", "updatedByUsername", "updatedOn"));
private final PlatformSecurityContext context;
private final NoteReadPlatformService readPlatformService;
private final DefaultToApiJsonSerializer<NoteData> toApiJsonSerializer;
private final ApiRequestParameterHelper apiRequestParameterHelper;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
private final CommandPipeline commandPipeline;
@GET
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve a Resource's description", description = "Retrieves a Resource's Notes\n\n"
+ "Note: Notes are returned in descending createOn order.\n" + "\n" + "Example Requests:\n" + "\n" + "clients/2/notes\n" + "\n"
+ "\n" + "groups/2/notes?fields=note,createdOn,createdByUsername")
public List<NoteData> retrieveNotesByResource(
@PathParam("resourceType") @Parameter(description = "resourceType") final String resourceType,
@PathParam("resourceId") @Parameter(description = "resourceId") final Long resourceId) {
@Operation(summary = "Retrieve a Resource's description", description = """
Retrieves a resource's notes
final NoteType noteType = NoteType.fromApiUrl(resourceType);
Note: results are returned in descending createOn order.
Example Requests:
- clients/2/notes
- groups/2/notes?fields=note,createdOn,createdByUsername
""")
public List<NoteData> retrieveNotesByResource(@PathParam("resourceType") final String resourceType,
@PathParam("resourceId") final Long resourceId) {
final var noteType = NoteType.fromApiUrl(resourceType);
if (noteType == null) {
throw new NoteResourceNotSupportedException(resourceType);
}
this.context.authenticatedUser().validateHasReadPermission(getResourceDetails(noteType, resourceId).entityName());
final Integer noteTypeId = noteType.getValue();
return readPlatformService.retrieveNotesByResource(resourceId, noteTypeId);
return readPlatformService.retrieveNotesByResource(resourceId, noteType.getValue());
}
@GET
@Path("{noteId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve a Resource Note", description = "Retrieves a Resource Note\n\n" + "Example Requests:\n" + "\n"
+ "clients/1/notes/76\n" + "\n" + "\n" + "groups/1/notes/20\n" + "\n" + "\n"
+ "clients/1/notes/76?fields=note,createdOn,createdByUsername\n" + "\n" + "\n"
+ "groups/1/notes/20?fields=note,createdOn,createdByUsername")
public NoteData retrieveNote(@PathParam("resourceType") @Parameter(description = "resourceType") final String resourceType,
@PathParam("resourceId") @Parameter(description = "resourceId") final Long resourceId,
@PathParam("noteId") @Parameter(description = "noteId") final Long noteId) {
@Operation(summary = "Retrieve a Resource Note", description = """
Retrieves a resource Note
Example Requests:
- clients/1/notes/76
- groups/1/notes/20
- clients/1/notes/76?fields=note,createdOn,createdByUsername
- groups/1/notes/20?fields=note,createdOn,createdByUsername
""")
public NoteData retrieveNote(@PathParam("resourceType") final String resourceType, @PathParam("resourceId") final Long resourceId,
@PathParam("noteId") final Long noteId) {
final NoteType noteType = NoteType.fromApiUrl(resourceType);
if (noteType == null) {
throw new NoteResourceNotSupportedException(resourceType);
}
this.context.authenticatedUser().validateHasReadPermission(getResourceDetails(noteType, resourceId).entityName());
final Integer noteTypeId = noteType.getValue();
final NoteData note = this.readPlatformService.retrieveNote(noteId, resourceId, noteTypeId);
return note;
return readPlatformService.retrieveNote(noteId, resourceId, noteType.getValue());
}
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Add a Resource Note", description = "Adds a new note to a supported resource.\n\n" + "Example Requests:\n" + "\n"
+ "clients/1/notes\n" + "\n" + "\n" + "groups/1/notes")
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = NoteRequest.class)))
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = NotesApiResourceSwagger.PostResourceTypeResourceIdNotesResponse.class))) })
public CommandProcessingResult addNewNote(@PathParam("resourceType") @Parameter(description = "resourceType") final String resourceType,
@PathParam("resourceId") @Parameter(description = "resourceId") final Long resourceId,
@Parameter(hidden = true) final NoteRequest noteRequest) {
@Operation(summary = "Add a Resource Note", description = """
Adds a new note to a supported resource.
final NoteType noteType = NoteType.fromApiUrl(resourceType);
Example Requests:
if (noteType == null) {
- clients/1/notes
- groups/1/notes
""")
public NoteCreateResponse addNewNote(@PathParam("resourceType") final String resourceType,
@PathParam("resourceId") final Long resourceId, @Valid final NoteCreateRequest request) {
final var type = NoteType.fromApiUrl(resourceType);
if (type == null) {
throw new NoteResourceNotSupportedException(resourceType);
}
final CommandWrapper resourceDetails = getResourceDetails(noteType, resourceId);
final CommandWrapper commandRequest = new CommandWrapperBuilder().createNote(resourceDetails, resourceType, resourceId)
.withJson(toApiJsonSerializer.serialize(noteRequest)).build();
request.setResourceId(resourceId);
request.setType(type);
return commandsSourceWritePlatformService.logCommandSource(commandRequest);
final var command = new NoteCreateCommand();
command.setPayload(request);
final Supplier<NoteCreateResponse> response = commandPipeline.send(command);
return response.get();
}
@PUT
@Path("{noteId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Update a Resource Note", description = "Updates a Resource Note")
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = NoteRequest.class)))
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = NotesApiResourceSwagger.PutResourceTypeResourceIdNotesNoteIdResponse.class))) })
public CommandProcessingResult updateNote(@PathParam("resourceType") @Parameter(description = "resourceType") final String resourceType,
@PathParam("resourceId") @Parameter(description = "resourceId") final Long resourceId,
@PathParam("noteId") @Parameter(description = "noteId") final Long noteId,
@Parameter(hidden = true) final NoteRequest noteRequest) {
@Operation(summary = "Update a Resource Note", description = """
Updates a Resource Note
""")
public NoteUpdateResponse updateNote(@PathParam("resourceType") final String resourceType,
@PathParam("resourceId") final Long resourceId, @PathParam("noteId") final Long noteId,
@Valid final NoteUpdateRequest request) {
final var type = NoteType.fromApiUrl(resourceType);
final NoteType noteType = NoteType.fromApiUrl(resourceType);
if (noteType == null) {
if (type == null) {
throw new NoteResourceNotSupportedException(resourceType);
}
final CommandWrapper resourceDetails = getResourceDetails(noteType, resourceId);
request.setId(noteId);
request.setResourceId(resourceId);
request.setType(type);
final CommandWrapper commandRequest = new CommandWrapperBuilder().updateNote(resourceDetails, resourceType, resourceId, noteId)
.withJson(toApiJsonSerializer.serialize(noteRequest)).build();
final var command = new NoteUpdateCommand();
return commandsSourceWritePlatformService.logCommandSource(commandRequest);
command.setPayload(request);
final Supplier<NoteUpdateResponse> response = commandPipeline.send(command);
return response.get();
}
@DELETE
@Path("{noteId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Delete a Resource Note", description = "Deletes a Resource Note")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = NotesApiResourceSwagger.DeleteResourceTypeResourceIdNotesNoteIdResponse.class))) })
public CommandProcessingResult deleteNote(@PathParam("resourceType") @Parameter(description = "resourceType") final String resourceType,
@PathParam("resourceId") @Parameter(description = "resourceId") final Long resourceId,
@PathParam("noteId") @Parameter(description = "noteId") final Long noteId) {
@Operation(summary = "Delete a Resource Note", description = """
Deletes a Resource Note
""")
public NoteDeleteResponse deleteNote(@PathParam("resourceType") final String resourceType,
@PathParam("resourceId") final Long resourceId, @PathParam("noteId") final Long noteId) {
final var type = NoteType.fromApiUrl(resourceType);
final NoteType noteType = NoteType.fromApiUrl(resourceType);
if (noteType == null) {
if (type == null) {
throw new NoteResourceNotSupportedException(resourceType);
}
final CommandWrapper resourceDetails = getResourceDetails(noteType, resourceId);
var request = NoteDeleteRequest.builder().id(noteId).resourceId(resourceId).type(type).build();
final CommandWrapper commandRequest = new CommandWrapperBuilder().deleteNote(resourceDetails, resourceType, resourceId, noteId)
.build();
final var command = new NoteDeleteCommand();
return commandsSourceWritePlatformService.logCommandSource(commandRequest);
}
command.setPayload(request);
private CommandWrapper getResourceDetails(final NoteType type, final Long resourceId) {
CommandWrapperBuilder resourceDetails = new CommandWrapperBuilder();
String resourceNameForPermissions;
switch (type) {
case CLIENT -> {
resourceNameForPermissions = CLIENTNOTE;
resourceDetails.withClientId(resourceId);
}
case LOAN -> {
resourceNameForPermissions = LOANNOTE;
resourceDetails.withLoanId(resourceId);
}
case LOAN_TRANSACTION -> {
resourceNameForPermissions = LOANTRANSACTIONNOTE;
// updating loanId, to distinguish saving transaction note and
// loan transaction note as we are using subEntityId for both.
resourceDetails.withLoanId(resourceId);
resourceDetails.withSubEntityId(resourceId);
}
case SAVING_ACCOUNT -> {
resourceNameForPermissions = SAVINGNOTE;
resourceDetails.withSavingsId(resourceId);
}
case GROUP -> {
resourceNameForPermissions = GROUPNOTE;
resourceDetails.withGroupId(resourceId);
}
default -> resourceNameForPermissions = INVALIDNOTE;
}
return resourceDetails.withEntityName(resourceNameForPermissions).build();
final Supplier<NoteDeleteResponse> response = commandPipeline.send(command);
return response.get();
}
}
@@ -1,82 +0,0 @@
/**
* 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.note.api;
import io.swagger.v3.oas.annotations.media.Schema;
/**
* Created by Chirag Gupta on 12/29/17.
*/
public final class NotesApiResourceSwagger {
private NotesApiResourceSwagger() {}
@Schema(description = "PostResourceTypeResourceIdNotesRequest")
public static final class PostResourceTypeResourceIdNotesRequest {
private PostResourceTypeResourceIdNotesRequest() {}
@Schema(example = "a note about the client")
public String note;
}
@Schema(description = "PostResourceTypeResourceIdNotesResponse")
public static final class PostResourceTypeResourceIdNotesResponse {
private PostResourceTypeResourceIdNotesResponse() {}
@Schema(example = "1")
public Long officeId;
@Schema(example = "1")
public Long clientId;
@Schema(example = "76")
public Long resourceId;
}
@Schema(description = "PutResourceTypeResourceIdNotesNoteIdResponse")
public static final class PutResourceTypeResourceIdNotesNoteIdResponse {
private PutResourceTypeResourceIdNotesNoteIdResponse() {}
static final class PutNotesChanges {
private PutNotesChanges() {}
@Schema(example = "a note about the client")
public String note;
}
@Schema(example = "1")
public Long officeId;
@Schema(example = "1")
public Long clientId;
@Schema(example = "76")
public Long resourceId;
public PutNotesChanges changes;
}
@Schema(description = "DeleteResourceTypeResourceIdNotesNoteIdResponse")
public static final class DeleteResourceTypeResourceIdNotesNoteIdResponse {
private DeleteResourceTypeResourceIdNotesNoteIdResponse() {}
@Schema(example = "76")
public Long resourceId;
}
}
@@ -18,16 +18,11 @@
*/
package org.apache.fineract.portfolio.note.command;
/**
* Immutable command used for create or update of notes.
*/
public class NoteCommand {
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.apache.fineract.command.core.Command;
import org.apache.fineract.portfolio.note.data.NoteCreateRequest;
@SuppressWarnings("unused")
private final String note;
public NoteCommand(final String note) {
this.note = note;
}
}
@Data
@EqualsAndHashCode(callSuper = true)
public class NoteCreateCommand extends Command<NoteCreateRequest> {}
@@ -16,21 +16,13 @@
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.note.data;
package org.apache.fineract.portfolio.note.command;
import java.io.Serial;
import java.io.Serializable;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.EqualsAndHashCode;
import org.apache.fineract.command.core.Command;
import org.apache.fineract.portfolio.note.data.NoteDeleteRequest;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class NoteRequest implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private String note;
}
@EqualsAndHashCode(callSuper = true)
public class NoteDeleteCommand extends Command<NoteDeleteRequest> {}
@@ -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.note.command;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.apache.fineract.command.core.Command;
import org.apache.fineract.portfolio.note.data.NoteUpdateRequest;
@Data
@EqualsAndHashCode(callSuper = true)
public class NoteUpdateCommand extends Command<NoteUpdateRequest> {}
@@ -23,10 +23,9 @@ import jakarta.persistence.Entity;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import java.util.LinkedHashMap;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.commons.lang3.Strings;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.portfolio.group.domain.Group;
@@ -74,13 +73,11 @@ public class Note extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@JoinColumn(name = "share_account_id", nullable = true)
private ShareAccount shareAccount;
public static Note clientNoteFromJson(final Client client, final JsonCommand command) {
final String note = command.stringValueOfParameterNamed("note");
public static Note clientNote(final Client client, final String note) {
return new Note(client, note);
}
public static Note groupNoteFromJson(final Group group, final JsonCommand command) {
final String note = command.stringValueOfParameterNamed("note");
public static Note groupNote(final Group group, final String note) {
return new Note(group, note);
}
@@ -164,20 +161,12 @@ public class Note extends AbstractAuditableWithUTCDateTimeCustom<Long> {
this.noteTypeId = NoteType.SHARE_ACCOUNT.getValue();
}
public Map<String, Object> update(final JsonCommand command) {
final Map<String, Object> actualChanges = new LinkedHashMap<>(7);
final String noteParamName = "note";
if (command.isChangeInStringParameterNamed(noteParamName, this.note)) {
final String newValue = command.stringValueOfParameterNamed(noteParamName);
actualChanges.put(noteParamName, newValue);
this.note = StringUtils.defaultIfEmpty(newValue, null);
public Map<String, Object> update(final String note) {
if (!Strings.CI.equals(note, this.note)) {
this.note = StringUtils.defaultIfEmpty(note, null);
return Map.of("note", note);
}
return actualChanges;
}
public boolean isNotAgainstClientWithIdOf(final Long clientId) {
return !this.client.identifiedBy(clientId);
return Map.of();
}
public String getNote() {
@@ -18,27 +18,34 @@
*/
package org.apache.fineract.portfolio.note.handler;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import io.github.resilience4j.retry.annotation.Retry;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.command.core.Command;
import org.apache.fineract.command.core.CommandHandler;
import org.apache.fineract.portfolio.note.data.NoteCreateRequest;
import org.apache.fineract.portfolio.note.data.NoteCreateResponse;
import org.apache.fineract.portfolio.note.service.NoteWritePlatformService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
@Service
public class DeleteNoteCommandHandler implements NewCommandSourceHandler {
@Slf4j
@Component
@RequiredArgsConstructor
public class NoteCreateCommandHandler implements CommandHandler<NoteCreateRequest, NoteCreateResponse> {
private final NoteWritePlatformService writePlatformService;
@Autowired
public DeleteNoteCommandHandler(final NoteWritePlatformService writePlatformService) {
this.writePlatformService = writePlatformService;
@Retry(name = "commandNoteCreate", fallbackMethod = "fallback")
@Override
@Transactional
public NoteCreateResponse handle(Command<NoteCreateRequest> command) {
return writePlatformService.createNote(command.getPayload());
}
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.deleteNote(command);
public NoteCreateResponse fallback(Command<NoteCreateRequest> command, Throwable t) {
// NOTE: fallback method needs to be in the same class
return CommandHandler.super.fallback(command, t);
}
}
@@ -18,29 +18,34 @@
*/
package org.apache.fineract.portfolio.note.handler;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import io.github.resilience4j.retry.annotation.Retry;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.command.core.Command;
import org.apache.fineract.command.core.CommandHandler;
import org.apache.fineract.portfolio.note.data.NoteDeleteRequest;
import org.apache.fineract.portfolio.note.data.NoteDeleteResponse;
import org.apache.fineract.portfolio.note.service.NoteWritePlatformService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
@Service
public class UpdateNoteCommandHandler implements NewCommandSourceHandler {
@Slf4j
@Component
@RequiredArgsConstructor
public class NoteDeleteCommandHandler implements CommandHandler<NoteDeleteRequest, NoteDeleteResponse> {
private final NoteWritePlatformService writePlatformService;
@Autowired
public UpdateNoteCommandHandler(final NoteWritePlatformService noteWritePlatformService) {
this.writePlatformService = noteWritePlatformService;
}
@Transactional
@Retry(name = "commandNoteDelete", fallbackMethod = "fallback")
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.updateNote(command);
@Transactional
public NoteDeleteResponse handle(Command<NoteDeleteRequest> command) {
return writePlatformService.deleteNote(command.getPayload());
}
@Override
public NoteDeleteResponse fallback(Command<NoteDeleteRequest> command, Throwable t) {
// NOTE: fallback method needs to be in the same class
return CommandHandler.super.fallback(command, t);
}
}
@@ -18,28 +18,34 @@
*/
package org.apache.fineract.portfolio.note.handler;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import io.github.resilience4j.retry.annotation.Retry;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.command.core.Command;
import org.apache.fineract.command.core.CommandHandler;
import org.apache.fineract.portfolio.note.data.NoteUpdateRequest;
import org.apache.fineract.portfolio.note.data.NoteUpdateResponse;
import org.apache.fineract.portfolio.note.service.NoteWritePlatformService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
@Service
public class CreateNoteCommandHandler implements NewCommandSourceHandler {
@Slf4j
@Component
@RequiredArgsConstructor
public class NoteUpdateCommandHandler implements CommandHandler<NoteUpdateRequest, NoteUpdateResponse> {
private final NoteWritePlatformService writePlatformService;
@Autowired
public CreateNoteCommandHandler(final NoteWritePlatformService writePlatformService) {
this.writePlatformService = writePlatformService;
}
@Transactional
@Retry(name = "commandNoteUpdate", fallbackMethod = "fallback")
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.createNote(command);
@Transactional
public NoteUpdateResponse handle(Command<NoteUpdateRequest> command) {
return writePlatformService.updateNote(command.getPayload());
}
@Override
public NoteUpdateResponse fallback(Command<NoteUpdateRequest> command, Throwable t) {
// NOTE: fallback method needs to be in the same class
return CommandHandler.super.fallback(command, t);
}
}
@@ -0,0 +1,49 @@
/**
* 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.note.listener;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.portfolio.note.data.NoteCreateRequest;
import org.apache.fineract.portfolio.note.service.NoteWritePlatformService;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@Slf4j
@RequiredArgsConstructor
@Component
class NoteListener {
private final NoteWritePlatformService noteWritePlatformService;
@EventListener
void onCreate(NoteCreateRequest request) {
noteWritePlatformService.createNote(request);
}
@EventListener
void onUpdate(NoteCreateRequest request) {
noteWritePlatformService.createNote(request);
}
@EventListener
void onDelete(NoteCreateRequest request) {
noteWritePlatformService.createNote(request);
}
}
@@ -1,102 +0,0 @@
/**
* 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.note.serialization;
import com.google.gson.JsonElement;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.serialization.AbstractFromApiJsonDeserializer;
import org.apache.fineract.infrastructure.core.serialization.FromApiJsonDeserializer;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.portfolio.note.command.NoteCommand;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* Implementation of {@link FromApiJsonDeserializer} for {@link NoteCommand} 's.
*/
@Component
public final class NoteCommandFromApiJsonDeserializer extends AbstractFromApiJsonDeserializer<NoteCommand> {
public static final String NOTE = "note";
/**
* The parameters supported for this command.
*/
private static final Set<String> SUPPORTED_PARAMETERS = new HashSet<>(List.of(NOTE));
private final FromJsonHelper fromApiJsonHelper;
@Autowired
public NoteCommandFromApiJsonDeserializer(final FromJsonHelper fromApiJsonHelper) {
this.fromApiJsonHelper = fromApiJsonHelper;
}
@Override
public NoteCommand commandFromApiJson(final String json) {
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final Type typeOfMap = new TypeToken<Map<String, Object>>() {
}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, SUPPORTED_PARAMETERS);
final JsonElement element = this.fromApiJsonHelper.parse(json);
final String note = this.fromApiJsonHelper.extractStringNamed(NOTE, element);
return new NoteCommand(note);
}
public void validateNote(final String json) {
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final Type typeOfMap = new TypeToken<Map<String, Object>>() {
}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, SUPPORTED_PARAMETERS);
final JsonElement element = this.fromApiJsonHelper.parse(json);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource(NOTE);
final String note = this.fromApiJsonHelper.extractStringNamed(NOTE, element);
baseDataValidator.reset().parameter(NOTE).value(note).notBlank().notExceedingLengthOf(1000);
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.",
dataValidationErrors);
}
}
}
@@ -18,30 +18,26 @@
*/
package org.apache.fineract.portfolio.note.service;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
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.client.domain.Client;
import org.apache.commons.lang3.Strings;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper;
import org.apache.fineract.portfolio.client.exception.ClientNotFoundException;
import org.apache.fineract.portfolio.group.domain.Group;
import org.apache.fineract.portfolio.group.domain.GroupRepository;
import org.apache.fineract.portfolio.group.exception.GroupNotFoundException;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.apache.fineract.portfolio.loanaccount.exception.LoanTransactionNotFoundException;
import org.apache.fineract.portfolio.note.data.NoteCreateRequest;
import org.apache.fineract.portfolio.note.data.NoteCreateResponse;
import org.apache.fineract.portfolio.note.data.NoteDeleteRequest;
import org.apache.fineract.portfolio.note.data.NoteDeleteResponse;
import org.apache.fineract.portfolio.note.data.NoteUpdateRequest;
import org.apache.fineract.portfolio.note.data.NoteUpdateResponse;
import org.apache.fineract.portfolio.note.domain.Note;
import org.apache.fineract.portfolio.note.domain.NoteRepository;
import org.apache.fineract.portfolio.note.domain.NoteType;
import org.apache.fineract.portfolio.note.exception.NoteNotFoundException;
import org.apache.fineract.portfolio.note.exception.NoteResourceNotSupportedException;
import org.apache.fineract.portfolio.note.serialization.NoteCommandFromApiJsonDeserializer;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepository;
import org.apache.fineract.portfolio.savings.exception.SavingsAccountNotFoundException;
@@ -53,419 +49,135 @@ public class NoteWritePlatformServiceJpaRepositoryImpl implements NoteWritePlatf
private final GroupRepository groupRepository;
private final LoanRepositoryWrapper loanRepository;
private final LoanTransactionRepository loanTransactionRepository;
private final NoteCommandFromApiJsonDeserializer fromApiJsonDeserializer;
private final SavingsAccountRepository savingsAccountRepository;
public NoteWritePlatformServiceJpaRepositoryImpl(final NoteRepository noteRepository, final ClientRepositoryWrapper clientRepository,
final GroupRepository groupRepository, final LoanRepositoryWrapper loanRepository,
final LoanTransactionRepository loanTransactionRepository, final NoteCommandFromApiJsonDeserializer fromApiJsonDeserializer,
final SavingsAccountRepository savingsAccountRepository) {
final LoanTransactionRepository loanTransactionRepository, final SavingsAccountRepository savingsAccountRepository) {
this.noteRepository = noteRepository;
this.clientRepository = clientRepository;
this.groupRepository = groupRepository;
this.loanRepository = loanRepository;
this.loanTransactionRepository = loanTransactionRepository;
this.fromApiJsonDeserializer = fromApiJsonDeserializer;
this.savingsAccountRepository = savingsAccountRepository;
}
private CommandProcessingResult createClientNote(final JsonCommand command) {
final Long resourceId = command.getClientId();
final Client client = this.clientRepository.findOneWithNotFoundDetection(resourceId);
if (client == null) {
throw new ClientNotFoundException(resourceId);
}
final Note newNote = Note.clientNoteFromJson(client, command);
this.noteRepository.saveAndFlush(newNote);
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(newNote.getId()) //
.withClientId(client.getId()) //
.withOfficeId(client.officeId()) //
.build();
}
@Override
public void createAndPersistClientNote(final Client client, final JsonCommand command) {
final String noteText = command.stringValueOfParameterNamed("note");
if (StringUtils.isNotBlank(noteText)) {
final Note newNote = new Note(client, noteText);
this.noteRepository.save(newNote);
}
}
public NoteCreateResponse createNote(final NoteCreateRequest request) {
Note note;
Long officeId;
private CommandProcessingResult createGroupNote(final JsonCommand command) {
final Long resourceId = command.getGroupId();
final Group group = this.groupRepository.findById(resourceId).orElseThrow(() -> new GroupNotFoundException(resourceId));
final Note newNote = Note.groupNoteFromJson(group, command);
this.noteRepository.saveAndFlush(newNote);
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(newNote.getId()) //
.withGroupId(group.getId()) //
.withOfficeId(group.officeId()) //
.build();
}
private CommandProcessingResult createLoanNote(final JsonCommand command) {
final Long resourceId = command.getLoanId();
final Loan loan = this.loanRepository.findOneWithNotFoundDetection(resourceId);
final String note = command.stringValueOfParameterNamed("note");
final Note newNote = Note.loanNote(loan, note);
this.noteRepository.saveAndFlush(newNote);
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(newNote.getId()) //
.withOfficeId(loan.getOfficeId()) //
.withLoanId(loan.getId()) //
.build();
}
private CommandProcessingResult createLoanTransactionNote(final JsonCommand command) {
final Long resourceId = command.subentityId();
final LoanTransaction loanTransaction = this.loanTransactionRepository.findById(resourceId)
.orElseThrow(() -> new LoanTransactionNotFoundException(resourceId));
final Loan loan = loanTransaction.getLoan();
final String note = command.stringValueOfParameterNamed("note");
final Note newNote = Note.loanTransactionNote(loan, loanTransaction, note);
this.noteRepository.saveAndFlush(newNote);
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(newNote.getId()) //
.withOfficeId(loan.getOfficeId())//
.withLoanId(loan.getId())// Loan can be associated
.build();
}
private CommandProcessingResult createSavingAccountNote(final JsonCommand command) {
final Long resourceId = command.getSavingsId();
final SavingsAccount savingAccount = this.savingsAccountRepository.findById(resourceId)
.orElseThrow(() -> new SavingsAccountNotFoundException(resourceId));
final String note = command.stringValueOfParameterNamed("note");
final Note newNote = Note.savingNote(savingAccount, note);
this.noteRepository.saveAndFlush(newNote);
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(newNote.getId()) //
.withOfficeId(savingAccount.getClient().getOffice().getId()) //
.withSavingsId(savingAccount.getId()) //
.build();
}
@Override
public CommandProcessingResult createNote(final JsonCommand command) {
this.fromApiJsonDeserializer.validateNote(command.json());
final String resourceUrl = getResourceUrlFromCommand(command); // command.getSupportedEntityType();
final NoteType type = NoteType.fromApiUrl(resourceUrl);
switch (type) {
switch (request.getType()) {
case CLIENT: {
return createClientNote(command);
final var client = this.clientRepository.findOneWithNotFoundDetection(request.getResourceId());
note = noteRepository.saveAndFlush(Note.clientNote(client, request.getNote()));
officeId = client.officeId();
}
break;
case GROUP: {
return createGroupNote(command);
final var group = groupRepository.findById(request.getResourceId())
.orElseThrow(() -> new GroupNotFoundException(request.getResourceId()));
note = noteRepository.saveAndFlush(Note.groupNote(group, request.getNote()));
officeId = group.officeId();
}
break;
case LOAN: {
return createLoanNote(command);
final var loan = loanRepository.findOneWithNotFoundDetection(request.getResourceId());
note = noteRepository.saveAndFlush(Note.loanNote(loan, request.getNote()));
officeId = loan.getOfficeId();
}
break;
case LOAN_TRANSACTION: {
return createLoanTransactionNote(command);
final var loanTransaction = this.loanTransactionRepository.findById(request.getResourceId())
.orElseThrow(() -> new LoanTransactionNotFoundException(request.getResourceId()));
note = noteRepository.saveAndFlush(Note.loanTransactionNote(loanTransaction.getLoan(), loanTransaction, request.getNote()));
officeId = loanTransaction.getLoan().getOfficeId();
}
break;
case SAVING_ACCOUNT: {
return createSavingAccountNote(command);
final var savingAccount = savingsAccountRepository.findById(request.getResourceId())
.orElseThrow(() -> new SavingsAccountNotFoundException(request.getResourceId()));
note = noteRepository.saveAndFlush(Note.savingNote(savingAccount, request.getNote()));
officeId = savingAccount.getClient().getOffice().getId();
}
break;
default:
throw new NoteResourceNotSupportedException(resourceUrl);
throw new NoteResourceNotSupportedException(request.getType().getApiUrl());
}
return NoteCreateResponse.builder().entityId(note.getId()).resourceId(note.getId()).officeId(officeId).build();
}
@Override
public void createLoanTransactionNote(final Long loanTransactionId, final String note) {
final LoanTransaction loanTransaction = this.loanTransactionRepository.findById(loanTransactionId)
.orElseThrow(() -> new LoanTransactionNotFoundException(loanTransactionId));
public NoteUpdateResponse updateNote(final NoteUpdateRequest request) {
final var result = getNote(request.getType(), request.getResourceId(), request.getId());
final var note = result.getLeft();
final var response = NoteUpdateResponse.builder().officeId(result.getRight()).resourceId(request.getResourceId());
final Loan loan = loanTransaction.getLoan();
final Note newNote = Note.loanTransactionNote(loan, loanTransaction, note);
this.noteRepository.save(newNote);
}
private String getResourceUrlFromCommand(JsonCommand command) {
final String resourceUrl;
if (command.getClientId() != null) {
resourceUrl = NoteType.CLIENT.getApiUrl();
} else if (command.getGroupId() != null) {
resourceUrl = NoteType.GROUP.getApiUrl();
} else if (command.getLoanId() != null) {
if (command.subentityId() != null) {
resourceUrl = NoteType.LOAN_TRANSACTION.getApiUrl();
} else {
resourceUrl = NoteType.LOAN.getApiUrl();
}
} else if (command.getSavingsId() != null) {
// TODO: SAVING_TRANSACTION type need to be add.
resourceUrl = NoteType.SAVING_ACCOUNT.getApiUrl();
} else {
resourceUrl = "";
if (!Strings.CI.equals(note.getNote(), request.getNote())) {
response.changes(note.update(request.getNote()));
noteRepository.saveAndFlush(note);
}
return resourceUrl;
}
private CommandProcessingResult updateClientNote(final JsonCommand command) {
final Long resourceId = command.getClientId();
final Long noteId = command.entityId();
final NoteType type = NoteType.CLIENT;
final Client client = this.clientRepository.findOneWithNotFoundDetection(resourceId);
final Note noteForUpdate = this.noteRepository.findByClientAndId(client, noteId);
if (noteForUpdate == null) {
throw new NoteNotFoundException(noteId, resourceId, type.name().toLowerCase());
}
final Map<String, Object> changes = noteForUpdate.update(command);
if (!changes.isEmpty()) {
this.noteRepository.saveAndFlush(noteForUpdate);
}
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(noteForUpdate.getId()) //
.withClientId(client.getId()) //
.withOfficeId(client.officeId()) //
.with(changes) //
.build();
}
private CommandProcessingResult updateGroupNote(final JsonCommand command) {
final Long resourceId = command.getGroupId();
final Long noteId = command.entityId();
final NoteType type = NoteType.GROUP;
final Group group = this.groupRepository.findById(resourceId).orElseThrow(() -> new GroupNotFoundException(resourceId));
final Note noteForUpdate = this.noteRepository.findByGroupAndId(group, noteId);
if (noteForUpdate == null) {
throw new NoteNotFoundException(noteId, resourceId, type.name().toLowerCase());
}
final Map<String, Object> changes = noteForUpdate.update(command);
if (!changes.isEmpty()) {
this.noteRepository.saveAndFlush(noteForUpdate);
}
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(noteForUpdate.getId()) //
.withGroupId(group.getId()) //
.withOfficeId(group.officeId()) //
.with(changes).build();
}
private CommandProcessingResult updateLoanNote(final JsonCommand command) {
final Long resourceId = command.getLoanId();
final Long noteId = command.entityId();
final NoteType type = NoteType.LOAN;
final Loan loan = this.loanRepository.findOneWithNotFoundDetection(resourceId);
final Note noteForUpdate = this.noteRepository.findByLoanAndId(loan, noteId);
if (noteForUpdate == null) {
throw new NoteNotFoundException(noteId, resourceId, type.name().toLowerCase());
}
final Map<String, Object> changes = noteForUpdate.update(command);
if (!changes.isEmpty()) {
this.noteRepository.saveAndFlush(noteForUpdate);
}
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(noteForUpdate.getId())
.withLoanId(loan.getId()).withOfficeId(loan.getOfficeId()).with(changes).build();
}
private CommandProcessingResult updateLoanTransactionNote(final JsonCommand command) {
final Long resourceId = command.subentityId();
final Long noteId = command.entityId();
final NoteType type = NoteType.LOAN_TRANSACTION;
final LoanTransaction loanTransaction = this.loanTransactionRepository.findById(resourceId)
.orElseThrow(() -> new LoanTransactionNotFoundException(resourceId));
final Loan loan = loanTransaction.getLoan();
final Note noteForUpdate = this.noteRepository.findByLoanTransactionAndId(loanTransaction, noteId);
if (noteForUpdate == null) {
throw new NoteNotFoundException(noteId, resourceId, type.name().toLowerCase());
}
final Map<String, Object> changes = noteForUpdate.update(command);
if (!changes.isEmpty()) {
this.noteRepository.saveAndFlush(noteForUpdate);
}
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(noteForUpdate.getId())
.withLoanId(loan.getId()).withOfficeId(loan.getOfficeId()).with(changes).build();
}
private CommandProcessingResult updateSavingAccountNote(final JsonCommand command) {
final Long resourceId = command.getSavingsId();
final Long noteId = command.entityId();
final NoteType type = NoteType.SAVING_ACCOUNT;
final SavingsAccount savingAccount = this.savingsAccountRepository.findById(resourceId)
.orElseThrow(() -> new SavingsAccountNotFoundException(resourceId));
final Note noteForUpdate = this.noteRepository.findBySavingsAccountAndId(savingAccount, noteId);
if (noteForUpdate == null) {
throw new NoteNotFoundException(noteId, resourceId, type.name().toLowerCase());
}
final Map<String, Object> changes = noteForUpdate.update(command);
if (!changes.isEmpty()) {
this.noteRepository.saveAndFlush(noteForUpdate);
}
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(noteForUpdate.getId()) //
.withOfficeId(savingAccount.getClient().getOffice().getId()) //
.withSavingsId(savingAccount.getId()) //
.with(changes) //
.build();
return response.build();
}
@Override
public CommandProcessingResult updateNote(final JsonCommand command) {
public NoteDeleteResponse deleteNote(final NoteDeleteRequest request) {
var note = getNote(request.getType(), request.getResourceId(), request.getId());
this.fromApiJsonDeserializer.validateNote(command.json());
noteRepository.delete(note.getLeft());
final String resourceUrl = getResourceUrlFromCommand(command); // command.getSupportedEntityType();
final NoteType type = NoteType.fromApiUrl(resourceUrl);
return NoteDeleteResponse.builder().resourceId(request.getId()).build();
}
private Pair<Note, Long> getNote(NoteType type, Long resourceId, Long noteId) {
Note note = null;
Long officeId = null;
switch (type) {
case CLIENT: {
return updateClientNote(command);
}
case GROUP: {
return updateGroupNote(command);
}
case LOAN: {
return updateLoanNote(command);
}
case LOAN_TRANSACTION: {
return updateLoanTransactionNote(command);
}
case SAVING_ACCOUNT: {
return updateSavingAccountNote(command);
}
default:
throw new NoteResourceNotSupportedException(resourceUrl);
}
}
@Override
public CommandProcessingResult deleteNote(final JsonCommand command) {
final Note noteForDelete = getNoteForDelete(command);
this.noteRepository.delete(noteForDelete);
return new CommandProcessingResultBuilder() //
.withCommandId(null) //
.withEntityId(command.entityId()) //
.build();
}
private Note getNoteForDelete(final JsonCommand command) {
final String resourceUrl = getResourceUrlFromCommand(command);// command.getSupportedEntityType();
final Long noteId = command.entityId();
final NoteType type = NoteType.fromApiUrl(resourceUrl);
Long resourceId = null;
Note noteForUpdate = null;
switch (type) {
case CLIENT: {
resourceId = command.getClientId();
final Client client = this.clientRepository.findOneWithNotFoundDetection(resourceId);
noteForUpdate = this.noteRepository.findByClientAndId(client, noteId);
final var client = clientRepository.findOneWithNotFoundDetection(resourceId);
note = noteRepository.findByClientAndId(client, noteId);
officeId = client.officeId();
}
break;
case GROUP: {
final Long groupId = command.getGroupId();
resourceId = groupId;
Group group = this.groupRepository.findById(groupId).orElseThrow(() -> new GroupNotFoundException(groupId));
noteForUpdate = this.noteRepository.findByGroupAndId(group, noteId);
final var group = groupRepository.findById(resourceId).orElseThrow(() -> new GroupNotFoundException(resourceId));
note = noteRepository.findByGroupAndId(group, noteId);
officeId = group.officeId();
}
break;
case LOAN: {
resourceId = command.getLoanId();
final Loan loan = this.loanRepository.findOneWithNotFoundDetection(resourceId);
noteForUpdate = this.noteRepository.findByLoanAndId(loan, noteId);
final var loan = loanRepository.findOneWithNotFoundDetection(resourceId);
note = noteRepository.findByLoanAndId(loan, noteId);
officeId = loan.getOfficeId();
}
break;
case LOAN_TRANSACTION: {
resourceId = command.subentityId();
final Long loanTransactionId = resourceId;
final LoanTransaction loanTransaction = this.loanTransactionRepository.findById(loanTransactionId)
.orElseThrow(() -> new LoanTransactionNotFoundException(loanTransactionId));
noteForUpdate = this.noteRepository.findByLoanTransactionAndId(loanTransaction, noteId);
final var loanTransaction = loanTransactionRepository.findById(resourceId)
.orElseThrow(() -> new LoanTransactionNotFoundException(resourceId));
note = noteRepository.findByLoanTransactionAndId(loanTransaction, noteId);
officeId = loanTransaction.getLoan().getOfficeId();
}
break;
case SAVING_ACCOUNT: {
final Long savinsAccountId = command.getSavingsId();
final SavingsAccount savingAccount = this.savingsAccountRepository.findById(savinsAccountId)
.orElseThrow(() -> new SavingsAccountNotFoundException(savinsAccountId));
noteForUpdate = this.noteRepository.findBySavingsAccountAndId(savingAccount, noteId);
final var savingAccount = savingsAccountRepository.findById(resourceId)
.orElseThrow(() -> new SavingsAccountNotFoundException(resourceId));
note = noteRepository.findBySavingsAccountAndId(savingAccount, noteId);
officeId = savingAccount.getClient().getOffice().getId();
}
break;
case SHARE_ACCOUNT:
log.error("TODO Implement getNoteForDelete for SHARE_ACCOUNT");
break;
case SAVINGS_TRANSACTION:
log.error("TODO Implement getNoteForDelete for SAVINGS_TRANSACTION");
log.error("Not yet implemented: {}", type);
break;
}
if (noteForUpdate == null) {
if (note == null) {
throw new NoteNotFoundException(noteId, resourceId, type.name().toLowerCase());
}
return noteForUpdate;
}
return Pair.of(note, officeId);
}
}
@@ -23,7 +23,6 @@ import org.apache.fineract.portfolio.group.domain.GroupRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.apache.fineract.portfolio.note.domain.NoteRepository;
import org.apache.fineract.portfolio.note.serialization.NoteCommandFromApiJsonDeserializer;
import org.apache.fineract.portfolio.note.service.NoteReadPlatformService;
import org.apache.fineract.portfolio.note.service.NoteReadPlatformServiceImpl;
import org.apache.fineract.portfolio.note.service.NoteWritePlatformService;
@@ -47,8 +46,8 @@ public class NoteAutoConfiguration {
@ConditionalOnMissingBean
public NoteWritePlatformService noteWritePlatformService(NoteRepository noteRepository, ClientRepositoryWrapper clientRepository,
GroupRepository groupRepository, LoanRepositoryWrapper loanRepository, LoanTransactionRepository loanTransactionRepository,
NoteCommandFromApiJsonDeserializer fromApiJsonDeserializer, SavingsAccountRepository savingsAccountRepository) {
SavingsAccountRepository savingsAccountRepository) {
return new NoteWritePlatformServiceJpaRepositoryImpl(noteRepository, clientRepository, groupRepository, loanRepository,
loanTransactionRepository, fromApiJsonDeserializer, savingsAccountRepository);
loanTransactionRepository, savingsAccountRepository);
}
}
@@ -57,7 +57,8 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
import org.apache.fineract.portfolio.loanaccount.service.LoanOfficerService;
import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformService;
import org.apache.fineract.portfolio.note.service.NoteWritePlatformService;
import org.apache.fineract.portfolio.note.data.NoteCreateRequest;
import org.apache.fineract.portfolio.note.domain.NoteType;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepositoryWrapper;
import org.apache.fineract.portfolio.savings.service.SavingsAccountWritePlatformService;
@@ -67,6 +68,7 @@ import org.apache.fineract.portfolio.transfer.exception.ClientNotAwaitingTransfe
import org.apache.fineract.portfolio.transfer.exception.ClientNotAwaitingTransferApprovalOrOnHoldException;
import org.apache.fineract.portfolio.transfer.exception.TransferNotSupportedException;
import org.apache.fineract.portfolio.transfer.exception.TransferNotSupportedException.TransferNotSupportedReason;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.transaction.annotation.Transactional;
@RequiredArgsConstructor
@@ -81,11 +83,11 @@ public class TransferWritePlatformServiceJpaRepositoryImpl implements TransferWr
private final LoanRepositoryWrapper loanRepositoryWrapper;
private final SavingsAccountRepositoryWrapper savingsAccountRepositoryWrapper;
private final TransfersDataValidator transfersDataValidator;
private final NoteWritePlatformService noteWritePlatformService;
private final StaffRepositoryWrapper staffRepositoryWrapper;
private final ClientTransferDetailsRepositoryWrapper clientTransferDetailsRepositoryWrapper;
private final PlatformSecurityContext context;
private final LoanOfficerService loanOfficerService;
private final ApplicationEventPublisher eventPublisher;
@Override
@Transactional
@@ -466,7 +468,8 @@ public class TransferWritePlatformServiceJpaRepositoryImpl implements TransferWr
client.updateProposedTransferDate(null);
}
this.noteWritePlatformService.createAndPersistClientNote(client, jsonCommand);
this.eventPublisher.publishEvent(NoteCreateRequest.builder().type(NoteType.CLIENT).resourceId(client.getId())
.note(jsonCommand.stringValueOfParameterNamed("note")).build());
this.clientTransferDetailsRepositoryWrapper
.save(ClientTransferDetails.instance(client.getId(), client.getOffice().getId(), destinationOffice.getId(), transferDate,
transferEventType.getValue(), DateUtils.getBusinessLocalDate(), this.context.authenticatedUser().getId()));
@@ -28,13 +28,13 @@ import org.apache.fineract.portfolio.group.domain.GroupRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.service.LoanOfficerService;
import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformService;
import org.apache.fineract.portfolio.note.service.NoteWritePlatformService;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepositoryWrapper;
import org.apache.fineract.portfolio.savings.service.SavingsAccountWritePlatformService;
import org.apache.fineract.portfolio.transfer.data.TransfersDataValidator;
import org.apache.fineract.portfolio.transfer.service.TransferWritePlatformService;
import org.apache.fineract.portfolio.transfer.service.TransferWritePlatformServiceJpaRepositoryImpl;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -47,14 +47,13 @@ public class TransferConfiguration {
OfficeRepositoryWrapper officeRepository, CalendarInstanceRepository calendarInstanceRepository,
LoanWritePlatformService loanWritePlatformService, GroupRepositoryWrapper groupRepository,
LoanRepositoryWrapper loanRepositoryWrapper, TransfersDataValidator transfersDataValidator,
NoteWritePlatformService noteWritePlatformService, StaffRepositoryWrapper staffRepositoryWrapper,
SavingsAccountRepositoryWrapper savingsAccountRepositoryWrapper,
StaffRepositoryWrapper staffRepositoryWrapper, SavingsAccountRepositoryWrapper savingsAccountRepositoryWrapper,
SavingsAccountWritePlatformService savingsAccountWritePlatformService,
ClientTransferDetailsRepositoryWrapper clientTransferDetailsRepositoryWrapper, PlatformSecurityContext context,
LoanOfficerService loanOfficerService) {
LoanOfficerService loanOfficerService, ApplicationEventPublisher eventPublisher) {
return new TransferWritePlatformServiceJpaRepositoryImpl(clientRepositoryWrapper, officeRepository, calendarInstanceRepository,
groupRepository, loanWritePlatformService, savingsAccountWritePlatformService, loanRepositoryWrapper,
savingsAccountRepositoryWrapper, transfersDataValidator, noteWritePlatformService, staffRepositoryWrapper,
clientTransferDetailsRepositoryWrapper, context, loanOfficerService);
savingsAccountRepositoryWrapper, transfersDataValidator, staffRepositoryWrapper, clientTransferDetailsRepositoryWrapper,
context, loanOfficerService, eventPublisher);
}
}
@@ -487,30 +487,58 @@ resilience4j.retry.instances.postInterest.enable-exponential-backoff=${FINERACT_
resilience4j.retry.instances.postInterest.exponential-backoff-multiplier=${FINERACT_PROCESS_POST_INTEREST_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2}
resilience4j.retry.instances.postInterest.retryExceptions=${FINERACT_PROCESS_POST_INTEREST_RETRY_EXCEPTIONS:org.springframework.dao.ConcurrencyFailureException,org.eclipse.persistence.exceptions.OptimisticLockException,jakarta.persistence.OptimisticLockException,org.springframework.orm.jpa.JpaOptimisticLockingFailureException}
# business date
resilience4j.retry.instances.commandBusinessDateUpdate.max-attempts=${FINERACT_PROCESS_COMMAND_BUSINESS_DATE_UPDATE_RETRY_MAX_ATTEMPTS:3}
resilience4j.retry.instances.commandBusinessDateUpdate.wait-duration=${FINERACT_PROCESS_COMMAND_BUSINESS_DATE_UPDATE_RETRY_WAIT_DURATION:1s}
resilience4j.retry.instances.commandBusinessDateUpdate.enable-exponential-backoff=${FINERACT_PROCESS_COMMAND_BUSINESS_DATE_UPDATE_RETRY_ENABLE_EXPONENTIAL_BACKOFF:true}
resilience4j.retry.instances.commandBusinessDateUpdate.exponential-backoff-multiplier=${FINERACT_PROCESS_COMMAND_BUSINESS_DATE_UPDATE_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2}
resilience4j.retry.instances.commandBusinessDateUpdate.retryExceptions=${FINERACT_PROCESS_COMMAND_BUSINESS_DATE_UPDATE_RETRY_EXCEPTIONS:org.springframework.dao.ConcurrencyFailureException,org.eclipse.persistence.exceptions.OptimisticLockException,jakarta.persistence.OptimisticLockException,org.springframework.orm.jpa.JpaOptimisticLockingFailureException}
# cache
resilience4j.retry.instances.commandCacheSwitch.max-attempts=${FINERACT_PROCESS_COMMAND_CACHE_SWITCH_RETRY_MAX_ATTEMPTS:3}
resilience4j.retry.instances.commandCacheSwitch.wait-duration=${FINERACT_PROCESS_COMMAND_CACHE_SWITCH_RETRY_WAIT_DURATION:1s}
resilience4j.retry.instances.commandCacheSwitch.enable-exponential-backoff=${FINERACT_PROCESS_COMMAND_CACHE_SWITCH_RETRY_ENABLE_EXPONENTIAL_BACKOFF:true}
resilience4j.retry.instances.commandCacheSwitch.exponential-backoff-multiplier=${FINERACT_PROCESS_COMMAND_CACHE_SWITCH_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2}
resilience4j.retry.instances.commandCacheSwitch.retryExceptions=${FINERACT_PROCESS_COMMAND_CACHE_SWITCH_RETRY_EXCEPTIONS:org.springframework.dao.ConcurrencyFailureException,org.eclipse.persistence.exceptions.OptimisticLockException,jakarta.persistence.OptimisticLockException,org.springframework.orm.jpa.JpaOptimisticLockingFailureException}
# currency
resilience4j.retry.instances.commandCurrencyUpdate.max-attempts=${FINERACT_PROCESS_COMMAND_CURRENCY_UPDATE_RETRY_MAX_ATTEMPTS:3}
resilience4j.retry.instances.commandCurrencyUpdate.wait-duration=${FINERACT_PROCESS_COMMAND_CURRENCY_UPDATE_RETRY_WAIT_DURATION:1s}
resilience4j.retry.instances.commandCurrencyUpdate.enable-exponential-backoff=${FINERACT_PROCESS_COMMAND_CURRENCY_UPDATE_RETRY_ENABLE_EXPONENTIAL_BACKOFF:true}
resilience4j.retry.instances.commandCurrencyUpdate.exponential-backoff-multiplier=${FINERACT_PROCESS_COMMAND_CURRENCY_UPDATE_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2}
resilience4j.retry.instances.commandCurrencyUpdate.retryExceptions=${FINERACT_PROCESS_COMMAND_CURRENCY_UPDATE_RETRY_EXCEPTIONS:org.springframework.dao.ConcurrencyFailureException,org.eclipse.persistence.exceptions.OptimisticLockException,jakarta.persistence.OptimisticLockException,org.springframework.orm.jpa.JpaOptimisticLockingFailureException}
# external event configuration
resilience4j.retry.instances.commandExternalEventConfigurationUpdate.max-attempts=${FINERACT_PROCESS_COMMAND_EXTERNAL_EVENT_CONFIGURATION_UPDATE_RETRY_MAX_ATTEMPTS:3}
resilience4j.retry.instances.commandExternalEventConfigurationUpdate.wait-duration=${FINERACT_PROCESS_COMMAND_EXTERNAL_EVENT_CONFIGURATION_UPDATE_RETRY_WAIT_DURATION:1s}
resilience4j.retry.instances.commandExternalEventConfigurationUpdate.enable-exponential-backoff=${FINERACT_PROCESS_COMMAND_EXTERNAL_EVENT_CONFIGURATION_UPDATE_RETRY_ENABLE_EXPONENTIAL_BACKOFF:true}
resilience4j.retry.instances.commandExternalEventConfigurationUpdate.exponential-backoff-multiplier=${FINERACT_PROCESS_COMMAND_EXTERNAL_EVENT_CONFIGURATION_UPDATE_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2}
resilience4j.retry.instances.commandExternalEventConfigurationUpdate.retryExceptions=${FINERACT_PROCESS_COMMAND_EXTERNAL_EVENT_CONFIGURATION_UPDATE_RETRY_EXCEPTIONS:org.springframework.dao.ConcurrencyFailureException,org.eclipse.persistence.exceptions.OptimisticLockException,jakarta.persistence.OptimisticLockException,org.springframework.orm.jpa.JpaOptimisticLockingFailureException}
# note
resilience4j.retry.instances.commandNoteCreate.max-attempts=${FINERACT_PROCESS_COMMAND_NOTE_CREATE_RETRY_MAX_ATTEMPTS:3}
resilience4j.retry.instances.commandNoteCreate.wait-duration=${FINERACT_PROCESS_COMMAND_NOTE_CREATE_RETRY_WAIT_DURATION:1s}
resilience4j.retry.instances.commandNoteCreate.enable-exponential-backoff=${FINERACT_PROCESS_COMMAND_NOTE_CREATE_RETRY_ENABLE_EXPONENTIAL_BACKOFF:true}
resilience4j.retry.instances.commandNoteCreate.exponential-backoff-multiplier=${FINERACT_PROCESS_COMMAND_NOTE_CREATE_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2}
resilience4j.retry.instances.commandNoteCreate.retryExceptions=${FINERACT_PROCESS_COMMAND_NOTE_CREATE_RETRY_EXCEPTIONS:org.springframework.dao.ConcurrencyFailureException,org.eclipse.persistence.exceptions.OptimisticLockException,jakarta.persistence.OptimisticLockException,org.springframework.orm.jpa.JpaOptimisticLockingFailureException}
resilience4j.retry.instances.commandNoteUpdate.max-attempts=${FINERACT_PROCESS_COMMAND_NOTE_UPDATE_RETRY_MAX_ATTEMPTS:3}
resilience4j.retry.instances.commandNoteUpdate.wait-duration=${FINERACT_PROCESS_COMMAND_NOTE_UPDATE_RETRY_WAIT_DURATION:1s}
resilience4j.retry.instances.commandNoteUpdate.enable-exponential-backoff=${FINERACT_PROCESS_COMMAND_NOTE_UPDATE_RETRY_ENABLE_EXPONENTIAL_BACKOFF:true}
resilience4j.retry.instances.commandNoteUpdate.exponential-backoff-multiplier=${FINERACT_PROCESS_COMMAND_NOTE_UPDATE_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2}
resilience4j.retry.instances.commandNoteUpdate.retryExceptions=${FINERACT_PROCESS_COMMAND_NOTE_UPDATE_RETRY_EXCEPTIONS:org.springframework.dao.ConcurrencyFailureException,org.eclipse.persistence.exceptions.OptimisticLockException,jakarta.persistence.OptimisticLockException,org.springframework.orm.jpa.JpaOptimisticLockingFailureException}
resilience4j.retry.instances.commandNoteDelete.max-attempts=${FINERACT_PROCESS_COMMAND_NOTE_DELETE_RETRY_MAX_ATTEMPTS:3}
resilience4j.retry.instances.commandNoteDelete.wait-duration=${FINERACT_PROCESS_COMMAND_NOTE_DELETE_RETRY_WAIT_DURATION:1s}
resilience4j.retry.instances.commandNoteDelete.enable-exponential-backoff=${FINERACT_PROCESS_COMMAND_NOTE_DELETE_RETRY_ENABLE_EXPONENTIAL_BACKOFF:true}
resilience4j.retry.instances.commandNoteDelete.exponential-backoff-multiplier=${FINERACT_PROCESS_COMMAND_NOTE_DELETE_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2}
resilience4j.retry.instances.commandNoteDelete.retryExceptions=${FINERACT_PROCESS_COMMAND_NOTE_DELETE_RETRY_EXCEPTIONS:org.springframework.dao.ConcurrencyFailureException,org.eclipse.persistence.exceptions.OptimisticLockException,jakarta.persistence.OptimisticLockException,org.springframework.orm.jpa.JpaOptimisticLockingFailureException}
fineract.command.enabled=true
fineract.command.executor=${FINERACT_COMMAND_EXECUTOR:sync}
fineract.command.ring-buffer-size=${FINERACT_COMMAND_RING_BUFFER_SIZE:1024}
@@ -518,6 +546,10 @@ fineract.command.producer-type=${FINERACT_COMMAND_PRODUCER_TYPE:single}
fineract.command.auditable=${FINERACT_COMMAND_AUDITABLE:true}
# this is duplicated on purpose to keep fineract-command independent; once all modules are migrated we clean up
fineract.command.idempotency-key-header-name=${FINERACT_IDEMPOTENCY_KEY_HEADER_NAME:Idempotency-Key}
fineract.command.file-dead-letter-queue-enabled=${FINERACT_FILE_DEAD_LETTER_QUEUE_ENABLED:false}
fineract.command.file-dead-letter-queue-path=${FINERACT_FILE_DEAD_LETTER_QUEUE_PATH:/tmp/fineract-command-audit}
# command
resilience4j.retry.instances.commandAuditProcessing.max-attempts=${FINERACT_PROCESS_COMMAND_AUDIT_PROCESSING_RETRY_MAX_ATTEMPTS:3}
resilience4j.retry.instances.commandAuditProcessing.wait-duration=${FINERACT_PROCESS_COMMAND_AUDIT_PROCESSING_RETRY_WAIT_DURATION:1s}