diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index 9cadecb2..c0a907c9 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -151,6 +151,21 @@ } ] }, + { + "id": "acquisition-invoice-events", + "version": "1.0", + "handlers": [ + { + "methods": [ + "GET" + ], + "pathPattern": "/audit-data/acquisition/invoice/{id}", + "permissionsRequired": [ + "acquisition.invoice.events.get" + ] + } + ] + }, { "id": "circulation-logs", "version": "1.2", @@ -264,6 +279,11 @@ "displayName": "Acquisition piece status change history events - get piece status change events", "description": "Get piece status change events" }, + { + "permissionName": "acquisition.invoice.events.get", + "displayName": "Acquisition invoice events - get invoice change events", + "description": "Get invoice change events" + }, { "permissionName": "audit.all", "displayName": "Audit - all permissions", @@ -278,7 +298,8 @@ "acquisition.order.events.get", "acquisition.order-line.events.get", "acquisition.piece.events.get", - "acquisition.piece.events.history.get" + "acquisition.piece.events.history.get", + "acquisition.invoice.events.get" ] } ], diff --git a/mod-audit-server/pom.xml b/mod-audit-server/pom.xml index 87ce1dc0..f1813522 100644 --- a/mod-audit-server/pom.xml +++ b/mod-audit-server/pom.xml @@ -2,13 +2,13 @@ 4.0.0 mod-audit-server - 2.10.1-SNAPSHOT + 3.0.0-SNAPSHOT jar mod-audit org.folio - 2.10.1-SNAPSHOT + 3.0.0-SNAPSHOT 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 new file mode 100644 index 00000000..fc955b63 --- /dev/null +++ b/mod-audit-server/src/main/java/org/folio/dao/acquisition/InvoiceEventsDao.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.InvoiceAuditEvent; +import org.folio.rest.jaxrs.model.InvoiceAuditEventCollection; + +public interface InvoiceEventsDao { + + /** + * Saves invoiceAuditEvent entity to DB + * + * @param invoiceAuditEvent InvoiceAuditEvent entity to save + * @param tenantId tenant id + * @return future with created row + */ + Future> save(InvoiceAuditEvent invoiceAuditEvent, String tenantId); + + /** + * Searches for invoice audit events by id + * + * @param invoiceId invoice id + * @param sortBy sort by + * @param sortInvoice sort invoice + * @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); +} 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 new file mode 100644 index 00000000..55a32c75 --- /dev/null +++ b/mod-audit-server/src/main/java/org/folio/dao/acquisition/impl/InvoiceEventsDaoImpl.java @@ -0,0 +1,111 @@ +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.InvoiceEventsDao; +import org.folio.rest.jaxrs.model.InvoiceAuditEvent; +import org.folio.rest.jaxrs.model.InvoiceAuditEventCollection; +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.INVOICE_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.TOTAL_RECORDS_FIELD; +import static org.folio.util.AuditEventDBConstants.USER_ID_FIELD; +import static org.folio.util.DbUtils.formatDBTableName; + +@Repository +public class InvoiceEventsDaoImpl implements InvoiceEventsDao { + + private static final Logger LOGGER = LogManager.getLogger(); + + public static final String TABLE_NAME = "acquisition_invoice_log"; + + public static final String GET_BY_INVOICE_ID_SQL = "SELECT id, action, invoice_id, user_id, event_date, action_date, modified_content_snapshot," + + " (SELECT count(*) AS total_records FROM %s WHERE invoice_id = $1) FROM %s WHERE invoice_id = $1 %s LIMIT $2 OFFSET $3"; + + public static final String INSERT_SQL = "INSERT INTO %s (id, action, invoice_id, user_id, event_date, action_date, modified_content_snapshot)" + + " VALUES ($1, $2, $3, $4, $5, $6, $7)"; + + private final PostgresClientFactory pgClientFactory; + + public InvoiceEventsDaoImpl(PostgresClientFactory pgClientFactory) { + this.pgClientFactory = pgClientFactory; + } + + @Override + public Future> save(InvoiceAuditEvent invoiceAuditEvent, String tenantId) { + LOGGER.debug("save:: Saving Invoice AuditEvent with tenant id : {}", tenantId); + String logTable = formatDBTableName(tenantId, TABLE_NAME); + String query = format(INSERT_SQL, logTable); + return makeSaveCall(query, invoiceAuditEvent, tenantId) + .onSuccess(rows -> LOGGER.info("save:: Saved Invoice AuditEvent with tenant id : {}", tenantId)) + .onFailure(e -> LOGGER.error("Failed to save record with id: {} for invoice id: {} in to table {}", + invoiceAuditEvent.getId(), invoiceAuditEvent.getInvoiceId(), TABLE_NAME, e)); + } + + @Override + public Future getAuditEventsByInvoiceId(String invoiceId, String sortBy, String sortInvoice, int limit, int offset, String tenantId) { + LOGGER.debug("getAuditEventsByInvoiceId:: Retrieving AuditEvent with invoice id : {}", invoiceId); + String logTable = formatDBTableName(tenantId, TABLE_NAME); + String query = format(GET_BY_INVOICE_ID_SQL, logTable, logTable, format(ORDER_BY_PATTERN, sortBy, sortInvoice)); + return pgClientFactory.createInstance(tenantId).execute(query, Tuple.of(UUID.fromString(invoiceId), limit, offset)) + .map(rowSet -> rowSet.rowCount() == 0 ? new InvoiceAuditEventCollection().withTotalItems(0) + : mapRowToListOfInvoiceEvent(rowSet)); + } + + private Future> makeSaveCall(String query, InvoiceAuditEvent invoiceAuditEvent, String tenantId) { + LOGGER.debug("makeSaveCall:: Making save call with query : {} and tenant id : {}", query, tenantId); + try { + return pgClientFactory.createInstance(tenantId).execute(query, Tuple.of(invoiceAuditEvent.getId(), + invoiceAuditEvent.getAction(), + invoiceAuditEvent.getInvoiceId(), + invoiceAuditEvent.getUserId(), + LocalDateTime.ofInstant(invoiceAuditEvent.getEventDate().toInstant(), ZoneId.systemDefault()), + LocalDateTime.ofInstant(invoiceAuditEvent.getActionDate().toInstant(), ZoneId.systemDefault()), + JsonObject.mapFrom(invoiceAuditEvent.getInvoiceSnapshot()))); + } catch (Exception e) { + return Future.failedFuture(e); + } + } + + private InvoiceAuditEventCollection mapRowToListOfInvoiceEvent(RowSet rowSet) { + LOGGER.debug("mapRowToListOfInvoiceEvent:: Mapping row to List of Invoice Events"); + InvoiceAuditEventCollection invoiceAuditEventCollection = new InvoiceAuditEventCollection(); + rowSet.iterator().forEachRemaining(row -> { + invoiceAuditEventCollection.getInvoiceAuditEvents().add(mapRowToInvoiceEvent(row)); + invoiceAuditEventCollection.setTotalItems(row.getInteger(TOTAL_RECORDS_FIELD)); + }); + LOGGER.debug("mapRowToListOfInvoiceEvent:: Mapped row to List of Invoice Events"); + return invoiceAuditEventCollection; + } + + private InvoiceAuditEvent mapRowToInvoiceEvent(Row row) { + LOGGER.debug("mapRowToInvoiceEvent:: Mapping row to Invoice Event"); + return new InvoiceAuditEvent() + .withId(row.getValue(ID_FIELD).toString()) + .withAction(row.get(InvoiceAuditEvent.Action.class, ACTION_FIELD)) + .withInvoiceId(row.getValue(INVOICE_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))) + .withInvoiceSnapshot(JsonObject.mapFrom(row.getValue(MODIFIED_CONTENT_FIELD))); + } +} 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 f405be20..152aaa84 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 @@ -7,12 +7,14 @@ import io.vertx.core.Future; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.folio.rest.jaxrs.model.AuditDataAcquisitionInvoiceIdGetSortOrder; 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.InvoiceAuditEventsService; import org.folio.services.acquisition.OrderAuditEventsService; import org.folio.services.acquisition.OrderLineAuditEventsService; import org.folio.services.acquisition.PieceAuditEventsService; @@ -35,6 +37,8 @@ public class AuditDataAcquisitionImpl implements AuditDataAcquisition { private OrderLineAuditEventsService orderLineAuditEventsService; @Autowired private PieceAuditEventsService pieceAuditEventsService; + @Autowired + private InvoiceAuditEventsService invoiceAuditEventsService; public AuditDataAcquisitionImpl() { SpringContextUtil.autowireDependencies(this, Vertx.currentContext()); @@ -110,6 +114,24 @@ public void getAuditDataAcquisitionPieceStatusChangeHistoryById(String pieceId, } } + @Override + public void getAuditDataAcquisitionInvoiceById(String invoiceId, String sortBy, + AuditDataAcquisitionInvoiceIdGetSortOrder sortOrder, + int limit, int offset, Map okapiHeaders, Handler> asyncResultHandler, Context vertxContext) { + LOGGER.debug("getAuditDataAcquisitionOrderLineById:: Retrieving Audit Data Acquisition Invoice Line By Id : {}", invoiceId); + String tenantId = TenantTool.tenantId(okapiHeaders); + try { + invoiceAuditEventsService.getAuditEventsByInvoiceId(invoiceId, sortBy, sortOrder.name(), limit, offset, tenantId) + .map(GetAuditDataAcquisitionInvoiceByIdResponse::respond200WithApplicationJson) + .map(Response.class::cast) + .otherwise(this::mapExceptionToResponse) + .onComplete(asyncResultHandler); + } catch (Exception e) { + LOGGER.error("Failed to get invoice audit events by piece id: {}", invoiceId, e); + asyncResultHandler.handle(Future.succeededFuture(mapExceptionToResponse(e))); + } + } + private Response mapExceptionToResponse(Throwable throwable) { LOGGER.debug("mapExceptionToResponse:: Mapping Exception :{} to Response", throwable.getMessage(), throwable); return GetAuditDataAcquisitionOrderByIdResponse 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 7a5028d7..c1491e9d 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 @@ -6,6 +6,7 @@ import io.vertx.core.Future; import io.vertx.core.Handler; import io.vertx.core.Promise; +import io.vertx.core.ThreadingModel; import io.vertx.core.Vertx; import io.vertx.core.spi.VerticleFactory; import org.apache.logging.log4j.LogManager; @@ -15,6 +16,7 @@ import org.folio.rest.resource.interfaces.InitAPI; import org.folio.spring.SpringContextUtil; import org.folio.verticle.SpringVerticleFactory; +import org.folio.verticle.acquisition.InvoiceEventConsumersVerticle; import org.folio.verticle.acquisition.OrderEventConsumersVerticle; import org.folio.verticle.acquisition.OrderLineEventConsumersVerticle; import org.folio.verticle.acquisition.PieceEventConsumersVerticle; @@ -29,10 +31,23 @@ public class InitAPIs implements InitAPI { @Value("${acq.orders.kafka.consumer.instancesNumber:1}") private int acqOrderConsumerInstancesNumber; + @Value("${acq.orders.kafka.consumer.pool.size:5}") + private int acqOrderConsumerPoolSize; + @Value("${acq.order-lines.kafka.consumer.instancesNumber:1}") private int acqOrderLineConsumerInstancesNumber; + @Value("${acq.order-lines.kafka.consumer.pool.size:5}") + private int acqOrderLineConsumerPoolSize; + @Value("${acq.pieces.kafka.consumer.instancesNumber:1}") private int acqPieceConsumerInstancesNumber; + @Value("${acq.orders.kafka.consumer.pool.size:5}") + private int acqPieceConsumerPoolSize; + + @Value("${acq.invoices.kafka.consumer.instancesNumber:1}") + private int acqInvoiceConsumerInstancesNumber; + @Value("${acq.orders.kafka.consumer.pool.size:5}") + private int acqInvoiceConsumerPoolSize; @Override public void init(Vertx vertx, Context context, Handler> handler) { @@ -64,27 +79,26 @@ private Future deployConsumersVerticles(Vertx vertx) { Promise orderEventsConsumer = Promise.promise(); Promise orderLineEventsConsumer = Promise.promise(); Promise pieceEventsConsumer = Promise.promise(); + Promise invoiceEventsConsumer = Promise.promise(); - vertx.deployVerticle(getVerticleName(verticleFactory, OrderEventConsumersVerticle.class), - new DeploymentOptions() - .setWorker(true) - .setInstances(acqOrderConsumerInstancesNumber), orderEventsConsumer); - - vertx.deployVerticle(getVerticleName(verticleFactory, OrderLineEventConsumersVerticle.class), - new DeploymentOptions() - .setWorker(true) - .setInstances(acqOrderLineConsumerInstancesNumber), orderLineEventsConsumer); - - vertx.deployVerticle(getVerticleName(verticleFactory, PieceEventConsumersVerticle.class), - new DeploymentOptions() - .setWorker(true) - .setInstances(acqPieceConsumerInstancesNumber), pieceEventsConsumer); + 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); LOGGER.info("deployConsumersVerticles:: All consumer verticles were successfully deployed"); return GenericCompositeFuture.all(Arrays.asList( orderEventsConsumer.future(), orderLineEventsConsumer.future(), - pieceEventsConsumer.future())); + pieceEventsConsumer.future(), + invoiceEventsConsumer.future())); + } + + private void deployVerticle(Vertx vertx, VerticleFactory verticleFactory, Class consumerClass, + int acqOrderConsumerInstancesNumber, int acqOrderConsumerPoolSize, Promise orderEventsConsumer) { + DeploymentOptions deploymentOptions = new DeploymentOptions().setThreadingModel(ThreadingModel.WORKER) + .setInstances(acqOrderConsumerInstancesNumber).setWorkerPoolSize(acqOrderConsumerPoolSize); + vertx.deployVerticle(getVerticleName(verticleFactory, consumerClass), deploymentOptions, orderEventsConsumer); } private String getVerticleName(VerticleFactory verticleFactory, Class clazz) { 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 new file mode 100644 index 00000000..782480fd --- /dev/null +++ b/mod-audit-server/src/main/java/org/folio/services/acquisition/InvoiceAuditEventsService.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.InvoiceAuditEvent; +import org.folio.rest.jaxrs.model.InvoiceAuditEventCollection; + +public interface InvoiceAuditEventsService { + + /** + * Saves InvoiceAuditEvent + * + * @param invoiceAuditEvent + * @param tenantId id of tenant + * @return successful future if event has not been processed, or failed future otherwise + */ + Future> saveInvoiceAuditEvent(InvoiceAuditEvent invoiceAuditEvent, String tenantId); + + /** + * Searches for invoice audit events by invoice id + * + * @param invoiceId invoice id + * @param sortBy sort by + * @param sortInvoice sort invoice + * @param limit limit + * @param offset offset + * @return future with InvoiceAuditEventCollection + */ + Future getAuditEventsByInvoiceId(String invoiceId, String sortBy, String sortInvoice, int limit, int offset, String tenantId); +} diff --git a/mod-audit-server/src/main/java/org/folio/services/acquisition/impl/InvoiceAuditEventsServiceImpl.java b/mod-audit-server/src/main/java/org/folio/services/acquisition/impl/InvoiceAuditEventsServiceImpl.java new file mode 100644 index 00000000..5541f564 --- /dev/null +++ b/mod-audit-server/src/main/java/org/folio/services/acquisition/impl/InvoiceAuditEventsServiceImpl.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.InvoiceEventsDao; +import org.folio.rest.jaxrs.model.InvoiceAuditEvent; +import org.folio.rest.jaxrs.model.InvoiceAuditEventCollection; +import org.folio.services.acquisition.InvoiceAuditEventsService; +import org.springframework.stereotype.Service; + +import static org.folio.util.ErrorUtils.handleFailures; + +@Service +public class InvoiceAuditEventsServiceImpl implements InvoiceAuditEventsService { + + private static final Logger LOGGER = LogManager.getLogger(); + + private final InvoiceEventsDao invoiceEventsDao; + + public InvoiceAuditEventsServiceImpl(InvoiceEventsDao invoiceEvenDao) { + this.invoiceEventsDao = invoiceEvenDao; + } + + @Override + public Future> saveInvoiceAuditEvent(InvoiceAuditEvent invoiceAuditEvent, String tenantId) { + LOGGER.debug("saveInvoiceAuditEvent:: Saving invoice audit event with invoiceId={} for tenantId={}", invoiceAuditEvent.getInvoiceId(), tenantId); + return invoiceEventsDao.save(invoiceAuditEvent, tenantId) + .recover(throwable -> { + LOGGER.error("handleFailures:: Could not save invoice audit event for Invoice id: {} in tenantId: {}", invoiceAuditEvent.getInvoiceId(), tenantId); + return handleFailures(throwable, invoiceAuditEvent.getId()); + }); + } + + @Override + public Future getAuditEventsByInvoiceId(String invoiceId, String sortBy, String sortInvoice, int limit, int offset, String tenantId) { + LOGGER.debug("getAuditEventsByInvoiceId:: Retrieving audit events for invoiceId={} and tenantId={}", invoiceId, tenantId); + return invoiceEventsDao.getAuditEventsByInvoiceId(invoiceId, sortBy, sortInvoice, 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 3ecb182e..4ae865a5 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 @@ -3,7 +3,8 @@ public enum AcquisitionEventType { ACQ_ORDER_CHANGED("ACQ_ORDER_CHANGED"), ACQ_ORDER_LINE_CHANGED("ACQ_ORDER_LINE_CHANGED"), - ACQ_PIECE_CHANGED("ACQ_PIECE_CHANGED"); + ACQ_PIECE_CHANGED("ACQ_PIECE_CHANGED"), + ACQ_INVOICE_CHANGED("ACQ_INVOICE_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 62d40cfc..cfbded99 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 @@ -14,6 +14,8 @@ private AuditEventDBConstants() {} public static final String PIECE_ID_FIELD = "piece_id"; + public static final String INVOICE_ID_FIELD = "invoice_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/InvoiceEventConsumersVerticle.java b/mod-audit-server/src/main/java/org/folio/verticle/acquisition/InvoiceEventConsumersVerticle.java new file mode 100644 index 00000000..25afa5cd --- /dev/null +++ b/mod-audit-server/src/main/java/org/folio/verticle/acquisition/InvoiceEventConsumersVerticle.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 InvoiceEventConsumersVerticle extends AbstractConsumersVerticle { + + @Autowired + private AsyncRecordHandler invoiceEventsHandler; + + @Override + public List getEvents() { + return List.of(AcquisitionEventType.ACQ_INVOICE_CHANGED.getTopicName()); + } + + @Override + public AsyncRecordHandler getHandler() { + return invoiceEventsHandler; + } +} 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 new file mode 100644 index 00000000..e24ffdfc --- /dev/null +++ b/mod-audit-server/src/main/java/org/folio/verticle/acquisition/consumers/InvoiceEventsHandler.java @@ -0,0 +1,61 @@ +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; +import org.folio.kafka.KafkaHeaderUtils; +import org.folio.kafka.exception.DuplicateEventException; +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 { + + private static final Logger LOGGER = LogManager.getLogger(); + + private final InvoiceAuditEventsService invoiceAuditEventsService; + private final Vertx vertx; + + public InvoiceEventsHandler(@Autowired Vertx vertx, + @Autowired 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); + 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()); + }) + .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(); + } +} diff --git a/mod-audit-server/src/main/resources/templates/db_scripts/acquisition/create_acquisition_invoice_log_table.sql b/mod-audit-server/src/main/resources/templates/db_scripts/acquisition/create_acquisition_invoice_log_table.sql new file mode 100644 index 00000000..10c8f7b9 --- /dev/null +++ b/mod-audit-server/src/main/resources/templates/db_scripts/acquisition/create_acquisition_invoice_log_table.sql @@ -0,0 +1,11 @@ +CREATE TABLE IF NOT EXISTS acquisition_invoice_log ( + id uuid PRIMARY KEY, + action text NOT NULL, + invoice_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 invoice_id_index ON acquisition_invoice_log USING BTREE (invoice_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 be4d0804..bebe29d3 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 @@ -79,6 +79,11 @@ "run": "after", "snippetPath": "acquisition/create_acquisition_piece_log_table.sql", "fromModuleVersion": "mod-audit-2.9.0" + }, + { + "run": "after", + "snippetPath": "acquisition/create_acquisition_invoice_log_table.sql", + "fromModuleVersion": "mod-audit-3.0.0" } ] } diff --git a/mod-audit-server/src/test/java/org/folio/CopilotGenerated.java b/mod-audit-server/src/test/java/org/folio/CopilotGenerated.java new file mode 100644 index 00000000..590f0c49 --- /dev/null +++ b/mod-audit-server/src/test/java/org/folio/CopilotGenerated.java @@ -0,0 +1,25 @@ +package org.folio; + +import org.springframework.core.annotation.AliasFor; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to indicate that test(s) in the class are generated by GitHub Copilot. + *

+ * Set value or partiallyGenerated attribute to true + * if the generated test(s) were significantly modified/altered by the developer. + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.SOURCE) +public @interface CopilotGenerated { + + @AliasFor("partiallyGenerated") + boolean value() default false; + + @AliasFor("value") + boolean partiallyGenerated() default false; +} 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 797a0916..5b1b3a30 100644 --- a/mod-audit-server/src/test/java/org/folio/TestSuite.java +++ b/mod-audit-server/src/test/java/org/folio/TestSuite.java @@ -12,6 +12,7 @@ import io.restassured.RestAssured; import io.vertx.core.DeploymentOptions; +import io.vertx.core.ThreadingModel; import io.vertx.core.Vertx; import io.vertx.core.json.JsonObject; import net.mguenther.kafka.junit.EmbeddedKafkaCluster; @@ -23,6 +24,7 @@ import org.folio.builder.service.ManualBlockRecordBuilderTest; import org.folio.builder.service.NoticeRecordBuilderTest; import org.folio.builder.service.RequestRecordBuilderTest; +import org.folio.dao.InvoiceEventsDaoTest; import org.folio.dao.OrderEventsDaoTest; import org.folio.dao.OrderLineEventsDaoTest; import org.folio.dao.PieceEventsDaoTest; @@ -32,10 +34,12 @@ import org.folio.rest.impl.AuditDataImplApiTest; import org.folio.rest.impl.AuditHandlersImplApiTest; import org.folio.rest.impl.CirculationLogsImplApiTest; +import org.folio.rest.impl.InvoiceEventsHandlerMockTest; 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.InvoiceAuditEventsServiceTest; import org.folio.services.OrderAuditEventsServiceTest; import org.folio.services.OrderLineAuditEventsServiceTest; import org.folio.services.PieceAuditEventsServiceTest; @@ -66,7 +70,7 @@ public static void globalInitialize() throws InterruptedException, ExecutionExce DeploymentOptions options = new DeploymentOptions(); options.setConfig(new JsonObject().put("http.port", port).put("mock.httpclient", "true")); - options.setWorker(true); + options.setThreadingModel(ThreadingModel.WORKER); startKafkaMockServer(); String[] hostAndPort = kafkaCluster.getBrokerList().split(":"); @@ -194,6 +198,18 @@ class PieceAuditEventsServiceNestedTest extends PieceAuditEventsServiceTest { class PieceEventsHandlerMockNestedTest extends PieceEventsHandlerMockTest { } + @Nested + class InvoiceEventsHandlerMockNestedTest extends InvoiceEventsHandlerMockTest { + } + + @Nested + class InvoiceAuditEventsServiceNestedTest extends InvoiceAuditEventsServiceTest { + } + + @Nested + class InvoiceEventsDaoNestedTest extends InvoiceEventsDaoTest { + } + @Nested class AuditDataImplApiTestNested extends AuditDataImplApiTest { } diff --git a/mod-audit-server/src/test/java/org/folio/dao/InvoiceEventsDaoTest.java b/mod-audit-server/src/test/java/org/folio/dao/InvoiceEventsDaoTest.java new file mode 100644 index 00000000..79f9a672 --- /dev/null +++ b/mod-audit-server/src/test/java/org/folio/dao/InvoiceEventsDaoTest.java @@ -0,0 +1,90 @@ +package org.folio.dao; + +import static org.folio.utils.EntityUtils.TENANT_ID; +import static org.folio.utils.EntityUtils.createInvoiceAuditEvent; +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.anyString; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +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 io.vertx.sqlclient.Tuple; +import org.folio.CopilotGenerated; +import org.folio.dao.acquisition.impl.InvoiceEventsDaoImpl; +import org.folio.rest.jaxrs.model.InvoiceAuditEvent; +import org.folio.rest.jaxrs.model.InvoiceAuditEventCollection; +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; + +@CopilotGenerated +public class InvoiceEventsDaoTest { + + @Spy + private PostgresClientFactory postgresClientFactory = new PostgresClientFactory(Vertx.vertx()); + @InjectMocks + InvoiceEventsDaoImpl invoiceEventDao = new InvoiceEventsDaoImpl(postgresClientFactory); + + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + invoiceEventDao = new InvoiceEventsDaoImpl(postgresClientFactory); + } + + @Test + void shouldCreateEventProcessed() { + var invoiceAuditEvent = createInvoiceAuditEvent(UUID.randomUUID().toString()); + + Future> saveFuture = invoiceEventDao.save(invoiceAuditEvent, TENANT_ID); + saveFuture.onComplete(ar -> { + assertTrue(ar.succeeded()); + verify(postgresClientFactory, times(1)).createInstance(TENANT_ID).execute(anyString(), any(Tuple.class)); + }); + } + + @Test + void shouldThrowConstraintViolation() { + var invoiceAuditEvent = createInvoiceAuditEvent(UUID.randomUUID().toString()); + + Future> saveFuture = invoiceEventDao.save(invoiceAuditEvent, TENANT_ID); + saveFuture.onComplete(ar -> { + Future> reSaveFuture = invoiceEventDao.save(invoiceAuditEvent, TENANT_ID); + reSaveFuture.onComplete(re -> { + assertTrue(re.failed()); + assertTrue(re.cause() instanceof PgException); + assertEquals("ERROR: duplicate key value violates unique constraint \"acquisition_invoice_log_pkey\" (23505)", re.cause().getMessage()); + verify(postgresClientFactory, times(1)).createInstance(TENANT_ID).execute(anyString(), any(Tuple.class)); + }); + }); + } + + @Test + void shouldGetCreatedEvent() { + String id = UUID.randomUUID().toString(); + var invoiceAuditEvent = createInvoiceAuditEvent(id); + + invoiceEventDao.save(invoiceAuditEvent, TENANT_ID); + + Future dto = invoiceEventDao.getAuditEventsByInvoiceId(id, "action_date", "desc", 1, 1, TENANT_ID); + dto.onComplete(ar -> { + InvoiceAuditEventCollection invoiceAuditEventOptional = ar.result(); + List invoiceAuditEventList = invoiceAuditEventOptional.getInvoiceAuditEvents(); + + assertEquals(invoiceAuditEventList.get(0).getId(), id); + assertEquals(InvoiceAuditEvent.Action.CREATE.value(), invoiceAuditEventList.get(0).getAction().value()); + verify(postgresClientFactory, times(1)).createInstance(TENANT_ID).selectRead(anyString(), any(Tuple.class), any()); + }); + } +} diff --git a/mod-audit-server/src/test/java/org/folio/rest/impl/ApiTestBase.java b/mod-audit-server/src/test/java/org/folio/rest/impl/ApiTestBase.java index 4889ca3f..f0a13313 100644 --- a/mod-audit-server/src/test/java/org/folio/rest/impl/ApiTestBase.java +++ b/mod-audit-server/src/test/java/org/folio/rest/impl/ApiTestBase.java @@ -5,7 +5,6 @@ import static org.folio.utils.TenantApiTestUtil.deleteTenantAndPurgeTables; import static org.folio.utils.TenantApiTestUtil.prepareTenant; -import java.io.IOException; import java.util.Locale; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; @@ -30,7 +29,7 @@ public class ApiTestBase { private static TenantJob tenantJob; @BeforeAll - public static void globalSetup() throws InterruptedException, ExecutionException, TimeoutException, IOException { + public static void globalSetup() throws InterruptedException, ExecutionException, TimeoutException { Locale.setDefault(Locale.US); if (!isInitialized) { 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 08788e50..63f97052 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 @@ -4,6 +4,7 @@ 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.INVOICE_ID; import static org.folio.utils.EntityUtils.createPieceAuditEvent; import static org.hamcrest.Matchers.containsString; @@ -15,9 +16,12 @@ import io.restassured.http.Header; import io.vertx.core.Vertx; import io.vertx.core.json.JsonObject; +import org.folio.CopilotGenerated; +import org.folio.dao.acquisition.impl.InvoiceEventsDaoImpl; 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.InvoiceAuditEvent; import org.folio.rest.jaxrs.model.OrderAuditEvent; import org.folio.rest.jaxrs.model.OrderLineAuditEvent; import org.folio.rest.jaxrs.model.PieceAuditEvent; @@ -38,6 +42,7 @@ public class AuditDataAcquisitionAPITest extends ApiTestBase { 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 ACQ_AUDIT_INVOICE_PATH = "/audit-data/acquisition/invoice/"; private static final String TENANT_ID = "modaudittest"; @Spy @@ -49,12 +54,15 @@ public class AuditDataAcquisitionAPITest extends ApiTestBase { OrderLineEventsDaoImpl orderLineEventDao; @InjectMocks PieceEventsDaoImpl pieceEventsDao; + @InjectMocks + InvoiceEventsDaoImpl invoiceEventsDao; @BeforeEach public void setUp() { MockitoAnnotations.openMocks(this); orderEventDao = new OrderEventsDaoImpl(postgresClientFactory); orderLineEventDao = new OrderLineEventsDaoImpl(postgresClientFactory); + invoiceEventsDao = new InvoiceEventsDaoImpl(postgresClientFactory); } @Test @@ -248,4 +256,47 @@ void shouldReturnPieceEventsStatusChangesHistoryGetByPieceId() { .then().log().all().statusCode(500) .body(containsString("UUID string too large")); } + + @Test + @CopilotGenerated + void shouldReturnInvoiceEventsOnGetByInvoiceId() { + JsonObject jsonObject = new JsonObject(); + jsonObject.put("name", "Test Product2"); + + InvoiceAuditEvent invoiceAuditEvent = new InvoiceAuditEvent() + .withId(UUID.randomUUID().toString()) + .withAction(InvoiceAuditEvent.Action.CREATE) + .withInvoiceId(INVOICE_ID) + .withUserId(UUID.randomUUID().toString()) + .withEventDate(new Date()) + .withActionDate(new Date()) + .withInvoiceSnapshot(jsonObject); + + invoiceEventsDao.save(invoiceAuditEvent, TENANT_ID); + + given().header(CONTENT_TYPE).header(TENANT).header(PERMS) + .get(ACQ_AUDIT_INVOICE_PATH + INVALID_ID) + .then().log().all().statusCode(200) + .body(containsString("invoiceAuditEvents")).body(containsString("totalItems")); + + given().header(CONTENT_TYPE).header(TENANT).header(PERMS) + .get(ACQ_AUDIT_INVOICE_PATH + INVOICE_ID) + .then().log().all().statusCode(200) + .body(containsString(INVOICE_ID)); + + given().header(CONTENT_TYPE).header(TENANT).header(PERMS) + .get(ACQ_AUDIT_INVOICE_PATH + INVOICE_ID + "?limit=1") + .then().log().all().statusCode(200) + .body(containsString(INVOICE_ID)); + + given().header(CONTENT_TYPE).header(TENANT).header(PERMS) + .get(ACQ_AUDIT_INVOICE_PATH + INVOICE_ID + "?sortBy=action_date") + .then().log().all().statusCode(200) + .body(containsString(INVOICE_ID)); + + given().header(CONTENT_TYPE).header(TENANT).header(PERMS) + .get(ACQ_AUDIT_INVOICE_PATH + INVOICE_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/InvoiceEventsHandlerMockTest.java b/mod-audit-server/src/test/java/org/folio/rest/impl/InvoiceEventsHandlerMockTest.java new file mode 100644 index 00000000..966f85f6 --- /dev/null +++ b/mod-audit-server/src/test/java/org/folio/rest/impl/InvoiceEventsHandlerMockTest.java @@ -0,0 +1,94 @@ +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.InvoiceEventsDaoImpl; +import org.folio.kafka.KafkaTopicNameHelper; +import org.folio.rest.jaxrs.model.InvoiceAuditEvent; +import org.folio.rest.util.OkapiConnectionParams; +import org.folio.services.acquisition.InvoiceAuditEventsService; +import org.folio.services.acquisition.impl.InvoiceAuditEventsServiceImpl; +import org.folio.util.PostgresClientFactory; +import org.folio.verticle.acquisition.consumers.InvoiceEventsHandler; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.*; + +import java.nio.charset.StandardCharsets; +import java.util.UUID; + +import static org.folio.kafka.KafkaTopicNameHelper.getDefaultNameSpace; +import static org.folio.utils.EntityUtils.createInvoiceAuditEvent; +import static org.folio.utils.EntityUtils.createInvoiceAuditEventWithoutSnapshot; + +@CopilotGenerated(partiallyGenerated = true) +public class InvoiceEventsHandlerMockTest { + + 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 + InvoiceEventsDaoImpl invoiceEventDao; + @Mock + InvoiceAuditEventsService invoiceAuditEventServiceImpl; + + private InvoiceEventsHandler invoiceEventsHandler; + + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + invoiceEventDao = new InvoiceEventsDaoImpl(postgresClientFactory); + invoiceAuditEventServiceImpl = new InvoiceAuditEventsServiceImpl(invoiceEventDao); + invoiceEventsHandler = new InvoiceEventsHandler(vertx, invoiceAuditEventServiceImpl); + } + + @Test + void shouldProcessEvent() { + var invoiceAuditEvent = createInvoiceAuditEvent(UUID.randomUUID().toString()); + KafkaConsumerRecord kafkaConsumerRecord = buildKafkaConsumerRecord(invoiceAuditEvent); + + Future saveFuture = invoiceEventsHandler.handle(kafkaConsumerRecord); + saveFuture.onComplete(ar -> Assertions.assertTrue(ar.succeeded())); + } + + @Test + void shouldNotProcessEvent() { + var invoiceAuditEvent = createInvoiceAuditEventWithoutSnapshot(); + KafkaConsumerRecord kafkaConsumerRecord = buildKafkaConsumerRecord(invoiceAuditEvent); + + Future save = invoiceEventsHandler.handle(kafkaConsumerRecord); + Assertions.assertTrue(save.failed()); + } + + private KafkaConsumerRecord buildKafkaConsumerRecord(InvoiceAuditEvent 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, InvoiceAuditEvent 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/InvoiceAuditEventsServiceTest.java b/mod-audit-server/src/test/java/org/folio/services/InvoiceAuditEventsServiceTest.java new file mode 100644 index 00000000..83f4c5db --- /dev/null +++ b/mod-audit-server/src/test/java/org/folio/services/InvoiceAuditEventsServiceTest.java @@ -0,0 +1,64 @@ +package org.folio.services; + +import static org.folio.utils.EntityUtils.TENANT_ID; +import static org.folio.utils.EntityUtils.createInvoiceAuditEvent; +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.sqlclient.Row; +import io.vertx.sqlclient.RowSet; +import org.folio.CopilotGenerated; +import org.folio.dao.acquisition.InvoiceEventsDao; +import org.folio.dao.acquisition.impl.InvoiceEventsDaoImpl; +import org.folio.rest.jaxrs.model.InvoiceAuditEvent; +import org.folio.rest.jaxrs.model.InvoiceAuditEventCollection; +import org.folio.services.acquisition.impl.InvoiceAuditEventsServiceImpl; +import org.folio.util.PostgresClientFactory; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; + +@CopilotGenerated +public class InvoiceAuditEventsServiceTest { + + @Spy + private PostgresClientFactory postgresClientFactory = new PostgresClientFactory(Vertx.vertx()); + @Mock + InvoiceEventsDao invoiceEventsDao = new InvoiceEventsDaoImpl(postgresClientFactory); + + @InjectMocks + InvoiceAuditEventsServiceImpl invoiceAuditEventService = new InvoiceAuditEventsServiceImpl(invoiceEventsDao); + + @Test + void shouldCallDaoForSuccessfulCase() { + var invoiceAuditEvent = createInvoiceAuditEvent(UUID.randomUUID().toString()); + + Future> saveFuture = invoiceAuditEventService.saveInvoiceAuditEvent(invoiceAuditEvent, TENANT_ID); + saveFuture.onComplete(ar -> { + assertTrue(ar.succeeded()); + }); + } + + @Test + void shouldGetDto() { + String id = UUID.randomUUID().toString(); + var invoiceAuditEvent = createInvoiceAuditEvent(id); + + invoiceAuditEventService.saveInvoiceAuditEvent(invoiceAuditEvent, TENANT_ID); + + Future dto = invoiceAuditEventService.getAuditEventsByInvoiceId(id, "action_date", "asc", 1, 1, TENANT_ID); + dto.onComplete(ar -> { + InvoiceAuditEventCollection invoiceAuditEventOptional = ar.result(); + List invoiceAuditEventList = invoiceAuditEventOptional.getInvoiceAuditEvents(); + + assertEquals(invoiceAuditEventList.get(0).getId(), id); + assertEquals(InvoiceAuditEvent.Action.CREATE.value(), invoiceAuditEventList.get(0).getAction().value()); + }); + } +} 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 d359311f..ed4008b7 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 @@ -4,16 +4,18 @@ import java.util.UUID; import io.vertx.core.json.JsonObject; +import org.folio.rest.jaxrs.model.InvoiceAuditEvent; 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 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 INVOICE_ID = "3f29b1a4-8c2b-4d3a-9b1e-5f2a1b4c8d3a"; public static OrderAuditEvent createOrderAuditEvent(String id) { JsonObject jsonObject = new JsonObject(); @@ -96,4 +98,29 @@ public static PieceAuditEvent createPieceAuditEventWithoutSnapshot() { .withActionDate(new Date()) .withPieceSnapshot("Test"); } + + public static InvoiceAuditEvent createInvoiceAuditEvent(String id) { + JsonObject jsonObject = new JsonObject(); + jsonObject.put("name", "Test Invoice 123"); + + return new InvoiceAuditEvent() + .withId(id) + .withAction(InvoiceAuditEvent.Action.CREATE) + .withInvoiceId(UUID.randomUUID().toString()) + .withUserId(UUID.randomUUID().toString()) + .withEventDate(new Date()) + .withActionDate(new Date()) + .withInvoiceSnapshot(jsonObject); + } + + public static InvoiceAuditEvent createInvoiceAuditEventWithoutSnapshot() { + return new InvoiceAuditEvent() + .withId(UUID.randomUUID().toString()) + .withAction(InvoiceAuditEvent.Action.CREATE) + .withInvoiceId(UUID.randomUUID().toString()) + .withUserId(UUID.randomUUID().toString()) + .withEventDate(new Date()) + .withActionDate(new Date()) + .withInvoiceSnapshot("Test"); + } } diff --git a/pom.xml b/pom.xml index f2b3d62d..23f1cd6d 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 org.folio mod-audit - 2.10.1-SNAPSHOT + 3.0.0-SNAPSHOT pom diff --git a/ramls/acquisition-events.raml b/ramls/acquisition-events.raml index 8bdd0850..f173a8e3 100644 --- a/ramls/acquisition-events.raml +++ b/ramls/acquisition-events.raml @@ -17,6 +17,8 @@ types: 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 + invoice-audit-event: !include invoice_audit_event.json + invoice-audit-event-collection: !include invoice_audit_event_collection.json traits: searchable: !include raml-util/traits/searchable.raml @@ -159,3 +161,38 @@ traits: example: strict: false value: !include raml-util/examples/errors.sample + + /invoice/{id}: + get: + description: Get list of invoice events by invoice_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: invoice-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/invoice_audit_event.json b/ramls/invoice_audit_event.json new file mode 100644 index 00000000..ea5359ac --- /dev/null +++ b/ramls/invoice_audit_event.json @@ -0,0 +1,40 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Invoice audit event", + "type": "object", + "properties": { + "id": { + "description": "UUID of the event", + "$ref": "common/uuid.json" + }, + "action": { + "description": "Action for invoice (Create, Edit or Delete)", + "type": "string", + "$ref": "event_action.json" + }, + "invoiceId": { + "description": "UUID of the invoice", + "$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 invoice action occurred", + "format": "date-time", + "type": "string" + }, + "invoiceSnapshot": { + "description": "Full snapshot of the invoice", + "type": "object", + "javaType": "java.lang.Object" + } + }, + "additionalProperties": true +} diff --git a/ramls/invoice_audit_event_collection.json b/ramls/invoice_audit_event_collection.json new file mode 100644 index 00000000..75fce73b --- /dev/null +++ b/ramls/invoice_audit_event_collection.json @@ -0,0 +1,25 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Collection of invoiceAuditEvents", + "type": "object", + "additionalProperties": false, + "properties": { + "invoiceAuditEvents": { + "description": "List of invoiceAuditEvents", + "type": "array", + "id": "invoiceAuditEventsList", + "items": { + "type": "object", + "$ref": "invoice_audit_event.json" + } + }, + "totalItems": { + "description": "total records", + "type": "integer" + } + }, + "required": [ + "invoiceAuditEvents", + "totalItems" + ] +}