diff --git a/NEWS.md b/NEWS.md index c1b89cea92..95cd9bea97 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,22 @@ +## 24.3.2 2024-11-30 +* Add mod-settings permission blocks (CIRC-2185) +* Fix automated patron blocks permission issue (CIRC-2185) +* Remove new mod-settings permissions (CIRC-2183) +* Fetch TLR settings from mod-config as fallback (CIRC-2171) +* Update API versions (CIRC-2153) +* Change ECS Primary request validation (CIRC-2151) +* Allow operation replace for instance with no items (CIRC-2137) +* Search title-level requests by both `itemId` and `instanceId` during check-in and check-out (CIRC-2125) +* Allowed SP endpoint should support `patronGroupId` parameter (CIRC-2116) +* Return empty result when search doesn't find anything (CIRC-2117) +* Pass additional `includeRoutingServicePoints` parameter when needed (CIRC-2109) +* Fetch item details across tenants (CIRC-2101) +* Create a facade for instance search (CIRC-2072) +* Fetch TLR settings from mod-settings (CIRC-2081) +* Add `ecsRequestRouting` parameter to allowed-service-points (CIRC-2051) +* Fix snapshot version (CIRC-2161) +* Review and cleanup Module Descriptors for mod-circulation (CIRC-2139) + ## 24.3.1 2024-11-27 * Patron notices for the trigger “Item recalled” not sent if the item is not 1st in the title request queue (CIRC-2168) diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index 590b2da834..c87bad9224 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -132,11 +132,11 @@ "methods": ["POST"], "pathPattern": "/circulation/loans/{id}/change-due-date", "permissionsRequired": [ - "circulation.loans.change-due-date.post", - "configuration.entries.collection.get" + "circulation.loans.change-due-date.post" ], "modulePermissions": [ - "modperms.circulation.loans.change-due-date.post" + "perms.circulation.loans.change-due-date.post", + "configuration.entries.collection.get" ] } ] @@ -152,7 +152,7 @@ "circulation.loans.claim-item-returned.post" ], "modulePermissions": [ - "modperms.circulation.loans.claim-item-returned.post" + "perms.circulation.loans.claim-item-returned.post" ] }, { @@ -167,6 +167,22 @@ } ] }, + { + "id": "instance-items", + "version": "0.1", + "handlers": [ + { + "methods": ["GET"], + "pathPattern": "/circulation/items-by-instance", + "permissionsRequired": [ + "circulation.items-by-instance.get" + ], + "modulePermissions": [ + "search.instances.collection.get" + ] + } + ] + }, { "id": "add-info", "version": "0.1", @@ -199,9 +215,9 @@ "modperms.circulation.check-out-by-barcode.post" ], "permissionsDesired": [ - "circulation.override-patron-block", - "circulation.override-item-limit-block", - "circulation.override-item-not-loanable-block" + "circulation.override-patron-block.post", + "circulation.override-item-limit-block.post", + "circulation.override-item-not-loanable-block.post" ] }, { @@ -225,11 +241,11 @@ "circulation.renew-by-barcode.post" ], "modulePermissions": [ - "circulation.renew-loan" + "circulation.renew-loan.all" ], "permissionsDesired": [ - "circulation.override-patron-block", - "circulation.override-renewal-block" + "circulation.override-patron-block.post", + "circulation.override-renewal-block.post" ] }, { @@ -241,7 +257,7 @@ "circulation.renew-by-id.post" ], "modulePermissions": [ - "circulation.renew-loan" + "circulation.renew-loan.all" ] }, { @@ -337,7 +353,7 @@ "circulation.requests.item.post" ], "permissionsDesired" : [ - "circulation.override-patron-block" + "circulation.override-patron-block.post" ], "modulePermissions": [ "modperms.circulation.requests.item.post" @@ -397,7 +413,7 @@ ], "pathPattern": "/circulation/requests/queue/item/{id}", "permissionsRequired": [ - "circulation.requests.queue.collection.get" + "circulation.requests.queue-item.collection.get" ], "modulePermissions": [ "modperms.circulation.requests.queue.collection.get" @@ -409,7 +425,7 @@ ], "pathPattern": "/circulation/requests/queue/item/{id}/reorder", "permissionsRequired": [ - "circulation.requests.queue.reorder.collection.post" + "circulation.requests.queue.item-reorder.collection.post" ], "modulePermissions": [ "modperms.circulation.requests.queue.reorder.collection.post" @@ -421,7 +437,7 @@ ], "pathPattern": "/circulation/requests/queue/instance/{id}", "permissionsRequired": [ - "circulation.requests.queue.collection.get" + "circulation.requests.queue-instance.collection.get" ], "modulePermissions": [ "modperms.circulation.requests.queue.collection.get" @@ -433,7 +449,7 @@ ], "pathPattern": "/circulation/requests/queue/instance/{id}/reorder", "permissionsRequired": [ - "circulation.requests.queue.reorder.collection.post" + "circulation.requests.queue.instance-reorder.collection.post" ], "modulePermissions": [ "modperms.circulation.requests.queue.reorder.collection.post" @@ -632,7 +648,7 @@ "circulation-storage.circulation-rules.get", "configuration.entries.item.get", "configuration.entries.collection.get", - "circulation.internal.fetch-items", + "circulation.internal.fetch-items.collection.get", "circulation-storage.loan-policies.collection.get", "users.item.get", "users.collection.get", @@ -649,7 +665,7 @@ }, { "id": "allowed-service-points", - "version": "1.0", + "version": "1.1", "handlers": [ { "methods": [ @@ -669,7 +685,7 @@ "circulation-storage.request-policies.collection.get", "inventory-storage.items.item.get", "inventory-storage.items.collection.get", - "circulation.internal.fetch-items", + "circulation.internal.fetch-items.collection.get", "inventory-storage.service-points.item.get", "inventory-storage.service-points.collection.get", "inventory-storage.holdings.item.get", @@ -677,7 +693,10 @@ "inventory-storage.instances.item.get", "inventory-storage.instances.collection.get", "configuration.entries.item.get", - "configuration.entries.collection.get" + "configuration.entries.collection.get", + "mod-settings.entries.item.get", + "mod-settings.entries.collection.get", + "mod-settings.global.read.circulation" ] } ] @@ -769,7 +788,7 @@ "patron-action-session-storage.patron-action-sessions.item.get", "patron-action-session-storage.patron-action-sessions.collection.get", "circulation-storage.loans.collection.get", - "circulation.internal.fetch-items", + "circulation.internal.fetch-items.collection.get", "circulation-storage.loan-policies.collection.get", "users.item.get", "users.collection.get", @@ -799,7 +818,7 @@ "actual-cost-record-storage.actual-cost-records.item.put", "accounts.item.get", "accounts.collection.get", - "circulation.internal.fetch-items", + "circulation.internal.fetch-items.collection.get", "lost-item-fees-policies.item.get", "lost-item-fees-policies.collection.get", "circulation-storage.loans.item.get", @@ -821,7 +840,7 @@ "circulation-storage.loans.item.get", "circulation-storage.loan-policies.item.get", "circulation-storage.loan-policies.collection.get", - "circulation.internal.fetch-items", + "circulation.internal.fetch-items.collection.get", "circulation.rules.loan-policy.get", "configuration.entries.collection.get", "patron-notice.post", @@ -853,7 +872,7 @@ "circulation-storage.loan-policies.item.get", "circulation-storage.loan-policies.collection.get", "circulation-storage.fixed-due-date-schedules.collection.get", - "circulation.internal.fetch-items", + "circulation.internal.fetch-items.collection.get", "circulation.rules.loan-policy.get", "configuration.entries.collection.get", "patron-notice.post", @@ -877,7 +896,7 @@ "scheduled-notice-storage.scheduled-notices.collection.get", "scheduled-notice-storage.scheduled-notices.item.delete", "scheduled-notice-storage.scheduled-notices.item.put", - "circulation.internal.fetch-items", + "circulation.internal.fetch-items.collection.get", "circulation-storage.loans.collection.get", "circulation-storage.requests.item.get", "circulation-storage.requests.collection.get", @@ -919,7 +938,7 @@ "circulation-storage.loans.item.get", "circulation-storage.loans.collection.get", "circulation-storage.circulation-rules.get", - "circulation.internal.fetch-items", + "circulation.internal.fetch-items.collection.get", "users.item.get", "users.collection.get", "addresstypes.collection.get", @@ -955,7 +974,7 @@ "circulation-storage.loans.item.get", "circulation-storage.loans.collection.get", "circulation-storage.circulation-rules.get", - "circulation.internal.fetch-items", + "circulation.internal.fetch-items.collection.get", "circulation-storage.loans-history.collection.get", "users.item.get", "users.collection.get", @@ -990,7 +1009,7 @@ "circulation-storage.loans.item.get", "circulation-storage.loans.collection.get", "circulation-storage.circulation-rules.get", - "circulation.internal.fetch-items", + "circulation.internal.fetch-items.collection.get", "users.item.get", "users.collection.get", "addresstypes.collection.get", @@ -1023,7 +1042,7 @@ "circulation.rules.request-policy.get", "circulation.rules.notice-policy.get", "inventory-storage.items.item.put", - "circulation.internal.fetch-items", + "circulation.internal.fetch-items.collection.get", "users.item.get", "users.collection.get", "addresstypes.collection.get", @@ -1054,7 +1073,7 @@ "circulation-storage.patron-notice-policies.collection.get", "circulation-storage.patron-notice-policies.item.get", "inventory-storage.items.item.put", - "circulation.internal.fetch-items", + "circulation.internal.fetch-items.collection.get", "lost-item-fees-policies.item.get", "lost-item-fees-policies.collection.get", "pubsub.publish.post", @@ -1077,7 +1096,7 @@ "circulation-storage.loans.item.get", "circulation-storage.loans.collection.get", "inventory-storage.items.item.put", - "circulation.internal.fetch-items", + "circulation.internal.fetch-items.collection.get", "lost-item-fees-policies.item.get", "lost-item-fees-policies.collection.get", "owners.collection.get", @@ -1115,7 +1134,7 @@ "circulation-storage.loan-policies.item.get", "circulation-storage.loan-policies.collection.get", "overdue-fines-policies.item.get", - "circulation.internal.fetch-items", + "circulation.internal.fetch-items.collection.get", "circulation.rules.loan-policy.get", "configuration.entries.collection.get", "patron-notice.post", @@ -1153,7 +1172,7 @@ ], "pathPattern": "/circulation/handlers/loan-related-fee-fine-closed", "permissionsRequired": [ - "pubsub.events.post" + "circulation.handlers.loan-related-fee-fine-closed.post" ], "modulePermissions": [ "modperms.circulation.handlers.loan-related-fee-fine-closed.post", @@ -1166,7 +1185,7 @@ ], "pathPattern": "/circulation/handlers/fee-fine-balance-changed", "permissionsRequired": [ - "pubsub.events.post" + "circulation.handlers.fee-fine-balance-changed.post" ], "modulePermissions": [ "modperms.circulation.handlers.fee-fine-balance-changed.post" @@ -1355,9 +1374,15 @@ "description": "create print event logs" }, { - "permissionName": "circulation.requests.queue.reorder.collection.post", + "permissionName": "circulation.requests.queue.item-reorder.collection.post", "displayName": "circulation - reorder queue for an item", - "description": "change request positions in queue for an item" + "description": "change request positions in queue for an item", + "replaces": ["circulation.requests.queue.reorder.collection.post"] + }, + { + "permissionName": "circulation.requests.queue.instance-reorder.collection.post", + "displayName": "circulation - reorder queue for an instance", + "description": "change request positions in queue for an instance" }, { "permissionName": "circulation.check-out-by-barcode.post", @@ -1535,7 +1560,13 @@ "description": "move individual request to another item" }, { - "permissionName": "circulation.requests.queue.collection.get", + "permissionName": "circulation.requests.queue-instance.collection.get", + "displayName": "circulation - request queue for an instance", + "description": "get request queue for an instance", + "replaces": ["circulation.requests.queue.collection.get"] + }, + { + "permissionName": "circulation.requests.queue-item.collection.get", "displayName": "circulation - request queue for an item", "description": "get request queue for an item" }, @@ -1570,24 +1601,28 @@ "description": "end patron action session" }, { - "permissionName": "circulation.override-renewal-block", + "permissionName": "circulation.override-renewal-block.post", "displayName": "circulation - override renewal block", - "description": "renewal block override" + "description": "renewal block override", + "replaces": ["circulation.override-renewal-block"] }, { - "permissionName": "circulation.override-item-limit-block", + "permissionName": "circulation.override-item-limit-block.post", "displayName": "circulation - override item limit block", - "description": "item limit block override" + "description": "item limit block override", + "replaces": ["circulation.override-item-limit-block"] }, { - "permissionName": "circulation.override-item-not-loanable-block", + "permissionName": "circulation.override-item-not-loanable-block.post", "displayName": "circulation - override item not loanable block", - "description": "item not loanable block override" + "description": "item not loanable block override", + "replaces": ["circulation.override-item-not-loanable-block.post"] }, { - "permissionName": "circulation.override-patron-block", + "permissionName": "circulation.override-patron-block.post", "displayName": "circulation - override patron block", - "description": "patron block override" + "description": "patron block override", + "replaces": ["circulation.override-patron-block"] }, { "permissionName": "circulation.requests.allowed-service-points.get", @@ -1595,14 +1630,9 @@ "description": "get allowed pickup service points for request" }, { - "permissionName": "mod-settings.global.write.mod-circulation", - "displayName": "circulation settings - Create configuration", - "description": "To create new configuration in mod settings" - }, - { - "permissionName": "mod-settings.global.read.mod-circulation", - "displayName": "circulation settings - Read configuration", - "description": "To read the configuration from mod settings." + "permissionName": "circulation.items-by-instance.get", + "displayName": "circulation - get items by instance", + "description": "get items by instance" }, { "permissionName": "circulation.settings.collection.get", @@ -1629,6 +1659,36 @@ "displayName": "circulation - delete circulation setting", "description": "delete circulation setting by ID" }, + { + "permissionName": "circulation.handlers.fee-fine-balance-changed.post", + "displayName": "circulation - fee/fine balance changed", + "description": "fee/fine balance changed" + }, + { + "permissionName": "circulation.handlers.loan-related-fee-fine-closed.post", + "displayName": "circulation - loan related fee/fine closed", + "description": "loan related fee/fine closed" + }, + { + "permissionName": "mod-settings.global.read.mod-circulation", + "displayName": "mod-circulation settings - read", + "description": "To read mod-circulation settings from mod-settings" + }, + { + "permissionName": "mod-settings.global.write.mod-circulation", + "displayName": "mod-circulation settings - write", + "description": "To create and edit mod-circulation settings in mod-settings" + }, + { + "permissionName": "mod-settings.global.read.circulation", + "displayName": "circulation functionality settings - read", + "description": "To read circulation functionality settings from mod-settings" + }, + { + "permissionName": "mod-settings.global.write.circulation", + "displayName": "circulation functionality settings - write", + "description": "To create and edit circulation functionality settings in mod-settings" + }, { "permissionName": "circulation.all", "displayName": "circulation - all permissions", @@ -1663,14 +1723,18 @@ "circulation.requests.item.delete", "circulation.requests.item.move.post", "circulation.requests.collection.delete", - "circulation.requests.queue.collection.get", + "circulation.requests.queue-instance.collection.get", + "circulation.requests.queue-item.collection.get", "circulation.requests.queue.reorder.collection.post", "circulation.requests.instances.item.post", "circulation.requests.hold-shelf-clearance-report.get", "circulation.requests.allowed-service-points.get", "circulation.inventory.items-in-transit-report.get", "circulation.pick-slips.get", - "circulation.search-slips.get" + "circulation.search-slips.get", + "circulation.handlers.loan-related-fee-fine-closed.post", + "circulation.handlers.fee-fine-balance-changed.post", + "circulation.items-by-instance.get" ] }, { @@ -1678,7 +1742,7 @@ "displayName" : "module permissions for one op", "description" : "to reduce X-Okapi-Token size", "subPermissions": [ - "circulation.internal.fetch-items", + "circulation.internal.fetch-items.collection.get", "circulation-storage.requests.item.get", "circulation-storage.requests.collection.get", "users.collection.get", @@ -1696,7 +1760,7 @@ "circulation-storage.loans.item.put", "inventory-storage.items.item.get", "inventory-storage.items.item.put", - "circulation.internal.fetch-items", + "circulation.internal.fetch-items.collection.get", "lost-item-fees-policies.item.get", "accounts.collection.get", "users.item.get", @@ -1724,7 +1788,7 @@ "inventory-storage.items.item.get", "inventory-storage.items.collection.get", "inventory-storage.items.item.put", - "circulation.internal.fetch-items", + "circulation.internal.fetch-items.collection.get", "lost-item-fees-policies.item.get", "lost-item-fees-policies.collection.get", "owners.collection.get", @@ -1763,9 +1827,9 @@ "circulation-storage.cancellation-reasons.item.get", "circulation-storage.fixed-due-date-schedules.collection.get", "circulation.rules.notice-policy.get", - "circulation.internal.apply-rules", + "circulation.internal.apply-rules.execute", "inventory-storage.items.item.put", - "circulation.internal.fetch-items", + "circulation.internal.fetch-items.collection.get", "users.item.get", "users.collection.get", "addresstypes.collection.get", @@ -1776,6 +1840,10 @@ "configuration.entries.collection.get", "calendar.endpoint.calendars.surroundingOpenings.get", "calendar.endpoint.calendars.allOpenings.get", + "mod-settings.entries.item.get", + "mod-settings.entries.collection.get", + "mod-settings.global.read.circulation", + "calendar.endpoint.dates.get", "pubsub.publish.post", "circulation-storage.loans-history.collection.get" ], @@ -1808,7 +1876,7 @@ "circulation.rules.request-policy.get", "inventory-storage.items.item.put", "circulation-item.item.put", - "circulation.internal.fetch-items", + "circulation.internal.fetch-items.collection.get", "users.item.get", "users.collection.get", "addresstypes.collection.get", @@ -1829,7 +1897,8 @@ "checkout-lock-storage.checkout-locks.item.delete", "mod-settings.entries.collection.get", "mod-settings.entries.item.get", - "mod-settings.global.read.mod-circulation" + "mod-settings.global.read.mod-circulation", + "mod-settings.global.read.circulation" ], "visible": false }, @@ -1856,7 +1925,7 @@ "circulation.rules.request-policy.get", "inventory-storage.items.item.put", "circulation-item.item.put", - "circulation.internal.fetch-items", + "circulation.internal.fetch-items.collection.get", "users.item.get", "users.collection.get", "addresstypes.collection.get", @@ -1881,8 +1950,10 @@ "scheduled-notice-storage.scheduled-notices.item.delete", "scheduled-notice-storage.scheduled-notices.collection.delete", "scheduled-notice-storage.scheduled-notices.item.post", - "accounts.refund.post", - "accounts.cancel.post", + "feesfines.accounts.refund.item.post", + "feesfines.accounts-bulk.refund.item.post", + "feesfines.accounts.cancel.item.post", + "feesfines.accounts-bulk.cancel.item.post", "configuration.entries.collection.get", "calendar.endpoint.calendars.surroundingOpenings.get", "calendar.endpoint.calendars.allOpenings.get", @@ -1891,12 +1962,15 @@ "actual-cost-fee-fine-cancel.post", "departments.item.get", "departments.collection.get", - "circulation-storage.loans-history.collection.get" + "circulation-storage.loans-history.collection.get", + "mod-settings.entries.item.get", + "mod-settings.entries.collection.get", + "mod-settings.global.read.circulation" ], "visible": false }, { - "permissionName": "circulation.renew-loan", + "permissionName": "circulation.renew-loan.all", "displayName" : "Renew a loan", "description" : "Permissions needed to renew a loan", "subPermissions": [ @@ -1916,7 +1990,7 @@ "circulation.rules.request-policy.get", "circulation.rules.notice-policy.get", "inventory-storage.items.item.put", - "circulation.internal.fetch-items", + "circulation.internal.fetch-items.collection.get", "users.item.get", "users.collection.get", "addresstypes.collection.get", @@ -1924,6 +1998,9 @@ "calendar.endpoint.calendars.surroundingOpenings.get", "calendar.endpoint.calendars.allOpenings.get", "configuration.entries.collection.get", + "mod-settings.entries.item.get", + "mod-settings.entries.collection.get", + "mod-settings.global.read.circulation", "scheduled-notice-storage.scheduled-notices.collection.delete", "scheduled-notice-storage.scheduled-notices.item.post", "patron-notice.post", @@ -1934,9 +2011,10 @@ "feefineactions.item.get", "owners.collection.get", "accounts.item.post", - "accounts.cancel.post", + "feesfines.accounts.cancel.item.post", + "feesfines.accounts-bulk.cancel.item.post", "pubsub.publish.post", - "automated-patron-blocks.collection.get", + "patron-blocks.automated-patron-blocks.collection.get", "scheduled-notice-storage.scheduled-notices.item.delete", "overdue-fines-policies.item.get", "lost-item-fees-policies.item.get", @@ -1947,10 +2025,11 @@ "actual-cost-fee-fine-cancel.post", "circulation-storage.loans-history.collection.get" ], - "visible": false + "visible": false, + "replaces": ["circulation.renew-loan"] }, { - "permissionName": "modperms.circulation.loans.anonymize", + "permissionName": "perms.circulation.loans.anonymize.all", "displayName" : "module permissions for one op", "description" : "to reduce X-Okapi-Token size", "subPermissions": [ @@ -1969,7 +2048,7 @@ "circulation.rules.loan-policy.get", "circulation.rules.request-policy.get", "inventory-storage.items.item.put", - "circulation.internal.fetch-items", + "circulation.internal.fetch-items.collection.get", "users.item.get", "users.collection.get", "addresstypes.collection.get", @@ -1984,7 +2063,8 @@ "pubsub.publish.post", "circulation-storage.loans-history.collection.get" ], - "visible": false + "visible": false, + "replaces": ["modperms.circulation.loans.anonymize"] }, { "permissionName": "modperms.circulation.loans.item.post", @@ -2008,7 +2088,7 @@ "circulation.rules.loan-policy.get", "circulation.rules.request-policy.get", "inventory-storage.items.item.put", - "circulation.internal.fetch-items", + "circulation.internal.fetch-items.collection.get", "users.item.get", "addresstypes.collection.get", "proxiesfor.collection.get", @@ -2024,7 +2104,7 @@ "circulation-storage.loans.collection.get", "circulation-storage.loan-policies.item.get", "circulation-storage.loan-policies.collection.get", - "circulation.internal.fetch-items", + "circulation.internal.fetch-items.collection.get", "users.collection.get", "users.item.get", "addresstypes.collection.get", @@ -2046,7 +2126,7 @@ "circulation-storage.loans.item.get", "circulation-storage.loan-policies.item.get", "circulation-storage.loan-policies.collection.get", - "circulation.internal.fetch-items", + "circulation.internal.fetch-items.collection.get", "accounts.collection.get", "usergroups.collection.get", "usergroups.item.get", @@ -2065,7 +2145,7 @@ "circulation-storage.requests.collection.get", "circulation-storage.loans.collection.get", "circulation-storage.loans.item.get", - "circulation.internal.fetch-items", + "circulation.internal.fetch-items.collection.get", "users.item.get", "users.collection.get", "addresstypes.collection.get", @@ -2095,7 +2175,7 @@ "circulation.rules.loan-policy.get", "circulation.rules.notice-policy.get", "circulation.rules.request-policy.get", - "circulation.internal.fetch-items", + "circulation.internal.fetch-items.collection.get", "inventory-storage.items.item.put", "users.item.get", "users.collection.get", @@ -2109,9 +2189,12 @@ "scheduled-notice-storage.scheduled-notices.collection.delete", "scheduled-notice-storage.scheduled-notices.item.post", "configuration.entries.collection.get", + "mod-settings.entries.item.get", + "mod-settings.entries.collection.get", + "mod-settings.global.read.circulation", "manualblocks.collection.get", "pubsub.publish.post", - "automated-patron-blocks.collection.get", + "patron-blocks.automated-patron-blocks.collection.get", "circulation-storage.loans-history.collection.get", "overdue-fines-policies.item.get", "overdue-fines-policies.collection.get" @@ -2127,7 +2210,7 @@ "circulation-storage.requests.collection.get", "circulation-storage.loans.collection.get", "circulation-storage.loans.item.get", - "circulation.internal.fetch-items", + "circulation.internal.fetch-items.collection.get", "users.item.get", "users.collection.get", "addresstypes.collection.get", @@ -2145,7 +2228,7 @@ "circulation-storage.requests.collection.get", "circulation-storage.loans.collection.get", "circulation-storage.loans.item.get", - "circulation.internal.fetch-items", + "circulation.internal.fetch-items.collection.get", "users.item.get", "users.collection.get", "addresstypes.collection.get", @@ -2181,7 +2264,7 @@ "circulation.rules.request-policy.get", "circulation.rules.notice-policy.get", "inventory-storage.items.item.put", - "circulation.internal.fetch-items", + "circulation.internal.fetch-items.collection.get", "users.item.get", "users.collection.get", "addresstypes.collection.get", @@ -2190,11 +2273,14 @@ "proxiesfor.collection.get", "patron-notice.post", "configuration.entries.collection.get", + "mod-settings.entries.item.get", + "mod-settings.entries.collection.get", + "mod-settings.global.read.circulation", "scheduled-notice-storage.scheduled-notices.collection.delete", "scheduled-notice-storage.scheduled-notices.item.post", "pubsub.publish.post", "manualblocks.collection.get", - "automated-patron-blocks.collection.get", + "patron-blocks.automated-patron-blocks.collection.get", "circulation-storage.loans-history.collection.get" ], "visible": false @@ -2208,7 +2294,7 @@ "circulation-storage.requests.collection.get", "circulation-storage.loans.collection.get", "circulation-storage.loans.item.get", - "circulation.internal.fetch-items", + "circulation.internal.fetch-items.collection.get", "users.item.get", "users.collection.get", "addresstypes.collection.get", @@ -2227,13 +2313,18 @@ "circulation-storage.requests.collection.get", "circulation-storage.loans.collection.get", "circulation-storage.loans.item.get", - "circulation.internal.fetch-items", + "circulation.internal.fetch-items.collection.get", "users.item.get", "users.collection.get", "addresstypes.collection.get", "usergroups.collection.get", "usergroups.item.get", - "pubsub.publish.post" + "pubsub.publish.post", + "configuration.entries.collection.get", + "configuration.entries.item.get", + "mod-settings.entries.item.get", + "mod-settings.entries.collection.get", + "mod-settings.global.read.circulation" ], "visible": false }, @@ -2258,7 +2349,7 @@ "circulation.rules.request-policy.get", "circulation.rules.notice-policy.get", "inventory-storage.items.item.put", - "circulation.internal.fetch-items", + "circulation.internal.fetch-items.collection.get", "users.item.get", "users.collection.get", "addresstypes.collection.get", @@ -2269,10 +2360,13 @@ "calendar.endpoint.calendars.surroundingOpenings.get", "calendar.endpoint.calendars.allOpenings.get", "configuration.entries.collection.get", + "mod-settings.entries.item.get", + "mod-settings.entries.collection.get", + "mod-settings.global.read.circulation", "scheduled-notice-storage.scheduled-notices.collection.delete", "scheduled-notice-storage.scheduled-notices.item.post", "manualblocks.collection.get", - "automated-patron-blocks.collection.get", + "patron-blocks.automated-patron-blocks.collection.get", "pubsub.publish.post", "circulation-storage.fixed-due-date-schedules.collection.get", "circulation-storage.loans-history.collection.get" @@ -2287,7 +2381,7 @@ "circulation-storage.loans.item.get", "circulation-storage.loans.item.put", "inventory-storage.items.item.put", - "circulation.internal.fetch-items", + "circulation.internal.fetch-items.collection.get", "lost-item-fees-policies.item.get", "owners.collection.get", "feefines.collection.get", @@ -2308,7 +2402,7 @@ "visible": false }, { - "permissionName": "modperms.circulation.loans.change-due-date.post", + "permissionName": "perms.circulation.loans.change-due-date.post", "displayName": "module permissions for one op", "description": "to reduce X-Okapi-Token size", "subPermissions": [ @@ -2319,14 +2413,19 @@ "circulation-storage.loans.item.put", "circulation-storage.patron-notice-policies.item.get", "inventory-storage.items.item.put", - "circulation.internal.fetch-items", + "circulation.internal.fetch-items.collection.get", "scheduled-notice-storage.scheduled-notices.collection.delete", "scheduled-notice-storage.scheduled-notices.item.post", "users.item.get", "addresstypes.collection.get", "pubsub.publish.post", "patron-notice.post", - "circulation-storage.loans-history.collection.get" + "circulation-storage.loans-history.collection.get", + "configuration.entries.collection.get", + "configuration.entries.item.get", + "mod-settings.entries.item.get", + "mod-settings.entries.collection.get", + "mod-settings.global.read.circulation" ], "visible": false }, @@ -2337,7 +2436,7 @@ "subPermissions": [ "circulation-storage.loans.item.get", "circulation-storage.loans.item.put", - "circulation.internal.fetch-items", + "circulation.internal.fetch-items.collection.get", "users.item.get", "addresstypes.collection.get", "pubsub.publish.post" @@ -2345,14 +2444,14 @@ "visible": false }, { - "permissionName": "modperms.circulation.loans.claim-item-returned.post", + "permissionName": "perms.circulation.loans.claim-item-returned.post", "displayName": "module permissions for one op", "description": "to reduce X-Okapi-Token size", "subPermissions": [ "circulation-storage.loans.item.get", "circulation-storage.loans.item.put", "inventory-storage.items.item.put", - "circulation.internal.fetch-items", + "circulation.internal.fetch-items.collection.get", "users.item.get", "addresstypes.collection.get", "notes.collection.get", @@ -2360,7 +2459,8 @@ "note.types.collection.get", "pubsub.publish.post" ], - "visible": false + "visible": false, + "replaces": ["modperms.circulation.loans.claim-item-returned.post"] }, { "permissionName": "modperms.circulation.loans.declare-claimed-returned-item-as-missing.post", @@ -2370,7 +2470,7 @@ "circulation-storage.loans.item.get", "circulation-storage.loans.item.put", "inventory-storage.items.item.put", - "circulation.internal.fetch-items", + "circulation.internal.fetch-items.collection.get", "users.item.get", "addresstypes.collection.get", "notes.item.post", @@ -2399,7 +2499,7 @@ "circulation.rules.request-policy.get", "circulation.rules.notice-policy.get", "inventory-storage.items.item.put", - "circulation.internal.fetch-items", + "circulation.internal.fetch-items.collection.get", "proxiesfor.collection.get", "proxiesfor.collection.get", "users.item.get", @@ -2420,7 +2520,7 @@ "displayName": "module permissions for one op", "description": "to reduce X-Okapi-Token size", "subPermissions": [ - "circulation.internal.fetch-items", + "circulation.internal.fetch-items.collection.get", "circulation-storage.requests.item.get", "circulation-storage.requests.collection.get", "circulation-storage.loans.collection.get", @@ -2433,7 +2533,7 @@ "displayName": "module permissions for one op", "description": "to reduce X-Okapi-Token size", "subPermissions": [ - "circulation.internal.fetch-items", + "circulation.internal.fetch-items.collection.get", "circulation-storage.requests.item.get", "circulation-storage.requests.collection.get", "users.item.get", @@ -2448,7 +2548,7 @@ "displayName": "module permissions for one op", "description": "to reduce X-Okapi-Token size", "subPermissions": [ - "circulation.internal.fetch-items", + "circulation.internal.fetch-items.collection.get", "circulation-storage.requests.item.get", "circulation-storage.requests.collection.get", "inventory-storage.holdings.item.get", @@ -2463,7 +2563,7 @@ "visible": false }, { - "permissionName": "circulation.internal.fetch-items", + "permissionName": "circulation.internal.fetch-items.collection.get", "displayName" : "Fetch item(s)", "description" : "Internal permission set for fetching item(s)", "subPermissions": [ @@ -2492,10 +2592,11 @@ "inventory-storage.identifier-types.item.get", "inventory-storage.identifier-types.collection.get" ], - "visible": false + "visible": false, + "replaces": ["circulation.internal.fetch-items"] }, { - "permissionName": "circulation.internal.apply-rules", + "permissionName": "circulation.internal.apply-rules.execute", "displayName" : "Apply circulation rules", "description" : "Internal permission set for applying circulation rules", "subPermissions": [ @@ -2509,7 +2610,8 @@ "circulation-storage.patron-notice-policies.item.get", "circulation-storage.patron-notice-policies.collection.get" ], - "visible": false + "visible": false, + "replaces": ["circulation.internal.apply-rules"] } ], "launchDescriptor": { diff --git a/pom.xml b/pom.xml index 4572bafd77..3821e1f663 100644 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 mod-circulation org.folio - 24.3.2-SNAPSHOT + 24.3.3-SNAPSHOT Apache License 2.0 @@ -33,6 +33,9 @@ 1.18.28 6.1.5 3.9.1 + + + 1.0.1 @@ -300,7 +303,7 @@ https://github.com/folio-org/mod-inventory scm:git:git://github.com:folio-org/mod-inventory.git scm:git:git@github.com:folio-org/mod-inventory.git - v24.3.0 + HEAD @@ -518,6 +521,21 @@ false + + org.folio + folio-module-descriptor-validator + ${folio-module-descriptor-validator.version} + + + + validate + + + + + false + + @@ -529,4 +547,14 @@ + + + + folio-nexus + FOLIO Maven repository + https://repository.folio.org/repository/maven-folio + + + + diff --git a/ramls/circulation.raml b/ramls/circulation.raml index ea45ea2779..dc8287451f 100644 --- a/ramls/circulation.raml +++ b/ramls/circulation.raml @@ -337,6 +337,14 @@ resourceTypes: description: "Instance ID" 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}$" required: false + useStubItem: + description: "When true, allows to apply circulation rules based on patron group only" + type: boolean + required: false + ecsRequestRouting: + description: "When true, returns only service points with ecsRequestRouting" + type: boolean + required: false responses: 200: description: "List of allowed service points was retrieved successfully" diff --git a/src/main/java/org/folio/circulation/CirculationVerticle.java b/src/main/java/org/folio/circulation/CirculationVerticle.java index 4956ddb53a..837d519891 100644 --- a/src/main/java/org/folio/circulation/CirculationVerticle.java +++ b/src/main/java/org/folio/circulation/CirculationVerticle.java @@ -21,6 +21,7 @@ import org.folio.circulation.resources.FeeFineNotRealTimeScheduledNoticeProcessingResource; import org.folio.circulation.resources.FeeFineScheduledNoticeProcessingResource; import org.folio.circulation.resources.HealthResource; +import org.folio.circulation.resources.ItemsByInstanceResource; import org.folio.circulation.resources.ItemsInTransitResource; import org.folio.circulation.resources.LoanAnonymizationResource; import org.folio.circulation.resources.LoanCirculationRulesEngineResource; @@ -94,6 +95,7 @@ public void start(Promise startFuture) { new RequestCollectionResource(client).register(router); new RequestQueueResource(client).register(router); new RequestByInstanceIdResource(client).register(router); + new ItemsByInstanceResource(client).register(router); new RequestHoldShelfClearanceResource( "/circulation/requests-reports/hold-shelf-clearance/:servicePointId", client) diff --git a/src/main/java/org/folio/circulation/domain/AllowedServicePointsRequest.java b/src/main/java/org/folio/circulation/domain/AllowedServicePointsRequest.java index 1e4bc603ef..c0ec5b2f06 100644 --- a/src/main/java/org/folio/circulation/domain/AllowedServicePointsRequest.java +++ b/src/main/java/org/folio/circulation/domain/AllowedServicePointsRequest.java @@ -17,8 +17,12 @@ public class AllowedServicePointsRequest { private Request.Operation operation; private String requesterId; + private String patronGroupId; private String instanceId; private String itemId; + private String requestId; + private boolean useStubItem; + private boolean ecsRequestRouting; public boolean isForTitleLevelRequest() { return instanceId != null; @@ -27,7 +31,6 @@ public boolean isForTitleLevelRequest() { public boolean isForItemLevelRequest() { return itemId != null; } - private String requestId; public AllowedServicePointsRequest updateWithRequestInformation(Request request) { log.debug("updateWithRequestInformation:: parameters request: {}", request); diff --git a/src/main/java/org/folio/circulation/domain/CreateRequestService.java b/src/main/java/org/folio/circulation/domain/CreateRequestService.java index 3893a9dd3a..66ec8fff43 100644 --- a/src/main/java/org/folio/circulation/domain/CreateRequestService.java +++ b/src/main/java/org/folio/circulation/domain/CreateRequestService.java @@ -227,6 +227,11 @@ private CompletableFuture> checkPolicy( boolean tlrFeatureEnabled = request.getTlrSettingsConfiguration().isTitleLevelRequestsFeatureEnabled(); if (tlrFeatureEnabled && request.isTitleLevel() && request.isHold()) { + if (request.getEcsRequestPhase() == EcsRequestPhase.PRIMARY) { + log.warn("checkPolicy:: ECS TLR primary Hold detected, skipping policy check"); + return ofAsync(() -> records); + } + log.info("checkPolicy:: checking policy for title-level hold"); return completedFuture(checkPolicyForTitleLevelHold(records)); } diff --git a/src/main/java/org/folio/circulation/domain/EcsRequestPhase.java b/src/main/java/org/folio/circulation/domain/EcsRequestPhase.java new file mode 100644 index 0000000000..8272931b2e --- /dev/null +++ b/src/main/java/org/folio/circulation/domain/EcsRequestPhase.java @@ -0,0 +1,37 @@ +package org.folio.circulation.domain; + +import static org.apache.commons.lang3.StringUtils.equalsIgnoreCase; + +import java.util.Arrays; + +public enum EcsRequestPhase { + NONE(""), + PRIMARY("Primary"), + SECONDARY("Secondary"); + + public final String value; + + public static EcsRequestPhase from(String value) { + return Arrays.stream(values()) + .filter(status -> status.nameMatches(value)) + .findFirst() + .orElse(NONE); + } + + EcsRequestPhase(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public boolean nameMatches(String value) { + return equalsIgnoreCase(getValue(), value); + } + + @Override + public String toString() { + return value; + } +} diff --git a/src/main/java/org/folio/circulation/domain/Item.java b/src/main/java/org/folio/circulation/domain/Item.java index d58a83346b..5fd69c5255 100644 --- a/src/main/java/org/folio/circulation/domain/Item.java +++ b/src/main/java/org/folio/circulation/domain/Item.java @@ -447,6 +447,17 @@ public String getDcbItemTitle() { return getProperty(itemRepresentation, "instanceTitle"); } + public String getTenantId() { + return getProperty(itemRepresentation, "tenantId"); + } + + public Item changeTenantId(String tenantId) { + if (itemRepresentation != null) { + write(itemRepresentation, "tenantId", tenantId); + } + return this; + } + public boolean isAtLocation(String locationCode) { return locationCode != null && getLocation() != null && ( locationCode.equals(getLocation().getCode()) || diff --git a/src/main/java/org/folio/circulation/domain/MoveRequestService.java b/src/main/java/org/folio/circulation/domain/MoveRequestService.java index 3077016093..d13be99cb9 100644 --- a/src/main/java/org/folio/circulation/domain/MoveRequestService.java +++ b/src/main/java/org/folio/circulation/domain/MoveRequestService.java @@ -10,6 +10,7 @@ import org.apache.logging.log4j.Logger; import org.folio.circulation.domain.validation.RequestLoanValidator; import org.folio.circulation.infrastructure.storage.ConfigurationRepository; +import org.folio.circulation.infrastructure.storage.SettingsRepository; import org.folio.circulation.infrastructure.storage.requests.RequestPolicyRepository; import org.folio.circulation.infrastructure.storage.requests.RequestQueueRepository; import org.folio.circulation.infrastructure.storage.requests.RequestRepository; @@ -27,13 +28,14 @@ public class MoveRequestService { private final ConfigurationRepository configurationRepository; private final EventPublisher eventPublisher; private final RequestQueueRepository requestQueueRepository; + private final SettingsRepository settingsRepository; private static final Logger log = LogManager.getLogger(MethodHandles.lookup().lookupClass()); public MoveRequestService(RequestRepository requestRepository, RequestPolicyRepository requestPolicyRepository, UpdateUponRequest updateUponRequest, MoveRequestProcessAdapter moveRequestHelper, RequestLoanValidator requestLoanValidator, RequestNoticeSender requestNoticeSender, ConfigurationRepository configurationRepository, EventPublisher eventPublisher, - RequestQueueRepository requestQueueRepository) { + RequestQueueRepository requestQueueRepository, SettingsRepository settingsRepository) { this.requestRepository = requestRepository; this.requestPolicyRepository = requestPolicyRepository; @@ -44,11 +46,12 @@ public MoveRequestService(RequestRepository requestRepository, RequestPolicyRepo this.configurationRepository = configurationRepository; this.eventPublisher = eventPublisher; this.requestQueueRepository = requestQueueRepository; + this.settingsRepository = settingsRepository; } public CompletableFuture> moveRequest( RequestAndRelatedRecords requestAndRelatedRecords, Request originalRequest) { - return configurationRepository.lookupTlrSettings() + return settingsRepository.lookupTlrSettings() .thenApply(r -> r.map(requestAndRelatedRecords::withTlrSettings)) .thenApply(r -> r.next(RequestServiceUtility::refuseTlrProcessingWhenFeatureIsDisabled)) .thenApply(r -> r.next(records -> RequestServiceUtility.refuseMovingToOrFromHoldTlr(records, diff --git a/src/main/java/org/folio/circulation/domain/Request.java b/src/main/java/org/folio/circulation/domain/Request.java index 4075fdf50d..597cda08ef 100644 --- a/src/main/java/org/folio/circulation/domain/Request.java +++ b/src/main/java/org/folio/circulation/domain/Request.java @@ -18,6 +18,7 @@ import static org.folio.circulation.domain.representations.RequestProperties.CANCELLATION_REASON_NAME; import static org.folio.circulation.domain.representations.RequestProperties.CANCELLATION_REASON_PUBLIC_DESCRIPTION; import static org.folio.circulation.domain.representations.RequestProperties.ITEM_LOCATION_CODE; +import static org.folio.circulation.domain.representations.RequestProperties.ECS_REQUEST_PHASE; import static org.folio.circulation.domain.representations.RequestProperties.HOLDINGS_RECORD_ID; import static org.folio.circulation.domain.representations.RequestProperties.HOLD_SHELF_EXPIRATION_DATE; import static org.folio.circulation.domain.representations.RequestProperties.INSTANCE_ID; @@ -269,6 +270,10 @@ public RequestType getRequestType() { return RequestType.from(getProperty(requestRepresentation, REQUEST_TYPE)); } + public EcsRequestPhase getEcsRequestPhase() { + return EcsRequestPhase.from(getProperty(requestRepresentation, ECS_REQUEST_PHASE)); + } + boolean allowedForItem() { return RequestTypeItemStatusWhiteList.canCreateRequestForItem(getItem().getStatus(), getRequestType()); } diff --git a/src/main/java/org/folio/circulation/domain/SearchInstance.java b/src/main/java/org/folio/circulation/domain/SearchInstance.java new file mode 100644 index 0000000000..809a121181 --- /dev/null +++ b/src/main/java/org/folio/circulation/domain/SearchInstance.java @@ -0,0 +1,50 @@ +package org.folio.circulation.domain; + +import static org.folio.circulation.support.json.JsonObjectArrayPropertyFetcher.mapToList; +import static org.folio.circulation.support.json.JsonPropertyWriter.write; + +import java.lang.invoke.MethodHandles; +import java.util.Collection; +import java.util.List; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.folio.circulation.domain.representations.ItemSummaryRepresentation; +import org.folio.circulation.storage.mappers.ItemMapper; + +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; +import lombok.NonNull; +import lombok.ToString; +import lombok.Value; + +@Value +@ToString(onlyExplicitlyIncluded = true) +public class SearchInstance { + + private static final Logger log = LogManager.getLogger(MethodHandles.lookup().lookupClass()); + JsonObject representation; + String id; + @NonNull Collection items; + + public static SearchInstance from(JsonObject representation) { + return new SearchInstance(representation, representation.getString("id"), mapItems(representation)); + } + + private static List mapItems(JsonObject representation) { + return mapToList(representation, "items", new ItemMapper()::toDomain); + } + + public SearchInstance changeItems(Collection items) { + JsonArray itemsArray = new JsonArray(); + for (Item item : items) { + itemsArray.add(new ItemSummaryRepresentation().createItemSummary(item)); + } + write(representation, "items", itemsArray); + return new SearchInstance(representation, id, items); + } + + public JsonObject toJson() { + return representation; + } +} diff --git a/src/main/java/org/folio/circulation/domain/configuration/TlrSettingsConfiguration.java b/src/main/java/org/folio/circulation/domain/configuration/TlrSettingsConfiguration.java index 75b12913ee..a16b61c95f 100644 --- a/src/main/java/org/folio/circulation/domain/configuration/TlrSettingsConfiguration.java +++ b/src/main/java/org/folio/circulation/domain/configuration/TlrSettingsConfiguration.java @@ -11,12 +11,14 @@ import io.vertx.core.json.JsonObject; import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.ToString; @AllArgsConstructor @Getter @ToString(onlyExplicitlyIncluded = true) +@EqualsAndHashCode public class TlrSettingsConfiguration { protected static final Logger log = LogManager.getLogger(MethodHandles.lookup().lookupClass()); diff --git a/src/main/java/org/folio/circulation/domain/override/OverridableBlockType.java b/src/main/java/org/folio/circulation/domain/override/OverridableBlockType.java index f28b744411..8880db0086 100644 --- a/src/main/java/org/folio/circulation/domain/override/OverridableBlockType.java +++ b/src/main/java/org/folio/circulation/domain/override/OverridableBlockType.java @@ -9,15 +9,15 @@ @RequiredArgsConstructor public enum OverridableBlockType { PATRON_BLOCK("patronBlock", - OkapiPermissions.of("circulation.override-patron-block")), + OkapiPermissions.of("circulation.override-patron-block.post")), ITEM_LIMIT_BLOCK("itemLimitBlock", - OkapiPermissions.of("circulation.override-item-limit-block")), + OkapiPermissions.of("circulation.override-item-limit-block.post")), ITEM_NOT_LOANABLE_BLOCK("itemNotLoanableBlock", - OkapiPermissions.of("circulation.override-item-not-loanable-block")), + OkapiPermissions.of("circulation.override-item-not-loanable-block.post")), RENEWAL_BLOCK("renewalBlock", - OkapiPermissions.of("circulation.override-renewal-block")), + OkapiPermissions.of("circulation.override-renewal-block.post")), RENEWAL_DUE_DATE_REQUIRED_BLOCK("renewalDueDateRequiredBlock", - OkapiPermissions.of("circulation.override-renewal-block")); + OkapiPermissions.of("circulation.override-renewal-block.post")); private final String name; private final OkapiPermissions requiredOverridePermissions; @@ -25,4 +25,4 @@ public enum OverridableBlockType { public OkapiPermissions getMissingOverridePermissions(OkapiPermissions existingPermissions) { return requiredOverridePermissions.getAllNotContainedIn(existingPermissions); } -} \ No newline at end of file +} diff --git a/src/main/java/org/folio/circulation/domain/representations/ItemSummaryRepresentation.java b/src/main/java/org/folio/circulation/domain/representations/ItemSummaryRepresentation.java index f18255537b..1dde6fc960 100644 --- a/src/main/java/org/folio/circulation/domain/representations/ItemSummaryRepresentation.java +++ b/src/main/java/org/folio/circulation/domain/representations/ItemSummaryRepresentation.java @@ -46,6 +46,7 @@ public JsonObject createItemSummary(Item item) { write(itemSummary, "copyNumber", item.getCopyNumber()); write(itemSummary, CALL_NUMBER_COMPONENTS, createCallNumberComponents(item.getCallNumberComponents())); + write(itemSummary, "tenantId", item.getTenantId()); JsonObject status = new JsonObject() .put("name", item.getStatus().getValue()); diff --git a/src/main/java/org/folio/circulation/domain/representations/RequestProperties.java b/src/main/java/org/folio/circulation/domain/representations/RequestProperties.java index 830f2109d7..2be39bd4a0 100644 --- a/src/main/java/org/folio/circulation/domain/representations/RequestProperties.java +++ b/src/main/java/org/folio/circulation/domain/representations/RequestProperties.java @@ -9,6 +9,7 @@ private RequestProperties() { } public static final String HOLDINGS_RECORD_ID = "holdingsRecordId"; public static final String REQUEST_LEVEL = "requestLevel"; public static final String REQUEST_TYPE = "requestType"; + public static final String ECS_REQUEST_PHASE = "ecsRequestPhase"; public static final String PROXY_USER_ID = "proxyUserId"; public static final String POSITION = "position"; public static final String HOLD_SHELF_EXPIRATION_DATE = "holdShelfExpirationDate"; diff --git a/src/main/java/org/folio/circulation/infrastructure/storage/ConfigurationRepository.java b/src/main/java/org/folio/circulation/infrastructure/storage/ConfigurationRepository.java index 52a974504d..992628921e 100644 --- a/src/main/java/org/folio/circulation/infrastructure/storage/ConfigurationRepository.java +++ b/src/main/java/org/folio/circulation/infrastructure/storage/ConfigurationRepository.java @@ -20,7 +20,9 @@ import org.folio.circulation.support.results.Result; import io.vertx.core.json.JsonObject; +import lombok.extern.log4j.Log4j2; +@Log4j2 public class ConfigurationRepository { private static final String CONFIGS_KEY = "configs"; private static final String MODULE_NAME_KEY = "module"; @@ -50,6 +52,7 @@ public CompletableFuture> lookupSessionTimeout() { } public CompletableFuture> lookupTlrSettings() { + log.info("lookupTlrSettings:: fetching TLR configuration"); Result queryResult = defineModuleNameAndConfigNameFilter( "SETTINGS", "TLR"); diff --git a/src/main/java/org/folio/circulation/infrastructure/storage/SearchRepository.java b/src/main/java/org/folio/circulation/infrastructure/storage/SearchRepository.java new file mode 100644 index 0000000000..ed441ce461 --- /dev/null +++ b/src/main/java/org/folio/circulation/infrastructure/storage/SearchRepository.java @@ -0,0 +1,94 @@ +package org.folio.circulation.infrastructure.storage; + +import static org.folio.circulation.support.StringUtil.urlEncode; +import static org.folio.circulation.support.results.Result.emptyAsync; +import static org.folio.circulation.support.results.Result.failed; +import static org.folio.circulation.support.results.ResultBinding.flatMapResult; +import static org.folio.circulation.support.results.ResultBinding.mapResult; + +import java.lang.invoke.MethodHandles; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.folio.circulation.domain.Item; +import org.folio.circulation.domain.MultipleRecords; +import org.folio.circulation.domain.SearchInstance; +import org.folio.circulation.infrastructure.storage.inventory.ItemRepository; +import org.folio.circulation.support.AsyncCoordinationUtil; +import org.folio.circulation.support.BadRequestFailure; +import org.folio.circulation.support.Clients; +import org.folio.circulation.support.CollectionResourceClient; +import org.folio.circulation.support.http.client.Response; +import org.folio.circulation.support.http.server.WebContext; +import org.folio.circulation.support.results.Result; + +import io.vertx.core.http.HttpClient; +import lombok.AllArgsConstructor; + +@AllArgsConstructor +public class SearchRepository { + + private static final Logger log = LogManager.getLogger(MethodHandles.lookup().lookupClass()); + private final WebContext webContext; + private final HttpClient httpClient; + private final CollectionResourceClient searchClient; + + public SearchRepository(WebContext webContext, HttpClient httpClient) { + this.webContext = webContext; + this.httpClient = httpClient; + this.searchClient = Clients.create(webContext, httpClient).searchClient(); + } + + public CompletableFuture> getInstanceWithItems(List queryParams) { + log.debug("getInstanceWithItems:: query {}", queryParams); + if (queryParams.isEmpty()) { + return CompletableFuture.completedFuture(failed(new BadRequestFailure( + "query is empty"))); + } + return searchClient.getManyWithQueryStringParameters(Map.of("expandAll", + "true", "query", urlEncode(queryParams.get(0)))) + .thenApply(flatMapResult(this::mapResponseToInstances)) + .thenApply(mapResult(MultipleRecords::firstOrNull)) + .thenCompose(r -> r.after(this::updateItemDetails)); + } + + private Result> mapResponseToInstances(Response response) { + return MultipleRecords.from(response, SearchInstance::from, "instances"); + } + + private CompletableFuture> updateItemDetails(SearchInstance searchInstance) { + log.debug("updateItemDetails:: searchInstance {}", () -> searchInstance); + if (searchInstance == null || searchInstance.getId() == null) { + log.info("updateItemDetails:: searchInstance is empty"); + return emptyAsync(); + } + + Map> itemsByTenant = searchInstance.getItems() + .stream() + .collect(Collectors.groupingBy(Item::getTenantId)); + + log.info("updateItemDetails:: fetching item details from tenants: {}", itemsByTenant::keySet); + + return AsyncCoordinationUtil.allOf(itemsByTenant, this::fetchItemDetails) + .thenApply(r -> r.map(lists -> lists.stream().flatMap(Collection::stream).toList())) + .thenApply(r -> r.map(searchInstance::changeItems)); + } + + private CompletableFuture>> fetchItemDetails(String tenantId, List items) { + ItemRepository itemRepository = new ItemRepository(Clients.create(webContext, httpClient, tenantId)); + + return AsyncCoordinationUtil.allOf(items, item -> fetchItemDetails(item, itemRepository)); + } + + private CompletableFuture> fetchItemDetails(Item searchItem, + ItemRepository itemRepository) { + + return itemRepository.fetchById(searchItem.getItemId()) + .thenApply(r -> r.map(item -> item.changeTenantId(searchItem.getTenantId()))); + } +} diff --git a/src/main/java/org/folio/circulation/infrastructure/storage/ServicePointRepository.java b/src/main/java/org/folio/circulation/infrastructure/storage/ServicePointRepository.java index fe9ec5c7e1..7225491566 100644 --- a/src/main/java/org/folio/circulation/infrastructure/storage/ServicePointRepository.java +++ b/src/main/java/org/folio/circulation/infrastructure/storage/ServicePointRepository.java @@ -41,7 +41,15 @@ public class ServicePointRepository { private final CollectionResourceClient servicePointsStorageClient; public ServicePointRepository(Clients clients) { - servicePointsStorageClient = clients.servicePointsStorage(); + this(clients, false); + } + + public ServicePointRepository(Clients clients, boolean includeRoutingServicePoints) { + if (includeRoutingServicePoints) { + servicePointsStorageClient = clients.routingServicePointsStorage(); + } else { + servicePointsStorageClient = clients.servicePointsStorage(); + } } public CompletableFuture> getServicePointById(UUID id) { @@ -206,22 +214,24 @@ public CompletableFuture>> findServicePointsById .thenApply(r -> r.map(MultipleRecords::getRecords)); } - public CompletableFuture>> fetchPickupLocationServicePoints() { + public CompletableFuture>> fetchServicePointsByIndexName( + String indexName) { + return createServicePointsFetcher().find(MultipleCqlIndexValuesCriteria.builder() - .indexName("pickupLocation") + .indexName(indexName) .indexOperator(CqlQuery::matchAny) .value("true") .build()) .thenApply(r -> r.map(MultipleRecords::getRecords)); } - public CompletableFuture>> fetchPickupLocationServicePointsByIds( - Set ids) { + public CompletableFuture>> + fetchPickupLocationServicePointsByIdsAndIndexName(Set ids, String indexName) { - log.debug("filterIdsByServicePointsAndPickupLocationExistence:: parameters ids: {}", - () -> collectionAsString(ids)); + log.debug("filterIdsByServicePointsAndPickupLocationExistence:: parameters ids: {}, " + + "indexName: {}", () -> collectionAsString(ids), () -> indexName); - Result pickupLocationQuery = exactMatch("pickupLocation", "true"); + Result pickupLocationQuery = exactMatch(indexName, "true"); return createServicePointsFetcher().findByIdIndexAndQuery(ids, "id", pickupLocationQuery) .thenApply(r -> r.map(MultipleRecords::getRecords)); diff --git a/src/main/java/org/folio/circulation/infrastructure/storage/SettingsRepository.java b/src/main/java/org/folio/circulation/infrastructure/storage/SettingsRepository.java index 89f2e845ba..cd738cd94b 100644 --- a/src/main/java/org/folio/circulation/infrastructure/storage/SettingsRepository.java +++ b/src/main/java/org/folio/circulation/infrastructure/storage/SettingsRepository.java @@ -6,6 +6,7 @@ import org.folio.circulation.domain.Configuration; import org.folio.circulation.domain.MultipleRecords; import org.folio.circulation.domain.configuration.CheckoutLockConfiguration; +import org.folio.circulation.domain.configuration.TlrSettingsConfiguration; import org.folio.circulation.support.Clients; import org.folio.circulation.support.GetManyRecordsClient; import org.folio.circulation.support.http.client.CqlQuery; @@ -13,28 +14,31 @@ import org.folio.circulation.support.results.Result; import java.lang.invoke.MethodHandles; +import java.util.Collection; +import java.util.List; import java.util.concurrent.CompletableFuture; +import static java.util.function.Function.identity; import static org.folio.circulation.support.http.client.CqlQuery.exactMatch; +import static org.folio.circulation.support.http.client.CqlQuery.exactMatchAny; +import static org.folio.circulation.support.results.Result.ofAsync; import static org.folio.circulation.support.results.Result.succeeded; public class SettingsRepository { private static final Logger log = LogManager.getLogger(MethodHandles.lookup().lookupClass()); private final GetManyRecordsClient settingsClient; + private final ConfigurationRepository configurationRepository; public SettingsRepository(Clients clients) { settingsClient = clients.settingsStorageClient(); + configurationRepository = new ConfigurationRepository(clients); } public CompletableFuture> lookUpCheckOutLockSettings() { + log.debug("lookUpCheckOutLockSettings:: fetching checkout lock settings"); try { - log.debug("lookUpCheckOutLockSettings:: fetching checkout lock settings"); - final Result moduleQuery = exactMatch("scope", "mod-circulation"); - final Result configNameQuery = exactMatch("key", "checkoutLockFeature"); - - return moduleQuery.combine(configNameQuery, CqlQuery::and) - .after(cqlQuery -> settingsClient.getMany(cqlQuery, PageLimit.noLimit())) - .thenApply(result -> result.next(response -> MultipleRecords.from(response, Configuration::new, "items"))) + return fetchSettings("mod-circulation", "checkoutLockFeature") + .thenApply(r -> r.map(records -> records.mapRecords(Configuration::new))) .thenApply(r -> r.map(r1 -> r1.getRecords().stream().findFirst() .map(Configuration::getValue) .map(JsonObject::new) @@ -49,4 +53,40 @@ public CompletableFuture> lookUpCheckOutLockSe return CompletableFuture.completedFuture(succeeded(CheckoutLockConfiguration.from(new JsonObject()))); } } + + public CompletableFuture> lookupTlrSettings() { + log.info("lookupTlrSettings:: fetching TLR settings"); + return fetchSettings("circulation", List.of("generalTlr", "regularTlr")) + .thenApply(r -> r.map(SettingsRepository::extractAndMergeValues)) + .thenCompose(r -> r.after(this::buildTlrSettings)); + } + + private CompletableFuture>> fetchSettings(String scope, String key) { + return fetchSettings(scope, List.of(key)); + } + + private CompletableFuture>> fetchSettings(String scope, + Collection keys) { + + return exactMatch("scope", scope) + .combine(exactMatchAny("key", keys), CqlQuery::and) + .after(query -> settingsClient.getMany(query, PageLimit.noLimit())) + .thenApply(r -> r.next(response -> MultipleRecords.from(response, identity(), "items"))); + } + + private static JsonObject extractAndMergeValues(MultipleRecords entries) { + return entries.getRecords() + .stream() + .map(rec -> rec.getJsonObject("value")) + .reduce(new JsonObject(), JsonObject::mergeIn); + } + + private CompletableFuture> buildTlrSettings(JsonObject tlrSettings) { + if (tlrSettings.isEmpty()) { + log.info("getTlrSettings:: failed to find TLR settings, falling back to legacy configuration"); + return configurationRepository.lookupTlrSettings(); + } + + return ofAsync(TlrSettingsConfiguration.from(tlrSettings)); + } } diff --git a/src/main/java/org/folio/circulation/infrastructure/storage/requests/RequestPolicyRepository.java b/src/main/java/org/folio/circulation/infrastructure/storage/requests/RequestPolicyRepository.java index 60f35c780d..42a1fe5abf 100644 --- a/src/main/java/org/folio/circulation/infrastructure/storage/requests/RequestPolicyRepository.java +++ b/src/main/java/org/folio/circulation/infrastructure/storage/requests/RequestPolicyRepository.java @@ -15,6 +15,7 @@ import java.util.Collection; import java.util.Map; import java.util.Set; +import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.function.BinaryOperator; import java.util.stream.Collectors; @@ -74,18 +75,18 @@ public CompletableFuture> lookupRequestPolicies(Request request) public CompletableFuture> lookupRequestPolicy(Item item, User user) { log.debug("lookupRequestPolicy:: parameters item: {}, user: {}", item, user); return lookupRequestPolicyId(item, user) - .thenComposeAsync(r -> r.after(this::lookupRequestPolicy)) + .thenComposeAsync(r -> r.after(this::lookupRequestPolicyById)) .thenApply(result -> result.map(RequestPolicy::from)); } public CompletableFuture>>> lookupRequestPolicies( - Collection items, User user) { + Collection items, String patronGroupId) { - log.debug("lookupRequestPolicies:: parameters items: {}, user: {}", - items::size, () -> asJson(user)); + log.debug("lookupRequestPolicies:: parameters items: {}, patronGroupId: {}", + items::size, () -> asJson(patronGroupId)); Map> criteriaMap = items.stream() - .map(item -> new CirculationRuleCriteria(item, user)) + .map(item -> new CirculationRuleCriteria(item, patronGroupId)) .collect(toMap(identity(), criteria -> Set.of(criteria.getItem()), itemsMergeOperator())); return allOf(criteriaMap.entrySet(), entry -> lookupRequestPolicyId(entry.getKey()) @@ -95,12 +96,21 @@ public CompletableFuture>>> lookupRequestPol .thenCompose(r -> r.after(this::lookupRequestPolicies)); } + public CompletableFuture> lookupRequestPolicy(String patronGroupId) { + // Circulation rules need to be executed with the patron group parameter only. + // All the item-related parameters should be random UUIDs. + return lookupRequestPolicyId(UUID.randomUUID().toString(), patronGroupId, + UUID.randomUUID().toString(), UUID.randomUUID().toString()) + .thenCompose(r -> r.after(this::lookupRequestPolicyById)) + .thenApply(result -> result.map(RequestPolicy::from)); + } + private BinaryOperator> itemsMergeOperator() { return (items1, items2) -> Stream.concat(items1.stream(), items2.stream()) .collect(Collectors.toSet()); } - private CompletableFuture> lookupRequestPolicy( + private CompletableFuture> lookupRequestPolicyById( String requestPolicyId) { log.debug("lookupRequestPolicy:: parameters requestPolicyId: {}", requestPolicyId); diff --git a/src/main/java/org/folio/circulation/infrastructure/storage/requests/RequestQueueRepository.java b/src/main/java/org/folio/circulation/infrastructure/storage/requests/RequestQueueRepository.java index bcfa69eec3..e4ecf9fdcb 100644 --- a/src/main/java/org/folio/circulation/infrastructure/storage/requests/RequestQueueRepository.java +++ b/src/main/java/org/folio/circulation/infrastructure/storage/requests/RequestQueueRepository.java @@ -1,5 +1,6 @@ package org.folio.circulation.infrastructure.storage.requests; +import static java.util.Collections.emptyList; import static java.util.concurrent.CompletableFuture.completedFuture; import static org.folio.circulation.domain.RequestLevel.ITEM; import static org.folio.circulation.domain.RequestLevel.TITLE; @@ -7,13 +8,16 @@ import static org.folio.circulation.support.http.client.CqlQuery.exactMatch; import static org.folio.circulation.support.http.client.CqlQuery.exactMatchAny; import static org.folio.circulation.support.http.client.PageLimit.oneThousand; +import static org.folio.circulation.support.results.Result.ofAsync; import static org.folio.circulation.support.results.Result.succeeded; import static org.folio.circulation.support.results.ResultBinding.mapResult; -import static org.folio.circulation.support.utils.LogUtil.collectionAsString; import java.lang.invoke.MethodHandles; import java.util.Collection; +import java.util.EnumSet; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; @@ -60,9 +64,10 @@ public CompletableFuture> get(RequestAndRelated public CompletableFuture> getQueue(TlrSettingsConfiguration tlrSettings, String instanceId, String itemId) { - return tlrSettings != null && tlrSettings.isTitleLevelRequestsFeatureEnabled() - ? getByInstanceId(instanceId) - : getByItemId(itemId); + boolean isTlrEnabled = tlrSettings != null && tlrSettings.isTitleLevelRequestsFeatureEnabled(); + log.info("getQueue:: TLR feature is {}", isTlrEnabled ? "enabled" : "disabled"); + + return isTlrEnabled ? getByInstanceId(instanceId) : getByItemId(itemId); } public CompletableFuture> get(RenewalContext context) { @@ -74,33 +79,47 @@ public CompletableFuture> get(RenewalContext context) { ).thenApply(result -> result.map(context::withRequestQueue)); } + public CompletableFuture> getByInstanceIdAndItemId(String instanceId, + String itemId) { + + return get(itemId, instanceId, EnumSet.of(ITEM, TITLE)); + } + public CompletableFuture> getByInstanceId(String instanceId) { - return get("instanceId", instanceId, List.of(ITEM, TITLE)); + return get(null, instanceId, EnumSet.of(ITEM, TITLE)); } public CompletableFuture> getByItemId(String itemId) { - return get("itemId", itemId, List.of(ITEM)); + return get(itemId, null, EnumSet.of(ITEM)); } - private CompletableFuture> get(String idFieldName, String id, - Collection requestLevels) { + private CompletableFuture> get(String itemId, String instanceId, + EnumSet requestLevels) { - log.debug("get:: parameters idFieldName: {}, id: {}, requestLevels: {}", - () -> idFieldName, () -> id, () -> collectionAsString(requestLevels)); + Map filters = new HashMap<>(); + if (itemId != null) { + filters.put("itemId", itemId); + } + if (instanceId != null) { + filters.put("instanceId", instanceId); + } + if (filters.isEmpty()) { + log.info("get:: itemId and instanceId are null, returning an empty queue"); + return ofAsync(new RequestQueue(emptyList())); + } List requestLevelStrings = requestLevels.stream() .map(RequestLevel::getValue) .collect(Collectors.toList()); - final Result itemIdQuery = exactMatch(idFieldName, id); final Result statusQuery = exactMatchAny("status", RequestStatus.openStates()); final Result requestLevelQuery = exactMatchAny("requestLevel", requestLevelStrings); - return itemIdQuery.combine(statusQuery, CqlQuery::and) + return CqlQuery.exactMatchAny(filters) + .combine(statusQuery, CqlQuery::and) .combine(requestLevelQuery, CqlQuery::and) .map(q -> q.sortBy(ascending("position"))) - .after(query -> requestRepository.findBy(query, - MAXIMUM_SUPPORTED_REQUEST_QUEUE_SIZE)) + .after(q -> requestRepository.findBy(q, MAXIMUM_SUPPORTED_REQUEST_QUEUE_SIZE)) .thenApply(r -> r.map(MultipleRecords::getRecords)) .thenApply(r -> r.map(RequestQueue::new)); } diff --git a/src/main/java/org/folio/circulation/resources/AllowedServicePointsResource.java b/src/main/java/org/folio/circulation/resources/AllowedServicePointsResource.java index 97fee14762..1e96d2b916 100644 --- a/src/main/java/org/folio/circulation/resources/AllowedServicePointsResource.java +++ b/src/main/java/org/folio/circulation/resources/AllowedServicePointsResource.java @@ -54,7 +54,8 @@ private void get(RoutingContext routingContext) { ofAsync(routingContext) .thenApply(r -> r.next(AllowedServicePointsResource::buildRequest)) - .thenCompose(r -> r.after(new AllowedServicePointsService(clients)::getAllowedServicePoints)) + .thenCompose(r -> r.after(request -> new AllowedServicePointsService( + clients, request.isEcsRequestRouting()).getAllowedServicePoints(request))) .thenApply(r -> r.map(AllowedServicePointsResource::toJson)) .thenApply(r -> r.map(JsonHttpResponse::ok)) .exceptionally(CommonFailures::failedDueToServerError) @@ -68,62 +69,58 @@ private static Result buildRequest(RoutingContext r .map(String::toUpperCase) .map(Request.Operation::valueOf) .orElse(null); - - AllowedServicePointsRequest request = new AllowedServicePointsRequest(operation, - queryParams.get("requesterId"), queryParams.get("instanceId"), queryParams.get("itemId"), - queryParams.get("requestId")); - - return validateAllowedServicePointsRequest(request); - } - - private static Result validateAllowedServicePointsRequest( - AllowedServicePointsRequest allowedServicePointsRequest) { - - log.debug("validateAllowedServicePointsRequest:: parameters allowedServicePointsRequest: {}", - allowedServicePointsRequest); - - Request.Operation operation = allowedServicePointsRequest.getOperation(); - String requesterId = allowedServicePointsRequest.getRequesterId(); - String instanceId = allowedServicePointsRequest.getInstanceId(); - String itemId = allowedServicePointsRequest.getItemId(); - String requestId = allowedServicePointsRequest.getRequestId(); + String requesterId = queryParams.get("requesterId"); + String instanceId = queryParams.get("instanceId"); + String itemId = queryParams.get("itemId"); + String requestId = queryParams.get("requestId"); + String useStubItem = queryParams.get("useStubItem"); + String ecsRequestRouting = queryParams.get("ecsRequestRouting"); + String patronGroupId = queryParams.get("patronGroupId"); List errors = new ArrayList<>(); // Checking UUID validity if (requesterId != null && !isUuid(requesterId)) { - log.warn("Requester ID is not a valid UUID: {}", requesterId); + log.warn("buildRequest:: Requester ID is not a valid UUID: {}", + requesterId); errors.add(String.format("Requester ID is not a valid UUID: %s.", requesterId)); } + if (patronGroupId != null && !isUuid(patronGroupId)) { + log.warn("buildRequest:: Patron Group ID is not a valid UUID: {}", patronGroupId); + errors.add(String.format("Patron Group ID is not a valid UUID: %s.", + patronGroupId)); + } + if (instanceId != null && !isUuid(instanceId)) { - log.warn("Instance ID is not a valid UUID: {}", requesterId); + log.warn("buildRequest:: Instance ID is not a valid UUID: {}", instanceId); errors.add(String.format("Instance ID is not a valid UUID: %s.", instanceId)); } if (itemId != null && !isUuid(itemId)) { - log.warn("Item ID is not a valid UUID: {}", itemId); + log.warn("buildRequest:: Item ID is not a valid UUID: {}", itemId); errors.add(String.format("Item ID is not a valid UUID: %s.", itemId)); } if (requestId != null && !isUuid(requestId)) { - log.warn("Request ID is not a valid UUID: {}", requestId); + log.warn("buildRequest:: Request ID is not a valid UUID: {}", requestId); errors.add(String.format("Request ID is not a valid UUID: %s.", requestId)); } - + validateBoolean(useStubItem, "useStubItem", errors); + validateBoolean(ecsRequestRouting, "ecsRequestRouting", errors); // Checking parameter combinations boolean allowedCombinationOfParametersDetected = false; - if (operation == Request.Operation.CREATE && requesterId != null && instanceId != null && + if (operation == Request.Operation.CREATE && (requesterId != null || patronGroupId != null) && instanceId != null && itemId == null && requestId == null) { log.info("validateAllowedServicePointsRequest:: TLR request creation case"); allowedCombinationOfParametersDetected = true; } - if (operation == Request.Operation.CREATE && requesterId != null && instanceId == null && + if (operation == Request.Operation.CREATE && (requesterId != null || patronGroupId != null) && instanceId == null && itemId != null && requestId == null) { log.info("validateAllowedServicePointsRequest:: ILR request creation case"); @@ -155,7 +152,17 @@ private static Result validateAllowedServicePointsR return failed(new BadRequestFailure(errorMessage)); } - return succeeded(allowedServicePointsRequest); + return succeeded(new AllowedServicePointsRequest(operation, requesterId, + patronGroupId, instanceId, itemId, requestId, Boolean.parseBoolean(useStubItem), + Boolean.parseBoolean(ecsRequestRouting))); + } + + private static void validateBoolean(String parameter, String parameterName, List errors) { + if (parameter != null && !"true".equals(parameter) && !"false".equals(parameter)) { + log.warn("validateBoolean:: {} is not a valid boolean: {}", + parameterName, parameter); + errors.add(String.format("%s is not a valid boolean: %s.", parameterName, parameter)); + } } private static JsonObject toJson(Map> allowedServicePoints) { diff --git a/src/main/java/org/folio/circulation/resources/ChangeDueDateResource.java b/src/main/java/org/folio/circulation/resources/ChangeDueDateResource.java index ec49593d85..0e43fcbc24 100644 --- a/src/main/java/org/folio/circulation/resources/ChangeDueDateResource.java +++ b/src/main/java/org/folio/circulation/resources/ChangeDueDateResource.java @@ -3,7 +3,9 @@ import static java.util.concurrent.CompletableFuture.completedFuture; import static org.folio.circulation.domain.representations.ChangeDueDateRequest.DUE_DATE; import static org.folio.circulation.domain.representations.LoanProperties.ITEM_ID; -import static org.folio.circulation.resources.handlers.error.CirculationErrorType.*; +import static org.folio.circulation.resources.handlers.error.CirculationErrorType.FAILED_TO_FETCH_USER; +import static org.folio.circulation.resources.handlers.error.CirculationErrorType.FAILED_TO_FIND_SINGLE_OPEN_LOAN; +import static org.folio.circulation.resources.handlers.error.CirculationErrorType.ITEM_DOES_NOT_EXIST; import static org.folio.circulation.support.ValidationErrorFailure.singleValidationError; import static org.folio.circulation.support.json.JsonPropertyFetcher.getDateTimeProperty; import static org.folio.circulation.support.results.MappingFunctions.toFixedValue; @@ -26,7 +28,7 @@ import org.folio.circulation.domain.representations.ChangeDueDateRequest; import org.folio.circulation.domain.validation.ItemStatusValidator; import org.folio.circulation.domain.validation.LoanValidator; -import org.folio.circulation.infrastructure.storage.ConfigurationRepository; +import org.folio.circulation.infrastructure.storage.SettingsRepository; import org.folio.circulation.infrastructure.storage.inventory.ItemRepository; import org.folio.circulation.infrastructure.storage.loans.LoanRepository; import org.folio.circulation.infrastructure.storage.loans.OverdueFinePolicyRepository; @@ -80,6 +82,8 @@ private CompletableFuture> processChangeDueDate( final var itemRepository = new ItemRepository(clients); final var userRepository = new UserRepository(clients); final var loanRepository = new LoanRepository(clients, itemRepository, userRepository); + final var settingsRepository = new SettingsRepository(clients); + final WebContext webContext = new WebContext(routingContext); final OkapiPermissions okapiPermissions = OkapiPermissions.from(webContext.getHeaders()); @@ -103,13 +107,12 @@ private CompletableFuture> processChangeDueDate( final LoanNoticeSender loanNoticeSender = LoanNoticeSender.using(clients, loanRepository); - final ConfigurationRepository configurationRepository = new ConfigurationRepository(clients); log.info("starting change due date process for loan {}", request.getLoanId()); return succeeded(request) .after(r -> getExistingLoan(loanRepository, r)) .thenApply(LoanValidator::refuseWhenLoanIsClosed) .thenApply(this::toLoanAndRelatedRecords) - .thenComposeAsync(r -> r.combineAfter(configurationRepository::lookupTlrSettings, + .thenComposeAsync(r -> r.combineAfter(settingsRepository::lookupTlrSettings, LoanAndRelatedRecords::withTlrSettings)) .thenComposeAsync(r -> r.after(requestQueueRepository::get)) .thenApply(itemStatusValidator::refuseWhenItemStatusDoesNotAllowDueDateChange) diff --git a/src/main/java/org/folio/circulation/resources/CheckInByBarcodeResource.java b/src/main/java/org/folio/circulation/resources/CheckInByBarcodeResource.java index be2089da03..192485e7d2 100644 --- a/src/main/java/org/folio/circulation/resources/CheckInByBarcodeResource.java +++ b/src/main/java/org/folio/circulation/resources/CheckInByBarcodeResource.java @@ -16,6 +16,7 @@ import org.folio.circulation.domain.representations.CheckInByBarcodeResponse; import org.folio.circulation.domain.validation.CheckInValidators; import org.folio.circulation.infrastructure.storage.ConfigurationRepository; +import org.folio.circulation.infrastructure.storage.SettingsRepository; import org.folio.circulation.infrastructure.storage.inventory.ItemRepository; import org.folio.circulation.infrastructure.storage.loans.LoanRepository; import org.folio.circulation.infrastructure.storage.requests.RequestQueueRepository; @@ -78,6 +79,7 @@ private void checkIn(RoutingContext routingContext) { final RequestNoticeSender requestNoticeSender = RequestNoticeSender.using(clients); final ConfigurationRepository configurationRepository = new ConfigurationRepository(clients); + final SettingsRepository settingsRepository = new SettingsRepository(clients); refuseWhenLoggedInUserNotPresent(context) .next(notUsed -> checkInRequestResult) @@ -87,7 +89,7 @@ private void checkIn(RoutingContext routingContext) { .withItemStatusBeforeCheckIn(item.getStatus())) .thenApply(checkInValidators::refuseWhenItemIsNotAllowedForCheckIn) .thenApply(checkInValidators::refuseWhenClaimedReturnedIsNotResolved) - .thenComposeAsync(r -> r.combineAfter(configurationRepository::lookupTlrSettings, + .thenComposeAsync(r -> r.combineAfter(settingsRepository::lookupTlrSettings, CheckInContext::withTlrSettings)) .thenComposeAsync(r -> r.combineAfter(configurationRepository::findTimeZoneConfiguration, CheckInContext::withTimeZone)) diff --git a/src/main/java/org/folio/circulation/resources/CheckInProcessAdapter.java b/src/main/java/org/folio/circulation/resources/CheckInProcessAdapter.java index 5179afe894..d9ec1c876d 100644 --- a/src/main/java/org/folio/circulation/resources/CheckInProcessAdapter.java +++ b/src/main/java/org/folio/circulation/resources/CheckInProcessAdapter.java @@ -179,7 +179,8 @@ CompletableFuture> getRequestQueue(CheckInContext context) return requestQueueRepository.getByItemId(context.getItem().getItemId()); } else { - return requestQueueRepository.getByInstanceId(context.getItem().getInstanceId()); + return requestQueueRepository.getByInstanceIdAndItemId(context.getItem().getInstanceId(), + context.getItem().getItemId()); } } diff --git a/src/main/java/org/folio/circulation/resources/CheckOutByBarcodeResource.java b/src/main/java/org/folio/circulation/resources/CheckOutByBarcodeResource.java index a4f804b70b..57e144f47f 100644 --- a/src/main/java/org/folio/circulation/resources/CheckOutByBarcodeResource.java +++ b/src/main/java/org/folio/circulation/resources/CheckOutByBarcodeResource.java @@ -22,6 +22,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.folio.circulation.domain.CheckOutLock; +import org.folio.circulation.domain.Item; import org.folio.circulation.domain.Loan; import org.folio.circulation.domain.LoanAndRelatedRecords; import org.folio.circulation.domain.LoanRepresentation; @@ -154,9 +155,10 @@ private void checkOut(RoutingContext routingContext) { .thenApply(validators::refuseWhenItemIsAlreadyCheckedOut) .thenApply(validators::refuseWhenItemIsNotAllowedForCheckOut) .thenComposeAsync(validators::refuseWhenItemHasOpenLoans) - .thenComposeAsync(r -> r.combineAfter(configurationRepository::lookupTlrSettings, + .thenComposeAsync(r -> r.combineAfter(settingsRepository::lookupTlrSettings, LoanAndRelatedRecords::withTlrSettings)) - .thenComposeAsync(r -> r.after(requestQueueRepository::get)) + .thenComposeAsync(r -> r.combineAfter(l -> getRequestQueue(l, requestQueueRepository), + LoanAndRelatedRecords::withRequestQueue)) .thenCompose(validators::refuseWhenRequestedByAnotherPatron) .thenComposeAsync(r -> r.after(l -> lookupLoanPolicy(l, loanPolicyRepository, errorHandler))) .thenComposeAsync(validators::refuseWhenItemLimitIsReached) @@ -390,4 +392,14 @@ private Result calculateDefaultInitialDueDate( .map(loan::changeDueDate) .map(loanAndRelatedRecords::withLoan); } + + private CompletableFuture> getRequestQueue( + LoanAndRelatedRecords loanAndRelatedRecords, RequestQueueRepository requestQueueRepository) { + + Item item = loanAndRelatedRecords.getItem(); + + return loanAndRelatedRecords.getTlrSettings().isTitleLevelRequestsFeatureEnabled() + ? requestQueueRepository.getByInstanceIdAndItemId(item.getInstanceId(), item.getItemId()) + : requestQueueRepository.getByItemId(item.getItemId()); + } } diff --git a/src/main/java/org/folio/circulation/resources/ItemsByInstanceResource.java b/src/main/java/org/folio/circulation/resources/ItemsByInstanceResource.java new file mode 100644 index 0000000000..381808e951 --- /dev/null +++ b/src/main/java/org/folio/circulation/resources/ItemsByInstanceResource.java @@ -0,0 +1,46 @@ +package org.folio.circulation.resources; + +import java.lang.invoke.MethodHandles; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.folio.circulation.domain.SearchInstance; +import org.folio.circulation.infrastructure.storage.SearchRepository; +import org.folio.circulation.support.http.server.JsonHttpResponse; +import org.folio.circulation.support.http.server.WebContext; + +import io.vertx.core.http.HttpClient; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.RoutingContext; + +public class ItemsByInstanceResource extends Resource { + + private static final Logger log = LogManager.getLogger(MethodHandles.lookup().lookupClass()); + + public ItemsByInstanceResource(HttpClient client) { + super(client); + } + + @Override + public void register(Router router) { + router.get("/circulation/items-by-instance") + .handler(this::getInstanceItems); + } + + private void getInstanceItems(RoutingContext routingContext) { + final WebContext context = new WebContext(routingContext); + new SearchRepository(context, client).getInstanceWithItems(routingContext.queryParam("query")) + .thenApply(r -> r.map(this::toJson)) + .thenApply(r -> r.map(JsonHttpResponse::ok)) + .thenAccept(context::writeResultToHttpResponse); + } + + private JsonObject toJson(SearchInstance searchInstance) { + log.debug("toJson:: searchInstance: {}", () -> searchInstance); + if (searchInstance != null) { + return searchInstance.toJson(); + } + return new JsonObject(); + } +} diff --git a/src/main/java/org/folio/circulation/resources/RequestByInstanceIdResource.java b/src/main/java/org/folio/circulation/resources/RequestByInstanceIdResource.java index 67c8409e99..36d9ba7ab8 100644 --- a/src/main/java/org/folio/circulation/resources/RequestByInstanceIdResource.java +++ b/src/main/java/org/folio/circulation/resources/RequestByInstanceIdResource.java @@ -63,7 +63,7 @@ import org.folio.circulation.domain.validation.ProxyRelationshipValidator; import org.folio.circulation.domain.validation.RequestLoanValidator; import org.folio.circulation.domain.validation.ServicePointPickupLocationValidator; -import org.folio.circulation.infrastructure.storage.ConfigurationRepository; +import org.folio.circulation.infrastructure.storage.SettingsRepository; import org.folio.circulation.infrastructure.storage.loans.LoanRepository; import org.folio.circulation.infrastructure.storage.requests.RequestQueueRepository; import org.folio.circulation.resources.handlers.error.FailFastErrorHandler; @@ -121,7 +121,7 @@ private void createInstanceLevelRequests(RoutingContext routingContext) { final var requestBody = routingContext.getBodyAsJson(); - new ConfigurationRepository(clients).lookupTlrSettings() + new SettingsRepository(clients).lookupTlrSettings() .thenCompose(r -> r.after(config -> buildAndPlaceRequests(clients, eventPublisher, repositories, itemFinder, config, requestBody))) .thenApply(r -> r.map(RequestAndRelatedRecords::getRequest)) diff --git a/src/main/java/org/folio/circulation/resources/RequestCollectionResource.java b/src/main/java/org/folio/circulation/resources/RequestCollectionResource.java index aab1681c96..a222818735 100644 --- a/src/main/java/org/folio/circulation/resources/RequestCollectionResource.java +++ b/src/main/java/org/folio/circulation/resources/RequestCollectionResource.java @@ -8,6 +8,10 @@ import static org.folio.circulation.support.results.MappingFunctions.toFixedValue; import static org.folio.circulation.support.results.MappingFunctions.when; +import java.lang.invoke.MethodHandles; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.folio.circulation.domain.CreateRequestService; import org.folio.circulation.domain.MoveRequestProcessAdapter; import org.folio.circulation.domain.MoveRequestService; @@ -30,6 +34,7 @@ import org.folio.circulation.infrastructure.storage.CalendarRepository; import org.folio.circulation.infrastructure.storage.ConfigurationRepository; import org.folio.circulation.infrastructure.storage.ServicePointRepository; +import org.folio.circulation.infrastructure.storage.SettingsRepository; import org.folio.circulation.infrastructure.storage.inventory.ItemRepository; import org.folio.circulation.infrastructure.storage.loans.LoanPolicyRepository; import org.folio.circulation.infrastructure.storage.loans.LoanRepository; @@ -56,6 +61,8 @@ import io.vertx.ext.web.RoutingContext; public class RequestCollectionResource extends CollectionResource { + private static final Logger log = LogManager.getLogger(MethodHandles.lookup().lookupClass()); + public RequestCollectionResource(HttpClient client) { super(client, "/circulation/requests"); } @@ -73,6 +80,8 @@ void create(RoutingContext routingContext) { final var representation = routingContext.getBodyAsJson(); + log.debug("create:: {}", representation); + final var eventPublisher = new EventPublisher(routingContext); RequestRelatedRepositories repositories = new RequestRelatedRepositories(clients); @@ -272,6 +281,7 @@ void move(RoutingContext routingContext) { final var loanPolicyRepository = new LoanPolicyRepository(clients); final var requestPolicyRepository = new RequestPolicyRepository(clients); final var configurationRepository = new ConfigurationRepository(clients); + final var settingsRepository = new SettingsRepository(clients); final var updateUponRequest = new UpdateUponRequest(new UpdateItem(itemRepository, new RequestQueueService(requestPolicyRepository, loanPolicyRepository)), @@ -287,7 +297,7 @@ void move(RoutingContext routingContext) { requestRepository, requestPolicyRepository, updateUponRequest, moveRequestProcessAdapter, new RequestLoanValidator(new ItemByInstanceIdFinder(clients.holdingsStorage(), itemRepository), loanRepository), RequestNoticeSender.using(clients), configurationRepository, eventPublisher, - requestQueueRepository); + requestQueueRepository, settingsRepository); fromFutureResult(requestRepository.getById(id)) .map(request -> request.withOperation(Request.Operation.MOVE)) diff --git a/src/main/java/org/folio/circulation/resources/RequestFromRepresentationService.java b/src/main/java/org/folio/circulation/resources/RequestFromRepresentationService.java index a11b86d231..9860f9b454 100644 --- a/src/main/java/org/folio/circulation/resources/RequestFromRepresentationService.java +++ b/src/main/java/org/folio/circulation/resources/RequestFromRepresentationService.java @@ -51,6 +51,7 @@ import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.folio.circulation.domain.EcsRequestPhase; import org.folio.circulation.domain.Item; import org.folio.circulation.domain.Loan; import org.folio.circulation.domain.MultipleRecords; @@ -66,6 +67,7 @@ import org.folio.circulation.domain.validation.ServicePointPickupLocationValidator; import org.folio.circulation.infrastructure.storage.ConfigurationRepository; import org.folio.circulation.infrastructure.storage.ServicePointRepository; +import org.folio.circulation.infrastructure.storage.SettingsRepository; import org.folio.circulation.infrastructure.storage.inventory.HoldingsRepository; import org.folio.circulation.infrastructure.storage.inventory.InstanceRepository; import org.folio.circulation.infrastructure.storage.inventory.ItemRepository; @@ -95,6 +97,7 @@ class RequestFromRepresentationService { private final LoanRepository loanRepository; private final ServicePointRepository servicePointRepository; private final ConfigurationRepository configurationRepository; + private final SettingsRepository settingsRepository; private final RequestPolicyRepository requestPolicyRepository; private final ProxyRelationshipValidator proxyRelationshipValidator; private final ServicePointPickupLocationValidator pickupLocationValidator; @@ -118,6 +121,7 @@ public RequestFromRepresentationService(Request.Operation operation, this.loanRepository = repositories.getLoanRepository(); this.servicePointRepository = repositories.getServicePointRepository(); this.configurationRepository = repositories.getConfigurationRepository(); + this.settingsRepository = repositories.getSettingsRepository(); this.requestPolicyRepository = repositories.getRequestPolicyRepository(); this.proxyRelationshipValidator = proxyRelationshipValidator; @@ -129,7 +133,7 @@ public RequestFromRepresentationService(Request.Operation operation, CompletableFuture> getRequestFrom(JsonObject representation) { - return configurationRepository.lookupTlrSettings() + return settingsRepository.lookupTlrSettings() .thenCompose(r -> r.after(tlrSettings -> initRequest(operation, tlrSettings, representation))) .thenApply(r -> r.next(this::validateStatus)) .thenApply(r -> r.next(this::validateRequestLevel)) @@ -249,7 +253,12 @@ private CompletableFuture> fetchItemAndLoan( Request request = records.getRequest(); Function>> itemAndLoanFetchingFunction; - if (request.isTitleLevel() && request.isPage()) { + log.info("fetchItemAndLoan:: Request phase is {}", request.getEcsRequestPhase().value); + if (request.getEcsRequestPhase() == EcsRequestPhase.PRIMARY) { + log.info("fetchItemAndLoan:: Primary ECS request detected, using default item fetcher"); + itemAndLoanFetchingFunction = this::fetchItemAndLoanDefault; + } + else if (request.isTitleLevel() && request.isPage()) { itemAndLoanFetchingFunction = this::fetchItemAndLoanForPageTlr; } else if (request.isTitleLevel() && request.isRecall()) { @@ -534,6 +543,11 @@ private Result refuseToCreateTlrLinkedToAnItem(Result request) } private Result validateAbsenceOfItemLinkInTlr(Request request) { + if (request.getEcsRequestPhase() == EcsRequestPhase.PRIMARY) { + log.info("validateAbsenceOfItemLinkInTlr:: Primary ECS request detected, skipping"); + return of(() -> request); + } + String itemId = request.getItemId(); String holdingsRecordId = request.getHoldingsRecordId(); diff --git a/src/main/java/org/folio/circulation/resources/RequestQueueResource.java b/src/main/java/org/folio/circulation/resources/RequestQueueResource.java index 3696483058..059eb284c5 100644 --- a/src/main/java/org/folio/circulation/resources/RequestQueueResource.java +++ b/src/main/java/org/folio/circulation/resources/RequestQueueResource.java @@ -27,6 +27,7 @@ import org.folio.circulation.infrastructure.storage.CalendarRepository; import org.folio.circulation.infrastructure.storage.ConfigurationRepository; import org.folio.circulation.infrastructure.storage.ServicePointRepository; +import org.folio.circulation.infrastructure.storage.SettingsRepository; import org.folio.circulation.infrastructure.storage.inventory.ItemRepository; import org.folio.circulation.infrastructure.storage.loans.LoanRepository; import org.folio.circulation.infrastructure.storage.requests.RequestQueueRepository; @@ -97,10 +98,7 @@ private void getQueue(RoutingContext routingContext, RequestQueueType requestQue final RequestRepresentation requestRepresentation = new RequestRepresentation(); - CompletableFuture> requestQueue = getRequestQueueByType(routingContext, - requestQueueType, requestQueueRepository); - - requestQueue + getRequestQueueByType(routingContext, requestQueueType, requestQueueRepository) .thenApply(r -> r.map(queue -> new MultipleRecords<>(queue.getRequests(), queue.size()))) .thenApply(r -> r.map(requests -> requests.asJson(requestRepresentation::extendedRepresentation, "requests"))) @@ -125,15 +123,14 @@ private void reorderQueue(RoutingContext routingContext, RequestQueueType reques final var requestRepository = RequestRepository.using(clients, itemRepository, userRepository, loanRepository); final var configurationRepository = new ConfigurationRepository(clients); + final var settingsRepository = new SettingsRepository(clients); final var requestQueueRepository = new RequestQueueRepository(requestRepository); final UpdateRequestQueue updateRequestQueue = new UpdateRequestQueue( requestQueueRepository, requestRepository, new ServicePointRepository(clients), configurationRepository, RequestQueueService.using(clients), new CalendarRepository(clients)); - getRequestQueueByType(routingContext, requestQueueType, requestQueueRepository); - - validateTlrFeatureStatus(configurationRepository, requestQueueType, idParamValue) + validateTlrFeatureStatus(settingsRepository, requestQueueType, idParamValue) .thenCompose(r -> r.after(tlrSettings -> getRequestQueueByType(routingContext, requestQueueType, requestQueueRepository))) .thenApply(r -> r.map(reorderContext::withRequestQueue)) @@ -152,10 +149,10 @@ requestQueueRepository, requestRepository, new ServicePointRepository(clients), } private CompletableFuture> validateTlrFeatureStatus( - ConfigurationRepository configurationRepository, RequestQueueType requestQueueType, + SettingsRepository settingsRepository, RequestQueueType requestQueueType, String idParamValue) { - return configurationRepository.lookupTlrSettings() + return settingsRepository.lookupTlrSettings() .thenApply(r -> r.failWhen( tlrSettings -> succeeded( requestQueueType == FOR_INSTANCE ^ tlrSettings.isTitleLevelRequestsFeatureEnabled()), diff --git a/src/main/java/org/folio/circulation/resources/renewal/RenewalResource.java b/src/main/java/org/folio/circulation/resources/renewal/RenewalResource.java index 4d60a026aa..e7e278c757 100644 --- a/src/main/java/org/folio/circulation/resources/renewal/RenewalResource.java +++ b/src/main/java/org/folio/circulation/resources/renewal/RenewalResource.java @@ -70,6 +70,7 @@ import org.folio.circulation.infrastructure.storage.AutomatedPatronBlocksRepository; import org.folio.circulation.infrastructure.storage.CalendarRepository; import org.folio.circulation.infrastructure.storage.ConfigurationRepository; +import org.folio.circulation.infrastructure.storage.SettingsRepository; import org.folio.circulation.infrastructure.storage.feesandfines.FeeFineOwnerRepository; import org.folio.circulation.infrastructure.storage.feesandfines.FeeFineRepository; import org.folio.circulation.infrastructure.storage.inventory.ItemRepository; @@ -150,6 +151,7 @@ private void renew(RoutingContext routingContext) { final LoanRepresentation loanRepresentation = new LoanRepresentation(); final ConfigurationRepository configurationRepository = new ConfigurationRepository(clients); + final SettingsRepository settingsRepository = new SettingsRepository(clients); final LoanScheduledNoticeService scheduledNoticeService = LoanScheduledNoticeService.using(clients); final ReminderFeeScheduledNoticeService scheduledRemindersService = new ReminderFeeScheduledNoticeService(clients); @@ -187,7 +189,7 @@ private void renew(RoutingContext routingContext) { .thenCompose(r -> r.after(ctx -> lookupOverdueFinePolicy(ctx, overdueFinePolicyRepository, errorHandler))) .thenComposeAsync(r -> r.after(ctx -> blockRenewalOfItemsWithReminderFees(ctx, errorHandler))) .thenCompose(r -> r.after(ctx -> lookupLoanPolicy(ctx, loanPolicyRepository, errorHandler))) - .thenCompose(r -> r.combineAfter(configurationRepository::lookupTlrSettings, + .thenCompose(r -> r.combineAfter(settingsRepository::lookupTlrSettings, RenewalContext::withTlrSettings)) .thenComposeAsync(r -> r.after( ctx -> lookupRequestQueue(ctx, requestQueueRepository, errorHandler))) diff --git a/src/main/java/org/folio/circulation/rules/CirculationRuleCriteria.java b/src/main/java/org/folio/circulation/rules/CirculationRuleCriteria.java index 3052577f4f..29f1d903d5 100644 --- a/src/main/java/org/folio/circulation/rules/CirculationRuleCriteria.java +++ b/src/main/java/org/folio/circulation/rules/CirculationRuleCriteria.java @@ -1,7 +1,6 @@ package org.folio.circulation.rules; import org.folio.circulation.domain.Item; -import org.folio.circulation.domain.User; import lombok.EqualsAndHashCode; import lombok.Getter; @@ -20,11 +19,11 @@ public class CirculationRuleCriteria { @EqualsAndHashCode.Exclude private Item item; - public CirculationRuleCriteria(@NonNull Item item, @NonNull User user) { + public CirculationRuleCriteria(@NonNull Item item, @NonNull String patronGroupId) { this.materialTypeId = item.getMaterialTypeId(); this.loanTypeId = item.getLoanTypeId(); this.locationId = item.getEffectiveLocationId(); - this.patronGroupId = user.getPatronGroupId(); + this.patronGroupId = patronGroupId; this.item = item; } } diff --git a/src/main/java/org/folio/circulation/services/AllowedServicePointsService.java b/src/main/java/org/folio/circulation/services/AllowedServicePointsService.java index 45fff76324..ef86ae8b28 100644 --- a/src/main/java/org/folio/circulation/services/AllowedServicePointsService.java +++ b/src/main/java/org/folio/circulation/services/AllowedServicePointsService.java @@ -20,6 +20,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.EnumMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -40,8 +41,8 @@ import org.folio.circulation.domain.User; import org.folio.circulation.domain.configuration.TlrSettingsConfiguration; import org.folio.circulation.domain.policy.RequestPolicy; -import org.folio.circulation.infrastructure.storage.ConfigurationRepository; import org.folio.circulation.infrastructure.storage.ServicePointRepository; +import org.folio.circulation.infrastructure.storage.SettingsRepository; import org.folio.circulation.infrastructure.storage.inventory.InstanceRepository; import org.folio.circulation.infrastructure.storage.inventory.ItemRepository; import org.folio.circulation.infrastructure.storage.requests.RequestPolicyRepository; @@ -57,24 +58,28 @@ public class AllowedServicePointsService { private static final Logger log = LogManager.getLogger(MethodHandles.lookup().lookupClass()); + private static final String ECS_REQUEST_ROUTING_INDEX_NAME = "ecsRequestRouting"; + private static final String PICKUP_LOCATION_INDEX_NAME = "pickupLocation"; private final ItemRepository itemRepository; private final UserRepository userRepository; private final RequestRepository requestRepository; private final RequestPolicyRepository requestPolicyRepository; private final ServicePointRepository servicePointRepository; private final ItemByInstanceIdFinder itemFinder; - private final ConfigurationRepository configurationRepository; + private final SettingsRepository settingsRepository; private final InstanceRepository instanceRepository; + private final String indexName; - public AllowedServicePointsService(Clients clients) { + public AllowedServicePointsService(Clients clients, boolean isEcsRequestRouting) { itemRepository = new ItemRepository(clients); userRepository = new UserRepository(clients); requestRepository = new RequestRepository(clients); requestPolicyRepository = new RequestPolicyRepository(clients); - servicePointRepository = new ServicePointRepository(clients); - configurationRepository = new ConfigurationRepository(clients); + servicePointRepository = new ServicePointRepository(clients, isEcsRequestRouting); + settingsRepository = new SettingsRepository(clients); instanceRepository = new InstanceRepository(clients); itemFinder = new ItemByInstanceIdFinder(clients.holdingsStorage(), itemRepository); + indexName = isEcsRequestRouting ? ECS_REQUEST_ROUTING_INDEX_NAME : PICKUP_LOCATION_INDEX_NAME; } public CompletableFuture>>> @@ -85,8 +90,9 @@ public AllowedServicePointsService(Clients clients) { return ofAsync(request) .thenCompose(r -> r.after(this::fetchInstance)) .thenCompose(r -> r.after(this::fetchRequest)) - .thenCompose(r -> r.after(this::fetchUser)) - .thenCompose(r -> r.after(user -> getAllowedServicePoints(request, user))); + .thenCompose(r -> r.after(this::getPatronGroupId)) + .thenCompose(r -> r.after(patronGroupId -> getAllowedServicePoints(request, + patronGroupId))); } private CompletableFuture> fetchInstance( @@ -123,38 +129,51 @@ private CompletableFuture> fetchRequest( .thenApply(r -> r.map(allowedServicePointsRequest::updateWithRequestInformation)); } - private CompletableFuture> fetchUser(AllowedServicePointsRequest request) { + private CompletableFuture> getPatronGroupId(AllowedServicePointsRequest request) { + + if (request.getPatronGroupId() != null) { + return ofAsync(request.getPatronGroupId()); + } + final String userId = request.getRequesterId(); return userRepository.getUser(userId) .thenApply(r -> r.failWhen( user -> succeeded(user == null), - user -> notFoundValidationFailure(userId, User.class))); + user -> notFoundValidationFailure(userId, User.class))) + .thenApply(result -> result.map(User::getPatronGroupId)); } private CompletableFuture>>> - getAllowedServicePoints(AllowedServicePointsRequest request, User user) { + getAllowedServicePoints(AllowedServicePointsRequest request, String patronGroupId) { - log.debug("getAllowedServicePoints:: parameters request: {}, user: {}", request, user); + log.debug("getAllowedServicePoints:: parameters request: {}, patronGroupId: {}", request, patronGroupId); return fetchItems(request) - .thenCompose(r -> r.after(items -> getAllowedServicePoints(request, user, items))); + .thenCompose(r -> r.after(items -> getAllowedServicePoints(request, patronGroupId, items))); } private CompletableFuture>>> - getAllowedServicePoints(AllowedServicePointsRequest request, User user, Collection items) { - - if (items.isEmpty() && request.isForTitleLevelRequest()) { - log.info("getAllowedServicePoints:: requested instance has no items"); - return getAllowedServicePointsForTitleWithNoItems(request); - } + getAllowedServicePoints(AllowedServicePointsRequest request, String patronGroupId, + Collection items) { BiFunction, CompletableFuture>>>> mappingFunction = request.isImplyingItemStatusIgnore() ? this::extractAllowedServicePointsIgnoringItemStatus : this::extractAllowedServicePointsConsideringItemStatus; - return requestPolicyRepository.lookupRequestPolicies(items, user) + if (request.isUseStubItem()) { + return requestPolicyRepository.lookupRequestPolicy(patronGroupId) + .thenCompose(r -> r.after(policy -> extractAllowedServicePointsIgnoringItemStatus( + policy, new HashSet<>()))); + } + + if (items.isEmpty() && request.isForTitleLevelRequest()) { + log.info("getAllowedServicePoints:: requested instance has no items"); + return getAllowedServicePointsForTitleWithNoItems(request); + } + + return requestPolicyRepository.lookupRequestPolicies(items, patronGroupId) .thenCompose(r -> r.after(policies -> allOf(policies, mappingFunction))) .thenApply(r -> r.map(this::combineAllowedServicePoints)); // TODO: remove irrelevant request types for REPLACE @@ -165,7 +184,7 @@ private CompletableFuture> fetchUser(AllowedServicePointsRequest re if (request.isForTitleLevelRequest() && request.getOperation() == CREATE) { log.info("getAllowedServicePointsForTitleWithNoItems:: checking TLR settings"); - return configurationRepository.lookupTlrSettings() + return settingsRepository.lookupTlrSettings() .thenCompose(r -> r.after(this::considerTlrSettings)); } @@ -353,7 +372,7 @@ private Map> combineAllowedServicePoints( } private CompletableFuture>> fetchAllowedServicePoints() { - return servicePointRepository.fetchPickupLocationServicePoints() + return servicePointRepository.fetchServicePointsByIndexName(indexName) .thenApply(r -> r.map(servicePoints -> servicePoints.stream() .map(AllowedServicePoint::new) .collect(Collectors.toSet()))); @@ -365,7 +384,7 @@ private CompletableFuture>> fetchPickupLocationS log.debug("filterIdsByServicePointsAndPickupLocationExistence:: parameters ids: {}", () -> collectionAsString(ids)); - return servicePointRepository.fetchPickupLocationServicePointsByIds(ids) + return servicePointRepository.fetchPickupLocationServicePointsByIdsAndIndexName(ids, indexName) .thenApply(servicePointsResult -> servicePointsResult .map(servicePoints -> servicePoints.stream() .map(AllowedServicePoint::new) diff --git a/src/main/java/org/folio/circulation/services/ItemForTlrService.java b/src/main/java/org/folio/circulation/services/ItemForTlrService.java index 49e19e6ff9..f4d9b66d61 100644 --- a/src/main/java/org/folio/circulation/services/ItemForTlrService.java +++ b/src/main/java/org/folio/circulation/services/ItemForTlrService.java @@ -1,5 +1,6 @@ package org.folio.circulation.services; +import static java.lang.String.format; import static java.util.stream.Collectors.groupingBy; import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toSet; @@ -18,6 +19,7 @@ import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.function.Function; +import java.util.stream.Collectors; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -42,6 +44,11 @@ public static ItemForTlrService using(RequestRelatedRepositories repositories) { } public List findAvailablePageableItems(Request request) { + log.info("findAvailablePageableItems:: instance items: {}", + () -> request.getInstanceItems().stream() + .map(item -> format("(%s, %s, %s)", item.getItemId(), item.getBarcode(), item.getStatus().getValue())) + .collect(Collectors.joining(", ")) + ); return request.getInstanceItems() .stream() diff --git a/src/main/java/org/folio/circulation/services/RequestQueueService.java b/src/main/java/org/folio/circulation/services/RequestQueueService.java index d110aee4fd..32d3a2b4b3 100644 --- a/src/main/java/org/folio/circulation/services/RequestQueueService.java +++ b/src/main/java/org/folio/circulation/services/RequestQueueService.java @@ -74,7 +74,8 @@ private CompletableFuture> isItemLevelRequestFulfillableByItem(I protected CompletableFuture> isTitleLevelRequestFulfillableByItem(Item item, Request request) { - if (!StringUtils.equals(request.getInstanceId(), item.getInstanceId())) { + if (!StringUtils.equals(request.getItemId(), item.getItemId()) && + !StringUtils.equals(request.getInstanceId(), item.getInstanceId())) { return ofAsync(false); } diff --git a/src/main/java/org/folio/circulation/storage/ItemByInstanceIdFinder.java b/src/main/java/org/folio/circulation/storage/ItemByInstanceIdFinder.java index 7cdeebf960..511e443125 100644 --- a/src/main/java/org/folio/circulation/storage/ItemByInstanceIdFinder.java +++ b/src/main/java/org/folio/circulation/storage/ItemByInstanceIdFinder.java @@ -7,23 +7,29 @@ import static org.folio.circulation.support.json.JsonKeys.byId; import static org.folio.circulation.support.results.Result.succeeded; +import java.lang.invoke.MethodHandles; import java.util.Collection; import java.util.List; import java.util.Set; import java.util.UUID; import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.folio.circulation.domain.Item; import org.folio.circulation.domain.MultipleRecords; import org.folio.circulation.infrastructure.storage.inventory.ItemRepository; import org.folio.circulation.support.FindWithCqlQuery; import org.folio.circulation.support.GetManyRecordsClient; import org.folio.circulation.support.http.client.CqlQuery; +import org.folio.circulation.support.http.client.PageLimit; import org.folio.circulation.support.results.Result; import io.vertx.core.json.JsonObject; public class ItemByInstanceIdFinder { + private static final Logger log = LogManager.getLogger(MethodHandles.lookup().lookupClass()); private final GetManyRecordsClient holdingsStorageClient; private final ItemRepository itemRepository; @@ -41,7 +47,8 @@ public CompletableFuture>> getItemsByInstanceId(UUID ins final FindWithCqlQuery fetcher = findWithCqlQuery( holdingsStorageClient, "holdingsRecords", identity()); - return fetcher.findByQuery(CqlQuery.exactMatch("instanceId", instanceId.toString())) + return fetcher.findByQuery(CqlQuery.exactMatch("instanceId", instanceId.toString()), + PageLimit.oneThousand()) .thenCompose(r -> getItems(r, failWhenNoHoldingsRecordsFound)); } @@ -50,6 +57,10 @@ private CompletableFuture>> getItems( boolean failWhenNoHoldingsRecordsFound) { return holdingsRecordsResult.after(holdingsRecords -> { + log.info("getItems:: holdings records: {}", holdingsRecords.getRecords().stream() + .map(h -> h.getString("id")) + .collect(Collectors.joining(", "))); + if (holdingsRecords == null || holdingsRecords.isEmpty()) { if (failWhenNoHoldingsRecordsFound) { return completedFuture(failedValidation( diff --git a/src/main/java/org/folio/circulation/support/Clients.java b/src/main/java/org/folio/circulation/support/Clients.java index 99be88183c..90b4dcb4a3 100644 --- a/src/main/java/org/folio/circulation/support/Clients.java +++ b/src/main/java/org/folio/circulation/support/Clients.java @@ -4,7 +4,9 @@ import org.folio.circulation.rules.CirculationRulesProcessor; import org.folio.circulation.services.PubSubPublishingService; +import org.folio.circulation.support.http.client.IncludeRoutingServicePoints; import org.folio.circulation.support.http.client.OkapiHttpClient; +import org.folio.circulation.support.http.client.QueryParameter; import org.folio.circulation.support.http.server.WebContext; import io.vertx.core.http.HttpClient; @@ -40,6 +42,7 @@ public class Clients { private final CollectionResourceClient circulationRulesStorageClient; private final CollectionResourceClient requestPoliciesStorageClient; private final CollectionResourceClient servicePointsStorageClient; + private final CollectionResourceClient routingServicePointsStorageClient; private final CollectionResourceClient calendarStorageClient; private final CollectionResourceClient patronGroupsStorageClient; private final CollectionResourceClient patronNoticePolicesStorageClient; @@ -68,6 +71,7 @@ public class Clients { private final CollectionResourceClient departmentClient; private final CollectionResourceClient checkOutLockStorageClient; private final CollectionResourceClient circulationItemClient; + private final CollectionResourceClient searchClient; private final GetManyRecordsClient settingsStorageClient; private final CollectionResourceClient circulationSettingsStorageClient; private final CollectionResourceClient printEventsStorageClient; @@ -77,6 +81,10 @@ public static Clients create(WebContext context, HttpClient httpClient) { return new Clients(context.createHttpClient(httpClient), context); } + public static Clients create(WebContext context, HttpClient httpClient, String tenantId) { + return new Clients(context.createHttpClient(httpClient, tenantId), context); + } + private Clients(OkapiHttpClient client, WebContext context) { try { requestsStorageClient = createRequestsStorageClient(client, context); @@ -110,6 +118,8 @@ private Clients(OkapiHttpClient client, WebContext context) { requestPoliciesStorageClient = createRequestPoliciesStorageClient(client, context); fixedDueDateSchedulesStorageClient = createFixedDueDateSchedulesStorageClient(client, context); servicePointsStorageClient = createServicePointsStorageClient(client, context); + routingServicePointsStorageClient = createServicePointsStorageWithCustomParam(client, + context, IncludeRoutingServicePoints.enabled()); patronGroupsStorageClient = createPatronGroupsStorageClient(client, context); calendarStorageClient = createCalendarStorageClient(client, context); patronNoticePolicesStorageClient = createPatronNoticePolicesStorageClient(client, context); @@ -139,6 +149,7 @@ private Clients(OkapiHttpClient client, WebContext context) { checkOutLockStorageClient = createCheckoutLockClient(client, context); settingsStorageClient = createSettingsStorageClient(client, context); circulationItemClient = createCirculationItemClient(client, context); + searchClient = createSearchClient(client, context); circulationSettingsStorageClient = createCirculationSettingsStorageClient(client, context); printEventsStorageClient = createPrintEventsStorageClient(client, context); @@ -244,6 +255,10 @@ public CollectionResourceClient servicePointsStorage() { return servicePointsStorageClient; } + public CollectionResourceClient routingServicePointsStorage() { + return routingServicePointsStorageClient; + } + public CollectionResourceClient patronGroupsStorage() { return patronGroupsStorageClient; } @@ -380,6 +395,10 @@ public CollectionResourceClient circulationItemClient() { return circulationItemClient; } + public CollectionResourceClient searchClient() { + return searchClient; + } + public CollectionResourceClient circulationSettingsStorageClient() { return circulationSettingsStorageClient; } @@ -396,6 +415,14 @@ private static CollectionResourceClient getCollectionResourceClient( return new CollectionResourceClient(client, context.getOkapiBasedUrl(path)); } + private static CollectionResourceClient getCollectionResourceClientWithCustomParam( + OkapiHttpClient client, WebContext context, String path, QueryParameter customParam) + throws MalformedURLException { + + return new CustomParamCollectionResourceClient(client, context.getOkapiBasedUrl(path), + customParam); + } + public CollectionResourceClient noticeTemplatesClient() { return noticeTemplatesClient; } @@ -638,6 +665,14 @@ private CollectionResourceClient createServicePointsStorageClient( return getCollectionResourceClient(client, context, "/service-points"); } + private CollectionResourceClient createServicePointsStorageWithCustomParam( + OkapiHttpClient client, WebContext context, QueryParameter customParam) + throws MalformedURLException { + + return getCollectionResourceClientWithCustomParam(client, context, "/service-points", + customParam); + } + private CollectionResourceClient createPatronGroupsStorageClient( OkapiHttpClient client, WebContext context) throws MalformedURLException { @@ -815,6 +850,12 @@ private CollectionResourceClient createCirculationItemClient( return getCollectionResourceClient(client, context, "/circulation-item"); } + private CollectionResourceClient createSearchClient( + OkapiHttpClient client, WebContext context) throws MalformedURLException { + + return getCollectionResourceClient(client, context, "/search/instances"); + } + private CollectionResourceClient createCirculationSettingsStorageClient( OkapiHttpClient client, WebContext context) throws MalformedURLException { diff --git a/src/main/java/org/folio/circulation/support/CollectionResourceClient.java b/src/main/java/org/folio/circulation/support/CollectionResourceClient.java index a5c827108c..0b3db44c9b 100644 --- a/src/main/java/org/folio/circulation/support/CollectionResourceClient.java +++ b/src/main/java/org/folio/circulation/support/CollectionResourceClient.java @@ -18,8 +18,8 @@ import io.vertx.core.json.JsonObject; public class CollectionResourceClient implements GetManyRecordsClient { - private final OkapiHttpClient client; - private final URL collectionRoot; + final OkapiHttpClient client; + final URL collectionRoot; public CollectionResourceClient(OkapiHttpClient client, URL collectionRoot) { this.collectionRoot = collectionRoot; @@ -109,7 +109,7 @@ public CompletableFuture> getMany(CqlQuery cqlQuery, return client.get(collectionRoot, cqlQuery, pageLimit, offset); } - private String individualRecordUrl(String id) { + String individualRecordUrl(String id) { return format("%s/%s", collectionRoot, id); } } diff --git a/src/main/java/org/folio/circulation/support/CustomParamCollectionResourceClient.java b/src/main/java/org/folio/circulation/support/CustomParamCollectionResourceClient.java new file mode 100644 index 0000000000..2c765e3acf --- /dev/null +++ b/src/main/java/org/folio/circulation/support/CustomParamCollectionResourceClient.java @@ -0,0 +1,46 @@ +package org.folio.circulation.support; + +import java.net.URL; +import java.util.concurrent.CompletableFuture; + +import org.folio.circulation.support.http.client.CqlQuery; +import org.folio.circulation.support.http.client.Offset; +import org.folio.circulation.support.http.client.OkapiHttpClient; +import org.folio.circulation.support.http.client.PageLimit; +import org.folio.circulation.support.http.client.QueryParameter; +import org.folio.circulation.support.http.client.Response; +import org.folio.circulation.support.results.Result; + +public class CustomParamCollectionResourceClient extends CollectionResourceClient { + + private QueryParameter customQueryParameter; + + public CustomParamCollectionResourceClient(OkapiHttpClient client, URL collectionRoot, + QueryParameter customQueryParameter) { + + super(client, collectionRoot); + this.customQueryParameter = customQueryParameter; + } + + @Override + public CompletableFuture> get() { + return client.get(collectionRoot.toString(), customQueryParameter); + } + + @Override + public CompletableFuture> get(PageLimit pageLimit) { + return client.get(collectionRoot, pageLimit, customQueryParameter); + } + + @Override + public CompletableFuture> get(String id) { + return client.get(individualRecordUrl(id), customQueryParameter); + } + + @Override + public CompletableFuture> getMany(CqlQuery cqlQuery, + PageLimit pageLimit, Offset offset) { + + return client.get(collectionRoot, cqlQuery, pageLimit, offset, customQueryParameter); + } +} diff --git a/src/main/java/org/folio/circulation/support/http/client/CqlQuery.java b/src/main/java/org/folio/circulation/support/http/client/CqlQuery.java index e8f87d6e0a..368d09e170 100644 --- a/src/main/java/org/folio/circulation/support/http/client/CqlQuery.java +++ b/src/main/java/org/folio/circulation/support/http/client/CqlQuery.java @@ -13,7 +13,9 @@ import java.net.URLEncoder; import java.util.Collection; import java.util.List; +import java.util.Map; import java.util.Objects; +import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; import org.folio.circulation.support.CqlSortBy; @@ -69,6 +71,20 @@ public static Result exactMatchAny(String indexName, Collection exactMatchAny(Map indicesToValues) { + String rawQuery = indicesToValues.entrySet() + .stream() + .filter(entry -> entry.getValue() != null) + .map(entry -> String.format("%s==\"%s\"", entry.getKey(), entry.getValue())) + .collect(Collectors.joining(" or ")); + + if (rawQuery.isEmpty()) { + return failedDueToServerError("Cannot generate empty CQL query"); + } + + return Result.of(() -> new CqlQuery("(" + rawQuery + ")", none())); + } + /** * Uses greater than ('>'), as not equals operator ('<>') is not supported in CQL at present */ diff --git a/src/main/java/org/folio/circulation/support/http/client/IncludeRoutingServicePoints.java b/src/main/java/org/folio/circulation/support/http/client/IncludeRoutingServicePoints.java new file mode 100644 index 0000000000..ff9d531cad --- /dev/null +++ b/src/main/java/org/folio/circulation/support/http/client/IncludeRoutingServicePoints.java @@ -0,0 +1,33 @@ +package org.folio.circulation.support.http.client; + +import static java.lang.String.format; + +public class IncludeRoutingServicePoints implements QueryParameter { + + private static final String PARAM_NAME = "includeRoutingServicePoints"; + private final Boolean value; + + public static IncludeRoutingServicePoints enabled() { + return new IncludeRoutingServicePoints(true); + } + + private IncludeRoutingServicePoints(Boolean value) { + this.value = value; + } + + @Override + public void consume(QueryStringParameterConsumer consumer) { + if (value != null) { + consumer.consume(PARAM_NAME, value.toString()); + } + } + + @Override + public String toString() { + if (value == null) { + return format("No %s", PARAM_NAME); + } + + return format("%s = \"%s\"", PARAM_NAME, value); + } +} diff --git a/src/main/java/org/folio/circulation/support/http/client/VertxWebClientOkapiHttpClient.java b/src/main/java/org/folio/circulation/support/http/client/VertxWebClientOkapiHttpClient.java index e4815a30bf..a203f4386f 100644 --- a/src/main/java/org/folio/circulation/support/http/client/VertxWebClientOkapiHttpClient.java +++ b/src/main/java/org/folio/circulation/support/http/client/VertxWebClientOkapiHttpClient.java @@ -27,7 +27,9 @@ import io.vertx.ext.web.client.HttpResponse; import io.vertx.ext.web.client.WebClient; import io.vertx.core.http.HttpMethod; +import lombok.extern.log4j.Log4j2; +@Log4j2 public class VertxWebClientOkapiHttpClient implements OkapiHttpClient { private static final Duration DEFAULT_TIMEOUT = Duration.of(20, SECONDS); private static final String ACCEPT = HttpHeaderNames.ACCEPT.toString(); @@ -184,6 +186,7 @@ public CompletableFuture> delete(String url, } private HttpRequest withStandardHeaders(HttpRequest request) { + log.debug("withStandardHeaders:: url={}, tenantId={}", request.uri(), tenantId); return request .putHeader(ACCEPT, "application/json, text/plain") .putHeader(OKAPI_URL, okapiUrl.toString()) diff --git a/src/main/java/org/folio/circulation/support/http/server/WebContext.java b/src/main/java/org/folio/circulation/support/http/server/WebContext.java index a38173b438..df6ec49778 100644 --- a/src/main/java/org/folio/circulation/support/http/server/WebContext.java +++ b/src/main/java/org/folio/circulation/support/http/server/WebContext.java @@ -76,6 +76,10 @@ public URL getOkapiBasedUrl(String path) throws MalformedURLException { } public OkapiHttpClient createHttpClient(HttpClient httpClient) { + return createHttpClient(httpClient, getTenantId()); + } + + public OkapiHttpClient createHttpClient(HttpClient httpClient, String tenantId) { URL okapiUrl; try { @@ -86,7 +90,7 @@ public OkapiHttpClient createHttpClient(HttpClient httpClient) { } return VertxWebClientOkapiHttpClient.createClientUsing(httpClient, - okapiUrl, getTenantId(), getOkapiToken(), getUserId(), + okapiUrl, tenantId, getOkapiToken(), getUserId(), getRequestId()); } diff --git a/src/main/java/org/folio/circulation/support/request/RequestRelatedRepositories.java b/src/main/java/org/folio/circulation/support/request/RequestRelatedRepositories.java index a7360d40c4..72b4c86b3c 100644 --- a/src/main/java/org/folio/circulation/support/request/RequestRelatedRepositories.java +++ b/src/main/java/org/folio/circulation/support/request/RequestRelatedRepositories.java @@ -2,6 +2,7 @@ import org.folio.circulation.infrastructure.storage.ConfigurationRepository; import org.folio.circulation.infrastructure.storage.ServicePointRepository; +import org.folio.circulation.infrastructure.storage.SettingsRepository; import org.folio.circulation.infrastructure.storage.inventory.HoldingsRepository; import org.folio.circulation.infrastructure.storage.inventory.InstanceRepository; import org.folio.circulation.infrastructure.storage.inventory.ItemRepository; @@ -28,6 +29,7 @@ public class RequestRelatedRepositories { private RequestQueueRepository requestQueueRepository; private RequestPolicyRepository requestPolicyRepository; private ConfigurationRepository configurationRepository; + private SettingsRepository settingsRepository; private ServicePointRepository servicePointRepository; private LocationRepository locationRepository; @@ -43,6 +45,7 @@ public RequestRelatedRepositories(Clients clients) { requestQueueRepository = new RequestQueueRepository(requestRepository); requestPolicyRepository = new RequestPolicyRepository(clients); configurationRepository = new ConfigurationRepository(clients); + settingsRepository = new SettingsRepository(clients); servicePointRepository = new ServicePointRepository(clients); locationRepository = LocationRepository.using(clients); } diff --git a/src/test/java/api/ItemsByInstanceResourceTest.java b/src/test/java/api/ItemsByInstanceResourceTest.java new file mode 100644 index 0000000000..08d072d432 --- /dev/null +++ b/src/test/java/api/ItemsByInstanceResourceTest.java @@ -0,0 +1,96 @@ +package api; + +import static api.support.APITestContext.clearTempTenantId; +import static api.support.APITestContext.setTempTenantId; +import static api.support.http.InterfaceUrls.itemsByInstanceUrl; +import static api.support.matchers.JsonObjectMatcher.hasJsonPath; +import static org.folio.HttpStatus.HTTP_OK; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.iterableWithSize; +import static org.hamcrest.core.Is.is; + +import java.util.List; +import java.util.UUID; + +import org.folio.circulation.support.http.client.Response; +import org.junit.jupiter.api.Test; + +import api.support.APITests; +import api.support.builders.SearchInstanceBuilder; +import api.support.http.IndividualResource; +import api.support.http.ResourceClient; +import api.support.matchers.UUIDMatcher; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; + +class ItemsByInstanceResourceTest extends APITests { + + private static final String TENANT_ID_COLLEGE = "college"; + private static final String TENANT_ID_UNIVERSITY = "university"; + + @Test + void canGetInstanceById() { + IndividualResource instance = instancesFixture.basedUponDunkirk(); + UUID instanceId = instance.getId(); + + // create item in tenant "college" + setTempTenantId(TENANT_ID_COLLEGE); + IndividualResource collegeLocation = locationsFixture.mainFloor(); + IndividualResource collegeHoldings = holdingsFixture.defaultWithHoldings(instanceId); + IndividualResource collegeItem = itemsFixture.createItemWithHoldingsAndLocation( + collegeHoldings.getId(), collegeLocation.getId()); + clearTempTenantId(); + + // create item in tenant "university" + setTempTenantId(TENANT_ID_UNIVERSITY); + IndividualResource universityLocation = locationsFixture.thirdFloor(); + IndividualResource universityHoldings = holdingsFixture.defaultWithHoldings(instanceId); + IndividualResource universityItem = itemsFixture.createItemWithHoldingsAndLocation( + universityHoldings.getId(), universityLocation.getId()); + clearTempTenantId(); + + // make sure neither item exists in current tenant + assertThat(itemsFixture.getById(collegeItem.getId()).getResponse().getStatusCode(), is(404)); + assertThat(itemsFixture.getById(universityItem.getId()).getResponse().getStatusCode(), is(404)); + + List searchItems = List.of( + collegeItem.getJson().put("tenantId", TENANT_ID_COLLEGE), + universityItem.getJson().put("tenantId", TENANT_ID_UNIVERSITY)); + + JsonObject searchInstance = new SearchInstanceBuilder(instance.getJson()) + .withItems(searchItems) + .create(); + + ResourceClient.forSearchClient().create(searchInstance); + Response response = get(String.format("query=(id==%s)", instanceId), 200); + JsonObject responseJson = response.getJson(); + JsonArray items = responseJson.getJsonArray("items"); + + assertThat(responseJson.getString("id"), UUIDMatcher.is(instanceId)); + assertThat(items, iterableWithSize(2)); + assertThat(items, hasItem(allOf( + hasJsonPath("id", UUIDMatcher.is(collegeItem.getId())), + hasJsonPath("tenantId", is(TENANT_ID_COLLEGE))))); + assertThat(items, hasItem(allOf( + hasJsonPath("id", UUIDMatcher.is(universityItem.getId())), + hasJsonPath("tenantId", is(TENANT_ID_UNIVERSITY))))); + } + + @Test + void canGetEmptyResult() { + UUID instanceId = UUID.randomUUID(); + + ResourceClient.forSearchClient().replace(instanceId, new JsonObject()); + Response response = get(String.format("query=(id==%s)", instanceId), HTTP_OK.toInt()); + JsonObject responseJson = response.getJson(); + + assertThat(responseJson.isEmpty(), is(true)); + } + + private Response get(String query, int expectedStatusCode) { + return restAssuredClient.get(itemsByInstanceUrl(query), expectedStatusCode, + "items-by-instance-request"); + } +} diff --git a/src/test/java/api/loans/CheckInByBarcodeTests.java b/src/test/java/api/loans/CheckInByBarcodeTests.java index 48973a4ff5..6df1466060 100644 --- a/src/test/java/api/loans/CheckInByBarcodeTests.java +++ b/src/test/java/api/loans/CheckInByBarcodeTests.java @@ -1639,7 +1639,7 @@ void availableNoticeIsSentUponCheckInWhenRequesterBarcodeWasChanged() { @Test void linkItemToHoldTLRWithHoldShelfWhenCheckedInItemThenFulfilledWithSuccess(){ reconfigureTlrFeature(TlrFeatureStatus.NOT_CONFIGURED); - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); UUID instanceId = instancesFixture.basedUponDunkirk().getId(); IndividualResource defaultWithHoldings = holdingsFixture.defaultWithHoldings(instanceId); IndividualResource checkedOutItem = itemsClient.create(buildCheckedOutItemWithHoldingRecordsId(defaultWithHoldings.getId())); @@ -1666,7 +1666,7 @@ void linkItemToHoldTLRWithHoldShelfWhenCheckedInItemThenFulfilledWithSuccess(){ @Test void checkInItemWhenServicePointHasChangedToNoPickupLocation() { - configurationsFixture.enableTlrFeature(); + reconfigureTlrFeature(TlrFeatureStatus.ENABLED); var instanceId = instancesFixture.basedUponDunkirk().getId(); var defaultWithHoldings = holdingsFixture.defaultWithHoldings(instanceId); var checkedOutItem = itemsClient.create(buildCheckedOutItemWithHoldingRecordsId( @@ -1683,7 +1683,7 @@ void checkInItemWhenServicePointHasChangedToNoPickupLocation() { ServicePointBuilder changedServicePoint = new ServicePointBuilder( servicePointsFixture.cd1().getId(), servicePointName, servicePointCode, discoveryDisplayName, - description, shelvingLagTime, Boolean.FALSE, null, KEEP_THE_CURRENT_DUE_DATE.name()); + description, shelvingLagTime, Boolean.FALSE, null, KEEP_THE_CURRENT_DUE_DATE.name(), false); // Update existing service point servicePointsFixture.update(servicePointCode, changedServicePoint); @@ -1715,7 +1715,7 @@ void checkInItemWhenServicePointHasChangedToNoPickupLocation() { @Test void linkItemToHoldTLRWithDeliveryWhenCheckedInThenFulfilledWithSuccess(){ reconfigureTlrFeature(TlrFeatureStatus.NOT_CONFIGURED); - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); UUID instanceId = instancesFixture.basedUponDunkirk().getId(); IndividualResource defaultWithHoldings = holdingsFixture.defaultWithHoldings(instanceId); IndividualResource checkedOutItem = itemsClient.create(buildCheckedOutItemWithHoldingRecordsId(defaultWithHoldings.getId())); @@ -1741,7 +1741,7 @@ void linkItemToHoldTLRWithDeliveryWhenCheckedInThenFulfilledWithSuccess(){ @Test void requestsShouldChangePositionWhenTheyGoInFulfillmentOnCheckIn() { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); List items = itemsFixture.createMultipleItemsForTheSameInstance(3); ItemResource firstItem = items.get(0); @@ -1792,7 +1792,7 @@ void requestsShouldChangePositionWhenTheyGoInFulfillmentOnCheckIn() { @Test void canCheckinItemWhenRequestForAnotherItemOfSameInstanceExists() { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); List items = itemsFixture.createMultipleItemsForTheSameInstance(2); ItemResource firstItem = items.get(0); @@ -1813,7 +1813,7 @@ void canCheckinItemWhenRequestForAnotherItemOfSameInstanceExists() { @Test void canFulFillRecallRequestWhenCheckInAnotherItemOfSameInstance() { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); List items = itemsFixture.createMultipleItemsForTheSameInstance(2); ItemResource firstItem = items.get(0); ItemResource secondItem = items.get(1); @@ -1839,7 +1839,7 @@ void canFulFillRecallRequestWhenCheckInAnotherItemOfSameInstance() { @Test void canFulFillRecallRequestWhenCheckInAnotherItemOfSameInstanceWithMultipleRecallRequests() { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); List items = itemsFixture.createMultipleItemsForTheSameInstance(3); ItemResource firstItem = items.get(0); ItemResource secondItem = items.get(1); @@ -1880,7 +1880,7 @@ void canFulFillRecallRequestWhenCheckInAnotherItemOfSameInstanceWithMultipleReca @Test void shouldNotLinkTitleLevelHoldRequestToAnItemUponCheckInWhenItemIsNonRequestable() { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); ItemResource item = itemsFixture.basedUponNod(); checkOutFixture.checkOutByBarcode(item, usersFixture.rebecca()); IndividualResource request = requestsFixture.placeTitleLevelRequest(HOLD, item.getInstanceId(), @@ -1910,7 +1910,7 @@ void shouldNotLinkTitleLevelHoldRequestToAnItemUponCheckInWhenItemIsNonRequestab @Test void shouldNotLinkTitleLevelHoldRequestToAnItemUponCheckInWhenItemIsNonLoanable() { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); ItemResource item = itemsFixture.basedUponNod(); checkOutFixture.checkOutByBarcode(item, usersFixture.rebecca()); IndividualResource request = requestsFixture.placeTitleLevelRequest(HOLD, item.getInstanceId(), @@ -1926,7 +1926,7 @@ void shouldNotLinkTitleLevelHoldRequestToAnItemUponCheckInWhenItemIsNonLoanable( @Test void shouldNotLinkTitleLevelRecallRequestToNewItemUponCheckInWhenItemIsNonRequestable() { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); UUID canCirculateLoanTypeId = loanTypesFixture.canCirculate().getId(); UUID readingRoomLoanTypeId = loanTypesFixture.readingRoom().getId(); @@ -1972,7 +1972,7 @@ void shouldNotLinkTitleLevelRecallRequestToNewItemUponCheckInWhenItemIsNonReques @Test void shouldNotLinkTitleLevelRecallRequestToNewItemUponCheckInWhenItemIsNonLoanable() { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); UUID canCirculateLoanTypeId = loanTypesFixture.canCirculate().getId(); UUID readingRoomLoanTypeId = loanTypesFixture.readingRoom().getId(); diff --git a/src/test/java/api/loans/CheckOutByBarcodeTests.java b/src/test/java/api/loans/CheckOutByBarcodeTests.java index a827a13001..d1da178711 100644 --- a/src/test/java/api/loans/CheckOutByBarcodeTests.java +++ b/src/test/java/api/loans/CheckOutByBarcodeTests.java @@ -36,6 +36,7 @@ import static api.support.matchers.RequestMatchers.hasPosition; import static api.support.matchers.RequestMatchers.isClosedFilled; import static api.support.matchers.RequestMatchers.isOpenAwaitingPickup; +import static api.support.matchers.RequestMatchers.isOpenInTransit; import static api.support.matchers.ResponseStatusCodeMatcher.hasStatus; import static api.support.matchers.TextDateTimeMatcher.isEquivalentTo; import static api.support.matchers.TextDateTimeMatcher.withinSecondsAfter; @@ -141,11 +142,11 @@ class CheckOutByBarcodeTests extends APITests { private static final ZonedDateTime TEST_DUE_DATE = ZonedDateTime.of(2019, 4, 20, 11, 30, 0, 0, UTC); public static final String OVERRIDE_ITEM_NOT_LOANABLE_BLOCK_PERMISSION = - "circulation.override-item-not-loanable-block"; + "circulation.override-item-not-loanable-block.post"; public static final String OVERRIDE_PATRON_BLOCK_PERMISSION = - "circulation.override-patron-block"; + "circulation.override-patron-block.post"; public static final String OVERRIDE_ITEM_LIMIT_BLOCK_PERMISSION = - "circulation.override-item-limit-block"; + "circulation.override-item-limit-block.post"; public static final String INSUFFICIENT_OVERRIDE_PERMISSIONS = "Insufficient override permissions"; private static final String TEST_COMMENT = "Some comment"; @@ -2492,7 +2493,7 @@ void canCheckOutUsingAlternateCheckoutRollingLoanPolicy() { @ParameterizedTest @EnumSource(value = TlrFeatureStatus.class, names = {"DISABLED", "NOT_CONFIGURED"}) void titleLevelRequestIsIgnoredWhenTlrFeatureIsNotEnabled(TlrFeatureStatus tlrFeatureStatus) { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); ItemResource item = itemsFixture.basedUponNod(); UserResource borrower = usersFixture.steve(); @@ -2519,7 +2520,7 @@ void titleLevelRequestIsIgnoredWhenTlrFeatureIsNotEnabled(TlrFeatureStatus tlrFe "Title, Title" }) void canFulfilPageAndHoldRequestsWithMixedLevels(String pageRequestLevel, String holdRequestLevel) { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); ItemResource item = itemsFixture.basedUponNod(); UserResource firstRequester = usersFixture.steve(); @@ -2582,7 +2583,7 @@ void canFulfilPageAndHoldRequestsWithMixedLevels(String pageRequestLevel, String @Test void canCheckoutItemWhenTitleLevelPageRequestsExistForDifferentItemsOfSameInstance() { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); List items = itemsFixture.createMultipleItemsForTheSameInstance(4); UUID instanceId = items.stream().findAny().orElseThrow().getInstanceId(); @@ -2613,7 +2614,7 @@ void canCheckoutItemWhenTitleLevelPageRequestsExistForDifferentItemsOfSameInstan void cannotCheckoutItemWhenTitleLevelPageRequestExistsForSameItem( String firstRequestLevel, String secondRequestLevel) { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); List items = itemsFixture.createMultipleItemsForTheSameInstance(2); ItemResource randomItem = items.stream().findAny().orElseThrow(); @@ -2752,6 +2753,45 @@ void concurrentCheckoutsWhenCheckoutLockFeatureEnabled() throws InterruptedExcep contains("Patron has reached maximum limit of 1 items for loan type"))); } + @Test + void circulationItemCheckOutUpdatesPrimaryEcsRequestStatus() { + settingsFixture.enableTlrFeature(); + UUID itemId = UUID.randomUUID(); + String itemBarcode = "item_barcode"; + UUID pickupServicePointId = servicePointsFixture.cd1().getId(); + UserResource requester = usersFixture.steve(); + IndividualResource realInstance = instancesFixture.basedUponDunkirk(); + + // place title-level hold on instance with no items + IndividualResource initialRequest = requestsFixture.placeTitleLevelHoldShelfRequest( + realInstance.getId(), requester, ZonedDateTime.now(), pickupServicePointId); + UUID requestId = initialRequest.getId(); + + // create circulation item which has same ID as the "real" item, but different holdingsId, instanceId, etc. + UUID dcbInstanceId = UUID.randomUUID(); + IndividualResource dcbHoldings = holdingsFixture.defaultWithHoldings(dcbInstanceId); + final IndividualResource circulationItem = circulationItemsFixture.createCirculationItem( + itemId, itemBarcode, dcbHoldings.getId(), locationsFixture.mainFloor().getId(), "DCB instance"); + + // update request same way DCB does it when a borrowing transaction is created + requestsStorageClient.replace(requestId, + requestsStorageClient.get(requestId) + .getJson() + .put("itemId", itemId.toString()) + .put("holdingsRecordId", dcbHoldings.getId().toString()) + .put("item", new JsonObject().put("barcode", itemBarcode))); + + UUID randomServicePointId = servicePointsFixture.cd2().getId(); + checkInFixture.checkInByBarcode(circulationItem, randomServicePointId); + assertThat(requestsFixture.getById(requestId).getJson(), isOpenInTransit()); + + checkInFixture.checkInByBarcode(circulationItem, pickupServicePointId); + assertThat(requestsFixture.getById(requestId).getJson(), isOpenAwaitingPickup()); + + checkOutFixture.checkOutByBarcode(circulationItem, requester); + assertThat(requestsFixture.getById(requestId).getJson(), isClosedFilled()); + } + private IndividualResource placeRequest(String requestLevel, ItemResource item, IndividualResource requester) { diff --git a/src/test/java/api/loans/ReminderFeeTests.java b/src/test/java/api/loans/ReminderFeeTests.java index a127cfbf83..139be3905b 100644 --- a/src/test/java/api/loans/ReminderFeeTests.java +++ b/src/test/java/api/loans/ReminderFeeTests.java @@ -58,7 +58,7 @@ class ReminderFeeTests extends APITests { private UUID remindersTwoDaysBetweenNotOnClosedDaysPolicyId; - private static final String OVERRIDE_RENEWAL_BLOCK_PERMISSION = "circulation.override-renewal-block"; + private static final String OVERRIDE_RENEWAL_BLOCK_PERMISSION = "circulation.override-renewal-block.post"; @BeforeEach void beforeEach() { diff --git a/src/test/java/api/loans/RenewalAPITests.java b/src/test/java/api/loans/RenewalAPITests.java index 5501f05c7c..b77c3a4770 100644 --- a/src/test/java/api/loans/RenewalAPITests.java +++ b/src/test/java/api/loans/RenewalAPITests.java @@ -91,7 +91,6 @@ import org.junit.jupiter.params.provider.ValueSource; import api.support.APITests; -import api.support.TlrFeatureStatus; import api.support.builders.CheckOutByBarcodeRequestBuilder; import api.support.builders.ClaimItemReturnedRequestBuilder; import api.support.builders.FeeFineBuilder; @@ -114,7 +113,6 @@ import api.support.http.ItemResource; import api.support.http.OkapiHeaders; import api.support.http.ResourceClient; -import api.support.http.UserResource; import api.support.matchers.OverdueFineMatcher; import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; @@ -123,10 +121,10 @@ public abstract class RenewalAPITests extends APITests { public static final String PATRON_BLOCK_NAME = "patronBlock"; private static final String TEST_COMMENT = "Some comment"; - private static final String OVERRIDE_PATRON_BLOCK_PERMISSION = "circulation.override-patron-block"; + private static final String OVERRIDE_PATRON_BLOCK_PERMISSION = "circulation.override-patron-block.post"; public static final String OVERRIDE_ITEM_LIMIT_BLOCK_PERMISSION = - "circulation.override-item-limit-block"; - private static final String OVERRIDE_RENEWAL_BLOCK_PERMISSION = "circulation.override-renewal-block"; + "circulation.override-item-limit-block.post"; + private static final String OVERRIDE_RENEWAL_BLOCK_PERMISSION = "circulation.override-renewal-block.post"; private static final String RENEWED_THROUGH_OVERRIDE = "renewedThroughOverride"; private static final String PATRON_WAS_BLOCKED_MESSAGE = "Patron blocked from renewing"; private static final String ITEM_IS_DECLARED_LOST = "item is Declared lost"; diff --git a/src/test/java/api/loans/scenarios/ChangeDueDateAPITests.java b/src/test/java/api/loans/scenarios/ChangeDueDateAPITests.java index 9c84c684dc..509f1229d5 100644 --- a/src/test/java/api/loans/scenarios/ChangeDueDateAPITests.java +++ b/src/test/java/api/loans/scenarios/ChangeDueDateAPITests.java @@ -436,7 +436,7 @@ void dueDateChangeShouldNotUnsetRenewalFlagValueWhenTlrFeatureEnabled() { assertThat(recalledLoan.getJson().getBoolean("dueDateChangedByRecall"), equalTo(true)); - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); requestsClient.create(new RequestBuilder() .recall() .titleRequestLevel() @@ -496,7 +496,7 @@ void dueDateChangeShouldUnsetRenewalFlagValueWhenTlrFeatureDisabledOrNotConfigur assertThat(recalledLoan.getJson().getBoolean("dueDateChangedByRecall"), equalTo(true)); - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); requestsClient.create(new RequestBuilder() .recall() diff --git a/src/test/java/api/loans/scenarios/CheckoutWithRequestScenarioTests.java b/src/test/java/api/loans/scenarios/CheckoutWithRequestScenarioTests.java index 6332320521..efcd458121 100644 --- a/src/test/java/api/loans/scenarios/CheckoutWithRequestScenarioTests.java +++ b/src/test/java/api/loans/scenarios/CheckoutWithRequestScenarioTests.java @@ -165,7 +165,7 @@ void checkingOutWithHoldRequestAppliesAlternatePeriodAndScheduledForFixedPolicy( @Test void alternatePeriodShouldBeAppliedWhenRequestQueueContainsHoldTlr() { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); List items = itemsFixture.createMultipleItemsForTheSameInstance(2); var firstItem = items.get(0); var secondItem = items.get(1); @@ -245,7 +245,7 @@ void alternatePeriodShouldBeAppliedWhenRequestQueueContainsHoldTlr() { @Test void alternatePeriodShouldNotBeAppliedWhenRequestQueueContainsHoldIlrForDifferentItem() { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); List items = itemsFixture.createMultipleItemsForTheSameInstance(2); var firstItem = items.get(0); var secondItem = items.get(1); diff --git a/src/test/java/api/queue/RequestQueueResourceTest.java b/src/test/java/api/queue/RequestQueueResourceTest.java index 98d70164b0..04fb3cffa8 100644 --- a/src/test/java/api/queue/RequestQueueResourceTest.java +++ b/src/test/java/api/queue/RequestQueueResourceTest.java @@ -303,7 +303,7 @@ void shouldGetRequestQueueForItemSuccessfully() { @Test void shouldGetRequestQueueForInstanceSuccessfully() { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); UUID isbnIdentifierId = identifierTypesFixture.isbn().getId(); String isbnValue = "9780866989427"; diff --git a/src/test/java/api/requests/AllowedServicePointsAPITests.java b/src/test/java/api/requests/AllowedServicePointsAPITests.java index e6995df3f4..189f25927c 100644 --- a/src/test/java/api/requests/AllowedServicePointsAPITests.java +++ b/src/test/java/api/requests/AllowedServicePointsAPITests.java @@ -22,6 +22,7 @@ import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.iterableWithSize; import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.core.Is.is; import java.time.ZonedDateTime; import java.util.ArrayList; @@ -137,6 +138,7 @@ void shouldReturnListOfAllowedServicePointsForRequest(RequestType requestType, List allowedSpInResponse) { var requesterId = usersFixture.steve().getId().toString(); + var patronGroupId = patronGroupsFixture.regular().getId().toString(); var items = itemsFixture.createMultipleItemForTheSameInstance(1, List.of(ib -> ib.withStatus(itemStatus.getValue()))); var item = items.get(0); @@ -154,8 +156,18 @@ void shouldReturnListOfAllowedServicePointsForRequest(RequestType requestType, .collect(Collectors.toSet())); var response = requestLevel == TITLE - ? get("create", requesterId, instanceId, null, null, HttpStatus.SC_OK).getJson() - : get("create", requesterId, null, itemId, null, HttpStatus.SC_OK).getJson(); + ? get("create", requesterId, null, instanceId, null, null, null, null, + HttpStatus.SC_OK).getJson() + : get("create", requesterId, null, null, itemId, null, null, null, + HttpStatus.SC_OK).getJson(); + + assertThat(response, allowedServicePointMatcher(Map.of(requestType, allowedSpInResponse))); + + response = requestLevel == TITLE + ? get("create", null, patronGroupId, instanceId, null, null, null, null, + HttpStatus.SC_OK).getJson() + : get("create", null, patronGroupId, null, itemId, null, null, null, + HttpStatus.SC_OK).getJson(); assertThat(response, allowedServicePointMatcher(Map.of(requestType, allowedSpInResponse))); } @@ -202,7 +214,7 @@ void shouldReturnListOfAllowedServicePointsForRequestReplacement( .map(UUID::fromString) .collect(Collectors.toSet())); - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); var pickupLocationId = allowedSpByPolicy.stream() .findFirst() .map(AllowedServicePoint::getId) @@ -223,11 +235,28 @@ void shouldReturnListOfAllowedServicePointsForRequestReplacement( var requestId = request == null ? null : request.getId().toString(); var response = - get("replace", null, null, null, requestId, HttpStatus.SC_OK).getJson(); + get("replace", null, null, null, null, requestId, null, null, + HttpStatus.SC_OK).getJson(); assertThat(response, allowedServicePointMatcher(Map.of(requestType, allowedSpInResponse))); } + @Test + void shouldReturnListOfAllowedServicePointsForHoldRequestReplacementWhenInstanceHasNoItems() { + settingsFixture.configureTlrFeature(true, false, null, null, null); + var requester = usersFixture.steve(); + var instanceId = instancesFixture.basedUponDunkirk().getId(); + var servicePointId = servicePointsFixture.cd1().getId(); + setRequestPolicyWithAllowedServicePoints(HOLD, Set.of(servicePointId)); + IndividualResource request = requestsFixture.placeTitleLevelHoldShelfRequest( + instanceId, requester, ZonedDateTime.now(), servicePointId); + + var response = get("replace", null, null, null, null, request.getId().toString(), "true", null, + HttpStatus.SC_OK).getJson(); + var expectedServicePoint = new AllowedServicePoint(servicePointId.toString(), "Circ Desk 1"); + assertThat(response, allowedServicePointMatcher(Map.of(HOLD, List.of(expectedServicePoint)))); + } + public static Object[] shouldReturnOnlyExistingAllowedServicePointForRequestParameters() { String sp1Id = randomId(); String sp2Id = randomId(); @@ -494,7 +523,7 @@ void allPickupLocationsAreReturnedForTitleLevelHoldWhenItIsDisabledAndInstanceHa boolean instanceHasHoldings) { // allow TLR-holds for instances with no holdings/items - configurationsFixture.configureTlrFeature(true, false, null, null, null); + settingsFixture.configureTlrFeature(true, false, null, null, null); IndividualResource sp1 = servicePointsFixture.cd1(); // pickup location IndividualResource sp2 = servicePointsFixture.cd2(); // pickup location @@ -515,7 +544,7 @@ void allPickupLocationsAreReturnedForTitleLevelHoldWhenItIsDisabledAndInstanceHa @Test void noAllowedServicePointsAreReturnedForTitleLevelHoldWhenItIsDisabledAndInstanceHasItems() { // allow TLR-holds for instances with no holdings/items - configurationsFixture.configureTlrFeature(true, false, null, null, null); + settingsFixture.configureTlrFeature(true, false, null, null, null); IndividualResource sp1 = servicePointsFixture.cd1(); // pickup location servicePointsFixture.cd2(); // pickup location @@ -653,8 +682,8 @@ void allowedServicePointsAreSortedByName() { @Test void getReplaceFailsWhenRequestDoesNotExist() { String requestId = randomId(); - Response response = get("replace", null, null, null, requestId, - HttpStatus.SC_UNPROCESSABLE_ENTITY); + Response response = get("replace", null, null, null, null, requestId, null, + null, HttpStatus.SC_UNPROCESSABLE_ENTITY); assertThat(response.getJson(), hasErrorWith(hasMessage( String.format("Request with ID %s was not found", requestId)))); } @@ -663,8 +692,8 @@ void getReplaceFailsWhenRequestDoesNotExist() { void getMoveFailsWhenRequestDoesNotExist() { String requestId = randomId(); String itemId = itemsFixture.basedUponNod().getId().toString(); - Response response = get("move", null, null, itemId, requestId, - HttpStatus.SC_UNPROCESSABLE_ENTITY); + Response response = get("move", null, null, null, itemId, requestId, null, + null, HttpStatus.SC_UNPROCESSABLE_ENTITY); assertThat(response.getJson(), hasErrorWith(hasMessage( String.format("Request with ID %s was not found", requestId)))); } @@ -703,7 +732,7 @@ void shouldReturnListOfAllowedServicePointsForRequestMove(RequestLevel requestLe setRequestPolicyWithAllowedServicePoints(PAGE, Set.of(sp1Uuid)); - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); IndividualResource request = requestsFixture.place(new RequestBuilder() .withRequestType(PAGE.toString()) @@ -725,62 +754,158 @@ void shouldReturnListOfAllowedServicePointsForRequestMove(RequestLevel requestLe // Valid "move" request var moveResponse = - get("move", null, null, itemToMoveToId, requestId, HttpStatus.SC_OK).getJson(); + get("move", null, null, null, itemToMoveToId, requestId, null, null, + HttpStatus.SC_OK).getJson(); assertThat(moveResponse, allowedServicePointMatcher(Map.of(HOLD, List.of(sp2)))); // Invalid "move" requests - var invalidMoveResponse1 = get("move", null, null, null, requestId, - HttpStatus.SC_BAD_REQUEST); + var invalidMoveResponse1 = get("move", null, null, null, null, requestId, + null, null, HttpStatus.SC_BAD_REQUEST); assertThat(invalidMoveResponse1.getBody(), equalTo("Invalid combination of query parameters")); - var invalidMoveResponse2 = get("move", null, null, itemToMoveToId, null, - HttpStatus.SC_BAD_REQUEST); + var invalidMoveResponse2 = get("move", null, null, null, itemToMoveToId, + null, + null, null, HttpStatus.SC_BAD_REQUEST); assertThat(invalidMoveResponse2.getBody(), equalTo("Invalid combination of query parameters")); - var invalidMoveResponse3 = get("move", null, null, null, null, - HttpStatus.SC_BAD_REQUEST); + var invalidMoveResponse3 = get("move", null, null, null, null, null, + null, null, HttpStatus.SC_BAD_REQUEST); assertThat(invalidMoveResponse3.getBody(), equalTo("Invalid combination of query parameters")); - var invalidMoveResponse4 = get("move", requesterId, null, itemToMoveToId, requestId, - HttpStatus.SC_BAD_REQUEST); + var invalidMoveResponse4 = get("move", requesterId,null, null, itemToMoveToId, requestId, + null, null, HttpStatus.SC_BAD_REQUEST); assertThat(invalidMoveResponse4.getBody(), equalTo("Invalid combination of query parameters")); - var invalidMoveResponse5 = get("move", null, instanceId, itemToMoveToId, requestId, - HttpStatus.SC_BAD_REQUEST); + var invalidMoveResponse5 = get("move", null, null, instanceId, + itemToMoveToId, requestId, + null, null, HttpStatus.SC_BAD_REQUEST); assertThat(invalidMoveResponse5.getBody(), equalTo("Invalid combination of query parameters")); // Valid "replace" request var replaceResponse = - get("replace", null, null, null, requestId, HttpStatus.SC_OK).getJson(); + get("replace", null, null, null, null, requestId, null, null, + HttpStatus.SC_OK).getJson(); assertThat(replaceResponse, allowedServicePointMatcher(Map.of(HOLD, List.of(sp2)))); // Invalid "replace" requests - var invalidReplaceResponse1 = get("replace", null, null, null, null, - HttpStatus.SC_BAD_REQUEST); + var invalidReplaceResponse1 = get("replace", null, null, null, null, null, + null, null, HttpStatus.SC_BAD_REQUEST); assertThat(invalidReplaceResponse1.getBody(), equalTo("Invalid combination of query parameters")); - var invalidReplaceResponse2 = get("replace", requesterId, null, null, requestId, - HttpStatus.SC_BAD_REQUEST); + var invalidReplaceResponse2 = get("replace", requesterId,null, null, null, requestId, + null, null, HttpStatus.SC_BAD_REQUEST); assertThat(invalidReplaceResponse2.getBody(), equalTo("Invalid combination of query parameters")); - var invalidReplaceResponse3 = get("replace", null, instanceId, null, requestId, - HttpStatus.SC_BAD_REQUEST); + var invalidReplaceResponse3 = get("replace", null, null, instanceId, null + , requestId, + null, null, HttpStatus.SC_BAD_REQUEST); assertThat(invalidReplaceResponse3.getBody(), equalTo("Invalid combination of query parameters")); - var invalidReplaceResponse4 = get("replace", null, null, requestedItemId, requestId, - HttpStatus.SC_BAD_REQUEST); + var invalidReplaceResponse4 = get("replace", null, null, null, + requestedItemId, requestId, + null, null, HttpStatus.SC_BAD_REQUEST); assertThat(invalidReplaceResponse4.getBody(), equalTo("Invalid combination of query parameters")); - var invalidReplaceResponse5 = get("replace", requesterId, instanceId, - requestedItemId, requestId, HttpStatus.SC_BAD_REQUEST); + var invalidReplaceResponse5 = get("replace", requesterId, null, instanceId, + requestedItemId, requestId, null, null, HttpStatus.SC_BAD_REQUEST); assertThat(invalidReplaceResponse5.getBody(), equalTo("Invalid combination of query parameters")); } + @Test + void shouldUseStubItemParameterInCirculationRuleMatchingWhenPresent() { + var requesterId = usersFixture.steve().getId().toString(); + var instanceId = itemsFixture.createMultipleItemsForTheSameInstance(2).get(0) + .getInstanceId().toString(); + var cd1 = servicePointsFixture.cd1(); + var cd2 = servicePointsFixture.cd2(); + var cd4 = servicePointsFixture.cd4(); + var cd5 = servicePointsFixture.cd5(); + final UUID book = materialTypesFixture.book().getId(); + final UUID patronGroup = patronGroupsFixture.regular().getId(); + circulationRulesFixture.updateCirculationRules(createRules("m " + book + + "+ g " + patronGroup, "g " + patronGroup)); + + var response = getCreateOp(requesterId, instanceId, null, "true", null, HttpStatus.SC_OK) + .getJson(); + assertThat(response, hasNoJsonPath(PAGE.getValue())); + JsonArray allowedServicePoints = response.getJsonArray(HOLD.getValue()); + assertServicePointsMatch(allowedServicePoints, List.of(cd1, cd2, cd4, cd5)); + allowedServicePoints = response.getJsonArray(RECALL.getValue()); + assertServicePointsMatch(allowedServicePoints, List.of(cd1, cd2, cd4, cd5)); + + response = getCreateOp(requesterId, instanceId, null, "false", null, HttpStatus.SC_OK) + .getJson(); + assertThat(response, hasNoJsonPath(HOLD.getValue())); + assertThat(response, hasNoJsonPath(RECALL.getValue())); + allowedServicePoints = response.getJsonArray(PAGE.getValue()); + assertServicePointsMatch(allowedServicePoints, List.of(cd1, cd2, cd4, cd5)); + + response = getCreateOp(requesterId, instanceId, null, HttpStatus.SC_OK).getJson(); + assertThat(response, hasNoJsonPath(HOLD.getValue())); + assertThat(response, hasNoJsonPath(RECALL.getValue())); + allowedServicePoints = response.getJsonArray(PAGE.getValue()); + assertServicePointsMatch(allowedServicePoints, List.of(cd1, cd2, cd4, cd5)); + } + + @Test + void shouldReturnErrorIfUseStubItemIsInvalid() { + Response errorResponse = getCreateOp(UUID.randomUUID().toString(), + UUID.randomUUID().toString(), null, "invalid", null, + HttpStatus.SC_BAD_REQUEST); + assertThat(errorResponse.getBody(), is("useStubItem is not a valid boolean: invalid.")); + } + + @Test + void shouldConsiderEcsRequestRoutingParameterForAllowedServicePoints() { + var requesterId = usersFixture.steve().getId().toString(); + var instanceId = itemsFixture.createMultipleItemsForTheSameInstance(2).get(0) + .getInstanceId().toString(); + var cd1 = servicePointsFixture.cd1(); + var cd2 = servicePointsFixture.cd2(); + var cd4 = servicePointsFixture.cd4(); + var cd11 = servicePointsFixture.cd11(); + + final Map> allowedServicePointsInPolicy = new HashMap<>(); + allowedServicePointsInPolicy.put(PAGE, Set.of(cd1.getId(), cd2.getId(), cd11.getId())); + allowedServicePointsInPolicy.put(HOLD, Set.of(cd4.getId(), cd2.getId(), cd11.getId())); + var requestPolicy = requestPoliciesFixture.createRequestPolicyWithAllowedServicePoints( + allowedServicePointsInPolicy, PAGE, HOLD); + policiesActivation.use(PoliciesToActivate.builder().requestPolicy(requestPolicy)); + + var response = getCreateOp(requesterId, instanceId, null, "false", "true", + HttpStatus.SC_OK).getJson(); + JsonArray allowedServicePoints = response.getJsonArray(PAGE.getValue()); + assertServicePointsMatch(allowedServicePoints, List.of(cd11)); + assertThat(response, hasNoJsonPath(HOLD.getValue())); + assertThat(response, hasNoJsonPath(RECALL.getValue())); + + response = getCreateOp(requesterId, instanceId, null, "false", "false", + HttpStatus.SC_OK).getJson(); + allowedServicePoints = response.getJsonArray(PAGE.getValue()); + assertServicePointsMatch(allowedServicePoints, List.of(cd1, cd2)); + assertThat(response, hasNoJsonPath(HOLD.getValue())); + assertThat(response, hasNoJsonPath(RECALL.getValue())); + + response = getCreateOp(requesterId, instanceId, null, "false", null, + HttpStatus.SC_OK).getJson(); + allowedServicePoints = response.getJsonArray(PAGE.getValue()); + assertServicePointsMatch(allowedServicePoints, List.of(cd1, cd2)); + assertThat(response, hasNoJsonPath(HOLD.getValue())); + assertThat(response, hasNoJsonPath(RECALL.getValue())); + } + + @Test + void shouldReturnErrorIfEcsRequestRoutingIsInvalid() { + Response errorResponse = getCreateOp(UUID.randomUUID().toString(), + UUID.randomUUID().toString(), null, null, "invalid", HttpStatus.SC_BAD_REQUEST); + assertThat(errorResponse.getBody(), is("ecsRequestRouting is not a valid boolean: invalid.")); + } + private void assertServicePointsMatch(JsonArray response, List expectedServicePoints) { @@ -805,24 +930,38 @@ private void assertServicePointsMatch(JsonArray response, .map(sp -> sp.getJson().getString("name")).toArray(String[]::new))); } + private Response getCreateOp(String requesterId, String instanceId, String itemId, + String useStubItem, String ecsRequestRouting, int expectedStatusCode) { + + return get("create", requesterId,null, instanceId, itemId, null, useStubItem, + ecsRequestRouting, expectedStatusCode); + } + private Response getCreateOp(String requesterId, String instanceId, String itemId, int expectedStatusCode) { - return get("create", requesterId, instanceId, itemId, null, expectedStatusCode); + return get("create", requesterId, null, instanceId, itemId, null, null, null, + expectedStatusCode); } private Response getReplaceOp(String requestId, int expectedStatusCode) { - return get("replace", null, null, null, requestId, expectedStatusCode); + return get("replace", null, null, null, null, requestId, null, null, + expectedStatusCode); } - private Response get(String operation, String requesterId, String instanceId, String itemId, - String requestId, int expectedStatusCode) { + private Response get(String operation, String requesterId, + String patronGroupId, String instanceId, String itemId, + String requestId, String useStubItem, String ecsRequestRouting, + int expectedStatusCode) { List queryParams = new ArrayList<>(); queryParams.add(namedParameter("operation", operation)); if (requesterId != null) { queryParams.add(namedParameter("requesterId", requesterId)); } + if (patronGroupId != null) { + queryParams.add(namedParameter("patronGroupId", patronGroupId)); + } if (instanceId != null) { queryParams.add(namedParameter("instanceId", instanceId)); } @@ -832,6 +971,12 @@ private Response get(String operation, String requesterId, String instanceId, St if (requestId != null) { queryParams.add(namedParameter("requestId", requestId)); } + if (useStubItem != null) { + queryParams.add(namedParameter("useStubItem", useStubItem)); + } + if (ecsRequestRouting != null) { + queryParams.add(namedParameter("ecsRequestRouting", ecsRequestRouting)); + } return restAssuredClient.get(allowedServicePointsUrl(), queryParams, expectedStatusCode, "allowed-service-points"); @@ -854,4 +999,21 @@ private ServicePointBuilder servicePointBuilder() { .withPickupLocation(TRUE) .withHoldShelfExpriyPeriod(30, "Days"); } + + private String createRules(String firstRuleCondition, String secondRuleCondition) { + final var loanPolicy = loanPoliciesFixture.canCirculateRolling().getId().toString(); + final var allowAllRequestPolicy = requestPoliciesFixture.allowAllRequestPolicy() + .getId().toString(); + final var holdAndRecallRequestPolicy = requestPoliciesFixture.allowHoldAndRecallRequestPolicy() + .getId().toString(); + final var noticePolicy = noticePoliciesFixture.activeNotice().getId().toString(); + final var overdueFinePolicy = overdueFinePoliciesFixture.facultyStandard().getId().toString(); + final var lostItemFeePolicy = lostItemFeePoliciesFixture.facultyStandard().getId().toString(); + + return String.join("\n", + "priority: t, s, c, b, a, m, g", + "fallback-policy: l " + loanPolicy + " r " + allowAllRequestPolicy + " n " + noticePolicy + " o " + overdueFinePolicy + " i " + lostItemFeePolicy, + firstRuleCondition + " : l " + loanPolicy + " r " + allowAllRequestPolicy + " n " + noticePolicy + " o " + overdueFinePolicy + " i " + lostItemFeePolicy, + secondRuleCondition + " : l " + loanPolicy + " r " + holdAndRecallRequestPolicy + " n " + noticePolicy + " o " + overdueFinePolicy + " i " + lostItemFeePolicy); + } } diff --git a/src/test/java/api/requests/RequestsAPICreationTests.java b/src/test/java/api/requests/RequestsAPICreationTests.java index 21985ad0fc..0d302544b6 100644 --- a/src/test/java/api/requests/RequestsAPICreationTests.java +++ b/src/test/java/api/requests/RequestsAPICreationTests.java @@ -182,7 +182,7 @@ public class RequestsAPICreationTests extends APITests { private static final UUID CANCELLATION_TEMPLATE_ID_FROM_TLR_SETTINGS = UUID.randomUUID(); public static final String CREATE_REQUEST_PERMISSION = "circulation.requests.item.post"; - public static final String OVERRIDE_PATRON_BLOCK_PERMISSION = "circulation.override-patron-block"; + public static final String OVERRIDE_PATRON_BLOCK_PERMISSION = "circulation.override-patron-block.post"; public static final OkapiHeaders HEADERS_WITH_ALL_OVERRIDE_PERMISSIONS = buildOkapiHeadersWithPermissions(CREATE_REQUEST_PERMISSION, OVERRIDE_PATRON_BLOCK_PERMISSION); public static final OkapiHeaders HEADERS_WITHOUT_OVERRIDE_PERMISSIONS = @@ -194,7 +194,7 @@ public class RequestsAPICreationTests extends APITests { @AfterEach public void afterEach() { mockClockManagerToReturnDefaultDateTime(); - configurationsFixture.deleteTlrFeatureConfig(); + settingsFixture.deleteTlrFeatureSettings(); } @Test @@ -476,7 +476,7 @@ void cannotCreateItemLevelRequestForUnknownInstance(String tlrFeatureStatus, @ParameterizedTest @CsvSource({"Page", "Hold", "Recall"}) void cannotCreateTitleLevelRequestForUnknownInstance(String requestType) { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); UUID patronId = usersFixture.charlotte().getId(); final UUID pickupServicePointId = servicePointsFixture.cd1().getId(); @@ -524,7 +524,7 @@ void cannotCreateTitleLevelRequestForUnknownInstance(String requestType) { }) void cannotCreateRequestForUnknownItem(String tlrFeatureEnabledString, String requestType) { if (Boolean.parseBoolean(tlrFeatureEnabledString)) { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); } IndividualResource instance = instancesFixture.basedUponDunkirk(); @@ -612,7 +612,7 @@ void canCreateTitleLevelRequestWhenTlrEnabled() { final var items = itemsFixture.createMultipleItemsForTheSameInstance(2); UUID instanceId = items.get(0).getInstanceId(); - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); IndividualResource requestResource = requestsClient.create(new RequestBuilder() .page() @@ -646,7 +646,7 @@ void createTitleLevelRequestWhenTlrEnabledSetLocation(String locationCode) { final var items = itemsFixture.createMultipleItemsForTheSameInstance(2); UUID instanceId = items.get(0).getInstanceId(); - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); IndividualResource requestResource = requestsClient.create(new RequestBuilder() .page() @@ -670,7 +670,7 @@ void createTitleLevelRequestWhenTlrEnabledSetLocationNoItems() { final var items = itemsFixture.createMultipleItemsForTheSameInstance(2); UUID instanceId = items.get(0).getInstanceId(); - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); Response response = requestsClient.attemptCreate( new RequestBuilder() @@ -697,7 +697,7 @@ void cannotCreateRequestWithNonExistentRequestLevelWhenTlrEnabled() { ItemResource item = itemsFixture.basedUponSmallAngryPlanet(); UUID instanceId = item.getInstanceId(); - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); Response postResponse = requestsClient.attemptCreate(new RequestBuilder() .recall() @@ -804,7 +804,7 @@ void cannotCreateTlrWhenUserAlreadyRequestedAnItemFromTheSameTitle() { @ParameterizedTest @EnumSource(value = RequestType.class, names = {"HOLD", "RECALL"}) void cannotCreateHoldTlrWhenAvailableItemForInstance(RequestType requestType) { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); List items = itemsFixture.createMultipleItemsForTheSameInstance(2); ItemResource item = items.get(0); @@ -1438,7 +1438,7 @@ void canCreatePagedRequestWhenItemStatusIsAvailable() { void cannotCreateTitleLevelPagedRequestIfThereAreNoAvailableItems() { UUID patronId = usersFixture.charlotte().getId(); final UUID pickupServicePointId = servicePointsFixture.cd1().getId(); - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); UUID instanceId = instancesFixture.basedUponDunkirk().getId(); IndividualResource defaultWithHoldings = holdingsFixture.defaultWithHoldings(instanceId); @@ -1470,7 +1470,7 @@ void canCreateTlrRecallWhenAvailableItemExistsButPageIsNotAllowedByPolicy() { overdueFinePoliciesFixture.facultyStandard().getId(), lostItemFeePoliciesFixture.facultyStandard().getId()); - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); final var items = itemsFixture.createMultipleItemsForTheSameInstance(2); var instanceId = items.get(0).getInstanceId(); @@ -1492,7 +1492,7 @@ void canCreateTlrRecallWhenAvailableItemExistsButPageIsNotAllowedByPolicy() { void canCreateTitleLevelPagedRequest() { UUID patronId = usersFixture.charlotte().getId(); final UUID pickupServicePointId = servicePointsFixture.cd1().getId(); - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); IndividualResource uponDunkirkInstance = instancesFixture.basedUponDunkirk(); UUID instanceId = uponDunkirkInstance.getId(); @@ -1524,7 +1524,7 @@ void canCreateTitleLevelPagedRequest() { void canHaveUserBarcodeInCheckInPublishedEventAfterTitleLevelRequest() { UUID patronId = usersFixture.charlotte().getId(); final UUID pickupServicePointId = servicePointsFixture.cd1().getId(); - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); IndividualResource uponDunkirkInstance = instancesFixture.basedUponDunkirk(); UUID instanceId = uponDunkirkInstance.getId(); @@ -1557,7 +1557,7 @@ void cannotCreateItemLevelRequestIfTitleLevelRequestForInstanceAlreadyCreated() UUID patronId = usersFixture.charlotte().getId(); UUID pickupServicePointId = servicePointsFixture.cd1().getId(); UUID instanceId = UUID.randomUUID(); - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); buildItem(instanceId, "111"); requestsClient.create(buildPageTitleLevelRequest(patronId, pickupServicePointId, instanceId)); @@ -1579,7 +1579,7 @@ void cannotCreateTitleLevelRequestIfItemLevelRequestAlreadyCreated() { UUID patronId = usersFixture.charlotte().getId(); UUID pickupServicePointId = servicePointsFixture.cd1().getId(); UUID instanceId = UUID.randomUUID(); - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); buildItem(instanceId, "111"); ItemResource secondItem = buildItem(instanceId, "222"); @@ -1601,7 +1601,7 @@ void canCreateItemLevelRequestAndTitleLevelRequestForDifferentInstances() { UUID patronId = usersFixture.charlotte().getId(); UUID pickupServicePointId = servicePointsFixture.cd1().getId(); UUID instanceId= UUID.randomUUID(); - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); buildItem(instanceId, "111"); requestsClient.create(buildPageTitleLevelRequest(patronId, pickupServicePointId, @@ -1621,7 +1621,7 @@ void cannotCreateTwoTitleLevelRequestsForSameInstance() { UUID userId = usersFixture.charlotte().getId(); UUID pickupServicePointId = servicePointsFixture.cd1().getId(); UUID instanceId = UUID.randomUUID(); - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); buildItem(instanceId, "111"); buildItem(instanceId, "222"); @@ -1643,7 +1643,7 @@ void cannotCreateTwoItemLevelRequestsForSameItem() { UUID userId = usersFixture.charlotte().getId(); UUID pickupServicePointId = servicePointsFixture.cd1().getId(); UUID instanceId = UUID.randomUUID(); - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); ItemResource item = buildItem(instanceId, "111"); requestsClient.create(buildItemLevelRequest(userId, pickupServicePointId, @@ -1761,7 +1761,7 @@ void canCreateRecallRequestWhenItemIsCheckedOut() { @Test void tlrRecallShouldPickItemWithLoanWithNextClosestDueDateIfAnotherRecallRequestExists() { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); var londonZoneId = ZoneId.of("Europe/London"); var items = itemsFixture.createMultipleItemsForTheSameInstance(3); var firstItem = items.get(0); @@ -1787,7 +1787,7 @@ void tlrRecallShouldPickItemWithLoanWithNextClosestDueDateIfAnotherRecallRequest @Test void tlrRecallShouldPickRecalledLoanWithClosestDueDateIfThereAreNoNotRecalledLoansAndSameAmountOfRecalls() { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); var londonZoneId = ZoneId.of("Europe/London"); var items = itemsFixture.createMultipleItemsForTheSameInstance(4); var firstItem = items.get(0); @@ -1833,7 +1833,7 @@ void tlrRecallWithoutLoanShouldPickRecallableItemFromRequestedInstance(String it IndividualResource inTransitPickupServicePoint = servicePointsFixture.cd2(); UUID instanceId = instancesFixture.basedUponDunkirk().getId(); IndividualResource defaultWithHoldings = holdingsFixture.defaultWithHoldings(instanceId); - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); if (itemStatus.equals("Paged")) { itemsClient.create(new ItemBuilder() .forHolding(defaultWithHoldings.getId()) @@ -1877,7 +1877,7 @@ void tlrRecallShouldFailWhenRequestHasNoLoanOrRecallableItem(String itemStatus) IndividualResource requestPickupServicePoint = servicePointsFixture.cd1(); UUID instanceId = instancesFixture.basedUponDunkirk().getId(); IndividualResource defaultWithHoldings = holdingsFixture.defaultWithHoldings(instanceId); - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); itemsFixture.basedUponDunkirk(holdingBuilder -> holdingBuilder, instanceBuilder -> instanceBuilder.withId(instanceId), itemBuilder -> itemBuilder @@ -2138,7 +2138,7 @@ void canCreateHoldRequestWhenItemIsMissing() { "Lost and paid", "Paged", "In process (non-requestable)", "Intellectual item", "Unavailable", "Restricted", "Unknown", "Awaiting delivery", "Order closed"}) void canCreateTlrHoldRequestWhenInstanceHasItemsWithStatusAllowedForHold(String itemStatus) { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); final ItemResource item = itemsFixture.basedUponSmallAngryPlanet( builder -> builder.withStatus(itemStatus)); IndividualResource response = requestsFixture.placeTitleLevelHoldShelfRequest(item.getInstanceId(), @@ -3307,7 +3307,7 @@ void cannotCreateItemLevelRequestWithoutInstanceId() { @Test void cannotCreateTitleLevelRequestWithoutInstanceId() { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); ItemResource item = itemsFixture.basedUponNod(); @@ -3364,7 +3364,7 @@ void cannotCreateRequestWithItemIdButNoHoldingsRecordId(RequestLevel requestLeve @Test void recallTlrRequestShouldBeAppliedToLoanWithClosestDueDate() { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); UUID pickupServicePointId = servicePointsFixture.cd1().getId(); UUID instanceId = UUID.randomUUID(); @@ -3439,7 +3439,7 @@ void recallTlrRequestShouldBeAppliedToLoanWithClosestDueDate() { void statusOfTlrRequestShouldBeChangedIfAssociatedItemCheckedIn() { UUID pickupServicePointId = servicePointsFixture.cd1().getId(); UUID instanceId = UUID.randomUUID(); - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); ItemResource firstItem = buildItem(instanceId, "111"); ItemResource secondItem = buildItem(instanceId, "222"); @@ -3467,7 +3467,7 @@ void statusOfTlrRequestShouldBeChangedIfAssociatedItemCheckedIn() { @Test void awaitingPickupNoticeShouldBeSentDuringCheckInWhenItemCreatedAfterHoldTlr() { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); NoticePolicyBuilder noticePolicy = new NoticePolicyBuilder() .withName("Policy with available notice") @@ -3499,7 +3499,7 @@ void awaitingPickupNoticeShouldBeSentDuringCheckInWhenItemCreatedAfterHoldTlr() @Test void awaitingPickupNoticeShouldBeSentDuringCheckInWhenItemIsReturnedAndTlrHoldExists() { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); NoticePolicyBuilder noticePolicy = new NoticePolicyBuilder() .withName("Policy with available notice") @@ -3529,7 +3529,7 @@ void awaitingPickupNoticeShouldBeSentDuringCheckInWhenItemIsReturnedAndTlrHoldEx @Test void awaitingPickupNoticesShouldBeSentToMultiplePatronsDuringPagedItemsCheckIn() { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); NoticePolicyBuilder noticePolicy = new NoticePolicyBuilder() .withName("Policy with available notice") .withLoanNotices(Collections.singletonList(new NoticeConfigurationBuilder() @@ -3645,7 +3645,7 @@ void pageRequestShouldNotBeCreatedIffulfillmentPreferenceIsNotValid(String fulfi @Test void itemCheckOutShouldNotAffectRequestAssociatedWithAnotherItemOfInstance() { UUID instanceId = UUID.randomUUID(); - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); ItemResource firstItem = buildItem(instanceId, "111"); ItemResource secondItem = buildItem(instanceId, "222"); ZonedDateTime requestDate = ZonedDateTime.of(2021, 7, 22, 10, 22, 54, 0, UTC); @@ -3680,7 +3680,7 @@ void itemCheckOutShouldNotAffectRequestAssociatedWithAnotherItemOfInstance() { @Test void itemCheckOutRecallRequestCreationShouldProduceNotice() { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); JsonObject recallToLoaneeConfiguration = new NoticeConfigurationBuilder() .withTemplateId(UUID.randomUUID()) .withEventType(NoticeEventType.ITEM_RECALLED.getRepresentation()) @@ -3723,7 +3723,7 @@ void itemCheckOutRecallRequestCreationShouldProduceNotice() { @Test void itemCheckOutRecallCancelAgainRecallRequestCreationShouldProduceNotice() { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); JsonObject recallToLoaneeConfiguration = new NoticeConfigurationBuilder() .withTemplateId(UUID.randomUUID()) .withEventType(NoticeEventType.ITEM_RECALLED.getRepresentation()) @@ -3767,7 +3767,7 @@ void itemCheckOutRecallCancelAgainRecallRequestCreationShouldProduceNotice() { @Test void shouldTriggerNoticesForTitleLevelRecall() { // Enable the Title Level Request feature - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); // Configure recall notice for the loan owner (borrower) JsonObject recallToLoaneeConfiguration = new NoticeConfigurationBuilder() @@ -3858,7 +3858,7 @@ private void verifyNumberOfNoticeEventsForUser(UUID userId, int expectedNoticeEv @ParameterizedTest @ValueSource(ints = {1, 2, 3, 4, 5}) void titleLevelPageRequestIsCreatedForItemClosestToPickupServicePoint(int testCase) { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); UUID pickupServicePointId = servicePointsFixture.create(new ServicePointBuilder( "Pickup service point", "PICKUP", "Display name") @@ -3960,13 +3960,117 @@ void titleLevelPageRequestIsCreatedForItemClosestToPickupServicePoint(int testCa assertThat(request.getJson().getString("itemId"), is(expectedItemId)); } + @Test + void primaryTlrCreationSkipsClosestServicePointLogicAndPoliciesIgnoredForHoldTlr() { + settingsFixture.configureTlrFeature(true, true, null, null, null); + + policiesActivation.use(new RequestPolicyBuilder( + UUID.randomUUID(), + List.of(PAGE), + "Test request policy", + "Test description", + null + )); + + UUID pickupServicePointId = servicePointsFixture.create(new ServicePointBuilder( + "Pickup service point", "PICKUP", "Display name") + .withPickupLocation(Boolean.TRUE)) + .getId(); + + UUID anotherServicePointId = servicePointsFixture.create(new ServicePointBuilder( + "Another service point", "OTHER", "Display name") + .withPickupLocation(Boolean.TRUE)) + .getId(); + + UUID institutionId = locationsFixture.createInstitution("Institution").getId(); + UUID campusIdA = locationsFixture.createCampus("Campus A", institutionId).getId(); + UUID campusIdB = locationsFixture.createCampus("Campus B", institutionId).getId(); + UUID libraryIdA1 = locationsFixture.createLibrary("Library A1", campusIdA).getId(); + UUID libraryIdB1 = locationsFixture.createLibrary("Library B1", campusIdB).getId(); + + UUID sameLibraryLocationId = locationsFixture.createLocation(new LocationBuilder() + .withName("Location in same library") + .withCode("3") + .forInstitution(institutionId) + .forCampus(campusIdA) + .forLibrary(libraryIdA1) + .withPrimaryServicePoint(anotherServicePointId) + .servedBy(anotherServicePointId)) + .getId(); + + UUID anotherLibraryLocationId = locationsFixture.createLocation(new LocationBuilder() + .withName("Location in another library") + .withCode("3") + .forInstitution(institutionId) + .forCampus(campusIdB) + .forLibrary(libraryIdB1) + .withPrimaryServicePoint(anotherServicePointId) + .servedBy(anotherServicePointId)) + .getId(); + + UUID instanceId = instancesFixture.basedUponDunkirk().getId(); + UUID holdingsId = holdingsFixture.defaultWithHoldings(instanceId).getId(); + + // Closest item + UUID closestItemId = itemsFixture.basedUponDunkirkWithCustomHoldingAndLocation( + holdingsId, sameLibraryLocationId).getId(); + + UUID expectedItemId = itemsFixture.basedUponDunkirkWithCustomHoldingAndLocation( + holdingsId, anotherLibraryLocationId).getId(); + + var requestBuilder = new RequestBuilder() + .page() + .fulfillToHoldShelf() + .titleRequestLevel() + .withInstanceId(instanceId) + .withItemId(expectedItemId) + .withHoldingsRecordId(holdingsId) + .withRequestDate(ZonedDateTime.now()) + .withRequesterId(usersFixture.steve().getId()) + .withPickupServicePointId(pickupServicePointId); + + // Request without ECS phase should fail + requestsFixture.attemptPlace(requestBuilder); + + // The same request with Primary ECS phase should succeed because validation is skipped + IndividualResource request = requestsFixture.place( + requestBuilder.withEcsRequestPhase("Primary")); + + assertThat(request.getJson().getString("itemId"), is(expectedItemId)); + + // To make sure there are no Available items left + requestsFixture.place( + requestBuilder + .withRequesterId(usersFixture.jessica().getId()) + .withItemId(closestItemId) + .withEcsRequestPhase("Primary")); + + // Placing TLR Hold request + var requestBuilderTlrHold = new RequestBuilder() + .hold() + .fulfillToHoldShelf() + .titleRequestLevel() + .withInstanceId(instanceId) + .withNoHoldingsRecordId() + .withNoItemId() + .withRequestDate(ZonedDateTime.now()) + .withRequesterId(usersFixture.steve().getId()) + .withPickupServicePointId(pickupServicePointId); + + // Request without ECS phase should fail + requestsFixture.attemptPlace(requestBuilderTlrHold); + + // The same request with Primary ECS phase should succeed because policy check is skipped + requestsFixture.place(requestBuilderTlrHold.withEcsRequestPhase("Primary")); + } + @Test void pageTlrSucceedsWhenClosestAvailableItemIsNotPageable() { // Page TLR should succeed when multiple available items exist, but the closest item to the // pickup service point is not requestable. At the same time, other available and requestable // items exist. - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); circulationRulesFixture.updateCirculationRules(differentRequestPoliciesBasedOnMaterialType()); UUID pickupServicePointId = servicePointsFixture.create(new ServicePointBuilder( @@ -4065,7 +4169,7 @@ void holdTlrShouldSucceedWhenAvailableItemsExistButTheyAreNotPageable() { // Hold TLR should be created when available items of the same instance exist, but all of them // are not pageable due to the request policy - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); circulationRulesFixture.updateCirculationRules(differentRequestPoliciesBasedOnMaterialType()); IndividualResource instance = instancesFixture.basedUponDunkirk(); @@ -4110,7 +4214,7 @@ void holdAndRecallTlrShouldFailWhenAvailablePageableItemsExist(RequestType type) // Hold TLR should fail when available items of the same instance exist and some of those // items are pageable (request policy allows page requests) - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); circulationRulesFixture.updateCirculationRules(differentRequestPoliciesBasedOnMaterialType()); IndividualResource instance = instancesFixture.basedUponDunkirk(); @@ -4163,7 +4267,7 @@ void holdAndRecallTlrShouldFailWhenAvailablePageableItemsExist(RequestType type) @Test void recallTlrShouldNotBeCreatedForInstanceWithOnlyAgedToLostItem() { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); UUID pickupServicePointId = servicePointsFixture.cd1().getId(); UUID instanceId = UUID.randomUUID(); @@ -4179,7 +4283,7 @@ void recallTlrShouldNotBeCreatedForInstanceWithOnlyAgedToLostItem() { @Test void recallTlrShouldBeCreatedForOnOrderItemIfInstanceHasOnOrderAndDeclaredLostItems() { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); useLostItemPolicy(lostItemFeePoliciesFixture.chargeFee().getId()); UUID pickupServicePointId = servicePointsFixture.cd1().getId(); UUID instanceId = UUID.randomUUID(); @@ -4218,7 +4322,7 @@ void recallTlrShouldBeCreatedForOnOrderItemIfInstanceHasOnOrderAndDeclaredLostIt @Test void recallTlrShouldNotBeCreatedForInstanceWithOnlyDeclaredLostItem() { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); useLostItemPolicy(lostItemFeePoliciesFixture.chargeFee().getId()); UUID instanceId = UUID.randomUUID(); @@ -4239,7 +4343,7 @@ void recallTlrShouldNotBeCreatedForInstanceWithOnlyDeclaredLostItem() { @Test void recallTlrShouldNotBeCreatedForInstanceWithOnlyClaimedReturnedItem() { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); useLostItemPolicy(lostItemFeePoliciesFixture.chargeFee().getId()); UUID instanceId = UUID.randomUUID(); @@ -4260,7 +4364,7 @@ void recallTlrShouldNotBeCreatedForInstanceWithOnlyClaimedReturnedItem() { @Test void recallTlrShouldFailWhenNotAllowedByPolicy() { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); circulationRulesFixture.updateCirculationRules(differentRequestPoliciesBasedOnMaterialType()); IndividualResource instance = instancesFixture.basedUponDunkirk(); @@ -4301,7 +4405,7 @@ void recallTlrShouldFailWhenNotAllowedByPolicy() { @Test void holdTlrShouldSucceedEvenWhenPolicyDoesNotAllowHolds() { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); circulationRulesFixture.updateCirculationRules(differentRequestPoliciesBasedOnMaterialType()); IndividualResource instance = instancesFixture.basedUponDunkirk(); @@ -4343,7 +4447,7 @@ void holdTlrShouldSucceedEvenWhenPolicyDoesNotAllowHolds() { @Test void titleLevelHoldFailsWhenItShouldFollowCirculationRulesAndNoneOfInstanceItemsAreAllowedForHold() { // enable TLR feature and make Hold requests respect circulation rules - configurationsFixture.configureTlrFeature(true, true, null, null, null); + settingsFixture.configureTlrFeature(true, true, null, null, null); IndividualResource book = materialTypesFixture.book(); IndividualResource video = materialTypesFixture.videoRecording(); @@ -4371,7 +4475,7 @@ void titleLevelHoldFailsWhenItShouldFollowCirculationRulesAndNoneOfInstanceItems @Test void titleLevelHoldIsPlacedWhenItShouldFollowCirculationRulesAndOneOfInstanceItemsIsAllowedForHold() { // enable TLR feature and make Hold requests respect circulation rules - configurationsFixture.configureTlrFeature(true, true, null, null, null); + settingsFixture.configureTlrFeature(true, true, null, null, null); IndividualResource book = materialTypesFixture.book(); IndividualResource video = materialTypesFixture.videoRecording(); @@ -4395,7 +4499,7 @@ void titleLevelHoldIsPlacedWhenItShouldFollowCirculationRulesAndOneOfInstanceIte @Test void titleLevelHoldIsPlacedWhenItCanIgnoreCirculationRulesAndNoneOfInstanceItemsAreAllowedForHold() { // enable TLR feature and make Hold requests ignore circulation rules - configurationsFixture.configureTlrFeature(true, false, null, null, null); + settingsFixture.configureTlrFeature(true, false, null, null, null); IndividualResource book = materialTypesFixture.book(); IndividualResource video = materialTypesFixture.videoRecording(); @@ -4456,7 +4560,7 @@ void itemLevelRequestIsNotPlacedWhenRequestPolicyDisallowsRequestedPickupService void titleLevelRequestIsNotPlacedWhenRequestPolicyDisallowsRequestedPickupServicePoint( RequestType requestType) { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); final UUID requestPolicyId = UUID.randomUUID(); policiesActivation.use(new RequestPolicyBuilder( @@ -4752,7 +4856,7 @@ void canCreateHoldTlrForInstanceWithNoHoldingsRecords() { @Test void recallTlrShouldSucceedWhenItNeedsToPickLeastRecalledLoanAndRequestsWithNoLoansAreInTheQueueScenario1() { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); final var patron1 = usersFixture.charlotte(); final var patron2 = usersFixture.jessica(); @@ -4851,7 +4955,7 @@ void recallTlrShouldSucceedWhenItNeedsToPickLeastRecalledLoanAndRequestsWithNoLo @Test void recallTlrShouldSucceedWhenItNeedsToPickLeastRecalledLoanAndRequestsWithNoLoansAreInTheQueueScenario2() { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); final var patron1 = usersFixture.charlotte(); final var patron2 = usersFixture.jessica(); diff --git a/src/test/java/api/requests/RequestsAPILoanHistoryTests.java b/src/test/java/api/requests/RequestsAPILoanHistoryTests.java index 4767c605d4..85c129025a 100644 --- a/src/test/java/api/requests/RequestsAPILoanHistoryTests.java +++ b/src/test/java/api/requests/RequestsAPILoanHistoryTests.java @@ -45,7 +45,7 @@ void creatingRecallRequestChangesTheOpenLoanForTheSameItem() { @Test void checkOutShouldNotTruncateLoanIfRecallRequestExistsForAnotherItemOfTheSameInstanceIfTlrIsEnabled() { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); final var items = itemsFixture.createMultipleItemsForTheSameInstance(2); var steve = usersFixture.steve(); var charlotte = usersFixture.charlotte(); diff --git a/src/test/java/api/requests/RequestsAPILoanRenewalTests.java b/src/test/java/api/requests/RequestsAPILoanRenewalTests.java index 20434ac2be..5b82140520 100644 --- a/src/test/java/api/requests/RequestsAPILoanRenewalTests.java +++ b/src/test/java/api/requests/RequestsAPILoanRenewalTests.java @@ -302,7 +302,7 @@ void forbidRenewalLoanByIdWhenLoanProfileIsFixedFirstRequestInQueueIsHoldAndRene @Test void allowRenewalWhenFirstRequestInQueueIsItemLevelHoldForDifferentItemOfSameInstance() { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); loanPolicyWithRollingProfileAndRenewingIsForbiddenWhenHoldIsPending(); List items = itemsFixture.createMultipleItemsForTheSameInstance(2); ItemResource itemForLoan = items.get(0); @@ -316,7 +316,7 @@ void allowRenewalWhenFirstRequestInQueueIsItemLevelHoldForDifferentItemOfSameIns @Test void allowRenewalWhenFirstRequestInQueueIsTitleLevelHoldForDifferentItemOfSameInstance() { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); loanPolicyWithRollingProfileAndRenewingIsForbiddenWhenHoldIsPending(); List items = itemsFixture.createMultipleItemsForTheSameInstance(2); ItemResource itemForLoan = items.get(0); @@ -334,7 +334,7 @@ void allowRenewalWhenFirstRequestInQueueIsTitleLevelHoldForDifferentItemOfSameIn @Test void forbidRenewalWhenFirstRequestInQueueIsTitleLevelHoldWithoutItemId() { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); loanPolicyWithRollingProfileAndRenewingIsForbiddenWhenHoldIsPending(); ItemResource item = itemsFixture.basedUponNod(); UserResource borrower = usersFixture.charlotte(); @@ -348,7 +348,7 @@ void forbidRenewalWhenFirstRequestInQueueIsTitleLevelHoldWithoutItemId() { @Test void alternateLoanPeriodIsNotUsedWhenFirstRequestInQueueIsItemLevelHoldForDifferentItemOfSameInstance() { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); useRollingPolicyWithRenewingAllowedForHoldingRequest(); // base loan period - 3 weeks, alternate - 4 weeks List items = itemsFixture.createMultipleItemsForTheSameInstance(2); ItemResource itemForLoan = items.get(0); @@ -364,7 +364,7 @@ void alternateLoanPeriodIsNotUsedWhenFirstRequestInQueueIsItemLevelHoldForDiffer @Test void alternateLoanPeriodIsNotUsedForRenewalWhenFirstRequestInQueueIsTitleLevelHoldForDifferentItemOfSameInstance() { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); useRollingPolicyWithRenewingAllowedForHoldingRequest(); List items = itemsFixture.createMultipleItemsForTheSameInstance(2); ItemResource itemForLoan = items.get(0); @@ -384,7 +384,7 @@ void alternateLoanPeriodIsNotUsedForRenewalWhenFirstRequestInQueueIsTitleLevelHo @Test void alternateLoanPeriodIsUsedForRenewalWhenFirstRequestInQueueIsTitleLevelHoldWithoutItemId() { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); useRollingPolicyWithRenewingAllowedForHoldingRequest(); // base loan period - 3 weeks, alternate - 4 weeks ItemResource item = itemsFixture.basedUponNod(); UUID instanceId = item.getInstanceId(); @@ -400,7 +400,7 @@ void alternateLoanPeriodIsUsedForRenewalWhenFirstRequestInQueueIsTitleLevelHoldW @Test void forbidRenewalWhenTitleLevelRecallRequestExistsForSameItem() { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); ItemResource item = itemsFixture.basedUponNod(); UserResource borrower = usersFixture.james(); checkOutFixture.checkOutByBarcode(item, borrower); @@ -418,7 +418,7 @@ void forbidRenewalWhenTitleLevelRecallRequestExistsForSameItem() { @Test void allowRenewalWhenTitleLevelRecallRequestExistsForDifferentItemOfSameInstance() { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); List items = itemsFixture.createMultipleItemsForTheSameInstance(2); ItemResource itemForLoan = items.get(0); ItemResource itemForRequest = items.get(1); @@ -598,7 +598,7 @@ void forbidRenewalOverrideWhenRecallIsForDifferentItemOfSameInstance() { @Test void forbidRenewalOverrideWhenTitleLevelRecallRequestExistsForDifferentItemOfSameInstance() { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); List items = itemsFixture.createMultipleItemsForTheSameInstance(2); ItemResource itemForLoan = items.get(0); ItemResource itemForRequest = items.get(1); diff --git a/src/test/java/api/requests/RequestsAPIRetrievalTests.java b/src/test/java/api/requests/RequestsAPIRetrievalTests.java index 3c71fcc441..bac64efab1 100644 --- a/src/test/java/api/requests/RequestsAPIRetrievalTests.java +++ b/src/test/java/api/requests/RequestsAPIRetrievalTests.java @@ -243,7 +243,7 @@ void canGetARequestById() { @Test void titleLevelRequestRetrievalById() { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); UUID isbnIdentifierId = identifierTypesFixture.isbn().getId(); String isbnValue = "9780866989427"; diff --git a/src/test/java/api/requests/RequestsAPIUpdatingTests.java b/src/test/java/api/requests/RequestsAPIUpdatingTests.java index a0483f1712..ba382cb4f7 100644 --- a/src/test/java/api/requests/RequestsAPIUpdatingTests.java +++ b/src/test/java/api/requests/RequestsAPIUpdatingTests.java @@ -866,7 +866,7 @@ void shouldUseCurrentUserIdAsSourceWhenSendingUpdateRequestMessage() { @Test void editingRecallTlrShouldNotChangeRecalledItem() { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); IndividualResource patron1 = usersFixture.steve(); IndividualResource patron2 = usersFixture.rebecca(); diff --git a/src/test/java/api/requests/StaffSlipsTests.java b/src/test/java/api/requests/StaffSlipsTests.java index d4be9194cc..190143a52d 100644 --- a/src/test/java/api/requests/StaffSlipsTests.java +++ b/src/test/java/api/requests/StaffSlipsTests.java @@ -546,7 +546,7 @@ void responseContainsSlipsWhenServicePointHasManyLocations(SlipsType slipsType) @Test void responseContainsSearchSlipsForTLR() { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); var servicePointId = servicePointsFixture.cd1().getId(); var steve = usersFixture.steve(); var instance = instancesFixture.basedUponDunkirk(); diff --git a/src/test/java/api/requests/scenarios/HoldShelfFulfillmentTests.java b/src/test/java/api/requests/scenarios/HoldShelfFulfillmentTests.java index ede4fbf442..91dceb4c8e 100644 --- a/src/test/java/api/requests/scenarios/HoldShelfFulfillmentTests.java +++ b/src/test/java/api/requests/scenarios/HoldShelfFulfillmentTests.java @@ -24,8 +24,6 @@ import java.time.ZonedDateTime; import java.util.List; import java.util.UUID; -import java.util.stream.Collectors; -import java.util.stream.IntStream; import org.folio.circulation.support.http.client.Response; import org.folio.circulation.support.utils.ClockUtil; @@ -38,14 +36,13 @@ import api.support.APITests; import api.support.TlrFeatureStatus; import api.support.builders.CheckInByBarcodeRequestBuilder; -import api.support.builders.InstanceBuilder; import api.support.http.IndividualResource; import api.support.http.ItemResource; class HoldShelfFulfillmentTests extends APITests { @AfterEach public void afterEach() { - configurationsFixture.deleteTlrFeatureConfig(); + settingsFixture.deleteTlrFeatureSettings(); } @ParameterizedTest @@ -84,7 +81,7 @@ void itemIsReadyForPickUpWhenCheckedInAtPickupServicePoint(TlrFeatureStatus tlrF @ParameterizedTest @ValueSource(ints = {1, 2}) void tlrRequestIsPositionedCorrectlyInUnifiedQueue(int checkedInItemNumber) { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); final IndividualResource pickupServicePoint = servicePointsFixture.cd1(); @@ -222,7 +219,7 @@ void canBeCheckedOutToRequestingPatronWhenReadyForPickup(TlrFeatureStatus tlrFea @Test void canBeCheckedOutToPatronRequestingTitleWhenReadyForPickup() { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); final IndividualResource pickupServicePoint = servicePointsFixture.cd1(); @@ -288,7 +285,7 @@ void checkInAtDifferentServicePointPlacesItemInTransit(TlrFeatureStatus tlrFeatu @Test void checkInItemWithTlrRequestAtDifferentServicePointPlacesItemInTransit() { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); final IndividualResource pickupServicePoint = servicePointsFixture.cd1(); final IndividualResource checkInServicePoint = servicePointsFixture.cd2(); @@ -363,7 +360,7 @@ void canBeCheckedOutToRequestingPatronWhenInTransit(TlrFeatureStatus tlrFeatureS @Test void canCheckoutItemForTitleLevelRequestWhenInTransit() { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); final IndividualResource pickupServicePoint = servicePointsFixture.cd1(); final IndividualResource checkInServicePoint = servicePointsFixture.cd2(); @@ -430,7 +427,7 @@ void itemIsReadyForPickUpWhenCheckedInAtPickupServicePointAfterTransit(TlrFeatur @Test void itemWithTlrRequestIsReadyForPickUpWhenCheckedInAtPickupServicePointAfterTransit() { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); final IndividualResource pickupServicePoint = servicePointsFixture.cd1(); final IndividualResource checkInServicePoint = servicePointsFixture.cd2(); @@ -508,7 +505,7 @@ void cannotCheckOutToOtherPatronWhenRequestIsAwaitingPickup(TlrFeatureStatus tlr @Test void cannotCheckOutToOtherPatronWhenTlrRequestIsAwaitingPickup() { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); ItemResource smallAngryPlanet = itemsFixture.basedUponSmallAngryPlanet(); IndividualResource james = usersFixture.james(); @@ -580,7 +577,7 @@ void cannotCheckOutToOtherPatronWhenRequestIsInTransitForPickup(TlrFeatureStatus @Test void cannotCheckOutToOtherPatronWhenTlrRequestIsInTransitForPickup() { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); final IndividualResource requestServicePoint = servicePointsFixture.cd1(); final IndividualResource checkInServicePoint = servicePointsFixture.cd2(); diff --git a/src/test/java/api/requests/scenarios/LoanDueDatesAfterRecallTests.java b/src/test/java/api/requests/scenarios/LoanDueDatesAfterRecallTests.java index e8e3022147..8ab2ea5b64 100644 --- a/src/test/java/api/requests/scenarios/LoanDueDatesAfterRecallTests.java +++ b/src/test/java/api/requests/scenarios/LoanDueDatesAfterRecallTests.java @@ -285,7 +285,7 @@ void recallRequestWithMGDAndRDValuesChangesDueDateToMGDWithCLDDM() { setFallbackPolicies(canCirculateRollingPolicy); - servicePointsFixture.create(new ServicePointBuilder(checkOutServicePointId, "CLDDM Desk", "clddm", "CLDDM Desk Test", null, null, TRUE, null, null)); + servicePointsFixture.create(new ServicePointBuilder(checkOutServicePointId, "CLDDM Desk", "clddm", "CLDDM Desk Test", null, null, TRUE, null, null, null)); // We use the loan date to calculate the minimum guaranteed due date (MGD) final ZonedDateTime loanDate = diff --git a/src/test/java/api/requests/scenarios/MoveRequestTests.java b/src/test/java/api/requests/scenarios/MoveRequestTests.java index 7a53868f15..2f9a30856c 100644 --- a/src/test/java/api/requests/scenarios/MoveRequestTests.java +++ b/src/test/java/api/requests/scenarios/MoveRequestTests.java @@ -184,7 +184,7 @@ void canMoveRequestFromOneItemCopyToAnother() { @Test void itemShouldRemainPagedIfHoldCreatedAfterRequestHasBeenMovedToAnotherItem() { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); val items = itemsFixture.createMultipleItemsForTheSameInstance(2); val firstItem = items.get(0); @@ -208,7 +208,7 @@ void itemShouldRemainPagedIfHoldCreatedAfterRequestHasBeenMovedToAnotherItem() { @Test void canMovePageTlrToAvailableItem() { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); val firstItem = itemsFixture.basedUponSmallAngryPlanet("89809"); val pageIlrForFirstItem = requestsFixture.placeTitleLevelPageRequest(firstItem.getInstanceId(), @@ -227,7 +227,7 @@ void canMovePageTlrToAvailableItem() { @Test void canMovePageTlrToRecall() { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); val firstItem = itemsFixture.basedUponSmallAngryPlanet("89809"); val pageTlrForFirstItem = requestsFixture.placeTitleLevelPageRequest(firstItem.getInstanceId(), usersFixture.james()); @@ -249,7 +249,7 @@ void canMovePageTlrToRecall() { @Test void canMoveRecallTlrToAnotherItem() { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); val items = itemsFixture.createMultipleItemsForTheSameInstance(2); val firstItem = items.get(0); @@ -269,7 +269,7 @@ void canMoveRecallTlrToAnotherItem() { @Test void canMoveRecallTlrToPage() { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); val items = itemsFixture.createMultipleItemsForTheSameInstance(2); val firstItem = items.get(0); @@ -289,7 +289,7 @@ void canMoveRecallTlrToPage() { @Test void whenRequestIsMovedItemShouldBecomeAvailableIfThereAreNoRequestsInTheQueueForThisItemIfTlrIsEnabled() { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); val items = itemsFixture.createMultipleItemsForTheSameInstance(2); val firstItem = items.get(0); @@ -308,7 +308,7 @@ void whenRequestIsMovedItemShouldBecomeAvailableIfThereAreNoRequestsInTheQueueFo @Test void whenRequestIsMovedPositionsShouldBeConsistentWhenTlrIsEnabled() { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); val items = itemsFixture.createMultipleItemsForTheSameInstance(3); @@ -369,7 +369,7 @@ void whenRequestIsMovedPositionsShouldBeConsistentWhenTlrIsEnabled() { @Test void cannotMoveRequestToAnItemFromDifferentInstance() { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); val nod = itemsFixture.basedUponNod(); val uprooted = itemsFixture.basedUponUprooted(); @@ -387,7 +387,7 @@ void cannotMoveRequestToAnItemFromDifferentInstance() { @Test void cannotMoveToOrFromHoldTlr() { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); val items = itemsFixture.createMultipleItemsForTheSameInstance(2); val firstItem = items.get(0); @@ -416,7 +416,7 @@ void cannotMoveToOrFromHoldTlr() { @Test void cannotMoveTlrToTheSameItem() { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); val item = itemsFixture.basedUponNod(); val jessica = usersFixture.jessica(); @@ -435,7 +435,7 @@ void cannotMoveTlrToTheSameItem() { @Test void cannotMoveTlrWhenFeatureIsDisabled() { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); val items = itemsFixture.createMultipleItemsForTheSameInstance(2); val firstItem = items.get(0); @@ -446,7 +446,7 @@ void cannotMoveTlrWhenFeatureIsDisabled() { val firstItemHoldTlr = requestsFixture.placeTitleLevelHoldShelfRequest(firstItem.getInstanceId(), usersFixture.james()); - configurationsFixture.disableTlrFeature(); + settingsFixture.disableTlrFeature(); Response response = requestsFixture.attemptMove(new MoveRequestBuilder(firstItemHoldTlr.getId(), secondItem.getId())); diff --git a/src/test/java/api/support/APITestContext.java b/src/test/java/api/support/APITestContext.java index 167f11e91e..c007163b71 100644 --- a/src/test/java/api/support/APITestContext.java +++ b/src/test/java/api/support/APITestContext.java @@ -9,6 +9,7 @@ import java.net.URL; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; +import java.util.Optional; import java.util.Properties; import java.util.Random; import java.util.concurrent.CompletableFuture; @@ -40,6 +41,7 @@ public class APITestContext { private static final Logger log = LogManager.getLogger(MethodHandles.lookup().lookupClass()); public static final String TENANT_ID = "test_tenant"; + public static String tempTenantId; private static String USER_ID = "79ff2a8b-d9c3-5b39-ad4a-0a84025ab085"; private static final String TOKEN = "eyJhbGciOiJIUzUxMiJ9eyJzdWIiOiJhZG1pbiIsInVzZXJfaWQiOiI3OWZmMmE4Yi1kOWMzLTViMzktYWQ0YS0wYTg0MDI1YWIwODUiLCJ0ZW5hbnQiOiJ0ZXN0X3RlbmFudCJ9BShwfHcNClt5ZXJ8ImQTMQtAM1sQEnhsfWNmXGsYVDpuaDN3RVQ9"; @@ -58,7 +60,7 @@ public class APITestContext { private static final int PORT = nextFreePort(); private static String fakeOkapiDeploymentId; - private static Boolean useOkapiForStorage; + private static Boolean useOkapiForStorage = false; private static Boolean useOkapiForInitialRequests; static String getToken() { @@ -66,7 +68,15 @@ static String getToken() { } public static String getTenantId() { - return TENANT_ID; + return Optional.ofNullable(tempTenantId).orElse(TENANT_ID); + } + + public static void setTempTenantId(String tenantId) { + tempTenantId = tenantId; + } + + public static void clearTempTenantId() { + setTempTenantId(null); } public static String getUserId() { diff --git a/src/test/java/api/support/APITests.java b/src/test/java/api/support/APITests.java index 6935d04e64..22d039de7d 100644 --- a/src/test/java/api/support/APITests.java +++ b/src/test/java/api/support/APITests.java @@ -24,6 +24,17 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; +import api.support.fixtures.SearchInstanceFixture; +import org.junit.Assert; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.testcontainers.containers.KafkaContainer; +import org.testcontainers.utility.DockerImageName; + +import api.support.fakes.FakeModNotify; +import api.support.fakes.FakePubSub; +import api.support.fakes.FakeStorageModule; import api.support.fixtures.AddInfoFixture; import api.support.fixtures.AddressTypesFixture; import api.support.fixtures.AgeToLostFixture; @@ -36,7 +47,6 @@ import api.support.fixtures.CirculationItemsFixture; import api.support.fixtures.CirculationRulesFixture; import api.support.fixtures.ClaimItemReturnedFixture; -import api.support.fixtures.ConfigurationsFixture; import api.support.fixtures.DeclareLostFixtures; import api.support.fixtures.DepartmentFixture; import api.support.fixtures.EndPatronSessionClient; @@ -70,16 +80,6 @@ import api.support.fixtures.TenantActivationFixture; import api.support.fixtures.UserManualBlocksFixture; import api.support.fixtures.UsersFixture; -import org.junit.Assert; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.testcontainers.containers.KafkaContainer; -import org.testcontainers.utility.DockerImageName; - -import api.support.fakes.FakeModNotify; -import api.support.fakes.FakePubSub; -import api.support.fakes.FakeStorageModule; import api.support.fixtures.policies.PoliciesActivationFixture; import api.support.http.IndividualResource; import api.support.http.ResourceClient; @@ -238,9 +238,6 @@ public abstract class APITests { protected final AddressTypesFixture addressTypesFixture = new AddressTypesFixture(ResourceClient.forAddressTypes()); - protected final ConfigurationsFixture configurationsFixture = - new ConfigurationsFixture(configClient); - protected final PatronGroupsFixture patronGroupsFixture = new PatronGroupsFixture(patronGroupsClient); @@ -306,6 +303,7 @@ public abstract class APITests { protected final DepartmentFixture departmentFixture = new DepartmentFixture(); protected final CheckOutLockFixture checkOutLockFixture = new CheckOutLockFixture(); protected final SettingsFixture settingsFixture = new SettingsFixture(); + protected final SearchInstanceFixture searchFixture = new SearchInstanceFixture(); protected APITests() { this(true, false); @@ -452,13 +450,13 @@ protected void mockClockManagerToReturnDefaultDateTime() { protected void reconfigureTlrFeature(TlrFeatureStatus tlrFeatureStatus) { if (tlrFeatureStatus == TlrFeatureStatus.ENABLED) { - configurationsFixture.enableTlrFeature(); + settingsFixture.enableTlrFeature(); } else if (tlrFeatureStatus == TlrFeatureStatus.DISABLED) { - configurationsFixture.disableTlrFeature(); + settingsFixture.disableTlrFeature(); } else { - configurationsFixture.deleteTlrFeatureConfig(); + settingsFixture.deleteTlrFeatureSettings(); } } @@ -474,15 +472,15 @@ protected void reconfigureTlrFeature(TlrFeatureStatus tlrFeatureStatus, UUID cancellationTemplateId, UUID expirationTemplateId) { if (tlrFeatureStatus == TlrFeatureStatus.ENABLED) { - configurationsFixture.configureTlrFeature(true, tlrHoldShouldFollowCirculationRules, + settingsFixture.configureTlrFeature(true, tlrHoldShouldFollowCirculationRules, confirmationTemplateId, cancellationTemplateId, expirationTemplateId); } else if (tlrFeatureStatus == TlrFeatureStatus.DISABLED) { - configurationsFixture.configureTlrFeature(false, tlrHoldShouldFollowCirculationRules, + settingsFixture.configureTlrFeature(false, tlrHoldShouldFollowCirculationRules, confirmationTemplateId, cancellationTemplateId, expirationTemplateId); } else { - configurationsFixture.deleteTlrFeatureConfig(); + settingsFixture.deleteTlrFeatureSettings(); } } diff --git a/src/test/java/api/support/RestAssuredConfiguration.java b/src/test/java/api/support/RestAssuredConfiguration.java index bae905f073..e14bf8db7f 100644 --- a/src/test/java/api/support/RestAssuredConfiguration.java +++ b/src/test/java/api/support/RestAssuredConfiguration.java @@ -27,7 +27,7 @@ public static RequestSpecification standardHeaders(OkapiHeaders okapiHeaders) { final HashMap headers = new HashMap<>(); headers.put(OKAPI_URL, okapiHeaders.getUrl().toString()); - headers.put(TENANT, okapiHeaders.getTenantId()); + headers.put(TENANT, APITestContext.getTenantId()); headers.put(TOKEN, okapiHeaders.getToken()); headers.put(REQUEST_ID, okapiHeaders.getRequestId()); headers.put(OKAPI_PERMISSIONS, okapiHeaders.getOkapiPermissions()); diff --git a/src/test/java/api/support/builders/InstanceBuilder.java b/src/test/java/api/support/builders/InstanceBuilder.java index 2d9c80ca73..b849c16a34 100644 --- a/src/test/java/api/support/builders/InstanceBuilder.java +++ b/src/test/java/api/support/builders/InstanceBuilder.java @@ -27,7 +27,7 @@ public InstanceBuilder(String title, UUID instanceTypeId) { this(UUID.randomUUID(), title, instanceTypeId); } - private InstanceBuilder(UUID id, String title, UUID instanceTypeId) { + public InstanceBuilder(UUID id, String title, UUID instanceTypeId) { this(id, title, new JsonArray(), instanceTypeId, Collections.emptyList(), new JsonArray(), new JsonArray()); } diff --git a/src/test/java/api/support/builders/RequestBuilder.java b/src/test/java/api/support/builders/RequestBuilder.java index c9eea6fa95..288249e756 100644 --- a/src/test/java/api/support/builders/RequestBuilder.java +++ b/src/test/java/api/support/builders/RequestBuilder.java @@ -64,6 +64,7 @@ public class RequestBuilder extends JsonBuilder implements Builder { private final Tags tags; private final String patronComments; private final BlockOverrides blockOverrides; + private final String ecsRequestPhase; private final String itemLocationCode; private final PrintDetails printDetails; @@ -94,6 +95,7 @@ public RequestBuilder() { null, null, null, + null, null); } @@ -126,6 +128,7 @@ public static RequestBuilder from(IndividualResource response) { new Tags((toStream(representation.getJsonObject("tags"), "tagList").collect(toList()))), getProperty(representation, "patronComments"), null, + getProperty(representation, "ecsRequestPhase"), getProperty(representation, ITEM_LOCATION_CODE), PrintDetails.fromRepresentation(representation) ); @@ -199,6 +202,10 @@ public JsonObject create() { } } + if (ecsRequestPhase != null) { + put(request, "ecsRequestPhase", ecsRequestPhase); + } + if (printDetails != null) { put(request, "printDetails", printDetails.toJsonObject()); } diff --git a/src/test/java/api/support/builders/SearchInstanceBuilder.java b/src/test/java/api/support/builders/SearchInstanceBuilder.java new file mode 100644 index 0000000000..92e67bdeb3 --- /dev/null +++ b/src/test/java/api/support/builders/SearchInstanceBuilder.java @@ -0,0 +1,24 @@ +package api.support.builders; + +import java.util.List; + +import io.vertx.core.json.JsonObject; + +public class SearchInstanceBuilder extends JsonBuilder implements Builder { + + private final JsonObject searchInstance; + + public SearchInstanceBuilder(JsonObject searchInstance) { + this.searchInstance = searchInstance; + } + + @Override + public JsonObject create() { + return searchInstance; + } + + public SearchInstanceBuilder withItems(List items) { + put(searchInstance, "items", items); + return new SearchInstanceBuilder(searchInstance); + } +} diff --git a/src/test/java/api/support/builders/ServicePointBuilder.java b/src/test/java/api/support/builders/ServicePointBuilder.java index e2fb90ec3f..480f354a67 100644 --- a/src/test/java/api/support/builders/ServicePointBuilder.java +++ b/src/test/java/api/support/builders/ServicePointBuilder.java @@ -18,6 +18,7 @@ public class ServicePointBuilder extends JsonBuilder implements Builder { private final JsonObject holdShelfExpiryPeriod; private final String holdShelfClosedLibraryDateManagement; + private final Boolean ecsRequestRouting; public ServicePointBuilder( UUID id, @@ -28,7 +29,8 @@ public ServicePointBuilder( Integer shelvingLagTime, Boolean pickupLocation, JsonObject holdShelfExpiryPeriod, - String holdShelfClosedLibraryDateManagement) { + String holdShelfClosedLibraryDateManagement, + Boolean ecsRequestRouting) { this.id = id; this.name = name; this.code = code; @@ -38,6 +40,7 @@ public ServicePointBuilder( this.pickupLocation = pickupLocation; this.holdShelfExpiryPeriod = holdShelfExpiryPeriod; this.holdShelfClosedLibraryDateManagement = holdShelfClosedLibraryDateManagement; + this.ecsRequestRouting = ecsRequestRouting; } public ServicePointBuilder(String name, String code, String discoveryDisplayName) { @@ -50,6 +53,7 @@ public ServicePointBuilder(String name, String code, String discoveryDisplayName null, false, null, + null, null); } @@ -64,8 +68,9 @@ public static ServicePointBuilder from(IndividualResource response) { getIntegerProperty(representation, "shelvingLagTime", null), getBooleanProperty(representation, "pickupLocation"), getObjectProperty(representation, "holdShelfExpiryPeriod"), - getProperty(representation, "holdShelfClosedLibraryDateManagement") - ); + getProperty(representation, "holdShelfClosedLibraryDateManagement"), + getBooleanProperty(representation, "ecsRequestRouting") + ); } @Override @@ -80,6 +85,7 @@ public JsonObject create() { put(servicePoint, "pickupLocation", this.pickupLocation); put(servicePoint, "holdShelfExpiryPeriod", this.holdShelfExpiryPeriod); put(servicePoint, "holdShelfClosedLibraryDateManagement", this.holdShelfClosedLibraryDateManagement); + put(servicePoint, "ecsRequestRouting", this.ecsRequestRouting); return servicePoint; } @@ -94,7 +100,8 @@ public ServicePointBuilder withId(UUID newId) { this.shelvingLagTime, this.pickupLocation, this.holdShelfExpiryPeriod, - this.holdShelfClosedLibraryDateManagement); + this.holdShelfClosedLibraryDateManagement, + this.ecsRequestRouting); } public ServicePointBuilder withName(String newName) { @@ -107,7 +114,8 @@ public ServicePointBuilder withName(String newName) { this.shelvingLagTime, this.pickupLocation, this.holdShelfExpiryPeriod, - this.holdShelfClosedLibraryDateManagement); + this.holdShelfClosedLibraryDateManagement, + this.ecsRequestRouting); } public ServicePointBuilder withCode(String newCode) { @@ -120,7 +128,8 @@ public ServicePointBuilder withCode(String newCode) { this.shelvingLagTime, this.pickupLocation, this.holdShelfExpiryPeriod, - this.holdShelfClosedLibraryDateManagement); + this.holdShelfClosedLibraryDateManagement, + this.ecsRequestRouting); } public ServicePointBuilder withDiscoveryDisplayName(String newDiscoveryDisplayName) { @@ -133,7 +142,8 @@ public ServicePointBuilder withDiscoveryDisplayName(String newDiscoveryDisplayNa this.shelvingLagTime, this.pickupLocation, this.holdShelfExpiryPeriod, - this.holdShelfClosedLibraryDateManagement); + this.holdShelfClosedLibraryDateManagement, + this.ecsRequestRouting); } public ServicePointBuilder withDescription(String newDescription) { @@ -146,7 +156,8 @@ public ServicePointBuilder withDescription(String newDescription) { this.shelvingLagTime, this.pickupLocation, this.holdShelfExpiryPeriod, - this.holdShelfClosedLibraryDateManagement); + this.holdShelfClosedLibraryDateManagement, + this.ecsRequestRouting); } public ServicePointBuilder withShelvingLagTime(Integer newShelvingLagTime) { @@ -159,7 +170,8 @@ public ServicePointBuilder withShelvingLagTime(Integer newShelvingLagTime) { newShelvingLagTime, this.pickupLocation, this.holdShelfExpiryPeriod, - this.holdShelfClosedLibraryDateManagement); + this.holdShelfClosedLibraryDateManagement, + this.ecsRequestRouting); } public ServicePointBuilder withPickupLocation(Boolean newPickupLocation) { @@ -172,7 +184,8 @@ public ServicePointBuilder withPickupLocation(Boolean newPickupLocation) { this.shelvingLagTime, newPickupLocation, this.holdShelfExpiryPeriod, - this.holdShelfClosedLibraryDateManagement); + this.holdShelfClosedLibraryDateManagement, + this.ecsRequestRouting); } public ServicePointBuilder withHoldShelfExpriyPeriod(int duration, String intervalId) { @@ -190,7 +203,8 @@ public ServicePointBuilder withHoldShelfExpriyPeriod(int duration, String interv this.shelvingLagTime, this.pickupLocation, holdShelfExpiryPeriod, - this.holdShelfClosedLibraryDateManagement); + this.holdShelfClosedLibraryDateManagement, + this.ecsRequestRouting); } public ServicePointBuilder withholdShelfClosedLibraryDateManagement(String expirationDateManagement) { @@ -203,6 +217,21 @@ public ServicePointBuilder withholdShelfClosedLibraryDateManagement(String expir this.shelvingLagTime, this.pickupLocation, this.holdShelfExpiryPeriod, - expirationDateManagement); + expirationDateManagement, + this.ecsRequestRouting); + } + + public ServicePointBuilder withEcsRequestRouting(Boolean ecsRequestRouting) { + return new ServicePointBuilder( + this.id, + this.name, + this.code, + this.discoveryDisplayName, + this.description, + this.shelvingLagTime, + this.pickupLocation, + this.holdShelfExpiryPeriod, + this.holdShelfClosedLibraryDateManagement, + ecsRequestRouting); } } diff --git a/src/test/java/api/support/builders/SettingsBuilder.java b/src/test/java/api/support/builders/SettingsBuilder.java index dd954c07aa..380b52fef2 100644 --- a/src/test/java/api/support/builders/SettingsBuilder.java +++ b/src/test/java/api/support/builders/SettingsBuilder.java @@ -8,7 +8,7 @@ public class SettingsBuilder implements Builder { private final JsonObject representation; - public SettingsBuilder(UUID id, String scope, String key, String value) { + public SettingsBuilder(UUID id, String scope, String key, Object value) { this.representation = new JsonObject() .put("id", id) .put("scope", scope) diff --git a/src/test/java/api/support/fakes/FakeCQLToJSONInterpreter.java b/src/test/java/api/support/fakes/FakeCQLToJSONInterpreter.java index 5f9808ea09..8abd49675e 100644 --- a/src/test/java/api/support/fakes/FakeCQLToJSONInterpreter.java +++ b/src/test/java/api/support/fakes/FakeCQLToJSONInterpreter.java @@ -14,13 +14,34 @@ import org.apache.commons.lang3.tuple.ImmutablePair; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.folio.circulation.support.http.server.WebContext; import io.vertx.core.json.JsonObject; public class FakeCQLToJSONInterpreter { private static final Logger log = LogManager.getLogger(MethodHandles.lookup().lookupClass()); + public List execute(Collection records, String query, + WebContext context) { + + var initiallyFilteredRecords = execute(records, query); + + // Routing SP filtering + String includeRoutingServicePointsParam = context.getStringParameter( + "includeRoutingServicePoints"); + if (Boolean.parseBoolean(includeRoutingServicePointsParam)) { + return records.stream() + .filter(json -> json.containsKey("ecsRequestRouting") + ? json.getBoolean("ecsRequestRouting") + : false) + .toList(); + } + + return initiallyFilteredRecords; + } + public List execute(Collection records, String query) { + final var queryAndSort = splitQueryAndSort(query); final var cqlPredicate = new CqlPredicate(queryAndSort.left); diff --git a/src/test/java/api/support/fakes/FakeOkapi.java b/src/test/java/api/support/fakes/FakeOkapi.java index 1ed5b8f5b0..5a6756dabd 100644 --- a/src/test/java/api/support/fakes/FakeOkapi.java +++ b/src/test/java/api/support/fakes/FakeOkapi.java @@ -23,11 +23,11 @@ import java.util.Objects; import org.apache.commons.lang3.exception.ExceptionUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.folio.circulation.support.ValidationErrorFailure; import org.folio.circulation.support.http.client.OkapiHttpClient; import org.folio.circulation.support.results.Result; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; import io.vertx.core.AbstractVerticle; import io.vertx.core.Promise; @@ -231,6 +231,12 @@ public void start(Promise startFuture) throws IOException { FakeCalendarOkapi.registerCalendarSurroundingDates(router); registerFakeStorageLoansAnonymize(router); + new FakeSearchModule().register(router); + new FakeStorageModuleBuilder() + .withRecordName(FakeSearchModule.RECORD_TYPE_NAME) + .withRootPath("/search/instances") + .create().register(router); + new FakeStorageModuleBuilder() .withRecordName("institution") .withRootPath("/location-units/institutions") @@ -276,6 +282,7 @@ public void start(Promise startFuture) throws IOException { .withRootPath("/service-points") .withRequiredProperties("name", "code", "discoveryDisplayName") .withUniqueProperties("name") + .withQueryParameters("includeRoutingServicePoints") .withChangeMetadata() .disallowCollectionDelete() .create() @@ -406,7 +413,6 @@ public void start(Promise startFuture) throws IOException { .withRootPath("/settings/entries") .withCollectionPropertyName("items") .withChangeMetadata() - .withRecordConstraint(this::userHasAlreadyAcquiredLock) .create().register(router); new FakeStorageModuleBuilder() diff --git a/src/test/java/api/support/fakes/FakeSearchModule.java b/src/test/java/api/support/fakes/FakeSearchModule.java new file mode 100644 index 0000000000..e4c4eafd45 --- /dev/null +++ b/src/test/java/api/support/fakes/FakeSearchModule.java @@ -0,0 +1,103 @@ +package api.support.fakes; + +import static java.lang.String.format; +import static org.folio.circulation.support.results.CommonFailures.failedDueToServerError; + +import java.lang.invoke.MethodHandles; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.folio.circulation.support.http.server.ClientErrorResponse; +import org.folio.circulation.support.http.server.WebContext; +import org.folio.circulation.support.results.Result; + +import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.HttpServerResponse; +import io.vertx.core.json.Json; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.RoutingContext; +import lombok.SneakyThrows; + +public class FakeSearchModule { + + private static final Logger log = LogManager.getLogger(MethodHandles.lookup().lookupClass()); + public static final String RECORD_TYPE_NAME = "search-instance"; + private static final String ID_REGEXP = "id\\s*(==|!=|>|>=|<|<=|\\|=|\\|=)\\s*" + + "([a-f0-9\\-]+)"; + private final Storage storage; + private final Pattern idPattern; + private static final String ROOT_PATH = "/search/instances"; + + public FakeSearchModule() { + this.idPattern = Pattern.compile(ID_REGEXP); + this.storage = Storage.getStorage(); + } + + @SneakyThrows + public void register(Router router) { + router.get("/search/instances").handler(this::getById); + } + + private void getById(RoutingContext routingContext) { + WebContext context = new WebContext(routingContext); + + Result idParsingResult = getIdParameter(routingContext); + + if (idParsingResult.failed()) { + idParsingResult.cause().writeTo(routingContext.response()); + return; + } + + Map resourcesForTenant = getResourcesForTenant(context); + + final String id = idParsingResult.value().toString(); + + if (resourcesForTenant.containsKey(id)) { + final JsonObject resourceRepresentation = resourcesForTenant.get(id); + + final JsonObject searchResult = new JsonObject(); + + searchResult.put("totalRecords", 1); + searchResult.put("instances", List.of(resourceRepresentation)); + + log.debug("Found {} resource: {}", RECORD_TYPE_NAME, + searchResult.encodePrettily()); + + HttpServerResponse response = routingContext.response(); + Buffer buffer = Buffer.buffer(Json.encodePrettily(searchResult), "UTF-8"); + + response.setStatusCode(200); + response.putHeader("content-type", "application/json; charset=utf-8"); + response.putHeader("content-length", Integer.toString(buffer.length())); + + response.write(buffer); + response.end(); + } + else { + log.debug("Failed to find {} resource: {}", RECORD_TYPE_NAME, + idParsingResult); + + ClientErrorResponse.notFound(routingContext.response()); + } + } + + private Result getIdParameter(RoutingContext routingContext) { + final String query = routingContext.request().getParam("query"); + Matcher matcher = idPattern.matcher(query); + String id = matcher.find() ? matcher.group(2) : null; + + return Result.of(() -> UUID.fromString(id)) + .mapFailure(r -> failedDueToServerError(format( + "ID parameter \"%s\" is not a valid UUID", id))); + } + + private Map getResourcesForTenant(WebContext context) { + return storage.getTenantResources(ROOT_PATH, context.getTenantId()); + } +} diff --git a/src/test/java/api/support/fakes/FakeStorageModule.java b/src/test/java/api/support/fakes/FakeStorageModule.java index 2224d6b95a..882be300aa 100644 --- a/src/test/java/api/support/fakes/FakeStorageModule.java +++ b/src/test/java/api/support/fakes/FakeStorageModule.java @@ -320,7 +320,7 @@ private void getById(RoutingContext routingContext) { Result idParsingResult = getIdParameter(routingContext); - if(idParsingResult.failed()) { + if (idParsingResult.failed()) { idParsingResult.cause().writeTo(routingContext.response()); return; } @@ -329,7 +329,7 @@ private void getById(RoutingContext routingContext) { final String id = idParsingResult.value().toString(); - if(resourcesForTenant.containsKey(id)) { + if (resourcesForTenant.containsKey(id)) { final JsonObject resourceRepresentation = resourcesForTenant.get(id); log.debug("Found {} resource: {}", recordTypeName, @@ -378,8 +378,8 @@ private void getMany(RoutingContext routingContext) { Map resourcesForTenant = getResourcesForTenant(context); - List filteredItems = new FakeCQLToJSONInterpreter() - .execute(resourcesForTenant.values(), query); + List filteredItems = getFakeCQLToJSONInterpreter() + .execute(resourcesForTenant.values(), query, context); List pagedItems = filteredItems.stream() .skip(offset) @@ -410,6 +410,10 @@ private void getMany(RoutingContext routingContext) { response.end(); } + FakeCQLToJSONInterpreter getFakeCQLToJSONInterpreter() { + return new FakeCQLToJSONInterpreter(); + } + private void empty(RoutingContext routingContext) { WebContext context = new WebContext(routingContext); @@ -432,7 +436,7 @@ private void deleteMany(RoutingContext routingContext) { Map resourcesForTenant = getResourcesForTenant(context); - new FakeCQLToJSONInterpreter() + getFakeCQLToJSONInterpreter() .execute(resourcesForTenant.values(), query) .forEach(item -> resourcesForTenant.remove(item.getString("id"))); @@ -446,7 +450,7 @@ private void delete(RoutingContext routingContext) { Map resourcesForTenant = getResourcesForTenant(context); - if(resourcesForTenant.containsKey(id)) { + if (resourcesForTenant.containsKey(id)) { resourcesForTenant.remove(id); noContent().writeTo(routingContext.response()); @@ -633,7 +637,8 @@ private void checkForUnexpectedQueryParameters(RoutingContext routingContext) { boolean isValidParameter = queryParameter.contains("query") || queryParameter.contains("offset") || isContainsQueryParameter(queryParameter) || - queryParameter.contains("limit"); + queryParameter.contains("limit") || + queryParameter.contains("expandAll"); return !isValidParameter; }) diff --git a/src/test/java/api/support/fixtures/CirculationItemsFixture.java b/src/test/java/api/support/fixtures/CirculationItemsFixture.java index 2444d9d89e..74c248a9c4 100644 --- a/src/test/java/api/support/fixtures/CirculationItemsFixture.java +++ b/src/test/java/api/support/fixtures/CirculationItemsFixture.java @@ -21,9 +21,18 @@ public CirculationItemsFixture( } public IndividualResource createCirculationItem(String barcode, UUID holdingId, UUID locationId, String instanceTitle) { - CirculationItemsBuilder circulationItemsBuilder = new CirculationItemsBuilder().withBarcode(barcode).withHoldingId(holdingId) - .withLoanType(loanTypesFixture.canCirculate().getId()).withMaterialType(materialTypesFixture.book().getId()) - .withLocationId(locationId).withInstanceTitle(instanceTitle); + return createCirculationItem(UUID.randomUUID(), barcode, holdingId, locationId, instanceTitle); + } + + public IndividualResource createCirculationItem(UUID itemId, String barcode, UUID holdingId, UUID locationId, String instanceTitle) { + CirculationItemsBuilder circulationItemsBuilder = new CirculationItemsBuilder() + .withItemId(itemId) + .withBarcode(barcode) + .withHoldingId(holdingId) + .withLoanType(loanTypesFixture.canCirculate().getId()) + .withMaterialType(materialTypesFixture.book().getId()) + .withLocationId(locationId) + .withInstanceTitle(instanceTitle); return circulationItemClient.create(circulationItemsBuilder); } diff --git a/src/test/java/api/support/fixtures/ConfigurationExample.java b/src/test/java/api/support/fixtures/ConfigurationExample.java index 5f7b9c7011..e06ac904c9 100644 --- a/src/test/java/api/support/fixtures/ConfigurationExample.java +++ b/src/test/java/api/support/fixtures/ConfigurationExample.java @@ -2,10 +2,7 @@ import static org.folio.circulation.support.json.JsonPropertyWriter.write; -import java.util.UUID; - import api.support.builders.ConfigRecordBuilder; -import api.support.builders.TlrSettingsConfigurationBuilder; import io.vertx.core.json.JsonObject; public class ConfigurationExample { @@ -40,37 +37,6 @@ public static ConfigRecordBuilder schedulerNoticesLimitConfiguration(String limi DEFAULT_NOTIFICATION_SCHEDULER_CONFIG_NAME, limit); } - public static ConfigRecordBuilder tlrFeatureEnabled() { - return new ConfigRecordBuilder("SETTINGS", "TLR", - new TlrSettingsConfigurationBuilder() - .withTitleLevelRequestsFeatureEnabled(true) - .create() - .encodePrettily()); - } - - public static ConfigRecordBuilder tlrFeatureDisabled() { - return new ConfigRecordBuilder("SETTINGS", "TLR", - new TlrSettingsConfigurationBuilder() - .withTitleLevelRequestsFeatureEnabled(false) - .create() - .encodePrettily()); - } - - public static ConfigRecordBuilder tlrFeatureConfiguration(boolean isTlrEnabled, - boolean holdShouldFollowCirculationRules, UUID confirmationTemplateId, - UUID cancellationTemplateId, UUID expirationTemplateId) { - - return new ConfigRecordBuilder("SETTINGS", "TLR", - new TlrSettingsConfigurationBuilder() - .withTitleLevelRequestsFeatureEnabled(isTlrEnabled) - .withTlrHoldShouldFollowCirculationRules(holdShouldFollowCirculationRules) - .withConfirmationPatronNoticeTemplateId(confirmationTemplateId) - .withCancellationPatronNoticeTemplateId(cancellationTemplateId) - .withExpirationPatronNoticeTemplateId(expirationTemplateId) - .create() - .encodePrettily()); - } - private static JsonObject combinedTimeZoneConfig(String timezone) { final JsonObject encodedValue = new JsonObject(); write(encodedValue, "locale", US_LOCALE); diff --git a/src/test/java/api/support/fixtures/ConfigurationsFixture.java b/src/test/java/api/support/fixtures/ConfigurationsFixture.java deleted file mode 100644 index f42e903052..0000000000 --- a/src/test/java/api/support/fixtures/ConfigurationsFixture.java +++ /dev/null @@ -1,41 +0,0 @@ -package api.support.fixtures; - -import java.util.UUID; - -import api.support.http.ResourceClient; - -public class ConfigurationsFixture { - private final ResourceClient client; - private UUID tlrConfigurationEntryId = null; - - public ConfigurationsFixture(ResourceClient client) { - this.client = client; - } - - public void enableTlrFeature() { - deleteTlrFeatureConfig(); - tlrConfigurationEntryId = client.create(ConfigurationExample.tlrFeatureEnabled()).getId(); - } - - public void disableTlrFeature() { - deleteTlrFeatureConfig(); - tlrConfigurationEntryId = client.create(ConfigurationExample.tlrFeatureDisabled()).getId(); - } - - public void deleteTlrFeatureConfig() { - if (tlrConfigurationEntryId != null) { - client.delete(tlrConfigurationEntryId); - tlrConfigurationEntryId = null; - } - } - - public void configureTlrFeature(boolean isTlrFeatureEnabled, boolean tlrHoldShouldFollowCirculationRules, - UUID confirmationTemplateId, UUID cancellationTemplateId, UUID expirationTemplateId) { - - deleteTlrFeatureConfig(); - tlrConfigurationEntryId = client.create(ConfigurationExample.tlrFeatureConfiguration( - isTlrFeatureEnabled, tlrHoldShouldFollowCirculationRules, confirmationTemplateId, - cancellationTemplateId, expirationTemplateId)) - .getId(); - } -} diff --git a/src/test/java/api/support/fixtures/SearchInstanceFixture.java b/src/test/java/api/support/fixtures/SearchInstanceFixture.java new file mode 100644 index 0000000000..e1bb144753 --- /dev/null +++ b/src/test/java/api/support/fixtures/SearchInstanceFixture.java @@ -0,0 +1,26 @@ +package api.support.fixtures; + +import java.util.List; +import java.util.UUID; + +import api.support.builders.InstanceBuilder; +import api.support.builders.SearchInstanceBuilder; +import api.support.http.ItemResource; +import api.support.http.ResourceClient; + +public class SearchInstanceFixture { + + private final ResourceClient searchClient; + + public SearchInstanceFixture() { + this.searchClient = ResourceClient.forSearchClient(); + } + + public void basedUponDunkirk(UUID instanceId, ItemResource itemResource) { + SearchInstanceBuilder builder = new SearchInstanceBuilder( + new InstanceBuilder( + "Dunkirk", UUID.randomUUID()).withId(instanceId).create()) + .withItems(List.of(itemResource.getJson())); + searchClient.create(builder); + } +} diff --git a/src/test/java/api/support/fixtures/ServicePointExamples.java b/src/test/java/api/support/fixtures/ServicePointExamples.java index 05dc048a41..3267a39341 100644 --- a/src/test/java/api/support/fixtures/ServicePointExamples.java +++ b/src/test/java/api/support/fixtures/ServicePointExamples.java @@ -79,4 +79,12 @@ static ServicePointBuilder basedUponCircDesk10() { .withholdShelfClosedLibraryDateManagement(ExpirationDateManagement.MOVE_TO_THE_END_OF_THE_NEXT_OPEN_DAY.name()) .withHoldShelfExpriyPeriod(6, "Months"); } + + static ServicePointBuilder basedUponCircDesk11() { + return new ServicePointBuilder("Circ Desk 11", "cd11", + "Circulation Desk -- Back Entrance") + .withPickupLocation(FALSE) + .withEcsRequestRouting(TRUE) + .withHoldShelfExpriyPeriod(6, "Months"); + } } diff --git a/src/test/java/api/support/fixtures/ServicePointsFixture.java b/src/test/java/api/support/fixtures/ServicePointsFixture.java index 52df0996c4..7cb92a7886 100644 --- a/src/test/java/api/support/fixtures/ServicePointsFixture.java +++ b/src/test/java/api/support/fixtures/ServicePointsFixture.java @@ -2,6 +2,7 @@ import static api.support.fixtures.ServicePointExamples.basedUponCircDesk1; import static api.support.fixtures.ServicePointExamples.basedUponCircDesk10; +import static api.support.fixtures.ServicePointExamples.basedUponCircDesk11; import static api.support.fixtures.ServicePointExamples.basedUponCircDesk2; import static api.support.fixtures.ServicePointExamples.basedUponCircDesk3; import static api.support.fixtures.ServicePointExamples.basedUponCircDesk4; @@ -81,6 +82,11 @@ public IndividualResource cd10() { return create(basedUponCircDesk10()); } + public IndividualResource cd11() { + + return create(basedUponCircDesk11()); + } + public IndividualResource create(ServicePointBuilder builder) { return servicePointRecordCreator.createIfAbsent(builder); diff --git a/src/test/java/api/support/fixtures/SettingsFixture.java b/src/test/java/api/support/fixtures/SettingsFixture.java index 23166fcf83..41220f073d 100644 --- a/src/test/java/api/support/fixtures/SettingsFixture.java +++ b/src/test/java/api/support/fixtures/SettingsFixture.java @@ -1,12 +1,15 @@ package api.support.fixtures; +import java.util.List; +import java.util.UUID; + import api.support.builders.SettingsBuilder; import api.support.http.ResourceClient; import io.vertx.core.json.JsonObject; -import java.util.UUID; - public class SettingsFixture { + private static final UUID GENERAL_TLR_SETTINGS_ID = UUID.randomUUID(); + private static final UUID REGULAR_TLR_SETTINGS_ID = UUID.randomUUID(); private final ResourceClient settingsClient; @@ -27,4 +30,58 @@ private SettingsBuilder buildCheckoutLockFeatureSettings(boolean checkoutFeature .encodePrettily() ); } + + public void enableTlrFeature() { + createGeneralTlrSettings(true, false); + } + + public void disableTlrFeature() { + createGeneralTlrSettings(false, false); + } + + public void deleteTlrFeatureSettings() { + settingsClient.delete(GENERAL_TLR_SETTINGS_ID); + settingsClient.delete(REGULAR_TLR_SETTINGS_ID); + } + + public void configureTlrFeature(boolean isTlrFeatureEnabled, boolean tlrHoldShouldFollowCirculationRules, + UUID confirmationTemplateId, UUID cancellationTemplateId, UUID expirationTemplateId) { + + deleteTlrFeatureSettings(); + createGeneralTlrSettings(isTlrFeatureEnabled, tlrHoldShouldFollowCirculationRules); + createRegularTlrSettings(confirmationTemplateId, cancellationTemplateId, expirationTemplateId); + } + + private void createGeneralTlrSettings(boolean isTlrFeatureEnabled, + boolean tlrHoldShouldFollowCirculationRules) { + + JsonObject value = new JsonObject() + .put("titleLevelRequestsFeatureEnabled", isTlrFeatureEnabled) + .put("createTitleLevelRequestsByDefault", false) + .put("tlrHoldShouldFollowCirculationRules", tlrHoldShouldFollowCirculationRules); + + SettingsBuilder builder = new SettingsBuilder(GENERAL_TLR_SETTINGS_ID, "circulation", + "generalTlr", value); + + settingsClient.create(builder); + } + + private void createRegularTlrSettings(UUID confirmationTemplateId, UUID cancellationTemplateId, + UUID expirationTemplateId) { + + JsonObject regularTlrValue = new JsonObject() + .put("cancellationPatronNoticeTemplateId", cancellationTemplateId) + .put("confirmationPatronNoticeTemplateId", confirmationTemplateId) + .put("expirationPatronNoticeTemplateId", expirationTemplateId); + + SettingsBuilder builder = new SettingsBuilder(REGULAR_TLR_SETTINGS_ID, "circulation", + "regularTlr", regularTlrValue); + + settingsClient.create(builder); + } + + public List getAll() { + return settingsClient.getAll(); + } + } diff --git a/src/test/java/api/support/http/InterfaceUrls.java b/src/test/java/api/support/http/InterfaceUrls.java index 23c5878ecc..c2f012900c 100644 --- a/src/test/java/api/support/http/InterfaceUrls.java +++ b/src/test/java/api/support/http/InterfaceUrls.java @@ -334,6 +334,13 @@ public static URL settingsStorageUrl() { return APITestContext.viaOkapiModuleUrl("/settings/entries"); } + public static URL searchUrl(String subPath) { + return APITestContext.viaOkapiModuleUrl("/search/instances" + subPath); + } + + public static URL itemsByInstanceUrl(String subPath) { + return circulationModuleUrl("/circulation/items-by-instance?" + subPath); + } public static URL circulationSettingsUrl(String subPath) { return circulationModuleUrl("/circulation/settings" + subPath); } diff --git a/src/test/java/api/support/http/ResourceClient.java b/src/test/java/api/support/http/ResourceClient.java index 73893b3b33..47469e8813 100644 --- a/src/test/java/api/support/http/ResourceClient.java +++ b/src/test/java/api/support/http/ResourceClient.java @@ -272,6 +272,10 @@ public static ResourceClient forActualCostRecordsStorage() { return new ResourceClient(InterfaceUrls::actualCostRecordsStorageUrl, "actualCostRecords"); } + public static ResourceClient forSearchClient() { + return new ResourceClient(InterfaceUrls::searchUrl, "instances"); + } + public static ResourceClient forCirculationSettings() { return new ResourceClient(InterfaceUrls::circulationSettingsUrl, "circulationSettings"); } diff --git a/src/test/java/api/support/matchers/ValidationErrorMatchers.java b/src/test/java/api/support/matchers/ValidationErrorMatchers.java index 36796055b6..5c197e4a14 100644 --- a/src/test/java/api/support/matchers/ValidationErrorMatchers.java +++ b/src/test/java/api/support/matchers/ValidationErrorMatchers.java @@ -238,7 +238,7 @@ public static Matcher isInsufficientPermissionsError( public static Matcher isInsufficientPermissionsToOverridePatronBlockError() { return isInsufficientPermissionsError("patronBlock", - List.of("circulation.override-patron-block")); + List.of("circulation.override-patron-block.post")); } } diff --git a/src/test/java/api/support/utl/BlockOverridesUtils.java b/src/test/java/api/support/utl/BlockOverridesUtils.java index 805b26ff2e..76f9121cfb 100644 --- a/src/test/java/api/support/utl/BlockOverridesUtils.java +++ b/src/test/java/api/support/utl/BlockOverridesUtils.java @@ -13,8 +13,8 @@ import io.vertx.core.json.JsonObject; public final class BlockOverridesUtils { - public static final String OVERRIDE_RENEWAL_PERMISSION = "circulation.override-renewal-block"; - public static final String OVERRIDE_PATRON_BLOCK_PERMISSION = "circulation.override-patron-block"; + public static final String OVERRIDE_RENEWAL_PERMISSION = "circulation.override-renewal-block.post"; + public static final String OVERRIDE_PATRON_BLOCK_PERMISSION = "circulation.override-patron-block.post"; private static final String OVERRIDABLE_BLOCK = "overridableBlock"; public static List getMissingPermissions(Response response) { diff --git a/src/test/java/org/folio/circulation/infrastructure/storage/SettingsRepositoryTest.java b/src/test/java/org/folio/circulation/infrastructure/storage/SettingsRepositoryTest.java index f3c7c60c92..ff17621af8 100644 --- a/src/test/java/org/folio/circulation/infrastructure/storage/SettingsRepositoryTest.java +++ b/src/test/java/org/folio/circulation/infrastructure/storage/SettingsRepositoryTest.java @@ -2,6 +2,9 @@ import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; +import lombok.SneakyThrows; + +import org.folio.circulation.domain.configuration.TlrSettingsConfiguration; import org.folio.circulation.support.Clients; import org.folio.circulation.support.CollectionResourceClient; import org.folio.circulation.support.ServerErrorFailure; @@ -13,11 +16,16 @@ import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import static org.folio.circulation.support.results.Result.ofAsync; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; class SettingsRepositoryTest { @@ -58,6 +66,93 @@ void testFetchSettingsWhenSettingsApiThrowError() throws ExecutionException, Int assertFalse(res.isCheckOutLockFeatureEnabled()); } + @Test + @SneakyThrows + void fetchTlrSettings() { + Clients clients = mock(Clients.class); + CollectionResourceClient settingsClient = mock(CollectionResourceClient.class); + CollectionResourceClient configurationClient = mock(CollectionResourceClient.class); + + JsonObject mockSettingsResponse = new JsonObject() + .put("items", new JsonArray() + .add(new JsonObject() + .put("id", UUID.randomUUID().toString()) + .put("scope", "circulation") + .put("key", "generalTlr") + .put("value", new JsonObject() + .put("titleLevelRequestsFeatureEnabled", true) + .put("createTitleLevelRequestsByDefault", true) + .put("tlrHoldShouldFollowCirculationRules", true)))) + .put("resultInfo", new JsonObject() + .put("totalRecords", 0) + .put("diagnostics", new JsonArray())); + + when(clients.settingsStorageClient()).thenReturn(settingsClient); + when(clients.configurationStorageClient()).thenReturn(configurationClient); + when(settingsClient.getMany(any(), any())) + .thenReturn(ofAsync(new Response(200, mockSettingsResponse.encode(), "application/json"))); + + TlrSettingsConfiguration actualResult = new SettingsRepository(clients) + .lookupTlrSettings() + .get(30, TimeUnit.SECONDS) + .value(); + + assertEquals(new TlrSettingsConfiguration(true, true, true, null, null, null), actualResult); + verify(settingsClient).getMany(any(), any()); + verifyNoInteractions(configurationClient); + } + + @Test + @SneakyThrows + void fallBackToLegacyConfigurationWhenTlrSettingsAreNotFound() { + Clients clients = mock(Clients.class); + CollectionResourceClient settingsClient = mock(CollectionResourceClient.class); + CollectionResourceClient configurationClient = mock(CollectionResourceClient.class); + + JsonObject mockEmptySettingsResponse = new JsonObject() + .put("items", new JsonArray()) + .put("resultInfo", new JsonObject() + .put("totalRecords", 0) + .put("diagnostics", new JsonArray())); + + JsonObject mockConfigurationResponse = new JsonObject() + .put("configs", new JsonArray().add( + new JsonObject() + .put("id", UUID.randomUUID().toString()) + .put("module", "SETTINGS") + .put("configName", "TLR") + .put("enabled", true) + .put("value", new JsonObject() + .put("titleLevelRequestsFeatureEnabled", true) + .put("createTitleLevelRequestsByDefault", true) + .put("tlrHoldShouldFollowCirculationRules", true) + .put("confirmationPatronNoticeTemplateId", null) + .put("cancellationPatronNoticeTemplateId", null) + .put("expirationPatronNoticeTemplateId", null) + .encode()))) + .put("totalRecords", 1) + .put("resultInfo", new JsonObject() + .put("totalRecords", 1) + .put("facets", new JsonArray()) + .put("diagnostics", new JsonArray())); + + when(clients.settingsStorageClient()).thenReturn(settingsClient); + when(clients.configurationStorageClient()).thenReturn(configurationClient); + when(settingsClient.getMany(any(), any())) + .thenReturn(ofAsync(new Response(200, mockEmptySettingsResponse.encode(), "application/json"))); + when(configurationClient.getMany(any(), any())) + .thenReturn(ofAsync(new Response(200, mockConfigurationResponse.encode(), "application/json"))); + + TlrSettingsConfiguration actualResult = new SettingsRepository(clients) + .lookupTlrSettings() + .get(30, TimeUnit.SECONDS) + .value(); + + assertEquals(new TlrSettingsConfiguration(true, true, true, null, null, null), actualResult); + verify(settingsClient).getMany(any(), any()); + verify(configurationClient).getMany(any(), any()); + } + private JsonObject createCheckoutLockJsonResponse(boolean checkoutFeatureFlag) { JsonObject checkoutLockResponseJson = new JsonObject(); checkoutLockResponseJson.put("id", UUID.randomUUID()) diff --git a/src/test/java/org/folio/circulation/support/http/client/CqlQueryTests.java b/src/test/java/org/folio/circulation/support/http/client/CqlQueryTests.java index 349b294343..0ddc0e11a7 100644 --- a/src/test/java/org/folio/circulation/support/http/client/CqlQueryTests.java +++ b/src/test/java/org/folio/circulation/support/http/client/CqlQueryTests.java @@ -2,6 +2,7 @@ import static java.lang.String.format; import static java.util.Arrays.asList; +import static java.util.Collections.emptyMap; import static org.folio.circulation.support.CqlSortBy.ascending; import static org.folio.circulation.support.http.client.CqlQuery.exactMatch; import static org.folio.circulation.support.http.client.CqlQuery.exactMatchAny; @@ -12,15 +13,20 @@ import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; import java.time.ZonedDateTime; import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; import org.folio.circulation.support.CqlSortBy; import org.folio.circulation.support.CqlSortClause; import org.folio.circulation.support.ServerErrorFailure; import org.folio.circulation.support.results.Result; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; class CqlQueryTests { @Test @@ -153,4 +159,32 @@ void shouldBeEqualToQueryWithSameDefinition() { assertThat(firstQuery.equals(secondQuery), is(true)); } + + @ParameterizedTest + @CsvSource(nullValues={"null"}, value = { + "index1, value1, index2, value2, (index1==\"value1\" or index2==\"value2\")", + "null, null, index2, value2, (index2==\"value2\")", + "index1, value1, null, null, (index1==\"value1\")", + }) + void canMatchByAnyIndex(String index1, String value1, String index2, String value2, + String expectedResult) { + + Map filters = new HashMap<>(); + filters.put(index1, value1); + filters.put(index2, value2); + + String actualResult = exactMatchAny(filters) + .value() + .asText(); + assertThat(actualResult, equalTo(expectedResult)); + } + + @Test + void exactMatchAnyFailsWhenListOfIndicesIsEmpty() { + Result result = exactMatchAny(emptyMap()); + assertThat("Failed result expected", result.failed()); + assertThat(result.cause(), instanceOf(ServerErrorFailure.class)); + assertThat(((ServerErrorFailure) result.cause()).getReason(), + equalTo("Cannot generate empty CQL query")); + } }