From fb5889aaeb02b2ae51a1e8995066d5eb0bd8984b Mon Sep 17 00:00:00 2001 From: Azizbek Khushvakov <113523904+azizbekxm@users.noreply.github.com> Date: Thu, 16 Nov 2023 14:39:22 +0500 Subject: [PATCH] [MODAUD-174] - Consume piece change events and implement endpoints (#155) * [MODAUD-174] - Consume piece change events and implement endpoints * [MODAUD-174] - Fixed unit tests * [MODAUD-174] - Fixed Code smell * [MODAUD-174] - Fixed Code smell * [MODAUD-174] - Fixed Unit tests * [MODAUD-174] - Minor improvements * [MODAUD-174] - Implements kafka handler and creating table * [MODAUD-174] - Increased code coverage * [MODAUD-174] - Increased code coverage * [MODAUD-174] - Minor improvements * [MODAUD-174] - Minor improvements * [MODAUD-174] - Minor improvements * [MODAUD-174] - Minor improvements * [MODAUD-174] - Minor improvements * [MODAUD-174] - Minor improvements * [MODAUD-174] - Implemented uniq-status api * [MODAUD-174] - Implemented status change history feature and cleaned code * [MODAUD-174] - Implemented status change history feature and cleaned code * [MODAUD-174] - Implemented status change history feature and cleaned code * [MODAUD-174] - draft changes * [MODAUD-174] - Fixed error * [MODAUD-174] - minor improvements * [MODAUD-174] - minor improvements * [MODAUD-174] - minor improvements * [MODAUD-174] - minor improvements * [MODAUD-174] - minor improvements * [MODAUD-174] - Fixed unit tests * [MODAUD-174] - Fixed unit tests * [MODAUD-174] - Fixed unit tests * [MODAUD-174] - Fixed unit tests * [MODAUD-174] - draft changes * [MODAUD-174] - Fixed unit tests * [MODAUD-174] - Minor improvements * [MODAUD-174] - Minor improvements * [MODAUD-174] - Minor improvements * [MODAUD-174] - Improved code by changing field injection to constructor injection and changed method name * [MODAUD-174] - Fixed unit tests * [MODAUD-174] - Fixed unit tests * [MODAUD-174] - Fixed sql query * [MODAUD-174] - Extracted duplicate methods * [MODAUD-174] - Added java docs * [MODAUD-174] - Improved java integration test * [MODAUD-174] - Extracted handleFauilures and optimized codebase * [MODAUD-174] - Fixed sql query * [MODAUD-174] - Fixed sql query * [MODAUD-174] - Minor improvement * [MODAUD-174] - Improved AuditDataAcquisitionImpl * [MODAUD-174] - Improved AuditDataAcquisitionImpl and changed debug to error level in catch block * [MODAUD-174] - Fixed ZoneOffset * [MODAUD-174] - Improved log.error message * [MODAUD-174] - Minor improvements --- .gitignore | 1 + descriptors/ModuleDescriptor-template.json | 32 +++- mod-audit-server/pom.xml | 4 +- .../folio/dao/acquisition/PieceEventsDao.java | 46 +++++ .../acquisition/impl/OrderEventsDaoImpl.java | 47 ++--- .../impl/OrderLineEventsDaoImpl.java | 50 ++--- .../acquisition/impl/PieceEventsDaoImpl.java | 157 ++++++++++++++++ .../rest/impl/AuditDataAcquisitionImpl.java | 91 ++++++--- .../java/org/folio/rest/impl/InitAPIs.java | 12 +- .../acquisition/PieceAuditEventsService.java | 44 +++++ .../impl/OrderAuditEventsServiceImpl.java | 25 +-- .../impl/OrderLineAuditEventsServiceImpl.java | 25 +-- .../impl/PieceAuditEventsServiceImpl.java | 48 +++++ .../org/folio/util/AcquisitionEventType.java | 3 +- ...stants.java => AuditEventDBConstants.java} | 9 +- .../src/main/java/org/folio/util/DbUtils.java | 13 ++ .../main/java/org/folio/util/ErrorUtils.java | 10 + .../org/folio/util/PostgresClientFactory.java | 5 +- .../PieceEventConsumersVerticle.java | 28 +++ .../consumers/PieceEventsHandler.java | 60 ++++++ .../create_acquisition_piece_log_table.sql | 11 ++ .../templates/db_scripts/schema.json | 5 + .../src/test/java/org/folio/TestSuite.java | 39 ++-- .../org/folio/dao/OrderEventsDaoTest.java | 66 ++----- .../org/folio/dao/OrderLineEventsDaoTest.java | 56 ++---- .../org/folio/dao/PieceEventsDaoTest.java | 77 ++++++++ .../impl/AuditDataAcquisitionAPITest.java | 174 +++++++++++++++--- .../rest/impl/OrderEventsHandlerMockTest.java | 24 +-- .../rest/impl/PieceEventsHandlerMockTest.java | 92 +++++++++ .../services/OrderAuditEventsServiceTest.java | 43 +---- .../OrderLineAuditEventsServiceTest.java | 33 +--- .../services/PieceAuditEventsServiceTest.java | 40 ++++ .../java/org/folio/utils/EntityUtils.java | 97 ++++++++++ pom.xml | 2 +- ramls/acquisition-events.raml | 71 +++++++ ramls/piece_audit_event.json | 40 ++++ ramls/piece_audit_event_collection.json | 25 +++ 37 files changed, 1269 insertions(+), 336 deletions(-) create mode 100644 mod-audit-server/src/main/java/org/folio/dao/acquisition/PieceEventsDao.java create mode 100644 mod-audit-server/src/main/java/org/folio/dao/acquisition/impl/PieceEventsDaoImpl.java create mode 100644 mod-audit-server/src/main/java/org/folio/services/acquisition/PieceAuditEventsService.java create mode 100644 mod-audit-server/src/main/java/org/folio/services/acquisition/impl/PieceAuditEventsServiceImpl.java rename mod-audit-server/src/main/java/org/folio/util/{OrderAuditEventDBConstants.java => AuditEventDBConstants.java} (76%) create mode 100644 mod-audit-server/src/main/java/org/folio/util/DbUtils.java create mode 100644 mod-audit-server/src/main/java/org/folio/verticle/acquisition/PieceEventConsumersVerticle.java create mode 100644 mod-audit-server/src/main/java/org/folio/verticle/acquisition/consumers/PieceEventsHandler.java create mode 100644 mod-audit-server/src/main/resources/templates/db_scripts/acquisition/create_acquisition_piece_log_table.sql create mode 100644 mod-audit-server/src/test/java/org/folio/dao/PieceEventsDaoTest.java create mode 100644 mod-audit-server/src/test/java/org/folio/rest/impl/PieceEventsHandlerMockTest.java create mode 100644 mod-audit-server/src/test/java/org/folio/services/PieceAuditEventsServiceTest.java create mode 100644 mod-audit-server/src/test/java/org/folio/utils/EntityUtils.java create mode 100644 ramls/piece_audit_event.json create mode 100644 ramls/piece_audit_event_collection.json diff --git a/.gitignore b/.gitignore index 151a78c6..21e3ddbc 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ nbproject/ .settings/ .classpath /bin/ +/src/main/resources/postgres-conf.json diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index e7bfceb7..8eaa93a8 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -121,6 +121,30 @@ } ] }, + { + "id": "acquisition-piece-events", + "version": "1.0", + "handlers": [ + { + "methods": [ + "GET" + ], + "pathPattern": "/audit-data/acquisition/piece/{id}", + "permissionsRequired": [ + "acquisition.piece.events.get" + ] + }, + { + "methods": [ + "GET" + ], + "pathPattern": "/audit-data/acquisition/piece/{id}/status-change-history", + "permissionsRequired": [ + "acquisition.piece.events.get" + ] + } + ] + }, { "id": "circulation-logs", "version": "1.2", @@ -223,6 +247,11 @@ "displayName": "Acquisition order-line events - get order-line change events", "description": "Get order-line change events" }, + { + "permissionName": "acquisition.piece.events.get", + "displayName": "Acquisition piece events - get piece change events", + "description": "Get piece change events" + }, { "permissionName": "audit.all", "displayName": "Audit - all permissions", @@ -235,7 +264,8 @@ "audit.item.delete", "circulation-logs.collection.get", "acquisition.order.events.get", - "acquisition.order-line.events.get" + "acquisition.order-line.events.get", + "acquisition.piece.events.get" ] } ], diff --git a/mod-audit-server/pom.xml b/mod-audit-server/pom.xml index 1bc17c9a..9e13cbf0 100644 --- a/mod-audit-server/pom.xml +++ b/mod-audit-server/pom.xml @@ -2,13 +2,13 @@ 4.0.0 mod-audit-server - 2.8.1-SNAPSHOT + 2.9.0-SNAPSHOT jar mod-audit org.folio - 2.8.1-SNAPSHOT + 2.9.0-SNAPSHOT diff --git a/mod-audit-server/src/main/java/org/folio/dao/acquisition/PieceEventsDao.java b/mod-audit-server/src/main/java/org/folio/dao/acquisition/PieceEventsDao.java new file mode 100644 index 00000000..b145c059 --- /dev/null +++ b/mod-audit-server/src/main/java/org/folio/dao/acquisition/PieceEventsDao.java @@ -0,0 +1,46 @@ +package org.folio.dao.acquisition; + +import io.vertx.core.Future; +import io.vertx.sqlclient.Row; +import io.vertx.sqlclient.RowSet; +import org.folio.rest.jaxrs.model.PieceAuditEvent; +import org.folio.rest.jaxrs.model.PieceAuditEventCollection; + +public interface PieceEventsDao { + + /** + * Saves pieceAuditEvent entity to DB + * + * @param pieceAuditEvent pieceAuditEvent entity to save + * @param tenantId tenant id + * @return future with created row + */ + Future> save(PieceAuditEvent pieceAuditEvent, String tenantId); + + /** + * Searches for piece audit events by id + * + * @param pieceId piece id + * @param sortBy sort by + * @param sortOrder sort order + * @param limit limit + * @param offset offset + * @param tenantId tenant id + * @return future with PieceAuditEventCollection + */ + Future getAuditEventsByPieceId(String pieceId, String sortBy, String sortOrder, + int limit, int offset, String tenantId); + + /** + * Searches for piece audit events with status changes by piece id + * @param pieceId piece id + * @param sortBy sort by + * @param sortOrder sort order + * @param limit limit + * @param offset offset + * @param tenantId tenant id + * @return future with PieceAuditEventCollection + */ + Future getAuditEventsWithStatusChangesByPieceId(String pieceId, String sortBy, String sortOrder, + int limit, int offset, String tenantId); +} diff --git a/mod-audit-server/src/main/java/org/folio/dao/acquisition/impl/OrderEventsDaoImpl.java b/mod-audit-server/src/main/java/org/folio/dao/acquisition/impl/OrderEventsDaoImpl.java index 8dfb02e6..d9ee92e4 100644 --- a/mod-audit-server/src/main/java/org/folio/dao/acquisition/impl/OrderEventsDaoImpl.java +++ b/mod-audit-server/src/main/java/org/folio/dao/acquisition/impl/OrderEventsDaoImpl.java @@ -1,5 +1,23 @@ package org.folio.dao.acquisition.impl; +import static java.lang.String.format; +import static org.folio.util.AuditEventDBConstants.ACTION_DATE_FIELD; +import static org.folio.util.AuditEventDBConstants.ACTION_FIELD; +import static org.folio.util.AuditEventDBConstants.EVENT_DATE_FIELD; +import static org.folio.util.AuditEventDBConstants.ID_FIELD; +import static org.folio.util.AuditEventDBConstants.MODIFIED_CONTENT_FIELD; +import static org.folio.util.AuditEventDBConstants.ORDER_BY_PATTERN; +import static org.folio.util.AuditEventDBConstants.ORDER_ID_FIELD; +import static org.folio.util.AuditEventDBConstants.TOTAL_RECORDS_FIELD; +import static org.folio.util.AuditEventDBConstants.USER_ID_FIELD; +import static org.folio.util.DbUtils.formatDBTableName; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.util.Date; +import java.util.UUID; + import io.vertx.core.Future; import io.vertx.core.Promise; import io.vertx.core.json.JsonObject; @@ -12,18 +30,8 @@ import org.folio.rest.jaxrs.model.OrderAuditEvent; import org.folio.rest.jaxrs.model.OrderAuditEventCollection; import org.folio.util.PostgresClientFactory; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Repository; -import java.time.LocalDateTime; -import java.time.ZoneOffset; -import java.util.Date; -import java.util.UUID; - -import static java.lang.String.format; -import static org.folio.rest.persist.PostgresClient.convertToPsqlStandard; -import static org.folio.util.OrderAuditEventDBConstants.*; - @Repository public class OrderEventsDaoImpl implements OrderEventsDao { @@ -37,10 +45,8 @@ public class OrderEventsDaoImpl implements OrderEventsDao { public static final String INSERT_SQL = "INSERT INTO %s (id, action, order_id, user_id, event_date, action_date, modified_content_snapshot)" + " VALUES ($1, $2, $3, $4, $5, $6, $7)"; - @Autowired private final PostgresClientFactory pgClientFactory; - @Autowired public OrderEventsDaoImpl(PostgresClientFactory pgClientFactory) { this.pgClientFactory = pgClientFactory; } @@ -49,10 +55,9 @@ public OrderEventsDaoImpl(PostgresClientFactory pgClientFactory) { public Future> save(OrderAuditEvent orderAuditEvent, String tenantId) { LOGGER.debug("save:: Saving Order AuditEvent with tenant id : {}", tenantId); Promise> promise = Promise.promise(); + LOGGER.debug("formatDBTableName:: Formatting DB Table Name with tenant id : {}", tenantId); String logTable = formatDBTableName(tenantId, TABLE_NAME); - String query = format(INSERT_SQL, logTable); - makeSaveCall(promise, query, orderAuditEvent, tenantId); LOGGER.info("save:: Saved Order AuditEvent with tenant id : {}", tenantId); return promise.future(); @@ -63,7 +68,7 @@ public Future getAuditEventsByOrderId(String orderId, LOGGER.debug("getAuditEventsByOrderId:: Retrieving AuditEvent with order id : {}", orderId); Promise> promise = Promise.promise(); try { - LOGGER.info("getAuditEventsByOrderId:: Trying to Retrieve AuditEvent with order id : {}", orderId); + LOGGER.debug("formatDBTableName:: Formatting DB Table Name with tenant id : {}", tenantId); String logTable = formatDBTableName(tenantId, TABLE_NAME); String query = format(GET_BY_ORDER_ID_SQL, logTable, logTable, format(ORDER_BY_PATTERN, sortBy, sortOrder)); Tuple queryParams = Tuple.of(UUID.fromString(orderId), limit, offset); @@ -85,11 +90,11 @@ private void makeSaveCall(Promise> promise, String query, OrderAudit orderAuditEvent.getAction(), orderAuditEvent.getOrderId(), orderAuditEvent.getUserId(), - LocalDateTime.ofInstant(orderAuditEvent.getEventDate().toInstant(), ZoneOffset.UTC), - LocalDateTime.ofInstant(orderAuditEvent.getActionDate().toInstant(), ZoneOffset.UTC), + LocalDateTime.ofInstant(orderAuditEvent.getEventDate().toInstant(), ZoneId.systemDefault()), + LocalDateTime.ofInstant(orderAuditEvent.getActionDate().toInstant(), ZoneId.systemDefault()), JsonObject.mapFrom(orderAuditEvent.getOrderSnapshot())), promise); } catch (Exception e) { - LOGGER.warn("Failed to save record with id: {} for order id: {} in to table {}", + LOGGER.error("Failed to save record with id: {} for order id: {} in to table {}", orderAuditEvent.getId(), orderAuditEvent.getOrderId(), TABLE_NAME, e); promise.fail(e); } @@ -102,7 +107,7 @@ private OrderAuditEventCollection mapRowToListOfOrderEvent(RowSet rowSet) { orderAuditEventCollection.getOrderAuditEvents().add(mapRowToOrderEvent(row)); orderAuditEventCollection.setTotalItems(row.getInteger(TOTAL_RECORDS_FIELD)); }); - LOGGER.info("mapRowToListOfOrderEvent:: Mapped row to List of Order Events"); + LOGGER.debug("mapRowToListOfOrderEvent:: Mapped row to List of Order Events"); return orderAuditEventCollection; } @@ -119,8 +124,4 @@ private OrderAuditEvent mapRowToOrderEvent(Row row) { .withOrderSnapshot(JsonObject.mapFrom(row.getValue(MODIFIED_CONTENT_FIELD))); } - private String formatDBTableName(String tenantId, String table) { - LOGGER.debug("formatDBTableName:: Formatting DB Table Name with tenant id : {}", tenantId); - return format("%s.%s", convertToPsqlStandard(tenantId), table); - } } diff --git a/mod-audit-server/src/main/java/org/folio/dao/acquisition/impl/OrderLineEventsDaoImpl.java b/mod-audit-server/src/main/java/org/folio/dao/acquisition/impl/OrderLineEventsDaoImpl.java index 26d50718..8909874b 100644 --- a/mod-audit-server/src/main/java/org/folio/dao/acquisition/impl/OrderLineEventsDaoImpl.java +++ b/mod-audit-server/src/main/java/org/folio/dao/acquisition/impl/OrderLineEventsDaoImpl.java @@ -1,5 +1,24 @@ package org.folio.dao.acquisition.impl; +import static java.lang.String.format; +import static org.folio.util.AuditEventDBConstants.ACTION_DATE_FIELD; +import static org.folio.util.AuditEventDBConstants.ACTION_FIELD; +import static org.folio.util.AuditEventDBConstants.EVENT_DATE_FIELD; +import static org.folio.util.AuditEventDBConstants.ID_FIELD; +import static org.folio.util.AuditEventDBConstants.MODIFIED_CONTENT_FIELD; +import static org.folio.util.AuditEventDBConstants.ORDER_BY_PATTERN; +import static org.folio.util.AuditEventDBConstants.ORDER_ID_FIELD; +import static org.folio.util.AuditEventDBConstants.ORDER_LINE_ID_FIELD; +import static org.folio.util.AuditEventDBConstants.TOTAL_RECORDS_FIELD; +import static org.folio.util.AuditEventDBConstants.USER_ID_FIELD; +import static org.folio.util.DbUtils.formatDBTableName; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.util.Date; +import java.util.UUID; + import io.vertx.core.Future; import io.vertx.core.Promise; import io.vertx.core.json.JsonObject; @@ -12,18 +31,8 @@ import org.folio.rest.jaxrs.model.OrderLineAuditEvent; import org.folio.rest.jaxrs.model.OrderLineAuditEventCollection; import org.folio.util.PostgresClientFactory; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Repository; -import java.time.LocalDateTime; -import java.time.ZoneOffset; -import java.util.Date; -import java.util.UUID; - -import static java.lang.String.format; -import static org.folio.rest.persist.PostgresClient.convertToPsqlStandard; -import static org.folio.util.OrderAuditEventDBConstants.*; - @Repository public class OrderLineEventsDaoImpl implements OrderLineEventsDao { @@ -38,10 +47,8 @@ public class OrderLineEventsDaoImpl implements OrderLineEventsDao { private static final String INSERT_SQL = "INSERT INTO %s (id, action, order_id, order_line_id, user_id, event_date, action_date, modified_content_snapshot) " + "VALUES ($1, $2, $3, $4, $5, $6, $7, $8)"; - @Autowired private final PostgresClientFactory pgClientFactory; - @Autowired public OrderLineEventsDaoImpl(PostgresClientFactory pgClientFactory) { this.pgClientFactory = pgClientFactory; } @@ -50,12 +57,10 @@ public OrderLineEventsDaoImpl(PostgresClientFactory pgClientFactory) { public Future> save(OrderLineAuditEvent orderLineAuditEvent, String tenantId) { LOGGER.debug("save:: Saving OrderLine AuditEvent with tenant id : {}", tenantId); Promise> promise = Promise.promise(); + LOGGER.debug("formatDBTableName:: Formatting DB Table Name with tenant id : {}", tenantId); String logTable = formatDBTableName(tenantId, TABLE_NAME); - String query = format(INSERT_SQL, logTable); - makeSaveCall(promise, query, orderLineAuditEvent, tenantId); - LOGGER.info("save:: Saved OrderLine AuditEvent with tenant id : {}", tenantId); return promise.future(); } @@ -65,7 +70,7 @@ public Future getAuditEventsByOrderLineId(String LOGGER.debug("getAuditEventsByOrderLineId:: Retrieving AuditEvent with order line id : {}", orderLineId); Promise> promise = Promise.promise(); try { - LOGGER.info("getAuditEventsByOrderLineId:: Trying to Retrieve AuditEvent with order line id : {}", orderLineId); + LOGGER.debug("formatDBTableName:: Formatting DB Table Name with tenant id : {}", tenantId); String logTable = formatDBTableName(tenantId, TABLE_NAME); String query = format(GET_BY_ORDER_LINE_ID_SQL, logTable, logTable, format(ORDER_BY_PATTERN, sortBy, sortOrder)); Tuple queryParams = Tuple.of(UUID.fromString(orderLineId), limit, offset); @@ -88,11 +93,11 @@ private void makeSaveCall(Promise> promise, String query, OrderLineA orderLineAuditEvent.getOrderId(), orderLineAuditEvent.getOrderLineId(), orderLineAuditEvent.getUserId(), - LocalDateTime.ofInstant(orderLineAuditEvent.getEventDate().toInstant(), ZoneOffset.UTC), - LocalDateTime.ofInstant(orderLineAuditEvent.getActionDate().toInstant(), ZoneOffset.UTC), + LocalDateTime.ofInstant(orderLineAuditEvent.getEventDate().toInstant(), ZoneId.systemDefault()), + LocalDateTime.ofInstant(orderLineAuditEvent.getActionDate().toInstant(), ZoneId.systemDefault()), JsonObject.mapFrom(orderLineAuditEvent.getOrderLineSnapshot())), promise); } catch (Exception e) { - LOGGER.warn("Failed to save record with id: {} for order line id: {} in to table {}", + LOGGER.error("Failed to save record with id: {} for order line id: {} in to table {}", orderLineAuditEvent.getId(), orderLineAuditEvent.getOrderLineId(), TABLE_NAME, e); promise.fail(e); } @@ -105,7 +110,7 @@ private OrderLineAuditEventCollection mapRowToListOfOrderLineEvent(RowSet r orderLineAuditEventCollection.getOrderLineAuditEvents().add(mapRowToOrderLineEvent(row)); orderLineAuditEventCollection.setTotalItems(row.getInteger(TOTAL_RECORDS_FIELD)); }); - LOGGER.info("mapRowToListOfOrderLineEvent:: Mapped row to List of Order Line Events"); + LOGGER.debug("mapRowToListOfOrderLineEvent:: Mapped row to List of Order Line Events"); return orderLineAuditEventCollection; } @@ -122,9 +127,4 @@ private OrderLineAuditEvent mapRowToOrderLineEvent(Row row) { .withActionDate(Date.from(row.getLocalDateTime(ACTION_DATE_FIELD).toInstant(ZoneOffset.UTC))) .withOrderLineSnapshot(JsonObject.mapFrom(row.getValue(MODIFIED_CONTENT_FIELD))); } - - private String formatDBTableName(String tenantId, String table) { - LOGGER.debug("formatDBTableName:: Formatting DB Table Name with tenant id : {}", tenantId); - return format("%s.%s", convertToPsqlStandard(tenantId), table); - } } diff --git a/mod-audit-server/src/main/java/org/folio/dao/acquisition/impl/PieceEventsDaoImpl.java b/mod-audit-server/src/main/java/org/folio/dao/acquisition/impl/PieceEventsDaoImpl.java new file mode 100644 index 00000000..09206956 --- /dev/null +++ b/mod-audit-server/src/main/java/org/folio/dao/acquisition/impl/PieceEventsDaoImpl.java @@ -0,0 +1,157 @@ +package org.folio.dao.acquisition.impl; + +import static java.lang.String.format; +import static org.folio.util.AuditEventDBConstants.ACTION_DATE_FIELD; +import static org.folio.util.AuditEventDBConstants.ACTION_FIELD; +import static org.folio.util.AuditEventDBConstants.EVENT_DATE_FIELD; +import static org.folio.util.AuditEventDBConstants.ID_FIELD; +import static org.folio.util.AuditEventDBConstants.MODIFIED_CONTENT_FIELD; +import static org.folio.util.AuditEventDBConstants.ORDER_BY_PATTERN; +import static org.folio.util.AuditEventDBConstants.PIECE_ID_FIELD; +import static org.folio.util.AuditEventDBConstants.TOTAL_RECORDS_FIELD; +import static org.folio.util.AuditEventDBConstants.USER_ID_FIELD; +import static org.folio.util.DbUtils.formatDBTableName; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.util.Date; +import java.util.UUID; + +import io.vertx.core.Future; +import io.vertx.core.Promise; +import io.vertx.core.json.JsonObject; +import io.vertx.sqlclient.Row; +import io.vertx.sqlclient.RowSet; +import io.vertx.sqlclient.Tuple; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.folio.dao.acquisition.PieceEventsDao; +import org.folio.rest.jaxrs.model.PieceAuditEvent; +import org.folio.rest.jaxrs.model.PieceAuditEventCollection; +import org.folio.util.PostgresClientFactory; +import org.springframework.stereotype.Repository; + +@Repository +public class PieceEventsDaoImpl implements PieceEventsDao { + private static final Logger LOGGER = LogManager.getLogger(); + private static final String TABLE_NAME = "acquisition_piece_log"; + private static final String GET_BY_PIECE_ID_SQL = "SELECT id, action, piece_id, user_id, event_date, action_date, modified_content_snapshot, " + + " (SELECT count(*) AS total_records FROM %s WHERE piece_id = $1) FROM %s WHERE piece_id = $1 %s LIMIT $2 OFFSET $3"; + private static final String GET_STATUS_CHANGE_HISTORY_BY_PIECE_ID_SQL = """ + WITH StatusChanges AS ( + SELECT id, action, piece_id, user_id, event_date, action_date, modified_content_snapshot, + LAG(modified_content_snapshot ->> 'receivingStatus') OVER (PARTITION BY piece_id ORDER BY action_date) AS previous_status + FROM %s WHERE piece_id=$1 + ) + SELECT id, action, piece_id, user_id, event_date, action_date, modified_content_snapshot, + (SELECT COUNT(*) AS total_records FROM StatusChanges + WHERE modified_content_snapshot ->> 'receivingStatus' <> COALESCE(previous_status, '')) + FROM StatusChanges WHERE modified_content_snapshot ->> 'receivingStatus' <> COALESCE(previous_status, '') + %s LIMIT $2 OFFSET $3 + """; + + private static final String INSERT_SQL = "INSERT INTO %s (id, action, piece_id, user_id, event_date, action_date, modified_content_snapshot)" + + " VALUES ($1, $2, $3, $4, $5, $6, $7)"; + + private final PostgresClientFactory pgClientFactory; + + public PieceEventsDaoImpl(PostgresClientFactory pgClientFactory) { + this.pgClientFactory = pgClientFactory; + } + + @Override + public Future> save(PieceAuditEvent pieceAuditEvent, String tenantId) { + LOGGER.debug("save:: Trying to save Piece AuditEvent with tenant id : {}", tenantId); + Promise> promise = Promise.promise(); + + LOGGER.debug("formatDBTableName:: Formatting DB Table Name with tenant id : {}", tenantId); + String logTable = formatDBTableName(tenantId, TABLE_NAME); + String query = format(INSERT_SQL, logTable); + + makeSaveCall(promise, query, pieceAuditEvent, tenantId); + LOGGER.info("save:: Saved Piece AuditEvent for pieceId={} in tenant id={}", pieceAuditEvent.getPieceId(), tenantId); + return promise.future(); + } + + @Override + public Future getAuditEventsByPieceId(String pieceId, String sortBy, String sortOrder, int limit, int offset, String tenantId) { + LOGGER.debug("getAuditEventsByOrderId:: Trying to retrieve AuditEvent with piece id : {}", pieceId); + Promise> promise = Promise.promise(); + try { + LOGGER.debug("formatDBTableName:: Formatting DB Table Name with tenant id : {}", tenantId); + String logTable = formatDBTableName(tenantId, TABLE_NAME); + String query = format(GET_BY_PIECE_ID_SQL, logTable, logTable, format(ORDER_BY_PATTERN, sortBy, sortOrder)); + Tuple queryParams = Tuple.of(UUID.fromString(pieceId), limit, offset); + pgClientFactory.createInstance(tenantId).selectRead(query, queryParams, promise); + } catch (Exception e) { + LOGGER.warn("Error getting piece audit events by piece id: {}", pieceId, e); + promise.fail(e); + } + LOGGER.info("getAuditEventsByOrderId:: Retrieved AuditEvent with piece id : {}", pieceId); + return promise.future().map(rowSet -> rowSet.rowCount() == 0 ? + new PieceAuditEventCollection().withTotalItems(0) : + mapRowToListOfPieceEvent(rowSet)); + } + + @Override + public Future getAuditEventsWithStatusChangesByPieceId(String pieceId, String sortBy, String sortOrder, int limit, int offset, String tenantId) { + LOGGER.debug("getAuditEventsByOrderId:: Retrieving AuditEvent with piece id : {}", pieceId); + Promise> promise = Promise.promise(); + try { + LOGGER.debug("formatDBTableName:: Formatting DB Table Name with tenant id : {}", tenantId); + String logTable = formatDBTableName(tenantId, TABLE_NAME); + String query = format(GET_STATUS_CHANGE_HISTORY_BY_PIECE_ID_SQL, logTable, format(ORDER_BY_PATTERN, sortBy, sortOrder)); + Tuple queryParams = Tuple.of(UUID.fromString(pieceId), limit, offset); + pgClientFactory.createInstance(tenantId).selectRead(query, queryParams, promise); + } catch (Exception e) { + LOGGER.warn("Error getting order audit events by piece id: {}", pieceId, e); + promise.fail(e); + } + LOGGER.info("getAuditEventsByOrderId:: Retrieved AuditEvent with piece id: {}", pieceId); + return promise.future().map(rowSet -> rowSet.rowCount() == 0 ? new PieceAuditEventCollection().withTotalItems(0) + : mapRowToListOfPieceEvent(rowSet)); + } + + private PieceAuditEventCollection mapRowToListOfPieceEvent(RowSet rowSet) { + LOGGER.debug("mapRowToListOfOrderEvent:: Mapping row to List of Piece Events"); + PieceAuditEventCollection pieceAuditEventCollection = new PieceAuditEventCollection(); + rowSet.iterator().forEachRemaining(row -> { + pieceAuditEventCollection.getPieceAuditEvents().add(mapRowToPieceEvent(row)); + pieceAuditEventCollection.setTotalItems(row.getInteger(TOTAL_RECORDS_FIELD)); + }); + LOGGER.debug("mapRowToListOfOrderEvent:: Mapped row to List of Piece Events"); + return pieceAuditEventCollection; + } + + private PieceAuditEvent mapRowToPieceEvent(Row row) { + LOGGER.debug("mapRowToPieceEvent:: Mapping row to Order Event"); + return new PieceAuditEvent() + .withId(row.getValue(ID_FIELD).toString()) + .withAction(row.get(PieceAuditEvent.Action.class, ACTION_FIELD)) + .withPieceId(row.getValue(PIECE_ID_FIELD).toString()) + .withUserId(row.getValue(USER_ID_FIELD).toString()) + .withEventDate(Date.from(row.getLocalDateTime(EVENT_DATE_FIELD).toInstant(ZoneOffset.UTC))) + .withActionDate(Date.from(row.getLocalDateTime(ACTION_DATE_FIELD).toInstant(ZoneOffset.UTC))) + .withPieceSnapshot(JsonObject.mapFrom(row.getValue(MODIFIED_CONTENT_FIELD))); + } + + private void makeSaveCall(Promise> promise, String query, PieceAuditEvent pieceAuditEvent, String tenantId) { + LOGGER.debug("makeSaveCall:: Making save call with query : {} and tenant id : {}", query, tenantId); + try { + pgClientFactory.createInstance(tenantId).execute(query, Tuple.of(pieceAuditEvent.getId(), + pieceAuditEvent.getAction(), + pieceAuditEvent.getPieceId(), + pieceAuditEvent.getUserId(), + LocalDateTime.ofInstant(pieceAuditEvent.getEventDate().toInstant(), ZoneId.systemDefault()), + LocalDateTime.ofInstant(pieceAuditEvent.getActionDate().toInstant(), ZoneId.systemDefault()), + JsonObject.mapFrom(pieceAuditEvent.getPieceSnapshot())), + promise); + } catch (Exception e) { + LOGGER.error("Failed to save record with id: {} for order id: {} in to table {}", + pieceAuditEvent.getId(), pieceAuditEvent.getPieceId(), TABLE_NAME, e); + promise.fail(e); + } + } + +} diff --git a/mod-audit-server/src/main/java/org/folio/rest/impl/AuditDataAcquisitionImpl.java b/mod-audit-server/src/main/java/org/folio/rest/impl/AuditDataAcquisitionImpl.java index e651c739..f405be20 100644 --- a/mod-audit-server/src/main/java/org/folio/rest/impl/AuditDataAcquisitionImpl.java +++ b/mod-audit-server/src/main/java/org/folio/rest/impl/AuditDataAcquisitionImpl.java @@ -9,10 +9,13 @@ import org.apache.logging.log4j.Logger; import org.folio.rest.jaxrs.model.AuditDataAcquisitionOrderIdGetSortOrder; import org.folio.rest.jaxrs.model.AuditDataAcquisitionOrderLineIdGetSortOrder; +import org.folio.rest.jaxrs.model.AuditDataAcquisitionPieceIdGetSortOrder; +import org.folio.rest.jaxrs.model.AuditDataAcquisitionPieceIdStatusChangeHistoryGetSortOrder; import org.folio.rest.jaxrs.resource.AuditDataAcquisition; import org.folio.rest.tools.utils.TenantTool; import org.folio.services.acquisition.OrderAuditEventsService; import org.folio.services.acquisition.OrderLineAuditEventsService; +import org.folio.services.acquisition.PieceAuditEventsService; import org.folio.spring.SpringContextUtil; import org.folio.util.ErrorUtils; import org.springframework.beans.factory.annotation.Autowired; @@ -28,9 +31,10 @@ public class AuditDataAcquisitionImpl implements AuditDataAcquisition { @Autowired private OrderAuditEventsService orderAuditEventsService; - @Autowired private OrderLineAuditEventsService orderLineAuditEventsService; + @Autowired + private PieceAuditEventsService pieceAuditEventsService; public AuditDataAcquisitionImpl() { SpringContextUtil.autowireDependencies(this, Vertx.currentContext()); @@ -41,20 +45,16 @@ public void getAuditDataAcquisitionOrderById(String orderId, String sortBy, Audi int limit, int offset, Map okapiHeaders, Handler> asyncResultHandler, Context vertxContext) { LOGGER.debug("getAuditDataAcquisitionOrderById:: Retrieving Audit Data Acquisition Order By Id : {}", orderId); String tenantId = TenantTool.tenantId(okapiHeaders); - - vertxContext.runOnContext(c -> { - try { - LOGGER.warn("Trying to get audit events by order id: {}", orderId); - orderAuditEventsService.getAuditEventsByOrderId(orderId, sortBy, sortOrder.name(), limit, offset, tenantId) - .map(GetAuditDataAcquisitionOrderByIdResponse::respond200WithApplicationJson) - .map(Response.class::cast) - .otherwise(this::mapExceptionToResponse) - .onComplete(asyncResultHandler); - } catch (Exception e) { - LOGGER.warn("Failed to get order audit events by order id: {}", orderId, e); - asyncResultHandler.handle(Future.succeededFuture(mapExceptionToResponse(e))); - } - }); + try { + orderAuditEventsService.getAuditEventsByOrderId(orderId, sortBy, sortOrder.name(), limit, offset, tenantId) + .map(GetAuditDataAcquisitionOrderByIdResponse::respond200WithApplicationJson) + .map(Response.class::cast) + .otherwise(this::mapExceptionToResponse) + .onComplete(asyncResultHandler); + } catch (Exception e) { + LOGGER.error("Failed to get order audit events by order id: {}", orderId, e); + asyncResultHandler.handle(Future.succeededFuture(mapExceptionToResponse(e))); + } } @Override @@ -62,27 +62,58 @@ public void getAuditDataAcquisitionOrderLineById(String orderLineId, String sort int limit, int offset, Map okapiHeaders, Handler> asyncResultHandler, Context vertxContext) { LOGGER.debug("getAuditDataAcquisitionOrderLineById:: Retrieving Audit Data Acquisition Order Line By Id : {}", orderLineId); String tenantId = TenantTool.tenantId(okapiHeaders); + try { + orderLineAuditEventsService.getAuditEventsByOrderLineId(orderLineId, sortBy, sortOrder.name(), limit, offset, tenantId) + .map(GetAuditDataAcquisitionOrderLineByIdResponse::respond200WithApplicationJson) + .map(Response.class::cast) + .otherwise(this::mapExceptionToResponse) + .onComplete(asyncResultHandler); + } catch (Exception e) { + LOGGER.error("Failed to get order line audit events by order line id: {}", orderLineId, e); + asyncResultHandler.handle(Future.succeededFuture(mapExceptionToResponse(e))); + } - vertxContext.runOnContext(c -> { - try { - LOGGER.warn("Trying to get audit events by order line id: {}", orderLineId); - orderLineAuditEventsService.getAuditEventsByOrderLineId(orderLineId, sortBy, sortOrder.name(), limit, offset, tenantId) - .map(GetAuditDataAcquisitionOrderLineByIdResponse::respond200WithApplicationJson) - .map(Response.class::cast) - .otherwise(this::mapExceptionToResponse) - .onComplete(asyncResultHandler); - } catch (Exception e) { - LOGGER.warn("Failed to get order line audit events by order line id: {}", orderLineId, e); - asyncResultHandler.handle(Future.succeededFuture(mapExceptionToResponse(e))); - } - }); + } + + @Override + public void getAuditDataAcquisitionPieceById(String pieceId, String sortBy, AuditDataAcquisitionPieceIdGetSortOrder sortOrder, + int limit, int offset, Map okapiHeaders, Handler> asyncResultHandler, Context vertxContext) { + LOGGER.debug("getAuditDataAcquisitionOrderById:: Retrieving Audit Data Acquisition Piece By Id : {}", pieceId); + String tenantId = TenantTool.tenantId(okapiHeaders); + try { + pieceAuditEventsService.getAuditEventsByPieceId(pieceId, sortBy, sortOrder.name(), limit, offset, tenantId) + .map(GetAuditDataAcquisitionPieceByIdResponse::respond200WithApplicationJson) + .map(Response.class::cast) + .otherwise(this::mapExceptionToResponse) + .onComplete(asyncResultHandler); + } catch (Exception e) { + LOGGER.error("Failed to get piece audit events by piece id: {}", pieceId, e); + asyncResultHandler.handle(Future.succeededFuture(mapExceptionToResponse(e))); + } + } + + @Override + public void getAuditDataAcquisitionPieceStatusChangeHistoryById(String pieceId, String sortBy, + AuditDataAcquisitionPieceIdStatusChangeHistoryGetSortOrder sortOrder, + int limit, int offset, Map okapiHeaders, Handler> asyncResultHandler, Context vertxContext) { + LOGGER.debug("getAuditDataAcquisitionOrderById:: Retrieving Audit Data Acquisition Piece with status changes By Id : {}", pieceId); + String tenantId = TenantTool.tenantId(okapiHeaders); + try { + pieceAuditEventsService.getAuditEventsWithStatusChangesByPieceId(pieceId, sortBy, sortOrder.name(), limit, offset, tenantId) + .map(GetAuditDataAcquisitionPieceByIdResponse::respond200WithApplicationJson) + .map(Response.class::cast) + .otherwise(this::mapExceptionToResponse) + .onComplete(asyncResultHandler); + } catch (Exception e) { + LOGGER.error("Failed to get piece audit events with unique status change by piece id: {}", pieceId, e); + asyncResultHandler.handle(Future.succeededFuture(mapExceptionToResponse(e))); + } } private Response mapExceptionToResponse(Throwable throwable) { LOGGER.debug("mapExceptionToResponse:: Mapping Exception :{} to Response", throwable.getMessage(), throwable); return GetAuditDataAcquisitionOrderByIdResponse - .respond500WithApplicationJson(ErrorUtils.buildErrors(GENERIC_ERROR_CODE.getCode(), throwable)); + .respond500WithApplicationJson(ErrorUtils.buildErrors(GENERIC_ERROR_CODE.getCode(), throwable)); } - } diff --git a/mod-audit-server/src/main/java/org/folio/rest/impl/InitAPIs.java b/mod-audit-server/src/main/java/org/folio/rest/impl/InitAPIs.java index d243823f..7a5028d7 100644 --- a/mod-audit-server/src/main/java/org/folio/rest/impl/InitAPIs.java +++ b/mod-audit-server/src/main/java/org/folio/rest/impl/InitAPIs.java @@ -17,6 +17,7 @@ import org.folio.verticle.SpringVerticleFactory; import org.folio.verticle.acquisition.OrderEventConsumersVerticle; import org.folio.verticle.acquisition.OrderLineEventConsumersVerticle; +import org.folio.verticle.acquisition.PieceEventConsumersVerticle; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.support.AbstractApplicationContext; @@ -30,6 +31,8 @@ public class InitAPIs implements InitAPI { private int acqOrderConsumerInstancesNumber; @Value("${acq.order-lines.kafka.consumer.instancesNumber:1}") private int acqOrderLineConsumerInstancesNumber; + @Value("${acq.pieces.kafka.consumer.instancesNumber:1}") + private int acqPieceConsumerInstancesNumber; @Override public void init(Vertx vertx, Context context, Handler> handler) { @@ -60,6 +63,7 @@ private Future deployConsumersVerticles(Vertx vertx) { Promise orderEventsConsumer = Promise.promise(); Promise orderLineEventsConsumer = Promise.promise(); + Promise pieceEventsConsumer = Promise.promise(); vertx.deployVerticle(getVerticleName(verticleFactory, OrderEventConsumersVerticle.class), new DeploymentOptions() @@ -71,10 +75,16 @@ private Future deployConsumersVerticles(Vertx vertx) { .setWorker(true) .setInstances(acqOrderLineConsumerInstancesNumber), orderLineEventsConsumer); + vertx.deployVerticle(getVerticleName(verticleFactory, PieceEventConsumersVerticle.class), + new DeploymentOptions() + .setWorker(true) + .setInstances(acqPieceConsumerInstancesNumber), pieceEventsConsumer); + LOGGER.info("deployConsumersVerticles:: All consumer verticles were successfully deployed"); return GenericCompositeFuture.all(Arrays.asList( orderEventsConsumer.future(), - orderLineEventsConsumer.future())); + orderLineEventsConsumer.future(), + pieceEventsConsumer.future())); } private String getVerticleName(VerticleFactory verticleFactory, Class clazz) { diff --git a/mod-audit-server/src/main/java/org/folio/services/acquisition/PieceAuditEventsService.java b/mod-audit-server/src/main/java/org/folio/services/acquisition/PieceAuditEventsService.java new file mode 100644 index 00000000..9b11db32 --- /dev/null +++ b/mod-audit-server/src/main/java/org/folio/services/acquisition/PieceAuditEventsService.java @@ -0,0 +1,44 @@ +package org.folio.services.acquisition; + +import io.vertx.core.Future; +import io.vertx.sqlclient.Row; +import io.vertx.sqlclient.RowSet; +import org.folio.rest.jaxrs.model.PieceAuditEvent; +import org.folio.rest.jaxrs.model.PieceAuditEventCollection; + +public interface PieceAuditEventsService { + + /** + * Saves Piece Audit Event + * + * @param pieceAuditEvent pieceAuditEvent + * @param tenantId id of tenant + * @return + */ + Future> savePieceAuditEvent(PieceAuditEvent pieceAuditEvent, String tenantId); + + /** + * Searches for piece audit events by piece id + * + * @param pieceId piece id + * @param sortBy sort by + * @param sortOrder sort order + * @param limit limit + * @param offset offset + * @return future with PieceAuditEventCollection + */ + Future getAuditEventsByPieceId(String pieceId, String sortBy, String sortOrder, + int limit, int offset, String tenantId); + + /** + * Searches for piece audit events which has unique status changes by piece id + * @param pieceId piece id + * @param sortBy sort by + * @param sortOrder sort order + * @param limit limit + * @param offset offset + * @return future with PieceAuditEventCollection + */ + Future getAuditEventsWithStatusChangesByPieceId(String pieceId, String sortBy, String sortOrder, + int limit, int offset, String tenantId); +} diff --git a/mod-audit-server/src/main/java/org/folio/services/acquisition/impl/OrderAuditEventsServiceImpl.java b/mod-audit-server/src/main/java/org/folio/services/acquisition/impl/OrderAuditEventsServiceImpl.java index 213b2b73..5f254824 100644 --- a/mod-audit-server/src/main/java/org/folio/services/acquisition/impl/OrderAuditEventsServiceImpl.java +++ b/mod-audit-server/src/main/java/org/folio/services/acquisition/impl/OrderAuditEventsServiceImpl.java @@ -1,17 +1,16 @@ package org.folio.services.acquisition.impl; +import static org.folio.util.ErrorUtils.handleFailures; + import io.vertx.core.Future; -import io.vertx.pgclient.PgException; import io.vertx.sqlclient.Row; import io.vertx.sqlclient.RowSet; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.folio.dao.acquisition.OrderEventsDao; -import org.folio.kafka.exception.DuplicateEventException; import org.folio.rest.jaxrs.model.OrderAuditEvent; import org.folio.rest.jaxrs.model.OrderAuditEventCollection; import org.folio.services.acquisition.OrderAuditEventsService; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service @@ -19,19 +18,20 @@ public class OrderAuditEventsServiceImpl implements OrderAuditEventsService { private static final Logger LOGGER = LogManager.getLogger(); - public static final String UNIQUE_CONSTRAINT_VIOLATION_CODE = "23505"; - - private OrderEventsDao orderEventsDao; + private final OrderEventsDao orderEventsDao; - @Autowired public OrderAuditEventsServiceImpl(OrderEventsDao orderEvenDao) { this.orderEventsDao = orderEvenDao; } @Override public Future> saveOrderAuditEvent(OrderAuditEvent orderAuditEvent, String tenantId) { - LOGGER.debug("saveOrderAuditEvent:: Saving order audit event with Id={} for tenantId={}", orderAuditEvent.getId(), tenantId); - return orderEventsDao.save(orderAuditEvent, tenantId).recover(throwable -> handleFailures(throwable, orderAuditEvent.getId())); + LOGGER.debug("saveOrderAuditEvent:: Saving order audit event with orderId={} for tenantId={}", orderAuditEvent.getOrderId(), tenantId); + return orderEventsDao.save(orderAuditEvent, tenantId) + .recover(throwable -> { + LOGGER.error("handleFailures:: Could not save order audit event for Order id: {} in tenantId: {}", orderAuditEvent.getOrderId(), tenantId); + return handleFailures(throwable, orderAuditEvent.getId()); + }); } @Override @@ -40,11 +40,4 @@ public Future getAuditEventsByOrderId(String orderId, return orderEventsDao.getAuditEventsByOrderId(orderId, sortBy, sortOrder, limit, offset, tenantId); } - private Future handleFailures(Throwable throwable, String id) { - LOGGER.debug("handleFailures:: Handling Failures with Id : {}", id); - return (throwable instanceof PgException && ((PgException) throwable).getCode().equals(UNIQUE_CONSTRAINT_VIOLATION_CODE)) ? - Future.failedFuture(new DuplicateEventException(String.format("Event with Id=%s is already processed.", id))) : - Future.failedFuture(throwable); - } - } diff --git a/mod-audit-server/src/main/java/org/folio/services/acquisition/impl/OrderLineAuditEventsServiceImpl.java b/mod-audit-server/src/main/java/org/folio/services/acquisition/impl/OrderLineAuditEventsServiceImpl.java index ecc275c0..0e3d299d 100644 --- a/mod-audit-server/src/main/java/org/folio/services/acquisition/impl/OrderLineAuditEventsServiceImpl.java +++ b/mod-audit-server/src/main/java/org/folio/services/acquisition/impl/OrderLineAuditEventsServiceImpl.java @@ -1,17 +1,16 @@ package org.folio.services.acquisition.impl; +import static org.folio.util.ErrorUtils.handleFailures; + import io.vertx.core.Future; -import io.vertx.pgclient.PgException; import io.vertx.sqlclient.Row; import io.vertx.sqlclient.RowSet; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.folio.dao.acquisition.OrderLineEventsDao; -import org.folio.kafka.exception.DuplicateEventException; import org.folio.rest.jaxrs.model.OrderLineAuditEvent; import org.folio.rest.jaxrs.model.OrderLineAuditEventCollection; import org.folio.services.acquisition.OrderLineAuditEventsService; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service @@ -19,19 +18,20 @@ public class OrderLineAuditEventsServiceImpl implements OrderLineAuditEventsServ private static final Logger LOGGER = LogManager.getLogger(); - public static final String UNIQUE_CONSTRAINT_VIOLATION_CODE = "23505"; - - private OrderLineEventsDao orderLineEventsDao; + private final OrderLineEventsDao orderLineEventsDao; - @Autowired public OrderLineAuditEventsServiceImpl(OrderLineEventsDao orderLineEventsDao) { this.orderLineEventsDao = orderLineEventsDao; } @Override public Future> saveOrderLineAuditEvent(OrderLineAuditEvent orderLineAuditEvent, String tenantId) { - LOGGER.debug("saveOrderLineAuditEvent:: Saving order line audit event with tenant Id : {}", tenantId); - return orderLineEventsDao.save(orderLineAuditEvent, tenantId).recover(throwable -> handleFailures(throwable, orderLineAuditEvent.getId())); + LOGGER.debug("saveOrderLineAuditEvent:: Saving order line audit event with id: {} in tenant Id : {}", orderLineAuditEvent.getId(), tenantId); + return orderLineEventsDao.save(orderLineAuditEvent, tenantId) + .recover(throwable -> { + LOGGER.error("handleFailures:: Could not save order audit event for OrderLine id: {} in tenantId: {}", orderLineAuditEvent.getOrderLineId(), tenantId); + return handleFailures(throwable, orderLineAuditEvent.getId()); + }); } @Override @@ -40,11 +40,4 @@ public Future getAuditEventsByOrderLineId(String return orderLineEventsDao.getAuditEventsByOrderLineId(orderLineId, sortBy, sortOrder, limit, offset, tenantId); } - private Future handleFailures(Throwable throwable, String id) { - LOGGER.debug("handleFailures:: Handling Failures with Id : {}", id); - return (throwable instanceof PgException && ((PgException) throwable).getCode().equals(UNIQUE_CONSTRAINT_VIOLATION_CODE)) ? - Future.failedFuture(new DuplicateEventException(String.format("Event with Id=%s is already processed.", id))) : - Future.failedFuture(throwable); - } - } diff --git a/mod-audit-server/src/main/java/org/folio/services/acquisition/impl/PieceAuditEventsServiceImpl.java b/mod-audit-server/src/main/java/org/folio/services/acquisition/impl/PieceAuditEventsServiceImpl.java new file mode 100644 index 00000000..299b6e0a --- /dev/null +++ b/mod-audit-server/src/main/java/org/folio/services/acquisition/impl/PieceAuditEventsServiceImpl.java @@ -0,0 +1,48 @@ +package org.folio.services.acquisition.impl; + +import static org.folio.util.ErrorUtils.handleFailures; + +import io.vertx.core.Future; +import io.vertx.sqlclient.Row; +import io.vertx.sqlclient.RowSet; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.folio.dao.acquisition.PieceEventsDao; +import org.folio.rest.jaxrs.model.PieceAuditEvent; +import org.folio.rest.jaxrs.model.PieceAuditEventCollection; +import org.folio.services.acquisition.PieceAuditEventsService; +import org.springframework.stereotype.Service; + +@Service +public class PieceAuditEventsServiceImpl implements PieceAuditEventsService { + private static final Logger LOGGER = LogManager.getLogger(); + + private final PieceEventsDao pieceEventsDao; + + public PieceAuditEventsServiceImpl(PieceEventsDao pieceEventsDao) { + this.pieceEventsDao = pieceEventsDao; + } + + @Override + public Future> savePieceAuditEvent(PieceAuditEvent pieceAuditEvent, String tenantId) { + LOGGER.debug("savePieceAuditEvent:: Trying to save piece audit event with id={} for tenantId={}", pieceAuditEvent.getPieceId(), tenantId); + return pieceEventsDao.save(pieceAuditEvent, tenantId) + .recover(throwable -> { + LOGGER.error("handleFailures:: Could not save order audit event for Piece id: {} in tenantId: {}", pieceAuditEvent.getPieceId(), tenantId); + return handleFailures(throwable, pieceAuditEvent.getId()); + }); + } + + @Override + public Future getAuditEventsByPieceId(String pieceId, String sortBy, String sortOrder, int limit, int offset, String tenantId) { + LOGGER.debug("getAuditEventsByOrderLineId:: Trying to retrieve audit events for piece Id : {} and tenant Id : {}", pieceId, tenantId); + return pieceEventsDao.getAuditEventsByPieceId(pieceId, sortBy, sortOrder, limit, offset, tenantId); + } + + @Override + public Future getAuditEventsWithStatusChangesByPieceId(String pieceId, String sortBy, String sortOrder, int limit, int offset, String tenantId) { + LOGGER.debug("getAuditEventsByOrderId:: Retrieving audit events with unique status changes for pieceId={} and tenantId={}", pieceId, tenantId); + return pieceEventsDao.getAuditEventsWithStatusChangesByPieceId(pieceId, sortBy, sortOrder, limit, offset, tenantId); + } + +} diff --git a/mod-audit-server/src/main/java/org/folio/util/AcquisitionEventType.java b/mod-audit-server/src/main/java/org/folio/util/AcquisitionEventType.java index e2c9a6ec..3ecb182e 100644 --- a/mod-audit-server/src/main/java/org/folio/util/AcquisitionEventType.java +++ b/mod-audit-server/src/main/java/org/folio/util/AcquisitionEventType.java @@ -2,7 +2,8 @@ public enum AcquisitionEventType { ACQ_ORDER_CHANGED("ACQ_ORDER_CHANGED"), - ACQ_ORDER_LINE_CHANGED("ACQ_ORDER_LINE_CHANGED"); + ACQ_ORDER_LINE_CHANGED("ACQ_ORDER_LINE_CHANGED"), + ACQ_PIECE_CHANGED("ACQ_PIECE_CHANGED"); private final String topicName; diff --git a/mod-audit-server/src/main/java/org/folio/util/OrderAuditEventDBConstants.java b/mod-audit-server/src/main/java/org/folio/util/AuditEventDBConstants.java similarity index 76% rename from mod-audit-server/src/main/java/org/folio/util/OrderAuditEventDBConstants.java rename to mod-audit-server/src/main/java/org/folio/util/AuditEventDBConstants.java index 77f10d14..62d40cfc 100644 --- a/mod-audit-server/src/main/java/org/folio/util/OrderAuditEventDBConstants.java +++ b/mod-audit-server/src/main/java/org/folio/util/AuditEventDBConstants.java @@ -1,10 +1,8 @@ package org.folio.util; -import org.apache.commons.lang3.StringUtils; +public class AuditEventDBConstants { -public class OrderAuditEventDBConstants { - - private OrderAuditEventDBConstants() {} + private AuditEventDBConstants() {} public static final String ID_FIELD = "id"; @@ -14,6 +12,8 @@ private OrderAuditEventDBConstants() {} public static final String ORDER_LINE_ID_FIELD = "order_line_id"; + public static final String PIECE_ID_FIELD = "piece_id"; + public static final String USER_ID_FIELD = "user_id"; public static final String EVENT_DATE_FIELD = "event_date"; @@ -26,4 +26,5 @@ private OrderAuditEventDBConstants() {} public static final String ORDER_BY_PATTERN = "ORDER BY %s %s"; + public static final String UNIQUE_CONSTRAINT_VIOLATION_CODE = "23505"; } diff --git a/mod-audit-server/src/main/java/org/folio/util/DbUtils.java b/mod-audit-server/src/main/java/org/folio/util/DbUtils.java new file mode 100644 index 00000000..7d6443cc --- /dev/null +++ b/mod-audit-server/src/main/java/org/folio/util/DbUtils.java @@ -0,0 +1,13 @@ +package org.folio.util; + +import static java.lang.String.format; +import static org.folio.rest.persist.PostgresClient.convertToPsqlStandard; + +public class DbUtils { + private DbUtils() { + } + + public static String formatDBTableName(String tenantId, String table) { + return format("%s.%s", convertToPsqlStandard(tenantId), table); + } +} diff --git a/mod-audit-server/src/main/java/org/folio/util/ErrorUtils.java b/mod-audit-server/src/main/java/org/folio/util/ErrorUtils.java index 12ebd9ae..4d7b8e74 100644 --- a/mod-audit-server/src/main/java/org/folio/util/ErrorUtils.java +++ b/mod-audit-server/src/main/java/org/folio/util/ErrorUtils.java @@ -1,6 +1,11 @@ package org.folio.util; +import static org.folio.util.AuditEventDBConstants.UNIQUE_CONSTRAINT_VIOLATION_CODE; + +import io.vertx.core.Future; +import io.vertx.pgclient.PgException; import org.folio.HttpStatus; +import org.folio.kafka.exception.DuplicateEventException; import org.folio.rest.jaxrs.model.Error; import org.folio.rest.jaxrs.model.Errors; @@ -20,4 +25,9 @@ public static Errors buildErrors(String statusCode, Throwable throwable) { .withTotalRecords(1); } + public static Future handleFailures(Throwable throwable, String id) { + return (throwable instanceof PgException pgException && pgException.getCode().equals(UNIQUE_CONSTRAINT_VIOLATION_CODE)) ? + Future.failedFuture(new DuplicateEventException(String.format("Event with id=%s is already processed.", id))) : + Future.failedFuture(throwable); + } } diff --git a/mod-audit-server/src/main/java/org/folio/util/PostgresClientFactory.java b/mod-audit-server/src/main/java/org/folio/util/PostgresClientFactory.java index 40aab653..304b981c 100644 --- a/mod-audit-server/src/main/java/org/folio/util/PostgresClientFactory.java +++ b/mod-audit-server/src/main/java/org/folio/util/PostgresClientFactory.java @@ -3,15 +3,14 @@ import io.vertx.core.Vertx; import org.folio.rest.persist.PostgresClient; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class PostgresClientFactory { - private Vertx vertx; + private final Vertx vertx; - public PostgresClientFactory(@Autowired Vertx vertx) { + public PostgresClientFactory(Vertx vertx) { this.vertx = vertx; } diff --git a/mod-audit-server/src/main/java/org/folio/verticle/acquisition/PieceEventConsumersVerticle.java b/mod-audit-server/src/main/java/org/folio/verticle/acquisition/PieceEventConsumersVerticle.java new file mode 100644 index 00000000..7e13ef16 --- /dev/null +++ b/mod-audit-server/src/main/java/org/folio/verticle/acquisition/PieceEventConsumersVerticle.java @@ -0,0 +1,28 @@ +package org.folio.verticle.acquisition; + +import java.util.List; + +import org.folio.kafka.AsyncRecordHandler; +import org.folio.util.AcquisitionEventType; +import org.folio.verticle.AbstractConsumersVerticle; +import org.springframework.stereotype.Component; + +@Component +public class PieceEventConsumersVerticle extends AbstractConsumersVerticle { + + private final AsyncRecordHandler pieceEventsHandler; + + public PieceEventConsumersVerticle(AsyncRecordHandler pieceEventsHandler) { + this.pieceEventsHandler = pieceEventsHandler; + } + + @Override + public List getEvents() { + return List.of(AcquisitionEventType.ACQ_PIECE_CHANGED.getTopicName()); + } + + @Override + public AsyncRecordHandler getHandler() { + return pieceEventsHandler; + } +} diff --git a/mod-audit-server/src/main/java/org/folio/verticle/acquisition/consumers/PieceEventsHandler.java b/mod-audit-server/src/main/java/org/folio/verticle/acquisition/consumers/PieceEventsHandler.java new file mode 100644 index 00000000..90a4e098 --- /dev/null +++ b/mod-audit-server/src/main/java/org/folio/verticle/acquisition/consumers/PieceEventsHandler.java @@ -0,0 +1,60 @@ +package org.folio.verticle.acquisition.consumers; + +import java.util.List; + +import io.vertx.core.Future; +import io.vertx.core.Promise; +import io.vertx.core.Vertx; +import io.vertx.core.json.JsonObject; +import io.vertx.kafka.client.consumer.KafkaConsumerRecord; +import io.vertx.kafka.client.producer.KafkaHeader; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.folio.kafka.AsyncRecordHandler; +import org.folio.kafka.KafkaHeaderUtils; +import org.folio.kafka.exception.DuplicateEventException; +import org.folio.rest.jaxrs.model.PieceAuditEvent; +import org.folio.rest.util.OkapiConnectionParams; +import org.folio.services.acquisition.PieceAuditEventsService; +import org.springframework.stereotype.Component; + +@Component +public class PieceEventsHandler implements AsyncRecordHandler { + + private static final Logger LOGGER = LogManager.getLogger(); + + private final PieceAuditEventsService pieceAuditEventsService; + private final Vertx vertx; + + public PieceEventsHandler(Vertx vertx, + PieceAuditEventsService pieceAuditEventsService) { + this.pieceAuditEventsService = pieceAuditEventsService; + this.vertx = vertx; + } + + @Override + public Future handle(KafkaConsumerRecord kafkaConsumerRecord) { + Promise result = Promise.promise(); + List kafkaHeaders = kafkaConsumerRecord.headers(); + OkapiConnectionParams okapiConnectionParams = new OkapiConnectionParams(KafkaHeaderUtils.kafkaHeadersToMap(kafkaHeaders), vertx); + PieceAuditEvent event = new JsonObject(kafkaConsumerRecord.value()).mapTo(PieceAuditEvent.class); + LOGGER.info("handle:: Starting processing of Piece audit event with id: {} for piece id: {}", event.getId(), event.getPieceId()); + + pieceAuditEventsService.savePieceAuditEvent(event, okapiConnectionParams.getTenantId()) + .onSuccess(ar -> { + LOGGER.info("handle:: Piece audit event with id: {} has been processed for piece id: {}", event.getId(), event.getPieceId()); + result.complete(event.getId()); + }) + .onFailure(e -> { + if (e instanceof DuplicateEventException) { + LOGGER.info("handle:: Duplicate Piece audit event with id: {} for piece id: {} received, skipped processing", event.getId(), event.getPieceId()); + result.complete(event.getId()); + } else { + LOGGER.error("Processing of Piece audit event with id: {} for piece id: {} has been failed", event.getId(), event.getPieceId(), e); + result.fail(e); + } + }); + + return result.future(); + } +} diff --git a/mod-audit-server/src/main/resources/templates/db_scripts/acquisition/create_acquisition_piece_log_table.sql b/mod-audit-server/src/main/resources/templates/db_scripts/acquisition/create_acquisition_piece_log_table.sql new file mode 100644 index 00000000..cf82a593 --- /dev/null +++ b/mod-audit-server/src/main/resources/templates/db_scripts/acquisition/create_acquisition_piece_log_table.sql @@ -0,0 +1,11 @@ +CREATE TABLE IF NOT EXISTS acquisition_piece_log ( + id uuid PRIMARY KEY, + action text NOT NULL, + piece_id uuid NOT NULL, + user_id uuid NOT NULL, + event_date timestamp NOT NULL, + action_date timestamp NOT NULL, + modified_content_snapshot jsonb +); + +CREATE INDEX IF NOT EXISTS piece_id_index ON acquisition_piece_log USING BTREE (piece_id); diff --git a/mod-audit-server/src/main/resources/templates/db_scripts/schema.json b/mod-audit-server/src/main/resources/templates/db_scripts/schema.json index 0262feff..be4d0804 100644 --- a/mod-audit-server/src/main/resources/templates/db_scripts/schema.json +++ b/mod-audit-server/src/main/resources/templates/db_scripts/schema.json @@ -74,6 +74,11 @@ "run": "after", "snippetPath": "acquisition/create_acquisition_order_line_log_table.sql", "fromModuleVersion": "mod-audit-2.7.0" + }, + { + "run": "after", + "snippetPath": "acquisition/create_acquisition_piece_log_table.sql", + "fromModuleVersion": "mod-audit-2.9.0" } ] } diff --git a/mod-audit-server/src/test/java/org/folio/TestSuite.java b/mod-audit-server/src/test/java/org/folio/TestSuite.java index b76663c3..797a0916 100644 --- a/mod-audit-server/src/test/java/org/folio/TestSuite.java +++ b/mod-audit-server/src/test/java/org/folio/TestSuite.java @@ -1,5 +1,8 @@ package org.folio; +import static net.mguenther.kafka.junit.EmbeddedKafkaCluster.provisionWith; +import static net.mguenther.kafka.junit.EmbeddedKafkaClusterConfig.defaultClusterConfig; + import java.util.Locale; import java.util.Objects; import java.util.concurrent.CompletableFuture; @@ -7,6 +10,10 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import io.restassured.RestAssured; +import io.vertx.core.DeploymentOptions; +import io.vertx.core.Vertx; +import io.vertx.core.json.JsonObject; import net.mguenther.kafka.junit.EmbeddedKafkaCluster; import org.folio.builder.service.CheckInRecordBuilderTest; import org.folio.builder.service.CheckOutRecordBuilderTest; @@ -18,25 +25,24 @@ import org.folio.builder.service.RequestRecordBuilderTest; import org.folio.dao.OrderEventsDaoTest; import org.folio.dao.OrderLineEventsDaoTest; +import org.folio.dao.PieceEventsDaoTest; import org.folio.postgres.testing.PostgresTesterContainer; import org.folio.rest.RestVerticle; -import org.folio.rest.impl.*; import org.folio.rest.impl.AuditDataAcquisitionAPITest; +import org.folio.rest.impl.AuditDataImplApiTest; +import org.folio.rest.impl.AuditHandlersImplApiTest; +import org.folio.rest.impl.CirculationLogsImplApiTest; +import org.folio.rest.impl.OrderEventsHandlerMockTest; +import org.folio.rest.impl.OrderLineEventsHandlerMockTest; +import org.folio.rest.impl.PieceEventsHandlerMockTest; import org.folio.rest.persist.PostgresClient; import org.folio.services.OrderAuditEventsServiceTest; import org.folio.services.OrderLineAuditEventsServiceTest; +import org.folio.services.PieceAuditEventsServiceTest; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Nested; -import io.restassured.RestAssured; -import io.vertx.core.DeploymentOptions; -import io.vertx.core.Vertx; -import io.vertx.core.json.JsonObject; - -import static net.mguenther.kafka.junit.EmbeddedKafkaCluster.provisionWith; -import static net.mguenther.kafka.junit.EmbeddedKafkaClusterConfig.defaultClusterConfig; - public class TestSuite { private static final String KAFKA_HOST = "KAFKA_HOST"; private static final String KAFKA_PORT = "KAFKA_PORT"; @@ -98,7 +104,7 @@ private static void startVerticle(DeploymentOptions options) CompletableFuture deploymentComplete = new CompletableFuture<>(); vertx.deployVerticle(RestVerticle.class.getName(), options, res -> { - if(res.succeeded()) { + if (res.succeeded()) { deploymentComplete.complete(res.result()); } else { @@ -176,6 +182,18 @@ class OrderLineAuditEventsServiceNestedTest extends OrderLineAuditEventsServiceT class OrderLineEventsDaoNestedTest extends OrderLineEventsDaoTest { } + @Nested + class PieceEventsDaoNestedTest extends PieceEventsDaoTest { + } + + @Nested + class PieceAuditEventsServiceNestedTest extends PieceAuditEventsServiceTest { + } + + @Nested + class PieceEventsHandlerMockNestedTest extends PieceEventsHandlerMockTest { + } + @Nested class AuditDataImplApiTestNested extends AuditDataImplApiTest { } @@ -184,5 +202,4 @@ class AuditDataImplApiTestNested extends AuditDataImplApiTest { class CirculationLogsImplApiTestNested extends CirculationLogsImplApiTest { } - } diff --git a/mod-audit-server/src/test/java/org/folio/dao/OrderEventsDaoTest.java b/mod-audit-server/src/test/java/org/folio/dao/OrderEventsDaoTest.java index 4077de08..d052046e 100644 --- a/mod-audit-server/src/test/java/org/folio/dao/OrderEventsDaoTest.java +++ b/mod-audit-server/src/test/java/org/folio/dao/OrderEventsDaoTest.java @@ -1,33 +1,30 @@ package org.folio.dao; +import static org.folio.utils.EntityUtils.TENANT_ID; +import static org.folio.utils.EntityUtils.createOrderAuditEvent; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.UUID; + import io.vertx.core.Future; import io.vertx.core.Vertx; -import io.vertx.core.json.JsonObject; import io.vertx.pgclient.PgException; import io.vertx.sqlclient.Row; import io.vertx.sqlclient.RowSet; import org.folio.dao.acquisition.impl.OrderEventsDaoImpl; import org.folio.rest.jaxrs.model.OrderAuditEvent; import org.folio.rest.jaxrs.model.OrderAuditEventCollection; +import org.folio.util.PostgresClientFactory; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.MockitoAnnotations; import org.mockito.Spy; -import org.folio.util.PostgresClientFactory; - -import java.util.Date; -import java.util.List; -import java.util.UUID; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; public class OrderEventsDaoTest { - private static final String TENANT_ID = "diku"; - public static final String ORDER_ID = "a21fc51c-d46b-439b-8c79-9b2be41b79a6"; - @Spy private PostgresClientFactory postgresClientFactory = new PostgresClientFactory(Vertx.vertx()); @InjectMocks @@ -41,63 +38,31 @@ public void setUp() { @Test void shouldCreateEventProcessed() { - JsonObject jsonObject = new JsonObject(); - jsonObject.put("name","Test Product 123 "); - - OrderAuditEvent orderAuditEvent = new OrderAuditEvent() - .withId(UUID.randomUUID().toString()) - .withAction(OrderAuditEvent.Action.CREATE) - .withOrderId(ORDER_ID) - .withUserId(UUID.randomUUID().toString()) - .withEventDate(new Date()) - .withActionDate(new Date()) - .withOrderSnapshot(jsonObject); + var orderAuditEvent = createOrderAuditEvent(UUID.randomUUID().toString()); Future> saveFuture = orderEventDao.save(orderAuditEvent, TENANT_ID); - saveFuture.onComplete(ar -> { - assertTrue(ar.succeeded()); - }); + saveFuture.onComplete(ar -> assertTrue(ar.succeeded())); } @Test void shouldThrowConstraintViolation() { - JsonObject jsonObject = new JsonObject(); - jsonObject.put("name","Test Product1"); - - OrderAuditEvent orderAuditEvent = new OrderAuditEvent() - .withId(UUID.randomUUID().toString()) - .withAction(OrderAuditEvent.Action.CREATE) - .withOrderId(ORDER_ID) - .withUserId(UUID.randomUUID().toString()) - .withEventDate(new Date()) - .withActionDate(new Date()) - .withOrderSnapshot(jsonObject); + var orderAuditEvent = createOrderAuditEvent(UUID.randomUUID().toString()); Future> saveFuture = orderEventDao.save(orderAuditEvent, TENANT_ID); saveFuture.onComplete(ar -> { Future> reSaveFuture = orderEventDao.save(orderAuditEvent, TENANT_ID); reSaveFuture.onComplete(re -> { assertTrue(re.failed()); - assertTrue(re.cause() instanceof PgException); + assertTrue(re.cause() instanceof PgException); assertEquals("ERROR: duplicate key value violates unique constraint \"acquisition_order_log_pkey\" (23505)", re.cause().getMessage()); }); }); - } + } @Test void shouldGetCreatedEvent() { - JsonObject jsonObject = new JsonObject(); - jsonObject.put("name","Test Product2"); String id = UUID.randomUUID().toString(); - - OrderAuditEvent orderAuditEvent = new OrderAuditEvent() - .withId(id) - .withAction(OrderAuditEvent.Action.CREATE) - .withOrderId(ORDER_ID) - .withUserId(UUID.randomUUID().toString()) - .withEventDate(new Date()) - .withActionDate(new Date()) - .withOrderSnapshot(jsonObject); + var orderAuditEvent = createOrderAuditEvent(id); orderEventDao.save(orderAuditEvent, TENANT_ID); @@ -108,7 +73,6 @@ void shouldGetCreatedEvent() { assertEquals(orderAuditEventList.get(0).getId(), id); assertEquals(OrderAuditEvent.Action.CREATE.value(), orderAuditEventList.get(0).getAction().value()); - }); } diff --git a/mod-audit-server/src/test/java/org/folio/dao/OrderLineEventsDaoTest.java b/mod-audit-server/src/test/java/org/folio/dao/OrderLineEventsDaoTest.java index 6e9df2d8..88d9ae87 100644 --- a/mod-audit-server/src/test/java/org/folio/dao/OrderLineEventsDaoTest.java +++ b/mod-audit-server/src/test/java/org/folio/dao/OrderLineEventsDaoTest.java @@ -1,8 +1,15 @@ package org.folio.dao; +import static org.folio.utils.EntityUtils.TENANT_ID; +import static org.folio.utils.EntityUtils.createOrderLineAuditEvent; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.UUID; + import io.vertx.core.Future; import io.vertx.core.Vertx; -import io.vertx.core.json.JsonObject; import io.vertx.pgclient.PgException; import io.vertx.sqlclient.Row; import io.vertx.sqlclient.RowSet; @@ -17,19 +24,8 @@ import org.mockito.MockitoAnnotations; import org.mockito.Spy; -import java.util.Date; -import java.util.List; -import java.util.UUID; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - public class OrderLineEventsDaoTest { - private static final String TENANT_ID = "diku"; - - public static final String ORDER_LINE_ID = "a21fc51c-d46b-439b-8c79-9b2be41b79a6"; - @Spy private PostgresClientFactory postgresClientFactory = new PostgresClientFactory(Vertx.vertx()); @@ -44,30 +40,14 @@ public void setUp() { @Test void shouldCreateEventProcessed() { - OrderLineAuditEvent orderLineAuditEvent = new OrderLineAuditEvent() - .withId(UUID.randomUUID().toString()) - .withAction(OrderLineAuditEvent.Action.CREATE) - .withOrderId(UUID.randomUUID().toString()) - .withOrderLineId(UUID.randomUUID().toString()) - .withUserId(UUID.randomUUID().toString()) - .withEventDate(new Date()) - .withActionDate(new Date()); + var orderLineAuditEvent = createOrderLineAuditEvent(UUID.randomUUID().toString()); Future> saveFuture = orderLineEventsDao.save(orderLineAuditEvent, TENANT_ID); - saveFuture.onComplete(ar -> { - assertTrue(ar.succeeded()); - }); + saveFuture.onComplete(ar -> assertTrue(ar.succeeded())); } @Test void shouldThrowConstraintViolation() { - OrderLineAuditEvent orderLineAuditEvent = new OrderLineAuditEvent() - .withId(UUID.randomUUID().toString()) - .withAction(OrderLineAuditEvent.Action.CREATE) - .withOrderId(UUID.randomUUID().toString()) - .withOrderLineId(UUID.randomUUID().toString()) - .withUserId(UUID.randomUUID().toString()) - .withEventDate(new Date()) - .withActionDate(new Date()); + var orderLineAuditEvent = createOrderLineAuditEvent(UUID.randomUUID().toString()); Future> saveFuture = orderLineEventsDao.save(orderLineAuditEvent, TENANT_ID); saveFuture.onComplete(ar -> { @@ -82,19 +62,8 @@ void shouldThrowConstraintViolation() { @Test void shouldGetCreatedEvent() { - JsonObject jsonObject = new JsonObject(); - jsonObject.put("name","Test Product2"); String id = UUID.randomUUID().toString(); - - OrderLineAuditEvent orderLineAuditEvent = new OrderLineAuditEvent() - .withId(id) - .withAction(OrderLineAuditEvent.Action.CREATE) - .withOrderId(UUID.randomUUID().toString()) - .withOrderLineId(ORDER_LINE_ID) - .withUserId(UUID.randomUUID().toString()) - .withEventDate(new Date()) - .withActionDate(new Date()) - .withOrderLineSnapshot(jsonObject); + var orderLineAuditEvent = createOrderLineAuditEvent(id); orderLineEventsDao.save(orderLineAuditEvent, TENANT_ID); @@ -105,7 +74,6 @@ void shouldGetCreatedEvent() { assertEquals(orderLineAuditEventList.get(0).getId(), id); assertEquals(OrderAuditEvent.Action.CREATE.value(), orderLineAuditEventList.get(0).getAction().value()); - }); } diff --git a/mod-audit-server/src/test/java/org/folio/dao/PieceEventsDaoTest.java b/mod-audit-server/src/test/java/org/folio/dao/PieceEventsDaoTest.java new file mode 100644 index 00000000..817d0b78 --- /dev/null +++ b/mod-audit-server/src/test/java/org/folio/dao/PieceEventsDaoTest.java @@ -0,0 +1,77 @@ +package org.folio.dao; + +import static org.folio.utils.EntityUtils.TENANT_ID; +import static org.folio.utils.EntityUtils.createPieceAuditEvent; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.UUID; + +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import io.vertx.pgclient.PgException; +import io.vertx.sqlclient.Row; +import io.vertx.sqlclient.RowSet; +import org.folio.dao.acquisition.impl.PieceEventsDaoImpl; +import org.folio.rest.jaxrs.model.PieceAuditEvent; +import org.folio.rest.jaxrs.model.PieceAuditEventCollection; +import org.folio.util.PostgresClientFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.MockitoAnnotations; +import org.mockito.Spy; + +public class PieceEventsDaoTest { + + @Spy + PostgresClientFactory postgresClientFactory = new PostgresClientFactory(Vertx.vertx()); + @InjectMocks + PieceEventsDaoImpl pieceEventsDao; + + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + pieceEventsDao = new PieceEventsDaoImpl(postgresClientFactory); + } + + @Test + void shouldCreateEventProcessed() { + var pieceAuditEvent = createPieceAuditEvent(UUID.randomUUID().toString()); + + Future> saveFuture = pieceEventsDao.save(pieceAuditEvent, TENANT_ID); + saveFuture.onComplete(ar -> assertTrue(ar.succeeded())); + } + + @Test + void shouldThrowConstrainViolation() { + var pieceAuditEvent = createPieceAuditEvent(UUID.randomUUID().toString()); + + Future> saveFuture = pieceEventsDao.save(pieceAuditEvent, TENANT_ID); + saveFuture.onComplete(ar -> { + Future> reSaveFuture = pieceEventsDao.save(pieceAuditEvent, TENANT_ID); + reSaveFuture.onComplete(re -> { + assertTrue(re.failed()); + assertTrue(re.cause() instanceof PgException); + assertEquals("ERROR: duplicate key value violates unique constraint \"acquisition_piece_log_pkey\" (23505)", re.cause().getMessage()); + }); + }); + } + + @Test + void shouldGetCreatedEvent() { + String id = UUID.randomUUID().toString(); + var pieceAuditEvent = createPieceAuditEvent(id); + + pieceEventsDao.save(pieceAuditEvent, TENANT_ID); + + Future saveFuture = pieceEventsDao.getAuditEventsByPieceId(id, "action_date", "desc", 1, 1, TENANT_ID); + saveFuture.onComplete(ar -> { + PieceAuditEventCollection pieceAuditEventCollection = ar.result(); + List pieceAuditEventList = pieceAuditEventCollection.getPieceAuditEvents(); + assertEquals(pieceAuditEventList.get(0).getId(), id); + assertEquals(PieceAuditEvent.Action.CREATE.value(), pieceAuditEventList.get(0).getAction().value()); + }); + } +} diff --git a/mod-audit-server/src/test/java/org/folio/rest/impl/AuditDataAcquisitionAPITest.java b/mod-audit-server/src/test/java/org/folio/rest/impl/AuditDataAcquisitionAPITest.java index ac7fde01..4d71eb05 100644 --- a/mod-audit-server/src/test/java/org/folio/rest/impl/AuditDataAcquisitionAPITest.java +++ b/mod-audit-server/src/test/java/org/folio/rest/impl/AuditDataAcquisitionAPITest.java @@ -1,12 +1,26 @@ package org.folio.rest.impl; +import static io.restassured.RestAssured.given; +import static org.folio.utils.EntityUtils.ORDER_ID; +import static org.folio.utils.EntityUtils.ORDER_LINE_ID; +import static org.folio.utils.EntityUtils.PIECE_ID; +import static org.folio.utils.EntityUtils.createPieceAuditEvent; +import static org.hamcrest.Matchers.containsString; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Date; +import java.util.UUID; + import io.restassured.http.Header; import io.vertx.core.Vertx; import io.vertx.core.json.JsonObject; import org.folio.dao.acquisition.impl.OrderEventsDaoImpl; import org.folio.dao.acquisition.impl.OrderLineEventsDaoImpl; +import org.folio.dao.acquisition.impl.PieceEventsDaoImpl; import org.folio.rest.jaxrs.model.OrderAuditEvent; import org.folio.rest.jaxrs.model.OrderLineAuditEvent; +import org.folio.rest.jaxrs.model.PieceAuditEvent; import org.folio.util.PostgresClientFactory; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -14,48 +28,33 @@ import org.mockito.MockitoAnnotations; import org.mockito.Spy; - -import java.util.Date; -import java.util.UUID; - -import static io.restassured.RestAssured.given; -import static org.hamcrest.Matchers.*; - public class AuditDataAcquisitionAPITest extends ApiTestBase { - public static final Header TENANT = new Header("X-Okapi-Tenant", "modaudittest"); - - protected static final Header PERMS = new Header("X-Okapi-Permissions", "audit.all"); - - protected static final Header CONTENT_TYPE = new Header("Content-Type", "application/json"); - - protected static final String INVALID_ID = "646ea52c-2c65-4d28-9a8f-e0d200fd6b00"; - - protected static final String ACQ_AUDIT_ORDER_PATH = "/audit-data/acquisition/order/"; - - protected static final String ACQ_AUDIT_ORDER_LINE_PATH = "/audit-data/acquisition/order-line/"; - + private static final Header TENANT = new Header("X-Okapi-Tenant", "modaudittest"); + private static final Header PERMS = new Header("X-Okapi-Permissions", "audit.all"); + private static final Header CONTENT_TYPE = new Header("Content-Type", "application/json"); + private static final String INVALID_ID = "646ea52c-2c65-4d28-9a8f-e0d200fd6b00"; + private static final String ACQ_AUDIT_ORDER_PATH = "/audit-data/acquisition/order/"; + private static final String ACQ_AUDIT_ORDER_LINE_PATH = "/audit-data/acquisition/order-line/"; + private static final String ACQ_AUDIT_PIECE_PATH = "/audit-data/acquisition/piece/"; + private static final String ACQ_AUDIT_PIECE_STATUS_CHANGE_HISTORY_PATH = "/status-change-history"; private static final String TENANT_ID = "modaudittest"; - public static final String ORDER_ID = "a21fc51c-d46b-439b-8c79-9b2be41b79a6"; - - public static final String ORDER_LINE_ID = "a22fc51c-d46b-439b-8c79-9b2be41b79a6"; - @Spy private PostgresClientFactory postgresClientFactory = new PostgresClientFactory(Vertx.vertx()); @InjectMocks - OrderEventsDaoImpl orderEventDao = new OrderEventsDaoImpl(postgresClientFactory); - + OrderEventsDaoImpl orderEventDao; @InjectMocks - OrderLineEventsDaoImpl orderLineEventDao = new OrderLineEventsDaoImpl(postgresClientFactory); + OrderLineEventsDaoImpl orderLineEventDao; + @InjectMocks + PieceEventsDaoImpl pieceEventsDao; @BeforeEach public void setUp() { MockitoAnnotations.openMocks(this); orderEventDao = new OrderEventsDaoImpl(postgresClientFactory); orderLineEventDao = new OrderLineEventsDaoImpl(postgresClientFactory); - } @Test @@ -104,10 +103,12 @@ void shouldReturnOrderLineEventsOnGetByOrderLineId() { orderLineEventDao.save(orderLineAuditEvent, TENANT_ID); - given().header(CONTENT_TYPE).header(TENANT).header(PERMS).get(ACQ_AUDIT_ORDER_LINE_PATH+ INVALID_ID).then().log().all().statusCode(200) + given().header(CONTENT_TYPE).header(TENANT).header(PERMS).get(ACQ_AUDIT_ORDER_LINE_PATH+ INVALID_ID) + .then().log().all().statusCode(200) .body(containsString("orderLineAuditEvents")).body(containsString("totalItems")); - given().header(CONTENT_TYPE).header(TENANT).header(PERMS).get(ACQ_AUDIT_ORDER_LINE_PATH+ ORDER_LINE_ID).then().log().all().statusCode(200) + given().header(CONTENT_TYPE).header(TENANT).header(PERMS).get(ACQ_AUDIT_ORDER_LINE_PATH+ ORDER_LINE_ID) + .then().log().all().statusCode(200) .body(containsString(ORDER_LINE_ID)); given().header(CONTENT_TYPE).header(TENANT).header(PERMS).get(ACQ_AUDIT_ORDER_LINE_PATH+ ORDER_LINE_ID +"?limit=1").then().log().all().statusCode(200) @@ -119,4 +120,119 @@ void shouldReturnOrderLineEventsOnGetByOrderLineId() { given().header(CONTENT_TYPE).header(TENANT).header(PERMS).get(ACQ_AUDIT_ORDER_PATH+ ORDER_LINE_ID + 123).then().log().all().statusCode(500) .body(containsString("UUID string too large")); } + + @Test + void shouldReturnPieceEventsOnGetByPieceId() { + JsonObject jsonObject = new JsonObject(); + jsonObject.put("name","Test Product2"); + + PieceAuditEvent pieceAuditEvent = new PieceAuditEvent() + .withId(UUID.randomUUID().toString()) + .withAction(PieceAuditEvent.Action.CREATE) + .withPieceId(PIECE_ID) + .withUserId(UUID.randomUUID().toString()) + .withEventDate(new Date()) + .withActionDate(new Date()) + .withPieceSnapshot(jsonObject); + + pieceEventsDao.save(pieceAuditEvent, TENANT_ID); + + given().header(CONTENT_TYPE).header(TENANT).header(PERMS) + .get(ACQ_AUDIT_PIECE_PATH + INVALID_ID) + .then().log().all().statusCode(200) + .body(containsString("pieceAuditEvents")).body(containsString("totalItems")); + + given().header(CONTENT_TYPE).header(TENANT).header(PERMS) + .get(ACQ_AUDIT_PIECE_PATH + PIECE_ID) + .then().log().all().statusCode(200) + .body(containsString(PIECE_ID)); + + given().header(CONTENT_TYPE).header(TENANT).header(PERMS) + .get(ACQ_AUDIT_PIECE_PATH + PIECE_ID +"?limit=1") + .then().log().all().statusCode(200) + .body(containsString(PIECE_ID)); + + given().header(CONTENT_TYPE).header(TENANT).header(PERMS) + .get(ACQ_AUDIT_PIECE_PATH + PIECE_ID + "?sortBy=action_date") + .then().log().all().statusCode(200) + .body(containsString(PIECE_ID)); + + given().header(CONTENT_TYPE).header(TENANT).header(PERMS) + .get(ACQ_AUDIT_PIECE_PATH + PIECE_ID + 123).then().log().all().statusCode(500) + .body(containsString("UUID string too large")); + } + + @Test + void shouldReturnPieceEventsStatusChangesHistoryGetByPieceId() { + String id1 = UUID.randomUUID().toString(); + String id2 = UUID.randomUUID().toString(); + String id3 = UUID.randomUUID().toString(); + String id4 = UUID.randomUUID().toString(); + String id5 = UUID.randomUUID().toString(); + String id6 = UUID.randomUUID().toString(); + String id7 = UUID.randomUUID().toString(); + var pieceAuditEvent1 = createPieceAuditEvent(id1, "STATUS 1"); + var pieceAuditEvent2 = createPieceAuditEvent(id2, "STATUS 1"); + var pieceAuditEvent3 = createPieceAuditEvent(id3, "STATUS 2"); + var pieceAuditEvent4 = createPieceAuditEvent(id4, "STATUS 2"); + var pieceAuditEvent5 = createPieceAuditEvent(id5, "STATUS 1"); + var pieceAuditEventWithDifferentPiece1 = createPieceAuditEvent(id6, "STATUS 3"); + var pieceAuditEventWithDifferentPiece2 = createPieceAuditEvent(id7, "STATUS 2"); + pieceAuditEventWithDifferentPiece1.setPieceId(UUID.randomUUID().toString()); + pieceAuditEventWithDifferentPiece2.setPieceId(UUID.randomUUID().toString()); + var localDateTime1 = LocalDateTime.of(2023, 4, 20, 6, 9, 30); + var localDateTime2 = LocalDateTime.of(2023, 4, 20, 6, 10, 30); + var localDateTime3 = LocalDateTime.of(2023, 4, 20, 6, 11, 30); + var localDateTime4 = LocalDateTime.of(2023, 4, 20, 6, 12, 30); + var localDateTime5 = LocalDateTime.of(2023, 4, 20, 6, 13, 30); + var localDateTime6 = LocalDateTime.of(2023, 4, 20, 6, 9, 25); + var localDateTime7 = LocalDateTime.of(2023, 4, 20, 6, 9, 20); + pieceAuditEvent1.setActionDate(Date.from(localDateTime1.atZone(ZoneId.systemDefault()).toInstant())); + pieceAuditEvent2.setActionDate(Date.from(localDateTime2.atZone(ZoneId.systemDefault()).toInstant())); + pieceAuditEvent3.setActionDate(Date.from(localDateTime3.atZone(ZoneId.systemDefault()).toInstant())); + pieceAuditEvent4.setActionDate(Date.from(localDateTime4.atZone(ZoneId.systemDefault()).toInstant())); + pieceAuditEvent5.setActionDate(Date.from(localDateTime5.atZone(ZoneId.systemDefault()).toInstant())); + pieceAuditEventWithDifferentPiece1.setActionDate(Date.from(localDateTime6.atZone(ZoneId.systemDefault()).toInstant())); + pieceAuditEventWithDifferentPiece2.setActionDate(Date.from(localDateTime7.atZone(ZoneId.systemDefault()).toInstant())); + + pieceEventsDao.save(pieceAuditEvent1, TENANT_ID); + pieceEventsDao.save(pieceAuditEvent2, TENANT_ID); + pieceEventsDao.save(pieceAuditEvent3, TENANT_ID); + pieceEventsDao.save(pieceAuditEvent4, TENANT_ID); + pieceEventsDao.save(pieceAuditEvent5, TENANT_ID); + pieceEventsDao.save(pieceAuditEventWithDifferentPiece1, TENANT_ID); + pieceEventsDao.save(pieceAuditEventWithDifferentPiece2, TENANT_ID); + + // based on our business logic, it returns pieceAuditEvent1, pieceAuditEvent3, pieceAuditEvent5 + given().header(CONTENT_TYPE).header(TENANT).header(PERMS) + .get(ACQ_AUDIT_PIECE_PATH + INVALID_ID + ACQ_AUDIT_PIECE_STATUS_CHANGE_HISTORY_PATH) + .then().log().all().statusCode(200) + .body(containsString("pieceAuditEvents")).body(containsString("totalItems")); + + given().header(CONTENT_TYPE).header(TENANT).header(PERMS) + .get(ACQ_AUDIT_PIECE_PATH + PIECE_ID + ACQ_AUDIT_PIECE_STATUS_CHANGE_HISTORY_PATH) + .then().log().all().statusCode(200) + .body(containsString(PIECE_ID)) + .body(containsString(id1)) + .body(containsString(id3)) + .body(containsString(id5)); + + given().header(CONTENT_TYPE).header(TENANT).header(PERMS) + .get(ACQ_AUDIT_PIECE_PATH + PIECE_ID + ACQ_AUDIT_PIECE_STATUS_CHANGE_HISTORY_PATH +"?limit=1") + .then().log().all().statusCode(200) + .body(containsString(PIECE_ID)); + + given().header(CONTENT_TYPE).header(TENANT).header(PERMS) + .get(ACQ_AUDIT_PIECE_PATH + PIECE_ID + ACQ_AUDIT_PIECE_STATUS_CHANGE_HISTORY_PATH +"?sortBy=action_date") + .then().log().all().statusCode(200) + .body(containsString(PIECE_ID)) + .body(containsString(id1)) + .body(containsString(id3)) + .body(containsString(id5)); + + given().header(CONTENT_TYPE).header(TENANT).header(PERMS) + .get(ACQ_AUDIT_PIECE_PATH + PIECE_ID + 123 + ACQ_AUDIT_PIECE_STATUS_CHANGE_HISTORY_PATH) + .then().log().all().statusCode(500) + .body(containsString("UUID string too large")); + } } diff --git a/mod-audit-server/src/test/java/org/folio/rest/impl/OrderEventsHandlerMockTest.java b/mod-audit-server/src/test/java/org/folio/rest/impl/OrderEventsHandlerMockTest.java index af96368b..31e321d9 100644 --- a/mod-audit-server/src/test/java/org/folio/rest/impl/OrderEventsHandlerMockTest.java +++ b/mod-audit-server/src/test/java/org/folio/rest/impl/OrderEventsHandlerMockTest.java @@ -3,7 +3,6 @@ import io.vertx.core.Future; import io.vertx.core.Vertx; import io.vertx.core.json.Json; -import io.vertx.core.json.JsonObject; import io.vertx.kafka.client.consumer.KafkaConsumerRecord; import io.vertx.kafka.client.consumer.impl.KafkaConsumerRecordImpl; @@ -22,10 +21,11 @@ import org.mockito.*; import java.nio.charset.StandardCharsets; -import java.util.Date; import java.util.UUID; import static org.folio.kafka.KafkaTopicNameHelper.getDefaultNameSpace; +import static org.folio.utils.EntityUtils.createOrderAuditEvent; +import static org.folio.utils.EntityUtils.createOrderAuditEventWithoutSnapshot; import static org.junit.Assert.assertTrue; public class OrderEventsHandlerMockTest { @@ -63,16 +63,7 @@ public void setUp() { @Test void shouldProcessEvent() { - JsonObject jsonObject = new JsonObject(); - jsonObject.put("Test","TestValue"); - - OrderAuditEvent orderAuditEvent = new OrderAuditEvent() - .withId(ID) - .withEventDate(new Date()) - .withOrderId(UUID.randomUUID().toString()) - .withActionDate(new Date()) - .withAction(OrderAuditEvent.Action.CREATE) - .withOrderSnapshot(jsonObject); + var orderAuditEvent = createOrderAuditEvent(UUID.randomUUID().toString()); KafkaConsumerRecord kafkaConsumerRecord = buildKafkaConsumerRecord(orderAuditEvent); Future saveFuture = orderEventsHandler.handle(kafkaConsumerRecord); @@ -83,14 +74,9 @@ void shouldProcessEvent() { @Test void shouldNotProcessEvent() { - OrderAuditEvent orderAuditEvent = new OrderAuditEvent() - .withId(UUID.randomUUID().toString()) - .withEventDate(new Date()) - .withOrderId(UUID.randomUUID().toString()) - .withActionDate(new Date()) - .withAction(OrderAuditEvent.Action.CREATE) - .withOrderSnapshot("Test"); + var orderAuditEvent = createOrderAuditEventWithoutSnapshot(); KafkaConsumerRecord kafkaConsumerRecord = buildKafkaConsumerRecord(orderAuditEvent); + Future save = orderEventsHandler.handle(kafkaConsumerRecord); assertTrue(save.failed()); } diff --git a/mod-audit-server/src/test/java/org/folio/rest/impl/PieceEventsHandlerMockTest.java b/mod-audit-server/src/test/java/org/folio/rest/impl/PieceEventsHandlerMockTest.java new file mode 100644 index 00000000..553a446c --- /dev/null +++ b/mod-audit-server/src/test/java/org/folio/rest/impl/PieceEventsHandlerMockTest.java @@ -0,0 +1,92 @@ +package org.folio.rest.impl; + + +import static org.folio.kafka.KafkaTopicNameHelper.getDefaultNameSpace; +import static org.folio.utils.EntityUtils.createPieceAuditEvent; +import static org.folio.utils.EntityUtils.createPieceAuditEventWithoutSnapshot; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.charset.StandardCharsets; +import java.util.UUID; + +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import io.vertx.core.json.Json; +import io.vertx.kafka.client.consumer.KafkaConsumerRecord; +import io.vertx.kafka.client.consumer.impl.KafkaConsumerRecordImpl; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.common.header.internals.RecordHeader; +import org.folio.dao.acquisition.impl.PieceEventsDaoImpl; +import org.folio.kafka.KafkaTopicNameHelper; +import org.folio.rest.jaxrs.model.PieceAuditEvent; +import org.folio.rest.util.OkapiConnectionParams; +import org.folio.services.acquisition.PieceAuditEventsService; +import org.folio.services.acquisition.impl.PieceAuditEventsServiceImpl; +import org.folio.util.PostgresClientFactory; +import org.folio.verticle.acquisition.consumers.PieceEventsHandler; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.Spy; + +public class PieceEventsHandlerMockTest { + private static final String TENANT_ID = "diku"; + protected static final String TOKEN = "token"; + protected static final String KAFKA_EVN = "folio"; + public static final String OKAPI_TOKEN_HEADER = "x-okapi-token"; + public static final String OKAPI_URL_HEADER = "x-okapi-url"; + + @Spy + private Vertx vertx = Vertx.vertx(); + @Spy + private PostgresClientFactory postgresClientFactory = new PostgresClientFactory(Vertx.vertx()); + + @Mock + PieceEventsDaoImpl pieceEventsDao; + @Mock + PieceAuditEventsService pieceAuditEventsService; + + private PieceEventsHandler pieceEventsHandler; + + @BeforeEach + public void setUp() throws Exception { + MockitoAnnotations.openMocks(this).close(); + pieceEventsDao = new PieceEventsDaoImpl(postgresClientFactory); + pieceAuditEventsService = new PieceAuditEventsServiceImpl(pieceEventsDao); + pieceEventsHandler = new PieceEventsHandler(vertx, pieceAuditEventsService); + } + + @Test + void shouldProcessEvent() { + var pieceAuditEvent = createPieceAuditEvent(UUID.randomUUID().toString()); + KafkaConsumerRecord kafkaConsumerRecord = buildKafkaConsumerRecord(pieceAuditEvent); + Future saveFuture = pieceEventsHandler.handle(kafkaConsumerRecord); + saveFuture.onComplete(are -> { + assertTrue(are.succeeded()); + }); + } + + @Test + void shouldNotProcessEvent() { + var pieceAuditEvent = createPieceAuditEventWithoutSnapshot(); + KafkaConsumerRecord kafkaConsumerRecord = buildKafkaConsumerRecord(pieceAuditEvent); + Future saveFuture = pieceEventsHandler.handle(kafkaConsumerRecord); + assertTrue(saveFuture.failed()); + } + + private KafkaConsumerRecord buildKafkaConsumerRecord(PieceAuditEvent event) { + String topic = KafkaTopicNameHelper.formatTopicName(KAFKA_EVN, getDefaultNameSpace(), TENANT_ID, event.getAction().name()); + ConsumerRecord consumerRecord = buildConsumerRecord(topic, event); + return new KafkaConsumerRecordImpl<>(consumerRecord); + } + + protected ConsumerRecord buildConsumerRecord(String topic, PieceAuditEvent event) { + ConsumerRecord consumer = new ConsumerRecord<>("folio", 0, 0, topic, Json.encode(event)); + consumer.headers().add(new RecordHeader(OkapiConnectionParams.OKAPI_TENANT_HEADER, TENANT_ID.getBytes(StandardCharsets.UTF_8))); + consumer.headers().add(new RecordHeader(OKAPI_URL_HEADER, ("https://localhost:" + 8080).getBytes(StandardCharsets.UTF_8))); + consumer.headers().add(new RecordHeader(OKAPI_TOKEN_HEADER, TOKEN.getBytes(StandardCharsets.UTF_8))); + return consumer; + } + +} diff --git a/mod-audit-server/src/test/java/org/folio/services/OrderAuditEventsServiceTest.java b/mod-audit-server/src/test/java/org/folio/services/OrderAuditEventsServiceTest.java index 89cc275f..2bd3904c 100644 --- a/mod-audit-server/src/test/java/org/folio/services/OrderAuditEventsServiceTest.java +++ b/mod-audit-server/src/test/java/org/folio/services/OrderAuditEventsServiceTest.java @@ -1,8 +1,15 @@ package org.folio.services; +import static org.folio.utils.EntityUtils.TENANT_ID; +import static org.folio.utils.EntityUtils.createOrderAuditEvent; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.UUID; + import io.vertx.core.Future; import io.vertx.core.Vertx; -import io.vertx.core.json.JsonObject; import io.vertx.sqlclient.Row; import io.vertx.sqlclient.RowSet; import org.folio.dao.acquisition.OrderEventsDao; @@ -16,17 +23,8 @@ import org.mockito.Mock; import org.mockito.Spy; -import java.util.Date; -import java.util.List; -import java.util.UUID; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - public class OrderAuditEventsServiceTest { - private static final String TENANT_ID = "diku"; - @Spy private PostgresClientFactory postgresClientFactory = new PostgresClientFactory(Vertx.vertx()); @Mock @@ -37,17 +35,7 @@ public class OrderAuditEventsServiceTest { @Test void shouldCallDaoForSuccessfulCase() { - JsonObject jsonObject = new JsonObject(); - jsonObject.put("name","Test Product"); - - OrderAuditEvent orderAuditEvent = new OrderAuditEvent() - .withId(UUID.randomUUID().toString()) - .withAction(OrderAuditEvent.Action.CREATE) - .withOrderId(UUID.randomUUID().toString()) - .withUserId(UUID.randomUUID().toString()) - .withEventDate(new Date()) - .withActionDate(new Date()) - .withOrderSnapshot(jsonObject); + var orderAuditEvent = createOrderAuditEvent(UUID.randomUUID().toString()); Future> saveFuture = orderAuditEventService.saveOrderAuditEvent(orderAuditEvent, TENANT_ID); saveFuture.onComplete(ar -> { @@ -57,18 +45,8 @@ void shouldCallDaoForSuccessfulCase() { @Test void shouldGetDto() { - JsonObject jsonObject = new JsonObject(); - jsonObject.put("name","Test Product"); - String id = UUID.randomUUID().toString(); - OrderAuditEvent orderAuditEvent = new OrderAuditEvent() - .withId(id) - .withAction(OrderAuditEvent.Action.CREATE) - .withOrderId(UUID.randomUUID().toString()) - .withUserId(UUID.randomUUID().toString()) - .withEventDate(new Date()) - .withActionDate(new Date()) - .withOrderSnapshot(jsonObject); + var orderAuditEvent = createOrderAuditEvent(id); orderAuditEventService.saveOrderAuditEvent(orderAuditEvent, TENANT_ID); @@ -79,7 +57,6 @@ void shouldGetDto() { assertEquals(orderAuditEventList.get(0).getId(), id); assertEquals(OrderAuditEvent.Action.CREATE.value(), orderAuditEventList.get(0).getAction().value()); - }); } diff --git a/mod-audit-server/src/test/java/org/folio/services/OrderLineAuditEventsServiceTest.java b/mod-audit-server/src/test/java/org/folio/services/OrderLineAuditEventsServiceTest.java index 3d0523d6..272c9816 100644 --- a/mod-audit-server/src/test/java/org/folio/services/OrderLineAuditEventsServiceTest.java +++ b/mod-audit-server/src/test/java/org/folio/services/OrderLineAuditEventsServiceTest.java @@ -2,7 +2,6 @@ import io.vertx.core.Future; import io.vertx.core.Vertx; -import io.vertx.core.json.JsonObject; import io.vertx.sqlclient.Row; import io.vertx.sqlclient.RowSet; import org.folio.dao.acquisition.OrderLineEventsDao; @@ -16,34 +15,27 @@ import org.mockito.Mock; import org.mockito.Spy; -import java.util.Date; import java.util.List; import java.util.UUID; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +import static org.folio.utils.EntityUtils.TENANT_ID; +import static org.folio.utils.EntityUtils.createOrderLineAuditEvent; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; public class OrderLineAuditEventsServiceTest { - private static final String TENANT_ID = "diku"; - @Spy private PostgresClientFactory postgresClientFactory = new PostgresClientFactory(Vertx.vertx()); @Mock OrderLineEventsDao orderLineEventsDao = new OrderLineEventsDaoImpl(postgresClientFactory); - @InjectMocks OrderLineAuditEventsServiceImpl orderLineAuditEventService = new OrderLineAuditEventsServiceImpl(orderLineEventsDao); @Test public void shouldCallDaoForSuccessfulCase() { - OrderLineAuditEvent orderLineAuditEvent = new OrderLineAuditEvent() - .withId(UUID.randomUUID().toString()) - .withAction(OrderLineAuditEvent.Action.CREATE) - .withOrderId(UUID.randomUUID().toString()) - .withUserId(UUID.randomUUID().toString()) - .withEventDate(new Date()) - .withActionDate(new Date()); + var orderLineAuditEvent = createOrderLineAuditEvent(UUID.randomUUID().toString()); + Future> saveFuture = orderLineAuditEventService.saveOrderLineAuditEvent(orderLineAuditEvent, TENANT_ID); saveFuture.onComplete(ar -> { @@ -53,19 +45,8 @@ public void shouldCallDaoForSuccessfulCase() { @Test void shouldGetOrderLineDto() { - JsonObject jsonObject = new JsonObject(); - jsonObject.put("name", "Test Product"); - String id = UUID.randomUUID().toString(); - OrderLineAuditEvent orderLineAuditEvent = new OrderLineAuditEvent() - .withId(id) - .withAction(OrderLineAuditEvent.Action.CREATE) - .withOrderId(UUID.randomUUID().toString()) - .withOrderLineId(UUID.randomUUID().toString()) - .withUserId(UUID.randomUUID().toString()) - .withEventDate(new Date()) - .withActionDate(new Date()) - .withOrderLineSnapshot(jsonObject); + var orderLineAuditEvent = createOrderLineAuditEvent(id); orderLineAuditEventService.saveOrderLineAuditEvent(orderLineAuditEvent, TENANT_ID); diff --git a/mod-audit-server/src/test/java/org/folio/services/PieceAuditEventsServiceTest.java b/mod-audit-server/src/test/java/org/folio/services/PieceAuditEventsServiceTest.java new file mode 100644 index 00000000..df2af446 --- /dev/null +++ b/mod-audit-server/src/test/java/org/folio/services/PieceAuditEventsServiceTest.java @@ -0,0 +1,40 @@ +package org.folio.services; + +import static org.folio.utils.EntityUtils.TENANT_ID; +import static org.folio.utils.EntityUtils.createPieceAuditEvent; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.UUID; + +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import io.vertx.sqlclient.Row; +import io.vertx.sqlclient.RowSet; +import org.folio.dao.acquisition.PieceEventsDao; +import org.folio.dao.acquisition.impl.PieceEventsDaoImpl; +import org.folio.services.acquisition.impl.PieceAuditEventsServiceImpl; +import org.folio.util.PostgresClientFactory; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; + +public class PieceAuditEventsServiceTest { + + @Spy + private PostgresClientFactory postgresClientFactory = new PostgresClientFactory(Vertx.vertx()); + @Mock + PieceEventsDao pieceEventsDao = new PieceEventsDaoImpl(postgresClientFactory); + @InjectMocks + PieceAuditEventsServiceImpl pieceAuditEventsService = new PieceAuditEventsServiceImpl(pieceEventsDao); + + @Test + void shouldCallDaoSuccessfully() { + var pieceAuditEvent = createPieceAuditEvent(UUID.randomUUID().toString()); + + Future> saveFuture = pieceAuditEventsService.savePieceAuditEvent(pieceAuditEvent, TENANT_ID); + saveFuture.onComplete(ar -> { + assertTrue(ar.succeeded()); + }); + } +} diff --git a/mod-audit-server/src/test/java/org/folio/utils/EntityUtils.java b/mod-audit-server/src/test/java/org/folio/utils/EntityUtils.java new file mode 100644 index 00000000..ce3ea468 --- /dev/null +++ b/mod-audit-server/src/test/java/org/folio/utils/EntityUtils.java @@ -0,0 +1,97 @@ +package org.folio.utils; + +import java.util.Date; +import java.util.UUID; + +import io.vertx.core.json.JsonObject; +import org.folio.rest.jaxrs.model.OrderAuditEvent; +import org.folio.rest.jaxrs.model.OrderLineAuditEvent; +import org.folio.rest.jaxrs.model.PieceAuditEvent; + +public class EntityUtils { + + public static String TENANT_ID = "diku"; + public static String PIECE_ID = "2cd4adc4-f287-49b6-a9c6-9eacdc4868e7"; + public static String ORDER_ID = "a21fc51c-d46b-439b-8c79-9b2be41b79a6"; + public static String ORDER_LINE_ID = "a22fc51c-d46b-439b-8c79-9b2be41b79a6"; + + public static OrderAuditEvent createOrderAuditEvent(String id) { + JsonObject jsonObject = new JsonObject(); + jsonObject.put("name", "Test Product 123 "); + + return new OrderAuditEvent() + .withId(UUID.randomUUID().toString()) + .withAction(OrderAuditEvent.Action.CREATE) + .withOrderId(ORDER_ID) + .withUserId(UUID.randomUUID().toString()) + .withEventDate(new Date()) + .withActionDate(new Date()) + .withOrderSnapshot(jsonObject); + } + + public static OrderAuditEvent createOrderAuditEventWithoutSnapshot() { + return new OrderAuditEvent() + .withId(UUID.randomUUID().toString()) + .withAction(OrderAuditEvent.Action.CREATE) + .withOrderId(ORDER_ID) + .withUserId(UUID.randomUUID().toString()) + .withEventDate(new Date()) + .withActionDate(new Date()) + .withOrderSnapshot("Test"); + } + + public static OrderLineAuditEvent createOrderLineAuditEvent(String id) { + JsonObject jsonObject = new JsonObject(); + jsonObject.put("name", "Test Product"); + + return new OrderLineAuditEvent() + .withId(id) + .withAction(OrderLineAuditEvent.Action.CREATE) + .withOrderId(ORDER_ID) + .withOrderLineId(ORDER_LINE_ID) + .withUserId(UUID.randomUUID().toString()) + .withEventDate(new Date()) + .withActionDate(new Date()) + .withOrderLineSnapshot(jsonObject); + } + + public static PieceAuditEvent createPieceAuditEvent(String id) { + JsonObject jsonObject = new JsonObject(); + jsonObject.put("name", "Test Product"); + + return new PieceAuditEvent() + .withId(id) + .withAction(PieceAuditEvent.Action.CREATE) + .withPieceId(PIECE_ID) + .withUserId(UUID.randomUUID().toString()) + .withEventDate(new Date()) + .withActionDate(new Date()) + .withPieceSnapshot(jsonObject); + } + + public static PieceAuditEvent createPieceAuditEvent(String id, String status) { + JsonObject jsonObject = new JsonObject(); + jsonObject.put("name", "Test Product"); + jsonObject.put("receivingStatus", status); + + return new PieceAuditEvent() + .withId(id) + .withAction(PieceAuditEvent.Action.CREATE) + .withPieceId(PIECE_ID) + .withUserId(UUID.randomUUID().toString()) + .withEventDate(new Date()) + .withActionDate(new Date()) + .withPieceSnapshot(jsonObject); + } + + public static PieceAuditEvent createPieceAuditEventWithoutSnapshot() { + return new PieceAuditEvent() + .withId(UUID.randomUUID().toString()) + .withAction(PieceAuditEvent.Action.CREATE) + .withPieceId(PIECE_ID) + .withUserId(UUID.randomUUID().toString()) + .withEventDate(new Date()) + .withActionDate(new Date()) + .withPieceSnapshot("Test"); + } +} diff --git a/pom.xml b/pom.xml index 8af9fa47..a4fd0e44 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 org.folio mod-audit - 2.8.1-SNAPSHOT + 2.9.0-SNAPSHOT pom diff --git a/ramls/acquisition-events.raml b/ramls/acquisition-events.raml index 3f9ce2b5..8bdd0850 100644 --- a/ramls/acquisition-events.raml +++ b/ramls/acquisition-events.raml @@ -15,6 +15,8 @@ types: order-audit-event: !include order_audit_event.json order-audit-event-collection: !include order_audit_event_collection.json order-line-audit-event-collection: !include order_line_audit_event_collection.json + piece-audit-event: !include piece_audit_event.json + piece-audit-event-collection: !include piece_audit_event_collection.json traits: searchable: !include raml-util/traits/searchable.raml @@ -88,3 +90,72 @@ traits: body: application/json: type: errors + + /piece/{id}: + get: + description: Get list of piece events by piece_id + is: [ + pageable, + validate + ] + queryParameters: + sortBy: + description: "sorting by field: actionDate" + type: string + default: action_date + sortOrder: + description: "sort order: asc or desc" + enum: [asc, desc] + type: string + default: desc + limit: + default: 2147483647 + offset: + default: 0 + responses: + 200: + body: + application/json: + type: piece-audit-event-collection + 500: + description: "Internal server error" + body: + application/json: + type: errors + example: + strict: false + value: !include raml-util/examples/errors.sample + /piece/{id}/status-change-history: + get: + description: Get list of piece events which have unique status changes by piece_id + is: [ + pageable, + validate + ] + queryParameters: + sortBy: + description: "sorting by field: actionDate" + type: string + default: action_date + sortOrder: + description: "sort order: asc or desc" + enum: [asc, desc] + type: string + default: desc + limit: + default: 2147483647 + offset: + default: 0 + responses: + 200: + body: + application/json: + type: piece-audit-event-collection + 500: + description: "Internal server error" + body: + application/json: + type: errors + example: + strict: false + value: !include raml-util/examples/errors.sample diff --git a/ramls/piece_audit_event.json b/ramls/piece_audit_event.json new file mode 100644 index 00000000..a1d5141e --- /dev/null +++ b/ramls/piece_audit_event.json @@ -0,0 +1,40 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Piece audit event", + "type": "object", + "properties": { + "id": { + "description": "UUID of the event", + "$ref": "common/uuid.json" + }, + "action": { + "description": "Action for piece (Create, Edit or Delete)", + "type": "string", + "$ref": "event_action.json" + }, + "pieceId": { + "description": "UUID of the piece", + "$ref": "common/uuid.json" + }, + "userId": { + "description": "UUID of the user who performed the action", + "$ref": "common/uuid.json" + }, + "eventDate": { + "description": "Date time when event triggered", + "format": "date-time", + "type": "string" + }, + "actionDate": { + "description": "Date time when piece action occurred", + "format": "date-time", + "type": "string" + }, + "pieceSnapshot": { + "description": "Full snapshot of the piece", + "type": "object", + "javaType": "java.lang.Object" + } + }, + "additionalProperties": false +} diff --git a/ramls/piece_audit_event_collection.json b/ramls/piece_audit_event_collection.json new file mode 100644 index 00000000..6183d57d --- /dev/null +++ b/ramls/piece_audit_event_collection.json @@ -0,0 +1,25 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Collection of pieceLineAuditEvents", + "type": "object", + "additionalProperties": false, + "properties": { + "pieceAuditEvents": { + "description": "List of pieceAuditEvents", + "type": "array", + "id": "pieceAuditEventsList", + "items": { + "type": "object", + "$ref": "piece_audit_event.json" + } + }, + "totalItems": { + "description": "total records", + "type": "integer" + } + }, + "required": [ + "pieceAuditEvents", + "totalItems" + ] +}