diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index 4610e6f4c..0067a2632 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -331,6 +331,37 @@ } ] }, + { + "id": "actual-cost-record-storage", + "version": "0.1", + "handlers": [ + { + "methods": ["GET"], + "pathPattern": "/actual-cost-record-storage/actual-cost-records", + "permissionsRequired": ["actual-cost-record-storage.actual-cost-records.collection.get"] + }, + { + "methods": ["GET"], + "pathPattern": "/actual-cost-record-storage/actual-cost-records/{id}", + "permissionsRequired": ["actual-cost-record-storage.actual-cost-records.item.get"] + }, + { + "methods": ["POST"], + "pathPattern": "/actual-cost-record-storage/actual-cost-records", + "permissionsRequired": ["actual-cost-record-storage.actual-cost-records.item.post"] + }, + { + "methods": ["PUT"], + "pathPattern": "/actual-cost-record-storage/actual-cost-records/{id}", + "permissionsRequired": ["actual-cost-record-storage.actual-cost-records.item.put"] + }, + { + "methods": ["DELETE"], + "pathPattern": "/actual-cost-record-storage/actual-cost-records/{id}", + "permissionsRequired": ["actual-cost-record-storage.actual-cost-records.item.delete"] + } + ] + }, { "id": "request-preference-storage", "version": "2.0", @@ -978,7 +1009,12 @@ "tlr-feature-toggle-job-storage.tlr-feature-toggle-jobs.collection.get", "tlr-feature-toggle-job-storage.tlr-feature-toggle-jobs.item.get", "tlr-feature-toggle-job-storage.tlr-feature-toggle-jobs.item.put", - "tlr-feature-toggle-job-storage.tlr-feature-toggle-jobs.item.delete" + "tlr-feature-toggle-job-storage.tlr-feature-toggle-jobs.item.delete", + "actual-cost-record-storage.actual-cost-records.collection.get", + "actual-cost-record-storage.actual-cost-records.item.get", + "actual-cost-record-storage.actual-cost-records.item.post", + "actual-cost-record-storage.actual-cost-records.item.put", + "actual-cost-record-storage.actual-cost-records.item.delete" ] }, { @@ -1035,6 +1071,31 @@ "permissionName": "tlr-feature-toggle-job-storage.tlr-feature-toggle-jobs.item.delete", "displayName": "Circulation storage - delete tlr feature toggle job", "description": "Delete tlr feature toggle job" + }, + { + "permissionName": "actual-cost-record-storage.actual-cost-records.item.post", + "displayName": "Circulation storage - post actual cost record", + "description": "Create actual cost record" + }, + { + "permissionName": "actual-cost-record-storage.actual-cost-records.collection.get", + "displayName": "Circulation storage - get actual cost record collection", + "description": "Get actual cost record collection" + }, + { + "permissionName": "actual-cost-record-storage.actual-cost-records.item.get", + "displayName": "Circulation storage - get actual cost record", + "description": "Get actual cost record" + }, + { + "permissionName": "actual-cost-record-storage.actual-cost-records.item.put", + "displayName": "Circulation storage - put actual cost record", + "description": "Update actual cost record" + }, + { + "permissionName": "actual-cost-record-storage.actual-cost-records.item.delete", + "displayName": "Circulation storage - delete actual cost record", + "description": "Delete actual cost record" } ], "launchDescriptor": { diff --git a/ramls/actual-cost-record-storage.raml b/ramls/actual-cost-record-storage.raml new file mode 100644 index 000000000..5fb760aa3 --- /dev/null +++ b/ramls/actual-cost-record-storage.raml @@ -0,0 +1,59 @@ +#%RAML 1.0 +title: Actual Cost Record Storage +version: v0.1 +protocols: [ HTTP, HTTPS ] +baseUri: http://localhost:9130 + +documentation: + - title: Actual cost record API + content: Storage for actual cost record + +types: + actual-cost-record: !include actual-cost-record.json + actual-cost-records: !include actual-cost-records.json + errors: !include raml-util/schemas/errors.schema + parameters: !include raml-util/schemas/parameters.schema + +traits: + language: !include raml-util/traits/language.raml + pageable: !include raml-util/traits/pageable.raml + searchable: !include raml-util/traits/searchable.raml + validate: !include raml-util/traits/validation.raml + +resourceTypes: + collection: !include raml-util/rtypes/collection.raml + collection-item: !include raml-util/rtypes/item-collection.raml + +/actual-cost-record-storage: + /actual-cost-records: + displayName: Actual cost records + type: + collection: + exampleCollection: !include examples/actual-cost-records.json + exampleItem: !include examples/actual-cost-record.json + schemaCollection: actual-cost-records + schemaItem: actual-cost-record + get: + is: [pageable, + searchable: {description: "by using CQL", + example: "lossType=\"Aged to lost\""} + ] + post: + is: [validate] + body: + application/json: + type: actual-cost-record + /{id}: + type: + collection-item: + exampleItem: !include examples/actual-cost-record.json + schema: actual-cost-record + get: + description: "Get actual cost record" + put: + description: "Update actual cost record" + is: [validate] + delete: + description: "Delete actual cost record" + is: [language] + diff --git a/ramls/actual-cost-record.json b/ramls/actual-cost-record.json new file mode 100644 index 000000000..71e1cd960 --- /dev/null +++ b/ramls/actual-cost-record.json @@ -0,0 +1,141 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "description": "Actual cost record", + "properties": { + "id": { + "description": "Actual cost record ID", + "type": "string", + "$ref": "raml-util/schemas/uuid.schema" + }, + "userId": { + "description": "ID of the patron the actual cost record was created for", + "type": "string", + "$ref": "raml-util/schemas/uuid.schema" + }, + "userBarcode": { + "description": "Barcode of the patron the actual cost record was created for", + "type": "string" + }, + "loanId": { + "description": "Unique ID (generated UUID) of the loan", + "type": "string", + "$ref": "raml-util/schemas/uuid.schema" + }, + "itemLossType": { + "description": "Type of the item loss", + "type": "string", + "enum": [ + "Aged to lost", + "Declared lost" + ] + }, + "dateOfLoss": { + "description": "Date and time when the item was lost", + "type": "string", + "format": "date-time" + }, + "title": { + "description": "The primary title (or label) associated with the resource", + "type": "string" + }, + "identifiers": { + "type": "array", + "description": "An extensible set of name-value pairs of identifiers associated with the resource", + "minItems": 0, + "items": { + "type": "object", + "properties": { + "value": { + "type": "string", + "description": "Resource identifier value" + }, + "identifierTypeId": { + "type": "string", + "description": "UUID of resource identifier type (e.g. ISBN, ISSN, LCCN, CODEN, Locally defined identifiers)", + "$ref": "raml-util/schemas/uuid.schema" + } + }, + "additionalProperties": false, + "required": [ + "value", + "identifierTypeId" + ] + } + }, + "itemBarcode": { + "description": "Barcode of the lost item", + "type": "string" + }, + "loanType": { + "description": "Loan type of the lost item", + "type": "string" + }, + "effectiveCallNumberComponents": { + "type": "object", + "description": "Elements of a full call number generated from the item or holding", + "properties": { + "callNumber": { + "type": "string", + "description": "Effective Call Number is an identifier assigned to an item or its holding and associated with the item.", + "readonly": true + }, + "prefix": { + "type": "string", + "description": "Effective Call Number Prefix is the prefix of the identifier assigned to an item or its holding and associated with the item.", + "readonly": true + }, + "suffix": { + "type": "string", + "description": "Effective Call Number Suffix is the suffix of the identifier assigned to an item or its holding and associated with the item.", + "readonly": true + } + } + }, + "permanentItemLocation": { + "description": "Permanent item location of the lost item", + "type": "string" + }, + "feeFineOwnerId": { + "description": "Fee/fine owner ID", + "type": "string", + "$ref": "raml-util/schemas/uuid.schema" + }, + "feeFineOwner": { + "description": "Fee/fine owner name", + "type": "string" + }, + "feeFineTypeId": { + "description": "Fee/fine type ID", + "type": "string", + "$ref": "raml-util/schemas/uuid.schema" + }, + "feeFineType": { + "description": "Fee/fine type name", + "type": "string" + }, + "metadata": { + "description": "Metadata about creation and changes, provided by the server (client should not provide)", + "type": "object", + "$ref": "raml-util/schemas/metadata.schema" + } + }, + "additionalProperties": false, + "required": [ + "userId", + "userBarcode", + "loanId", + "itemLossType", + "dateOfLoss", + "title", + "identifiers", + "itemBarcode", + "loanType", + "effectiveCallNumberComponents", + "permanentItemLocation", + "feeFineOwnerId", + "feeFineOwner", + "feeFineTypeId", + "feeFineType" + ] +} diff --git a/ramls/actual-cost-records.json b/ramls/actual-cost-records.json new file mode 100644 index 000000000..81fb549d0 --- /dev/null +++ b/ramls/actual-cost-records.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Collection of Actual cost records", + "type": "object", + "properties": { + "actualCostRecords": { + "description": "List of Actual cost records", + "id": "actualCostRecords", + "type": "array", + "items": { + "type": "object", + "$ref": "actual-cost-record.json" + } + }, + "totalRecords": { + "type": "integer" + } + }, + "required": [ + "actualCostRecords", + "totalRecords" + ] +} + diff --git a/ramls/examples/actual-cost-record.json b/ramls/examples/actual-cost-record.json new file mode 100644 index 000000000..72cdd3675 --- /dev/null +++ b/ramls/examples/actual-cost-record.json @@ -0,0 +1,27 @@ +{ + "id": "89105c06-dbdb-4aa0-9695-d4d19c733270", + "userId": "88805c06-dbdb-4aa0-9695-d4d19c733221", + "userBarcode": "777", + "loanId": "98805c06-dbdb-4aa0-9695-d4d19c733221", + "itemLossType": "Aged to lost", + "dateOfLoss": "2022-01-01T22:25:37Z", + "title": "Test Title", + "identifiers": [ + { + "identifierTypeId": "8261054f-be78-422d-bd51-4ed9f33c3422", + "value": "9781466636897" + } + ], + "itemBarcode": "888", + "loanType": "Can Circulate", + "effectiveCallNumber": { + "callNumber": "TX809.M17J66", + "suffix": "1993", + "prefix": "f" + }, + "permanentItemLocation": "UC/HP/JCL/Sci", + "feeFineOwnerId": "88805c06-dbdb-4aa0-9695-d4d19c733221", + "feeFineOwner": "Main circ desk", + "feeFineTypeId": "88805c06-dbdb-4aa0-9695-d4d19c733221", + "feeFineType": "Lost Item fee (actual cost)" +} diff --git a/ramls/examples/actual-cost-records.json b/ramls/examples/actual-cost-records.json new file mode 100644 index 000000000..4166f16d5 --- /dev/null +++ b/ramls/examples/actual-cost-records.json @@ -0,0 +1,32 @@ +{ + "actualCostRecords" : [ + { + "id": "89105c06-dbdb-4aa0-9695-d4d19c733270", + "userId": "88805c06-dbdb-4aa0-9695-d4d19c733221", + "userBarcode": "777", + "loanId": "98805c06-dbdb-4aa0-9695-d4d19c733221", + "itemLossType": "Aged to lost", + "dateOfLoss": "2022-01-01T22:25:37Z", + "title": "Test Title", + "identifiers": [ + { + "identifierTypeId": "8261054f-be78-422d-bd51-4ed9f33c3422", + "value": "9781466636897" + } + ], + "itemBarcode": "888", + "loanType": "Can Circulate", + "effectiveCallNumber": { + "callNumber": "TX809.M17J66", + "suffix": "1993", + "prefix": "f" + }, + "permanentItemLocation": "UC/HP/JCL/Sci", + "feeFineOwnerId": "88805c06-dbdb-4aa0-9695-d4d19c733221", + "feeFineOwner": "Main circ desk", + "feeFineTypeId": "88805c06-dbdb-4aa0-9695-d4d19c733221", + "feeFineType": "Lost Item fee (actual cost)" + } + ], + "totalRecords": 1 +} diff --git a/src/main/java/org/folio/rest/impl/ActualCostRecordAPI.java b/src/main/java/org/folio/rest/impl/ActualCostRecordAPI.java new file mode 100644 index 000000000..2d01469c8 --- /dev/null +++ b/src/main/java/org/folio/rest/impl/ActualCostRecordAPI.java @@ -0,0 +1,62 @@ +package org.folio.rest.impl; + +import java.util.Map; + +import javax.ws.rs.core.Response; + +import org.folio.rest.jaxrs.model.ActualCostRecord; +import org.folio.rest.jaxrs.model.ActualCostRecords; +import org.folio.rest.jaxrs.resource.ActualCostRecordStorage; +import org.folio.rest.persist.PgUtil; + +import io.vertx.core.AsyncResult; +import io.vertx.core.Context; +import io.vertx.core.Handler; +import static org.folio.support.ModuleConstants.ACTUAL_COST_RECORD_CLASS; +import static org.folio.support.ModuleConstants.ACTUAL_COST_RECORD_TABLE; + +public class ActualCostRecordAPI implements ActualCostRecordStorage { + + public void getActualCostRecordStorageActualCostRecords(int offset, int limit, String query, + String lang, Map okapiHeaders, Handler> asyncResultHandler, + Context vertxContext) { + + PgUtil.get(ACTUAL_COST_RECORD_TABLE, ACTUAL_COST_RECORD_CLASS, ActualCostRecords.class, + query, offset, limit, okapiHeaders, vertxContext, + GetActualCostRecordStorageActualCostRecordsResponse.class, asyncResultHandler); + } + + public void postActualCostRecordStorageActualCostRecords(String lang, ActualCostRecord entity, + Map okapiHeaders, Handler> asyncResultHandler, + Context vertxContext) { + + PgUtil.post(ACTUAL_COST_RECORD_TABLE, entity, okapiHeaders, vertxContext, + PostActualCostRecordStorageActualCostRecordsResponse.class, asyncResultHandler); + } + + public void getActualCostRecordStorageActualCostRecordsById(String id, String lang, + Map okapiHeaders, Handler> asyncResultHandler, + Context vertxContext) { + + PgUtil.getById(ACTUAL_COST_RECORD_TABLE, ACTUAL_COST_RECORD_CLASS, id, + okapiHeaders, vertxContext, GetActualCostRecordStorageActualCostRecordsByIdResponse.class, + asyncResultHandler); + } + + public void putActualCostRecordStorageActualCostRecordsById(String id, String lang, + ActualCostRecord entity, Map okapiHeaders, + Handler> asyncResultHandler, Context vertxContext) { + + PgUtil.put(ACTUAL_COST_RECORD_TABLE, entity, id, okapiHeaders, + vertxContext, PutActualCostRecordStorageActualCostRecordsByIdResponse.class, + asyncResultHandler); + } + + public void deleteActualCostRecordStorageActualCostRecordsById(String id, String lang, + Map okapiHeaders, Handler> asyncResultHandler, + Context vertxContext) { + + PgUtil.deleteById(ACTUAL_COST_RECORD_TABLE, id, okapiHeaders, vertxContext, + DeleteActualCostRecordStorageActualCostRecordsByIdResponse.class, asyncResultHandler); + } +} diff --git a/src/main/java/org/folio/support/ModuleConstants.java b/src/main/java/org/folio/support/ModuleConstants.java index 684c444dc..854457886 100644 --- a/src/main/java/org/folio/support/ModuleConstants.java +++ b/src/main/java/org/folio/support/ModuleConstants.java @@ -1,5 +1,6 @@ package org.folio.support; +import org.folio.rest.jaxrs.model.ActualCostRecord; import org.folio.rest.jaxrs.model.CheckIn; import org.folio.rest.jaxrs.model.Loan; import org.folio.rest.jaxrs.model.Request; @@ -17,6 +18,8 @@ public class ModuleConstants { public static final String CHECKIN_TABLE = "check_in"; public static final Class CHECKIN_CLASS = CheckIn.class; public static final String TLR_FEATURE_TOGGLE_JOB_TABLE = "tlr_feature_toggle_job"; + public static final String ACTUAL_COST_RECORD_TABLE = "actual_cost_record"; + public static final Class ACTUAL_COST_RECORD_CLASS = ActualCostRecord.class; public static final String TLR_FEATURE_TOGGLE_JOB_STATUS_FIELD = "'status'"; public static final String REQUEST_STATUS_FIELD = "'status'"; public static final Class TLR_FEATURE_TOGGLE_JOB_CLASS = diff --git a/src/main/resources/templates/db_scripts/schema.json b/src/main/resources/templates/db_scripts/schema.json index 53f5332ed..8982409b5 100644 --- a/src/main/resources/templates/db_scripts/schema.json +++ b/src/main/resources/templates/db_scripts/schema.json @@ -366,6 +366,11 @@ "tableName": "tlr_feature_toggle_job", "withMetadata": true, "withAuditing": false + }, + { + "tableName": "actual_cost_record", + "withMetadata": true, + "withAuditing": false } ], "scripts": [ diff --git a/src/test/java/org/folio/rest/api/ActualCostRecordAPITest.java b/src/test/java/org/folio/rest/api/ActualCostRecordAPITest.java new file mode 100644 index 000000000..f9f44a42d --- /dev/null +++ b/src/test/java/org/folio/rest/api/ActualCostRecordAPITest.java @@ -0,0 +1,129 @@ +package org.folio.rest.api; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import org.folio.rest.jaxrs.model.ActualCostRecord; +import org.folio.rest.jaxrs.model.EffectiveCallNumberComponents; +import org.folio.rest.jaxrs.model.Identifier; +import org.folio.rest.support.ApiTests; +import org.folio.rest.support.http.AssertingRecordClient; +import org.folio.rest.support.http.InterfaceUrls; +import org.folio.rest.support.spring.TestContextConfiguration; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.rules.SpringClassRule; +import org.springframework.test.context.junit4.rules.SpringMethodRule; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import io.vertx.core.json.JsonObject; +import lombok.SneakyThrows; +import static org.folio.rest.jaxrs.model.ActualCostRecord.ItemLossType.AGED_TO_LOST; +import static org.folio.rest.jaxrs.model.ActualCostRecord.ItemLossType.DECLARED_LOST; +import static org.folio.rest.support.matchers.JsonMatchers.hasSameProperties; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsIterableContaining.hasItems; + +@ContextConfiguration(classes = TestContextConfiguration.class) +public class ActualCostRecordAPITest extends ApiTests { + private static final String ACTUAL_COST_RECORD_TABLE = "actual_cost_record"; + + @ClassRule + public static final SpringClassRule classRule = new SpringClassRule(); + @Rule + public final SpringMethodRule methodRule = new SpringMethodRule(); + + @Autowired + private ObjectMapper objectMapper; + + private final AssertingRecordClient actualCostRecordClient = + new AssertingRecordClient(client, StorageTestSuite.TENANT_ID, + InterfaceUrls::actualCostRecord, "actualCostRecords"); + + @Before + public void beforeEach() throws Exception { + StorageTestSuite.cleanUpTable(ACTUAL_COST_RECORD_TABLE); + } + + @Test + @SneakyThrows + public void canCreateAndGetAndDeleteActualCostRecords() { + JsonObject actualCostRecord1 = toJsonObject(createActualCostRecord()); + JsonObject actualCostRecord2 = toJsonObject(createActualCostRecord()); + JsonObject createResult1 = actualCostRecordClient.create(actualCostRecord1).getJson(); + JsonObject createResult2 = actualCostRecordClient.create(actualCostRecord2).getJson(); + + assertThat(createResult1, hasSameProperties(actualCostRecord1)); + assertThat(createResult2, hasSameProperties(actualCostRecord2)); + + List actualCostRecords = new ArrayList<>( + actualCostRecordClient.getMany("itemLossType==Aged to lost").getRecords()); + + assertThat(actualCostRecords, hasItems(hasSameProperties(createResult1), + hasSameProperties(createResult2))); + + for (JsonObject current : actualCostRecords) { + actualCostRecordClient.deleteById(UUID.fromString(current.getString("id"))); + } + + assertThat(actualCostRecordClient.getAll().getTotalRecords(), is(0)); + } + + @Test + @SneakyThrows + public void canCreateAndGetAndUpdateActualCostRecord() { + JsonObject actualCostRecord = toJsonObject(createActualCostRecord()); + JsonObject createResult = actualCostRecordClient.create(actualCostRecord).getJson(); + + assertThat(createResult, hasSameProperties(actualCostRecord)); + + JsonObject updatedJson = createResult.put("lossType", DECLARED_LOST.value()); + + actualCostRecordClient.attemptPutById(updatedJson); + + JsonObject fetchedJson = actualCostRecordClient.getById(updatedJson.getString("id")).getJson(); + + fetchedJson.remove("metadata"); + assertThat(updatedJson, hasSameProperties(fetchedJson)); + } + + private ActualCostRecord createActualCostRecord() { + return new ActualCostRecord() + .withUserId(UUID.randomUUID().toString()) + .withUserBarcode("777") + .withLoanId(UUID.randomUUID().toString()) + .withItemLossType(AGED_TO_LOST) + .withDateOfLoss(new DateTime(DateTimeZone.UTC).toDate()) + .withTitle("Test") + .withIdentifiers(List.of(new Identifier() + .withValue("9781466636897") + .withIdentifierTypeId(UUID.randomUUID().toString()))) + .withItemBarcode("888") + .withLoanType("Can Circulate") + .withEffectiveCallNumberComponents(new EffectiveCallNumberComponents() + .withCallNumber("callnumber") + .withPrefix("prefix") + .withSuffix("suffix")) + .withPermanentItemLocation("Main circ desk") + .withFeeFineOwnerId(UUID.randomUUID().toString()) + .withFeeFineOwner("Main circ desk") + .withFeeFineTypeId(UUID.randomUUID().toString()) + .withFeeFineType("Lost Item fee (actual cost)"); + } + + private JsonObject toJsonObject(ActualCostRecord actualCostRecord1) + throws JsonProcessingException { + return new JsonObject(objectMapper.writeValueAsString(actualCostRecord1)); + } + +} diff --git a/src/test/java/org/folio/rest/api/StorageTestSuite.java b/src/test/java/org/folio/rest/api/StorageTestSuite.java index 24ba8611d..950f0a4d9 100644 --- a/src/test/java/org/folio/rest/api/StorageTestSuite.java +++ b/src/test/java/org/folio/rest/api/StorageTestSuite.java @@ -79,7 +79,8 @@ RequestUpdateTriggerTest.class, JsonPropertyWriterTest.class, IsbnNormalizationTest.class, - TlrFeatureToggleJobAPITest.class + TlrFeatureToggleJobAPITest.class, + ActualCostRecordAPITest.class }) public class StorageTestSuite { diff --git a/src/test/java/org/folio/rest/support/builders/JsonBuilder.java b/src/test/java/org/folio/rest/support/builders/JsonBuilder.java index c79a97115..33240b557 100644 --- a/src/test/java/org/folio/rest/support/builders/JsonBuilder.java +++ b/src/test/java/org/folio/rest/support/builders/JsonBuilder.java @@ -5,7 +5,6 @@ import org.joda.time.LocalDate; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.ISODateTimeFormat; - import java.util.UUID; public class JsonBuilder { diff --git a/src/test/java/org/folio/rest/support/http/InterfaceUrls.java b/src/test/java/org/folio/rest/support/http/InterfaceUrls.java index f52b216f1..38e46b3bf 100644 --- a/src/test/java/org/folio/rest/support/http/InterfaceUrls.java +++ b/src/test/java/org/folio/rest/support/http/InterfaceUrls.java @@ -18,6 +18,10 @@ public static URL loanStorageUrl(String subPath) return storageUrl("/loan-storage/loans" + subPath); } + public static URL actualCostRecord(String subPath) throws MalformedURLException { + return storageUrl("/actual-cost-record-storage/actual-cost-records" + subPath); + } + public static URL loanHistoryUrl(String subPath) throws MalformedURLException { return storageUrl("/loan-storage/loan-history" + subPath); }