diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json
index 7d4581ed..fb89305c 100644
--- a/descriptors/ModuleDescriptor-template.json
+++ b/descriptors/ModuleDescriptor-template.json
@@ -16,7 +16,9 @@
"methods": ["POST"],
"pathPattern": "/tlr/ecs-tlr",
"permissionsRequired": ["tlr.ecs-tlr.post"],
- "modulePermissions": []
+ "modulePermissions": [
+ "circulation.requests.item.post"
+ ]
}
]
},
diff --git a/pom.xml b/pom.xml
index 2f422aa0..043b812f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -105,6 +105,11 @@
provided
+
+ org.springframework
+ spring-webflux
+
+
org.springframework.boot
diff --git a/src/main/java/org/folio/client/CirculationClient.java b/src/main/java/org/folio/client/CirculationClient.java
new file mode 100644
index 00000000..043dc9df
--- /dev/null
+++ b/src/main/java/org/folio/client/CirculationClient.java
@@ -0,0 +1,13 @@
+package org.folio.client;
+
+import org.folio.domain.dto.Request;
+import org.folio.spring.config.FeignClientConfiguration;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.web.bind.annotation.PostMapping;
+
+@FeignClient(name = "circulation", url = "${folio.okapi-url}", configuration = FeignClientConfiguration.class)
+public interface CirculationClient {
+
+ @PostMapping("/circulation/requests")
+ Request createTitleLevelRequest(Request request);
+}
diff --git a/src/main/java/org/folio/controller/EcsTlrController.java b/src/main/java/org/folio/controller/EcsTlrController.java
index 9fe5e947..7f461ce5 100644
--- a/src/main/java/org/folio/controller/EcsTlrController.java
+++ b/src/main/java/org/folio/controller/EcsTlrController.java
@@ -34,6 +34,6 @@ public ResponseEntity getEcsTlrById(UUID requestId) {
public ResponseEntity postEcsTlr(EcsTlr ecsTlr) {
log.debug("postEcsTlr:: parameters ecsTlr: {}", ecsTlr);
- return ResponseEntity.status(CREATED).body(ecsTlrService.post(ecsTlr));
+ return ResponseEntity.status(CREATED).body(ecsTlrService.create(ecsTlr));
}
}
diff --git a/src/main/java/org/folio/domain/mapper/EcsTlrMapper.java b/src/main/java/org/folio/domain/mapper/EcsTlrMapper.java
index f4d97f9d..cb06de46 100644
--- a/src/main/java/org/folio/domain/mapper/EcsTlrMapper.java
+++ b/src/main/java/org/folio/domain/mapper/EcsTlrMapper.java
@@ -1,6 +1,7 @@
package org.folio.domain.mapper;
import org.folio.domain.dto.EcsTlr;
+import org.folio.domain.dto.Request;
import org.folio.domain.entity.EcsTlrEntity;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
@@ -36,17 +37,19 @@ default EcsTlr.FulfillmentPreferenceEnum mapFulfillmentPreference(String fulfill
}
@Named("RequestTypeToString")
- default String mapRequestTypeToString(EcsTlr.RequestTypeEnum requestTypeEnum) {
- return requestTypeEnum != null ? requestTypeEnum.getValue() : null;
+ default String mapRequestType(EcsTlr.RequestTypeEnum requestType) {
+ return requestType != null ? requestType.getValue() : null;
}
@Named("RequestLevelToString")
- default String mapRequestLevelToString(EcsTlr.RequestLevelEnum requestLevelEnum) {
- return requestLevelEnum != null ? requestLevelEnum.getValue() : null;
+ default String mapRequestLevel(EcsTlr.RequestLevelEnum requestLevel) {
+ return requestLevel != null ? requestLevel.getValue() : null;
}
@Named("FulfillmentPreferenceToString")
- default String mapFulfillmentPreferenceToString(EcsTlr.FulfillmentPreferenceEnum fulfillmentPreferenceEnum) {
- return fulfillmentPreferenceEnum != null ? fulfillmentPreferenceEnum.getValue() : null;
+ default String mapFulfillmentPreference(EcsTlr.FulfillmentPreferenceEnum fulfillmentPreference) {
+ return fulfillmentPreference != null ? fulfillmentPreference.getValue() : null;
}
+
+ Request mapDtoToRequest(EcsTlr ecsTlr);
}
diff --git a/src/main/java/org/folio/service/EcsTlrService.java b/src/main/java/org/folio/service/EcsTlrService.java
index 4d7da52a..bbee5bb8 100644
--- a/src/main/java/org/folio/service/EcsTlrService.java
+++ b/src/main/java/org/folio/service/EcsTlrService.java
@@ -7,5 +7,5 @@
public interface EcsTlrService {
Optional get(UUID requestId);
- EcsTlr post(EcsTlr ecsTlr);
+ EcsTlr create(EcsTlr ecsTlr);
}
diff --git a/src/main/java/org/folio/service/TenantScopedExecutionService.java b/src/main/java/org/folio/service/TenantScopedExecutionService.java
new file mode 100644
index 00000000..d20bf68c
--- /dev/null
+++ b/src/main/java/org/folio/service/TenantScopedExecutionService.java
@@ -0,0 +1,8 @@
+package org.folio.service;
+
+import java.util.concurrent.Callable;
+
+public interface TenantScopedExecutionService {
+
+ T execute(String tenantId, Callable action);
+}
diff --git a/src/main/java/org/folio/service/impl/EcsTlrServiceImpl.java b/src/main/java/org/folio/service/impl/EcsTlrServiceImpl.java
index 43d20595..058312f4 100644
--- a/src/main/java/org/folio/service/impl/EcsTlrServiceImpl.java
+++ b/src/main/java/org/folio/service/impl/EcsTlrServiceImpl.java
@@ -3,9 +3,12 @@
import java.util.Optional;
import java.util.UUID;
+import org.folio.client.CirculationClient;
import org.folio.domain.dto.EcsTlr;
+import org.folio.domain.dto.Request;
import org.folio.domain.mapper.EcsTlrMapper;
import org.folio.repository.EcsTlrRepository;
+import org.folio.service.TenantScopedExecutionService;
import org.folio.service.EcsTlrService;
import org.springframework.stereotype.Service;
@@ -19,6 +22,8 @@ public class EcsTlrServiceImpl implements EcsTlrService {
private final EcsTlrRepository ecsTlrRepository;
private final EcsTlrMapper requestsMapper;
+ private final CirculationClient circulationClient;
+ private final TenantScopedExecutionService tenantScopedExecutionService;
@Override
public Optional get(UUID id) {
@@ -29,8 +34,12 @@ public Optional get(UUID id) {
}
@Override
- public EcsTlr post(EcsTlr ecsTlr) {
- log.debug("post:: parameters ecsTlr: {}", () -> ecsTlr);
+ public EcsTlr create(EcsTlr ecsTlr) {
+ log.debug("create:: parameters ecsTlr: {}", () -> ecsTlr);
+ Request mappedRequest = requestsMapper.mapDtoToRequest(ecsTlr);
+ Request cretedRequest = tenantScopedExecutionService.execute("dummy-tenant", // TODO: replace with real tenantId
+ () -> circulationClient.createTitleLevelRequest(mappedRequest));
+ log.info("create:: title-level request created: {}", cretedRequest.getId());
return requestsMapper.mapEntityToDto(ecsTlrRepository.save(
requestsMapper.mapDtoToEntity(ecsTlr)));
diff --git a/src/main/java/org/folio/service/impl/TenantScopedExecutionServiceImpl.java b/src/main/java/org/folio/service/impl/TenantScopedExecutionServiceImpl.java
new file mode 100644
index 00000000..4248458b
--- /dev/null
+++ b/src/main/java/org/folio/service/impl/TenantScopedExecutionServiceImpl.java
@@ -0,0 +1,39 @@
+package org.folio.service.impl;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Callable;
+
+import org.folio.service.TenantScopedExecutionService;
+import org.folio.spring.FolioExecutionContext;
+import org.folio.spring.FolioModuleMetadata;
+import org.folio.spring.integration.XOkapiHeaders;
+import org.folio.spring.scope.FolioExecutionContextSetter;
+import org.springframework.stereotype.Service;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
+
+@Service
+@RequiredArgsConstructor
+@Log4j2
+public class TenantScopedExecutionServiceImpl implements TenantScopedExecutionService {
+
+ private final FolioModuleMetadata moduleMetadata;
+ private final FolioExecutionContext executionContext;
+
+ @Override
+ public T execute(String tenantId, Callable action) {
+ log.info("execute:: tenantId={}", tenantId);
+ Map> headers = executionContext.getAllHeaders();
+ headers.put(XOkapiHeaders.TENANT, List.of(tenantId));
+
+ try (var x = new FolioExecutionContextSetter(moduleMetadata, headers)) {
+ return action.call();
+ } catch (Exception e) {
+ log.error("execute:: tenantId={}", tenantId, e);
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 5fa89cd8..c3fef02e 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -43,6 +43,10 @@ folio:
enabled: true
environment: ${ENV:folio}
okapi-url: ${OKAPI_URL:http://okapi:9130}
+ logging:
+ feign:
+ enabled: true
+ level: full
management:
endpoints:
web:
diff --git a/src/main/resources/swagger.api/ecs-tlr.yaml b/src/main/resources/swagger.api/ecs-tlr.yaml
index 3a2064a0..657e0354 100644
--- a/src/main/resources/swagger.api/ecs-tlr.yaml
+++ b/src/main/resources/swagger.api/ecs-tlr.yaml
@@ -45,7 +45,9 @@ components:
ecs-tlr:
$ref: 'schemas/EcsTlr.yaml#/EcsTlr'
errorResponse:
- $ref: schemas/errors.json
+ $ref: 'schemas/errors.json'
+ request:
+ $ref: 'schemas/request.json'
parameters:
requestId:
name: requestId
diff --git a/src/main/resources/swagger.api/schemas/override-blocks.json b/src/main/resources/swagger.api/schemas/override-blocks.json
new file mode 100644
index 00000000..51985abd
--- /dev/null
+++ b/src/main/resources/swagger.api/schemas/override-blocks.json
@@ -0,0 +1,57 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "type": "object",
+ "description": "Blocks to override (e.g. during checkout or renewal)",
+ "properties": {
+ "itemNotLoanableBlock": {
+ "description": "'Item not loanable' block",
+ "type": "object",
+ "properties": {
+ "dueDate": {
+ "description": "Due date for a new loan",
+ "type": "string",
+ "format": "date-time"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "dueDate"
+ ]
+ },
+ "patronBlock": {
+ "description": "Automated patron block",
+ "type": "object",
+ "additionalProperties": false
+ },
+ "itemLimitBlock": {
+ "description": "Item limit block",
+ "type": "object",
+ "additionalProperties": false
+ },
+ "renewalBlock": {
+ "description": "Renewal block",
+ "type": "object",
+ "additionalProperties": false
+ },
+ "renewalDueDateRequiredBlock": {
+ "description": "Override renewal block which requires due date field",
+ "type": "object",
+ "properties": {
+ "dueDate": {
+ "description": "Due date for a new loan",
+ "type": "string",
+ "format": "date-time"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "dueDate"
+ ]
+ },
+ "comment": {
+ "description": "Reason for override",
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+}
diff --git a/src/main/resources/swagger.api/schemas/request-search-index.json b/src/main/resources/swagger.api/schemas/request-search-index.json
new file mode 100644
index 00000000..25ff8c84
--- /dev/null
+++ b/src/main/resources/swagger.api/schemas/request-search-index.json
@@ -0,0 +1,35 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "description": "Request fields used for search",
+ "type": "object",
+ "properties": {
+ "callNumberComponents": {
+ "type": "object",
+ "description": "Effective call number components",
+ "properties": {
+ "callNumber": {
+ "type": "string",
+ "description": "Effective Call Number is an identifier assigned to an item or its holding and associated with the item."
+ },
+ "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."
+ },
+ "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."
+ }
+ },
+ "additionalProperties": false
+ },
+ "shelvingOrder": {
+ "type": "string",
+ "description": "A system generated normalization of the call number that allows for call number sorting in reports and search results"
+ },
+ "pickupServicePointName": {
+ "description": "The name of the request pickup service point",
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+}
diff --git a/src/main/resources/swagger.api/schemas/request.json b/src/main/resources/swagger.api/schemas/request.json
new file mode 100644
index 00000000..cb43c1f3
--- /dev/null
+++ b/src/main/resources/swagger.api/schemas/request.json
@@ -0,0 +1,395 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "title": "A request for an item",
+ "description": "Request for an item that might be at a different location or already checked out to another patron",
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "UUID of the request",
+ "type": "string",
+ "$ref": "uuid.json"
+ },
+ "requestType": {
+ "description": "Whether the item should be held upon return, recalled or paged for",
+ "type": "string",
+ "enum": ["Hold", "Recall", "Page"]
+ },
+ "requestLevel": {
+ "description": "Level of the request - Item or Title",
+ "type": "string",
+ "enum": ["Item", "Title"]
+ },
+ "requestDate": {
+ "description": "Date the request was made",
+ "type": "string",
+ "format": "date-time"
+ },
+ "patronComments": {
+ "description": "Comments made by the patron",
+ "type": "string"
+ },
+ "requesterId": {
+ "description": "ID of the user who made the request",
+ "type": "string",
+ "$ref": "uuid.json"
+ },
+ "proxyUserId": {
+ "description": "ID of the user representing a proxy for the patron",
+ "type": "string",
+ "$ref": "uuid.json"
+ },
+ "instanceId": {
+ "description": "ID of the instance being requested",
+ "type": "string",
+ "$ref": "uuid.json"
+ },
+ "holdingsRecordId": {
+ "description": "ID of the holdings record being requested",
+ "type": "string",
+ "$ref": "uuid.json"
+ },
+ "itemId": {
+ "description": "ID of the item being requested",
+ "type": "string",
+ "$ref": "uuid.json"
+ },
+ "status": {
+ "description": "Status of the request",
+ "type": "string",
+ "enum": [
+ "Open - Not yet filled",
+ "Open - Awaiting pickup",
+ "Open - In transit",
+ "Open - Awaiting delivery",
+ "Closed - Filled",
+ "Closed - Cancelled",
+ "Closed - Unfilled",
+ "Closed - Pickup expired"
+ ]
+ },
+ "cancellationReasonId": {
+ "description": "The id of the request reason",
+ "type": "string",
+ "$ref": "uuid.json"
+ },
+ "cancelledByUserId": {
+ "description": "The id of the user that cancelled the request",
+ "type": "string",
+ "$ref": "uuid.json"
+ },
+ "cancellationAdditionalInformation": {
+ "description": "Additional information about a cancellation",
+ "type": "string"
+ },
+ "cancelledDate": {
+ "description": "Date the request was cancelled",
+ "type": "string",
+ "format": "date-time"
+ },
+ "position": {
+ "description": "position of the request in a per-item request queue",
+ "type": "integer",
+ "minimum": 1
+ },
+ "instance": {
+ "description": "Copy of some instance metadata (used for searching and sorting)",
+ "type": "object",
+ "properties": {
+ "title": {
+ "description": "title of the item",
+ "type": "string"
+ },
+ "identifiers": {
+ "type": "array",
+ "description": "An extensible set of name-value pairs of identifiers associated with the resource",
+ "minItems": 0,
+ "items": {
+ "type": "object",
+ "properties": {
+ "value": {
+ "type": "string",
+ "description": "Resource identifier value"
+ },
+ "identifierTypeId": {
+ "type": "string",
+ "description": "UUID of resource identifier type (e.g. ISBN, ISSN, LCCN, CODEN, Locally defined identifiers)",
+ "$ref": "uuid.json"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "value",
+ "identifierTypeId"
+ ]
+ }
+ }
+ }
+ },
+ "item": {
+ "description": "Copy of some item metadata (used for searching and sorting)",
+ "type": "object",
+ "properties": {
+ "barcode": {
+ "description": "barcode of the item",
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ },
+ "requester": {
+ "description": "Copy of some requesting patron metadata (used for searching and sorting), will be taken from the user referred to by the requesterId",
+ "readonly": true,
+ "type": "object",
+ "properties": {
+ "firstName": {
+ "description": "first name of the patron (read only, defined by the server)",
+ "type": "string",
+ "readonly": true
+ },
+ "lastName": {
+ "description": "last name of the patron (read only, defined by the server)",
+ "type": "string",
+ "readonly": true
+ },
+ "middleName": {
+ "description": "middle name of the patron (read only, defined by the server)",
+ "type": "string",
+ "readonly": true
+ },
+ "barcode": {
+ "description": "barcode of the patron (read only, defined by the server)",
+ "type": "string",
+ "readonly": true
+ },
+ "patronGroupId": {
+ "description": "UUID for the patron group that this user belongs to",
+ "type": "string",
+ "readonly": true,
+ "$ref": "uuid.json"
+ },
+ "patronGroup": {
+ "description": "record for the user's patron group",
+ "type": "object",
+ "additionalProperties": false,
+ "readonly": true,
+ "properties": {
+ "id": {
+ "description": "ID of the patron group",
+ "type": "string",
+ "readonly": true,
+ "$ref": "uuid.json"
+ },
+ "group": {
+ "description": "The unique name of the patron group",
+ "type": "string",
+ "readonly": true
+ },
+ "desc": {
+ "description": "A description of the patron group",
+ "type": "string",
+ "readonly": true
+ }
+ }
+ }
+ },
+ "additionalProperties": false
+ },
+ "proxy": {
+ "description": "Copy of some proxy patron metadata (used for searching and sorting), will be taken from the user referred to by the proxyUserId",
+ "readonly": true,
+ "type": "object",
+ "properties": {
+ "firstName": {
+ "description": "first name of the proxy patron (read only, defined by the server)",
+ "type": "string",
+ "readonly": true
+ },
+ "lastName": {
+ "description": "last name of the proxy patron (read only, defined by the server)",
+ "type": "string",
+ "readonly": true
+ },
+ "middleName": {
+ "description": "middle name of the proxy patron (read only, defined by the server)",
+ "type": "string",
+ "readonly": true
+ },
+ "barcode": {
+ "description": "barcode of the proxy patron (read only, defined by the server)",
+ "type": "string",
+ "readonly": true
+ },
+ "patronGroupId": {
+ "description": "UUID for the patrongroup that this user belongs to",
+ "type": "string",
+ "readonly": true,
+ "$ref": "uuid.json"
+ },
+ "patronGroup": {
+ "description": "record for the user's patrongroup",
+ "type": "object",
+ "readonly": true,
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "description": "ID of the patrongroup",
+ "type": "string",
+ "readonly": true,
+ "$ref": "uuid.json"
+ },
+ "group": {
+ "description": "The unique name of the patrongroup",
+ "type": "string",
+ "readonly": true
+ },
+ "desc": {
+ "description": "A description of the patrongroup",
+ "type": "string",
+ "readonly": true
+ }
+ }
+ }
+ },
+ "additionalProperties": false
+ },
+ "fulfillmentPreference": {
+ "description": "How should the request be fulfilled (whether the item should be kept on the hold shelf for collection or delivered to the requester)",
+ "type": "string",
+ "enum": ["Hold Shelf", "Delivery"]
+ },
+ "deliveryAddressTypeId": {
+ "description": "Deliver to the address of this type, for the requesting patron",
+ "type": "string",
+ "$ref": "uuid.json"
+ },
+ "deliveryAddress": {
+ "description": "Address the item is to be delivered to (derived from requester information)",
+ "type": "object",
+ "readonly": true,
+ "properties": {
+ "addressLine1": {
+ "description": "Address line 1",
+ "type": "string",
+ "readonly": true
+ },
+ "addressLine2": {
+ "description": "Address line 2",
+ "type": "string",
+ "readonly": true
+ },
+ "city": {
+ "description": "City name",
+ "type": "string",
+ "readonly": true
+ },
+ "region": {
+ "description": "Region",
+ "type": "string",
+ "readonly": true
+ },
+ "postalCode": {
+ "description": "Postal code",
+ "type": "string",
+ "readonly": true
+ },
+ "countryId": {
+ "description": "Country code",
+ "type": "string",
+ "readonly": true
+ },
+ "addressTypeId": {
+ "description": "Type of address (refers to address types)",
+ "type": "string",
+ "readonly": true,
+ "$ref": "uuid.json"
+ }
+ },
+ "additionalProperties": false
+ },
+ "requestExpirationDate": {
+ "description": "Date when the request expires",
+ "type": "string",
+ "format": "date-time"
+ },
+ "holdShelfExpirationDate": {
+ "description": "Date when an item returned to the hold shelf expires",
+ "type": "string",
+ "format": "date-time"
+ },
+ "pickupServicePointId": {
+ "description": "The ID of the Service Point where this request can be picked up",
+ "type": "string",
+ "$ref": "uuid.json"
+ },
+ "pickupServicePoint": {
+ "description": "The full object of the Service Point record from pickupServicePointId",
+ "additionalProperties": false,
+ "readonly": true,
+ "properties": {
+ "name": {
+ "description": "Unique name for the service point",
+ "type": "string",
+ "readonly": true
+ },
+ "code": {
+ "description": "Unique code for the service point",
+ "type": "string",
+ "readonly": true
+ },
+ "discoveryDisplayName": {
+ "description": "Human-readable name for the service point",
+ "type": "string",
+ "readonly": true
+ },
+ "description": {
+ "description": "Description of the service point",
+ "type": "string",
+ "readonly": true
+ },
+ "shelvingLagTime": {
+ "description": "Shelving lag time",
+ "type": "integer",
+ "readonly": true
+ },
+ "pickupLocation": {
+ "description": "Is this service point a pickup location?",
+ "type": "boolean",
+ "readonly": true
+ }
+ }
+ },
+ "tags": {
+ "type": "object",
+ "description": "Tags",
+ "$ref": "tags.json"
+ },
+ "metadata": {
+ "description": "Metadata about creation and changes to requests, provided by the server (client should not provide)",
+ "type": "object",
+ "$ref": "metadata.json"
+ },
+ "requestProcessingParameters": {
+ "type": "object",
+ "description": "Additional parameters used for request processing and discarded afterwards. Not part of request record.",
+ "properties": {
+ "overrideBlocks": {
+ "type": "object",
+ "description": "Blocks to override if user has corresponding permissions",
+ "$ref": "override-blocks.json"
+ }
+ }
+ },
+ "searchIndex": {
+ "description": "Request fields used for search",
+ "type": "object",
+ "$ref": "request-search-index.json"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "requesterId",
+ "requestType",
+ "requestDate",
+ "fulfillmentPreference"
+ ]
+}
diff --git a/src/main/resources/swagger.api/schemas/tags.json b/src/main/resources/swagger.api/schemas/tags.json
new file mode 100644
index 00000000..cc7cfcfc
--- /dev/null
+++ b/src/main/resources/swagger.api/schemas/tags.json
@@ -0,0 +1,16 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "title": "tags",
+ "description": "List of simple tags that can be added to an object",
+ "type": "object",
+ "properties": {
+ "tagList": {
+ "description": "List of tags",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ "additionalProperties": false
+}
diff --git a/src/main/resources/swagger.api/schemas/uuid.json b/src/main/resources/swagger.api/schemas/uuid.json
new file mode 100644
index 00000000..1f907f9e
--- /dev/null
+++ b/src/main/resources/swagger.api/schemas/uuid.json
@@ -0,0 +1,5 @@
+{
+ "description": "A universally unique identifier (UUID), this is a 128-bit number used to identify a record and is shown in hex with dashes, for example 6312d172-f0cf-40f6-b27d-9fa8feaf332f; the UUID version must be from 1-5; see https://dev.folio.org/guides/uuids/",
+ "type": "string",
+ "pattern": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$"
+}
diff --git a/src/test/java/org/folio/api/BaseIT.java b/src/test/java/org/folio/api/BaseIT.java
index e34adfc0..c324081c 100644
--- a/src/test/java/org/folio/api/BaseIT.java
+++ b/src/test/java/org/folio/api/BaseIT.java
@@ -3,6 +3,7 @@
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
import com.github.tomakehurst.wiremock.WireMockServer;
import lombok.SneakyThrows;
import org.folio.spring.integration.XOkapiHeaders;
@@ -14,20 +15,25 @@
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.support.TestPropertySourceUtils;
import org.springframework.test.util.TestSocketUtils;
+import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
+import org.springframework.web.reactive.function.BodyInserters;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Testcontainers;
import java.util.List;
+import java.util.UUID;
import static org.springframework.http.MediaType.APPLICATION_JSON;
-import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@@ -38,14 +44,18 @@
public class BaseIT {
@Autowired
protected MockMvc mockMvc;
- public static WireMockServer wireMockServer;
+ protected static WireMockServer wireMockServer;
protected static final String TOKEN = "test_token";
- public static final String TENANT = "diku";
- protected static PostgreSQLContainer> postgresDBContainer = new PostgreSQLContainer<>("postgres:12-alpine");
- public final static int WIRE_MOCK_PORT = TestSocketUtils.findAvailableTcpPort();
- protected static final ObjectMapper OBJECT_MAPPER = new ObjectMapper().setSerializationInclusion(JsonInclude.Include.NON_NULL)
+ protected static final String TENANT = "diku";
+ private static final PostgreSQLContainer> postgresDBContainer = new PostgreSQLContainer<>("postgres:12-alpine");
+ private static final int WIRE_MOCK_PORT = TestSocketUtils.findAvailableTcpPort();
+ private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper().setSerializationInclusion(JsonInclude.Include.NON_NULL)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
- .configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);
+ .configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true)
+ .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
+ @LocalServerPort
+ private int serverPort;
+ private WebTestClient webClient;
static {
postgresDBContainer.start();
@@ -69,7 +79,7 @@ static void tearDown() {
@SneakyThrows
protected static void setUpTenant(MockMvc mockMvc) {
- mockMvc.perform(post("/_/tenant")
+ mockMvc.perform(MockMvcRequestBuilders.post("/_/tenant")
.content(asJsonString(new TenantAttributes().moduleTo("mod-tlr")))
.headers(defaultHeaders())
.contentType(APPLICATION_JSON)).andExpect(status().isNoContent());
@@ -102,4 +112,33 @@ public void initialize(@NotNull ConfigurableApplicationContext applicationContex
}
}
+ protected WebTestClient webClient() {
+ if (webClient == null) {
+ webClient = WebTestClient.bindToServer()
+ .baseUrl("http://localhost:" + serverPort).build();
+ }
+ return webClient;
+ }
+
+ protected WebTestClient.RequestBodySpec buildRequest(HttpMethod method, String uri) {
+ return webClient().method(method)
+ .uri(uri)
+ .accept(APPLICATION_JSON)
+ .contentType(APPLICATION_JSON)
+ .header(XOkapiHeaders.TENANT, TENANT)
+ .header(XOkapiHeaders.URL, wireMockServer.baseUrl())
+ .header(XOkapiHeaders.TOKEN, TOKEN)
+ .header(XOkapiHeaders.USER_ID, UUID.randomUUID().toString());
+ }
+
+ protected WebTestClient.ResponseSpec doPost(String uri, Object payload) {
+ return buildRequest(HttpMethod.POST, uri)
+ .body(BodyInserters.fromValue(payload))
+ .exchange();
+ }
+
+ protected static String randomId() {
+ return UUID.randomUUID().toString();
+ }
+
}
diff --git a/src/test/java/org/folio/api/EcsTlrApiTest.java b/src/test/java/org/folio/api/EcsTlrApiTest.java
index 71e8e898..c24dad22 100644
--- a/src/test/java/org/folio/api/EcsTlrApiTest.java
+++ b/src/test/java/org/folio/api/EcsTlrApiTest.java
@@ -1,20 +1,109 @@
package org.folio.api;
+import org.apache.http.HttpStatus;
+import org.folio.domain.dto.EcsTlr;
+import org.folio.spring.FolioExecutionContext;
+import org.folio.spring.FolioModuleMetadata;
+import org.folio.spring.scope.FolioExecutionContextSetter;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.MediaType;
+
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
import java.util.UUID;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.jsonResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching;
+import static java.util.stream.Collectors.toMap;
+import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+import com.github.tomakehurst.wiremock.client.WireMock;
+
class EcsTlrApiTest extends BaseIT {
- private static final String TLR_URL = "/tlr/ecs-tlr/";
+ private static final String TLR_URL = "/tlr/ecs-tlr";
+ private static final String ANOTHER_TENANT = "dummy-tenant";
+ private static final String TENANT_HEADER = "x-okapi-tenant";
+
+ @Autowired
+ private FolioExecutionContext context;
+ @Autowired
+ private FolioModuleMetadata moduleMetadata;
+ @Autowired
+ private TestRestTemplate restTemplate;
+ private FolioExecutionContextSetter contextSetter;
+
+ @BeforeEach
+ public void beforeEach() {
+ contextSetter = initContext();
+ }
+
+ @AfterEach
+ public void afterEach() {
+ contextSetter.close();
+ }
@Test
void getByIdNotFound() throws Exception {
mockMvc.perform(
- get(TLR_URL + UUID.randomUUID())
+ get(TLR_URL + "/" + UUID.randomUUID())
.headers(defaultHeaders())
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isNotFound());
}
+
+ @Test
+ public void titleLevelRequestIsCreatedForDifferentTenant() {
+ EcsTlr ecsTlr = buildEcsTlr();
+ wireMockServer.stubFor(WireMock.post(urlMatching(".*/circulation/requests"))
+ .withHeader(TENANT_HEADER, equalTo(ANOTHER_TENANT))
+ .willReturn(jsonResponse(asJsonString(ecsTlr), HttpStatus.SC_CREATED)));
+ assertEquals(getCurrentTenantId(), TENANT);
+
+ doPost(TLR_URL, ecsTlr)
+ .expectStatus().isCreated()
+ .expectBody().json(asJsonString(ecsTlr));
+
+ assertEquals(getCurrentTenantId(), TENANT);
+ wireMockServer.verify(postRequestedFor(urlMatching(".*/circulation/requests"))
+ .withHeader(TENANT_HEADER, equalTo(ANOTHER_TENANT)));
+ }
+
+ private String getCurrentTenantId() {
+ return context.getTenantId();
+ }
+
+ private static Map> buildDefaultHeaders() {
+ return new HashMap<>(defaultHeaders().entrySet()
+ .stream()
+ .collect(toMap(Map.Entry::getKey, Map.Entry::getValue)));
+ }
+
+ private FolioExecutionContextSetter initContext() {
+ return new FolioExecutionContextSetter(moduleMetadata, buildDefaultHeaders());
+ }
+
+ private static EcsTlr buildEcsTlr() {
+ return new EcsTlr()
+ .id(randomId())
+ .itemId(randomId())
+ .instanceId(randomId())
+ .requesterId(randomId())
+ .pickupServicePointId(randomId())
+ .fulfillmentPreference(EcsTlr.FulfillmentPreferenceEnum.DELIVERY)
+ .patronComments("random comment")
+ .requestExpirationDate(new Date())
+ .requestType(EcsTlr.RequestTypeEnum.PAGE)
+ .requestLevel(EcsTlr.RequestLevelEnum.TITLE);
+ }
+
}
diff --git a/src/test/java/org/folio/controller/EcsTlrControllerTest.java b/src/test/java/org/folio/controller/EcsTlrControllerTest.java
index f7f5021b..47e3477f 100644
--- a/src/test/java/org/folio/controller/EcsTlrControllerTest.java
+++ b/src/test/java/org/folio/controller/EcsTlrControllerTest.java
@@ -5,7 +5,6 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.http.HttpStatus.CREATED;
-import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR;
import java.util.Optional;
@@ -44,7 +43,7 @@ void getById() {
@Test
void ecsTlrShouldSuccessfullyBeCreated() {
var mockRequest = new EcsTlr();
- when(requestsService.post(any(EcsTlr.class))).thenReturn(mockRequest);
+ when(requestsService.create(any(EcsTlr.class))).thenReturn(mockRequest);
var response = requestsController.postEcsTlr(new EcsTlr());
diff --git a/src/test/java/org/folio/service/EcsTlrServiceTest.java b/src/test/java/org/folio/service/EcsTlrServiceTest.java
index 4eaf0752..6c034c96 100644
--- a/src/test/java/org/folio/service/EcsTlrServiceTest.java
+++ b/src/test/java/org/folio/service/EcsTlrServiceTest.java
@@ -72,7 +72,7 @@ void postEcsTlr() {
ecsTlr.setPickupServicePointId(pickupServicePointId.toString());
when(ecsTlrRepository.save(any(EcsTlrEntity.class))).thenReturn(mockEcsTlrEntity);
- var postEcsTlr = ecsTlrService.post(ecsTlr);
+ var postEcsTlr = ecsTlrService.create(ecsTlr);
assertEquals(id.toString(), postEcsTlr.getId());
assertEquals(instanceId.toString(), postEcsTlr.getInstanceId());