From a16ec012a30d04f13ae9e63db5b51c38bdb79f8d Mon Sep 17 00:00:00 2001 From: BKadirkhodjaev Date: Tue, 12 Nov 2024 21:17:22 +0500 Subject: [PATCH] [MODAUD-194]. Implement consumer & endpoint for organization records --- descriptors/ModuleDescriptor-template.json | 23 +++- .../dao/acquisition/InvoiceEventsDao.java | 4 +- .../acquisition/OrganizationEventsDao.java | 32 +++++ .../impl/InvoiceEventsDaoImpl.java | 6 +- .../impl/InvoiceLineEventsDaoImpl.java | 6 +- .../acquisition/impl/OrderEventsDaoImpl.java | 6 +- .../impl/OrderLineEventsDaoImpl.java | 6 +- .../impl/OrganizationEventsDaoImpl.java | 113 ++++++++++++++++++ .../acquisition/impl/PieceEventsDaoImpl.java | 10 +- .../rest/impl/AuditDataAcquisitionImpl.java | 26 +++- .../java/org/folio/rest/impl/InitAPIs.java | 11 +- .../InvoiceAuditEventsService.java | 4 +- .../OrganizationAuditEventsService.java | 31 +++++ .../OrganizationAuditEventsServiceImpl.java | 42 +++++++ .../org/folio/util/AcquisitionEventType.java | 3 +- .../org/folio/util/AuditEventDBConstants.java | 2 + .../OrderEventConsumersVerticle.java | 6 +- .../OrderLineEventConsumersVerticle.java | 4 +- .../OrganizationEventConsumersVerticle.java | 26 ++++ .../consumers/InvoiceEventsHandler.java | 30 ++--- .../consumers/InvoiceLineEventsHandler.java | 29 ++--- .../consumers/OrderEventsHandler.java | 38 ++---- .../consumers/OrderLineEventsHandler.java | 49 +++----- .../consumers/OrganizationEventsHandler.java | 49 ++++++++ .../consumers/PieceEventsHandler.java | 27 ++--- ...ate_acquisition_organization_log_table.sql | 11 ++ .../templates/db_scripts/schema.json | 5 + .../folio/dao/OrganizationEventsDaoTest.java | 80 +++++++++++++ .../impl/AuditDataAcquisitionAPITest.java | 51 ++++++++ .../rest/impl/OrderEventsHandlerMockTest.java | 6 +- .../OrganizationEventsHandlerMockTest.java | 95 +++++++++++++++ .../OrganizationAuditEventsServiceTest.java | 92 ++++++++++++++ .../java/org/folio/utils/EntityUtils.java | 31 ++++- ramls/acquisition-events.raml | 38 +++++- ramls/organization_audit_event.json | 40 +++++++ .../organization_audit_event_collection.json | 25 ++++ 36 files changed, 904 insertions(+), 153 deletions(-) create mode 100644 mod-audit-server/src/main/java/org/folio/dao/acquisition/OrganizationEventsDao.java create mode 100644 mod-audit-server/src/main/java/org/folio/dao/acquisition/impl/OrganizationEventsDaoImpl.java create mode 100644 mod-audit-server/src/main/java/org/folio/services/acquisition/OrganizationAuditEventsService.java create mode 100644 mod-audit-server/src/main/java/org/folio/services/acquisition/impl/OrganizationAuditEventsServiceImpl.java create mode 100644 mod-audit-server/src/main/java/org/folio/verticle/acquisition/OrganizationEventConsumersVerticle.java create mode 100644 mod-audit-server/src/main/java/org/folio/verticle/acquisition/consumers/OrganizationEventsHandler.java create mode 100644 mod-audit-server/src/main/resources/templates/db_scripts/acquisition/create_acquisition_organization_log_table.sql create mode 100644 mod-audit-server/src/test/java/org/folio/dao/OrganizationEventsDaoTest.java create mode 100644 mod-audit-server/src/test/java/org/folio/rest/impl/OrganizationEventsHandlerMockTest.java create mode 100644 mod-audit-server/src/test/java/org/folio/services/OrganizationAuditEventsServiceTest.java create mode 100644 ramls/organization_audit_event.json create mode 100644 ramls/organization_audit_event_collection.json diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index bf8b01cd..4e5bc7f4 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -181,6 +181,21 @@ } ] }, + { + "id": "acquisition-organization-events", + "version": "1.0", + "handlers": [ + { + "methods": [ + "GET" + ], + "pathPattern": "/audit-data/acquisition/organization/{id}", + "permissionsRequired": [ + "acquisition.organization.events.get" + ] + } + ] + }, { "id": "circulation-logs", "version": "1.2", @@ -304,6 +319,11 @@ "displayName": "Acquisition invoice-line events - get invoice-line change events", "description": "Get invoice-line change events" }, + { + "permissionName": "acquisition.organization.events.get", + "displayName": "Acquisition organization events - get organization change events", + "description": "Get organization change events" + }, { "permissionName": "audit.all", "displayName": "Audit - all permissions", @@ -320,7 +340,8 @@ "acquisition.piece.events.get", "acquisition.piece.events.history.get", "acquisition.invoice.events.get", - "acquisition.invoice-line.events.get" + "acquisition.invoice-line.events.get", + "acquisition.organization.events.get" ] } ], diff --git a/mod-audit-server/src/main/java/org/folio/dao/acquisition/InvoiceEventsDao.java b/mod-audit-server/src/main/java/org/folio/dao/acquisition/InvoiceEventsDao.java index fc955b63..c8fcdece 100644 --- a/mod-audit-server/src/main/java/org/folio/dao/acquisition/InvoiceEventsDao.java +++ b/mod-audit-server/src/main/java/org/folio/dao/acquisition/InvoiceEventsDao.java @@ -22,11 +22,11 @@ public interface InvoiceEventsDao { * * @param invoiceId invoice id * @param sortBy sort by - * @param sortInvoice sort invoice + * @param sortOrder sort order * @param limit limit * @param offset offset * @param tenantId tenant id * @return future with InvoiceAuditEventCollection */ - Future getAuditEventsByInvoiceId(String invoiceId, String sortBy, String sortInvoice, int limit, int offset, String tenantId); + Future getAuditEventsByInvoiceId(String invoiceId, String sortBy, String sortOrder, int limit, int offset, String tenantId); } diff --git a/mod-audit-server/src/main/java/org/folio/dao/acquisition/OrganizationEventsDao.java b/mod-audit-server/src/main/java/org/folio/dao/acquisition/OrganizationEventsDao.java new file mode 100644 index 00000000..94041d6c --- /dev/null +++ b/mod-audit-server/src/main/java/org/folio/dao/acquisition/OrganizationEventsDao.java @@ -0,0 +1,32 @@ +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.OrganizationAuditEvent; +import org.folio.rest.jaxrs.model.OrganizationAuditEventCollection; + +public interface OrganizationEventsDao { + + /** + * Saves organizationAuditEvent entity to DB + * + * @param organizationAuditEvent OrganizationAuditEvent entity to save + * @param tenantId tenant id + * @return future with created row + */ + Future> save(OrganizationAuditEvent organizationAuditEvent, String tenantId); + + /** + * Searches for organization audit events by id + * + * @param organizationId organization id + * @param sortBy sort by + * @param sortOrder sort order + * @param limit limit + * @param offset offset + * @param tenantId tenant id + * @return future with OrganizationAuditEventCollection + */ + Future getAuditEventsByOrganizationId(String organizationId, String sortBy, String sortOrder, int limit, int offset, String tenantId); +} diff --git a/mod-audit-server/src/main/java/org/folio/dao/acquisition/impl/InvoiceEventsDaoImpl.java b/mod-audit-server/src/main/java/org/folio/dao/acquisition/impl/InvoiceEventsDaoImpl.java index 8e31a4a4..e8c73f6a 100644 --- a/mod-audit-server/src/main/java/org/folio/dao/acquisition/impl/InvoiceEventsDaoImpl.java +++ b/mod-audit-server/src/main/java/org/folio/dao/acquisition/impl/InvoiceEventsDaoImpl.java @@ -67,8 +67,7 @@ public Future getAuditEventsByInvoiceId(String invo String logTable = formatDBTableName(tenantId, TABLE_NAME); String query = format(GET_BY_INVOICE_ID_SQL, logTable, logTable, format(ORDER_BY_PATTERN, sortBy, sortOrder)); return pgClientFactory.createInstance(tenantId).execute(query, Tuple.of(UUID.fromString(invoiceId), limit, offset)) - .map(rowSet -> rowSet.rowCount() == 0 ? new InvoiceAuditEventCollection().withTotalItems(0) - : mapRowToListOfInvoiceEvent(rowSet)); + .map(this::mapRowToListOfInvoiceEvent); } private Future> makeSaveCall(String query, InvoiceAuditEvent invoiceAuditEvent, String tenantId) { @@ -88,6 +87,9 @@ private Future> makeSaveCall(String query, InvoiceAuditEvent invoice private InvoiceAuditEventCollection mapRowToListOfInvoiceEvent(RowSet rowSet) { LOGGER.debug("mapRowToListOfInvoiceEvent:: Mapping row to List of Invoice Events"); + if (rowSet.rowCount() == 0) { + return new InvoiceAuditEventCollection().withTotalItems(0); + } InvoiceAuditEventCollection invoiceAuditEventCollection = new InvoiceAuditEventCollection(); rowSet.iterator().forEachRemaining(row -> { invoiceAuditEventCollection.getInvoiceAuditEvents().add(mapRowToInvoiceEvent(row)); diff --git a/mod-audit-server/src/main/java/org/folio/dao/acquisition/impl/InvoiceLineEventsDaoImpl.java b/mod-audit-server/src/main/java/org/folio/dao/acquisition/impl/InvoiceLineEventsDaoImpl.java index 242c635b..bf7fa678 100644 --- a/mod-audit-server/src/main/java/org/folio/dao/acquisition/impl/InvoiceLineEventsDaoImpl.java +++ b/mod-audit-server/src/main/java/org/folio/dao/acquisition/impl/InvoiceLineEventsDaoImpl.java @@ -69,8 +69,7 @@ public Future getAuditEventsByInvoiceLineId(Str String logTable = formatDBTableName(tenantId, TABLE_NAME); String query = format(GET_BY_INVOICE_LINE_ID_SQL, logTable, logTable, format(ORDER_BY_PATTERN, sortBy, sortOrder)); return pgClientFactory.createInstance(tenantId).execute(query, Tuple.of(UUID.fromString(invoiceLineId), limit, offset)) - .map(rowSet -> rowSet.rowCount() == 0 ? new InvoiceLineAuditEventCollection().withTotalItems(0) - : mapRowToListOfInvoiceLineEvent(rowSet)); + .map(this::mapRowToListOfInvoiceLineEvent); } private Future> makeSaveCall(String query, InvoiceLineAuditEvent invoiceLineAuditEvent, String tenantId) { @@ -91,6 +90,9 @@ private Future> makeSaveCall(String query, InvoiceLineAuditEvent inv private InvoiceLineAuditEventCollection mapRowToListOfInvoiceLineEvent(RowSet rowSet) { LOGGER.debug("mapRowToListOfInvoiceLineEvent:: Mapping row to List of Invoice Line Events"); + if (rowSet.rowCount() == 0) { + return new InvoiceLineAuditEventCollection().withTotalItems(0); + } InvoiceLineAuditEventCollection invoiceLineAuditEventCollection = new InvoiceLineAuditEventCollection(); rowSet.iterator().forEachRemaining(row -> { invoiceLineAuditEventCollection.getInvoiceLineAuditEvents().add(mapRowToInvoiceLineEvent(row)); 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 d9ee92e4..384a9aaf 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 @@ -78,8 +78,7 @@ public Future getAuditEventsByOrderId(String orderId, promise.fail(e); } LOGGER.info("getAuditEventsByOrderId:: Retrieved AuditEvent with order id : {}", orderId); - return promise.future().map(rowSet -> rowSet.rowCount() == 0 ? new OrderAuditEventCollection().withTotalItems(0) - : mapRowToListOfOrderEvent(rowSet)); + return promise.future().map(this::mapRowToListOfOrderEvent); } private void makeSaveCall(Promise> promise, String query, OrderAuditEvent orderAuditEvent, String tenantId) { @@ -102,6 +101,9 @@ private void makeSaveCall(Promise> promise, String query, OrderAudit private OrderAuditEventCollection mapRowToListOfOrderEvent(RowSet rowSet) { LOGGER.debug("mapRowToListOfOrderEvent:: Mapping row to List of Order Events"); + if (rowSet.rowCount() == 0) { + return new OrderAuditEventCollection().withTotalItems(0); + } OrderAuditEventCollection orderAuditEventCollection = new OrderAuditEventCollection(); rowSet.iterator().forEachRemaining(row -> { orderAuditEventCollection.getOrderAuditEvents().add(mapRowToOrderEvent(row)); 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 8909874b..44a088a2 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 @@ -81,8 +81,7 @@ public Future getAuditEventsByOrderLineId(String } LOGGER.info("getAuditEventsByOrderLineId:: Retrieved AuditEvent with order line id : {}", orderLineId); - return promise.future().map(rowSet -> rowSet.rowCount() == 0 ? new OrderLineAuditEventCollection().withTotalItems(0) - : mapRowToListOfOrderLineEvent(rowSet)); + return promise.future().map(this::mapRowToListOfOrderLineEvent); } private void makeSaveCall(Promise> promise, String query, OrderLineAuditEvent orderLineAuditEvent, String tenantId) { @@ -105,6 +104,9 @@ private void makeSaveCall(Promise> promise, String query, OrderLineA private OrderLineAuditEventCollection mapRowToListOfOrderLineEvent(RowSet rowSet) { LOGGER.debug("mapRowToListOfOrderLineEvent:: Mapping row to List of Order Line Events"); + if (rowSet.rowCount() == 0) { + return new OrderLineAuditEventCollection().withTotalItems(0); + } OrderLineAuditEventCollection orderLineAuditEventCollection = new OrderLineAuditEventCollection(); rowSet.iterator().forEachRemaining(row -> { orderLineAuditEventCollection.getOrderLineAuditEvents().add(mapRowToOrderLineEvent(row)); diff --git a/mod-audit-server/src/main/java/org/folio/dao/acquisition/impl/OrganizationEventsDaoImpl.java b/mod-audit-server/src/main/java/org/folio/dao/acquisition/impl/OrganizationEventsDaoImpl.java new file mode 100644 index 00000000..1c173902 --- /dev/null +++ b/mod-audit-server/src/main/java/org/folio/dao/acquisition/impl/OrganizationEventsDaoImpl.java @@ -0,0 +1,113 @@ +package org.folio.dao.acquisition.impl; + +import io.vertx.core.Future; +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.OrganizationEventsDao; +import org.folio.rest.jaxrs.model.OrganizationAuditEvent; +import org.folio.rest.jaxrs.model.OrganizationAuditEventCollection; +import org.folio.util.PostgresClientFactory; +import org.springframework.stereotype.Repository; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.util.Date; +import java.util.UUID; + +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.ORGANIZATION_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; + +@Repository +public class OrganizationEventsDaoImpl implements OrganizationEventsDao { + + private static final Logger LOGGER = LogManager.getLogger(); + + public static final String TABLE_NAME = "acquisition_organization_log"; + + public static final String GET_BY_INVOICE_ID_SQL = "SELECT id, action, organization_id, user_id, event_date, action_date, modified_content_snapshot," + + " (SELECT count(*) AS total_records FROM %s WHERE organization_id = $1) FROM %s WHERE organization_id = $1 %s LIMIT $2 OFFSET $3"; + + public static final String INSERT_SQL = "INSERT INTO %s (id, action, organization_id, user_id, event_date, action_date, modified_content_snapshot)" + + " VALUES ($1, $2, $3, $4, $5, $6, $7)"; + + private final PostgresClientFactory pgClientFactory; + + public OrganizationEventsDaoImpl(PostgresClientFactory pgClientFactory) { + this.pgClientFactory = pgClientFactory; + } + + @Override + public Future> save(OrganizationAuditEvent organizationAuditEvent, String tenantId) { + LOGGER.debug("save:: Saving Organization AuditEvent with tenant id : {}", tenantId); + String logTable = formatDBTableName(tenantId, TABLE_NAME); + String query = format(INSERT_SQL, logTable); + return makeSaveCall(query, organizationAuditEvent, tenantId) + .onSuccess(rows -> LOGGER.info("save:: Saved Organization AuditEvent with tenant id : {}", tenantId)) + .onFailure(e -> LOGGER.error("Failed to save record with id: {} for organization id: {} in to table {}", + organizationAuditEvent.getId(), organizationAuditEvent.getOrganizationId(), TABLE_NAME, e)); + } + + @Override + public Future getAuditEventsByOrganizationId(String organizationId, String sortBy, String sortOrder, int limit, int offset, String tenantId) { + LOGGER.debug("getAuditEventsByOrganizationId:: Retrieving AuditEvent with organization id : {}", organizationId); + String logTable = formatDBTableName(tenantId, TABLE_NAME); + String query = format(GET_BY_INVOICE_ID_SQL, logTable, logTable, format(ORDER_BY_PATTERN, sortBy, sortOrder)); + return pgClientFactory.createInstance(tenantId).execute(query, Tuple.of(UUID.fromString(organizationId), limit, offset)) + .map(this::mapRowToListOfOrganizationEvent); + } + + private Future> makeSaveCall(String query, OrganizationAuditEvent organizationAuditEvent, String tenantId) { + LOGGER.debug("makeSaveCall:: Making save call with query : {} and tenant id : {}", query, tenantId); + try { + return pgClientFactory.createInstance(tenantId).execute(query, Tuple.of(organizationAuditEvent.getId(), + organizationAuditEvent.getAction(), + organizationAuditEvent.getOrganizationId(), + organizationAuditEvent.getUserId(), + LocalDateTime.ofInstant(organizationAuditEvent.getEventDate().toInstant(), ZoneId.systemDefault()), + LocalDateTime.ofInstant(organizationAuditEvent.getActionDate().toInstant(), ZoneId.systemDefault()), + JsonObject.mapFrom(organizationAuditEvent.getOrganizationSnapshot()))); + } catch (Exception e) { + return Future.failedFuture(e); + } + } + + private OrganizationAuditEventCollection mapRowToListOfOrganizationEvent(RowSet rowSet) { + LOGGER.debug("mapRowToListOfOrganizationEvent:: Mapping row to List of Organization Events"); + if (rowSet.rowCount() == 0) { + return new OrganizationAuditEventCollection().withTotalItems(0); + } + OrganizationAuditEventCollection organizationAuditEventCollection = new OrganizationAuditEventCollection(); + rowSet.iterator().forEachRemaining(row -> { + organizationAuditEventCollection.getOrganizationAuditEvents().add(mapRowToOrganizationEvent(row)); + organizationAuditEventCollection.setTotalItems(row.getInteger(TOTAL_RECORDS_FIELD)); + }); + LOGGER.debug("mapRowToListOfOrganizationEvent:: Mapped row to List of Organization Events"); + return organizationAuditEventCollection; + } + + private OrganizationAuditEvent mapRowToOrganizationEvent(Row row) { + LOGGER.debug("mapRowToOrganizationEvent:: Mapping row to Organization Event"); + return new OrganizationAuditEvent() + .withId(row.getValue(ID_FIELD).toString()) + .withAction(row.get(OrganizationAuditEvent.Action.class, ACTION_FIELD)) + .withOrganizationId(row.getValue(ORGANIZATION_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))) + .withOrganizationSnapshot(JsonObject.mapFrom(row.getValue(MODIFIED_CONTENT_FIELD))); + } +} 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 index 946bfac6..85395aed 100644 --- 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 @@ -94,9 +94,7 @@ public Future getAuditEventsByPieceId(String pieceId, 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)); + return promise.future().map(this::mapRowToListOfPieceEvent); } @Override @@ -114,12 +112,14 @@ public Future getAuditEventsWithStatusChangesByPieceI 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)); + return promise.future().map(this::mapRowToListOfPieceEvent); } private PieceAuditEventCollection mapRowToListOfPieceEvent(RowSet rowSet) { LOGGER.debug("mapRowToListOfOrderEvent:: Mapping row to List of Piece Events"); + if (rowSet.rowCount() == 0) { + return new PieceAuditEventCollection().withTotalItems(0); + } PieceAuditEventCollection pieceAuditEventCollection = new PieceAuditEventCollection(); rowSet.iterator().forEachRemaining(row -> { pieceAuditEventCollection.getPieceAuditEvents().add(mapRowToPieceEvent(row)); 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 c42c9dd1..ce02a44f 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 @@ -11,6 +11,7 @@ import org.folio.rest.jaxrs.model.AuditDataAcquisitionInvoiceLineIdGetSortOrder; import org.folio.rest.jaxrs.model.AuditDataAcquisitionOrderIdGetSortOrder; import org.folio.rest.jaxrs.model.AuditDataAcquisitionOrderLineIdGetSortOrder; +import org.folio.rest.jaxrs.model.AuditDataAcquisitionOrganizationIdGetSortOrder; import org.folio.rest.jaxrs.model.AuditDataAcquisitionPieceIdGetSortOrder; import org.folio.rest.jaxrs.model.AuditDataAcquisitionPieceIdStatusChangeHistoryGetSortOrder; import org.folio.rest.jaxrs.resource.AuditDataAcquisition; @@ -19,6 +20,7 @@ import org.folio.services.acquisition.InvoiceLineAuditEventsService; import org.folio.services.acquisition.OrderAuditEventsService; import org.folio.services.acquisition.OrderLineAuditEventsService; +import org.folio.services.acquisition.OrganizationAuditEventsService; import org.folio.services.acquisition.PieceAuditEventsService; import org.folio.spring.SpringContextUtil; import org.folio.util.ErrorUtils; @@ -43,6 +45,8 @@ public class AuditDataAcquisitionImpl implements AuditDataAcquisition { private InvoiceAuditEventsService invoiceAuditEventsService; @Autowired private InvoiceLineAuditEventsService invoiceLineAuditEventsService; + @Autowired + private OrganizationAuditEventsService organizationAuditEventsService; public AuditDataAcquisitionImpl() { SpringContextUtil.autowireDependencies(this, Vertx.currentContext()); @@ -131,7 +135,7 @@ public void getAuditDataAcquisitionInvoiceById(String invoiceId, String sortBy, .otherwise(this::mapExceptionToResponse) .onComplete(asyncResultHandler); } catch (Exception e) { - LOGGER.error("Failed to get invoice audit events by piece id: {}", invoiceId, e); + LOGGER.error("Failed to get invoice audit events by invoice id: {}", invoiceId, e); asyncResultHandler.handle(Future.succeededFuture(mapExceptionToResponse(e))); } } @@ -148,7 +152,25 @@ public void getAuditDataAcquisitionInvoiceLineById(String invoiceLineId, String .otherwise(this::mapExceptionToResponse) .onComplete(asyncResultHandler); } catch (Exception e) { - LOGGER.error("Failed to get invoice line audit events by order line id: {}", invoiceLineId, e); + LOGGER.error("Failed to get invoice line audit events by invoice line id: {}", invoiceLineId, e); + asyncResultHandler.handle(Future.succeededFuture(mapExceptionToResponse(e))); + } + } + + @Override + public void getAuditDataAcquisitionOrganizationById(String organizationId, String sortBy, + AuditDataAcquisitionOrganizationIdGetSortOrder sortOrder, + int limit, int offset, Map okapiHeaders, Handler> asyncResultHandler, Context vertxContext) { + LOGGER.debug("getAuditDataAcquisitionOrganizationById:: Retrieving Audit Data Acquisition Organization By Id : {}", organizationId); + String tenantId = TenantTool.tenantId(okapiHeaders); + try { + organizationAuditEventsService.getAuditEventsByOrganizationId(organizationId, sortBy, sortOrder.name(), limit, offset, tenantId) + .map(GetAuditDataAcquisitionOrganizationByIdResponse::respond200WithApplicationJson) + .map(Response.class::cast) + .otherwise(this::mapExceptionToResponse) + .onComplete(asyncResultHandler); + } catch (Exception e) { + LOGGER.error("Failed to get organization audit events by organization id: {}", organizationId, e); asyncResultHandler.handle(Future.succeededFuture(mapExceptionToResponse(e))); } } 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 9e60c8dd..16d1099d 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 @@ -20,6 +20,7 @@ import org.folio.verticle.acquisition.InvoiceLineEventConsumersVerticle; import org.folio.verticle.acquisition.OrderEventConsumersVerticle; import org.folio.verticle.acquisition.OrderLineEventConsumersVerticle; +import org.folio.verticle.acquisition.OrganizationEventConsumersVerticle; import org.folio.verticle.acquisition.PieceEventConsumersVerticle; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.support.AbstractApplicationContext; @@ -55,6 +56,11 @@ public class InitAPIs implements InitAPI { @Value("${acq.invoice-lines.kafka.consumer.pool.size:5}") private int acqInvoiceLineConsumerPoolSize; + @Value("${acq.organizations.kafka.consumer.instancesNumber:1}") + private int acqOrganizationConsumerInstancesNumber; + @Value("${acq.organizations.kafka.consumer.pool.size:5}") + private int acqOrganizationConsumerPoolSize; + @Override public void init(Vertx vertx, Context context, Handler> handler) { LOGGER.debug("init:: InitAPI starting..."); @@ -87,12 +93,14 @@ private Future deployConsumersVerticles(Vertx vertx) { Promise pieceEventsConsumer = Promise.promise(); Promise invoiceEventsConsumer = Promise.promise(); Promise invoiceLineEventsConsumer = Promise.promise(); + Promise organizationsEventsConsumer = Promise.promise(); deployVerticle(vertx, verticleFactory, OrderEventConsumersVerticle.class, acqOrderConsumerInstancesNumber, acqOrderConsumerPoolSize, orderEventsConsumer); deployVerticle(vertx, verticleFactory, OrderLineEventConsumersVerticle.class, acqOrderLineConsumerInstancesNumber, acqOrderLineConsumerPoolSize, orderLineEventsConsumer); deployVerticle(vertx, verticleFactory, PieceEventConsumersVerticle.class, acqPieceConsumerInstancesNumber, acqPieceConsumerPoolSize, pieceEventsConsumer); deployVerticle(vertx, verticleFactory, InvoiceEventConsumersVerticle.class, acqInvoiceConsumerInstancesNumber, acqInvoiceConsumerPoolSize, invoiceEventsConsumer); deployVerticle(vertx, verticleFactory, InvoiceLineEventConsumersVerticle.class, acqInvoiceLineConsumerInstancesNumber, acqInvoiceLineConsumerPoolSize, invoiceLineEventsConsumer); + deployVerticle(vertx, verticleFactory, OrganizationEventConsumersVerticle.class, acqOrganizationConsumerInstancesNumber, acqOrganizationConsumerPoolSize, organizationsEventsConsumer); LOGGER.info("deployConsumersVerticles:: All consumer verticles were successfully deployed"); return GenericCompositeFuture.all(Arrays.asList( @@ -100,7 +108,8 @@ private Future deployConsumersVerticles(Vertx vertx) { orderLineEventsConsumer.future(), pieceEventsConsumer.future(), invoiceEventsConsumer.future(), - invoiceLineEventsConsumer.future())); + invoiceLineEventsConsumer.future(), + organizationsEventsConsumer.future())); } private void deployVerticle(Vertx vertx, VerticleFactory verticleFactory, Class consumerClass, diff --git a/mod-audit-server/src/main/java/org/folio/services/acquisition/InvoiceAuditEventsService.java b/mod-audit-server/src/main/java/org/folio/services/acquisition/InvoiceAuditEventsService.java index 782480fd..e271de7d 100644 --- a/mod-audit-server/src/main/java/org/folio/services/acquisition/InvoiceAuditEventsService.java +++ b/mod-audit-server/src/main/java/org/folio/services/acquisition/InvoiceAuditEventsService.java @@ -22,10 +22,10 @@ public interface InvoiceAuditEventsService { * * @param invoiceId invoice id * @param sortBy sort by - * @param sortInvoice sort invoice + * @param sortOrder sort order * @param limit limit * @param offset offset * @return future with InvoiceAuditEventCollection */ - Future getAuditEventsByInvoiceId(String invoiceId, String sortBy, String sortInvoice, int limit, int offset, String tenantId); + Future getAuditEventsByInvoiceId(String invoiceId, String sortBy, String sortOrder, int limit, int offset, String tenantId); } diff --git a/mod-audit-server/src/main/java/org/folio/services/acquisition/OrganizationAuditEventsService.java b/mod-audit-server/src/main/java/org/folio/services/acquisition/OrganizationAuditEventsService.java new file mode 100644 index 00000000..c6154ff4 --- /dev/null +++ b/mod-audit-server/src/main/java/org/folio/services/acquisition/OrganizationAuditEventsService.java @@ -0,0 +1,31 @@ +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.OrganizationAuditEvent; +import org.folio.rest.jaxrs.model.OrganizationAuditEventCollection; + +public interface OrganizationAuditEventsService { + + /** + * Saves OrganizationAuditEvent + * + * @param organizationAuditEvent + * @param tenantId id of tenant + * @return successful future if event has not been processed, or failed future otherwise + */ + Future> saveOrganizationAuditEvent(OrganizationAuditEvent organizationAuditEvent, String tenantId); + + /** + * Searches for organization audit events by organization id + * + * @param organizationId organization id + * @param sortBy sort by + * @param sortOrder sort order + * @param limit limit + * @param offset offset + * @return future with OrganizationAuditEventCollection + */ + Future getAuditEventsByOrganizationId(String organizationId, String sortBy, String sortOrder, int limit, int offset, String tenantId); +} diff --git a/mod-audit-server/src/main/java/org/folio/services/acquisition/impl/OrganizationAuditEventsServiceImpl.java b/mod-audit-server/src/main/java/org/folio/services/acquisition/impl/OrganizationAuditEventsServiceImpl.java new file mode 100644 index 00000000..2404f6b0 --- /dev/null +++ b/mod-audit-server/src/main/java/org/folio/services/acquisition/impl/OrganizationAuditEventsServiceImpl.java @@ -0,0 +1,42 @@ +package org.folio.services.acquisition.impl; + +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.OrganizationEventsDao; +import org.folio.rest.jaxrs.model.OrganizationAuditEvent; +import org.folio.rest.jaxrs.model.OrganizationAuditEventCollection; +import org.folio.services.acquisition.OrganizationAuditEventsService; +import org.springframework.stereotype.Service; + +import static org.folio.util.ErrorUtils.handleFailures; + +@Service +public class OrganizationAuditEventsServiceImpl implements OrganizationAuditEventsService { + + private static final Logger LOGGER = LogManager.getLogger(); + + private final OrganizationEventsDao organizationEventsDao; + + public OrganizationAuditEventsServiceImpl(OrganizationEventsDao organizationEvenDao) { + this.organizationEventsDao = organizationEvenDao; + } + + @Override + public Future> saveOrganizationAuditEvent(OrganizationAuditEvent organizationAuditEvent, String tenantId) { + LOGGER.debug("saveOrganizationAuditEvent:: Saving organization audit event with organizationId={} for tenantId={}", organizationAuditEvent.getOrganizationId(), tenantId); + return organizationEventsDao.save(organizationAuditEvent, tenantId) + .recover(throwable -> { + LOGGER.error("handleFailures:: Could not save organization audit event for Organization id: {} in tenantId: {}", organizationAuditEvent.getOrganizationId(), tenantId); + return handleFailures(throwable, organizationAuditEvent.getId()); + }); + } + + @Override + public Future getAuditEventsByOrganizationId(String organizationId, String sortBy, String sortOrder, int limit, int offset, String tenantId) { + LOGGER.debug("getAuditEventsByOrganizationId:: Retrieving audit events for organizationId={} and tenantId={}", organizationId, tenantId); + return organizationEventsDao.getAuditEventsByOrganizationId(organizationId, 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 f30acdf4..d8150206 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 @@ -5,7 +5,8 @@ public enum AcquisitionEventType { ACQ_ORDER_LINE_CHANGED("ACQ_ORDER_LINE_CHANGED"), ACQ_PIECE_CHANGED("ACQ_PIECE_CHANGED"), ACQ_INVOICE_CHANGED("ACQ_INVOICE_CHANGED"), - ACQ_INVOICE_LINE_CHANGED("ACQ_INVOICE_LINE_CHANGED"); + ACQ_INVOICE_LINE_CHANGED("ACQ_INVOICE_LINE_CHANGED"), + ACQ_ORGANIZATION_CHANGED("ACQ_ORGANIZATION_CHANGED"); private final String topicName; diff --git a/mod-audit-server/src/main/java/org/folio/util/AuditEventDBConstants.java b/mod-audit-server/src/main/java/org/folio/util/AuditEventDBConstants.java index 27f0cbca..7f3fc3d0 100644 --- a/mod-audit-server/src/main/java/org/folio/util/AuditEventDBConstants.java +++ b/mod-audit-server/src/main/java/org/folio/util/AuditEventDBConstants.java @@ -18,6 +18,8 @@ private AuditEventDBConstants() {} public static final String INVOICE_LINE_ID_FIELD = "invoice_line_id"; + public static final String ORGANIZATION_ID_FIELD = "organization_id"; + public static final String USER_ID_FIELD = "user_id"; public static final String EVENT_DATE_FIELD = "event_date"; diff --git a/mod-audit-server/src/main/java/org/folio/verticle/acquisition/OrderEventConsumersVerticle.java b/mod-audit-server/src/main/java/org/folio/verticle/acquisition/OrderEventConsumersVerticle.java index 63e4694d..64c082bd 100644 --- a/mod-audit-server/src/main/java/org/folio/verticle/acquisition/OrderEventConsumersVerticle.java +++ b/mod-audit-server/src/main/java/org/folio/verticle/acquisition/OrderEventConsumersVerticle.java @@ -1,7 +1,6 @@ package org.folio.verticle.acquisition; import org.folio.kafka.AsyncRecordHandler; -import org.folio.kafka.KafkaConfig; import org.folio.util.AcquisitionEventType; import org.folio.verticle.AbstractConsumersVerticle; import org.springframework.beans.factory.annotation.Autowired; @@ -11,15 +10,16 @@ @Component public class OrderEventConsumersVerticle extends AbstractConsumersVerticle { - @Autowired - private KafkaConfig kafkaConfig; + @Autowired private AsyncRecordHandler orderEventsHandler; + @Override public List getEvents() { return List.of(AcquisitionEventType.ACQ_ORDER_CHANGED.getTopicName()); } + @Override public AsyncRecordHandler getHandler() { return orderEventsHandler; } diff --git a/mod-audit-server/src/main/java/org/folio/verticle/acquisition/OrderLineEventConsumersVerticle.java b/mod-audit-server/src/main/java/org/folio/verticle/acquisition/OrderLineEventConsumersVerticle.java index 1cbc45c3..ac7d4e3b 100644 --- a/mod-audit-server/src/main/java/org/folio/verticle/acquisition/OrderLineEventConsumersVerticle.java +++ b/mod-audit-server/src/main/java/org/folio/verticle/acquisition/OrderLineEventConsumersVerticle.java @@ -1,7 +1,6 @@ package org.folio.verticle.acquisition; import org.folio.kafka.AsyncRecordHandler; -import org.folio.kafka.KafkaConfig; import org.folio.util.AcquisitionEventType; import org.folio.verticle.AbstractConsumersVerticle; import org.springframework.beans.factory.annotation.Autowired; @@ -11,8 +10,7 @@ @Component public class OrderLineEventConsumersVerticle extends AbstractConsumersVerticle { - @Autowired - private KafkaConfig kafkaConfig; + @Autowired private AsyncRecordHandler orderLineEventsHandler; diff --git a/mod-audit-server/src/main/java/org/folio/verticle/acquisition/OrganizationEventConsumersVerticle.java b/mod-audit-server/src/main/java/org/folio/verticle/acquisition/OrganizationEventConsumersVerticle.java new file mode 100644 index 00000000..28bf6afd --- /dev/null +++ b/mod-audit-server/src/main/java/org/folio/verticle/acquisition/OrganizationEventConsumersVerticle.java @@ -0,0 +1,26 @@ +package org.folio.verticle.acquisition; + +import org.folio.kafka.AsyncRecordHandler; +import org.folio.util.AcquisitionEventType; +import org.folio.verticle.AbstractConsumersVerticle; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +public class OrganizationEventConsumersVerticle extends AbstractConsumersVerticle { + + @Autowired + private AsyncRecordHandler organizationEventsHandler; + + @Override + public List getEvents() { + return List.of(AcquisitionEventType.ACQ_ORGANIZATION_CHANGED.getTopicName()); + } + + @Override + public AsyncRecordHandler getHandler() { + return organizationEventsHandler; + } +} diff --git a/mod-audit-server/src/main/java/org/folio/verticle/acquisition/consumers/InvoiceEventsHandler.java b/mod-audit-server/src/main/java/org/folio/verticle/acquisition/consumers/InvoiceEventsHandler.java index e24ffdfc..145aca99 100644 --- a/mod-audit-server/src/main/java/org/folio/verticle/acquisition/consumers/InvoiceEventsHandler.java +++ b/mod-audit-server/src/main/java/org/folio/verticle/acquisition/consumers/InvoiceEventsHandler.java @@ -1,11 +1,9 @@ package org.folio.verticle.acquisition.consumers; 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; @@ -14,11 +12,8 @@ import org.folio.rest.jaxrs.model.InvoiceAuditEvent; import org.folio.rest.util.OkapiConnectionParams; import org.folio.services.acquisition.InvoiceAuditEventsService; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; -import java.util.List; - @Component public class InvoiceEventsHandler implements AsyncRecordHandler { @@ -27,35 +22,28 @@ public class InvoiceEventsHandler implements AsyncRecordHandler private final InvoiceAuditEventsService invoiceAuditEventsService; private final Vertx vertx; - public InvoiceEventsHandler(@Autowired Vertx vertx, - @Autowired InvoiceAuditEventsService invoiceAuditEventsService) { + public InvoiceEventsHandler(Vertx vertx, + InvoiceAuditEventsService invoiceAuditEventsService) { this.vertx = vertx; this.invoiceAuditEventsService = invoiceAuditEventsService; } @Override public Future handle(KafkaConsumerRecord kafkaConsumerRecord) { - Promise result = Promise.promise(); - List kafkaHeaders = kafkaConsumerRecord.headers(); - OkapiConnectionParams okapiConnectionParams = new OkapiConnectionParams(KafkaHeaderUtils.kafkaHeadersToMap(kafkaHeaders), vertx); - InvoiceAuditEvent event = new JsonObject(kafkaConsumerRecord.value()).mapTo(InvoiceAuditEvent.class); + var kafkaHeaders = kafkaConsumerRecord.headers(); + var okapiConnectionParams = new OkapiConnectionParams(KafkaHeaderUtils.kafkaHeadersToMap(kafkaHeaders), vertx); + var event = new JsonObject(kafkaConsumerRecord.value()).mapTo(InvoiceAuditEvent.class); LOGGER.info("handle:: Starting processing of Invoice audit event with id: {} for invoice id: {}", event.getId(), event.getInvoiceId()); - invoiceAuditEventsService.saveInvoiceAuditEvent(event, okapiConnectionParams.getTenantId()) - .onSuccess(ar -> { - LOGGER.info("handle:: Invoice audit event with id: {} has been processed for invoice id: {}", event.getId(), event.getInvoiceId()); - result.complete(event.getId()); - }) + return invoiceAuditEventsService.saveInvoiceAuditEvent(event, okapiConnectionParams.getTenantId()) + .onSuccess(ar -> LOGGER.info("handle:: Invoice audit event with id: {} has been processed for invoice id: {}", event.getId(), event.getInvoiceId())) .onFailure(e -> { if (e instanceof DuplicateEventException) { LOGGER.info("handle:: Duplicate Invoice audit event with id: {} for invoice id: {} received, skipped processing", event.getId(), event.getInvoiceId()); - result.complete(event.getId()); } else { LOGGER.error("Processing of Invoice audit event with id: {} for invoice id: {} has been failed", event.getId(), event.getInvoiceId(), e); - result.fail(e); } - }); - - return result.future(); + }) + .map(event.getId()); } } diff --git a/mod-audit-server/src/main/java/org/folio/verticle/acquisition/consumers/InvoiceLineEventsHandler.java b/mod-audit-server/src/main/java/org/folio/verticle/acquisition/consumers/InvoiceLineEventsHandler.java index 538ac0e4..cab6f33e 100644 --- a/mod-audit-server/src/main/java/org/folio/verticle/acquisition/consumers/InvoiceLineEventsHandler.java +++ b/mod-audit-server/src/main/java/org/folio/verticle/acquisition/consumers/InvoiceLineEventsHandler.java @@ -1,11 +1,9 @@ package org.folio.verticle.acquisition.consumers; 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; @@ -16,8 +14,6 @@ import org.folio.services.acquisition.InvoiceLineAuditEventsService; import org.springframework.stereotype.Component; -import java.util.List; - @Component public class InvoiceLineEventsHandler implements AsyncRecordHandler { @@ -34,29 +30,20 @@ public InvoiceLineEventsHandler(Vertx vertx, @Override public Future handle(KafkaConsumerRecord kafkaConsumerRecord) { - Promise result = Promise.promise(); - List kafkaHeaders = kafkaConsumerRecord.headers(); - OkapiConnectionParams okapiConnectionParams = new OkapiConnectionParams(KafkaHeaderUtils.kafkaHeadersToMap(kafkaHeaders), vertx); - InvoiceLineAuditEvent event = new JsonObject(kafkaConsumerRecord.value()).mapTo(InvoiceLineAuditEvent.class); - LOGGER.info("handle:: Starting processing of Invoice Line audit event with id: {} for invoice line id: {}", - event.getId(), event.getInvoiceLineId()); + var kafkaHeaders = kafkaConsumerRecord.headers(); + var okapiConnectionParams = new OkapiConnectionParams(KafkaHeaderUtils.kafkaHeadersToMap(kafkaHeaders), vertx); + var event = new JsonObject(kafkaConsumerRecord.value()).mapTo(InvoiceLineAuditEvent.class); + LOGGER.info("handle:: Starting processing of Invoice Line audit event with id: {} for invoice line id: {}", event.getId(), event.getInvoiceLineId()); - invoiceLineAuditEventsService.saveInvoiceLineAuditEvent(event, okapiConnectionParams.getTenantId()) - .onSuccess(ar -> { - LOGGER.info("handle:: Invoice Line audit event with id: {} has been processed for invoice line id: {}", - event.getId(), event.getInvoiceLineId()); - result.complete(event.getId()); - }) + return invoiceLineAuditEventsService.saveInvoiceLineAuditEvent(event, okapiConnectionParams.getTenantId()) + .onSuccess(ar -> LOGGER.info("handle:: Invoice Line audit event with id: {} has been processed for invoice line id: {}", event.getId(), event.getInvoiceLineId())) .onFailure(e -> { if (e instanceof DuplicateEventException) { LOGGER.info("handle:: Duplicate Invoice Line audit event with id: {} for invoice line id: {} received, skipped processing", event.getId(), event.getInvoiceLineId()); - result.complete(event.getId()); } else { LOGGER.error("Processing of Invoice Line audit event with id: {} for invoice line id: {} has been failed", event.getId(), event.getInvoiceLineId(), e); - result.fail(e); } - }); - - return result.future(); + }) + .map(event.getId()); } } diff --git a/mod-audit-server/src/main/java/org/folio/verticle/acquisition/consumers/OrderEventsHandler.java b/mod-audit-server/src/main/java/org/folio/verticle/acquisition/consumers/OrderEventsHandler.java index 8d1f5a9e..208a91bf 100644 --- a/mod-audit-server/src/main/java/org/folio/verticle/acquisition/consumers/OrderEventsHandler.java +++ b/mod-audit-server/src/main/java/org/folio/verticle/acquisition/consumers/OrderEventsHandler.java @@ -1,11 +1,9 @@ package org.folio.verticle.acquisition.consumers; 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; @@ -14,50 +12,38 @@ import org.folio.rest.jaxrs.model.OrderAuditEvent; import org.folio.rest.util.OkapiConnectionParams; import org.folio.services.acquisition.OrderAuditEventsService; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; -import java.util.List; - @Component public class OrderEventsHandler implements AsyncRecordHandler { private static final Logger LOGGER = LogManager.getLogger(); - private OrderAuditEventsService orderAuditEventsService; - - private Vertx vertx; + private final OrderAuditEventsService orderAuditEventsService; + private final Vertx vertx; - public OrderEventsHandler(@Autowired Vertx vertx, - @Autowired OrderAuditEventsService orderAuditEventsService) { + public OrderEventsHandler(Vertx vertx, + OrderAuditEventsService orderAuditEventsService) { this.vertx = vertx; this.orderAuditEventsService = orderAuditEventsService; } @Override - public Future handle(KafkaConsumerRecord record) { - - Promise result = Promise.promise(); - List kafkaHeaders = record.headers(); - OkapiConnectionParams okapiConnectionParams = new OkapiConnectionParams(KafkaHeaderUtils.kafkaHeadersToMap(kafkaHeaders), vertx); - OrderAuditEvent event = new JsonObject(record.value()).mapTo(OrderAuditEvent.class); + public Future handle(KafkaConsumerRecord kafkaConsumerRecord) { + var kafkaHeaders = kafkaConsumerRecord.headers(); + var okapiConnectionParams = new OkapiConnectionParams(KafkaHeaderUtils.kafkaHeadersToMap(kafkaHeaders), vertx); + var event = new JsonObject(kafkaConsumerRecord.value()).mapTo(OrderAuditEvent.class); LOGGER.info("handle:: Starting processing of Order audit event with id: {} for order id: {}", event.getId(), event.getOrderId()); - orderAuditEventsService.saveOrderAuditEvent(event, okapiConnectionParams.getTenantId()) - .onSuccess(ar -> { - LOGGER.info("handle:: Order audit event with id: {} has been processed for order id: {}", event.getId(), event.getOrderId()); - result.complete(event.getId()); - }) + return orderAuditEventsService.saveOrderAuditEvent(event, okapiConnectionParams.getTenantId()) + .onSuccess(ar -> LOGGER.info("handle:: Order audit event with id: {} has been processed for order id: {}", event.getId(), event.getOrderId())) .onFailure(e -> { if (e instanceof DuplicateEventException) { LOGGER.info("handle:: Duplicate Order audit event with id: {} for order id: {} received, skipped processing", event.getId(), event.getOrderId()); - result.complete(event.getId()); } else { LOGGER.error("Processing of Order audit event with id: {} for order id: {} has been failed", event.getId(), event.getOrderId(), e); - result.fail(e); } - }); - - return result.future(); + }) + .map(event.getId()); } } diff --git a/mod-audit-server/src/main/java/org/folio/verticle/acquisition/consumers/OrderLineEventsHandler.java b/mod-audit-server/src/main/java/org/folio/verticle/acquisition/consumers/OrderLineEventsHandler.java index d45c3618..28d98b2c 100644 --- a/mod-audit-server/src/main/java/org/folio/verticle/acquisition/consumers/OrderLineEventsHandler.java +++ b/mod-audit-server/src/main/java/org/folio/verticle/acquisition/consumers/OrderLineEventsHandler.java @@ -1,11 +1,9 @@ package org.folio.verticle.acquisition.consumers; 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; @@ -14,53 +12,38 @@ import org.folio.rest.jaxrs.model.OrderLineAuditEvent; import org.folio.rest.util.OkapiConnectionParams; import org.folio.services.acquisition.OrderLineAuditEventsService; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; -import java.util.List; - @Component public class OrderLineEventsHandler implements AsyncRecordHandler { private static final Logger LOGGER = LogManager.getLogger(); - private OrderLineAuditEventsService orderLineAuditEventsService; - - private Vertx vertx; + private final OrderLineAuditEventsService orderLineAuditEventsService; + private final Vertx vertx; - public OrderLineEventsHandler(@Autowired Vertx vertx, - @Autowired OrderLineAuditEventsService orderLineAuditEventsService) { + public OrderLineEventsHandler(Vertx vertx, + OrderLineAuditEventsService orderLineAuditEventsService) { this.vertx = vertx; this.orderLineAuditEventsService = orderLineAuditEventsService; } @Override - public Future handle(KafkaConsumerRecord record) { - Promise result = Promise.promise(); - List kafkaHeaders = record.headers(); - OkapiConnectionParams okapiConnectionParams = new OkapiConnectionParams(KafkaHeaderUtils.kafkaHeadersToMap(kafkaHeaders), vertx); - OrderLineAuditEvent event = new JsonObject(record.value()).mapTo(OrderLineAuditEvent.class); - LOGGER.info("handle:: Starting processing of Order Line audit event with id: {} for order id: {} and order line id: {}", - event.getId(), event.getOrderId(), event.getOrderLineId()); - - orderLineAuditEventsService.saveOrderLineAuditEvent(event, okapiConnectionParams.getTenantId()) - .onSuccess(ar -> { - LOGGER.info("handle:: Order Line audit event with id: {} has been processed for order id: {} and order line id: {}", - event.getId(), event.getOrderId(), event.getOrderLineId()); - result.complete(event.getId()); - }) + public Future handle(KafkaConsumerRecord kafkaConsumerRecord) { + var kafkaHeaders = kafkaConsumerRecord.headers(); + var okapiConnectionParams = new OkapiConnectionParams(KafkaHeaderUtils.kafkaHeadersToMap(kafkaHeaders), vertx); + var event = new JsonObject(kafkaConsumerRecord.value()).mapTo(OrderLineAuditEvent.class); + LOGGER.info("handle:: Starting processing of Order Line audit event with id: {} for order id: {} and order line id: {}", event.getId(), event.getOrderId(), event.getOrderLineId()); + + return orderLineAuditEventsService.saveOrderLineAuditEvent(event, okapiConnectionParams.getTenantId()) + .onSuccess(ar -> LOGGER.info("handle:: Order Line audit event with id: {} has been processed for order id: {} and order line id: {}", event.getId(), event.getOrderId(), event.getOrderLineId())) .onFailure(e -> { if (e instanceof DuplicateEventException) { - LOGGER.info("handle:: Duplicate Order Line audit event with id: {} for order id: {} and order line id: {} received, skipped processing", - event.getId(), event.getOrderId(), event.getOrderLineId()); - result.complete(event.getId()); + LOGGER.info("handle:: Duplicate Order Line audit event with id: {} for order id: {} and order line id: {} received, skipped processing", event.getId(), event.getOrderId(), event.getOrderLineId()); } else { - LOGGER.error("Processing of Order Line audit event with id: {} for order id: {} and order line id: {} has been failed", - event.getId(), event.getOrderId(), event.getOrderLineId(), e); - result.fail(e); + LOGGER.error("Processing of Order Line audit event with id: {} for order id: {} and order line id: {} has been failed", event.getId(), event.getOrderId(), event.getOrderLineId(), e); } - }); - - return result.future(); + }) + .map(event.getId()); } } diff --git a/mod-audit-server/src/main/java/org/folio/verticle/acquisition/consumers/OrganizationEventsHandler.java b/mod-audit-server/src/main/java/org/folio/verticle/acquisition/consumers/OrganizationEventsHandler.java new file mode 100644 index 00000000..807ef875 --- /dev/null +++ b/mod-audit-server/src/main/java/org/folio/verticle/acquisition/consumers/OrganizationEventsHandler.java @@ -0,0 +1,49 @@ +package org.folio.verticle.acquisition.consumers; + +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import io.vertx.core.json.JsonObject; +import io.vertx.kafka.client.consumer.KafkaConsumerRecord; +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.OrganizationAuditEvent; +import org.folio.rest.util.OkapiConnectionParams; +import org.folio.services.acquisition.OrganizationAuditEventsService; +import org.springframework.stereotype.Component; + +@Component +public class OrganizationEventsHandler implements AsyncRecordHandler { + + private static final Logger LOGGER = LogManager.getLogger(); + + private final OrganizationAuditEventsService organizationAuditEventsService; + private final Vertx vertx; + + public OrganizationEventsHandler(Vertx vertx, + OrganizationAuditEventsService organizationAuditEventsService) { + this.vertx = vertx; + this.organizationAuditEventsService = organizationAuditEventsService; + } + + @Override + public Future handle(KafkaConsumerRecord kafkaConsumerRecord) { + var kafkaHeaders = kafkaConsumerRecord.headers(); + var okapiConnectionParams = new OkapiConnectionParams(KafkaHeaderUtils.kafkaHeadersToMap(kafkaHeaders), vertx); + var event = new JsonObject(kafkaConsumerRecord.value()).mapTo(OrganizationAuditEvent.class); + LOGGER.info("handle:: Starting processing of Organization audit event with id: {} for organization id: {}", event.getId(), event.getOrganizationId()); + + return organizationAuditEventsService.saveOrganizationAuditEvent(event, okapiConnectionParams.getTenantId()) + .onSuccess(ar -> LOGGER.info("handle:: Organization audit event with id: {} has been processed for organization id: {}", event.getId(), event.getOrganizationId())) + .onFailure(e -> { + if (e instanceof DuplicateEventException) { + LOGGER.info("handle:: Duplicate Organization audit event with id: {} for organization id: {} received, skipped processing", event.getId(), event.getOrganizationId()); + } else { + LOGGER.error("Processing of Organization audit event with id: {} for organization id: {} has been failed", event.getId(), event.getOrganizationId(), e); + } + }) + .map(event.getId()); + } +} 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 index 90a4e098..3f08f3b5 100644 --- 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 @@ -1,13 +1,9 @@ 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; @@ -28,33 +24,26 @@ public class PieceEventsHandler implements AsyncRecordHandler { public PieceEventsHandler(Vertx vertx, PieceAuditEventsService pieceAuditEventsService) { - this.pieceAuditEventsService = pieceAuditEventsService; this.vertx = vertx; + this.pieceAuditEventsService = pieceAuditEventsService; } @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); + var kafkaHeaders = kafkaConsumerRecord.headers(); + var okapiConnectionParams = new OkapiConnectionParams(KafkaHeaderUtils.kafkaHeadersToMap(kafkaHeaders), vertx); + var 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()); - }) + return 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())) .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(); + }) + .map(event.getId()); } } diff --git a/mod-audit-server/src/main/resources/templates/db_scripts/acquisition/create_acquisition_organization_log_table.sql b/mod-audit-server/src/main/resources/templates/db_scripts/acquisition/create_acquisition_organization_log_table.sql new file mode 100644 index 00000000..791766f2 --- /dev/null +++ b/mod-audit-server/src/main/resources/templates/db_scripts/acquisition/create_acquisition_organization_log_table.sql @@ -0,0 +1,11 @@ +CREATE TABLE IF NOT EXISTS acquisition_organization_log ( + id uuid PRIMARY KEY, + action text NOT NULL, + organization_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 organization_id_index ON acquisition_organization_log USING BTREE (organization_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 4254e723..7cfe0f9a 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 @@ -89,6 +89,11 @@ "run": "after", "snippetPath": "acquisition/create_acquisition_invoice_line_log_table.sql", "fromModuleVersion": "mod-audit-3.0.0" + }, + { + "run": "after", + "snippetPath": "acquisition/create_acquisition_organization_log_table.sql", + "fromModuleVersion": "mod-audit-3.0.0" } ] } diff --git a/mod-audit-server/src/test/java/org/folio/dao/OrganizationEventsDaoTest.java b/mod-audit-server/src/test/java/org/folio/dao/OrganizationEventsDaoTest.java new file mode 100644 index 00000000..72f2204b --- /dev/null +++ b/mod-audit-server/src/test/java/org/folio/dao/OrganizationEventsDaoTest.java @@ -0,0 +1,80 @@ +package org.folio.dao; + +import io.vertx.core.Vertx; +import io.vertx.pgclient.PgException; +import org.folio.CopilotGenerated; +import org.folio.dao.acquisition.impl.OrganizationEventsDaoImpl; +import org.folio.rest.jaxrs.model.OrganizationAuditEvent; +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 java.util.UUID; + +import static org.folio.utils.EntityUtils.TENANT_ID; +import static org.folio.utils.EntityUtils.createOrganizationAuditEvent; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +@CopilotGenerated +public class OrganizationEventsDaoTest { + + @Spy + private PostgresClientFactory postgresClientFactory = new PostgresClientFactory(Vertx.vertx()); + @InjectMocks + OrganizationEventsDaoImpl organizationEventDao; + + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + organizationEventDao = new OrganizationEventsDaoImpl(postgresClientFactory); + } + + @Test + void shouldCreateEventProcessed() { + var organizationAuditEvent = createOrganizationAuditEvent(UUID.randomUUID().toString()); + + var saveFuture = organizationEventDao.save(organizationAuditEvent, TENANT_ID); + saveFuture.onComplete(ar -> assertTrue(ar.succeeded())); + verify(postgresClientFactory, times(1)).createInstance(TENANT_ID); + } + + @Test + void shouldThrowConstraintViolation() { + var organizationAuditEvent = createOrganizationAuditEvent(UUID.randomUUID().toString()); + + var saveFuture = organizationEventDao.save(organizationAuditEvent, TENANT_ID); + saveFuture.onComplete(ar -> { + var reSaveFuture = organizationEventDao.save(organizationAuditEvent, TENANT_ID); + reSaveFuture.onComplete(re -> { + assertTrue(re.failed()); + assertTrue(re.cause() instanceof PgException); + assertEquals("ERROR: duplicate key value violates unique constraint \"acquisition_organization_log_pkey\" (23505)", re.cause().getMessage()); + }); + }); + verify(postgresClientFactory, times(1)).createInstance(TENANT_ID); + } + + @Test + void shouldGetCreatedEvent() { + var id = UUID.randomUUID().toString(); + var organizationAuditEvent = createOrganizationAuditEvent(id); + + organizationEventDao.save(organizationAuditEvent, TENANT_ID); + + var dto = organizationEventDao.getAuditEventsByOrganizationId(id, "action_date", "desc", 1, 1, TENANT_ID); + dto.onComplete(ar -> { + var organizationAuditEventOptional = ar.result(); + var organizationAuditEventList = organizationAuditEventOptional.getOrganizationAuditEvents(); + + assertEquals(organizationAuditEventList.get(0).getId(), id); + assertEquals(OrganizationAuditEvent.Action.CREATE.value(), organizationAuditEventList.get(0).getAction().value()); + }); + verify(postgresClientFactory, times(2)).createInstance(TENANT_ID); + } +} 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 c14a4199..940bb813 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 @@ -3,6 +3,7 @@ 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.ORGANIZATION_ID; import static org.folio.utils.EntityUtils.PIECE_ID; import static org.folio.utils.EntityUtils.INVOICE_ID; import static org.folio.utils.EntityUtils.createPieceAuditEvent; @@ -21,11 +22,13 @@ import org.folio.dao.acquisition.impl.InvoiceLineEventsDaoImpl; import org.folio.dao.acquisition.impl.OrderEventsDaoImpl; import org.folio.dao.acquisition.impl.OrderLineEventsDaoImpl; +import org.folio.dao.acquisition.impl.OrganizationEventsDaoImpl; import org.folio.dao.acquisition.impl.PieceEventsDaoImpl; import org.folio.rest.jaxrs.model.InvoiceAuditEvent; import org.folio.rest.jaxrs.model.InvoiceLineAuditEvent; import org.folio.rest.jaxrs.model.OrderAuditEvent; import org.folio.rest.jaxrs.model.OrderLineAuditEvent; +import org.folio.rest.jaxrs.model.OrganizationAuditEvent; import org.folio.rest.jaxrs.model.PieceAuditEvent; import org.folio.util.PostgresClientFactory; import org.junit.jupiter.api.BeforeEach; @@ -46,6 +49,7 @@ public class AuditDataAcquisitionAPITest extends ApiTestBase { private static final String ACQ_AUDIT_PIECE_STATUS_CHANGE_HISTORY_PATH = "/status-change-history"; private static final String ACQ_AUDIT_INVOICE_PATH = "/audit-data/acquisition/invoice/"; private static final String ACQ_AUDIT_INVOICE_LINE_PATH = "/audit-data/acquisition/invoice-line/"; + private static final String ACQ_AUDIT_ORGANIZATION_PATH = "/audit-data/acquisition/organization/"; private static final String TENANT_ID = "modaudittest"; @Spy @@ -61,6 +65,8 @@ public class AuditDataAcquisitionAPITest extends ApiTestBase { InvoiceEventsDaoImpl invoiceEventsDao; @InjectMocks InvoiceLineEventsDaoImpl invoiceLineEventsDao; + @InjectMocks + OrganizationEventsDaoImpl organizationEventsDao; @BeforeEach public void setUp() { @@ -69,6 +75,7 @@ public void setUp() { orderLineEventDao = new OrderLineEventsDaoImpl(postgresClientFactory); invoiceEventsDao = new InvoiceEventsDaoImpl(postgresClientFactory); invoiceLineEventsDao = new InvoiceLineEventsDaoImpl(postgresClientFactory); + organizationEventsDao = new OrganizationEventsDaoImpl(postgresClientFactory); } @Test @@ -307,6 +314,7 @@ void shouldReturnInvoiceEventsOnGetByInvoiceId() { } @Test + @CopilotGenerated void shouldReturnInvoiceLineEventsOnGetByInvoiceLineId() { JsonObject jsonObject = new JsonObject(); jsonObject.put("name", "Test Product2"); @@ -343,4 +351,47 @@ void shouldReturnInvoiceLineEventsOnGetByInvoiceLineId() { .body(containsString("UUID string too large")); }); } + + @Test + @CopilotGenerated + void shouldReturnOrganizationEventsOnGetByOrganizationId() { + JsonObject jsonObject = new JsonObject(); + jsonObject.put("name", "Test Product2"); + + OrganizationAuditEvent organizationAuditEvent = new OrganizationAuditEvent() + .withId(UUID.randomUUID().toString()) + .withAction(OrganizationAuditEvent.Action.CREATE) + .withOrganizationId(ORGANIZATION_ID) + .withUserId(UUID.randomUUID().toString()) + .withEventDate(new Date()) + .withActionDate(new Date()) + .withOrganizationSnapshot(jsonObject); + + organizationEventsDao.save(organizationAuditEvent, TENANT_ID); + + given().header(CONTENT_TYPE).header(TENANT).header(PERMS) + .get(ACQ_AUDIT_ORGANIZATION_PATH + INVALID_ID) + .then().log().all().statusCode(200) + .body(containsString("organizationAuditEvents")).body(containsString("totalItems")); + + given().header(CONTENT_TYPE).header(TENANT).header(PERMS) + .get(ACQ_AUDIT_ORGANIZATION_PATH + ORGANIZATION_ID) + .then().log().all().statusCode(200) + .body(containsString(ORGANIZATION_ID)); + + given().header(CONTENT_TYPE).header(TENANT).header(PERMS) + .get(ACQ_AUDIT_ORGANIZATION_PATH + ORGANIZATION_ID + "?limit=1") + .then().log().all().statusCode(200) + .body(containsString(ORGANIZATION_ID)); + + given().header(CONTENT_TYPE).header(TENANT).header(PERMS) + .get(ACQ_AUDIT_ORGANIZATION_PATH + ORGANIZATION_ID + "?sortBy=action_date") + .then().log().all().statusCode(200) + .body(containsString(ORGANIZATION_ID)); + + given().header(CONTENT_TYPE).header(TENANT).header(PERMS) + .get(ACQ_AUDIT_ORGANIZATION_PATH + ORGANIZATION_ID + 123) + .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 31e321d9..4bf4733a 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 @@ -81,9 +81,9 @@ void shouldNotProcessEvent() { assertTrue(save.failed()); } - private KafkaConsumerRecord buildKafkaConsumerRecord(OrderAuditEvent record) { - String topic = KafkaTopicNameHelper.formatTopicName(KAFKA_ENV, getDefaultNameSpace(), TENANT_ID, record.getAction().toString()); - ConsumerRecord consumerRecord = buildConsumerRecord(topic, record); + private KafkaConsumerRecord buildKafkaConsumerRecord(OrderAuditEvent kafkaConsumerRecord) { + String topic = KafkaTopicNameHelper.formatTopicName(KAFKA_ENV, getDefaultNameSpace(), TENANT_ID, kafkaConsumerRecord.getAction().toString()); + ConsumerRecord consumerRecord = buildConsumerRecord(topic, kafkaConsumerRecord); return new KafkaConsumerRecordImpl<>(consumerRecord); } diff --git a/mod-audit-server/src/test/java/org/folio/rest/impl/OrganizationEventsHandlerMockTest.java b/mod-audit-server/src/test/java/org/folio/rest/impl/OrganizationEventsHandlerMockTest.java new file mode 100644 index 00000000..3d4b0e02 --- /dev/null +++ b/mod-audit-server/src/test/java/org/folio/rest/impl/OrganizationEventsHandlerMockTest.java @@ -0,0 +1,95 @@ +package org.folio.rest.impl; + +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.CopilotGenerated; +import org.folio.dao.acquisition.impl.OrganizationEventsDaoImpl; +import org.folio.kafka.KafkaTopicNameHelper; +import org.folio.rest.jaxrs.model.OrganizationAuditEvent; +import org.folio.rest.util.OkapiConnectionParams; +import org.folio.services.acquisition.OrganizationAuditEventsService; +import org.folio.services.acquisition.impl.OrganizationAuditEventsServiceImpl; +import org.folio.util.PostgresClientFactory; +import org.folio.verticle.acquisition.consumers.OrganizationEventsHandler; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.Spy; + +import java.nio.charset.StandardCharsets; +import java.util.UUID; + +import static org.folio.kafka.KafkaTopicNameHelper.getDefaultNameSpace; +import static org.folio.utils.EntityUtils.createOrganizationAuditEvent; +import static org.folio.utils.EntityUtils.createOrganizationAuditEventWithoutSnapshot; + +@CopilotGenerated(partiallyGenerated = true) +public class OrganizationEventsHandlerMockTest { + + private static final String TENANT_ID = "diku"; + protected static final String TOKEN = "token"; + private static final String KAFKA_ENV = "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 + OrganizationEventsDaoImpl organizationEventDao; + @Mock + OrganizationAuditEventsService organizationAuditEventServiceImpl; + + private OrganizationEventsHandler organizationEventsHandler; + + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + organizationEventDao = new OrganizationEventsDaoImpl(postgresClientFactory); + organizationAuditEventServiceImpl = new OrganizationAuditEventsServiceImpl(organizationEventDao); + organizationEventsHandler = new OrganizationEventsHandler(vertx, organizationAuditEventServiceImpl); + } + + @Test + void shouldProcessEvent() { + var organizationAuditEvent = createOrganizationAuditEvent(UUID.randomUUID().toString()); + KafkaConsumerRecord kafkaConsumerRecord = buildKafkaConsumerRecord(organizationAuditEvent); + + Future saveFuture = organizationEventsHandler.handle(kafkaConsumerRecord); + saveFuture.onComplete(ar -> Assertions.assertTrue(ar.succeeded())); + } + + @Test + void shouldNotProcessEvent() { + var organizationAuditEvent = createOrganizationAuditEventWithoutSnapshot(); + KafkaConsumerRecord kafkaConsumerRecord = buildKafkaConsumerRecord(organizationAuditEvent); + + Future save = organizationEventsHandler.handle(kafkaConsumerRecord); + Assertions.assertTrue(save.failed()); + } + + private KafkaConsumerRecord buildKafkaConsumerRecord(OrganizationAuditEvent kafkaConsumerRecord) { + String topic = KafkaTopicNameHelper.formatTopicName(KAFKA_ENV, getDefaultNameSpace(), TENANT_ID, kafkaConsumerRecord.getAction().toString()); + ConsumerRecord consumerRecord = buildConsumerRecord(topic, kafkaConsumerRecord); + return new KafkaConsumerRecordImpl<>(consumerRecord); + } + + protected ConsumerRecord buildConsumerRecord(String topic, OrganizationAuditEvent event) { + ConsumerRecord consumerRecord = new ConsumerRecord<>("folio", 0, 0, topic, Json.encode(event)); + consumerRecord.headers().add(new RecordHeader(OkapiConnectionParams.OKAPI_TENANT_HEADER, TENANT_ID.getBytes(StandardCharsets.UTF_8))); + consumerRecord.headers().add(new RecordHeader(OKAPI_URL_HEADER, ("http://localhost:" + 8080).getBytes(StandardCharsets.UTF_8))); + consumerRecord.headers().add(new RecordHeader(OKAPI_TOKEN_HEADER, (TOKEN).getBytes(StandardCharsets.UTF_8))); + return consumerRecord; + } +} diff --git a/mod-audit-server/src/test/java/org/folio/services/OrganizationAuditEventsServiceTest.java b/mod-audit-server/src/test/java/org/folio/services/OrganizationAuditEventsServiceTest.java new file mode 100644 index 00000000..8ce23718 --- /dev/null +++ b/mod-audit-server/src/test/java/org/folio/services/OrganizationAuditEventsServiceTest.java @@ -0,0 +1,92 @@ +package org.folio.services; + +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import io.vertx.sqlclient.Row; +import io.vertx.sqlclient.RowSet; +import io.vertx.sqlclient.Tuple; +import org.folio.CopilotGenerated; +import org.folio.dao.acquisition.OrganizationEventsDao; +import org.folio.dao.acquisition.impl.OrganizationEventsDaoImpl; +import org.folio.rest.jaxrs.model.OrganizationAuditEvent; +import org.folio.rest.jaxrs.model.OrganizationAuditEventCollection; +import org.folio.rest.persist.PostgresClient; +import org.folio.services.acquisition.impl.OrganizationAuditEventsServiceImpl; +import org.folio.util.PostgresClientFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.List; +import java.util.UUID; + +import static org.folio.utils.EntityUtils.ACTION_DATE_SORT_BY; +import static org.folio.utils.EntityUtils.DESC_ORDER; +import static org.folio.utils.EntityUtils.LIMIT; +import static org.folio.utils.EntityUtils.OFFSET; +import static org.folio.utils.EntityUtils.TENANT_ID; +import static org.folio.utils.EntityUtils.createOrganizationAuditEvent; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +@CopilotGenerated +public class OrganizationAuditEventsServiceTest { + + @Mock + private RowSet rowSet; + @Mock + private PostgresClient postgresClient; + + private OrganizationEventsDao organizationEventsDao; + private OrganizationAuditEventsServiceImpl organizationAuditEventService; + + @BeforeEach + public void setUp() throws Exception { + try (var ignored = MockitoAnnotations.openMocks(this)) { + var postgresClientFactory = spy(new PostgresClientFactory(Vertx.vertx())); + organizationEventsDao = spy(new OrganizationEventsDaoImpl(postgresClientFactory)); + organizationAuditEventService = new OrganizationAuditEventsServiceImpl(organizationEventsDao); + + doReturn(postgresClient).when(postgresClientFactory).createInstance(TENANT_ID); + } + } + + @Test + void shouldCallDaoForSuccessfulCase() { + var organizationAuditEvent = createOrganizationAuditEvent(UUID.randomUUID().toString()); + doReturn(Future.succeededFuture(rowSet)).when(postgresClient).execute(anyString(), any(Tuple.class)); + + var saveFuture = organizationAuditEventService.saveOrganizationAuditEvent(organizationAuditEvent, TENANT_ID); + saveFuture.onComplete(asyncResult -> assertTrue(asyncResult.succeeded())); + + verify(organizationEventsDao, times(1)).save(organizationAuditEvent, TENANT_ID); + } + + @Test + void shouldGetDto() { + var id = UUID.randomUUID().toString(); + var organizationAuditEvent = createOrganizationAuditEvent(id); + var organizationAuditEventCollection = new OrganizationAuditEventCollection().withOrganizationAuditEvents(List.of(organizationAuditEvent)).withTotalItems(1); + + doReturn(Future.succeededFuture(organizationAuditEventCollection)).when(organizationEventsDao).getAuditEventsByOrganizationId(anyString(), anyString(), anyString(), anyInt(), anyInt(), anyString()); + + var dto = organizationAuditEventService.getAuditEventsByOrganizationId(id, ACTION_DATE_SORT_BY, DESC_ORDER, LIMIT, OFFSET, TENANT_ID); + dto.onComplete(asyncResult -> { + var organizationAuditEventOptional = asyncResult.result(); + var organizationAuditEventList = organizationAuditEventOptional.getOrganizationAuditEvents(); + + assertEquals(organizationAuditEventList.get(0).getId(), id); + assertEquals(OrganizationAuditEvent.Action.CREATE, organizationAuditEventList.get(0).getAction()); + }); + + verify(organizationEventsDao, times(1)).getAuditEventsByOrganizationId(id, ACTION_DATE_SORT_BY, DESC_ORDER, LIMIT, OFFSET, TENANT_ID); + } +} 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 index 71c4b82d..e1a57beb 100644 --- a/mod-audit-server/src/test/java/org/folio/utils/EntityUtils.java +++ b/mod-audit-server/src/test/java/org/folio/utils/EntityUtils.java @@ -8,6 +8,7 @@ import org.folio.rest.jaxrs.model.InvoiceLineAuditEvent; import org.folio.rest.jaxrs.model.OrderAuditEvent; import org.folio.rest.jaxrs.model.OrderLineAuditEvent; +import org.folio.rest.jaxrs.model.OrganizationAuditEvent; import org.folio.rest.jaxrs.model.PieceAuditEvent; public class EntityUtils { @@ -19,9 +20,10 @@ public class EntityUtils { public static final String TENANT_ID = "diku"; public static final String PIECE_ID = "2cd4adc4-f287-49b6-a9c6-9eacdc4868e7"; public static final String ORDER_ID = "a21fc51c-d46b-439b-8c79-9b2be41b79a6"; - public static final String ORDER_LINE_ID = "a22fc51c-d46b-439b-8c79-9b2be41b79a6"; + public static final String ORDER_LINE_ID = "3448efa3-ef4b-4518-a924-d1b36255cc20"; public static final String INVOICE_ID = "3f29b1a4-8c2b-4d3a-9b1e-5f2a1b4c8d3a"; public static final String INVOICE_LINE_ID = "550e8400-e29b-41d4-a716-446655440001"; + public static final String ORGANIZATION_ID = "39e7362e-b487-4d51-8bdb-cbd6bf29d1c7"; public static OrderAuditEvent createOrderAuditEvent(String id) { JsonObject jsonObject = new JsonObject(); @@ -144,4 +146,31 @@ public static InvoiceLineAuditEvent createInvoiceLineAuditEvent(String id) { .withActionDate(new Date()) .withInvoiceLineSnapshot(jsonObject); } + + + + public static OrganizationAuditEvent createOrganizationAuditEvent(String id) { + JsonObject jsonObject = new JsonObject(); + jsonObject.put("name", "Test Organization 123"); + + return new OrganizationAuditEvent() + .withId(id) + .withAction(OrganizationAuditEvent.Action.CREATE) + .withOrganizationId(UUID.randomUUID().toString()) + .withUserId(UUID.randomUUID().toString()) + .withEventDate(new Date()) + .withActionDate(new Date()) + .withOrganizationSnapshot(jsonObject); + } + + public static OrganizationAuditEvent createOrganizationAuditEventWithoutSnapshot() { + return new OrganizationAuditEvent() + .withId(UUID.randomUUID().toString()) + .withAction(OrganizationAuditEvent.Action.CREATE) + .withOrganizationId(UUID.randomUUID().toString()) + .withUserId(UUID.randomUUID().toString()) + .withEventDate(new Date()) + .withActionDate(new Date()) + .withOrganizationSnapshot("Test"); + } } diff --git a/ramls/acquisition-events.raml b/ramls/acquisition-events.raml index 9b21ea0b..e57e2d58 100644 --- a/ramls/acquisition-events.raml +++ b/ramls/acquisition-events.raml @@ -8,7 +8,6 @@ documentation: - title: Acquisition Audit events API content: API for retrieving events for acquisition changes - types: errors: !include raml-util/schemas/errors.schema order-line-audit-event: !include order_line_audit_event.json @@ -20,6 +19,8 @@ types: invoice-audit-event: !include invoice_audit_event.json invoice-audit-event-collection: !include invoice_audit_event_collection.json invoice-line-audit-event-collection: !include invoice_line_audit_event_collection.json + organization-audit-event: !include organization_audit_event.json + organization-audit-event-collection: !include organization_audit_event_collection.json traits: searchable: !include raml-util/traits/searchable.raml @@ -229,3 +230,38 @@ traits: body: application/json: type: errors + + /organization/{id}: + get: + description: Get list of organization events by organization_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: organization-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/organization_audit_event.json b/ramls/organization_audit_event.json new file mode 100644 index 00000000..053f96a9 --- /dev/null +++ b/ramls/organization_audit_event.json @@ -0,0 +1,40 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Organization audit event", + "type": "object", + "properties": { + "id": { + "description": "UUID of the event", + "$ref": "common/uuid.json" + }, + "action": { + "description": "Action for organization (Create, Edit or Delete)", + "type": "string", + "$ref": "event_action.json" + }, + "organizationId": { + "description": "UUID of the organization", + "$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 organization action occurred", + "format": "date-time", + "type": "string" + }, + "organizationSnapshot": { + "description": "Full snapshot of the organization", + "type": "object", + "javaType": "java.lang.Object" + } + }, + "additionalProperties": true +} diff --git a/ramls/organization_audit_event_collection.json b/ramls/organization_audit_event_collection.json new file mode 100644 index 00000000..667c1319 --- /dev/null +++ b/ramls/organization_audit_event_collection.json @@ -0,0 +1,25 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Collection of organizationAuditEvents", + "type": "object", + "additionalProperties": false, + "properties": { + "organizationAuditEvents": { + "description": "List of organizationAuditEvents", + "type": "array", + "id": "organizationAuditEventsList", + "items": { + "type": "object", + "$ref": "organization_audit_event.json" + } + }, + "totalItems": { + "description": "total records", + "type": "integer" + } + }, + "required": [ + "organizationAuditEvents", + "totalItems" + ] +}