From 960987b6456041e70960af26b2b8839513902cc1 Mon Sep 17 00:00:00 2001 From: PBobylev Date: Wed, 2 Apr 2025 16:49:44 +0500 Subject: [PATCH 1/2] MODLD-686: Item and Holding sync prototype --- .../kafka/KafkaListenerConfiguration.java | 8 + .../InventoryDomainEventListener.java | 47 ++ src/main/resources/application.yml | 8 + .../resources/swagger.api/folio-modules.yaml | 2 + .../recordMetadata.json} | 0 .../storage/electronicAccessItem.json | 32 ++ .../inventory/storage/holdingsNote.json | 24 + .../storage/holdingsReceivingHistory.json | 21 + .../holdingsReceivingHistoryEntry.json | 22 + .../inventory/storage/holdingsRecord.json | 190 ++++++++ .../inventory/storage/holdingsStatement.json | 22 + .../storage/inventoryDomainEvent.json | 53 +++ .../storage/inventoryDomainEventPayload.json | 17 + .../inventory/storage/inventoryItem.json | 426 ++++++++++++++++++ .../storage/inventoryItemWithInstanceId.json | 17 + .../srs/record/sourceRecord.json | 2 +- .../srs/sourceRecordDomainEvent.json | 1 - .../InventoryDomainEventListenerIT.java | 49 ++ .../org/folio/linked/data/test/TestUtil.java | 2 + .../kafka/KafkaEventsTestDataFixture.java | 16 + .../samples/inventoryDomainEventHolding.json | 0 .../samples/inventoryDomainEventItem.json | 0 22 files changed, 957 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/folio/linked/data/integration/kafka/listener/InventoryDomainEventListener.java rename src/main/resources/swagger.api/folio-modules/{srs/record/sourceRecordMetadata.json => common/recordMetadata.json} (100%) create mode 100644 src/main/resources/swagger.api/folio-modules/inventory/storage/electronicAccessItem.json create mode 100644 src/main/resources/swagger.api/folio-modules/inventory/storage/holdingsNote.json create mode 100644 src/main/resources/swagger.api/folio-modules/inventory/storage/holdingsReceivingHistory.json create mode 100644 src/main/resources/swagger.api/folio-modules/inventory/storage/holdingsReceivingHistoryEntry.json create mode 100644 src/main/resources/swagger.api/folio-modules/inventory/storage/holdingsRecord.json create mode 100644 src/main/resources/swagger.api/folio-modules/inventory/storage/holdingsStatement.json create mode 100644 src/main/resources/swagger.api/folio-modules/inventory/storage/inventoryDomainEvent.json create mode 100644 src/main/resources/swagger.api/folio-modules/inventory/storage/inventoryDomainEventPayload.json create mode 100644 src/main/resources/swagger.api/folio-modules/inventory/storage/inventoryItem.json create mode 100644 src/main/resources/swagger.api/folio-modules/inventory/storage/inventoryItemWithInstanceId.json create mode 100644 src/test/java/org/folio/linked/data/integration/kafka/listener/InventoryDomainEventListenerIT.java create mode 100644 src/test/resources/samples/inventoryDomainEventHolding.json create mode 100644 src/test/resources/samples/inventoryDomainEventItem.json diff --git a/src/main/java/org/folio/linked/data/configuration/kafka/KafkaListenerConfiguration.java b/src/main/java/org/folio/linked/data/configuration/kafka/KafkaListenerConfiguration.java index ab1848afa..1cd957caf 100644 --- a/src/main/java/org/folio/linked/data/configuration/kafka/KafkaListenerConfiguration.java +++ b/src/main/java/org/folio/linked/data/configuration/kafka/KafkaListenerConfiguration.java @@ -9,6 +9,7 @@ import java.util.Map; import lombok.RequiredArgsConstructor; import org.apache.kafka.common.serialization.StringDeserializer; +import org.folio.linked.data.domain.dto.InventoryDomainEvent; import org.folio.linked.data.domain.dto.InventoryInstanceEvent; import org.folio.linked.data.domain.dto.SourceRecordDomainEvent; import org.folio.spring.tools.kafka.FolioKafkaProperties; @@ -61,6 +62,13 @@ public ConsumerFactory inventoryInstanceEventCon return errorHandlingConsumerFactory(InventoryInstanceEvent.class); } + @Bean + public ConcurrentKafkaListenerContainerFactory inventoryDomainEventListenerFactory( + ConsumerFactory inventoryDomainEventListenerFactory + ) { + return concurrentKafkaBatchListenerContainerFactory(inventoryDomainEventListenerFactory); + } + private ConcurrentKafkaListenerContainerFactory concurrentKafkaBatchListenerContainerFactory( ConsumerFactory consumerFactory) { var factory = new ConcurrentKafkaListenerContainerFactory(); diff --git a/src/main/java/org/folio/linked/data/integration/kafka/listener/InventoryDomainEventListener.java b/src/main/java/org/folio/linked/data/integration/kafka/listener/InventoryDomainEventListener.java new file mode 100644 index 000000000..c1f9c869a --- /dev/null +++ b/src/main/java/org/folio/linked/data/integration/kafka/listener/InventoryDomainEventListener.java @@ -0,0 +1,47 @@ +package org.folio.linked.data.integration.kafka.listener; + +import static org.folio.linked.data.util.Constants.STANDALONE_PROFILE; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.folio.linked.data.domain.dto.InventoryDomainEvent; +import org.springframework.context.annotation.Profile; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.stereotype.Component; + +@Log4j2 +@Component +@RequiredArgsConstructor +@Profile("!" + STANDALONE_PROFILE) +public class InventoryDomainEventListener { + + private static final String ITEM_DOMAIN_EVENT_LISTENER = "mod-linked-data-inventory-item-domain-event-listener"; + private static final String HOLDING_DOMAIN_EVENT_LISTENER = "mod-linked-data-inventory-holding-domain-event-listener"; + + @KafkaListener( + id = ITEM_DOMAIN_EVENT_LISTENER, + containerFactory = "inventoryDomainEventListenerFactory", + groupId = "#{folioKafkaProperties.listener['inventory-item-domain-event'].groupId}", + concurrency = "#{folioKafkaProperties.listener['inventory-item-domain-event'].concurrency}", + topicPattern = "#{folioKafkaProperties.listener['inventory-item-domain-event'].topicPattern}") + public void handleInventoryItemDomainEvent(List> consumerRecords) { + consumerRecords.forEach(cr -> processRecord(cr, "Item")); + } + + @KafkaListener( + id = HOLDING_DOMAIN_EVENT_LISTENER, + containerFactory = "inventoryDomainEventListenerFactory", + groupId = "#{folioKafkaProperties.listener['inventory-holding-domain-event'].groupId}", + concurrency = "#{folioKafkaProperties.listener['inventory-holding-domain-event'].concurrency}", + topicPattern = "#{folioKafkaProperties.listener['inventory-holding-domain-event'].topicPattern}") + public void handleInventoryHoldingDomainEvent(List> consumerRecords) { + consumerRecords.forEach(cr -> processRecord(cr, "Holding")); + } + + private void processRecord(ConsumerRecord event, String entityType) { + log.info("Received [{}] Domain Event: [key {}], value [{}]", entityType, event.key(), event.value()); + } + +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 696ff72d4..258e9f8f3 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -65,6 +65,14 @@ folio: concurrency: ${KAFKA_INVENTORY_INSTANCE_EVENT_CONCURRENCY:1} topic-pattern: ${KAFKA_INVENTORY_INSTANCE_EVENT_TOPIC_PATTERN:(${folio.environment}\.)(.*\.)inventory.instance} group-id: ${folio.environment}-linked-data-inventory-instance-event-group + inventory-item-domain-event: + concurrency: ${KAFKA_INVENTORY_ITEM_DOMAIN_EVENT_CONCURRENCY:1} + topic-pattern: ${KAFKA_INVENTORY_ITEM_DOMAIN_EVENT_TOPIC_PATTERN:(${folio.environment}\.)(.*\.)inventory.item} + group-id: ${folio.environment}-linked-data-inventory-item-domain-event-group + inventory-holding-domain-event: + concurrency: ${KAFKA_INVENTORY_HOLDING_DOMAIN_EVENT_CONCURRENCY:1} + topic-pattern: ${KAFKA_INVENTORY_HOLDING_DOMAIN_EVENT_TOPIC_PATTERN:(${folio.environment}\.)(.*\.)inventory.holdings-record} + group-id: ${folio.environment}-linked-data-inventory-holding-domain-event-group retry-interval-ms: ${KAFKA_RETRY_INTERVAL_MS:2000} retry-delivery-attempts: ${KAFKA_RETRY_DELIVERY_ATTEMPTS:6} topics: diff --git a/src/main/resources/swagger.api/folio-modules.yaml b/src/main/resources/swagger.api/folio-modules.yaml index b86a52c40..4c75073e7 100644 --- a/src/main/resources/swagger.api/folio-modules.yaml +++ b/src/main/resources/swagger.api/folio-modules.yaml @@ -26,3 +26,5 @@ components: $ref: folio-modules/srs/sourceRecordDomainEvent.json inventoryInstanceEvent: $ref: folio-modules/inventory/inventoryInstanceEvent.json + inventoryDomainEvent: + $ref: folio-modules/inventory/storage/inventoryDomainEvent.json diff --git a/src/main/resources/swagger.api/folio-modules/srs/record/sourceRecordMetadata.json b/src/main/resources/swagger.api/folio-modules/common/recordMetadata.json similarity index 100% rename from src/main/resources/swagger.api/folio-modules/srs/record/sourceRecordMetadata.json rename to src/main/resources/swagger.api/folio-modules/common/recordMetadata.json diff --git a/src/main/resources/swagger.api/folio-modules/inventory/storage/electronicAccessItem.json b/src/main/resources/swagger.api/folio-modules/inventory/storage/electronicAccessItem.json new file mode 100644 index 000000000..ce336ee26 --- /dev/null +++ b/src/main/resources/swagger.api/folio-modules/inventory/storage/electronicAccessItem.json @@ -0,0 +1,32 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Electronic access item", + "additionalProperties": false, + "type": "object", + "properties": { + "uri": { + "type": "string", + "description": "uniform resource identifier (URI) is a string of characters designed for unambiguous identification of resources" + }, + "linkText": { + "type": "string", + "description": "the value of the MARC tag field 856 2nd indicator, where the values are: no information provided, resource, version of resource, related resource, no display constant generated" + }, + "materialsSpecification": { + "type": "string", + "description": "materials specified is used to specify to what portion or aspect of the resource the electronic location and access information applies (e.g. a portion or subset of the item is electronic, or a related electronic resource is being linked to the record)" + }, + "publicNote": { + "type": "string", + "description": "URL public note to be displayed in the discovery" + }, + "relationshipId": { + "type": "string", + "description": "relationship between the electronic resource at the location identified and the item described in the record as a whole" + } + }, + "required": [ + "uri" + ] +} + diff --git a/src/main/resources/swagger.api/folio-modules/inventory/storage/holdingsNote.json b/src/main/resources/swagger.api/folio-modules/inventory/storage/holdingsNote.json new file mode 100644 index 000000000..d85dd5700 --- /dev/null +++ b/src/main/resources/swagger.api/folio-modules/inventory/storage/holdingsNote.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "A holdings record note", + "javaType": "org.folio.rest.jaxrs.model.HoldingsNote", + "additionalProperties": false, + "type": "object", + "properties": { + "holdingsNoteTypeId": { + "type": "string", + "description": "ID of the type of note", + "$ref": "../../common/uuid.json" + }, + "note": { + "type": "string", + "description": "Text content of the note" + }, + "staffOnly": { + "type": "boolean", + "description": "If true, determines that the note should not be visible for others than staff", + "default": false + } + } +} + diff --git a/src/main/resources/swagger.api/folio-modules/inventory/storage/holdingsReceivingHistory.json b/src/main/resources/swagger.api/folio-modules/inventory/storage/holdingsReceivingHistory.json new file mode 100644 index 000000000..0d09ce6da --- /dev/null +++ b/src/main/resources/swagger.api/folio-modules/inventory/storage/holdingsReceivingHistory.json @@ -0,0 +1,21 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Receiving history of holdings record", + "javaType": "org.folio.rest.jaxrs.model.HoldingsReceivingHistory", + "additionalProperties": false, + "type": "object", + "properties": { + "displayType": { + "type": "string", + "description": "Display hint. 1: Display fields separately. 2: Display fields concatenated" + }, + "entries": { + "type": "array", + "description": "Entries of receiving history", + "items": { + "$ref": "holdingsReceivingHistoryEntry.json" + } + } + } +} + diff --git a/src/main/resources/swagger.api/folio-modules/inventory/storage/holdingsReceivingHistoryEntry.json b/src/main/resources/swagger.api/folio-modules/inventory/storage/holdingsReceivingHistoryEntry.json new file mode 100644 index 000000000..8117951c1 --- /dev/null +++ b/src/main/resources/swagger.api/folio-modules/inventory/storage/holdingsReceivingHistoryEntry.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Receiving history entry of holdings record", + "javaType": "org.folio.rest.jaxrs.model.HoldingsReceivingHistoryEntry", + "additionalProperties": false, + "type": "object", + "properties": { + "publicDisplay": { + "type": "boolean", + "description": "Defines if the receivingHistory should be visible to the public." + }, + "enumeration": { + "type": "string", + "description": "This is the volume/issue number (e.g. v.71:no.6-2)" + }, + "chronology": { + "type": "string", + "description": "Repeated element from Receiving history - Enumeration AND Receiving history - Chronology" + } + } +} + diff --git a/src/main/resources/swagger.api/folio-modules/inventory/storage/holdingsRecord.json b/src/main/resources/swagger.api/folio-modules/inventory/storage/holdingsRecord.json new file mode 100644 index 000000000..96e79a23a --- /dev/null +++ b/src/main/resources/swagger.api/folio-modules/inventory/storage/holdingsRecord.json @@ -0,0 +1,190 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "A holdings record", + "javaType": "org.folio.rest.jaxrs.model.HoldingsRecord", + "additionalProperties": false, + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "the unique ID of the holdings record; UUID", + "$ref": "../../common/uuid.json" + }, + "_version": { + "type": "integer", + "description": "Record version for optimistic locking" + }, + "sourceId": { + "description": "(A reference to) the source of a holdings record", + "type": "string", + "$ref": "../../common/uuid.json" + }, + "hrid": { + "type": "string", + "description": "the human readable ID, also called eye readable ID. A system-assigned sequential ID which maps to the Instance ID" + }, + "holdingsTypeId": { + "type": "string", + "description": "unique ID for the type of this holdings record, a UUID", + "$ref": "../../common/uuid.json" + }, + "formerIds": { + "type": "array", + "description": "Previous ID(s) assigned to the holdings record", + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "instanceId": { + "description": "Inventory instances identifier", + "type": "string", + "$ref": "../../common/uuid.json" + }, + "permanentLocationId": { + "type": "string", + "description": "The permanent shelving location in which an item resides.", + "$ref": "../../common/uuid.json" + }, + "temporaryLocationId": { + "type": "string", + "description": "Temporary location is the temporary location, shelving location, or holding which is a physical place where items are stored, or an Online location.", + "$ref": "../../common/uuid.json" + }, + "effectiveLocationId": { + "type": "string", + "description": "Effective location is calculated by the system based on the values in the permanent and temporary locationId fields.", + "$ref": "../../common/uuid.json" + }, + "electronicAccess": { + "description": "List of electronic access items", + "type": "array", + "items": { + "type": "object", + "$ref": "electronicAccessItem.json" + } + }, + "callNumberTypeId": { + "type": "string", + "description": "unique ID for the type of call number on a holdings record, a UUID", + "$ref": "../../common/uuid.json" + }, + "callNumberPrefix": { + "type": "string", + "description": "Prefix of the call number on the holding level." + }, + "callNumber": { + "type": "string", + "description": "Call Number is an identifier assigned to an item, usually printed on a label attached to the item." + }, + "callNumberSuffix": { + "type": "string", + "description": "Suffix of the call number on the holding level." + }, + "shelvingTitle": { + "type": "string", + "description": "Indicates the shelving form of title." + }, + "acquisitionFormat": { + "type": "string", + "description": "Format of holdings record acquisition" + }, + "acquisitionMethod": { + "type": "string", + "description": "Method of holdings record acquisition" + }, + "receiptStatus": { + "type": "string", + "description": "Receipt status (e.g. pending, awaiting receipt, partially received, fully received, receipt not required, and cancelled)" + }, + "administrativeNotes":{ + "type": "array", + "description": "Administrative notes", + "minItems": 0, + "items": { + "type": "string" + } + }, + "notes": { + "type": "array", + "description": "Notes about action, copy, binding etc.", + "items": { + "type": "object", + "$ref": "holdingsNote.json" + } + }, + "illPolicyId": { + "type": "string", + "description": "unique ID for an ILL policy, a UUID", + "$ref": "../../common/uuid.json" + }, + "retentionPolicy": { + "type": "string", + "description": "Records information regarding how long we have agreed to keep something." + }, + "digitizationPolicy": { + "description": "Records information regarding digitization aspects.", + "type": "string" + }, + "holdingsStatements": { + "description": "Holdings record statements", + "type": "array", + "items": { + "type": "object", + "$ref": "holdingsStatement.json" + } + }, + "holdingsStatementsForIndexes": { + "description": "Holdings record indexes statements", + "type": "array", + "items": { + "type": "object", + "$ref": "holdingsStatement.json" + } + }, + "holdingsStatementsForSupplements": { + "description": "Holdings record supplements statements", + "type": "array", + "items": { + "type": "object", + "$ref": "holdingsStatement.json" + } + }, + "copyNumber": { + "type": "string", + "description": "Item/Piece ID (usually barcode) for systems that do not use item records. Ability to designate the copy number if institution chooses to use copy numbers." + }, + "numberOfItems": { + "type": "string", + "description": "Text (Number)" + }, + "receivingHistory": { + "description": "Receiving history of holdings record", + "$ref": "holdingsReceivingHistory.json" + }, + "discoverySuppress": { + "type": "boolean", + "description": "records the fact that the record should not be displayed in a discovery system" + }, + "statisticalCodeIds": { + "type": "array", + "description": "List of statistical code IDs", + "items": { + "type": "string", + "$ref": "../../common/uuid.json" + }, + "uniqueItems": true + }, + "metadata": { + "type": "object", + "$ref": "../../common/recordMetadata.json", + "readonly": true + } + }, + "required": [ + "sourceId", + "instanceId", + "permanentLocationId" + ] +} + diff --git a/src/main/resources/swagger.api/folio-modules/inventory/storage/holdingsStatement.json b/src/main/resources/swagger.api/folio-modules/inventory/storage/holdingsStatement.json new file mode 100644 index 000000000..3220746e9 --- /dev/null +++ b/src/main/resources/swagger.api/folio-modules/inventory/storage/holdingsStatement.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Holdings record statement", + "javaType": "org.folio.rest.jaxrs.model.HoldingsStatement", + "additionalProperties": false, + "type": "object", + "properties": { + "statement": { + "type": "string", + "description": "Specifies the exact content to which the library has access, typically for continuing publications." + }, + "note": { + "type": "string", + "description": "Note attached to a holdings statement" + }, + "staffNote": { + "type": "string", + "description": "Private note attached to a holdings statement" + } + } +} + diff --git a/src/main/resources/swagger.api/folio-modules/inventory/storage/inventoryDomainEvent.json b/src/main/resources/swagger.api/folio-modules/inventory/storage/inventoryDomainEvent.json new file mode 100644 index 000000000..f3b7a5aeb --- /dev/null +++ b/src/main/resources/swagger.api/folio-modules/inventory/storage/inventoryDomainEvent.json @@ -0,0 +1,53 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Inventory domain event data model", + "type": "object", + "additionalProperties": false, + "properties": { + "eventId": { + "description": "UUID", + "$ref": "../../common/uuid.json" + }, + "eventTs": { + "description": "Event timestamp", + "type": "string" + }, + "oldEntity": { + "description": "Old entity", + "$ref": "inventoryDomainEventPayload.json" + }, + "newEntity": { + "description": "New entity", + "$ref": "inventoryDomainEventPayload.json" + }, + "type": { + "type": "string", + "enum": [ + "UPDATE", + "DELETE", + "CREATE", + "DELETE_ALL", + "REINDEX", + "ITERATE", + "MIGRATION" + ], + "description": "Inventory domain event type" + }, + "eventMetadata": { + "type": "object", + "$ref": "../../common/eventMetadata.json", + "description": "Event metadata" + }, + "tenant": { + "type": "string", + "description": "Tenant id" + } + }, + "excludedFromEqualsAndHashCode": [ + "eventMetadata" + ], + "required": [ + "id", + "eventType" + ] +} diff --git a/src/main/resources/swagger.api/folio-modules/inventory/storage/inventoryDomainEventPayload.json b/src/main/resources/swagger.api/folio-modules/inventory/storage/inventoryDomainEventPayload.json new file mode 100644 index 000000000..68f833bd4 --- /dev/null +++ b/src/main/resources/swagger.api/folio-modules/inventory/storage/inventoryDomainEventPayload.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Inventory Domain Event payload", + "type": "object", + "additionalProperties": false, + "oneOf": [ + { + "$ref": "inventoryItemWithInstanceId.json" + }, + { + "$ref": "inventoryItem.json" + }, + { + "$ref": "holdingsRecord.json" + } + ] +} diff --git a/src/main/resources/swagger.api/folio-modules/inventory/storage/inventoryItem.json b/src/main/resources/swagger.api/folio-modules/inventory/storage/inventoryItem.json new file mode 100644 index 000000000..00c02ad7e --- /dev/null +++ b/src/main/resources/swagger.api/folio-modules/inventory/storage/inventoryItem.json @@ -0,0 +1,426 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "An item record", + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique ID of the item record" + }, + "_version": { + "type": "integer", + "description": "Record version for optimistic locking" + }, + "hrid": { + "type": "string", + "description": "The human readable ID, also called eye readable ID. A system-assigned sequential alternate ID" + }, + "holdingsRecordId": { + "type": "string", + "description": "ID of the holdings record the item is a member of." + }, + "formerIds": { + "type": "array", + "description": "Previous identifiers assigned to the item", + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "discoverySuppress": { + "type": "boolean", + "description": "Records the fact that the record should not be displayed in a discovery system" + }, + "displaySummary": { + "description": "Display summary about the item", + "type": "string" + }, + "accessionNumber": { + "type": "string", + "description": "Also called inventar number" + }, + "barcode": { + "type": "string", + "description": "Unique inventory control number for physical resources, used largely for circulation purposes" + }, + "effectiveShelvingOrder": { + "type": "string", + "description": "A system generated normalization of the call number that allows for call number sorting in reports and search results", + "readonly": true + }, + "itemLevelCallNumber": { + "type": "string", + "description": "Call Number is an identifier assigned to an item, usually printed on a label attached to the item. The call number is used to determine the items physical position in a shelving sequence, e.g. K1 .M44. The Item level call number, is the call number on item level." + }, + "itemLevelCallNumberPrefix": { + "type": "string", + "description": "Prefix of the call number on the item level." + }, + "itemLevelCallNumberSuffix": { + "type": "string", + "description": "Suffix of the call number on the item level." + }, + "itemLevelCallNumberTypeId": { + "type": "string", + "description": "Identifies the source of the call number, e.g., LCC, Dewey, NLM, etc." + }, + "effectiveCallNumberComponents": { + "type": "object", + "description": "Elements of a full call number generated from the item or holding", + "properties": { + "callNumber": { + "type": "string", + "description": "Effective Call Number is an identifier assigned to an item or its holding and associated with the item.", + "readonly": true + }, + "prefix": { + "type": "string", + "description": "Effective Call Number Prefix is the prefix of the identifier assigned to an item or its holding and associated with the item.", + "readonly": true + }, + "suffix": { + "type": "string", + "description": "Effective Call Number Suffix is the suffix of the identifier assigned to an item or its holding and associated with the item.", + "readonly": true + }, + "typeId": { + "type": "string", + "description": "Effective Call Number Type Id is the call number type id of the item, if available, otherwise that of the holding.", + "$ref": "../../common/uuid.json", + "readonly": true + } + }, + "additionalProperties": false + }, + "volume": { + "type": "string", + "description": "Volume is intended for monographs when a multipart monograph (e.g. a biography of George Bernard Shaw in three volumes)." + }, + "enumeration": { + "type": "string", + "description": "Enumeration is the descriptive information for the numbering scheme of a serial." + }, + "chronology": { + "type": "string", + "description": "Chronology is the descriptive information for the dating scheme of a serial." + }, + "yearCaption": { + "type": "array", + "description": "In multipart monographs, a caption is a character(s) used to label a level of chronology, e.g., year 1985.", + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "itemIdentifier": { + "type": "string", + "description": "Item identifier number, e.g. imported from the union catalogue (read only)." + }, + "copyNumber": { + "type": "string", + "description": "Copy number is the piece identifier. The copy number reflects if the library has a copy of a single-volume monograph; one copy of a multi-volume, (e.g. Copy 1, or C.7.)" + }, + "numberOfPieces": { + "type": "string", + "description": "Number of pieces. Used when an item is checked out or returned to verify that all parts are present (e.g. 7 CDs in a set)." + }, + "descriptionOfPieces": { + "description": "Description of item pieces.", + "type": "string" + }, + "numberOfMissingPieces": { + "type": "string", + "description": "Number of missing pieces." + }, + "missingPieces": { + "type": "string", + "description": "Description of the missing pieces. " + }, + "missingPiecesDate": { + "type": "string", + "description": "Date when the piece(s) went missing." + }, + "itemDamagedStatusId": { + "description": "Item dame status id identifier.", + "type": "string" + }, + "itemDamagedStatusDate": { + "description": "Date and time when the item was damaged.", + "type": "string" + }, + "administrativeNotes":{ + "type": "array", + "description": "Administrative notes", + "minItems": 0, + "items": { + "type": "string" + } + }, + "notes": { + "type": "array", + "description": "Notes about action, copy, binding etc.", + "items": { + "type": "object", + "additionalProperties": false, + "javaType": "org.folio.rest.jaxrs.model.ItemNote", + "properties": { + "itemNoteTypeId": { + "type": "string", + "description": "ID of the type of note" + }, + "itemNoteType": { + "description": "Type of item's note", + "type": "object", + "folio:$ref": "itemnotetype.json", + "javaType": "org.folio.rest.jaxrs.model.itemNoteTypeVirtual", + "readonly": true, + "folio:isVirtual": true, + "folio:linkBase": "item-note-types", + "folio:linkFromField": "itemNoteTypeId", + "folio:linkToField": "id", + "folio:includedElement": "itemNoteTypes.0" + }, + "note": { + "type": "string", + "description": "Text content of the note" + }, + "staffOnly": { + "type": "boolean", + "description": "If true, determines that the note should not be visible for others than staff", + "default": false + } + } + } + }, + "circulationNotes": { + "type": "array", + "description": "Notes to be displayed in circulation processes", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The id of the circulation note" + }, + "noteType": { + "type": "string", + "description": "Type of circulation process that the note applies to", + "enum": ["Check in", "Check out"] + }, + "note": { + "type": "string", + "description": "Text to display" + }, + "source": { + "type": "object", + "description": "The user who added/updated the note. The property is generated by the server and needed to support sorting. Points to /users/{id} resource.", + "properties": { + "id": { + "type": "string", + "description": "The id of the user who added/updated the note. The user information is generated by the server and needed to support sorting. Points to /users/{id} resource." + }, + "personal": { + "type": "object", + "description": "Personal information about the user", + "properties": { + "lastName": { + "description": "The user's last name", + "type": "string" + }, + "firstName": { + "description": "The user's first name", + "type": "string" + } + } + } + } + }, + "date": { + "type": "string", + "description": "Date and time the record is added/updated. The property is generated by the server and needed to support sorting." + }, + "staffOnly": { + "type": "boolean", + "description": "Flag to restrict display of this note", + "default": false + } + }, + "additionalProperties": false + } + }, + "status": { + "description": "The status of the item", + "type": "object", + "properties": { + "name": { + "description": "Name of the status e.g. Available, Checked out, In transit", + "type": "string", + "enum": [ + "Aged to lost", + "Available", + "Awaiting pickup", + "Awaiting delivery", + "Checked out", + "Claimed returned", + "Declared lost", + "In process", + "In process (non-requestable)", + "In transit", + "Intellectual item", + "Long missing", + "Lost and paid", + "Missing", + "On order", + "Paged", + "Restricted", + "Order closed", + "Unavailable", + "Unknown", + "Withdrawn" + ] + }, + "date": { + "description": "Date and time when the status was last changed", + "type": "string", + "format": "date-time", + "readonly": true + } + }, + "required": ["name"], + "additionalProperties": false + }, + "materialTypeId": { + "type": "string", + "description": "Material type, term. Define what type of thing the item is." + }, + "permanentLoanTypeId": { + "type": "string", + "description": "The permanent loan type, is the default loan type for a given item. Loan types are tenant-defined." + }, + "temporaryLoanTypeId": { + "type": "string", + "description": "Temporary loan type, is the temporary loan type for a given item." + }, + "permanentLocationId": { + "type": "string", + "description": "Permanent item location is the default location, shelving location, or holding which is a physical place where items are stored, or an Online location." + }, + "permanentLocation": { + "description": "The permanent shelving location in which an item resides", + "type": "object", + "folio:$ref": "location.json", + "readonly": true, + "folio:isVirtual": true, + "folio:linkBase": "locations", + "folio:linkFromField": "permanentLocationId", + "folio:linkToField": "id", + "folio:includedElement": "locations.0" + }, + "temporaryLocationId": { + "type": "string", + "description": "Temporary item location is the temporarily location, shelving location, or holding which is a physical place where items are stored, or an Online location." + }, + "temporaryLocation": { + "description": "Temporary location, shelving location, or holding which is a physical place where items are stored, or an Online location", + "type": "object", + "folio:$ref": "location.json", + "readonly": true, + "folio:isVirtual": true, + "folio:linkBase": "locations", + "folio:linkFromField": "temporaryLocationId", + "folio:linkToField": "id", + "folio:includedElement": "locations.0" + }, + "effectiveLocationId": { + "type": "string", + "description": "Read only current home location for the item.", + "$ref": "../../common/uuid.json", + "readonly": true + }, + "electronicAccess": { + "type": "array", + "description": "References for accessing the item by URL.", + "items": { + "type": "object", + "properties": { + "uri": { + "type": "string", + "description": "uniform resource identifier (URI) is a string of characters designed for unambiguous identification of resources" + }, + "linkText": { + "type": "string", + "description": "the value of the MARC tag field 856 2nd indicator, where the values are: no information provided, resource, version of resource, related resource, no display constant generated" + }, + "materialsSpecification": { + "type": "string", + "description": "materials specified is used to specify to what portion or aspect of the resource the electronic location and access information applies (e.g. a portion or subset of the item is electronic, or a related electronic resource is being linked to the record)" + }, + "publicNote": { + "type": "string", + "description": "URL public note to be displayed in the discovery" + }, + "relationshipId": { + "type": "string", + "description": "relationship between the electronic resource at the location identified and the item described in the record as a whole" + } + }, + "additionalProperties": false, + "required": [ + "uri" + ] + } + }, + "inTransitDestinationServicePointId": { + "description": "Service point an item is intended to be transited to (should only be present when in transit)", + "type": "string", + "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" + }, + "statisticalCodeIds": { + "type": "array", + "description": "List of statistical code IDs", + "items": { + "type": "string", + "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[1-5][a-fA-F0-9]{3}-[89abAB][a-fA-F0-9]{3}-[a-fA-F0-9]{12}$" + }, + "uniqueItems": true + }, + "purchaseOrderLineIdentifier": { + "type": "string", + "description": "ID referencing a remote purchase order object related to this item" + }, + "metadata": { + "type": "object", + "$ref": "../../common/recordMetadata.json", + "readonly": true + }, + "lastCheckIn": { + "type": "object", + "additionalProperties": false, + "description": "Information about when an item was last scanned in the Inventory app.", + "properties": { + "dateTime": { + "type": "string", + "description": "Date and time of the last check in of the item.", + "format": "date-time" + }, + "servicePointId": { + "type": "string", + "description": "Service point ID being used by a staff member when item was scanned in Check in app.", + "$ref": "../../common/uuid.json" + }, + "staffMemberId": { + "type": "string", + "description": "ID a staff member who scanned the item", + "$ref": "../../common/uuid.json" + } + } + } + }, + "additionalProperties": false, + "required": [ + "materialTypeId", + "permanentLoanTypeId", + "holdingsRecordId", + "status" + ] +} diff --git a/src/main/resources/swagger.api/folio-modules/inventory/storage/inventoryItemWithInstanceId.json b/src/main/resources/swagger.api/folio-modules/inventory/storage/inventoryItemWithInstanceId.json new file mode 100644 index 000000000..d7c8069ff --- /dev/null +++ b/src/main/resources/swagger.api/folio-modules/inventory/storage/inventoryItemWithInstanceId.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Inventory Item with InstanceId", + "type": "object", + "additionalProperties": false, + "properties": { + "instanceId": { + "description": "Inventory Instance ID", + "type": "string" + }, + "item": { + "type": "object", + "$ref": "inventoryItem.json", + "description": "Inventory Item" + } + } +} diff --git a/src/main/resources/swagger.api/folio-modules/srs/record/sourceRecord.json b/src/main/resources/swagger.api/folio-modules/srs/record/sourceRecord.json index a6f589034..869bbe52b 100644 --- a/src/main/resources/swagger.api/folio-modules/srs/record/sourceRecord.json +++ b/src/main/resources/swagger.api/folio-modules/srs/record/sourceRecord.json @@ -46,7 +46,7 @@ "metadata": { "description": "Metadata provided by the server", "type": "object", - "$ref": "sourceRecordMetadata.json", + "$ref": "../../common/recordMetadata.json", "readonly": true } }, diff --git a/src/main/resources/swagger.api/folio-modules/srs/sourceRecordDomainEvent.json b/src/main/resources/swagger.api/folio-modules/srs/sourceRecordDomainEvent.json index bd14c174f..4d2080ac7 100644 --- a/src/main/resources/swagger.api/folio-modules/srs/sourceRecordDomainEvent.json +++ b/src/main/resources/swagger.api/folio-modules/srs/sourceRecordDomainEvent.json @@ -1,7 +1,6 @@ { "$schema": "http://json-schema.org/draft-04/schema#", "description": "Source record domain event data model", - "javaType": "org.folio.rest.jaxrs.model.SourceRecordDomainEvent", "type": "object", "additionalProperties": false, "properties": { diff --git a/src/test/java/org/folio/linked/data/integration/kafka/listener/InventoryDomainEventListenerIT.java b/src/test/java/org/folio/linked/data/integration/kafka/listener/InventoryDomainEventListenerIT.java new file mode 100644 index 000000000..2bb26163f --- /dev/null +++ b/src/test/java/org/folio/linked/data/integration/kafka/listener/InventoryDomainEventListenerIT.java @@ -0,0 +1,49 @@ +package org.folio.linked.data.integration.kafka.listener; + +import static org.folio.linked.data.test.TestUtil.TENANT_ID; +import static org.folio.linked.data.test.kafka.KafkaEventsTestDataFixture.getInventoryDomainEventHoldingSampleProducerRecord; +import static org.folio.linked.data.test.kafka.KafkaEventsTestDataFixture.getInventoryDomainEventItemSampleProducerRecord; + +import org.folio.linked.data.e2e.base.IntegrationTest; +import org.folio.spring.tools.kafka.KafkaAdminService; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.kafka.core.KafkaTemplate; + +@IntegrationTest +class InventoryDomainEventListenerIT { + + @Autowired + private KafkaTemplate eventKafkaTemplate; + + @BeforeAll + static void setup(@Autowired KafkaAdminService kafkaAdminService) { + kafkaAdminService.createTopics(TENANT_ID); + } + + @Test + void shouldHandleItemDomainEvent() { + // given + var eventProducerRecord = getInventoryDomainEventItemSampleProducerRecord(); + + // when + eventKafkaTemplate.send(eventProducerRecord); + + // then + // check logs, that's enough for Spike + } + + @Test + void shouldHandleHoldingDomainEvent() { + // given + var eventProducerRecord = getInventoryDomainEventHoldingSampleProducerRecord(); + + // when + eventKafkaTemplate.send(eventProducerRecord); + + // then + // check logs, that's enough for Spike + } + +} diff --git a/src/test/java/org/folio/linked/data/test/TestUtil.java b/src/test/java/org/folio/linked/data/test/TestUtil.java index 7b6171096..de2eaa591 100644 --- a/src/test/java/org/folio/linked/data/test/TestUtil.java +++ b/src/test/java/org/folio/linked/data/test/TestUtil.java @@ -65,6 +65,8 @@ public class TestUtil { public static final String TENANT_ID = "test_tenant"; public static final String RECORD_DOMAIN_EVENT_TOPIC = "srs.source_records"; public static final String INVENTORY_INSTANCE_EVENT_TOPIC = "inventory.instance"; + public static final String INVENTORY_DOMAIN_EVENT_ITEM_TOPIC = "inventory.item"; + public static final String INVENTORY_DOMAIN_EVENT_HOLDING_TOPIC = "inventory.holdings-record"; public static final RequestProcessingExceptionBuilder EMPTY_EXCEPTION_BUILDER = new RequestProcessingExceptionBuilder(new ErrorResponseConfig()); public static final ObjectMapper OBJECT_MAPPER = new ObjectMapperConfig().objectMapper(EMPTY_EXCEPTION_BUILDER); diff --git a/src/test/java/org/folio/linked/data/test/kafka/KafkaEventsTestDataFixture.java b/src/test/java/org/folio/linked/data/test/kafka/KafkaEventsTestDataFixture.java index af405e890..b5a13046d 100644 --- a/src/test/java/org/folio/linked/data/test/kafka/KafkaEventsTestDataFixture.java +++ b/src/test/java/org/folio/linked/data/test/kafka/KafkaEventsTestDataFixture.java @@ -2,6 +2,8 @@ import static org.folio.linked.data.domain.dto.SourceRecordDomainEvent.EventTypeEnum; import static org.folio.linked.data.domain.dto.SourceRecordType.MARC_BIB; +import static org.folio.linked.data.test.TestUtil.INVENTORY_DOMAIN_EVENT_HOLDING_TOPIC; +import static org.folio.linked.data.test.TestUtil.INVENTORY_DOMAIN_EVENT_ITEM_TOPIC; import static org.folio.linked.data.test.TestUtil.INVENTORY_INSTANCE_EVENT_TOPIC; import static org.folio.linked.data.test.TestUtil.OBJECT_MAPPER; import static org.folio.linked.data.test.TestUtil.RECORD_DOMAIN_EVENT_TOPIC; @@ -37,6 +39,20 @@ public static ProducerRecord getSrsDomainEventSampleProducerReco return new ProducerRecord(topic, 0, "1", value, headers); } + public static ProducerRecord getInventoryDomainEventItemSampleProducerRecord() { + var topic = getTenantTopicName(INVENTORY_DOMAIN_EVENT_ITEM_TOPIC, TENANT_ID); + var value = TestUtil.loadResourceAsString("samples/inventoryDomainEventItem.json"); + var headers = new ArrayList<>(defaultKafkaHeaders()); + return new ProducerRecord(topic, 0, "1", value, headers); + } + + public static ProducerRecord getInventoryDomainEventHoldingSampleProducerRecord() { + var topic = getTenantTopicName(INVENTORY_DOMAIN_EVENT_HOLDING_TOPIC, TENANT_ID); + var value = TestUtil.loadResourceAsString("samples/inventoryDomainEventHolding.json"); + var headers = new ArrayList<>(defaultKafkaHeaders()); + return new ProducerRecord(topic, 0, "1", value, headers); + } + @SneakyThrows public static ProducerRecord getSrsDomainEventProducerRecord(String id, String marc, diff --git a/src/test/resources/samples/inventoryDomainEventHolding.json b/src/test/resources/samples/inventoryDomainEventHolding.json new file mode 100644 index 000000000..e69de29bb diff --git a/src/test/resources/samples/inventoryDomainEventItem.json b/src/test/resources/samples/inventoryDomainEventItem.json new file mode 100644 index 000000000..e69de29bb From a083216146e72c741653161c6dec2dff022ea312 Mon Sep 17 00:00:00 2001 From: PBobylev Date: Fri, 11 Apr 2025 16:44:55 +0500 Subject: [PATCH 2/2] MODLD-686: working proto --- .../kafka/KafkaListenerConfiguration.java | 26 ++++++-- .../InventoryDomainEventListener.java | 17 ++--- .../resources/swagger.api/folio-modules.yaml | 6 +- .../storage/inventoryDomainEventPayload.json | 17 ----- ....json => inventoryDomainHoldingEvent.json} | 10 +-- .../storage/inventoryDomainItemEvent.json | 53 ++++++++++++++++ .../inventory/storage/inventoryItem.json | 4 ++ .../storage/inventoryItemWithInstanceId.json | 17 ----- .../InventoryDomainEventListenerIT.java | 10 ++- src/test/resources/application-test.yml | 4 ++ .../samples/inventoryDomainEventHolding.json | 31 ++++++++++ .../samples/inventoryDomainEventItem.json | 62 +++++++++++++++++++ 12 files changed, 202 insertions(+), 55 deletions(-) delete mode 100644 src/main/resources/swagger.api/folio-modules/inventory/storage/inventoryDomainEventPayload.json rename src/main/resources/swagger.api/folio-modules/inventory/storage/{inventoryDomainEvent.json => inventoryDomainHoldingEvent.json} (83%) create mode 100644 src/main/resources/swagger.api/folio-modules/inventory/storage/inventoryDomainItemEvent.json delete mode 100644 src/main/resources/swagger.api/folio-modules/inventory/storage/inventoryItemWithInstanceId.json diff --git a/src/main/java/org/folio/linked/data/configuration/kafka/KafkaListenerConfiguration.java b/src/main/java/org/folio/linked/data/configuration/kafka/KafkaListenerConfiguration.java index 1cd957caf..09703995f 100644 --- a/src/main/java/org/folio/linked/data/configuration/kafka/KafkaListenerConfiguration.java +++ b/src/main/java/org/folio/linked/data/configuration/kafka/KafkaListenerConfiguration.java @@ -9,7 +9,8 @@ import java.util.Map; import lombok.RequiredArgsConstructor; import org.apache.kafka.common.serialization.StringDeserializer; -import org.folio.linked.data.domain.dto.InventoryDomainEvent; +import org.folio.linked.data.domain.dto.InventoryDomainHoldingEvent; +import org.folio.linked.data.domain.dto.InventoryDomainItemEvent; import org.folio.linked.data.domain.dto.InventoryInstanceEvent; import org.folio.linked.data.domain.dto.SourceRecordDomainEvent; import org.folio.spring.tools.kafka.FolioKafkaProperties; @@ -63,10 +64,27 @@ public ConsumerFactory inventoryInstanceEventCon } @Bean - public ConcurrentKafkaListenerContainerFactory inventoryDomainEventListenerFactory( - ConsumerFactory inventoryDomainEventListenerFactory + public ConsumerFactory holdingEventConsumerFactory() { + return errorHandlingConsumerFactory(InventoryDomainHoldingEvent.class); + } + + @Bean + public ConcurrentKafkaListenerContainerFactory holdingListenerFactory( + ConsumerFactory inventoryDomainEventConsumerFactory + ) { + return concurrentKafkaBatchListenerContainerFactory(inventoryDomainEventConsumerFactory); + } + + @Bean + public ConsumerFactory itemEventConsumerFactory() { + return errorHandlingConsumerFactory(InventoryDomainItemEvent.class); + } + + @Bean + public ConcurrentKafkaListenerContainerFactory itemListenerFactory( + ConsumerFactory inventoryDomainEventConsumerFactory ) { - return concurrentKafkaBatchListenerContainerFactory(inventoryDomainEventListenerFactory); + return concurrentKafkaBatchListenerContainerFactory(inventoryDomainEventConsumerFactory); } private ConcurrentKafkaListenerContainerFactory concurrentKafkaBatchListenerContainerFactory( diff --git a/src/main/java/org/folio/linked/data/integration/kafka/listener/InventoryDomainEventListener.java b/src/main/java/org/folio/linked/data/integration/kafka/listener/InventoryDomainEventListener.java index c1f9c869a..e2908fbb8 100644 --- a/src/main/java/org/folio/linked/data/integration/kafka/listener/InventoryDomainEventListener.java +++ b/src/main/java/org/folio/linked/data/integration/kafka/listener/InventoryDomainEventListener.java @@ -6,7 +6,8 @@ import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.apache.kafka.clients.consumer.ConsumerRecord; -import org.folio.linked.data.domain.dto.InventoryDomainEvent; +import org.folio.linked.data.domain.dto.InventoryDomainHoldingEvent; +import org.folio.linked.data.domain.dto.InventoryDomainItemEvent; import org.springframework.context.annotation.Profile; import org.springframework.kafka.annotation.KafkaListener; import org.springframework.stereotype.Component; @@ -22,26 +23,26 @@ public class InventoryDomainEventListener { @KafkaListener( id = ITEM_DOMAIN_EVENT_LISTENER, - containerFactory = "inventoryDomainEventListenerFactory", + containerFactory = "itemListenerFactory", groupId = "#{folioKafkaProperties.listener['inventory-item-domain-event'].groupId}", concurrency = "#{folioKafkaProperties.listener['inventory-item-domain-event'].concurrency}", topicPattern = "#{folioKafkaProperties.listener['inventory-item-domain-event'].topicPattern}") - public void handleInventoryItemDomainEvent(List> consumerRecords) { + public void handleInventoryItemDomainEvent(List> consumerRecords) { consumerRecords.forEach(cr -> processRecord(cr, "Item")); } @KafkaListener( id = HOLDING_DOMAIN_EVENT_LISTENER, - containerFactory = "inventoryDomainEventListenerFactory", + containerFactory = "holdingListenerFactory", groupId = "#{folioKafkaProperties.listener['inventory-holding-domain-event'].groupId}", concurrency = "#{folioKafkaProperties.listener['inventory-holding-domain-event'].concurrency}", topicPattern = "#{folioKafkaProperties.listener['inventory-holding-domain-event'].topicPattern}") - public void handleInventoryHoldingDomainEvent(List> consumerRecords) { - consumerRecords.forEach(cr -> processRecord(cr, "Holding")); + public void handleInventoryHoldingDomainEvent(List> records) { + records.forEach(cr -> processRecord(cr, "Holding")); } - private void processRecord(ConsumerRecord event, String entityType) { - log.info("Received [{}] Domain Event: [key {}], value [{}]", entityType, event.key(), event.value()); + private void processRecord(Object event, String entityType) { + log.info("Received [{}] Domain Event: {}", entityType, event); } } diff --git a/src/main/resources/swagger.api/folio-modules.yaml b/src/main/resources/swagger.api/folio-modules.yaml index 4c75073e7..5067d5533 100644 --- a/src/main/resources/swagger.api/folio-modules.yaml +++ b/src/main/resources/swagger.api/folio-modules.yaml @@ -26,5 +26,7 @@ components: $ref: folio-modules/srs/sourceRecordDomainEvent.json inventoryInstanceEvent: $ref: folio-modules/inventory/inventoryInstanceEvent.json - inventoryDomainEvent: - $ref: folio-modules/inventory/storage/inventoryDomainEvent.json + inventoryDomainHoldingEvent: + $ref: folio-modules/inventory/storage/inventoryDomainHoldingEvent.json + inventoryDomainItemEvent: + $ref: folio-modules/inventory/storage/inventoryDomainItemEvent.json diff --git a/src/main/resources/swagger.api/folio-modules/inventory/storage/inventoryDomainEventPayload.json b/src/main/resources/swagger.api/folio-modules/inventory/storage/inventoryDomainEventPayload.json deleted file mode 100644 index 68f833bd4..000000000 --- a/src/main/resources/swagger.api/folio-modules/inventory/storage/inventoryDomainEventPayload.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Inventory Domain Event payload", - "type": "object", - "additionalProperties": false, - "oneOf": [ - { - "$ref": "inventoryItemWithInstanceId.json" - }, - { - "$ref": "inventoryItem.json" - }, - { - "$ref": "holdingsRecord.json" - } - ] -} diff --git a/src/main/resources/swagger.api/folio-modules/inventory/storage/inventoryDomainEvent.json b/src/main/resources/swagger.api/folio-modules/inventory/storage/inventoryDomainHoldingEvent.json similarity index 83% rename from src/main/resources/swagger.api/folio-modules/inventory/storage/inventoryDomainEvent.json rename to src/main/resources/swagger.api/folio-modules/inventory/storage/inventoryDomainHoldingEvent.json index f3b7a5aeb..d4d7a8a3c 100644 --- a/src/main/resources/swagger.api/folio-modules/inventory/storage/inventoryDomainEvent.json +++ b/src/main/resources/swagger.api/folio-modules/inventory/storage/inventoryDomainHoldingEvent.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Inventory domain event data model", + "description": "Inventory Holding domain event data model", "type": "object", "additionalProperties": false, "properties": { @@ -12,13 +12,13 @@ "description": "Event timestamp", "type": "string" }, - "oldEntity": { + "old": { "description": "Old entity", - "$ref": "inventoryDomainEventPayload.json" + "$ref": "holdingsRecord.json" }, - "newEntity": { + "new": { "description": "New entity", - "$ref": "inventoryDomainEventPayload.json" + "$ref": "holdingsRecord.json" }, "type": { "type": "string", diff --git a/src/main/resources/swagger.api/folio-modules/inventory/storage/inventoryDomainItemEvent.json b/src/main/resources/swagger.api/folio-modules/inventory/storage/inventoryDomainItemEvent.json new file mode 100644 index 000000000..c0660e06d --- /dev/null +++ b/src/main/resources/swagger.api/folio-modules/inventory/storage/inventoryDomainItemEvent.json @@ -0,0 +1,53 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Inventory Item domain event data model", + "type": "object", + "additionalProperties": false, + "properties": { + "eventId": { + "description": "UUID", + "$ref": "../../common/uuid.json" + }, + "eventTs": { + "description": "Event timestamp", + "type": "string" + }, + "old": { + "description": "Old entity", + "$ref": "inventoryItem.json" + }, + "new": { + "description": "New entity", + "$ref": "inventoryItem.json" + }, + "type": { + "type": "string", + "enum": [ + "UPDATE", + "DELETE", + "CREATE", + "DELETE_ALL", + "REINDEX", + "ITERATE", + "MIGRATION" + ], + "description": "Inventory domain event type" + }, + "eventMetadata": { + "type": "object", + "$ref": "../../common/eventMetadata.json", + "description": "Event metadata" + }, + "tenant": { + "type": "string", + "description": "Tenant id" + } + }, + "excludedFromEqualsAndHashCode": [ + "eventMetadata" + ], + "required": [ + "id", + "eventType" + ] +} diff --git a/src/main/resources/swagger.api/folio-modules/inventory/storage/inventoryItem.json b/src/main/resources/swagger.api/folio-modules/inventory/storage/inventoryItem.json index 00c02ad7e..8f6ea713d 100644 --- a/src/main/resources/swagger.api/folio-modules/inventory/storage/inventoryItem.json +++ b/src/main/resources/swagger.api/folio-modules/inventory/storage/inventoryItem.json @@ -3,6 +3,10 @@ "description": "An item record", "type": "object", "properties": { + "instanceId": { + "description": "Inventory Instance ID", + "type": "string" + }, "id": { "type": "string", "description": "Unique ID of the item record" diff --git a/src/main/resources/swagger.api/folio-modules/inventory/storage/inventoryItemWithInstanceId.json b/src/main/resources/swagger.api/folio-modules/inventory/storage/inventoryItemWithInstanceId.json deleted file mode 100644 index d7c8069ff..000000000 --- a/src/main/resources/swagger.api/folio-modules/inventory/storage/inventoryItemWithInstanceId.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Inventory Item with InstanceId", - "type": "object", - "additionalProperties": false, - "properties": { - "instanceId": { - "description": "Inventory Instance ID", - "type": "string" - }, - "item": { - "type": "object", - "$ref": "inventoryItem.json", - "description": "Inventory Item" - } - } -} diff --git a/src/test/java/org/folio/linked/data/integration/kafka/listener/InventoryDomainEventListenerIT.java b/src/test/java/org/folio/linked/data/integration/kafka/listener/InventoryDomainEventListenerIT.java index 2bb26163f..b3fa3dce5 100644 --- a/src/test/java/org/folio/linked/data/integration/kafka/listener/InventoryDomainEventListenerIT.java +++ b/src/test/java/org/folio/linked/data/integration/kafka/listener/InventoryDomainEventListenerIT.java @@ -1,8 +1,11 @@ package org.folio.linked.data.integration.kafka.listener; import static org.folio.linked.data.test.TestUtil.TENANT_ID; +import static org.folio.linked.data.test.TestUtil.awaitAndAssert; import static org.folio.linked.data.test.kafka.KafkaEventsTestDataFixture.getInventoryDomainEventHoldingSampleProducerRecord; import static org.folio.linked.data.test.kafka.KafkaEventsTestDataFixture.getInventoryDomainEventItemSampleProducerRecord; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; import org.folio.linked.data.e2e.base.IntegrationTest; import org.folio.spring.tools.kafka.KafkaAdminService; @@ -10,12 +13,15 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; @IntegrationTest class InventoryDomainEventListenerIT { @Autowired private KafkaTemplate eventKafkaTemplate; + @MockitoSpyBean + private InventoryDomainEventListener inventoryDomainEventListener; @BeforeAll static void setup(@Autowired KafkaAdminService kafkaAdminService) { @@ -31,7 +37,7 @@ void shouldHandleItemDomainEvent() { eventKafkaTemplate.send(eventProducerRecord); // then - // check logs, that's enough for Spike + awaitAndAssert(() -> verify(inventoryDomainEventListener).handleInventoryItemDomainEvent(any())); } @Test @@ -43,7 +49,7 @@ void shouldHandleHoldingDomainEvent() { eventKafkaTemplate.send(eventProducerRecord); // then - // check logs, that's enough for Spike + awaitAndAssert(() -> verify(inventoryDomainEventListener).handleInventoryHoldingDomainEvent(any())); } } diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml index 86da61acc..88add8349 100644 --- a/src/test/resources/application-test.yml +++ b/src/test/resources/application-test.yml @@ -18,6 +18,10 @@ folio: num-partitions: 1 - name: inventory.instance num-partitions: 1 + - name: inventory.item + num-partitions: 1 + - name: inventory.holdings-record + num-partitions: 1 mod-linked-data: reindex: diff --git a/src/test/resources/samples/inventoryDomainEventHolding.json b/src/test/resources/samples/inventoryDomainEventHolding.json index e69de29bb..0428e20aa 100644 --- a/src/test/resources/samples/inventoryDomainEventHolding.json +++ b/src/test/resources/samples/inventoryDomainEventHolding.json @@ -0,0 +1,31 @@ +{ + "new": { + "id": "d48996b4-170e-43e4-8f4d-cafaa75e7b52", + "_version": 1, + "sourceId": "f32d531e-df79-46b3-8932-cdd35f7a2264", + "hrid": "oho00056184712", + "formerIds": [], + "instanceId": "c1ef011b-f97e-4ed0-aec3-636cd2f64107", + "permanentLocationId": "574326a1-f240-4222-82b3-21ccd6d6e5e5", + "effectiveLocationId": "574326a1-f240-4222-82b3-21ccd6d6e5e5", + "electronicAccess": [], + "callNumberTypeId": "95467209-6d7b-468b-94df-0f5d7ad2747d", + "callNumber": "1986", + "administrativeNotes": [], + "notes": [], + "holdingsStatements": [], + "holdingsStatementsForIndexes": [], + "holdingsStatementsForSupplements": [], + "statisticalCodeIds": [], + "metadata": { + "createdDate": "2025-04-11T11:39:26.882+00:00", + "createdByUserId": "cee695b3-8732-447c-90d2-534f1fa16991", + "updatedDate": "2025-04-11T11:39:26.882+00:00", + "updatedByUserId": "cee695b3-8732-447c-90d2-534f1fa16991" + } + }, + "type": "CREATE", + "tenant": "fs09000000", + "eventId": "38daa6bb-76cc-4492-a1de-80f3a4728182", + "eventTs": 1744371567012 +} \ No newline at end of file diff --git a/src/test/resources/samples/inventoryDomainEventItem.json b/src/test/resources/samples/inventoryDomainEventItem.json index e69de29bb..8369c603c 100644 --- a/src/test/resources/samples/inventoryDomainEventItem.json +++ b/src/test/resources/samples/inventoryDomainEventItem.json @@ -0,0 +1,62 @@ +{ + "new": { + "instanceId": "c1ef011b-f97e-4ed0-aec3-636cd2f64107", + "id": "bafdd67b-f8f5-45e7-971a-8ab0fb76b03a", + "_version": 1, + "hrid": "oit00160053757", + "holdingsRecordId": "d48996b4-170e-43e4-8f4d-cafaa75e7b52", + "formerIds": [], + "displaySummary": "Enum display summary", + "barcode": "Barcode", + "effectiveShelvingOrder": "41509 VOLUME VALUE ENUMERATION VALUE CHRONOLOGY VALUE", + "itemLevelCallNumber": "1509", + "effectiveCallNumberComponents": { + "callNumber": "1509", + "typeId": "95467209-6d7b-468b-94df-0f5d7ad2747d" + }, + "volume": "Volume value", + "enumeration": "Enumeration value", + "chronology": "Chronology value", + "yearCaption": [ + "Year, caption" + ], + "itemDamagedStatusId": "516b82eb-1f19-4a63-8c48-8f1a3e9ff311", + "itemDamagedStatusDate": "2025-04-11", + "administrativeNotes": [], + "notes": [ + { + "itemNoteTypeId": "f618d7d8-0ab2-40ed-b76a-9a6cff141643", + "note": "Action note", + "staffOnly": false + }, + { + "itemNoteTypeId": "2864ca78-7d78-4a90-8ff6-13a4e30128b4", + "note": "Note", + "staffOnly": false + } + ], + "circulationNotes": [], + "status": { + "name": "Available", + "date": "2025-04-11T11:44:08.630+00:00" + }, + "materialTypeId": "025ba2c5-5e96-4667-a677-8186463aee69", + "permanentLoanTypeId": "668c4500-fc4e-4140-8a9f-872b54f5ef31", + "effectiveLocationId": "574326a1-f240-4222-82b3-21ccd6d6e5e5", + "electronicAccess": [], + "statisticalCodeIds": [], + "tags": { + "tagList": [] + }, + "metadata": { + "createdDate": "2025-04-11T11:44:08.629+00:00", + "createdByUserId": "cee695b3-8732-447c-90d2-534f1fa16991", + "updatedDate": "2025-04-11T11:44:08.629+00:00", + "updatedByUserId": "cee695b3-8732-447c-90d2-534f1fa16991" + } + }, + "type": "CREATE", + "tenant": "fs09000000", + "eventId": "ae3aa2db-c71a-4a32-83d7-3d7534178f5a", + "eventTs": 1744371848744 +} \ No newline at end of file