You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@fineract.apache.org by al...@apache.org on 2026/02/05 11:55:12 UTC
(fineract) branch develop updated: FINERACT-2452: New command processing - audit
This is an automated email from the ASF dual-hosted git repository.
aleks pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git
The following commit(s) were added to refs/heads/develop by this push:
new 8d064ddf6c FINERACT-2452: New command processing - audit
8d064ddf6c is described below
commit 8d064ddf6c674b5bc23bb326a23d985a30e76251
Author: Aleks <al...@apache.org>
AuthorDate: Mon Feb 2 19:43:45 2026 +0100
FINERACT-2452: New command processing - audit
Co-Authored-By: Adam Saghy <ad...@dpc.hu>
Co-Authored-By: Yu Wati Nyi <ny...@mtholyoke.edu>
---
config/docker/compose/fineract.yml | 2 +-
.../docker/env/mariadb.env | 11 +-
docker-compose-community-app.yml | 1 -
docker-compose-custom.yml | 1 -
docker-compose-development.yml | 2 -
...community-app.yml => docker-compose-mariadb.yml | 19 ++-
docker-compose.yml | 2 -
.../module/fineract-cob/persistence.xml | 4 -
fineract-command/README.md | 8 +-
fineract-command/REFACTORING.md | 3 +-
fineract-command/dependencies.gradle | 6 +-
.../org/apache/fineract/command/core/Command.java | 27 +++-
.../{CommandConstants.java => CommandAuditor.java} | 11 +-
.../fineract/command/core/CommandConstants.java | 5 +-
.../fineract/command/core/CommandHandler.java | 8 +
.../fineract/command/core/CommandProperties.java | 12 ++
.../AsynchronousCommandExecutor.java | 52 ++++---
.../BaseCommandExecutor.java} | 18 ++-
.../implementation/BaseCommandPipeline.java | 90 +++++++++++
.../implementation/DefaultCommandAuditor.java | 168 +++++++++++++++++++++
.../implementation/DefaultCommandPipeline.java | 11 +-
.../implementation/DisruptorCommandExecutor.java | 39 ++---
.../implementation/SynchronousCommandExecutor.java | 38 +++--
.../converter/JsonNodeReadingConverter.java} | 26 ++--
...onverter.java => JsonNodeWritingConverter.java} | 34 +----
.../command/persistence/domain/CommandEntity.java | 82 +++++-----
.../domain/CommandEntityState.java} | 8 +-
.../persistence/domain/CommandRepository.java | 10 +-
.../persistence/mapping/CommandJsonMapper.java | 9 +-
.../command/persistence/mapping/CommandMapper.java | 38 ++++-
.../command/starter/CommandConfiguration.java | 7 +-
.../starter/CommandPersistenceConfiguration.java | 16 +-
.../module/command/module-changelog-master.xml | 27 ++++
.../module/command/parts}/0001_command_init.xml | 10 ++
.../command/parts/0002_command_fix_structure.xml | 60 ++++++++
.../module/fineract-command/persistence.xml | 43 ------
.../apache/fineract/command/CommandBaseTest.java | 59 ++++++--
.../fineract/command/CommandPersistenceTest.java | 24 ++-
.../fineract/command/CommandPipelineBenchmark.java | 38 +++--
.../fineract/command/CommandPipelineTest.java | 53 +++++--
.../fineract/command/CommandSampleApiTest.java | 83 ++++++++--
.../apache/fineract/command/TestConfiguration.java | 5 +-
.../command/sample/api/DummyApiController.java | 17 +--
.../command/sample/command/DummyErrorCommand.java} | 15 +-
.../{DummyRequest.java => DummyErrorRequest.java} | 5 +-
...{DummyResponse.java => DummyErrorResponse.java} | 8 +-
.../fineract/command/sample/data/DummyRequest.java | 3 -
.../command/sample/data/DummyResponse.java | 5 -
.../command/sample/exception/DummyException.java} | 9 +-
.../sample/handler/DummyCommandHandler.java | 10 ++
...dHandler.java => DummyErrorCommandHandler.java} | 14 +-
.../middleware/DummyIdempotencyMiddleware.java | 50 ------
.../sample/service/DefaultDummyService.java | 3 +-
.../src/test/resources/application-test.properties | 4 +-
.../businessdate/api/BusinessDateApiResource.java | 9 +-
.../handler/BusinessDateUpdateHandler.java | 8 +
.../infrastructure/cache/api/CacheApiResource.java | 4 -
.../cache/handler/CacheSwitchCommandHandler.java | 8 +
.../api/ExternalEventConfigurationApiResource.java | 8 +-
.../ExternalEventConfigurationUpdateHandler.java | 8 +
.../monetary/api/CurrenciesApiResource.java | 4 -
.../handler/CurrencyUpdateCommandHandler.java | 9 ++
.../fineract/test/helper/BusinessDateHelper.java | 2 +-
.../test/stepdef/common/BusinessDateStepDef.java | 4 +-
.../suite/ExternalEventSuiteInitializerStep.java | 2 +-
.../infrastructure/core/config/jpa/JPAConfig.java | 3 +-
.../src/main/resources/application.properties | 57 ++++++-
.../resources/db/changelog/db.changelog-master.xml | 1 +
.../module/fineract-provider/persistence.xml | 5 -
.../ClasspathDuplicatesStepDefinitions.java | 2 +-
.../src/test/resources/application-test.properties | 1 +
.../feign/helpers/FeignBusinessDateHelper.java | 2 +-
.../common/BusinessDateHelper.java | 2 +-
.../common/externalevents/ExternalEventHelper.java | 2 +-
74 files changed, 1005 insertions(+), 449 deletions(-)
diff --git a/config/docker/compose/fineract.yml b/config/docker/compose/fineract.yml
index 0d411844ea..ae5323c41c 100644
--- a/config/docker/compose/fineract.yml
+++ b/config/docker/compose/fineract.yml
@@ -21,7 +21,7 @@ version: "3.8"
services:
fineract:
# user: "${FINERACT_USER}:${FINERACT_GROUP}"
- image: ${IMAGE_NAME:-apache/fineract:latest}
+ image: fineract:latest
volumes:
- ${PWD}/config/docker/logback/logback-override.xml:/app/logback-override.xml
- ${PWD}/config/docker/aws/etc/credentials:/etc/aws/credentials:ro
diff --git a/docker-compose-community-app.yml b/config/docker/env/mariadb.env
similarity index 81%
copy from docker-compose-community-app.yml
copy to config/docker/env/mariadb.env
index 612cec5c76..26aae0972b 100644
--- a/docker-compose-community-app.yml
+++ b/config/docker/env/mariadb.env
@@ -1,3 +1,4 @@
+#
# 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
@@ -16,12 +17,4 @@
# under the License.
#
-version: "3.8"
-services:
- # Frontend service
- community-app:
- image: openmf/community-app:latest
- container_name: mifos-ui
- restart: always
- ports:
- - 9090:80
+MARIADB_ROOT_PASSWORD=mysql
\ No newline at end of file
diff --git a/docker-compose-community-app.yml b/docker-compose-community-app.yml
index 612cec5c76..b066084529 100644
--- a/docker-compose-community-app.yml
+++ b/docker-compose-community-app.yml
@@ -16,7 +16,6 @@
# under the License.
#
-version: "3.8"
services:
# Frontend service
community-app:
diff --git a/docker-compose-custom.yml b/docker-compose-custom.yml
index ecb2d72ecd..b6c29dd311 100644
--- a/docker-compose-custom.yml
+++ b/docker-compose-custom.yml
@@ -17,7 +17,6 @@
#
# You can replace and test a more recent version of docker compose
-version: "3.8"
services:
db:
extends:
diff --git a/docker-compose-development.yml b/docker-compose-development.yml
index 6ce2dc7d40..d32e271a25 100644
--- a/docker-compose-development.yml
+++ b/docker-compose-development.yml
@@ -27,8 +27,6 @@ x-logging: &default-logging
- regex:
expression: '^(?P<date>\d{4}-\d{2}-\d{2}) (?P<time>\d{2}:\d{2}:\d{2}.\d{3}) (?P<message>(?s:.*))$$'
-version: "3.8"
-
services:
loki:
extends:
diff --git a/docker-compose-community-app.yml b/docker-compose-mariadb.yml
similarity index 62%
copy from docker-compose-community-app.yml
copy to docker-compose-mariadb.yml
index 612cec5c76..59f18160b9 100644
--- a/docker-compose-community-app.yml
+++ b/docker-compose-mariadb.yml
@@ -16,12 +16,19 @@
# under the License.
#
-version: "3.8"
services:
- # Frontend service
- community-app:
- image: openmf/community-app:latest
- container_name: mifos-ui
+ mariadb:
+ container_name: mariadb
+ image: mariadb:11.4
+ volumes:
+ - ./config/docker/mysql/conf.d/server_collation.cnf:/etc/mysql/conf.d/server_collation.cnf:ro
+ - ./config/docker/mysql/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d:Z,ro
restart: always
+ env_file:
+ - ./config/docker/env/mariadb.env
+ healthcheck:
+ test: [ "CMD", "healthcheck.sh", "--su-mysql", "--connect", "--innodb_initialized" ]
+ timeout: 10s
+ retries: 10
ports:
- - 9090:80
+ - "3306:3306"
diff --git a/docker-compose.yml b/docker-compose.yml
index d67579612e..f37fb6f2ca 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -16,8 +16,6 @@
# under the License.
#
-version: "3.8"
-
services:
db:
diff --git a/fineract-cob/src/main/resources/jpa/static-weaving/module/fineract-cob/persistence.xml b/fineract-cob/src/main/resources/jpa/static-weaving/module/fineract-cob/persistence.xml
index 2798800004..78d58dca75 100644
--- a/fineract-cob/src/main/resources/jpa/static-weaving/module/fineract-cob/persistence.xml
+++ b/fineract-cob/src/main/resources/jpa/static-weaving/module/fineract-cob/persistence.xml
@@ -66,10 +66,6 @@
<class>org.apache.fineract.infrastructure.businessdate.domain.BusinessDate</class>
<class>org.apache.fineract.infrastructure.codes.domain.CodeValue</class>
- <!-- Command Module Entities -->
- <class>org.apache.fineract.command.persistence.domain.CommandEntity</class>
- <class>org.apache.fineract.command.persistence.converter.JsonAttributeConverter</class>
-
<!-- Validation Module Entities -->
<exclude-unlisted-classes>false</exclude-unlisted-classes>
diff --git a/fineract-command/README.md b/fineract-command/README.md
index 4e8c0d4a8e..f386996ee9 100644
--- a/fineract-command/README.md
+++ b/fineract-command/README.md
@@ -89,15 +89,11 @@ These different perfromance level implementations need to be absolute drop-in re
NOTE: we should consider providing a command processing implementation based on Apache Camel once this concept is approved and we migrated already a couple of services. They are specialized for exactly this kind of use cases and have more dedicated people working on it's implementation. Could give more flexibility without us needing to maintain code.
-3. Middlewares
+3. Command Handlers
TBD
-4. Command Handlers
-
-TBD
-
-5. References to users (aka AppUser)
+4. References to users (aka AppUser)
Keep things lightweight and only reference users by their user names.f
diff --git a/fineract-command/REFACTORING.md b/fineract-command/REFACTORING.md
index 87753e111b..2db31baa08 100644
--- a/fineract-command/REFACTORING.md
+++ b/fineract-command/REFACTORING.md
@@ -52,7 +52,6 @@ Please make sure to always follow this package structure pattern:
- `handler`: contains all command handlers
- `service`: contains business logic services
- `mapping`: contains MapStruct interfaces
-- `middleware`: contains middleware related to command processing; I am not expecting that we will need any of this during the refactoring process
- `security`: might contain later so called Spring Security "authorization managers" for more complex use cases
- `serialization`: technically we should not need this package anymore after we are done with the refactorings; in theory there could be very complex data structures that are not easily digestable by Jackson; for those case we could still use this package to add de-/serialization helpers (Jackson provides a proper API for this)
- `starter`: Spring Java configuration that allows us to make Fineract customizable (make parts of the system replaceable)
@@ -237,7 +236,7 @@ Command Pipeline preserving Type Information
public class DummyApiResource {
...
@GET
- DummyResponse dummy(@HeaderParam("x-fineract-request-id") UUID requestId, @HeaderParam("x-fineract-tenant-id") String tenantId, DummyRequest request) {
+ DummyResponse dummy(@HeaderParam("x-fineract-request-id") UUID requestId, @HeaderParam("Fineract-Platform-TenantId") String tenantId, DummyRequest request) {
var command = new DummyCommand();
command.setId(requestId);
command.setPayload(request);
diff --git a/fineract-command/dependencies.gradle b/fineract-command/dependencies.gradle
index 8cb5b3f1cf..94c2287fe1 100644
--- a/fineract-command/dependencies.gradle
+++ b/fineract-command/dependencies.gradle
@@ -21,13 +21,16 @@ dependencies {
implementation(
'org.springframework.boot:spring-boot-starter',
'org.springframework.boot:spring-boot-starter-validation',
- 'org.springframework.boot:spring-boot-starter-data-jpa',
+ 'org.springframework.boot:spring-boot-starter-data-jdbc',
+ 'org.springframework.boot:spring-boot-starter-web',
+ 'org.springframework.security:spring-security-core',
'io.github.resilience4j:resilience4j-spring-boot3',
'org.liquibase:liquibase-core',
'com.mysql:mysql-connector-j',
'org.postgresql:postgresql',
+ 'org.mariadb.jdbc:mariadb-java-client',
'com.fasterxml.jackson.core:jackson-databind',
@@ -66,6 +69,7 @@ dependencies {
'org.openjdk.jmh:jmh-core',
'org.testcontainers:junit-jupiter',
'org.testcontainers:postgresql',
+ 'org.testcontainers:mariadb',
'org.testcontainers:mysql',
'org.springframework.restdocs:spring-restdocs-mockmvc',
'org.springframework.restdocs:spring-restdocs-webtestclient',
diff --git a/fineract-command/src/main/java/org/apache/fineract/command/core/Command.java b/fineract-command/src/main/java/org/apache/fineract/command/core/Command.java
index 77aba0ca7d..8441f3ca62 100644
--- a/fineract-command/src/main/java/org/apache/fineract/command/core/Command.java
+++ b/fineract-command/src/main/java/org/apache/fineract/command/core/Command.java
@@ -20,8 +20,7 @@ package org.apache.fineract.command.core;
import java.io.Serial;
import java.io.Serializable;
-import java.time.OffsetDateTime;
-import java.util.UUID;
+import java.time.Instant;
import lombok.Data;
import lombok.experimental.FieldNameConstants;
@@ -32,15 +31,31 @@ public class Command<T> implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
- private UUID id;
+ private Long commandId;
private String idempotencyKey;
- private OffsetDateTime createdAt;
+ private String ipAddress;
- private String tenantId;
+ private Instant createdAt;
- private String username;
+ private Instant updatedAt;
+
+ private Instant executedAt;
+
+ private Instant approvedAt;
+
+ private Instant rejectedAt;
+
+ private String initiatedByUsername;
+
+ private String executedByUsername;
+
+ private String approvedByUsername;
+
+ private String rejectedByUsername;
+
+ private String error;
private T payload;
}
diff --git a/fineract-command/src/main/java/org/apache/fineract/command/core/CommandConstants.java b/fineract-command/src/main/java/org/apache/fineract/command/core/CommandAuditor.java
similarity index 79%
copy from fineract-command/src/main/java/org/apache/fineract/command/core/CommandConstants.java
copy to fineract-command/src/main/java/org/apache/fineract/command/core/CommandAuditor.java
index 110f86edbf..929c99f959 100644
--- a/fineract-command/src/main/java/org/apache/fineract/command/core/CommandConstants.java
+++ b/fineract-command/src/main/java/org/apache/fineract/command/core/CommandAuditor.java
@@ -18,10 +18,13 @@
*/
package org.apache.fineract.command.core;
-import lombok.experimental.UtilityClass;
+public interface CommandAuditor {
-@UtilityClass
-public final class CommandConstants {
+ Object getResponseByIdempotencyKey(String key);
- public static final String COMMAND_REQUEST_ID = "x-fineract-request-id";
+ void processing(Command<?> command);
+
+ void processed(Command<?> command, Object response);
+
+ void error(Command<?> command);
}
diff --git a/fineract-command/src/main/java/org/apache/fineract/command/core/CommandConstants.java b/fineract-command/src/main/java/org/apache/fineract/command/core/CommandConstants.java
index 110f86edbf..f630a206f0 100644
--- a/fineract-command/src/main/java/org/apache/fineract/command/core/CommandConstants.java
+++ b/fineract-command/src/main/java/org/apache/fineract/command/core/CommandConstants.java
@@ -23,5 +23,8 @@ import lombok.experimental.UtilityClass;
@UtilityClass
public final class CommandConstants {
- public static final String COMMAND_REQUEST_ID = "x-fineract-request-id";
+ public static final String COMMAND_JSON_CLASS_ATTRIBUTE = "@class";
+ public static final String COMMAND_HTTP_HEADER_REQUEST_ID = "x-fineract-request-id";
+ public static final String COMMAND_HTTP_HEADER_TENANT_ID = "Fineract-Platform-TenantId";
+ public static final String COMMAND_HTTP_HEADER_IP = "IP";
}
diff --git a/fineract-command/src/main/java/org/apache/fineract/command/core/CommandHandler.java b/fineract-command/src/main/java/org/apache/fineract/command/core/CommandHandler.java
index d246b1ad7f..daa4160fe4 100644
--- a/fineract-command/src/main/java/org/apache/fineract/command/core/CommandHandler.java
+++ b/fineract-command/src/main/java/org/apache/fineract/command/core/CommandHandler.java
@@ -19,11 +19,19 @@
package org.apache.fineract.command.core;
import com.google.common.reflect.TypeToken;
+import lombok.SneakyThrows;
public interface CommandHandler<REQ, RES> {
RES handle(Command<REQ> command);
+ @SneakyThrows
+ default RES fallback(Command<REQ> command, Throwable t) {
+ // NOTE: I think this should be enough for now; the errors bubble up and will eventually be handled the global
+ // error handler. Any command handler can override this default to implement more specialized fallbacks.
+ throw t;
+ }
+
default boolean matches(Command<REQ> command) {
TypeToken<REQ> handlerType = new TypeToken<>(getClass()) {};
diff --git a/fineract-command/src/main/java/org/apache/fineract/command/core/CommandProperties.java b/fineract-command/src/main/java/org/apache/fineract/command/core/CommandProperties.java
index 333ccea1ca..5f909c7680 100644
--- a/fineract-command/src/main/java/org/apache/fineract/command/core/CommandProperties.java
+++ b/fineract-command/src/main/java/org/apache/fineract/command/core/CommandProperties.java
@@ -51,6 +51,18 @@ public final class CommandProperties implements Serializable {
@Builder.Default
private ProducerType producerType = ProducerType.SINGLE;
+ @Builder.Default
+ private Boolean auditable = true;
+
+ @Builder.Default
+ private Boolean fileDeadLetterQueueEnabled = false;
+
+ @Builder.Default
+ private String fileDeadLetterQueuePath = "/tmp/fineract-command-audit";
+
+ @Builder.Default
+ private String idemPotencyKeyHeaderName = "Idempotency-Key";
+
public enum CommandExecutorType {
sync, //
async, //
diff --git a/fineract-command/src/main/java/org/apache/fineract/command/implementation/AsynchronousCommandExecutor.java b/fineract-command/src/main/java/org/apache/fineract/command/implementation/AsynchronousCommandExecutor.java
index b4c8f4f828..67ae52257c 100644
--- a/fineract-command/src/main/java/org/apache/fineract/command/implementation/AsynchronousCommandExecutor.java
+++ b/fineract-command/src/main/java/org/apache/fineract/command/implementation/AsynchronousCommandExecutor.java
@@ -18,41 +18,57 @@
*/
package org.apache.fineract.command.implementation;
-import java.util.List;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
import java.util.function.Supplier;
-import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.command.core.Command;
-import org.apache.fineract.command.core.CommandExecutor;
+import org.apache.fineract.command.core.CommandAuditor;
import org.apache.fineract.command.core.CommandHandler;
-import org.apache.fineract.command.core.CommandMiddleware;
import org.apache.fineract.command.core.CommandRouter;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
-import org.springframework.stereotype.Component;
@Slf4j
-@RequiredArgsConstructor
-@Component
-@ConditionalOnProperty(value = "fineract.command.executor", havingValue = "async")
-public class AsynchronousCommandExecutor implements CommandExecutor {
-
- private final List<CommandMiddleware> middlewares;
+// TODO: not ready yet for prime time
+// @Component
+// @ConditionalOnProperty(value = "fineract.command.executor", havingValue = "async")
+public class AsynchronousCommandExecutor extends BaseCommandExecutor {
- private final CommandRouter router;
+ public AsynchronousCommandExecutor(CommandRouter router, CommandAuditor auditor) {
+ super(router, auditor);
+ }
@Override
- public <REQ, RES> Supplier<RES> execute(Command<REQ> command) {
+ public <REQ, RES> Supplier<RES> execute(final Command<REQ> command) {
CompletableFuture<RES> future = CompletableFuture.supplyAsync(() -> {
- for (CommandMiddleware middleware : middlewares) {
- middleware.invoke(command);
- }
+ auditor.processing(command);
CommandHandler<REQ, RES> handler = router.route(command);
return handler.handle(command);
+ }).whenComplete((response, t) -> {
+ if (t == null) {
+ auditor.processed(command, response);
+ } else {
+ command.setError(t.getMessage());
+
+ auditor.error(command);
+ }
});
- return future::join;
+ return () -> {
+ try {
+ // TODO: make this configurable
+ return future.get(3, SECONDS);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ } catch (ExecutionException e) {
+ throw new RuntimeException(e);
+ } catch (TimeoutException e) {
+ throw new RuntimeException(e);
+ }
+ };
}
}
diff --git a/fineract-command/src/main/java/org/apache/fineract/command/core/CommandHandler.java b/fineract-command/src/main/java/org/apache/fineract/command/implementation/BaseCommandExecutor.java
similarity index 61%
copy from fineract-command/src/main/java/org/apache/fineract/command/core/CommandHandler.java
copy to fineract-command/src/main/java/org/apache/fineract/command/implementation/BaseCommandExecutor.java
index d246b1ad7f..959da8055d 100644
--- a/fineract-command/src/main/java/org/apache/fineract/command/core/CommandHandler.java
+++ b/fineract-command/src/main/java/org/apache/fineract/command/implementation/BaseCommandExecutor.java
@@ -16,17 +16,19 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.apache.fineract.command.core;
+package org.apache.fineract.command.implementation;
-import com.google.common.reflect.TypeToken;
+import org.apache.fineract.command.core.CommandAuditor;
+import org.apache.fineract.command.core.CommandExecutor;
+import org.apache.fineract.command.core.CommandRouter;
-public interface CommandHandler<REQ, RES> {
+public abstract class BaseCommandExecutor implements CommandExecutor {
- RES handle(Command<REQ> command);
+ protected final CommandRouter router;
+ protected final CommandAuditor auditor;
- default boolean matches(Command<REQ> command) {
- TypeToken<REQ> handlerType = new TypeToken<>(getClass()) {};
-
- return handlerType.getRawType().isAssignableFrom(command.getPayload().getClass());
+ protected BaseCommandExecutor(CommandRouter router, CommandAuditor auditor) {
+ this.router = router;
+ this.auditor = auditor;
}
}
diff --git a/fineract-command/src/main/java/org/apache/fineract/command/implementation/BaseCommandPipeline.java b/fineract-command/src/main/java/org/apache/fineract/command/implementation/BaseCommandPipeline.java
new file mode 100644
index 0000000000..d1cd189fac
--- /dev/null
+++ b/fineract-command/src/main/java/org/apache/fineract/command/implementation/BaseCommandPipeline.java
@@ -0,0 +1,90 @@
+/**
+ * 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.command.implementation;
+
+import static org.apache.fineract.command.core.CommandConstants.COMMAND_HTTP_HEADER_IP;
+
+import java.time.Instant;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.fineract.command.core.Command;
+import org.apache.fineract.command.core.CommandExecutor;
+import org.apache.fineract.command.core.CommandPipeline;
+import org.apache.fineract.command.core.CommandProperties;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+@Slf4j
+@RequiredArgsConstructor
+public abstract class BaseCommandPipeline implements CommandPipeline {
+
+ protected final CommandExecutor executor;
+ protected final CommandProperties properties;
+
+ protected void setAttributes(Command<?> command) {
+ if (StringUtils.isEmpty(command.getIpAddress())) {
+ command.setIpAddress(getHeader(COMMAND_HTTP_HEADER_IP));
+ }
+ if (StringUtils.isEmpty(command.getIdempotencyKey())) {
+ command.setIdempotencyKey(getHeader(properties.getIdemPotencyKeyHeaderName()));
+ }
+ if (command.getCreatedAt() == null) {
+ command.setCreatedAt(Instant.now());
+ }
+ if (StringUtils.isEmpty(command.getInitiatedByUsername())) {
+ command.setInitiatedByUsername(getUsername());
+ }
+ }
+
+ protected String getHeader(String name) {
+ return getHeader(name, false);
+ }
+
+ protected String getHeader(String name, boolean searchParameter) {
+ var attributes = RequestContextHolder.getRequestAttributes();
+
+ if (attributes instanceof ServletRequestAttributes servletAttributes) {
+ var value = servletAttributes.getRequest().getHeader(name.toLowerCase());
+
+ if (searchParameter && StringUtils.isEmpty(value)) {
+ value = servletAttributes.getRequest().getParameter(name.toLowerCase());
+ }
+
+ return value;
+ }
+
+ return null;
+ }
+
+ protected String getUsername() {
+ final var context = SecurityContextHolder.getContext();
+
+ if (context != null) {
+ final var auth = context.getAuthentication();
+
+ if (auth != null) {
+ return auth.getName();
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/fineract-command/src/main/java/org/apache/fineract/command/implementation/DefaultCommandAuditor.java b/fineract-command/src/main/java/org/apache/fineract/command/implementation/DefaultCommandAuditor.java
new file mode 100644
index 0000000000..410560f9a5
--- /dev/null
+++ b/fineract-command/src/main/java/org/apache/fineract/command/implementation/DefaultCommandAuditor.java
@@ -0,0 +1,168 @@
+/**
+ * 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.command.implementation;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.apache.fineract.command.core.CommandConstants.COMMAND_JSON_CLASS_ATTRIBUTE;
+import static org.apache.fineract.command.persistence.domain.CommandEntityState.ERROR;
+import static org.apache.fineract.command.persistence.domain.CommandEntityState.PROCESSED;
+import static org.apache.fineract.command.persistence.domain.CommandEntityState.UNDER_PROCESSING;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.github.resilience4j.retry.annotation.Retry;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.time.Instant;
+import lombok.RequiredArgsConstructor;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.io.FileUtils;
+import org.apache.fineract.command.core.Command;
+import org.apache.fineract.command.core.CommandAuditor;
+import org.apache.fineract.command.core.CommandProperties;
+import org.apache.fineract.command.persistence.domain.CommandEntity;
+import org.apache.fineract.command.persistence.domain.CommandRepository;
+import org.apache.fineract.command.persistence.mapping.CommandMapper;
+import org.springframework.boot.context.event.ApplicationStartedEvent;
+import org.springframework.context.event.EventListener;
+import org.springframework.stereotype.Component;
+
+@Slf4j
+@RequiredArgsConstructor
+@Component
+public class DefaultCommandAuditor implements CommandAuditor {
+
+ private final CommandMapper mapper;
+ private final CommandRepository repository;
+ private final CommandProperties properties;
+ private final ObjectMapper objectMapper;
+
+ @SneakyThrows
+ @Override
+ public Object getResponseByIdempotencyKey(String key) {
+ final var result = repository.findOneByIdempotencyKey(key);
+ final var response = result.map(CommandEntity::getResponse).orElse(null);
+
+ if (response != null) {
+ final var clazz = response.get(COMMAND_JSON_CLASS_ATTRIBUTE).asText();
+
+ return objectMapper.convertValue(response, Class.forName(clazz));
+ }
+
+ return null;
+ }
+
+ @Retry(name = "commandAuditProcessing", fallbackMethod = "fallback")
+ @Override
+ public void processing(Command<?> command) {
+ if (Boolean.TRUE.equals(properties.getAuditable())) {
+ final var now = Instant.now();
+ final var commandEntity = mapper.map(command);
+
+ commandEntity.setCreatedAt(now);
+ commandEntity.setUpdatedAt(now);
+ commandEntity.setState(UNDER_PROCESSING);
+
+ repository.save(commandEntity);
+
+ command.setCreatedAt(now);
+ command.setUpdatedAt(now);
+ command.setCommandId(commandEntity.getId());
+ }
+ }
+
+ @Retry(name = "commandAuditProcessed", fallbackMethod = "fallback")
+ @Override
+ public void processed(Command<?> command, Object response) {
+ if (Boolean.TRUE.equals(properties.getAuditable())) {
+ final var now = Instant.now();
+ final var commandEntity = mapper.map(command, response);
+
+ // TODO: improve when maker-checker functionality lands
+ commandEntity.setExecutedByUsername(command.getInitiatedByUsername());
+ commandEntity.setUpdatedAt(now);
+ commandEntity.setExecutedAt(now);
+ commandEntity.setState(PROCESSED);
+
+ repository.save(commandEntity);
+
+ command.setUpdatedAt(now);
+ command.setExecutedAt(now);
+ }
+ }
+
+ @Retry(name = "commandAuditError", fallbackMethod = "fallback")
+ @Override
+ public void error(Command<?> command) {
+ if (Boolean.TRUE.equals(properties.getAuditable())) {
+ final var now = Instant.now();
+ final var commandEntity = mapper.map(command);
+
+ commandEntity.setUpdatedAt(now);
+ commandEntity.setState(ERROR);
+
+ repository.save(commandEntity);
+
+ command.setUpdatedAt(now);
+ command.setExecutedAt(now);
+ }
+ }
+
+ @EventListener(ApplicationStartedEvent.class)
+ void onStartup() {
+ if (Boolean.TRUE.equals(properties.getFileDeadLetterQueueEnabled())) {
+ try {
+ var created = Path.of(properties.getFileDeadLetterQueuePath()).toFile().mkdirs();
+ log.info("Created command dead-letter queue: {} ({})", properties.getFileDeadLetterQueuePath(), created);
+ } catch (Exception e) {
+ log.error("Unable to initialize command dead-letter queue:", e);
+ }
+ }
+ }
+
+ @Retry(name = "commandAuditFallback", fallbackMethod = "fatal")
+ @SuppressWarnings("UnusedMethod")
+ private void fallback(Command<?> command, Throwable t) throws Exception {
+ if (Boolean.TRUE.equals(properties.getFileDeadLetterQueueEnabled())) {
+ setError(command, t);
+ write(command);
+ } else {
+ fatal(command, t);
+ }
+ }
+
+ @SuppressWarnings("UnusedMethod")
+ private void fatal(Command<?> command, Throwable t) {
+ // note: last line of defense; if this fails then all is lost
+ setError(command, t);
+ log.error("Command audit error: {}", command, t);
+ }
+
+ private void setError(Command<?> command, Throwable t) {
+ if (command != null && t != null) {
+ command.setUpdatedAt(Instant.now());
+ command.setError(t.getMessage());
+ }
+ }
+
+ private void write(Command<?> command) throws IOException {
+ var file = Path.of(properties.getFileDeadLetterQueuePath(), command.getCreatedAt().toEpochMilli() + ".json").toFile();
+ FileUtils.write(file, objectMapper.writeValueAsString(command), UTF_8);
+ }
+}
diff --git a/fineract-command/src/main/java/org/apache/fineract/command/implementation/DefaultCommandPipeline.java b/fineract-command/src/main/java/org/apache/fineract/command/implementation/DefaultCommandPipeline.java
index 67c52007cc..600ac2c006 100644
--- a/fineract-command/src/main/java/org/apache/fineract/command/implementation/DefaultCommandPipeline.java
+++ b/fineract-command/src/main/java/org/apache/fineract/command/implementation/DefaultCommandPipeline.java
@@ -21,26 +21,29 @@ package org.apache.fineract.command.implementation;
import static java.util.Objects.requireNonNull;
import java.util.function.Supplier;
-import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.command.core.Command;
import org.apache.fineract.command.core.CommandExecutor;
import org.apache.fineract.command.core.CommandPipeline;
+import org.apache.fineract.command.core.CommandProperties;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.stereotype.Component;
@Slf4j
-@RequiredArgsConstructor
@Component
@ConditionalOnBean(CommandPipeline.class)
-public class DefaultCommandPipeline implements CommandPipeline {
+public class DefaultCommandPipeline extends BaseCommandPipeline {
- private final CommandExecutor executor;
+ public DefaultCommandPipeline(CommandExecutor executor, CommandProperties properties) {
+ super(executor, properties);
+ }
@Override
public <REQ, RES> Supplier<RES> send(final Command<REQ> command) {
requireNonNull(command, "Command must not be null");
+ setAttributes(command);
+
return executor.execute(command);
}
}
diff --git a/fineract-command/src/main/java/org/apache/fineract/command/implementation/DisruptorCommandExecutor.java b/fineract-command/src/main/java/org/apache/fineract/command/implementation/DisruptorCommandExecutor.java
index 91ca1213b9..b2eddb39a9 100644
--- a/fineract-command/src/main/java/org/apache/fineract/command/implementation/DisruptorCommandExecutor.java
+++ b/fineract-command/src/main/java/org/apache/fineract/command/implementation/DisruptorCommandExecutor.java
@@ -22,7 +22,6 @@ import com.lmax.disruptor.EventHandler;
import com.lmax.disruptor.dsl.Disruptor;
import java.io.Closeable;
import java.io.IOException;
-import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.function.Supplier;
import lombok.Getter;
@@ -30,26 +29,38 @@ import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.command.core.Command;
-import org.apache.fineract.command.core.CommandExecutor;
-import org.apache.fineract.command.core.CommandMiddleware;
+import org.apache.fineract.command.core.CommandAuditor;
import org.apache.fineract.command.core.CommandRouter;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
-import org.springframework.stereotype.Component;
@Slf4j
-@RequiredArgsConstructor
-@Component
-@ConditionalOnProperty(value = "fineract.command.executor", havingValue = "disruptor")
+// TODO: not ready yet for prime time
+// @Component
+// @ConditionalOnProperty(value = "fineract.command.executor", havingValue = "disruptor")
@SuppressWarnings({ "unchecked", "rawtypes" })
-public class DisruptorCommandExecutor implements CommandExecutor, Closeable {
+public class DisruptorCommandExecutor extends BaseCommandExecutor implements Closeable {
private final Disruptor<CommandEvent> disruptor;
+ public DisruptorCommandExecutor(Disruptor<CommandEvent> disruptor, CommandRouter router, CommandAuditor auditor) {
+ super(router, auditor);
+ this.disruptor = disruptor;
+ }
+
@Override
public <REQ, RES> Supplier<RES> execute(Command<REQ> command) {
CommandEvent<REQ, RES> processedEvent = next(command);
- return processedEvent.getFuture()::join;
+ var future = processedEvent.getFuture().whenComplete((response, t) -> {
+ if (t == null) {
+ auditor.processed(command, response);
+ } else {
+ command.setError(t.getMessage());
+
+ auditor.error(command);
+ }
+ });
+
+ return future::join;
}
@Override
@@ -82,17 +93,11 @@ public class DisruptorCommandExecutor implements CommandExecutor, Closeable {
@SuppressWarnings({ "unchecked", "rawtypes" })
public static class CompleteableCommandEventHandler implements EventHandler<CommandEvent> {
- private final List<CommandMiddleware> middlewares;
-
private final CommandRouter router;
@Override
- public void onEvent(CommandEvent event, long sequence, boolean endOfBatch) throws Exception {
+ public void onEvent(CommandEvent event, long sequence, boolean endOfBatch) {
try {
- for (CommandMiddleware middleware : middlewares) {
- middleware.invoke(event.getCommand());
- }
-
var handler = router.route(event.getCommand());
event.getFuture().complete(handler.handle(event.getCommand()));
diff --git a/fineract-command/src/main/java/org/apache/fineract/command/implementation/SynchronousCommandExecutor.java b/fineract-command/src/main/java/org/apache/fineract/command/implementation/SynchronousCommandExecutor.java
index 680cae7fa8..e672b22a4c 100644
--- a/fineract-command/src/main/java/org/apache/fineract/command/implementation/SynchronousCommandExecutor.java
+++ b/fineract-command/src/main/java/org/apache/fineract/command/implementation/SynchronousCommandExecutor.java
@@ -18,37 +18,45 @@
*/
package org.apache.fineract.command.implementation;
-import java.util.List;
import java.util.function.Supplier;
-import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.command.core.Command;
-import org.apache.fineract.command.core.CommandExecutor;
+import org.apache.fineract.command.core.CommandAuditor;
import org.apache.fineract.command.core.CommandHandler;
-import org.apache.fineract.command.core.CommandMiddleware;
import org.apache.fineract.command.core.CommandRouter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
@Slf4j
-@RequiredArgsConstructor
@Component
@ConditionalOnProperty(value = "fineract.command.executor", havingValue = "sync")
@SuppressWarnings({ "unchecked" })
-public class SynchronousCommandExecutor implements CommandExecutor {
+public class SynchronousCommandExecutor extends BaseCommandExecutor {
- private final List<CommandMiddleware> middlewares;
-
- private final CommandRouter router;
+ public SynchronousCommandExecutor(CommandRouter router, CommandAuditor auditor) {
+ super(router, auditor);
+ }
@Override
- public <REQ, RES> Supplier<RES> execute(Command<REQ> command) {
- for (CommandMiddleware middleware : middlewares) {
- middleware.invoke(command);
- }
-
+ public <REQ, RES> Supplier<RES> execute(final Command<REQ> command) {
CommandHandler<REQ, RES> handler = router.route(command);
- return () -> handler.handle(command);
+ return () -> {
+ try {
+ auditor.processing(command);
+
+ RES response = handler.handle(command);
+
+ auditor.processed(command, response);
+
+ return response;
+ } catch (Exception e) {
+ command.setError(e.getMessage());
+
+ auditor.error(command);
+
+ throw e;
+ }
+ };
}
}
diff --git a/fineract-command/src/test/java/org/apache/fineract/command/sample/middleware/DummyMiddleware.java b/fineract-command/src/main/java/org/apache/fineract/command/persistence/converter/JsonNodeReadingConverter.java
similarity index 63%
rename from fineract-command/src/test/java/org/apache/fineract/command/sample/middleware/DummyMiddleware.java
rename to fineract-command/src/main/java/org/apache/fineract/command/persistence/converter/JsonNodeReadingConverter.java
index 5cfeba6633..7c74adc0d2 100644
--- a/fineract-command/src/test/java/org/apache/fineract/command/sample/middleware/DummyMiddleware.java
+++ b/fineract-command/src/main/java/org/apache/fineract/command/persistence/converter/JsonNodeReadingConverter.java
@@ -16,24 +16,26 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.apache.fineract.command.sample.middleware;
+package org.apache.fineract.command.persistence.converter;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.apache.fineract.command.core.Command;
-import org.apache.fineract.command.core.CommandMiddleware;
-import org.apache.fineract.command.sample.command.DummyCommand;
+import lombok.SneakyThrows;
+import org.springframework.core.convert.converter.Converter;
+import org.springframework.data.convert.ReadingConverter;
import org.springframework.stereotype.Component;
-@Slf4j
-@RequiredArgsConstructor
@Component
-public class DummyMiddleware implements CommandMiddleware {
+@RequiredArgsConstructor
+@ReadingConverter
+public class JsonNodeReadingConverter implements Converter<String, JsonNode> {
+
+ private final ObjectMapper mapper;
+ @SneakyThrows
@Override
- public void invoke(Command<?> command) {
- if (command instanceof DummyCommand c) {
- c.getPayload().setId(command.getId());
- }
+ public JsonNode convert(String source) {
+ return mapper.readTree(source);
}
}
diff --git a/fineract-command/src/main/java/org/apache/fineract/command/persistence/converter/JsonAttributeConverter.java b/fineract-command/src/main/java/org/apache/fineract/command/persistence/converter/JsonNodeWritingConverter.java
similarity index 56%
rename from fineract-command/src/main/java/org/apache/fineract/command/persistence/converter/JsonAttributeConverter.java
rename to fineract-command/src/main/java/org/apache/fineract/command/persistence/converter/JsonNodeWritingConverter.java
index 2cf21bf5c3..4ec0401b37 100644
--- a/fineract-command/src/main/java/org/apache/fineract/command/persistence/converter/JsonAttributeConverter.java
+++ b/fineract-command/src/main/java/org/apache/fineract/command/persistence/converter/JsonNodeWritingConverter.java
@@ -20,40 +20,22 @@ package org.apache.fineract.command.persistence.converter;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
-import jakarta.persistence.AttributeConverter;
-import jakarta.persistence.Converter;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
-import lombok.extern.slf4j.Slf4j;
+import org.springframework.core.convert.converter.Converter;
+import org.springframework.data.convert.WritingConverter;
+import org.springframework.stereotype.Component;
-@Slf4j
+@Component
@RequiredArgsConstructor
-@Converter
-public class JsonAttributeConverter implements AttributeConverter<JsonNode, String> {
- // TODO: it would be nicer to use a native JSON type on the database side, but not every system supports this;
- // string/text are the lowest common denominator that should work on every database
+@WritingConverter
+public class JsonNodeWritingConverter implements Converter<JsonNode, String> {
private final ObjectMapper mapper;
- @Override
@SneakyThrows
- public String convertToDatabaseColumn(JsonNode source) {
- if (source != null) {
- return mapper.writeValueAsString(source);
- }
-
- // TODO: throw exception?
- return null;
- }
-
@Override
- @SneakyThrows
- public JsonNode convertToEntityAttribute(String source) {
- if (source != null) {
- return mapper.readTree(source);
- }
-
- // TODO: throw exception?
- return null;
+ public String convert(JsonNode source) {
+ return mapper.writeValueAsString(source);
}
}
diff --git a/fineract-command/src/main/java/org/apache/fineract/command/persistence/domain/CommandEntity.java b/fineract-command/src/main/java/org/apache/fineract/command/persistence/domain/CommandEntity.java
index e7d5e4e86b..e940e772a4 100644
--- a/fineract-command/src/main/java/org/apache/fineract/command/persistence/domain/CommandEntity.java
+++ b/fineract-command/src/main/java/org/apache/fineract/command/persistence/domain/CommandEntity.java
@@ -19,65 +19,73 @@
package org.apache.fineract.command.persistence.domain;
import com.fasterxml.jackson.databind.JsonNode;
-import jakarta.persistence.Column;
-import jakarta.persistence.Convert;
-import jakarta.persistence.Entity;
-import jakarta.persistence.GeneratedValue;
-import jakarta.persistence.GenerationType;
-import jakarta.persistence.PostLoad;
-import jakarta.persistence.PrePersist;
-import jakarta.persistence.Table;
-import jakarta.persistence.Transient;
import java.io.Serial;
import java.io.Serializable;
-import java.time.OffsetDateTime;
-import java.util.UUID;
-import lombok.AccessLevel;
+import java.time.Instant;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.FieldNameConstants;
-import org.apache.fineract.command.persistence.converter.JsonAttributeConverter;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.relational.core.mapping.Column;
+import org.springframework.data.relational.core.mapping.Table;
@Getter
@Setter
@ToString
@FieldNameConstants
-@Entity
-@Table(name = "m_command")
+@Table("m_command")
public class CommandEntity implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
- @jakarta.persistence.Id
- @GeneratedValue(strategy = GenerationType.IDENTITY)
- @Column(name = "id")
+ @Id
+ @Column("id")
private Long id;
- @Transient
- @Setter(value = AccessLevel.NONE)
- private boolean isNew = true;
+ @Column("created_at")
+ private Instant createdAt;
- @Column(name = "command_id")
- private UUID commandId;
+ @Column("updated_at")
+ private Instant updatedAt;
- @Column(name = "created_at")
- private OffsetDateTime createdAt;
+ @Column("executed_at")
+ private Instant executedAt;
- @Column(name = "tenant_id")
- private String tenantId;
+ @Column("approved_at")
+ private Instant approvedAt;
- @Column(name = "username")
- private String username;
+ @Column("rejected_at")
+ private Instant rejectedAt;
- @Column(name = "payload")
- @Convert(converter = JsonAttributeConverter.class)
- private JsonNode payload;
+ @Column("initiated_by_username")
+ private String initiatedByUsername;
- @PrePersist
- @PostLoad
- void markNotNew() {
- this.isNew = false;
- }
+ @Column("executed_by_username")
+ private String executedByUsername;
+
+ @Column("approved_by_username")
+ private String approvedByUsername;
+
+ @Column("rejected_by_username")
+ private String rejectedByUsername;
+
+ @Column("idempotency_key")
+ private String idempotencyKey;
+
+ @Column("state")
+ private CommandEntityState state;
+
+ @Column("error")
+ private String error;
+
+ @Column("ip_address")
+ private String ipAddress;
+
+ @Column("request")
+ private JsonNode request;
+
+ @Column("response")
+ private JsonNode response;
}
diff --git a/fineract-command/src/main/java/org/apache/fineract/command/core/CommandMiddleware.java b/fineract-command/src/main/java/org/apache/fineract/command/persistence/domain/CommandEntityState.java
similarity index 82%
copy from fineract-command/src/main/java/org/apache/fineract/command/core/CommandMiddleware.java
copy to fineract-command/src/main/java/org/apache/fineract/command/persistence/domain/CommandEntityState.java
index 1ae3b18646..55a947a33b 100644
--- a/fineract-command/src/main/java/org/apache/fineract/command/core/CommandMiddleware.java
+++ b/fineract-command/src/main/java/org/apache/fineract/command/persistence/domain/CommandEntityState.java
@@ -16,10 +16,8 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.apache.fineract.command.core;
+package org.apache.fineract.command.persistence.domain;
-@FunctionalInterface
-public interface CommandMiddleware {
-
- void invoke(Command<?> command);
+public enum CommandEntityState {
+ INVALID, PROCESSED, AWAITING_APPROVAL, REJECTED, UNDER_PROCESSING, ERROR
}
diff --git a/fineract-command/src/main/java/org/apache/fineract/command/persistence/domain/CommandRepository.java b/fineract-command/src/main/java/org/apache/fineract/command/persistence/domain/CommandRepository.java
index 26bf1ecce8..9e88e37cc6 100644
--- a/fineract-command/src/main/java/org/apache/fineract/command/persistence/domain/CommandRepository.java
+++ b/fineract-command/src/main/java/org/apache/fineract/command/persistence/domain/CommandRepository.java
@@ -18,7 +18,11 @@
*/
package org.apache.fineract.command.persistence.domain;
-import org.springframework.data.jpa.repository.JpaRepository;
-import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import java.util.Optional;
+import org.springframework.data.repository.ListCrudRepository;
+import org.springframework.data.repository.query.QueryByExampleExecutor;
-public interface CommandRepository extends JpaRepository<CommandEntity, Long>, JpaSpecificationExecutor<CommandEntity> {}
+public interface CommandRepository extends ListCrudRepository<CommandEntity, Long>, QueryByExampleExecutor<CommandEntity> {
+
+ Optional<CommandEntity> findOneByIdempotencyKey(String idempotencyKey);
+}
diff --git a/fineract-command/src/main/java/org/apache/fineract/command/persistence/mapping/CommandJsonMapper.java b/fineract-command/src/main/java/org/apache/fineract/command/persistence/mapping/CommandJsonMapper.java
index 06574c4019..f86c52798f 100644
--- a/fineract-command/src/main/java/org/apache/fineract/command/persistence/mapping/CommandJsonMapper.java
+++ b/fineract-command/src/main/java/org/apache/fineract/command/persistence/mapping/CommandJsonMapper.java
@@ -18,6 +18,8 @@
*/
package org.apache.fineract.command.persistence.mapping;
+import static org.apache.fineract.command.core.CommandConstants.COMMAND_JSON_CLASS_ATTRIBUTE;
+
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
@@ -29,9 +31,8 @@ import org.springframework.stereotype.Component;
@Slf4j
@RequiredArgsConstructor
@Component
-final class CommandJsonMapper {
+public final class CommandJsonMapper {
- private static final String CLASS_ATTRIBUTE = "@class";
private final ObjectMapper mapper;
@SuppressWarnings("unchecked")
@@ -40,7 +41,7 @@ final class CommandJsonMapper {
return null;
}
- var canonicalName = source.get(CLASS_ATTRIBUTE).asText();
+ var canonicalName = source.get(COMMAND_JSON_CLASS_ATTRIBUTE).asText();
try {
return (T) mapper.convertValue(source, Class.forName(canonicalName));
@@ -58,7 +59,7 @@ final class CommandJsonMapper {
var json = mapper.convertValue(source, ObjectNode.class);
- json.set(CLASS_ATTRIBUTE, new TextNode(source.getClass().getCanonicalName()));
+ json.set(COMMAND_JSON_CLASS_ATTRIBUTE, new TextNode(source.getClass().getCanonicalName()));
return json;
}
diff --git a/fineract-command/src/main/java/org/apache/fineract/command/persistence/mapping/CommandMapper.java b/fineract-command/src/main/java/org/apache/fineract/command/persistence/mapping/CommandMapper.java
index 31a2c43510..878a3f415c 100644
--- a/fineract-command/src/main/java/org/apache/fineract/command/persistence/mapping/CommandMapper.java
+++ b/fineract-command/src/main/java/org/apache/fineract/command/persistence/mapping/CommandMapper.java
@@ -30,14 +30,42 @@ import org.mapstruct.Mapping;
@Mapper(componentModel = SPRING, injectionStrategy = CONSTRUCTOR, uses = { CommandJsonMapper.class })
public interface CommandMapper {
- @Mapping(ignore = true, target = "id")
- @Mapping(source = "id", target = "commandId")
+ @Mapping(source = "commandId", target = "id")
@Mapping(source = "createdAt", target = "createdAt")
- @Mapping(source = "tenantId", target = "tenantId")
- @Mapping(source = "username", target = "username")
- @Mapping(source = "payload", target = "payload")
+ @Mapping(source = "updatedAt", target = "updatedAt")
+ @Mapping(source = "executedAt", target = "executedAt")
+ @Mapping(source = "approvedAt", target = "approvedAt")
+ @Mapping(source = "rejectedAt", target = "rejectedAt")
+ @Mapping(source = "initiatedByUsername", target = "initiatedByUsername")
+ @Mapping(source = "executedByUsername", target = "executedByUsername")
+ @Mapping(source = "approvedByUsername", target = "approvedByUsername")
+ @Mapping(source = "rejectedByUsername", target = "rejectedByUsername")
+ @Mapping(source = "payload", target = "request")
+ @Mapping(source = "ipAddress", target = "ipAddress")
+ @Mapping(source = "idempotencyKey", target = "idempotencyKey")
+ @Mapping(source = "error", target = "error")
+ @Mapping(ignore = true, target = "state")
+ @Mapping(ignore = true, target = "response")
CommandEntity map(Command source);
+ @Mapping(source = "source.commandId", target = "id")
+ @Mapping(source = "source.createdAt", target = "createdAt")
+ @Mapping(source = "source.updatedAt", target = "updatedAt")
+ @Mapping(source = "source.executedAt", target = "executedAt")
+ @Mapping(source = "source.approvedAt", target = "approvedAt")
+ @Mapping(source = "source.rejectedAt", target = "rejectedAt")
+ @Mapping(source = "source.initiatedByUsername", target = "initiatedByUsername")
+ @Mapping(source = "source.executedByUsername", target = "executedByUsername")
+ @Mapping(source = "source.approvedByUsername", target = "approvedByUsername")
+ @Mapping(source = "source.rejectedByUsername", target = "rejectedByUsername")
+ @Mapping(source = "source.payload", target = "request")
+ @Mapping(source = "source.ipAddress", target = "ipAddress")
+ @Mapping(source = "source.idempotencyKey", target = "idempotencyKey")
+ @Mapping(source = "source.error", target = "error")
+ @Mapping(source = "response", target = "response")
+ @Mapping(ignore = true, target = "state")
+ CommandEntity map(Command source, Object response);
+
@InheritInverseConfiguration
Command map(CommandEntity source);
}
diff --git a/fineract-command/src/main/java/org/apache/fineract/command/starter/CommandConfiguration.java b/fineract-command/src/main/java/org/apache/fineract/command/starter/CommandConfiguration.java
index f16969b20a..a4c46b0373 100644
--- a/fineract-command/src/main/java/org/apache/fineract/command/starter/CommandConfiguration.java
+++ b/fineract-command/src/main/java/org/apache/fineract/command/starter/CommandConfiguration.java
@@ -23,8 +23,6 @@ import com.lmax.disruptor.WaitStrategy;
import com.lmax.disruptor.YieldingWaitStrategy;
import com.lmax.disruptor.dsl.Disruptor;
import com.lmax.disruptor.util.DaemonThreadFactory;
-import java.util.List;
-import org.apache.fineract.command.core.CommandMiddleware;
import org.apache.fineract.command.core.CommandProperties;
import org.apache.fineract.command.core.CommandRouter;
import org.apache.fineract.command.implementation.DisruptorCommandExecutor;
@@ -50,15 +48,14 @@ class CommandConfiguration {
@Bean
@ConditionalOnProperty(value = "fineract.command.executor", havingValue = "disruptor")
- Disruptor<?> disruptor(CommandProperties properties, WaitStrategy waitStrategy, List<CommandMiddleware> middlewares,
- CommandRouter router) {
+ Disruptor<?> disruptor(CommandProperties properties, WaitStrategy waitStrategy, CommandRouter router) {
// TODO: make this more configurable
// Create the disruptor
Disruptor<DisruptorCommandExecutor.CommandEvent> disruptor = new Disruptor<>(DisruptorCommandExecutor.CommandEvent::new,
properties.getRingBufferSize(), DaemonThreadFactory.INSTANCE, properties.getProducerType(), waitStrategy);
- disruptor.handleEventsWith(new DisruptorCommandExecutor.CompleteableCommandEventHandler(middlewares, router));
+ disruptor.handleEventsWith(new DisruptorCommandExecutor.CompleteableCommandEventHandler(router));
disruptor.setDefaultExceptionHandler(new IgnoreExceptionHandler());
// Start the disruptor
diff --git a/fineract-command/src/main/java/org/apache/fineract/command/starter/CommandPersistenceConfiguration.java b/fineract-command/src/main/java/org/apache/fineract/command/starter/CommandPersistenceConfiguration.java
index a255726ff8..43d3a73b0b 100644
--- a/fineract-command/src/main/java/org/apache/fineract/command/starter/CommandPersistenceConfiguration.java
+++ b/fineract-command/src/main/java/org/apache/fineract/command/starter/CommandPersistenceConfiguration.java
@@ -18,9 +18,23 @@
*/
package org.apache.fineract.command.starter;
+import java.util.List;
+import org.apache.fineract.command.persistence.converter.JsonNodeReadingConverter;
+import org.apache.fineract.command.persistence.converter.JsonNodeWritingConverter;
+import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
+import org.springframework.data.jdbc.core.convert.JdbcCustomConversions;
+import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories;
@Configuration
+@EnableJdbcRepositories(basePackages = { "org.apache.fineract.command.persistence" })
@ComponentScan("org.apache.fineract.command.persistence")
-class CommandPersistenceConfiguration {}
+class CommandPersistenceConfiguration {
+
+ @Bean
+ JdbcCustomConversions customConversions(JsonNodeReadingConverter jsonNodeReadingConverter,
+ JsonNodeWritingConverter jsonNodeWritingConverter) {
+ return new JdbcCustomConversions(List.of(jsonNodeReadingConverter, jsonNodeWritingConverter));
+ }
+}
diff --git a/fineract-command/src/main/resources/db/changelog/tenant/module/command/module-changelog-master.xml b/fineract-command/src/main/resources/db/changelog/tenant/module/command/module-changelog-master.xml
new file mode 100644
index 0000000000..ac42af8aaf
--- /dev/null
+++ b/fineract-command/src/main/resources/db/changelog/tenant/module/command/module-changelog-master.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+ 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.
+
+-->
+<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
+ <include relativeToChangelogFile="true" file="parts/0001_command_init.xml"/>
+ <include relativeToChangelogFile="true" file="parts/0002_command_fix_structure.xml"/>
+</databaseChangeLog>
diff --git a/fineract-command/src/main/resources/db/custom-changelog/0001_command_init.xml b/fineract-command/src/main/resources/db/changelog/tenant/module/command/parts/0001_command_init.xml
similarity index 88%
rename from fineract-command/src/main/resources/db/custom-changelog/0001_command_init.xml
rename to fineract-command/src/main/resources/db/changelog/tenant/module/command/parts/0001_command_init.xml
index 0b23bcf374..d2069bcf99 100644
--- a/fineract-command/src/main/resources/db/custom-changelog/0001_command_init.xml
+++ b/fineract-command/src/main/resources/db/changelog/tenant/module/command/parts/0001_command_init.xml
@@ -23,6 +23,11 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<changeSet author="fineract" id="command-00001" context="postgresql">
+ <preConditions onFail="MARK_RAN">
+ <not>
+ <tableExists tableName="m_command"/>
+ </not>
+ </preConditions>
<createTable tableName="m_command">
<column autoIncrement="true" name="id" type="bigint" remarks="Internal ID">
<constraints nullable="false" primaryKey="true"/>
@@ -35,6 +40,11 @@
</createTable>
</changeSet>
<changeSet author="fineract" id="command-00002" context="mysql">
+ <preConditions onFail="MARK_RAN">
+ <not>
+ <tableExists tableName="command"/>
+ </not>
+ </preConditions>
<createTable tableName="command">
<column autoIncrement="true" name="id" type="bigint" remarks="Internal ID">
<constraints nullable="false" primaryKey="true"/>
diff --git a/fineract-command/src/main/resources/db/changelog/tenant/module/command/parts/0002_command_fix_structure.xml b/fineract-command/src/main/resources/db/changelog/tenant/module/command/parts/0002_command_fix_structure.xml
new file mode 100644
index 0000000000..9e561ca181
--- /dev/null
+++ b/fineract-command/src/main/resources/db/changelog/tenant/module/command/parts/0002_command_fix_structure.xml
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+ 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.
+
+-->
+<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
+ <changeSet author="fineract" id="command-00002" context="mysql">
+ <renameTable oldTableName="command" newTableName="m_command" />
+ <addColumn tableName="m_command">
+ <column name="updated_at" type="datetime" />
+ <column name="executed_at" type="datetime" />
+ <column name="approved_at" type="datetime" />
+ <column name="rejected_at" type="datetime" />
+ </addColumn>
+ </changeSet>
+ <changeSet author="fineract" id="command-00003" context="postgresql">
+ <addColumn tableName="m_command">
+ <column name="updated_at" type="timestamp with time zone" />
+ <column name="executed_at" type="timestamp with time zone" />
+ <column name="approved_at" type="timestamp with time zone" />
+ <column name="rejected_at" type="timestamp with time zone" />
+ </addColumn>
+ </changeSet>
+ <changeSet author="fineract" id="command-00004" context="postgresql,mysql">
+ <renameColumn tableName="m_command" oldColumnName="username" newColumnName="initiated_by_username" columnDataType="varchar(255)" />
+ <renameColumn tableName="m_command" oldColumnName="payload" newColumnName="request" columnDataType="text" />
+
+ <addColumn tableName="m_command">
+ <column name="idempotency_key" type="varchar(255)" />
+ <column name="executed_by_username" type="varchar(255)" />
+ <column name="approved_by_username" type="varchar(255)" />
+ <column name="rejected_by_username" type="varchar(255)" />
+ <column name="ip_address" type="varchar(255)" />
+ <column name="state" type="varchar(255)" />
+ <column name="response" type="text" />
+ <column name="error" type="text" />
+ </addColumn>
+ <createIndex tableName="m_command" indexName="m_command_idempotency_key_index">
+ <column name="idempotency_key" />
+ </createIndex>
+ </changeSet>
+</databaseChangeLog>
diff --git a/fineract-command/src/main/resources/jpa/static-weaving/module/fineract-command/persistence.xml b/fineract-command/src/main/resources/jpa/static-weaving/module/fineract-command/persistence.xml
deleted file mode 100644
index 465ba382f9..0000000000
--- a/fineract-command/src/main/resources/jpa/static-weaving/module/fineract-command/persistence.xml
+++ /dev/null
@@ -1,43 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-
- 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.
-
--->
-
-
-<persistence version="2.0"
- xmlns="http://java.sun.com/xml/ns/persistence"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
-
- <!-- This file is only used for static weaving, nothing more. -->
- <!-- You can find the runtime configuration in the JPAConfig class -->
- <persistence-unit name="jpa-pu" transaction-type="RESOURCE_LOCAL">
- <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
-
- <!-- Command Module Entities -->
- <class>org.apache.fineract.command.persistence.domain.CommandEntity</class>
- <class>org.apache.fineract.command.persistence.converter.JsonAttributeConverter</class>
-
- <exclude-unlisted-classes>false</exclude-unlisted-classes>
- <properties>
- <property name="eclipselink.weaving" value="static" />
- </properties>
- </persistence-unit>
-</persistence>
diff --git a/fineract-command/src/test/java/org/apache/fineract/command/CommandBaseTest.java b/fineract-command/src/test/java/org/apache/fineract/command/CommandBaseTest.java
index bbcaf3d06e..29ec3c6b3d 100644
--- a/fineract-command/src/test/java/org/apache/fineract/command/CommandBaseTest.java
+++ b/fineract-command/src/test/java/org/apache/fineract/command/CommandBaseTest.java
@@ -18,7 +18,6 @@
*/
package org.apache.fineract.command;
-import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.command.persistence.domain.CommandRepository;
import org.apache.fineract.command.persistence.mapping.CommandMapper;
@@ -27,9 +26,12 @@ import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
-import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.MariaDBContainer;
+import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.containers.Network;
+import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.utility.DockerImageName;
@Slf4j
@SpringBootTest
@@ -40,9 +42,16 @@ abstract class CommandBaseTest {
protected static Network network = Network.newNetwork();
@Container
- protected static final GenericContainer POSTGRES_CONTAINER = new GenericContainer("postgres:16").withNetwork(network)
- .withNetworkAliases("postgres").withExposedPorts(5432)
- .withEnv(Map.of("POSTGRES_DB", "fineract-test", "POSTGRES_USER", "root", "POSTGRES_PASSWORD", "mifos"));
+ private static final PostgreSQLContainer<?> POSTGRES_CONTAINER = new PostgreSQLContainer<>(DockerImageName.parse("postgres:16"))
+ .withNetwork(network).withUsername("root").withPassword("mifos").withDatabaseName("fineract-test");
+
+ @Container
+ private static final MariaDBContainer<?> MARIADB_CONTAINER = new MariaDBContainer<>(DockerImageName.parse("mariadb:11.4"))
+ .withNetwork(network).withUsername("root").withPassword("mifos").withDatabaseName("fineract-test");
+
+ @Container
+ private static final MySQLContainer<?> MYSQL_CONTAINER = new MySQLContainer<>(DockerImageName.parse("mysql:8")).withNetwork(network)
+ .withUsername("root").withPassword("mifos").withDatabaseName("fineract-test");
@Autowired
protected CommandRepository commandRepository;
@@ -52,13 +61,43 @@ abstract class CommandBaseTest {
@DynamicPropertySource
protected static void configure(DynamicPropertyRegistry registry) {
+ postgres(registry);
+ // mariadb(registry);
+ // mysql(registry);
+ }
+
+ private static void postgres(DynamicPropertyRegistry registry) {
POSTGRES_CONTAINER.start();
- registry.add("spring.datasource.driver-class-name", () -> "org.postgresql.Driver");
- registry.add("spring.datasource.username", () -> "root");
- registry.add("spring.datasource.password", () -> "mifos");
- registry.add("spring.datasource.url", () -> "jdbc:postgresql://" + POSTGRES_CONTAINER.getHost() + ":"
- + POSTGRES_CONTAINER.getMappedPort(5432) + "/fineract-test");
+ registry.add("spring.datasource.driver-class-name", POSTGRES_CONTAINER::getDriverClassName);
+ registry.add("spring.datasource.username", POSTGRES_CONTAINER::getUsername);
+ registry.add("spring.datasource.password", POSTGRES_CONTAINER::getPassword);
+ registry.add("spring.datasource.url", POSTGRES_CONTAINER::getJdbcUrl);
registry.add("spring.datasource.platform", () -> "postgresql");
+ registry.add("spring.liquibase.contexts", () -> "postgresql");
+ }
+
+ @SuppressWarnings("UnusedMethod")
+ private static void mariadb(DynamicPropertyRegistry registry) {
+ MARIADB_CONTAINER.start();
+
+ registry.add("spring.datasource.driver-class-name", MARIADB_CONTAINER::getDriverClassName);
+ registry.add("spring.datasource.username", MARIADB_CONTAINER::getUsername);
+ registry.add("spring.datasource.password", MARIADB_CONTAINER::getPassword);
+ registry.add("spring.datasource.url", MARIADB_CONTAINER::getJdbcUrl);
+ registry.add("spring.datasource.platform", () -> "mysql");
+ registry.add("spring.liquibase.contexts", () -> "mysql");
+ }
+
+ @SuppressWarnings("UnusedMethod")
+ private static void mysql(DynamicPropertyRegistry registry) {
+ MYSQL_CONTAINER.start();
+
+ registry.add("spring.datasource.driver-class-name", MYSQL_CONTAINER::getDriverClassName);
+ registry.add("spring.datasource.username", MYSQL_CONTAINER::getUsername);
+ registry.add("spring.datasource.password", MYSQL_CONTAINER::getPassword);
+ registry.add("spring.datasource.url", MYSQL_CONTAINER::getJdbcUrl);
+ registry.add("spring.datasource.platform", () -> "mysql");
+ registry.add("spring.liquibase.contexts", () -> "mysql");
}
}
diff --git a/fineract-command/src/test/java/org/apache/fineract/command/CommandPersistenceTest.java b/fineract-command/src/test/java/org/apache/fineract/command/CommandPersistenceTest.java
index 3d6142e9f3..d0959cd667 100644
--- a/fineract-command/src/test/java/org/apache/fineract/command/CommandPersistenceTest.java
+++ b/fineract-command/src/test/java/org/apache/fineract/command/CommandPersistenceTest.java
@@ -18,10 +18,13 @@
*/
package org.apache.fineract.command;
-import java.util.UUID;
+import static org.apache.fineract.command.persistence.domain.CommandEntityState.ERROR;
+
+import java.time.Instant;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.command.sample.command.DummyCommand;
import org.apache.fineract.command.sample.data.DummyRequest;
+import org.apache.fineract.command.sample.data.DummyResponse;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ContextConfiguration;
@@ -35,10 +38,25 @@ class CommandPersistenceTest extends CommandBaseTest {
void save() {
var content = "hello";
var command = new DummyCommand();
- command.setId(UUID.randomUUID());
+ var now = Instant.now();
+
command.setPayload(DummyRequest.builder().content(content).build());
+ command.setIdempotencyKey("1234567890");
+ command.setIpAddress("127.0.0.1");
+ command.setCreatedAt(now);
+ command.setExecutedAt(now);
+ command.setApprovedAt(now);
+ command.setRejectedAt(now);
+ command.setInitiatedByUsername("abc");
+ command.setExecutedByUsername("abc");
+ command.setApprovedByUsername("abc");
+ command.setRejectedByUsername("abc");
+
+ var response = DummyResponse.builder().tenantId("dummy").content(content).build();
- var commandEntity = commandMapper.map(command);
+ var commandEntity = commandMapper.map(command, response);
+ commandEntity.setState(ERROR);
+ commandEntity.setError("Some error message");
var result = commandRepository.save(commandEntity);
diff --git a/fineract-command/src/test/java/org/apache/fineract/command/CommandPipelineBenchmark.java b/fineract-command/src/test/java/org/apache/fineract/command/CommandPipelineBenchmark.java
index c2238c9e3d..403301b57a 100644
--- a/fineract-command/src/test/java/org/apache/fineract/command/CommandPipelineBenchmark.java
+++ b/fineract-command/src/test/java/org/apache/fineract/command/CommandPipelineBenchmark.java
@@ -18,27 +18,31 @@
*/
package org.apache.fineract.command;
+import static org.mockito.Mockito.mock;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
import com.lmax.disruptor.YieldingWaitStrategy;
import com.lmax.disruptor.dsl.Disruptor;
import com.lmax.disruptor.dsl.ProducerType;
import com.lmax.disruptor.util.DaemonThreadFactory;
import java.util.List;
-import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.command.core.CommandPipeline;
-import org.apache.fineract.command.core.CommandRouter;
+import org.apache.fineract.command.core.CommandProperties;
+import org.apache.fineract.command.implementation.DefaultCommandAuditor;
import org.apache.fineract.command.implementation.DefaultCommandPipeline;
import org.apache.fineract.command.implementation.DefaultCommandRouter;
import org.apache.fineract.command.implementation.DisruptorCommandExecutor;
+import org.apache.fineract.command.persistence.domain.CommandRepository;
+import org.apache.fineract.command.persistence.mapping.CommandJsonMapper;
+import org.apache.fineract.command.persistence.mapping.CommandMapperImpl;
import org.apache.fineract.command.sample.command.DummyCommand;
import org.apache.fineract.command.sample.data.DummyRequest;
import org.apache.fineract.command.sample.data.DummyResponse;
import org.apache.fineract.command.sample.handler.DummyCommandHandler;
-import org.apache.fineract.command.sample.middleware.DummyIdempotencyMiddleware;
-import org.apache.fineract.command.sample.middleware.DummyMiddleware;
import org.apache.fineract.command.sample.service.DefaultDummyService;
import org.apache.fineract.command.sample.service.DefaultDummyTenantService;
import org.openjdk.jmh.annotations.Benchmark;
@@ -58,26 +62,37 @@ import org.openjdk.jmh.annotations.TearDown;
@SuppressWarnings({ "raw" })
public class CommandPipelineBenchmark {
- private CommandRouter router;
private Disruptor<DisruptorCommandExecutor.CommandEvent> disruptor;
private CommandPipeline pipeline;
@Setup(Level.Iteration)
public void setUp() {
- this.router = new DefaultCommandRouter(List.of(new DummyCommandHandler(new DefaultDummyService(new DefaultDummyTenantService()))));
+ // create router
+ var router = new DefaultCommandRouter(List.of(new DummyCommandHandler(new DefaultDummyService(new DefaultDummyTenantService()))));
+
+ // create mapper
+ var mapper = new CommandMapperImpl(new CommandJsonMapper(new ObjectMapper()));
+
+ // mock repository
+ var repository = mock(CommandRepository.class);
+
+ // default properties
+ var properties = new CommandProperties();
+
+ // create auditor
+ var auditor = new DefaultCommandAuditor(mapper, repository, properties, new ObjectMapper());
- // Create the disruptor
+ // create the disruptor
this.disruptor = new Disruptor<>(DisruptorCommandExecutor.CommandEvent::new, 2048, DaemonThreadFactory.INSTANCE, ProducerType.MULTI,
new YieldingWaitStrategy());
- disruptor.handleEventsWith(new DisruptorCommandExecutor.CompleteableCommandEventHandler(
- List.of(new DummyMiddleware(), new DummyIdempotencyMiddleware()), router));
+ disruptor.handleEventsWith(new DisruptorCommandExecutor.CompleteableCommandEventHandler(router));
- // Start the disruptor
+ // start the disruptor
disruptor.start();
- pipeline = new DefaultCommandPipeline(new DisruptorCommandExecutor(disruptor));
+ pipeline = new DefaultCommandPipeline(new DisruptorCommandExecutor(disruptor, router, auditor), properties);
}
@TearDown(Level.Iteration)
@@ -89,7 +104,6 @@ public class CommandPipelineBenchmark {
@Benchmark
public void processCommand() {
var command = new DummyCommand();
- command.setId(UUID.randomUUID());
command.setPayload(DummyRequest.builder().content("hello").build());
Supplier<DummyResponse> result = pipeline.send(command);
diff --git a/fineract-command/src/test/java/org/apache/fineract/command/CommandPipelineTest.java b/fineract-command/src/test/java/org/apache/fineract/command/CommandPipelineTest.java
index 26994bc23a..51001efbfb 100644
--- a/fineract-command/src/test/java/org/apache/fineract/command/CommandPipelineTest.java
+++ b/fineract-command/src/test/java/org/apache/fineract/command/CommandPipelineTest.java
@@ -21,14 +21,20 @@ package org.apache.fineract.command;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import java.util.Locale;
-import java.util.UUID;
+import java.util.function.Supplier;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.command.core.CommandPipeline;
+import org.apache.fineract.command.persistence.domain.CommandRepository;
import org.apache.fineract.command.sample.command.DummyCommand;
+import org.apache.fineract.command.sample.command.DummyErrorCommand;
+import org.apache.fineract.command.sample.data.DummyErrorRequest;
+import org.apache.fineract.command.sample.data.DummyErrorResponse;
import org.apache.fineract.command.sample.data.DummyRequest;
import org.apache.fineract.command.sample.data.DummyResponse;
+import org.apache.fineract.command.sample.exception.DummyException;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@@ -42,29 +48,54 @@ class CommandPipelineTest extends CommandBaseTest {
@Autowired
private CommandPipeline pipeline;
+ @Autowired
+ private CommandRepository repository;
+
@Test
- void processCommand() {
+ void process() {
var content = "hello";
var command = new DummyCommand();
- command.setId(UUID.randomUUID());
+
command.setPayload(DummyRequest.builder().content(content).build());
+ command.setIdempotencyKey("1234567890");
+ command.setIpAddress("127.0.0.1");
+ command.setInitiatedByUsername("abc");
- var result = pipeline.send(command);
+ Supplier<DummyResponse> result = pipeline.send(command);
assertNotNull(result, "Response should not be null.");
- Object response = result.get();
+ var response = result.get();
assertNotNull(response, "Response should not be null.");
assertInstanceOf(DummyResponse.class, response, "Response is of wrong type.");
- if (response instanceof DummyResponse dummyResponse) {
- log.warn("Result: {}", dummyResponse);
+ log.info("Result: {}", response);
+ log.info("Command ID: {}", command.getCommandId());
+
+ assertNotNull(command.getCommandId(), "Command should have an ID after processing.");
+ assertNotNull(response.getContent(), "Response body should not be null.");
+ assertEquals(content.toUpperCase(Locale.ROOT), response.getContent(), "Wrong response content.");
+ }
+
+ @Test
+ void error() {
+ var content = "hello error";
+ var command = new DummyErrorCommand();
+
+ command.setPayload(DummyErrorRequest.builder().content(content).build());
+
+ Supplier<DummyErrorResponse> result = pipeline.send(command);
+
+ var exception = assertThrows(DummyException.class, () -> {
+ var response = result.get();
+ });
+
+ assertNotNull(exception, "Expected exception!");
+
+ var commands = repository.findAll();
- assertNotNull(dummyResponse.getContent(), "Response body should not be null.");
- assertNotNull(dummyResponse.getRequestId(), "Request ID should not be null.");
- assertEquals(content.toUpperCase(Locale.ROOT), dummyResponse.getContent(), "Wrong response content.");
- }
+ log.info("All commands: {}", commands);
}
}
diff --git a/fineract-command/src/test/java/org/apache/fineract/command/CommandSampleApiTest.java b/fineract-command/src/test/java/org/apache/fineract/command/CommandSampleApiTest.java
index 5927a3414b..12abbbca79 100644
--- a/fineract-command/src/test/java/org/apache/fineract/command/CommandSampleApiTest.java
+++ b/fineract-command/src/test/java/org/apache/fineract/command/CommandSampleApiTest.java
@@ -18,7 +18,8 @@
*/
package org.apache.fineract.command;
-import static org.apache.fineract.command.core.CommandConstants.COMMAND_REQUEST_ID;
+import static org.apache.fineract.command.core.CommandConstants.COMMAND_HTTP_HEADER_REQUEST_ID;
+import static org.apache.fineract.command.core.CommandConstants.COMMAND_HTTP_HEADER_TENANT_ID;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -35,14 +36,19 @@ import java.util.Locale;
import java.util.Map;
import java.util.UUID;
import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.command.core.CommandAuditor;
+import org.apache.fineract.command.core.CommandProperties;
import org.apache.fineract.command.sample.data.DummyRequest;
import org.apache.fineract.command.sample.data.DummyResponse;
import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.web.server.LocalServerPort;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
import org.springframework.http.ProblemDetail;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.test.context.ContextConfiguration;
@@ -63,13 +69,19 @@ class CommandSampleApiTest extends CommandBaseTest {
@Autowired
private TestRestTemplate restTemplate;
+ @Autowired
+ private CommandAuditor auditor;
+
+ @Autowired
+ private CommandProperties properties;
+
@BeforeEach
public void setUp() {
this.baseUrl = "http://localhost:" + port + "/test/dummy";
- this.interceptors = Collections.singletonList((request, body, execution) -> {
+ this.interceptors = List.of((request, body, execution) -> {
var headers = request.getHeaders();
- headers.add(COMMAND_REQUEST_ID, UUID.randomUUID().toString());
- headers.add("x-fineract-tenant-id", "dummy");
+ headers.add(COMMAND_HTTP_HEADER_REQUEST_ID, UUID.randomUUID().toString());
+ headers.add(COMMAND_HTTP_HEADER_TENANT_ID, "dummy");
headers.add(CONTENT_TYPE, APPLICATION_JSON_VALUE);
headers.addAll(ACCEPT, List.of(APPLICATION_JSON_VALUE, APPLICATION_PROBLEM_JSON_VALUE));
return execution.execute(request, body);
@@ -88,45 +100,86 @@ class CommandSampleApiTest extends CommandBaseTest {
@Test
void dummyApiSync() {
- var content = "test-sync";
+ final var content = "test-sync";
+ final var idempotencyKey = UUID.randomUUID().toString();
restTemplate.getRestTemplate().setInterceptors(interceptors);
- var result = restTemplate.postForObject(baseUrl + "/sync", DummyRequest.builder().content(content).build(), DummyResponse.class);
- log.warn("Result (sync) : {} ({})", result.getContent(), result.getRequestId());
+ final var headers = new HttpHeaders();
+ headers.add(properties.getIdemPotencyKeyHeaderName(), idempotencyKey);
+
+ final var request = new HttpEntity<>(DummyRequest.builder().content(content).build(), headers);
+
+ final var result = restTemplate.postForObject(baseUrl + "/sync", request, DummyResponse.class);
+
+ log.warn("Result (sync) : {}", result.getContent());
assertNotNull(result, "Response should not be null.");
assertNotNull(result.getContent(), "Response body should not be null.");
- assertNotNull(result.getRequestId(), "Request ID should not be null.");
assertNotNull(result.getTenantId(), "Tenant ID should not be null.");
assertEquals("dummy", result.getTenantId(), "Unexpected tenant ID.");
assertEquals(content.toUpperCase(Locale.ROOT), result.getContent(), "Wrong response content.");
+
+ var response = auditor.getResponseByIdempotencyKey(idempotencyKey);
+
+ if (response instanceof DummyResponse dummyResponse) {
+ assertNotNull(dummyResponse, "There should be a command entity stored with a dummyResponse.");
+ assertNotNull(dummyResponse, "Response should not be null.");
+ assertNotNull(dummyResponse.getContent(), "Response body should not be null.");
+ assertNotNull(dummyResponse.getTenantId(), "Tenant ID should not be null.");
+ assertEquals("dummy", dummyResponse.getTenantId(), "Unexpected tenant ID.");
+ assertEquals(content.toUpperCase(Locale.ROOT), dummyResponse.getContent(), "Wrong response content.");
+ }
+
+ log.warn("Command response: {}", response);
}
@Test
void dummyApiAsync() {
- var content = "test-async";
+ final var content = "test-async";
+ final var idempotencyKey = UUID.randomUUID().toString();
restTemplate.getRestTemplate().setInterceptors(interceptors);
- var result = restTemplate.postForObject(baseUrl + "/async", DummyRequest.builder().content(content).build(), DummyResponse.class);
- log.warn("Result (async): {} ({})", result.getContent(), result.getRequestId());
+ final var headers = new HttpHeaders();
+ headers.add(properties.getIdemPotencyKeyHeaderName(), idempotencyKey);
+
+ final var request = new HttpEntity<>(DummyRequest.builder().content(content).build(), headers);
+
+ final var result = restTemplate.postForObject(baseUrl + "/async", request, DummyResponse.class);
+
+ log.warn("Result (async): {}", result.getContent());
assertNotNull(result, "Response should not be null.");
assertNotNull(result.getContent(), "Response body should not be null.");
- assertNotNull(result.getRequestId(), "Request ID should not be null.");
// TODO: make sure all ThreadLocal variables are available
// assertNotNull(result.getTenantId(), "Tenant ID should not be null.");
// assertEquals("dummy", result.getTenantId(), "Unexpected tenant ID.");
assertEquals(content.toUpperCase(Locale.ROOT), result.getContent(), "Wrong response content.");
+
+ var response = auditor.getResponseByIdempotencyKey(idempotencyKey);
+
+ // TODO: always null; need to fix this before we release async mode
+ if (response instanceof DummyResponse dummyResponse) {
+ assertNotNull(dummyResponse, "There should be a command entity stored with a dummyResponse.");
+ assertNotNull(dummyResponse, "Response should not be null.");
+ assertNotNull(dummyResponse.getContent(), "Response body should not be null.");
+ assertNotNull(dummyResponse.getTenantId(), "Tenant ID should not be null.");
+ assertEquals("dummy", dummyResponse.getTenantId(), "Unexpected tenant ID.");
+ assertEquals(content.toUpperCase(Locale.ROOT), dummyResponse.getContent(), "Wrong response content.");
+ }
+
+ log.warn("Command response: {}", response);
}
@Test
+ @Disabled // TODO: implement idempotency properly with backwards compatibility
void dummyApiIdempotencyAsync() {
dummyApiIdempotency("/async");
}
@Test
+ @Disabled // TODO: implement idempotency properly with backwards compatibility
void dummyApiIdempotencySync() {
dummyApiIdempotency("/sync");
}
@@ -137,8 +190,8 @@ class CommandSampleApiTest extends CommandBaseTest {
var request = DummyRequest.builder().content(content).build();
List<ClientHttpRequestInterceptor> interceptors = Collections.singletonList((req, body, execution) -> {
var headers = req.getHeaders();
- headers.add(COMMAND_REQUEST_ID, id);
- headers.add("x-fineract-tenant-id", "dummy");
+ headers.add(COMMAND_HTTP_HEADER_REQUEST_ID, id);
+ headers.add(COMMAND_HTTP_HEADER_TENANT_ID, "dummy");
headers.add(CONTENT_TYPE, APPLICATION_JSON_VALUE);
headers.addAll(ACCEPT, List.of(APPLICATION_JSON_VALUE, APPLICATION_PROBLEM_JSON_VALUE));
return execution.execute(req, body);
@@ -154,7 +207,7 @@ class CommandSampleApiTest extends CommandBaseTest {
// second request fails, because we are using the same request ID in both cases
response = restTemplate.postForEntity(baseUrl + path, request, Map.class);
- log.warn("Body: {} - {}", response.getBody(), response.getStatusCode());
+ log.info("Body: {} - {}", response.getBody(), response.getStatusCode());
// Assert HTTP status
assertThat(response.getStatusCode()).isEqualTo(INTERNAL_SERVER_ERROR);
diff --git a/fineract-command/src/test/java/org/apache/fineract/command/TestConfiguration.java b/fineract-command/src/test/java/org/apache/fineract/command/TestConfiguration.java
index fa42c6745e..8f20d0c5ef 100644
--- a/fineract-command/src/test/java/org/apache/fineract/command/TestConfiguration.java
+++ b/fineract-command/src/test/java/org/apache/fineract/command/TestConfiguration.java
@@ -21,19 +21,16 @@ package org.apache.fineract.command;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.command.core.CommandProperties;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
-import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
-import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.scheduling.annotation.EnableAsync;
@Slf4j
@Configuration
-@EnableConfigurationProperties({ CommandProperties.class, JpaProperties.class })
+@EnableConfigurationProperties({ CommandProperties.class })
@EnableAutoConfiguration
-@EnableJpaRepositories(basePackages = { "org.apache.fineract.command.persistence" })
@EnableAsync
@PropertySource("classpath:application-test.properties")
@ComponentScan("org.apache.fineract.command.sample")
diff --git a/fineract-command/src/test/java/org/apache/fineract/command/sample/api/DummyApiController.java b/fineract-command/src/test/java/org/apache/fineract/command/sample/api/DummyApiController.java
index e8c19a7178..044a9c235f 100644
--- a/fineract-command/src/test/java/org/apache/fineract/command/sample/api/DummyApiController.java
+++ b/fineract-command/src/test/java/org/apache/fineract/command/sample/api/DummyApiController.java
@@ -18,13 +18,10 @@
*/
package org.apache.fineract.command.sample.api;
-import static org.apache.fineract.command.core.CommandConstants.COMMAND_REQUEST_ID;
+import static org.apache.fineract.command.core.CommandConstants.COMMAND_HTTP_HEADER_TENANT_ID;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
import static org.springframework.http.MediaType.APPLICATION_PROBLEM_JSON_VALUE;
-import java.time.OffsetDateTime;
-import java.time.ZoneId;
-import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.function.Supplier;
import lombok.RequiredArgsConstructor;
@@ -47,19 +44,16 @@ import org.springframework.web.bind.annotation.RestController;
@RequestMapping(value = "/test/dummy", consumes = APPLICATION_JSON_VALUE, produces = { APPLICATION_JSON_VALUE,
APPLICATION_PROBLEM_JSON_VALUE })
class DummyApiController {
- // "application/vnd.fineract+json;charset=UTF-8;version=1.0"
private final DefaultDummyTenantService tenantService;
private final CommandPipeline pipeline;
@PostMapping("/sync")
- DummyResponse dummySync(@RequestHeader(value = COMMAND_REQUEST_ID, required = false) UUID requestId,
- @RequestHeader(value = "x-fineract-tenant-id", required = false) String tenantId, @RequestBody DummyRequest request) {
+ DummyResponse dummySync(@RequestHeader(value = COMMAND_HTTP_HEADER_TENANT_ID, required = false) String tenantId,
+ @RequestBody DummyRequest request) {
var command = new DummyCommand();
- command.setId(requestId);
command.setPayload(request);
- command.setCreatedAt(OffsetDateTime.now(ZoneId.of("UTC")));
tenantService.set(tenantId);
@@ -70,10 +64,9 @@ class DummyApiController {
@Async
@PostMapping("/async")
- CompletableFuture<DummyResponse> dummyAsync(@RequestHeader(value = COMMAND_REQUEST_ID, required = false) UUID requestId,
- @RequestHeader(value = "x-fineract-tenant-id", required = false) String tenantId, @RequestBody DummyRequest request) {
+ CompletableFuture<DummyResponse> dummyAsync(@RequestHeader(value = COMMAND_HTTP_HEADER_TENANT_ID, required = false) String tenantId,
+ @RequestBody DummyRequest request) {
var command = new DummyCommand();
- command.setId(requestId);
command.setPayload(request);
tenantService.set(tenantId);
diff --git a/fineract-command/src/main/java/org/apache/fineract/command/core/CommandConstants.java b/fineract-command/src/test/java/org/apache/fineract/command/sample/command/DummyErrorCommand.java
similarity index 70%
copy from fineract-command/src/main/java/org/apache/fineract/command/core/CommandConstants.java
copy to fineract-command/src/test/java/org/apache/fineract/command/sample/command/DummyErrorCommand.java
index 110f86edbf..4ec3d8a283 100644
--- a/fineract-command/src/main/java/org/apache/fineract/command/core/CommandConstants.java
+++ b/fineract-command/src/test/java/org/apache/fineract/command/sample/command/DummyErrorCommand.java
@@ -16,12 +16,13 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.apache.fineract.command.core;
+package org.apache.fineract.command.sample.command;
-import lombok.experimental.UtilityClass;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.apache.fineract.command.core.Command;
+import org.apache.fineract.command.sample.data.DummyErrorRequest;
-@UtilityClass
-public final class CommandConstants {
-
- public static final String COMMAND_REQUEST_ID = "x-fineract-request-id";
-}
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class DummyErrorCommand extends Command<DummyErrorRequest> {}
diff --git a/fineract-command/src/test/java/org/apache/fineract/command/sample/data/DummyRequest.java b/fineract-command/src/test/java/org/apache/fineract/command/sample/data/DummyErrorRequest.java
similarity index 93%
copy from fineract-command/src/test/java/org/apache/fineract/command/sample/data/DummyRequest.java
copy to fineract-command/src/test/java/org/apache/fineract/command/sample/data/DummyErrorRequest.java
index 394afa5ecf..ed4ac77f8a 100644
--- a/fineract-command/src/test/java/org/apache/fineract/command/sample/data/DummyRequest.java
+++ b/fineract-command/src/test/java/org/apache/fineract/command/sample/data/DummyErrorRequest.java
@@ -21,7 +21,6 @@ package org.apache.fineract.command.sample.data;
import jakarta.validation.constraints.NotBlank;
import java.io.Serial;
import java.io.Serializable;
-import java.util.UUID;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
@@ -33,13 +32,11 @@ import lombok.experimental.FieldNameConstants;
@NoArgsConstructor
@AllArgsConstructor
@FieldNameConstants
-public class DummyRequest implements Serializable {
+public class DummyErrorRequest implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
- private UUID id;
-
@NotBlank(message = "{org.apache.fineract.dummy.request.content.not-empty}")
private String content;
}
diff --git a/fineract-command/src/test/java/org/apache/fineract/command/sample/data/DummyResponse.java b/fineract-command/src/test/java/org/apache/fineract/command/sample/data/DummyErrorResponse.java
similarity index 90%
copy from fineract-command/src/test/java/org/apache/fineract/command/sample/data/DummyResponse.java
copy to fineract-command/src/test/java/org/apache/fineract/command/sample/data/DummyErrorResponse.java
index a839e21b5f..b7300c7612 100644
--- a/fineract-command/src/test/java/org/apache/fineract/command/sample/data/DummyResponse.java
+++ b/fineract-command/src/test/java/org/apache/fineract/command/sample/data/DummyErrorResponse.java
@@ -20,7 +20,6 @@ package org.apache.fineract.command.sample.data;
import java.io.Serial;
import java.io.Serializable;
-import java.util.UUID;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
@@ -32,16 +31,11 @@ import lombok.experimental.FieldNameConstants;
@NoArgsConstructor
@AllArgsConstructor
@FieldNameConstants
-public class DummyResponse implements Serializable {
+public class DummyErrorResponse implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
- private UUID requestId;
-
- private String tenantId;
-
private String content;
-
private String error;
}
diff --git a/fineract-command/src/test/java/org/apache/fineract/command/sample/data/DummyRequest.java b/fineract-command/src/test/java/org/apache/fineract/command/sample/data/DummyRequest.java
index 394afa5ecf..08b2fb32ee 100644
--- a/fineract-command/src/test/java/org/apache/fineract/command/sample/data/DummyRequest.java
+++ b/fineract-command/src/test/java/org/apache/fineract/command/sample/data/DummyRequest.java
@@ -21,7 +21,6 @@ package org.apache.fineract.command.sample.data;
import jakarta.validation.constraints.NotBlank;
import java.io.Serial;
import java.io.Serializable;
-import java.util.UUID;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
@@ -38,8 +37,6 @@ public class DummyRequest implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
- private UUID id;
-
@NotBlank(message = "{org.apache.fineract.dummy.request.content.not-empty}")
private String content;
}
diff --git a/fineract-command/src/test/java/org/apache/fineract/command/sample/data/DummyResponse.java b/fineract-command/src/test/java/org/apache/fineract/command/sample/data/DummyResponse.java
index a839e21b5f..4f7d2d0b39 100644
--- a/fineract-command/src/test/java/org/apache/fineract/command/sample/data/DummyResponse.java
+++ b/fineract-command/src/test/java/org/apache/fineract/command/sample/data/DummyResponse.java
@@ -20,7 +20,6 @@ package org.apache.fineract.command.sample.data;
import java.io.Serial;
import java.io.Serializable;
-import java.util.UUID;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
@@ -37,11 +36,7 @@ public class DummyResponse implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
- private UUID requestId;
-
private String tenantId;
-
private String content;
-
private String error;
}
diff --git a/fineract-command/src/main/java/org/apache/fineract/command/core/CommandMiddleware.java b/fineract-command/src/test/java/org/apache/fineract/command/sample/exception/DummyException.java
similarity index 81%
rename from fineract-command/src/main/java/org/apache/fineract/command/core/CommandMiddleware.java
rename to fineract-command/src/test/java/org/apache/fineract/command/sample/exception/DummyException.java
index 1ae3b18646..fe79229131 100644
--- a/fineract-command/src/main/java/org/apache/fineract/command/core/CommandMiddleware.java
+++ b/fineract-command/src/test/java/org/apache/fineract/command/sample/exception/DummyException.java
@@ -16,10 +16,11 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.apache.fineract.command.core;
+package org.apache.fineract.command.sample.exception;
-@FunctionalInterface
-public interface CommandMiddleware {
+public class DummyException extends RuntimeException {
- void invoke(Command<?> command);
+ public DummyException(String message) {
+ super(message);
+ }
}
diff --git a/fineract-command/src/test/java/org/apache/fineract/command/sample/handler/DummyCommandHandler.java b/fineract-command/src/test/java/org/apache/fineract/command/sample/handler/DummyCommandHandler.java
index 32bbb4c436..2f29ef0a0b 100644
--- a/fineract-command/src/test/java/org/apache/fineract/command/sample/handler/DummyCommandHandler.java
+++ b/fineract-command/src/test/java/org/apache/fineract/command/sample/handler/DummyCommandHandler.java
@@ -18,6 +18,7 @@
*/
package org.apache.fineract.command.sample.handler;
+import io.github.resilience4j.retry.annotation.Retry;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.command.core.Command;
@@ -26,6 +27,7 @@ import org.apache.fineract.command.sample.data.DummyRequest;
import org.apache.fineract.command.sample.data.DummyResponse;
import org.apache.fineract.command.sample.service.DummyService;
import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
@Slf4j
@RequiredArgsConstructor
@@ -34,8 +36,16 @@ public class DummyCommandHandler implements CommandHandler<DummyRequest, DummyRe
private final DummyService dummyService;
+ @Retry(name = "commandDummy", fallbackMethod = "fallback")
+ @Transactional
@Override
public DummyResponse handle(Command<DummyRequest> command) {
return dummyService.process(command.getPayload());
}
+
+ @Override
+ public DummyResponse fallback(Command<DummyRequest> command, Throwable t) {
+ // NOTE: fallback method needs to be in the same class
+ return CommandHandler.super.fallback(command, t);
+ }
}
diff --git a/fineract-command/src/test/java/org/apache/fineract/command/sample/handler/DummyCommandHandler.java b/fineract-command/src/test/java/org/apache/fineract/command/sample/handler/DummyErrorCommandHandler.java
similarity index 70%
copy from fineract-command/src/test/java/org/apache/fineract/command/sample/handler/DummyCommandHandler.java
copy to fineract-command/src/test/java/org/apache/fineract/command/sample/handler/DummyErrorCommandHandler.java
index 32bbb4c436..0ad99c3ddf 100644
--- a/fineract-command/src/test/java/org/apache/fineract/command/sample/handler/DummyCommandHandler.java
+++ b/fineract-command/src/test/java/org/apache/fineract/command/sample/handler/DummyErrorCommandHandler.java
@@ -22,20 +22,18 @@ 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.command.sample.data.DummyRequest;
-import org.apache.fineract.command.sample.data.DummyResponse;
-import org.apache.fineract.command.sample.service.DummyService;
+import org.apache.fineract.command.sample.data.DummyErrorRequest;
+import org.apache.fineract.command.sample.data.DummyErrorResponse;
+import org.apache.fineract.command.sample.exception.DummyException;
import org.springframework.stereotype.Component;
@Slf4j
@RequiredArgsConstructor
@Component
-public class DummyCommandHandler implements CommandHandler<DummyRequest, DummyResponse> {
-
- private final DummyService dummyService;
+public class DummyErrorCommandHandler implements CommandHandler<DummyErrorRequest, DummyErrorResponse> {
@Override
- public DummyResponse handle(Command<DummyRequest> command) {
- return dummyService.process(command.getPayload());
+ public DummyErrorResponse handle(Command<DummyErrorRequest> command) {
+ throw new DummyException("Simulating a business logic error: " + command.getPayload().getContent());
}
}
diff --git a/fineract-command/src/test/java/org/apache/fineract/command/sample/middleware/DummyIdempotencyMiddleware.java b/fineract-command/src/test/java/org/apache/fineract/command/sample/middleware/DummyIdempotencyMiddleware.java
deleted file mode 100644
index 2fbf9e774b..0000000000
--- a/fineract-command/src/test/java/org/apache/fineract/command/sample/middleware/DummyIdempotencyMiddleware.java
+++ /dev/null
@@ -1,50 +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.command.sample.middleware;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.UUID;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.apache.fineract.command.core.Command;
-import org.apache.fineract.command.core.CommandMiddleware;
-import org.apache.fineract.command.core.exception.CommandIllegalArgumentException;
-import org.apache.fineract.command.sample.command.DummyCommand;
-import org.springframework.stereotype.Component;
-
-@Slf4j
-@RequiredArgsConstructor
-@Component
-public class DummyIdempotencyMiddleware implements CommandMiddleware {
-
- // NOTE: in production you would use of course a database or Redis
- private static final List<UUID> IDS = new ArrayList<>();
-
- @Override
- public void invoke(Command<?> command) {
- if (command instanceof DummyCommand c) {
- if (IDS.contains(c.getId())) {
- throw new CommandIllegalArgumentException(c, "Duplicate request ID: " + c.getId());
- }
-
- IDS.add(c.getId());
- }
- }
-}
diff --git a/fineract-command/src/test/java/org/apache/fineract/command/sample/service/DefaultDummyService.java b/fineract-command/src/test/java/org/apache/fineract/command/sample/service/DefaultDummyService.java
index 75e96591a8..1044394dca 100644
--- a/fineract-command/src/test/java/org/apache/fineract/command/sample/service/DefaultDummyService.java
+++ b/fineract-command/src/test/java/org/apache/fineract/command/sample/service/DefaultDummyService.java
@@ -34,7 +34,6 @@ public class DefaultDummyService implements DummyService {
@Override
public DummyResponse process(DummyRequest request) {
- return DummyResponse.builder().requestId(request.getId()).tenantId(tenantService.get())
- .content(request.getContent().toUpperCase(Locale.ROOT)).build();
+ return DummyResponse.builder().tenantId(tenantService.get()).content(request.getContent().toUpperCase(Locale.ROOT)).build();
}
}
diff --git a/fineract-command/src/test/resources/application-test.properties b/fineract-command/src/test/resources/application-test.properties
index 399bfa8664..e168500db2 100644
--- a/fineract-command/src/test/resources/application-test.properties
+++ b/fineract-command/src/test/resources/application-test.properties
@@ -26,9 +26,9 @@ fineract.command.enabled=true
fineract.command.executor=sync
fineract.command.ring-buffer-size=1024
fineract.command.producer-type=single
+fineract.command.auditable=true
spring.liquibase.enabled=true
spring.liquibase.drop-first=true
spring.liquibase.default-schema=public
-spring.liquibase.contexts=postgresql
-spring.liquibase.change-log=classpath:/db/custom-changelog/0001_command_init.xml
+spring.liquibase.change-log=classpath:/db/changelog/tenant/module/command/module-changelog-master.xml
diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/businessdate/api/BusinessDateApiResource.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/businessdate/api/BusinessDateApiResource.java
index ad9d127632..ff83077a92 100644
--- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/businessdate/api/BusinessDateApiResource.java
+++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/businessdate/api/BusinessDateApiResource.java
@@ -24,14 +24,12 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
-import jakarta.ws.rs.HeaderParam;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import java.util.List;
-import java.util.UUID;
import java.util.function.Supplier;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.command.core.CommandPipeline;
@@ -41,7 +39,6 @@ import org.apache.fineract.infrastructure.businessdate.data.api.BusinessDateUpda
import org.apache.fineract.infrastructure.businessdate.data.api.BusinessDateUpdateResponse;
import org.apache.fineract.infrastructure.businessdate.mapper.BusinessDateMapper;
import org.apache.fineract.infrastructure.businessdate.service.BusinessDateReadPlatformService;
-import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.springframework.stereotype.Component;
@RequiredArgsConstructor
@@ -75,14 +72,10 @@ public class BusinessDateApiResource {
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Update Business Date", description = "")
- public BusinessDateUpdateResponse updateBusinessDate(@HeaderParam("Idempotency-Key") String idempotencyKey,
- @Valid BusinessDateUpdateRequest request) {
+ public BusinessDateUpdateResponse updateBusinessDate(@Valid BusinessDateUpdateRequest request) {
final BusinessDateUpdateCommand command = new BusinessDateUpdateCommand();
- command.setId(UUID.randomUUID());
- command.setIdempotencyKey(idempotencyKey);
- command.setCreatedAt(DateUtils.getAuditOffsetDateTime());
command.setPayload(request);
final Supplier<BusinessDateUpdateResponse> response = commandPipeline.send(command);
diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/businessdate/handler/BusinessDateUpdateHandler.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/businessdate/handler/BusinessDateUpdateHandler.java
index 23f0f1ce1b..1c4623615d 100644
--- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/businessdate/handler/BusinessDateUpdateHandler.java
+++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/businessdate/handler/BusinessDateUpdateHandler.java
@@ -18,6 +18,7 @@
*/
package org.apache.fineract.infrastructure.businessdate.handler;
+import io.github.resilience4j.retry.annotation.Retry;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.command.core.Command;
@@ -38,6 +39,7 @@ public class BusinessDateUpdateHandler implements CommandHandler<BusinessDateUpd
private final BusinessDateWritePlatformService businessDateWritePlatformService;
private final BusinessDateMapper businessDateMapper;
+ @Retry(name = "commandBusinessDateUpdate", fallbackMethod = "fallback")
@Transactional
@Override
public BusinessDateUpdateResponse handle(Command<BusinessDateUpdateRequest> command) {
@@ -45,4 +47,10 @@ public class BusinessDateUpdateHandler implements CommandHandler<BusinessDateUpd
businessDateDto = businessDateWritePlatformService.updateBusinessDate(businessDateDto);
return businessDateMapper.mapUpdateResponse(businessDateDto);
}
+
+ @Override
+ public BusinessDateUpdateResponse fallback(Command<BusinessDateUpdateRequest> command, Throwable t) {
+ // NOTE: fallback method needs to be in the same class
+ return CommandHandler.super.fallback(command, t);
+ }
}
diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/cache/api/CacheApiResource.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/cache/api/CacheApiResource.java
index 4d4c857517..b9ccd55774 100644
--- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/cache/api/CacheApiResource.java
+++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/cache/api/CacheApiResource.java
@@ -28,7 +28,6 @@ import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import java.util.Collection;
-import java.util.UUID;
import java.util.function.Supplier;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.command.core.CommandPipeline;
@@ -37,7 +36,6 @@ import org.apache.fineract.infrastructure.cache.data.CacheData;
import org.apache.fineract.infrastructure.cache.data.CacheSwitchRequest;
import org.apache.fineract.infrastructure.cache.data.CacheSwitchResponse;
import org.apache.fineract.infrastructure.cache.service.RuntimeDelegatingCacheManager;
-import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
@@ -78,8 +76,6 @@ public class CacheApiResource {
public CacheSwitchResponse switchCache(@Valid CacheSwitchRequest request) {
final var command = new CacheSwitchCommand();
- command.setId(UUID.randomUUID());
- command.setCreatedAt(DateUtils.getAuditOffsetDateTime());
command.setPayload(request);
final Supplier<CacheSwitchResponse> response = commandPipeline.send(command);
diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/cache/handler/CacheSwitchCommandHandler.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/cache/handler/CacheSwitchCommandHandler.java
index f609863fa1..a07418a25c 100644
--- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/cache/handler/CacheSwitchCommandHandler.java
+++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/cache/handler/CacheSwitchCommandHandler.java
@@ -18,6 +18,7 @@
*/
package org.apache.fineract.infrastructure.cache.handler;
+import io.github.resilience4j.retry.annotation.Retry;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.command.core.Command;
@@ -36,6 +37,7 @@ public class CacheSwitchCommandHandler implements CommandHandler<CacheSwitchRequ
private final CacheWritePlatformService cacheService;
+ @Retry(name = "commandCacheSwitch", fallbackMethod = "fallback")
@Transactional
@Override
public CacheSwitchResponse handle(final Command<CacheSwitchRequest> command) {
@@ -45,4 +47,10 @@ public class CacheSwitchCommandHandler implements CommandHandler<CacheSwitchRequ
return CacheSwitchResponse.builder().changes(changes).cacheType(request.getCacheType()).build();
}
+
+ @Override
+ public CacheSwitchResponse fallback(Command<CacheSwitchRequest> command, Throwable t) {
+ // NOTE: fallback method needs to be in the same class
+ return CommandHandler.super.fallback(command, t);
+ }
}
diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/external/api/ExternalEventConfigurationApiResource.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/external/api/ExternalEventConfigurationApiResource.java
index 204c6b3de9..fb26622794 100644
--- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/external/api/ExternalEventConfigurationApiResource.java
+++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/external/api/ExternalEventConfigurationApiResource.java
@@ -23,16 +23,13 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
-import jakarta.ws.rs.HeaderParam;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
-import java.util.UUID;
import java.util.function.Supplier;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.command.core.CommandPipeline;
-import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.event.external.command.ExternalConfigurationsUpdateCommand;
import org.apache.fineract.infrastructure.event.external.data.ExternalEventConfigurationResponse;
import org.apache.fineract.infrastructure.event.external.data.ExternalEventConfigurationUpdateRequest;
@@ -61,13 +58,10 @@ public class ExternalEventConfigurationApiResource {
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Enable/Disable external events posting", description = "")
- public ExternalEventConfigurationUpdateResponse updateExternalEventConfigurations(@HeaderParam("Idempotency-Key") String idempotencyKey,
+ public ExternalEventConfigurationUpdateResponse updateExternalEventConfigurations(
@Valid ExternalEventConfigurationUpdateRequest request) {
final var command = new ExternalConfigurationsUpdateCommand();
- command.setId(UUID.randomUUID());
- command.setIdempotencyKey(idempotencyKey);
- command.setCreatedAt(DateUtils.getAuditOffsetDateTime());
command.setPayload(request);
final Supplier<ExternalEventConfigurationUpdateResponse> response = commandPipeline.send(command);
diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/external/handler/ExternalEventConfigurationUpdateHandler.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/external/handler/ExternalEventConfigurationUpdateHandler.java
index 2c7ca64b30..73598384ed 100644
--- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/external/handler/ExternalEventConfigurationUpdateHandler.java
+++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/external/handler/ExternalEventConfigurationUpdateHandler.java
@@ -18,6 +18,7 @@
*/
package org.apache.fineract.infrastructure.event.external.handler;
+import io.github.resilience4j.retry.annotation.Retry;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.command.core.Command;
@@ -36,9 +37,16 @@ public class ExternalEventConfigurationUpdateHandler
private final ExternalEventConfigurationWritePlatformService writePlatformService;
+ @Retry(name = "commandExternalEventConfigurationUpdate", fallbackMethod = "fallback")
@Transactional
@Override
public ExternalEventConfigurationUpdateResponse handle(Command<ExternalEventConfigurationUpdateRequest> command) {
return writePlatformService.updateConfigurations(command.getPayload());
}
+
+ @Override
+ public ExternalEventConfigurationUpdateResponse fallback(Command<ExternalEventConfigurationUpdateRequest> command, Throwable t) {
+ // NOTE: fallback method needs to be in the same class
+ return CommandHandler.super.fallback(command, t);
+ }
}
diff --git a/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/api/CurrenciesApiResource.java b/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/api/CurrenciesApiResource.java
index a1a0f6cdf8..ff3ac4e617 100644
--- a/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/api/CurrenciesApiResource.java
+++ b/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/api/CurrenciesApiResource.java
@@ -27,11 +27,9 @@ import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
-import java.util.UUID;
import java.util.function.Supplier;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.command.core.CommandPipeline;
-import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.organisation.monetary.command.CurrencyUpdateCommand;
import org.apache.fineract.organisation.monetary.data.CurrencyConfigurationData;
import org.apache.fineract.organisation.monetary.data.CurrencyUpdateRequest;
@@ -70,8 +68,6 @@ public class CurrenciesApiResource {
public CurrencyUpdateResponse updateCurrencies(@Valid CurrencyUpdateRequest request) {
final var command = new CurrencyUpdateCommand();
- command.setId(UUID.randomUUID());
- command.setCreatedAt(DateUtils.getAuditOffsetDateTime());
command.setPayload(request);
final Supplier<CurrencyUpdateResponse> response = commandPipeline.send(command);
diff --git a/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/handler/CurrencyUpdateCommandHandler.java b/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/handler/CurrencyUpdateCommandHandler.java
index 2a64efa814..3123448625 100644
--- a/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/handler/CurrencyUpdateCommandHandler.java
+++ b/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/handler/CurrencyUpdateCommandHandler.java
@@ -18,6 +18,7 @@
*/
package org.apache.fineract.organisation.monetary.handler;
+import io.github.resilience4j.retry.annotation.Retry;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.command.core.Command;
@@ -35,9 +36,17 @@ public class CurrencyUpdateCommandHandler implements CommandHandler<CurrencyUpda
private final CurrencyWritePlatformService writePlatformService;
+ @Retry(name = "commandCurrencyUpdate", fallbackMethod = "fallback")
@Transactional
@Override
public CurrencyUpdateResponse handle(final Command<CurrencyUpdateRequest> command) {
+ // NOTE: fallback method needs to be in the same class
return writePlatformService.updateAllowedCurrencies(command.getPayload());
}
+
+ @Override
+ public CurrencyUpdateResponse fallback(Command<CurrencyUpdateRequest> command, Throwable t) {
+ // NOTE: fallback method needs to be in the same class
+ return CommandHandler.super.fallback(command, t);
+ }
}
diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/BusinessDateHelper.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/BusinessDateHelper.java
index 4e3105f309..e81dacd906 100644
--- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/BusinessDateHelper.java
+++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/BusinessDateHelper.java
@@ -48,7 +48,7 @@ public class BusinessDateHelper {
BusinessDateUpdateRequest businessDateRequest = defaultBusinessDateRequest().date(businessDate);
try {
BusinessDateUpdateResponse response = ok(
- () -> fineractClient.businessDateManagement().updateBusinessDate(null, businessDateRequest, Map.of()));
+ () -> fineractClient.businessDateManagement().updateBusinessDate(businessDateRequest, Map.of()));
TestContext.INSTANCE.set(TestContextKey.BUSINESS_DATE_RESPONSE, response);
ok(() -> fineractClient.businessDateManagement().getBusinessDate(BUSINESS_DATE, Map.of()));
diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/common/BusinessDateStepDef.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/common/BusinessDateStepDef.java
index 1a3881792a..2fcb775454 100644
--- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/common/BusinessDateStepDef.java
+++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/common/BusinessDateStepDef.java
@@ -71,7 +71,7 @@ public class BusinessDateStepDef extends AbstractStepDef {
BusinessDateUpdateRequest businessDateRequest = businessDateHelper.defaultBusinessDateRequest().date(businessDate);
try {
- fineractClient.businessDateManagement().updateBusinessDate(null, businessDateRequest, Map.of());
+ fineractClient.businessDateManagement().updateBusinessDate(businessDateRequest, Map.of());
throw new AssertionError("Expected FeignException but request succeeded");
} catch (FeignException e) {
final ErrorResponse errorDetails = ErrorResponse.fromFeignException(e);
@@ -92,7 +92,7 @@ public class BusinessDateStepDef extends AbstractStepDef {
Integer httpStatusCodeExpected = 400;
try {
- fineractClient.businessDateManagement().updateBusinessDate(null, businessDateRequest, Map.of());
+ fineractClient.businessDateManagement().updateBusinessDate(businessDateRequest, Map.of());
throw new AssertionError("Expected FeignException but request succeeded");
} catch (FeignException e) {
ErrorResponse errorResponse = ErrorResponse.fromFeignException(e);
diff --git a/fineract-e2e-tests-runner/src/test/java/org/apache/fineract/test/initializer/suite/ExternalEventSuiteInitializerStep.java b/fineract-e2e-tests-runner/src/test/java/org/apache/fineract/test/initializer/suite/ExternalEventSuiteInitializerStep.java
index 5a6451b8b7..8755a51769 100644
--- a/fineract-e2e-tests-runner/src/test/java/org/apache/fineract/test/initializer/suite/ExternalEventSuiteInitializerStep.java
+++ b/fineract-e2e-tests-runner/src/test/java/org/apache/fineract/test/initializer/suite/ExternalEventSuiteInitializerStep.java
@@ -71,7 +71,7 @@ public class ExternalEventSuiteInitializerStep implements FineractSuiteInitializ
ExternalEventConfigurationUpdateRequest request = new ExternalEventConfigurationUpdateRequest()
.externalEventConfigurations(eventConfigMap);
- executeVoid(() -> fineractClient.externalEventConfiguration().updateExternalEventConfigurations(null, request, Map.of()));
+ executeVoid(() -> fineractClient.externalEventConfiguration().updateExternalEventConfigurations(request, Map.of()));
log.debug("=== External event configuration updated - all events enabled ===");
// Step 2: Wait for JMS Listener to be ready before proceeding
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/jpa/JPAConfig.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/jpa/JPAConfig.java
index 1863377f89..9a4f64e3e1 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/jpa/JPAConfig.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/jpa/JPAConfig.java
@@ -58,8 +58,7 @@ import org.springframework.transaction.support.TransactionTemplate;
@Configuration
@EnableJpaAuditing
-@EnableJpaRepositories(basePackages = { "org.apache.fineract.**.domain", "org.apache.fineract.**.repository",
- "org.apache.fineract.command.persistence" })
+@EnableJpaRepositories(basePackages = { "org.apache.fineract.**.domain", "org.apache.fineract.**.repository" })
@EnableConfigurationProperties(JpaProperties.class)
@Import(JpaAuditingHandlerRegistrar.class)
public class JPAConfig extends JpaBaseConfiguration {
diff --git a/fineract-provider/src/main/resources/application.properties b/fineract-provider/src/main/resources/application.properties
index 77c4ef0daf..b3d2a86aeb 100644
--- a/fineract-provider/src/main/resources/application.properties
+++ b/fineract-provider/src/main/resources/application.properties
@@ -487,7 +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}
+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}
+
+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}
+
+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}
+
+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}
+
fineract.command.enabled=true
-fineract.command.executor=sync
-fineract.command.ring-buffer-size=1024
-fineract.command.producer-type=single
+fineract.command.executor=${FINERACT_COMMAND_EXECUTOR:sync}
+fineract.command.ring-buffer-size=${FINERACT_COMMAND_RING_BUFFER_SIZE:1024}
+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}
+
+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}
+resilience4j.retry.instances.commandAuditProcessing.enable-exponential-backoff=${FINERACT_PROCESS_COMMAND_AUDIT_PROCESSING_RETRY_ENABLE_EXPONENTIAL_BACKOFF:true}
+resilience4j.retry.instances.commandAuditProcessing.exponential-backoff-multiplier=${FINERACT_PROCESS_COMMAND_AUDIT_PROCESSING_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2}
+resilience4j.retry.instances.commandAuditProcessing.retryExceptions=${FINERACT_PROCESS_COMMAND_AUDIT_PROCESSING_RETRY_EXCEPTIONS:org.springframework.dao.ConcurrencyFailureException,org.eclipse.persistence.exceptions.OptimisticLockException,jakarta.persistence.OptimisticLockException,org.springframework.orm.jpa.JpaOptimisticLockingFailureException}
+
+resilience4j.retry.instances.commandAuditProcessed.max-attempts=${FINERACT_PROCESS_COMMAND_AUDIT_PROCESSED_RETRY_MAX_ATTEMPTS:3}
+resilience4j.retry.instances.commandAuditProcessed.wait-duration=${FINERACT_PROCESS_COMMAND_AUDIT_PROCESSED_RETRY_WAIT_DURATION:1s}
+resilience4j.retry.instances.commandAuditProcessed.enable-exponential-backoff=${FINERACT_PROCESS_COMMAND_AUDIT_PROCESSED_RETRY_ENABLE_EXPONENTIAL_BACKOFF:true}
+resilience4j.retry.instances.commandAuditProcessed.exponential-backoff-multiplier=${FINERACT_PROCESS_COMMAND_AUDIT_PROCESSED_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2}
+resilience4j.retry.instances.commandAuditProcessed.retryExceptions=${FINERACT_PROCESS_COMMAND_AUDIT_PROCESSED_RETRY_EXCEPTIONS:org.springframework.dao.ConcurrencyFailureException,org.eclipse.persistence.exceptions.OptimisticLockException,jakarta.persistence.OptimisticLockException,org.springframework.orm.jpa.JpaOptimisticLockingFailureException}
+
+resilience4j.retry.instances.commandAuditError.max-attempts=${FINERACT_PROCESS_COMMAND_AUDIT_ERROR_RETRY_MAX_ATTEMPTS:3}
+resilience4j.retry.instances.commandAuditError.wait-duration=${FINERACT_PROCESS_COMMAND_AUDIT_ERROR_RETRY_WAIT_DURATION:1s}
+resilience4j.retry.instances.commandAuditError.enable-exponential-backoff=${FINERACT_PROCESS_COMMAND_AUDIT_ERROR_RETRY_ENABLE_EXPONENTIAL_BACKOFF:true}
+resilience4j.retry.instances.commandAuditError.exponential-backoff-multiplier=${FINERACT_PROCESS_COMMAND_AUDIT_ERROR_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2}
+resilience4j.retry.instances.commandAuditError.retryExceptions=${FINERACT_PROCESS_COMMAND_AUDIT_ERROR_RETRY_EXCEPTIONS:org.springframework.dao.ConcurrencyFailureException,org.eclipse.persistence.exceptions.OptimisticLockException,jakarta.persistence.OptimisticLockException,org.springframework.orm.jpa.JpaOptimisticLockingFailureException}
+
+resilience4j.retry.instances.commandAuditFallback.max-attempts=${FINERACT_PROCESS_COMMAND_AUDIT_FALLBACK_RETRY_MAX_ATTEMPTS:1}
+resilience4j.retry.instances.commandAuditFallback.wait-duration=${FINERACT_PROCESS_COMMAND_AUDIT_FALLBACK_RETRY_WAIT_DURATION:1s}
+resilience4j.retry.instances.commandAuditFallback.enable-exponential-backoff=${FINERACT_PROCESS_COMMAND_AUDIT_FALLBACK_RETRY_ENABLE_EXPONENTIAL_BACKOFF:true}
+resilience4j.retry.instances.commandAuditFallback.exponential-backoff-multiplier=${FINERACT_PROCESS_COMMAND_AUDIT_FALLBACK_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2}
+resilience4j.retry.instances.commandAuditFallback.retryExceptions=${FINERACT_PROCESS_COMMAND_AUDIT_FALLBACK_RETRY_EXCEPTIONS:org.springframework.dao.ConcurrencyFailureException,org.eclipse.persistence.exceptions.OptimisticLockException,jakarta.persistence.OptimisticLockException,org.springframework.orm.jpa.JpaOptimisticLockingFailureException}
diff --git a/fineract-provider/src/main/resources/db/changelog/db.changelog-master.xml b/fineract-provider/src/main/resources/db/changelog/db.changelog-master.xml
index fa2b25a088..d83add9c8e 100644
--- a/fineract-provider/src/main/resources/db/changelog/db.changelog-master.xml
+++ b/fineract-provider/src/main/resources/db/changelog/db.changelog-master.xml
@@ -38,6 +38,7 @@
<includeAll path="db/custom-changelog" errorIfMissingOrEmpty="false" context="tenant_db AND !initial_switch AND custom_changelog"/>
<include file="/db/changelog/tenant/module/progressiveloan/module-changelog-master.xml" context="tenant_db AND !initial_switch"/>
<include file="db/changelog/tenant/module/loanorigination/module-changelog-master.xml" context="tenant_db AND !initial_switch"/>
+ <include file="db/changelog/tenant/module/command/module-changelog-master.xml" context="tenant_db AND !initial_switch"/>
<!-- Scripts to run after the modules were initialized -->
<include file="tenant/final-changelog-tenant.xml" relativeToChangelogFile="true" context="tenant_db AND !initial_switch"/>
</databaseChangeLog>
diff --git a/fineract-provider/src/main/resources/jpa/static-weaving/module/fineract-provider/persistence.xml b/fineract-provider/src/main/resources/jpa/static-weaving/module/fineract-provider/persistence.xml
index f3998cc5a5..7b05c2e7dc 100644
--- a/fineract-provider/src/main/resources/jpa/static-weaving/module/fineract-provider/persistence.xml
+++ b/fineract-provider/src/main/resources/jpa/static-weaving/module/fineract-provider/persistence.xml
@@ -152,11 +152,6 @@
<class>org.apache.fineract.portfolio.self.pockets.domain.PocketAccountMapping</class>
<class>org.apache.fineract.portfolio.self.registration.domain.SelfServiceRegistration</class>
- <!-- Command Module Entities -->
-
- <class>org.apache.fineract.command.persistence.domain.CommandEntity</class>
- <class>org.apache.fineract.command.persistence.converter.JsonAttributeConverter</class>
-
<!-- Charge Module Entities -->
<class>org.apache.fineract.portfolio.charge.domain.Charge</class>
diff --git a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/classpath/ClasspathDuplicatesStepDefinitions.java b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/classpath/ClasspathDuplicatesStepDefinitions.java
index f24f6d95d2..49b235d7c5 100644
--- a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/classpath/ClasspathDuplicatesStepDefinitions.java
+++ b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/classpath/ClasspathDuplicatesStepDefinitions.java
@@ -200,7 +200,7 @@ public class ClasspathDuplicatesStepDefinitions implements En {
// errorprone with Java 11 integration leaks to classpath, which
// causes a conflict between
// checkerframework/checker-qual and checkerframework/dataflow
- || resourcePath.startsWith("org/checkerframework/dataflow/qual/") //
+ || resourcePath.startsWith("org/checkerframework/") //
// Pentaho reports harmless duplicates
|| resourcePath.endsWith("overview.html") //
|| resourcePath.endsWith("classic-engine.properties") //
diff --git a/fineract-provider/src/test/resources/application-test.properties b/fineract-provider/src/test/resources/application-test.properties
index d708c5b610..541be16325 100644
--- a/fineract-provider/src/test/resources/application-test.properties
+++ b/fineract-provider/src/test/resources/application-test.properties
@@ -273,6 +273,7 @@ spring.datasource.hikari.dataSourceProperties['maintainTimeStats']=false
spring.datasource.hikari.dataSourceProperties['logSlowQueries']=true
spring.datasource.hikari.dataSourceProperties['dumpQueriesOnException']=true
spring.jpa.open-in-view=false
+spring.data.jdbc.dialect=maria
spring.main.allow-bean-definition-overriding=true
spring.batch.initialize-schema=NEVER
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/helpers/FeignBusinessDateHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/helpers/FeignBusinessDateHelper.java
index 7941c510b8..88038af59f 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/helpers/FeignBusinessDateHelper.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/helpers/FeignBusinessDateHelper.java
@@ -45,7 +45,7 @@ public class FeignBusinessDateHelper {
.dateFormat("yyyy-MM-dd")//
.locale(LoanTestData.LOCALE);
- ok(() -> fineractClient.businessDateManagement().updateBusinessDate(null, request, Collections.emptyMap()));
+ ok(() -> fineractClient.businessDateManagement().updateBusinessDate(request, Collections.emptyMap()));
}
public void runAt(String date, Runnable action) {
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/BusinessDateHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/BusinessDateHelper.java
index d97f4e2baa..6d4b34b28b 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/BusinessDateHelper.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/BusinessDateHelper.java
@@ -57,7 +57,7 @@ public final class BusinessDateHelper {
public static BusinessDateUpdateResponse updateBusinessDate(final BusinessDateUpdateRequest request) {
log.info("------------------UPDATE BUSINESS DATE----------------------");
log.info("------------------Type: {}, date: {}----------------------", request.getType(), request.getDate());
- return Calls.ok(FineractClientHelper.getFineractClient().businessDateManagement.updateBusinessDate(null, request));
+ return Calls.ok(FineractClientHelper.getFineractClient().businessDateManagement.updateBusinessDate(request));
}
// TODO: Rewrite to use fineract-client instead!
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/externalevents/ExternalEventHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/externalevents/ExternalEventHelper.java
index 41666a5ab5..fc862beeae 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/externalevents/ExternalEventHelper.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/externalevents/ExternalEventHelper.java
@@ -123,7 +123,7 @@ public final class ExternalEventHelper {
public void configureBusinessEvent(String eventName, boolean enabled) {
ExternalEventConfigurationUpdateResponse result = Calls
- .ok(FineractClientHelper.getFineractClient().externalEventConfigurationApi.updateExternalEventConfigurations("",
+ .ok(FineractClientHelper.getFineractClient().externalEventConfigurationApi.updateExternalEventConfigurations(
new ExternalEventConfigurationUpdateRequest().externalEventConfigurations(Map.of(eventName, enabled))));
Map<String, Object> changes = result.getChanges();
Assertions.assertNotNull(changes);