From b9a137d5e97daf554e9164172020c068b26a54fc Mon Sep 17 00:00:00 2001 From: Pavel Filippov Date: Fri, 20 Feb 2026 20:01:05 +0200 Subject: [PATCH 1/8] MODDCB-257: Implement transaction expiry and settings API --- HTTP_EXCHANGE_AUTO_REGISTRATION.md | 150 ----------- README.md | 89 +++++-- descriptors/ModuleDescriptor-template.json | 69 ++++++ pom.xml | 5 + .../folio/dcb/config/JpaAuditingConfig.java | 6 +- .../dcb/controller/SettingsController.java | 50 ++++ .../dcb/domain/entity/SettingEntity.java | 50 ++++ .../domain/entity/base/AuditableEntity.java | 24 +- .../dcb/domain/mapper/SettingMapper.java | 48 ++++ .../kafka/CirculationEventListener.java | 10 +- .../dcb/repository/SettingRepository.java | 33 +++ .../dcb/repository/TransactionRepository.java | 4 +- .../ServicePointExpirationPeriodService.java | 19 +- .../dcb/service/ServicePointService.java | 4 +- .../org/folio/dcb/service/SettingService.java | 111 +++++++++ .../entities/DcbServicePointService.java | 3 +- .../dcb/service/impl/BaseLibraryService.java | 34 +++ .../impl/BorrowingLibraryServiceImpl.java | 2 +- .../impl/LendingLibraryServiceImpl.java | 27 +- ...rvicePointExpirationPeriodServiceImpl.java | 54 +++- .../service/impl/ServicePointServiceImpl.java | 9 +- .../db/changelog/changelog-master.xml | 1 + .../changelog/changes/add-settings-table.xml | 33 +++ .../swagger.api/dcb_transaction.yaml | 146 +++++++++++ .../swagger.api/schemas/setting.yaml | 46 ++++ .../folio/dcb/it/BorrowerTransactionIT.java | 32 ++- .../dcb/it/BorrowingPickupTransactionIT.java | 34 ++- .../dcb/it/EcsRequestTransactionsIT.java | 8 +- .../dcb/it/FlexibleEffectiveLocationIT.java | 16 +- .../org/folio/dcb/it/LenderTransactionIT.java | 39 ++- .../org/folio/dcb/it/PickupTransactionIT.java | 37 ++- .../dcb/it/SelfBorrowingTransactionIT.java | 6 +- .../java/org/folio/dcb/it/SettingsIT.java | 218 ++++++++++++++++ .../dcb/it/base/BaseIntegrationTest.java | 8 +- .../it/base/BaseTenantIntegrationTest.java | 1 + .../CirculationCheckInEventListenerTest.java | 14 +- .../CirculationRequestEventListenerTest.java | 21 +- .../dcb/service/BaseLibraryServiceTest.java | 84 +++++-- .../service/BorrowingLibraryServiceTest.java | 2 +- .../service/LendingLibraryServiceTest.java | 55 +---- ...rvicePointExpirationPeriodServiceTest.java | 121 +++++++-- .../dcb/service/ServicePointServiceTest.java | 31 ++- .../folio/dcb/service/SettingServiceTest.java | 233 ++++++++++++++++++ .../entities/DcbServicePointServiceTest.java | 7 +- .../java/org/folio/dcb/utils/EntityUtils.java | 35 ++- .../folio/dcb/utils/EventDataProvider.java | 12 +- .../org/folio/dcb/utils/JsonTestUtils.java | 7 +- .../folio/dcb/utils/SettingsApiHelper.java | 94 +++++++ .../org/folio/dcb/utils/TestJdbcHelper.java | 95 ++++--- .../db/scripts/cleanup_dcb_tables.sql | 1 + .../204-put(Virtual+custom).json | 36 +++ 51 files changed, 1838 insertions(+), 436 deletions(-) delete mode 100644 HTTP_EXCHANGE_AUTO_REGISTRATION.md create mode 100644 src/main/java/org/folio/dcb/controller/SettingsController.java create mode 100644 src/main/java/org/folio/dcb/domain/entity/SettingEntity.java create mode 100644 src/main/java/org/folio/dcb/domain/mapper/SettingMapper.java create mode 100644 src/main/java/org/folio/dcb/repository/SettingRepository.java create mode 100644 src/main/java/org/folio/dcb/service/SettingService.java create mode 100644 src/main/resources/db/changelog/changes/add-settings-table.xml create mode 100644 src/main/resources/swagger.api/schemas/setting.yaml create mode 100644 src/test/java/org/folio/dcb/it/SettingsIT.java create mode 100644 src/test/java/org/folio/dcb/service/SettingServiceTest.java create mode 100644 src/test/java/org/folio/dcb/utils/SettingsApiHelper.java create mode 100644 src/test/resources/stubs/mod-inventory-storage/service-points/204-put(Virtual+custom).json diff --git a/HTTP_EXCHANGE_AUTO_REGISTRATION.md b/HTTP_EXCHANGE_AUTO_REGISTRATION.md deleted file mode 100644 index 2eecde89..00000000 --- a/HTTP_EXCHANGE_AUTO_REGISTRATION.md +++ /dev/null @@ -1,150 +0,0 @@ -# Automated HTTP Exchange Client Registration - -## Overview - -This project uses an automated approach to register all `@HttpExchange` annotated interfaces as Spring beans, eliminating the need for manual `@Bean` definitions. - -## How It Works - -### Before (Manual Registration) -Previously, you had to manually create a bean for each HTTP client: - -```java -@Configuration -public class HttpExchangeConfiguration { - - @Bean - public CalendarClient calendarClient(HttpServiceProxyFactory factory) { - return factory.createClient(CalendarClient.class); - } - - @Bean - public UsersClient usersClient(HttpServiceProxyFactory factory) { - return factory.createClient(UsersClient.class); - } - - @Bean - public CirculationClient circulationClient(HttpServiceProxyFactory factory) { - return factory.createClient(CirculationClient.class); - } - - // ... many more bean definitions -} -``` - -### After (Automated Registration) -Now, the `HttpExchangeConfiguration` class automatically scans and registers all `@HttpExchange` interfaces: - -```java -@Configuration -public class HttpExchangeConfiguration implements BeanFactoryPostProcessor { - // Automatically scans org.folio.dcb.integration package - // and registers all @HttpExchange interfaces as beans -} -``` - -## Creating New HTTP Clients - -Simply create an interface annotated with `@HttpExchange` in the `org.folio.dcb.integration` package: - -```java -package org.folio.dcb.integration.myservice; - -import org.springframework.web.service.annotation.HttpExchange; -import org.springframework.web.service.annotation.GetExchange; - -@HttpExchange("my-service") -public interface MyServiceClient { - - @GetExchange("/data") - MyData getData(); -} -``` - -The client will be automatically registered as a Spring bean with the name `myServiceClient` (interface name with lowercase first letter). - -## Using HTTP Clients - -Inject the client like any other Spring bean: - -```java -@Service -@RequiredArgsConstructor -public class MyService { - - private final CalendarClient calendarClient; - private final UsersClient usersClient; - - public void doSomething() { - Calendar calendar = calendarClient.createCalendar(...); - User user = usersClient.createUser(...); - } -} -``` - -## Currently Registered Clients - -The following clients are automatically registered: -- `calendarClient` - CalendarClient -- `usersClient` - UsersClient -- `groupClient` - GroupClient -- `circulationClient` - CirculationClient -- `circulationItemClient` - CirculationItemClient -- `circulationRequestClient` - CirculationRequestClient -- `circulationLoanPolicyStorageClient` - CirculationLoanPolicyStorageClient -- `cancellationReasonClient` - CancellationReasonClient -- `instanceClient` - InstanceClient -- `inventoryItemStorageClient` - InventoryItemStorageClient -- `holdingsStorageClient` - HoldingsStorageClient -- `holdingSourcesClient` - HoldingSourcesClient -- `locationsClient` - LocationsClient -- `locationUnitClient` - LocationUnitClient -- `materialTypeClient` - MaterialTypeClient -- `servicePointClient` - ServicePointClient -- `instanceTypeClient` - InstanceTypeClient -- `loanTypeClient` - LoanTypeClient - -## Technical Details - -The implementation uses: -- **BeanFactoryPostProcessor** - Registers beans during Spring context initialization -- **ClassPathScanningCandidateComponentProvider** - Scans for classes with `@HttpExchange` annotation -- **GenericBeanDefinition** - Creates bean definitions dynamically -- **HttpServiceProxyFactory** - Creates the actual HTTP client proxies - -## Benefits - -1. ✅ **Less Boilerplate** - No need to write `@Bean` methods for each client -2. ✅ **Automatic Discovery** - New clients are automatically registered -3. ✅ **Consistent Naming** - Bean names follow a consistent convention -4. ✅ **Type Safety** - Full compile-time type checking -5. ✅ **Easy Maintenance** - Add new clients by just creating the interface - -## Alternative Approaches - -### Spring Framework 6.1+ (Future) -Spring Framework 6.1+ plans to introduce `@HttpExchangeScan`: - -```java -@Configuration -@HttpExchangeScan(basePackages = "org.folio.dcb.integration") -public class HttpExchangeConfiguration { - // Even simpler! -} -``` - -However, this is not yet available/stable in Spring Boot 4.0.2. - -### Individual Bean Registration -If you need custom configuration for specific clients, you can still use manual registration: - -```java -@Bean -public CustomClient customClient(HttpServiceProxyFactory factory) { - // Custom configuration - return factory.createClient(CustomClient.class); -} -``` - -The automated registration will not interfere with manually defined beans. - diff --git a/README.md b/README.md index 5d1e622c..1bb1e792 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,8 @@ Version 2.0. See the file "[LICENSE](LICENSE)" for more information. * [API documentation](#api-documentation) * [Code analysis](#code-analysis) * [Service Point Hold Shelf Period Expiration](#service-point-hold-shelf-period-expiration) + * [Settings Configuration](#settings-configuration) + * [Table: service_point_expiration_period](#table-service_point_expiration_period) * [Other documentation](#other-documentation) @@ -125,14 +127,73 @@ This module's [API documentation](https://dev.folio.org/reference/api/#mod-dcb). ### Service Point Hold Shelf Period Expiration -When creating a **DCB** transaction with the roles **LENDER** or **BORROWING-PICKUP**, -the creation of the **DCB** service point and its property **hold shelf expiration period** -depends on the values stored in the `service_point_expiration_period` table in the database. +When creating a **DCB** transaction with the roles **LENDER** or **BORROWER**, +the creation of the **DCB** service point and its property **hold shelf expiration period** +depends on the values stored in the `settings` or `service_point_expiration_period` tables in the database. + +#### Settings Configuration + +The following settings are relevant for the hold shelf expiration period and the can be created using: +```text +POST {{gateway}}/dcb/settings + +Headers: + Content-Type: application/json + x-okapi-tenant: {{tenant}} + x-okapi-token: {{token}} + +Request Body: + {{request body (examples below)}} +``` + +- Lender: + ```json + { + "id": "0980d067-ac36-4dc7-ab78-d5c6274ef2bc", + "key": "lender.hold-shelf-expiry-period", + "scope": "mod-dcb", + "value": { + "duration": 10, + "intervalId": "Days" + } + } + ``` + +- Borrower: + ```json + { + "id": "33ef5144-927f-4fd9-b53a-17815054b4e8", + "key": "borrower.hold-shelf-expiry-period", + "scope": "mod-dcb", + "value": { + "duration": 10, + "intervalId": "Days" + } + } + ``` + +- DCB (Virtual) Service Point: + ```json + { + "id": "33ef5144-927f-4fd9-b53a-17815054b4e8", + "key": "dcb.hold-shelf-expiry-period", + "scope": "mod-dcb", + "value": { + "duration": 10, + "intervalId": "Days" + } + } + ``` + +As a fallback, `service_point_expiration_period` table is used to determine the hold shelf expiration period for the +service point. if it's empty, the default value of 10 days will be used. + +#### Table: service_point_expiration_period - If the table is empty, the **hold shelf expiration period** will be set to the default value of **10 Days**. - If the table contains a value, the stored value will be used instead. -The **F.S.E. team** is responsible for updating the values in this table. +The **F.S.E. team** is responsible for updating the values in this table. To update the values, the following PL/pgSQL script can be executed: ```sql @@ -160,15 +221,15 @@ BEGIN -- If no record exists, insert one; otherwise, update the existing record IF raw_id IS NULL THEN sql_query := format( - 'INSERT INTO %I.service_point_expiration_period (id, duration, interval_id) - VALUES (gen_random_uuid(), %L, %L)', + 'INSERT INTO %I.service_point_expiration_period (id, duration, interval_id) + VALUES (gen_random_uuid(), %L, %L)', schema_name, new_duration, new_interval_id ); ELSE sql_query := format( - 'UPDATE %I.service_point_expiration_period + 'UPDATE %I.service_point_expiration_period SET duration = %L, interval_id = %L - WHERE id = %L', + WHERE id = %L', schema_name, new_duration, new_interval_id, raw_id ); END IF; @@ -180,15 +241,15 @@ END; $$ LANGUAGE plpgsql; ``` -**Updating Values in the Table** -To update the values, simply modify the new_duration and new_interval_id variables in the DECLARE section +**Updating Values in the Table** +To update the values, simply modify the new_duration and new_interval_id variables in the DECLARE section of the script to reflect the new values. -**Expiration Period Handling** +**Expiration Period Handling** For Existing Service Points -When creating a new transaction with an existing DCB service point, the hold shelf expiration period -will be checked. -If the value in the transaction payload differs from the value stored +When creating a new transaction with an existing DCB service point, the hold shelf expiration period +will be checked. +If the value in the transaction payload differs from the value stored in the database, it will be updated accordingly. [SonarQube analysis](https://sonarcloud.io/project/overview?id=org.folio:mod-dcb). diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index fea28cf7..b34139f1 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -310,6 +310,37 @@ } ] }, + { + "id": "dcb.settings", + "version": "1.0", + "handlers": [ + { + "methods": ["GET"], + "pathPattern": "/dcb/settings", + "permissionsRequired": ["dcb.settings.collection.get"] + }, + { + "methods": ["GET"], + "pathPattern": "/dcb/settings/{id}", + "permissionsRequired": ["dcb.settings.item.get"] + }, + { + "methods": ["POST"], + "pathPattern": "/dcb/settings", + "permissionsRequired": ["dcb.settings.item.post"] + }, + { + "methods": ["PUT"], + "pathPattern": "/dcb/settings/{id}", + "permissionsRequired": ["dcb.settings.item.put"] + }, + { + "methods": ["DELETE"], + "pathPattern": "/dcb/settings/{id}", + "permissionsRequired": ["dcb.settings.item.delete"] + } + ] + }, { "id": "_tenant", "version": "2.0", @@ -365,6 +396,7 @@ "displayName": "DCB module - all permissions", "description": "All permissions for dcb module", "subPermissions": [ + "dcb.settings.all", "dcb.transactions.post", "dcb.transactions.status.put", "dcb.transactions.status.get", @@ -433,6 +465,43 @@ "permissionName": "dcb.shadow_locations.refresh.post", "displayName": "trigger refresh of shadow locations", "description": "trigger refresh of shadow locations" + }, + { + "permissionName" : "dcb.settings.collection.get", + "displayName" : "mod-dcb - retrieve settings by cql", + "description" : "Get a collection of settings" + }, + { + "permissionName" : "dcb.settings.item.get", + "displayName" : "mod-dcb - retrieve a setting by id", + "description" : "Fetch a setting" + }, + { + "permissionName" : "dcb.settings.item.put", + "displayName" : "mod-dcb - Update a setting by id", + "description" : "Update a setting" + }, + { + "permissionName" : "dcb.settings.item.post", + "displayName" : "mod-dcb - Save a setting", + "description" : "Add a setting" + }, + { + "permissionName" : "dcb.settings.item.delete", + "displayName" : "mod-dcb - Remove a setting by id", + "description" : "Delete a setting" + }, + { + "permissionName" : "dcb.settings.all", + "displayName" : "mod-dcb - settings all", + "description" : "All permissions for setting", + "subPermissions" : [ + "dcb.settings.collection.get", + "dcb.settings.item.get", + "dcb.settings.item.post", + "dcb.settings.item.put", + "dcb.settings.item.delete" + ] } ], "metadata": { diff --git a/pom.xml b/pom.xml index 16c28f4d..4f3159ff 100644 --- a/pom.xml +++ b/pom.xml @@ -80,6 +80,11 @@ folio-spring-system-user ${folio-spring-support.version} + + org.folio + folio-spring-cql + ${folio-spring-support.version} + org.springframework.boot spring-boot-starter-actuator diff --git a/src/main/java/org/folio/dcb/config/JpaAuditingConfig.java b/src/main/java/org/folio/dcb/config/JpaAuditingConfig.java index 077c0e04..f69e298b 100644 --- a/src/main/java/org/folio/dcb/config/JpaAuditingConfig.java +++ b/src/main/java/org/folio/dcb/config/JpaAuditingConfig.java @@ -1,5 +1,6 @@ package org.folio.dcb.config; +import java.time.temporal.ChronoUnit; import lombok.RequiredArgsConstructor; import org.folio.spring.FolioExecutionContext; import org.springframework.context.annotation.Bean; @@ -13,8 +14,8 @@ import java.util.UUID; @Configuration -@EnableJpaAuditing(dateTimeProviderRef = "dateTimeProvider", modifyOnCreate = false) @RequiredArgsConstructor +@EnableJpaAuditing(dateTimeProviderRef = "dateTimeProvider") public class JpaAuditingConfig implements AuditorAware{ private final FolioExecutionContext folioExecutionContext; @@ -26,7 +27,6 @@ public Optional getCurrentAuditor() { @Bean public DateTimeProvider dateTimeProvider() { - return () -> Optional.of(OffsetDateTime.now()); + return () -> Optional.of(OffsetDateTime.now().truncatedTo(ChronoUnit.MICROS)); } - } diff --git a/src/main/java/org/folio/dcb/controller/SettingsController.java b/src/main/java/org/folio/dcb/controller/SettingsController.java new file mode 100644 index 00000000..a6898465 --- /dev/null +++ b/src/main/java/org/folio/dcb/controller/SettingsController.java @@ -0,0 +1,50 @@ +package org.folio.dcb.controller; + +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import org.folio.dcb.domain.dto.Setting; +import org.folio.dcb.domain.dto.SettingsCollection; +import org.folio.dcb.rest.resource.SettingApi; +import org.folio.dcb.service.SettingService; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class SettingsController implements SettingApi { + + private final SettingService settingService; + + @Override + public ResponseEntity createDcbSetting(Setting setting) { + var createdSetting = settingService.createSetting(setting); + return ResponseEntity.status(HttpStatus.CREATED).body(createdSetting); + } + + @Override + public ResponseEntity findDcbSettings(String query, Integer limit, Integer offset) { + var foundSettings = settingService.findByQuery(query, limit, offset); + return ResponseEntity.ok(new SettingsCollection() + .items(foundSettings.getResult()) + .totalRecords(foundSettings.getTotalRecords())); + } + + @Override + public ResponseEntity getDcbSettingById(UUID id) { + var settingById = settingService.getSettingById(id); + return ResponseEntity.ok(settingById); + } + + @Override + public ResponseEntity deleteDcbSettingById(UUID id) { + settingService.deleteSettingById(id); + return ResponseEntity.noContent().build(); + } + + @Override + public ResponseEntity updateDcbSettingById(UUID id, Setting setting) { + settingService.updateSetting(setting); + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/org/folio/dcb/domain/entity/SettingEntity.java b/src/main/java/org/folio/dcb/domain/entity/SettingEntity.java new file mode 100644 index 00000000..a07a0bd2 --- /dev/null +++ b/src/main/java/org/folio/dcb/domain/entity/SettingEntity.java @@ -0,0 +1,50 @@ +package org.folio.dcb.domain.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.PrePersist; +import jakarta.persistence.Table; +import jakarta.persistence.Transient; +import jakarta.persistence.Version; +import java.util.UUID; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.folio.dcb.domain.entity.base.AuditableEntity; +import org.hibernate.annotations.ColumnTransformer; +import org.springframework.data.domain.Persistable; + +@Data +@Entity +@Table(name = "settings") +@EqualsAndHashCode(callSuper = true) +public class SettingEntity extends AuditableEntity implements Persistable { + + @Id + private UUID id; + + @Column(nullable = false) + private String key; + + @Column + private String scope; + + @Column(columnDefinition = "jsonb") + @ColumnTransformer(write = "?::jsonb") + private String value; + + @Column + @Version + private Integer version; + + @Override + @Transient + public boolean isNew() { + return version == null; + } + + @PrePersist + private void initVersion() { + version = 1; + } +} diff --git a/src/main/java/org/folio/dcb/domain/entity/base/AuditableEntity.java b/src/main/java/org/folio/dcb/domain/entity/base/AuditableEntity.java index e6e392a6..063277ec 100644 --- a/src/main/java/org/folio/dcb/domain/entity/base/AuditableEntity.java +++ b/src/main/java/org/folio/dcb/domain/entity/base/AuditableEntity.java @@ -4,9 +4,7 @@ import jakarta.persistence.Column; import jakarta.persistence.EntityListeners; import jakarta.persistence.MappedSuperclass; -import lombok.Getter; -import lombok.Setter; -import lombok.ToString; +import lombok.Data; import org.springframework.data.annotation.CreatedBy; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedBy; @@ -16,28 +14,42 @@ import java.time.OffsetDateTime; import java.util.UUID; -@Getter -@Setter -@ToString +/** + * Auditable is a mapped superclass that contains fields for auditing and the corresponding annotations for JPA + * auditing. + */ +@Data @MappedSuperclass @EntityListeners(AuditingEntityListener.class) public abstract class AuditableEntity { + /** + * Indicates the date the entity was created. + */ @JsonIgnore @CreatedDate @Column(name = "created_date", nullable = false, updatable = false) private OffsetDateTime createdDate; + /** + * Indicates the user who created the entity. + */ @JsonIgnore @CreatedBy @Column(name = "created_by", updatable = false) private UUID createdBy; + /** + * Indicates the date the entity was last modified. + */ @JsonIgnore @LastModifiedDate @Column(name = "updated_date") private OffsetDateTime updatedDate; + /** + * Indicates the user who last modified the entity. + */ @JsonIgnore @LastModifiedBy @Column(name = "updated_by") diff --git a/src/main/java/org/folio/dcb/domain/mapper/SettingMapper.java b/src/main/java/org/folio/dcb/domain/mapper/SettingMapper.java new file mode 100644 index 00000000..edb01b1e --- /dev/null +++ b/src/main/java/org/folio/dcb/domain/mapper/SettingMapper.java @@ -0,0 +1,48 @@ +package org.folio.dcb.domain.mapper; + +import org.apache.commons.lang3.Strings; +import org.folio.dcb.domain.dto.Setting; +import org.folio.dcb.domain.dto.SettingScope; +import org.folio.dcb.domain.entity.SettingEntity; +import org.mapstruct.InjectionStrategy; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.springframework.beans.factory.annotation.Autowired; +import tools.jackson.databind.json.JsonMapper; + +@Mapper(componentModel = "spring", injectionStrategy = InjectionStrategy.CONSTRUCTOR) +public abstract class SettingMapper { + + protected JsonMapper jsonMapper; + + @Mapping(target = "value", expression = "java(jsonMapper.readTree(entity.getValue()))") + @Mapping(target = "scope", expression = "java(parseSettingScopeFromString(entity.getScope()))") + @Mapping(target = "metadata.createdDate", source = "entity.createdDate") + @Mapping(target = "metadata.createdByUserId", source = "entity.createdBy") + @Mapping(target = "metadata.updatedDate", source = "entity.updatedDate") + @Mapping(target = "metadata.updatedByUserId", source = "entity.updatedBy") + public abstract Setting convert(SettingEntity entity); + + @Mapping(target = "createdDate", ignore = true) + @Mapping(target = "createdBy", ignore = true) + @Mapping(target = "updatedDate", ignore = true) + @Mapping(target = "updatedBy", ignore = true) + @Mapping(target = "scope", expression = "java(dto.getScope() != null ? dto.getScope().getValue() : null)") + @Mapping(target = "value", expression = "java(jsonMapper.writeValueAsString(dto.getValue()))") + public abstract SettingEntity convert(Setting dto); + + @Autowired + private void setJsonMapper(JsonMapper jsonMapper) { + this.jsonMapper = jsonMapper; + } + + public static SettingScope parseSettingScopeFromString(String scope) { + for (var value : SettingScope.values()) { + if (Strings.CI.equals(value.getValue(), scope)) { + return value; + } + } + + return null; + } +} diff --git a/src/main/java/org/folio/dcb/integration/kafka/CirculationEventListener.java b/src/main/java/org/folio/dcb/integration/kafka/CirculationEventListener.java index da291638..dbffeaaf 100644 --- a/src/main/java/org/folio/dcb/integration/kafka/CirculationEventListener.java +++ b/src/main/java/org/folio/dcb/integration/kafka/CirculationEventListener.java @@ -40,9 +40,6 @@ public class CirculationEventListener { private final SystemUserScopedExecutionService systemUserScopedExecutionService; private final BaseLibraryService baseLibraryService; - @Qualifier("lendingLibraryService") - private final LibraryService lendingLibraryService; - @KafkaListener( id = CHECK_OUT_LOAN_LISTENER_ID, topicPattern = "#{folioKafkaProperties.listener['loan'].topicPattern}", @@ -125,9 +122,8 @@ private void handleCirculationCheckInEvent(String tenantId, EventData eventData) systemUserScopedExecutionService.executeAsyncSystemUserScoped(tenantId, () -> { var itemUuid = UUID.fromString(eventData.getItemId()); - transactionRepository.findExpiredLenderTransactionsByItemId(itemUuid).forEach(entity -> - ((LendingLibraryServiceImpl) lendingLibraryService) - .closeExpiredTransactionEntity(entity, eventData.getCheckInServicePointId())); + transactionRepository.findExpiredTransactionsByItemId(itemUuid).forEach(entity -> + baseLibraryService.closeExpiredTransactionEntity(entity, eventData.getCheckInServicePointId())); }); } @@ -156,7 +152,7 @@ private void handleRequestEvent(TransactionEntity transactionEntity, EventData e baseLibraryService.updateTransactionEntity(transactionEntity, TransactionStatus.StatusEnum.OPEN); } else if (type == EventData.EventType.AWAITING_PICKUP && (role == BORROWING_PICKUP || role == PICKUP)) { baseLibraryService.updateTransactionEntity(transactionEntity, TransactionStatus.StatusEnum.AWAITING_PICKUP); - } else if (type == EventData.EventType.EXPIRED && role == LENDER) { + } else if (type == EventData.EventType.EXPIRED) { baseLibraryService.updateTransactionEntity(transactionEntity, TransactionStatus.StatusEnum.EXPIRED); } else { log.info("handleRequestEvent:: status for event {} can not be updated", eventData); diff --git a/src/main/java/org/folio/dcb/repository/SettingRepository.java b/src/main/java/org/folio/dcb/repository/SettingRepository.java new file mode 100644 index 00000000..e4a7cbe8 --- /dev/null +++ b/src/main/java/org/folio/dcb/repository/SettingRepository.java @@ -0,0 +1,33 @@ +package org.folio.dcb.repository; + +import static org.apache.commons.lang3.StringUtils.isBlank; + +import java.util.UUID; +import org.folio.dcb.domain.entity.SettingEntity; +import org.folio.spring.cql.JpaCqlRepository; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Repository; + +@Repository +public interface SettingRepository extends JpaCqlRepository { + + /** + * Returns all records if query is empty or searches using it otherwise. + * + * @param query - CQL query as {@link String} + * @param pageable - {@link Pageable} object for pagination + * @return {@link Page} containing {@link SettingEntity} records + */ + default Page findByQuery(String query, Pageable pageable) { + return isBlank(query) ? findAll(pageable) : findByCql(query, pageable); + } + + /** + * Checks if record with the specified key exists. + * + * @param key - the key to check for existence + * @return true if a record with the specified key exists, false otherwise + */ + boolean existsByKey(String key); +} diff --git a/src/main/java/org/folio/dcb/repository/TransactionRepository.java b/src/main/java/org/folio/dcb/repository/TransactionRepository.java index ff6f0d00..109aea89 100644 --- a/src/main/java/org/folio/dcb/repository/TransactionRepository.java +++ b/src/main/java/org/folio/dcb/repository/TransactionRepository.java @@ -18,8 +18,8 @@ public interface TransactionRepository extends JpaRepository findTransactionsByItemIdAndStatusNotInClosed(@Param("itemId") UUID itemId); - @Query(value = "SELECT * FROM transactions where item_id = :itemId AND status = 'EXPIRED' AND role = 'LENDER'", nativeQuery = true) - List findExpiredLenderTransactionsByItemId(@Param("itemId") UUID itemId); + @Query(value = "SELECT * FROM transactions where item_id = :itemId AND status = 'EXPIRED'", nativeQuery = true) + List findExpiredTransactionsByItemId(@Param("itemId") UUID itemId); @Query(value = "SELECT * FROM transactions where item_id = :itemId AND status not in ('CLOSED', 'CANCELLED', 'ERROR')", nativeQuery = true) Optional findSingleTransactionsByItemIdAndStatusNotInClosed(@Param("itemId") UUID itemId); diff --git a/src/main/java/org/folio/dcb/service/ServicePointExpirationPeriodService.java b/src/main/java/org/folio/dcb/service/ServicePointExpirationPeriodService.java index 107fe9ac..fab495b4 100644 --- a/src/main/java/org/folio/dcb/service/ServicePointExpirationPeriodService.java +++ b/src/main/java/org/folio/dcb/service/ServicePointExpirationPeriodService.java @@ -3,5 +3,22 @@ import org.folio.dcb.domain.dto.HoldShelfExpiryPeriod; public interface ServicePointExpirationPeriodService { - HoldShelfExpiryPeriod getShelfExpiryPeriod(); + + /** + * Retrieves the hold shelf expiry period for a given settings key. + * + * @param settingKey the settings key for the service point + * @return the hold shelf expiry period + */ + HoldShelfExpiryPeriod getShelfExpiryPeriod(String settingKey); + + /** + * Constructs the settings key for a service point using the given prefix. + * + * @param prefix the prefix for the service point + * @return the settings key in the format "{prefix}.hold-shelf-expiry-period" + */ + static String getSettingsKey(String prefix) { + return "%s.hold-shelf-expiry-period".formatted(prefix.toLowerCase()); + } } diff --git a/src/main/java/org/folio/dcb/service/ServicePointService.java b/src/main/java/org/folio/dcb/service/ServicePointService.java index 0ec9e5ec..72ec5b9e 100644 --- a/src/main/java/org/folio/dcb/service/ServicePointService.java +++ b/src/main/java/org/folio/dcb/service/ServicePointService.java @@ -1,8 +1,8 @@ package org.folio.dcb.service; -import org.folio.dcb.domain.dto.DcbPickup; +import org.folio.dcb.domain.dto.DcbTransaction; import org.folio.dcb.domain.dto.ServicePointRequest; public interface ServicePointService { - ServicePointRequest createServicePointIfNotExists(DcbPickup pickupServicePoint); + ServicePointRequest createServicePointIfNotExists(DcbTransaction dcbTransaction); } diff --git a/src/main/java/org/folio/dcb/service/SettingService.java b/src/main/java/org/folio/dcb/service/SettingService.java new file mode 100644 index 00000000..27fbaf74 --- /dev/null +++ b/src/main/java/org/folio/dcb/service/SettingService.java @@ -0,0 +1,111 @@ +package org.folio.dcb.service; + +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.folio.dcb.domain.ResultList; +import org.folio.dcb.domain.dto.Setting; +import org.folio.dcb.domain.dto.SettingsCollection; +import org.folio.dcb.domain.mapper.SettingMapper; +import org.folio.dcb.repository.SettingRepository; +import org.folio.spring.data.OffsetRequest; +import org.folio.spring.exception.NotFoundException; +import org.springframework.beans.BeanUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Log4j2 +@Service +@RequiredArgsConstructor +public class SettingService { + + private final SettingMapper settingMapper; + private final SettingRepository settingRepository; + + /** + * Create and persist a new {@link Setting} object. + * + * @param setting setting DTO to create + * @return created setting DTO with any generated fields populated (for example id) + */ + @Transactional + public Setting createSetting(Setting setting) { + if (settingRepository.existsById(setting.getId())) { + throw new IllegalArgumentException("Setting already exists with id: " + setting.getId()); + } + + if (settingRepository.existsByKey(setting.getKey())) { + log.debug("createSetting:: Setting already exists by key: {}", setting.getKey()); + throw new IllegalArgumentException("Setting with key already exists: " + setting.getKey()); + } + + var entity = settingMapper.convert(setting.version(null)); + var savedEntity = settingRepository.save(entity); + return settingMapper.convert(savedEntity); + } + + /** + * Retrieve a {@link Setting} object by its identifier. + * + * @param id UUID of the requested setting + * @return the setting DTO + * @throws NotFoundException when no setting with the given id exists + */ + @Transactional(readOnly = true) + public Setting getSettingById(UUID id) { + return settingRepository.findById(id) + .map(settingMapper::convert) + .orElseThrow(() -> new NotFoundException("Setting not found by id: " + id)); + } + + /** + * Find {@link Setting} objects by a query string with pagination. + * + * @param query query string used to filter settings + * @param limit maximum number of items to return + * @param offset zero-based offset into the result set + * @return {@link SettingsCollection} containing the matched settings and total record information + */ + @Transactional(readOnly = true) + public ResultList findByQuery(String query, int limit, int offset) { + var pageable = OffsetRequest.of(offset, limit); + var foundSettings = settingRepository.findByQuery(query, pageable).map(settingMapper::convert); + return ResultList.of((int) foundSettings.getTotalElements(), foundSettings.getContent()); + } + + /** + * Update an existing {@link Setting}. The setting must already exist. + * + * @param updatedSetting setting DTO with updated values + * @throws NotFoundException when no setting with the given id exists + */ + @Transactional + public void updateSetting(Setting updatedSetting) { + var existingEntity = settingRepository.findById(updatedSetting.getId()) + .orElseThrow(() -> new NotFoundException("Setting not found by id: " + updatedSetting.getId())); + + if (!existingEntity.getKey().equals(updatedSetting.getKey())) { + throw new IllegalArgumentException("Setting key cannot be modified: " + existingEntity.getKey()); + } + + var updatedEntity = settingMapper.convert(updatedSetting); + BeanUtils.copyProperties(updatedEntity, existingEntity, "id", "key", "metadata"); + settingRepository.save(existingEntity); + log.debug("updateSetting:: Setting was updated for key: {}", updatedEntity.getKey()); + } + + /** + * Delete a {@link Setting} by its id. + * + * @param settingId id of the setting to delete + * @throws NotFoundException when no setting with the given id exists + */ + @Transactional + public void deleteSettingById(UUID settingId) { + var entityToDelete = settingRepository.findById(settingId) + .orElseThrow(() -> new NotFoundException("Setting not found by id: " + settingId)); + + settingRepository.deleteById(settingId); + log.debug("deleteSettingById:: Setting was deleted for key: {}", entityToDelete.getKey()); + } +} diff --git a/src/main/java/org/folio/dcb/service/entities/DcbServicePointService.java b/src/main/java/org/folio/dcb/service/entities/DcbServicePointService.java index 5d0346ef..a68e48d6 100644 --- a/src/main/java/org/folio/dcb/service/entities/DcbServicePointService.java +++ b/src/main/java/org/folio/dcb/service/entities/DcbServicePointService.java @@ -1,5 +1,6 @@ package org.folio.dcb.service.entities; +import static org.folio.dcb.service.ServicePointExpirationPeriodService.getSettingsKey; import static org.folio.dcb.service.impl.ServicePointServiceImpl.HOLD_SHELF_CLOSED_LIBRARY_DATE_MANAGEMENT; import static org.folio.dcb.utils.CqlQuery.exactMatchByName; import static org.folio.dcb.utils.DCBConstants.CODE; @@ -33,7 +34,7 @@ public Optional findDcbEntity() { @Override public ServicePointRequest createDcbEntity() { log.debug("createDcbEntity:: Creating a new DCB Service Point"); - var shelfExpiryPeriod = servicePointExpirationPeriodService.getShelfExpiryPeriod(); + var shelfExpiryPeriod = servicePointExpirationPeriodService.getShelfExpiryPeriod(getSettingsKey("dcb")); var dcbServicePoint = getDcbServicePoint(shelfExpiryPeriod); var createdServicePoint = servicePointClient.createServicePoint(dcbServicePoint); log.info("createDcbEntity:: DCB Service Point created"); diff --git a/src/main/java/org/folio/dcb/service/impl/BaseLibraryService.java b/src/main/java/org/folio/dcb/service/impl/BaseLibraryService.java index 51ecebf1..ae207008 100644 --- a/src/main/java/org/folio/dcb/service/impl/BaseLibraryService.java +++ b/src/main/java/org/folio/dcb/service/impl/BaseLibraryService.java @@ -3,17 +3,21 @@ import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.ObjectUtils; import org.folio.dcb.domain.dto.CirculationItem; import org.folio.dcb.domain.dto.CirculationRequest; import org.folio.dcb.domain.dto.DcbItem; import org.folio.dcb.domain.dto.DcbPatron; import org.folio.dcb.domain.dto.DcbTransaction; +import org.folio.dcb.domain.dto.DcbTransaction.RoleEnum; import org.folio.dcb.domain.dto.DcbUpdateItem; +import org.folio.dcb.domain.dto.ItemStatus; import org.folio.dcb.domain.dto.TransactionStatus; import org.folio.dcb.domain.dto.TransactionStatusResponse; import org.folio.dcb.domain.entity.TransactionEntity; import org.folio.dcb.domain.mapper.TransactionMapper; import org.folio.dcb.exception.CirculationRequestException; +import org.folio.dcb.exception.InventoryItemNotFound; import org.folio.dcb.exception.ResourceAlreadyExistException; import org.folio.dcb.repository.TransactionRepository; import org.folio.dcb.service.CirculationItemService; @@ -27,6 +31,8 @@ import java.util.Optional; import java.util.UUID; +import static org.folio.dcb.domain.dto.ItemStatus.NameEnum.AVAILABLE; +import static org.folio.dcb.domain.dto.ItemStatus.NameEnum.IN_TRANSIT; import static org.folio.dcb.domain.dto.TransactionStatus.StatusEnum.CANCELLED; import static org.folio.dcb.domain.dto.TransactionStatus.StatusEnum.CLOSED; import static org.folio.dcb.domain.dto.TransactionStatus.StatusEnum.CREATED; @@ -162,6 +168,34 @@ public void updateTransactionDetails(TransactionEntity transactionEntity, DcbUpd updateItemDetailsAndSaveEntity(transactionEntity, item, dcbItem.getMaterialType(), holdRequest.getId()); } + /** + * Closes the expired transaction entity if the associated item is available. + * + * @param dcbTransaction the DCB transaction entity to be closed + * @param expectedServicePointId the expected service point ID, used in logging + */ + public void closeExpiredTransactionEntity(TransactionEntity dcbTransaction, String expectedServicePointId) { + var itemId = dcbTransaction.getItemId(); + var role = dcbTransaction.getRole(); + if (role != RoleEnum.LENDER) { + log.debug("closeTransactionEntityIfItemIsAvailable:: closing expired transaction: {}", dcbTransaction.getId()); + updateTransactionEntity(dcbTransaction, TransactionStatus.StatusEnum.CLOSED); + return; + } + + try { + var inventoryItem = itemService.findItemByIdAfterCheckIn(itemId, expectedServicePointId); + var itemStatus = inventoryItem.getStatus(); + if (itemStatus != null && Objects.equals(itemStatus.getName(), AVAILABLE)) { + log.debug("closeTransactionEntityIfItemIsAvailable:: closing expired transaction: {}", dcbTransaction.getId()); + updateTransactionEntity(dcbTransaction, TransactionStatus.StatusEnum.CLOSED); + } + } catch (InventoryItemNotFound exception) { + log.warn("closeExpiredTransactionEntity:: Failed to fetch item with id {} after check-in: {}", + itemId, expectedServicePointId, exception); + } + } + private void updateItemDetailsAndSaveEntity(TransactionEntity transactionEntity, CirculationItem item, String materialType, String requestId) { transactionEntity.setItemId(item.getId()); diff --git a/src/main/java/org/folio/dcb/service/impl/BorrowingLibraryServiceImpl.java b/src/main/java/org/folio/dcb/service/impl/BorrowingLibraryServiceImpl.java index 798fea01..2047110b 100644 --- a/src/main/java/org/folio/dcb/service/impl/BorrowingLibraryServiceImpl.java +++ b/src/main/java/org/folio/dcb/service/impl/BorrowingLibraryServiceImpl.java @@ -33,7 +33,7 @@ public class BorrowingLibraryServiceImpl implements LibraryService { private final ServicePointService servicePointService; @Override public TransactionStatusResponse createCirculation(String dcbTransactionId, DcbTransaction dcbTransaction) { - ServicePointRequest pickupServicePoint = servicePointService.createServicePointIfNotExists(dcbTransaction.getPickup()); + ServicePointRequest pickupServicePoint = servicePointService.createServicePointIfNotExists(dcbTransaction); dcbTransaction.getPickup().setServicePointId(pickupServicePoint.getId()); return libraryService.createBorrowingLibraryTransaction(dcbTransactionId, dcbTransaction, pickupServicePoint.getId()); } diff --git a/src/main/java/org/folio/dcb/service/impl/LendingLibraryServiceImpl.java b/src/main/java/org/folio/dcb/service/impl/LendingLibraryServiceImpl.java index 2fa5eb5a..99f4c77c 100644 --- a/src/main/java/org/folio/dcb/service/impl/LendingLibraryServiceImpl.java +++ b/src/main/java/org/folio/dcb/service/impl/LendingLibraryServiceImpl.java @@ -1,6 +1,5 @@ package org.folio.dcb.service.impl; -import java.util.Objects; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.folio.dcb.domain.dto.CirculationRequest; @@ -9,17 +8,14 @@ import org.folio.dcb.domain.dto.TransactionStatus; import org.folio.dcb.domain.dto.TransactionStatusResponse; import org.folio.dcb.domain.entity.TransactionEntity; -import org.folio.dcb.exception.InventoryItemNotFound; import org.folio.dcb.repository.TransactionRepository; import org.folio.dcb.service.CirculationService; -import org.folio.dcb.service.ItemService; import org.folio.dcb.service.LibraryService; import org.folio.dcb.service.RequestService; import org.folio.dcb.service.ServicePointService; import org.folio.dcb.service.UserService; import org.springframework.stereotype.Service; -import static org.folio.dcb.domain.dto.ItemStatus.NameEnum.AVAILABLE; import static org.folio.dcb.domain.dto.TransactionStatus.StatusEnum.AWAITING_PICKUP; import static org.folio.dcb.domain.dto.TransactionStatus.StatusEnum.CREATED; import static org.folio.dcb.domain.dto.TransactionStatus.StatusEnum.ITEM_CHECKED_IN; @@ -38,7 +34,6 @@ public class LendingLibraryServiceImpl implements LibraryService { private final CirculationService circulationService; private final BaseLibraryService baseLibraryService; private final ServicePointService servicePointService; - private final ItemService itemService; @Override public TransactionStatusResponse createCirculation(String dcbTransactionId, DcbTransaction dcbTransaction) { @@ -49,7 +44,7 @@ public TransactionStatusResponse createCirculation(String dcbTransactionId, DcbT var patron = dcbTransaction.getPatron(); var user = userService.fetchOrCreateUser(patron); - ServicePointRequest pickupServicePoint = servicePointService.createServicePointIfNotExists(dcbTransaction.getPickup()); + ServicePointRequest pickupServicePoint = servicePointService.createServicePointIfNotExists(dcbTransaction); dcbTransaction.getPickup().setServicePointId(pickupServicePoint.getId()); CirculationRequest pageRequest = requestService.createRequestBasedOnItemStatus(user, item, pickupServicePoint.getId()); baseLibraryService.saveDcbTransaction(dcbTransactionId, dcbTransaction, pageRequest.getId()); @@ -89,26 +84,6 @@ public void updateTransactionStatus(TransactionEntity dcbTransaction, Transactio } } - /** - * Closes the expired transaction entity if the associated item is available. - * - * @param dcbTransaction the DCB transaction entity to be closed - * @param expectedServicePointId the expected service point ID, used in logging - */ - public void closeExpiredTransactionEntity(TransactionEntity dcbTransaction, String expectedServicePointId) { - var itemId = dcbTransaction.getItemId(); - try { - var inventoryItem = itemService.findItemByIdAfterCheckIn(itemId, expectedServicePointId); - var itemStatus = inventoryItem.getStatus(); - if (itemStatus != null && Objects.equals(itemStatus.getName(), AVAILABLE)) { - log.debug("closeTransactionEntityIfItemIsAvailable:: closing expired transaction: {}", dcbTransaction.getId()); - updateTransactionEntity(dcbTransaction, TransactionStatus.StatusEnum.CLOSED); - } - } catch (InventoryItemNotFound e) { - log.warn("Failed to fetch item with id {} from inventory after check-in: {}", itemId, expectedServicePointId, e); - } - } - private void updateTransactionEntity (TransactionEntity transactionEntity, TransactionStatus.StatusEnum transactionStatusEnum) { log.info("updateTransactionEntity:: updating transaction entity from {} to {}", transactionEntity.getStatus(), transactionStatusEnum); transactionEntity.setStatus(transactionStatusEnum); diff --git a/src/main/java/org/folio/dcb/service/impl/ServicePointExpirationPeriodServiceImpl.java b/src/main/java/org/folio/dcb/service/impl/ServicePointExpirationPeriodServiceImpl.java index 5d476aca..d39c3bb3 100644 --- a/src/main/java/org/folio/dcb/service/impl/ServicePointExpirationPeriodServiceImpl.java +++ b/src/main/java/org/folio/dcb/service/impl/ServicePointExpirationPeriodServiceImpl.java @@ -1,37 +1,65 @@ package org.folio.dcb.service.impl; +import static org.apache.commons.collections4.ListUtils.emptyIfNull; +import static org.apache.commons.lang3.ObjectUtils.allNotNull; import static org.folio.dcb.utils.DCBConstants.DEFAULT_PERIOD; import java.util.List; - +import java.util.Objects; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; import org.folio.dcb.domain.dto.HoldShelfExpiryPeriod; +import org.folio.dcb.domain.dto.Setting; import org.folio.dcb.domain.entity.ServicePointExpirationPeriodEntity; import org.folio.dcb.repository.ServicePointExpirationPeriodRepository; import org.folio.dcb.service.ServicePointExpirationPeriodService; +import org.folio.dcb.service.SettingService; +import org.folio.dcb.utils.CqlQuery; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; +import tools.jackson.databind.json.JsonMapper; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -@Slf4j @RequiredArgsConstructor +@Log4j2 @Service -public class ServicePointExpirationPeriodServiceImpl - implements ServicePointExpirationPeriodService { +@RequiredArgsConstructor +public class ServicePointExpirationPeriodServiceImpl implements ServicePointExpirationPeriodService { + private final JsonMapper jsonMapper; + private final SettingService settingService; private final ServicePointExpirationPeriodRepository servicePointExpirationPeriodRepository; @Override - public HoldShelfExpiryPeriod getShelfExpiryPeriod() { + @Transactional(readOnly = true) + public HoldShelfExpiryPeriod getShelfExpiryPeriod(String settingKey) { + var settingsByKey = settingService.findByQuery(CqlQuery.exactMatch("key", settingKey).getQuery(), 1, 0); + return emptyIfNull(settingsByKey.getResult()).stream() + .filter(Objects::nonNull) + .findFirst() + .flatMap(this::extractHoldShelfExpiryPeriod) + .orElseGet(this::findShelfExpiryPeriodInDatabase); + } + + private Optional extractHoldShelfExpiryPeriod(Setting setting) { + try { + var convertedValue = jsonMapper.convertValue(setting.getValue(), HoldShelfExpiryPeriod.class); + return Optional.ofNullable(convertedValue) + .filter(value -> allNotNull(value.getDuration(), value.getIntervalId())); + } catch (Exception e) { + log.warn("extractHoldShelfExpiryPeriod:: failed to extract setting value: {}", setting, e); + return Optional.empty(); + } + } + + private HoldShelfExpiryPeriod findShelfExpiryPeriodInDatabase() { List periodList = servicePointExpirationPeriodRepository.findAll(); if (CollectionUtils.isEmpty(periodList)) { - log.info("getShelfExpiryPeriod:: default hold shelf expire period will be set: {}", - DEFAULT_PERIOD); + log.debug("getShelfExpiryPeriod:: default hold shelf expire period will be set: {}", DEFAULT_PERIOD); return DEFAULT_PERIOD; } else { - var customPeriod = getCustomPeriod(periodList.get(0)); - log.info("getShelfExpiryPeriod:: custom hold shelf expire period will be set: {}", - customPeriod); + var customPeriod = getCustomPeriod(periodList.getFirst()); + log.debug("getShelfExpiryPeriod:: custom hold shelf expire period will be set: {}", customPeriod); return customPeriod; } } diff --git a/src/main/java/org/folio/dcb/service/impl/ServicePointServiceImpl.java b/src/main/java/org/folio/dcb/service/impl/ServicePointServiceImpl.java index 183a857f..77043672 100644 --- a/src/main/java/org/folio/dcb/service/impl/ServicePointServiceImpl.java +++ b/src/main/java/org/folio/dcb/service/impl/ServicePointServiceImpl.java @@ -1,8 +1,11 @@ package org.folio.dcb.service.impl; +import static org.folio.dcb.service.ServicePointExpirationPeriodService.getSettingsKey; + import java.util.UUID; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; +import org.folio.dcb.domain.dto.DcbTransaction; import org.folio.dcb.integration.invstorage.ServicePointClient; import org.folio.dcb.domain.dto.DcbPickup; import org.folio.dcb.domain.dto.HoldShelfExpiryPeriod; @@ -24,13 +27,15 @@ public class ServicePointServiceImpl implements ServicePointService { public static final String HOLD_SHELF_CLOSED_LIBRARY_DATE_MANAGEMENT = "Keep_the_current_due_date"; @Override - public ServicePointRequest createServicePointIfNotExists(DcbPickup pickupServicePoint) { + public ServicePointRequest createServicePointIfNotExists(DcbTransaction dcbTransaction) { + var pickupServicePoint = dcbTransaction.getPickup(); log.debug("createServicePoint:: automate service point creation {} ", pickupServicePoint); String servicePointName = getServicePointName(pickupServicePoint.getLibraryCode(), pickupServicePoint.getServicePointName()); var query = CqlQuery.exactMatchByName(servicePointName).getQuery(); var servicePointRequestList = servicePointClient.findByQuery(query).getResult(); - var shelfExpiryPeriod = servicePointExpirationPeriodService.getShelfExpiryPeriod(); + var settingsKey = getSettingsKey(dcbTransaction.getRole().getValue()); + var shelfExpiryPeriod = servicePointExpirationPeriodService.getShelfExpiryPeriod(settingsKey); if (servicePointRequestList.isEmpty()) { String servicePointId = UUID.randomUUID().toString(); String servicePointCode = getServicePointCode(pickupServicePoint.getLibraryCode(), diff --git a/src/main/resources/db/changelog/changelog-master.xml b/src/main/resources/db/changelog/changelog-master.xml index 667e2b7f..dabde8e9 100644 --- a/src/main/resources/db/changelog/changelog-master.xml +++ b/src/main/resources/db/changelog/changelog-master.xml @@ -14,5 +14,6 @@ + diff --git a/src/main/resources/db/changelog/changes/add-settings-table.xml b/src/main/resources/db/changelog/changes/add-settings-table.xml new file mode 100644 index 00000000..a13c8f75 --- /dev/null +++ b/src/main/resources/db/changelog/changes/add-settings-table.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/swagger.api/dcb_transaction.yaml b/src/main/resources/swagger.api/dcb_transaction.yaml index afd27dd8..d0d26910 100644 --- a/src/main/resources/swagger.api/dcb_transaction.yaml +++ b/src/main/resources/swagger.api/dcb_transaction.yaml @@ -201,6 +201,110 @@ paths: $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalServerError' + + /dcb/settings: + description: DCB settings endpoint + get: + description: Search DCB settings by CQL query with pagination + operationId: findDcbSettings + tags: + - setting + parameters: + - $ref: '#/components/parameters/query' + - $ref: '#/components/parameters/limit' + - $ref: '#/components/parameters/offset' + responses: + '200': + description: List of DCB settings matching the query + content: + application/json: + schema: + $ref: '#/components/schemas/SettingsCollection' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalServerError' + post: + description: Create DCB setting + operationId: createDcbSetting + tags: + - setting + requestBody: + description: DCB setting to create + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Setting' + responses: + '200': + description: created DCB setting + content: + application/json: + schema: + $ref: '#/components/schemas/Setting' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalServerError' + + /dcb/settings/{id}: + get: + description: Get DCB setting by id + operationId: getDcbSettingById + tags: + - setting + parameters: + - $ref: '#/components/parameters/pathId' + responses: + '200': + description: DCB setting with the given id + content: + application/json: + schema: + $ref: '#/components/schemas/Setting' + '400': + $ref: '#/components/responses/BadRequest' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' + put: + description: Update DCB setting by id + operationId: updateDcbSettingById + tags: + - setting + requestBody: + description: DCB setting to create + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Setting' + parameters: + - $ref: '#/components/parameters/pathId' + responses: + '204': + description: DCB setting updated successfully + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' + delete: + description: Delete DCB setting by id + operationId: deleteDcbSettingById + tags: + - setting + parameters: + - $ref: '#/components/parameters/pathId' + responses: + '204': + description: DCB setting deleted successfully + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' + components: requestBodies: TransactionStatusBody: @@ -339,6 +443,44 @@ components: minimum: 1 maximum: 2147483647 required: false + query: + in: query + name: query + description: A query string to filter records based on matching criteria in fields. + required: false + schema: + type: string + default: cql.allRecords=1 + example: role + limit: + in: query + name: limit + description: Limit the number of elements returned in the response. + required: false + schema: + type: integer + default: 50 + minimum: 0 + example: 100 + offset: + in: query + name: offset + description: Skip over a number of elements by specifying an offset value for the query. + required: false + schema: + type: integer + default: 0 + minimum: 0 + example: 0 + pathId: + in: path + required: true + name: id + description: Record identifier + schema: + type: string + format: uuid + example: 1e111e76-1111-401c-ad8e-0d121a11111e schemas: RenewByIdRequest: $ref: 'schemas/RenewByIdRequest.yaml#/RenewByIdRequest' @@ -370,3 +512,7 @@ components: $ref: 'schemas/MaterialType.yaml#/MaterialTypeCollection' CalendarCollection: $ref: 'schemas/Calendar.yaml#/CalendarCollection' + Setting: + $ref: 'schemas/setting.yaml#/Setting' + SettingsCollection: + $ref: 'schemas/setting.yaml#/SettingsCollection' diff --git a/src/main/resources/swagger.api/schemas/setting.yaml b/src/main/resources/swagger.api/schemas/setting.yaml new file mode 100644 index 00000000..d752e984 --- /dev/null +++ b/src/main/resources/swagger.api/schemas/setting.yaml @@ -0,0 +1,46 @@ +SettingsCollection: + $schema: "http://json-schema.org/draft-04/schema#" + description: "Collection of settings" + type: object + properties: + items: + type: array + items: + $ref: "setting.yaml#/Setting" + totalRecords: + description: "The number of setting records returned in this collection" + type: integer + additionalProperties: false + required: [ items, totalRecords ] + +Setting: + $schema: "http://json-schema.org/draft-04/schema#" + description: "Setting" + type: object + properties: + id: + description: "Setting Identifier" + type: string + format: uuid + scope: + $ref: "setting.yaml#/SettingScope" + key: + type: string + description: "Key within scope for this setting" + value: + type: object + description: "Settings value (any type)" + _version: + type: integer + description: "Record version for optimistic locking" + metadata: + $ref: "metadata.json" + + additionalProperties: false + required: [ id, scope, key, value ] + +SettingScope: + type: string + description: "Scope for this entry (normally a module)" + enum: + - "mod-dcb" diff --git a/src/test/java/org/folio/dcb/it/BorrowerTransactionIT.java b/src/test/java/org/folio/dcb/it/BorrowerTransactionIT.java index c37d099e..4aacb352 100644 --- a/src/test/java/org/folio/dcb/it/BorrowerTransactionIT.java +++ b/src/test/java/org/folio/dcb/it/BorrowerTransactionIT.java @@ -17,18 +17,21 @@ import static org.folio.dcb.domain.dto.TransactionStatus.StatusEnum.ITEM_CHECKED_IN; import static org.folio.dcb.domain.dto.TransactionStatus.StatusEnum.ITEM_CHECKED_OUT; import static org.folio.dcb.domain.dto.TransactionStatus.StatusEnum.OPEN; -import static org.folio.dcb.utils.EntityUtils.BORROWER_SERVICE_POINT_ID; +import static org.folio.dcb.utils.EntityUtils.VIRTUAL_SERVICE_POINT_ID; import static org.folio.dcb.utils.EntityUtils.DCB_TRANSACTION_ID; import static org.folio.dcb.utils.EntityUtils.EXISTED_PATRON_ID; import static org.folio.dcb.utils.EntityUtils.ITEM_ID; import static org.folio.dcb.utils.EntityUtils.LOAN_ID; import static org.folio.dcb.utils.EntityUtils.NOT_EXISTED_PATRON_ID; import static org.folio.dcb.utils.EntityUtils.PATRON_TYPE_USER_ID; +import static org.folio.dcb.utils.EntityUtils.TEST_TENANT; import static org.folio.dcb.utils.EntityUtils.borrowerDcbTransaction; import static org.folio.dcb.utils.EntityUtils.dcbItem; import static org.folio.dcb.utils.EntityUtils.dcbPatron; import static org.folio.dcb.utils.EntityUtils.dcbTransactionUpdate; import static org.folio.dcb.utils.EntityUtils.transactionStatus; +import static org.folio.dcb.utils.EventDataProvider.expiredRequestMessage; +import static org.folio.dcb.utils.EventDataProvider.itemCheckInMessage; import static org.folio.dcb.utils.JsonTestUtils.asJsonString; import static org.hamcrest.Matchers.containsInRelativeOrder; import static org.hamcrest.Matchers.containsString; @@ -251,7 +254,7 @@ void updateTransaction_positive_newDcbItemProvidedAsReplacement() throws Excepti .withRequestBody(matchingJsonPath("$.status", equalTo("Closed - Cancelled"))) .withRequestBody(matchingJsonPath("$.instanceId", equalTo(DCBConstants.INSTANCE_ID))) .withRequestBody(matchingJsonPath("$.requesterId", equalTo(PATRON_TYPE_USER_ID))) - .withRequestBody(matchingJsonPath("$.pickupServicePointId", equalTo(BORROWER_SERVICE_POINT_ID))) + .withRequestBody(matchingJsonPath("$.pickupServicePointId", equalTo(VIRTUAL_SERVICE_POINT_ID))) .withRequestBody(matchingJsonPath("$.holdingsRecordId", equalTo(DCBConstants.HOLDING_ID)))); } @@ -439,6 +442,29 @@ void updateTransactionStatus_parameterized_invalidTransitionToExpiredStatus( "Status transition will not be possible from %s to EXPIRED", sourceStatus)))); } + @Test + void updateStatus_positive_awaitingPickupTransactionExpiration() throws Exception { + testJdbcHelper.saveDcbTransaction(DCB_TRANSACTION_ID, AWAITING_PICKUP, borrowerDcbTransaction()); + getDcbTransactionStatus(DCB_TRANSACTION_ID) + .andExpect(jsonPath("$.status").value(AWAITING_PICKUP.getValue())); + + testEventHelper.sendMessage(expiredRequestMessage(TEST_TENANT)); + awaitUntilAsserted(() -> getDcbTransactionStatus(DCB_TRANSACTION_ID) + .andExpect(jsonPath("$.status").value(EXPIRED.getValue()))); + } + + @Test + void updateStatus_positive_expiredToClosedTransitionAfterCheckInMessage() throws Exception { + testJdbcHelper.saveDcbTransaction(DCB_TRANSACTION_ID, EXPIRED, borrowerDcbTransaction()); + getDcbTransactionStatus(DCB_TRANSACTION_ID) + .andExpect(jsonPath("$.status").value(EXPIRED.getValue())); + + testEventHelper.sendMessage(itemCheckInMessage(TEST_TENANT)); + + awaitUntilAsserted(() -> getDcbTransactionStatus(DCB_TRANSACTION_ID) + .andExpect(jsonPath("$.status").value(CLOSED.getValue()))); + } + private static void verifyPostCirculationRequestCalledOnce(String requesterId) { verifyPostCirculationRequestCalledOnce(ITEM_ID, requesterId); } @@ -449,7 +475,7 @@ private static void verifyPostCirculationRequestCalledOnce(String itemId, String .withRequestBody(matchingJsonPath("$.itemId", equalTo(itemId))) .withRequestBody(matchingJsonPath("$.instanceId", equalTo(DCBConstants.INSTANCE_ID))) .withRequestBody(matchingJsonPath("$.requesterId", equalTo(requesterId))) - .withRequestBody(matchingJsonPath("$.pickupServicePointId", equalTo(BORROWER_SERVICE_POINT_ID))) + .withRequestBody(matchingJsonPath("$.pickupServicePointId", equalTo(VIRTUAL_SERVICE_POINT_ID))) .withRequestBody(matchingJsonPath("$.holdingsRecordId", equalTo(DCBConstants.HOLDING_ID)))); } } diff --git a/src/test/java/org/folio/dcb/it/BorrowingPickupTransactionIT.java b/src/test/java/org/folio/dcb/it/BorrowingPickupTransactionIT.java index da52df54..c777e09c 100644 --- a/src/test/java/org/folio/dcb/it/BorrowingPickupTransactionIT.java +++ b/src/test/java/org/folio/dcb/it/BorrowingPickupTransactionIT.java @@ -9,6 +9,7 @@ import static com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching; import static java.lang.String.format; import static org.assertj.core.api.Assertions.assertThat; +import static org.folio.dcb.domain.dto.TransactionStatus.StatusEnum.AWAITING_PICKUP; import static org.folio.dcb.domain.dto.TransactionStatus.StatusEnum.CANCELLED; import static org.folio.dcb.domain.dto.TransactionStatus.StatusEnum.CLOSED; import static org.folio.dcb.domain.dto.TransactionStatus.StatusEnum.CREATED; @@ -16,7 +17,7 @@ import static org.folio.dcb.domain.dto.TransactionStatus.StatusEnum.ITEM_CHECKED_IN; import static org.folio.dcb.domain.dto.TransactionStatus.StatusEnum.ITEM_CHECKED_OUT; import static org.folio.dcb.domain.dto.TransactionStatus.StatusEnum.OPEN; -import static org.folio.dcb.utils.EntityUtils.BORROWER_SERVICE_POINT_ID; +import static org.folio.dcb.utils.EntityUtils.VIRTUAL_SERVICE_POINT_ID; import static org.folio.dcb.utils.EntityUtils.DCB_TRANSACTION_ID; import static org.folio.dcb.utils.EntityUtils.EXISTED_PATRON_ID; import static org.folio.dcb.utils.EntityUtils.ITEM_ID; @@ -24,12 +25,14 @@ import static org.folio.dcb.utils.EntityUtils.NOT_EXISTED_PATRON_ID; import static org.folio.dcb.utils.EntityUtils.PATRON_TYPE_USER_ID; import static org.folio.dcb.utils.EntityUtils.PICKUP_SERVICE_POINT_ID; -import static org.folio.dcb.utils.EntityUtils.borrowerDcbTransaction; +import static org.folio.dcb.utils.EntityUtils.TEST_TENANT; import static org.folio.dcb.utils.EntityUtils.borrowingPickupDcbTransaction; import static org.folio.dcb.utils.EntityUtils.dcbItem; import static org.folio.dcb.utils.EntityUtils.dcbPatron; import static org.folio.dcb.utils.EntityUtils.dcbTransactionUpdate; import static org.folio.dcb.utils.EntityUtils.transactionStatus; +import static org.folio.dcb.utils.EventDataProvider.expiredRequestMessage; +import static org.folio.dcb.utils.EventDataProvider.itemCheckInMessage; import static org.folio.dcb.utils.JsonTestUtils.asJsonString; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.is; @@ -284,7 +287,7 @@ void updateTransaction_positive_newDcbItemProvidedAsReplacement() throws Excepti .withRequestBody(matchingJsonPath("$.status", equalTo("Closed - Cancelled"))) .withRequestBody(matchingJsonPath("$.instanceId", equalTo(DCBConstants.INSTANCE_ID))) .withRequestBody(matchingJsonPath("$.requesterId", equalTo(PATRON_TYPE_USER_ID))) - .withRequestBody(matchingJsonPath("$.pickupServicePointId", equalTo(BORROWER_SERVICE_POINT_ID))) + .withRequestBody(matchingJsonPath("$.pickupServicePointId", equalTo(VIRTUAL_SERVICE_POINT_ID))) .withRequestBody(matchingJsonPath("$.holdingsRecordId", equalTo(DCBConstants.HOLDING_ID)))); } @@ -302,7 +305,7 @@ void updateTransaction_positive_invalidState() throws Exception { @EnumSource(value = TransactionStatus.StatusEnum.class, names = "EXPIRED", mode = EXCLUDE) void updateTransactionStatus_parameterized_invalidTransitionToExpiredStatus( TransactionStatus.StatusEnum sourceStatus) throws Exception { - testJdbcHelper.saveDcbTransaction(DCB_TRANSACTION_ID, sourceStatus, borrowerDcbTransaction()); + testJdbcHelper.saveDcbTransaction(DCB_TRANSACTION_ID, sourceStatus, borrowingPickupDcbTransaction()); putDcbTransactionStatusAttempt(DCB_TRANSACTION_ID, transactionStatus(EXPIRED)) .andExpect(status().isBadRequest()) @@ -311,6 +314,29 @@ void updateTransactionStatus_parameterized_invalidTransitionToExpiredStatus( "Status transition will not be possible from %s to EXPIRED", sourceStatus)))); } + @Test + void updateStatus_positive_awaitingPickupTransactionExpiration() throws Exception { + testJdbcHelper.saveDcbTransaction(DCB_TRANSACTION_ID, AWAITING_PICKUP, borrowingPickupDcbTransaction()); + getDcbTransactionStatus(DCB_TRANSACTION_ID) + .andExpect(jsonPath("$.status").value(AWAITING_PICKUP.getValue())); + + testEventHelper.sendMessage(expiredRequestMessage(TEST_TENANT)); + awaitUntilAsserted(() -> getDcbTransactionStatus(DCB_TRANSACTION_ID) + .andExpect(jsonPath("$.status").value(EXPIRED.getValue()))); + } + + @Test + void updateStatus_positive_expiredToClosedTransitionAfterCheckInMessage() throws Exception { + testJdbcHelper.saveDcbTransaction(DCB_TRANSACTION_ID, EXPIRED, borrowingPickupDcbTransaction()); + getDcbTransactionStatus(DCB_TRANSACTION_ID) + .andExpect(jsonPath("$.status").value(EXPIRED.getValue())); + + testEventHelper.sendMessage(itemCheckInMessage(TEST_TENANT)); + + awaitUntilAsserted(() -> getDcbTransactionStatus(DCB_TRANSACTION_ID) + .andExpect(jsonPath("$.status").value(CLOSED.getValue()))); + } + private static void verifyPostCirculationRequestCalledOnce(String requesterId) { verifyPostCirculationRequestCalledOnce(ITEM_ID, requesterId); } diff --git a/src/test/java/org/folio/dcb/it/EcsRequestTransactionsIT.java b/src/test/java/org/folio/dcb/it/EcsRequestTransactionsIT.java index 0b333af7..aeb98f88 100644 --- a/src/test/java/org/folio/dcb/it/EcsRequestTransactionsIT.java +++ b/src/test/java/org/folio/dcb/it/EcsRequestTransactionsIT.java @@ -4,7 +4,7 @@ import static com.github.tomakehurst.wiremock.client.WireMock.matchingJsonPath; import static com.github.tomakehurst.wiremock.client.WireMock.putRequestedFor; import static com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching; -import static org.folio.dcb.utils.EntityUtils.BORROWER_SERVICE_POINT_ID; +import static org.folio.dcb.utils.EntityUtils.VIRTUAL_SERVICE_POINT_ID; import static org.folio.dcb.utils.EntityUtils.CIRCULATION_REQUEST_ID; import static org.folio.dcb.utils.EntityUtils.EXISTED_PATRON_ID; import static org.folio.dcb.utils.EntityUtils.ITEM_ID; @@ -73,7 +73,7 @@ void createBorrowingEcsRequestTest() throws Exception { .withRequestBody(matchingJsonPath("$.status", equalTo("Open - Not yet filled"))) .withRequestBody(matchingJsonPath("$.instanceId", equalTo(DCBConstants.INSTANCE_ID))) .withRequestBody(matchingJsonPath("$.requesterId", equalTo(PATRON_TYPE_USER_ID))) - .withRequestBody(matchingJsonPath("$.pickupServicePointId", equalTo(BORROWER_SERVICE_POINT_ID))) + .withRequestBody(matchingJsonPath("$.pickupServicePointId", equalTo(VIRTUAL_SERVICE_POINT_ID))) .withRequestBody(matchingJsonPath("$.holdingsRecordId", equalTo(DCBConstants.HOLDING_ID)))); } @@ -96,7 +96,7 @@ void createPickupEcsRequestTest() throws Exception { .withRequestBody(matchingJsonPath("$.status", equalTo("Open - Not yet filled"))) .withRequestBody(matchingJsonPath("$.instanceId", equalTo(DCBConstants.INSTANCE_ID))) .withRequestBody(matchingJsonPath("$.requesterId", equalTo(PATRON_TYPE_USER_ID))) - .withRequestBody(matchingJsonPath("$.pickupServicePointId", equalTo(BORROWER_SERVICE_POINT_ID))) + .withRequestBody(matchingJsonPath("$.pickupServicePointId", equalTo(VIRTUAL_SERVICE_POINT_ID))) .withRequestBody(matchingJsonPath("$.holdingsRecordId", equalTo(DCBConstants.HOLDING_ID)))); } @@ -119,7 +119,7 @@ void createBorrowingPickupEcsRequestTest() throws Exception { .withRequestBody(matchingJsonPath("$.status", equalTo("Open - Not yet filled"))) .withRequestBody(matchingJsonPath("$.instanceId", equalTo(DCBConstants.INSTANCE_ID))) .withRequestBody(matchingJsonPath("$.requesterId", equalTo(PATRON_TYPE_USER_ID))) - .withRequestBody(matchingJsonPath("$.pickupServicePointId", equalTo(BORROWER_SERVICE_POINT_ID))) + .withRequestBody(matchingJsonPath("$.pickupServicePointId", equalTo(VIRTUAL_SERVICE_POINT_ID))) .withRequestBody(matchingJsonPath("$.holdingsRecordId", equalTo(DCBConstants.HOLDING_ID)))); } diff --git a/src/test/java/org/folio/dcb/it/FlexibleEffectiveLocationIT.java b/src/test/java/org/folio/dcb/it/FlexibleEffectiveLocationIT.java index d35e67d8..dee8e6a8 100644 --- a/src/test/java/org/folio/dcb/it/FlexibleEffectiveLocationIT.java +++ b/src/test/java/org/folio/dcb/it/FlexibleEffectiveLocationIT.java @@ -8,7 +8,7 @@ import static com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching; import static java.util.Collections.emptyList; import static org.folio.dcb.support.wiremock.WiremockContainerExtension.getWireMockClient; -import static org.folio.dcb.utils.EntityUtils.BORROWER_SERVICE_POINT_ID; +import static org.folio.dcb.utils.EntityUtils.VIRTUAL_SERVICE_POINT_ID; import static org.folio.dcb.utils.EntityUtils.DCB_TRANSACTION_ID; import static org.folio.dcb.utils.EntityUtils.EXISTED_PATRON_ID; import static org.folio.dcb.utils.EntityUtils.ITEM_ID; @@ -104,7 +104,7 @@ void createTransaction_positive_locationCodeMatched() throws Exception { verifyGetRequestBeingCalledOnce("/locations", QUERY_BY_SHADOW_LOCATION_CODE); verifyPostCirculationItemIsCalledOnce(SHADOW_LOCATION_ID); - verifyPostCirculationRequestCalledOnce(EXISTED_PATRON_ID, BORROWER_SERVICE_POINT_ID); + verifyPostCirculationRequestCalledOnce(EXISTED_PATRON_ID, VIRTUAL_SERVICE_POINT_ID); } @Test @@ -136,7 +136,7 @@ void createTransaction_positive_locationCodeNotMatchedAndLendingLibraryMatched() verifyGetRequestBeingCalledOnce("/locations", "libraryId==\"%s\"".formatted(SHADOW_LIBRARY_ID)); verifyGetRequestBeingCalledOnce("/location-units/libraries", QUERY_BY_SHADOW_LOCATION_CODE); verifyPostCirculationItemIsCalledOnce(SHADOW_LOCATION_ID); - verifyPostCirculationRequestCalledOnce(EXISTED_PATRON_ID, BORROWER_SERVICE_POINT_ID); + verifyPostCirculationRequestCalledOnce(EXISTED_PATRON_ID, VIRTUAL_SERVICE_POINT_ID); } @Test @@ -166,7 +166,7 @@ void createTransaction_positive_nothingMatched() throws Exception { verifyGetRequestBeingCalledOnce("/locations", QUERY_BY_SHADOW_LOCATION_CODE); verifyGetRequestBeingCalledOnce("/location-units/libraries", QUERY_BY_SHADOW_LOCATION_CODE); verifyPostCirculationItemIsCalledOnce(DCB_LOCATION_ID); - verifyPostCirculationRequestCalledOnce(EXISTED_PATRON_ID, BORROWER_SERVICE_POINT_ID); + verifyPostCirculationRequestCalledOnce(EXISTED_PATRON_ID, VIRTUAL_SERVICE_POINT_ID); } @Test @@ -191,7 +191,7 @@ void createTransaction_positive_lendingLibraryCodeMatched() throws Exception { verifyGetRequestBeingCalledOnce("/location-units/libraries", QUERY_BY_SHADOW_LOCATION_CODE); verifyGetRequestBeingCalledOnce("/locations", "libraryId==\"%s\"".formatted(SHADOW_LIBRARY_ID)); verifyPostCirculationItemIsCalledOnce(SHADOW_LOCATION_ID); - verifyPostCirculationRequestCalledOnce(EXISTED_PATRON_ID, BORROWER_SERVICE_POINT_ID); + verifyPostCirculationRequestCalledOnce(EXISTED_PATRON_ID, VIRTUAL_SERVICE_POINT_ID); } @Test @@ -214,7 +214,7 @@ void createTransaction_positive_lendingLibraryCodeNotMatched() throws Exception verifyGetRequestBeingCalledOnce("/location-units/libraries", QUERY_BY_SHADOW_LOCATION_CODE); verifyPostCirculationItemIsCalledOnce(DCB_LOCATION_ID); - verifyPostCirculationRequestCalledOnce(EXISTED_PATRON_ID, BORROWER_SERVICE_POINT_ID); + verifyPostCirculationRequestCalledOnce(EXISTED_PATRON_ID, VIRTUAL_SERVICE_POINT_ID); } } @@ -323,7 +323,7 @@ void createTransaction_positive_lendingLibraryCodeMatched() throws Exception { verifyGetRequestBeingCalledOnce("/location-units/libraries", QUERY_BY_SHADOW_LOCATION_CODE); verifyGetRequestBeingCalledOnce("/locations", "libraryId==\"%s\"".formatted(SHADOW_LIBRARY_ID)); verifyPostCirculationItemIsCalledOnce(SHADOW_LOCATION_ID); - verifyPostCirculationRequestCalledOnce(EXISTED_PATRON_ID, BORROWER_SERVICE_POINT_ID); + verifyPostCirculationRequestCalledOnce(EXISTED_PATRON_ID, VIRTUAL_SERVICE_POINT_ID); } @Test @@ -346,7 +346,7 @@ void createTransaction_positive_lendingLibraryCodeNotMatched() throws Exception verifyGetRequestBeingCalledOnce("/location-units/libraries", QUERY_BY_SHADOW_LOCATION_CODE); verifyPostCirculationItemIsCalledOnce(DCB_LOCATION_ID); - verifyPostCirculationRequestCalledOnce(EXISTED_PATRON_ID, BORROWER_SERVICE_POINT_ID); + verifyPostCirculationRequestCalledOnce(EXISTED_PATRON_ID, VIRTUAL_SERVICE_POINT_ID); } } diff --git a/src/test/java/org/folio/dcb/it/LenderTransactionIT.java b/src/test/java/org/folio/dcb/it/LenderTransactionIT.java index d60ae3ad..1a6ae187 100644 --- a/src/test/java/org/folio/dcb/it/LenderTransactionIT.java +++ b/src/test/java/org/folio/dcb/it/LenderTransactionIT.java @@ -17,7 +17,7 @@ import static org.folio.dcb.domain.dto.TransactionStatus.StatusEnum.ITEM_CHECKED_IN; import static org.folio.dcb.domain.dto.TransactionStatus.StatusEnum.ITEM_CHECKED_OUT; import static org.folio.dcb.domain.dto.TransactionStatus.StatusEnum.OPEN; -import static org.folio.dcb.utils.EntityUtils.BORROWER_SERVICE_POINT_ID; +import static org.folio.dcb.utils.EntityUtils.VIRTUAL_SERVICE_POINT_ID; import static org.folio.dcb.utils.EntityUtils.DCB_TRANSACTION_ID; import static org.folio.dcb.utils.EntityUtils.EXISTED_PATRON_ID; import static org.folio.dcb.utils.EntityUtils.HOLDING_RECORD_ID; @@ -30,6 +30,7 @@ import static org.folio.dcb.utils.EntityUtils.dcbPatron; import static org.folio.dcb.utils.EntityUtils.dcbTransactionUpdate; import static org.folio.dcb.utils.EntityUtils.lenderDcbTransaction; +import static org.folio.dcb.utils.EntityUtils.lenderHoldShelfExpirationSetting; import static org.folio.dcb.utils.EntityUtils.transactionStatus; import static org.folio.dcb.utils.EventDataProvider.expiredRequestMessage; import static org.folio.dcb.utils.EventDataProvider.itemCheckInMessage; @@ -42,6 +43,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import org.folio.dcb.domain.dto.HoldShelfExpiryPeriod; +import org.folio.dcb.domain.dto.IntervalIdEnum; import org.folio.dcb.domain.dto.TransactionStatus.StatusEnum; import org.folio.dcb.domain.dto.TransactionStatusResponse; import org.folio.dcb.it.base.BaseTenantIntegrationTest; @@ -120,6 +123,34 @@ void createTransaction_positive_availableItem() throws Exception { verifyPostCirculationRequestCalledOnce(PAGE.getValue(), EXISTED_PATRON_ID); } + @Test + @WireMockStub(value = { + "/stubs/mod-users/users/200-get-by-query(user id+barcode).json", + "/stubs/mod-users/groups/200-get-by-query(staff).json", + "/stubs/mod-inventory-storage/service-points/200-get-by-name(Virtual).json", + "/stubs/mod-inventory-storage/service-points/204-put(Virtual+custom).json", + "/stubs/mod-calendar/calendars/200-get-all.json", + "/stubs/mod-inventory-storage/item-storage/200-get-by-query(id+barcode).json", + "/stubs/mod-inventory-storage/holdings-storage/200-get-by-id.json", + "/stubs/mod-circulation/requests/201-post(any).json" + }) + void createTransaction_positive_availableItemAndCustomHoldShelfExpiration() throws Exception { + var holdShelfExpiryPeriod = new HoldShelfExpiryPeriod().duration(25).intervalId(IntervalIdEnum.MINUTES); + testJdbcHelper.saveDcbSetting(TEST_TENANT, lenderHoldShelfExpirationSetting(holdShelfExpiryPeriod)); + postDcbTransaction(DCB_TRANSACTION_ID, lenderDcbTransaction()) + .andExpect(jsonPath("$.status").value("CREATED")) + .andExpect(jsonPath("$.item").value(dcbItem())) + .andExpect(jsonPath("$.patron").value(dcbPatron(EXISTED_PATRON_ID))) + .andExpect(jsonPath("$.item.locationCode").doesNotExist()); + + auditEntityVerifier.assertThatLatestEntityIsNotDuplicate(DCB_TRANSACTION_ID); + verifyPostCirculationRequestCalledOnce(PAGE.getValue(), EXISTED_PATRON_ID); + + wiremock.verifyThat(1, putRequestedFor(urlPathEqualTo("/service-points/" + VIRTUAL_SERVICE_POINT_ID)) + .withRequestBody(matchingJsonPath("$.holdShelfExpiryPeriod.duration", equalTo("25"))) + .withRequestBody(matchingJsonPath("$.holdShelfExpiryPeriod.intervalId", equalTo("Minutes")))); + } + @Test @WireMockStub(value = { "/stubs/mod-users/users/200-get-by-query(user id+barcode).json", @@ -380,7 +411,7 @@ void updateTransactionStatus_positive_fromCreatedToCancelled() throws Exception .withRequestBody(matchingJsonPath("$.status", equalTo("Closed - Cancelled"))) .withRequestBody(matchingJsonPath("$.instanceId", equalTo(INSTANCE_ID))) .withRequestBody(matchingJsonPath("$.requesterId", equalTo(EXISTED_PATRON_ID))) - .withRequestBody(matchingJsonPath("$.pickupServicePointId", equalTo(BORROWER_SERVICE_POINT_ID))) + .withRequestBody(matchingJsonPath("$.pickupServicePointId", equalTo(VIRTUAL_SERVICE_POINT_ID))) .withRequestBody(matchingJsonPath("$.holdingsRecordId", equalTo(HOLDING_RECORD_ID)))); } @@ -400,7 +431,7 @@ void updateTransactionStatus_positive_fromAwaitingPickupToCancelled() throws Exc .withRequestBody(matchingJsonPath("$.status", equalTo("Closed - Cancelled"))) .withRequestBody(matchingJsonPath("$.instanceId", equalTo(INSTANCE_ID))) .withRequestBody(matchingJsonPath("$.requesterId", equalTo(EXISTED_PATRON_ID))) - .withRequestBody(matchingJsonPath("$.pickupServicePointId", equalTo(BORROWER_SERVICE_POINT_ID))) + .withRequestBody(matchingJsonPath("$.pickupServicePointId", equalTo(VIRTUAL_SERVICE_POINT_ID))) .withRequestBody(matchingJsonPath("$.holdingsRecordId", equalTo(HOLDING_RECORD_ID)))); } @@ -510,7 +541,7 @@ private static void verifyPostCirculationRequestCalledOnce(String type, String r .withRequestBody(matchingJsonPath("$.itemId", equalTo(ITEM_ID))) .withRequestBody(matchingJsonPath("$.instanceId", equalTo(INSTANCE_ID))) .withRequestBody(matchingJsonPath("$.requesterId", equalTo(requesterId))) - .withRequestBody(matchingJsonPath("$.pickupServicePointId", equalTo(BORROWER_SERVICE_POINT_ID))) + .withRequestBody(matchingJsonPath("$.pickupServicePointId", equalTo(VIRTUAL_SERVICE_POINT_ID))) .withRequestBody(matchingJsonPath("$.holdingsRecordId", equalTo(HOLDING_RECORD_ID)))); } } diff --git a/src/test/java/org/folio/dcb/it/PickupTransactionIT.java b/src/test/java/org/folio/dcb/it/PickupTransactionIT.java index 61cf4300..5c9a26e4 100644 --- a/src/test/java/org/folio/dcb/it/PickupTransactionIT.java +++ b/src/test/java/org/folio/dcb/it/PickupTransactionIT.java @@ -6,12 +6,14 @@ import static com.github.tomakehurst.wiremock.client.WireMock.putRequestedFor; import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; import static com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching; +import static org.folio.dcb.domain.dto.TransactionStatus.StatusEnum.AWAITING_PICKUP; import static org.folio.dcb.domain.dto.TransactionStatus.StatusEnum.CANCELLED; +import static org.folio.dcb.domain.dto.TransactionStatus.StatusEnum.CLOSED; import static org.folio.dcb.domain.dto.TransactionStatus.StatusEnum.CREATED; import static org.folio.dcb.domain.dto.TransactionStatus.StatusEnum.EXPIRED; import static org.folio.dcb.domain.dto.TransactionStatus.StatusEnum.ITEM_CHECKED_IN; import static org.folio.dcb.domain.dto.TransactionStatus.StatusEnum.OPEN; -import static org.folio.dcb.utils.EntityUtils.BORROWER_SERVICE_POINT_ID; +import static org.folio.dcb.utils.EntityUtils.VIRTUAL_SERVICE_POINT_ID; import static org.folio.dcb.utils.EntityUtils.DCB_TRANSACTION_ID; import static org.folio.dcb.utils.EntityUtils.EXISTED_PATRON_ID; import static org.folio.dcb.utils.EntityUtils.ITEM_ID; @@ -19,11 +21,15 @@ import static org.folio.dcb.utils.EntityUtils.PATRON_GROUP_ID; import static org.folio.dcb.utils.EntityUtils.PATRON_TYPE_USER_ID; import static org.folio.dcb.utils.EntityUtils.PICKUP_SERVICE_POINT_ID; +import static org.folio.dcb.utils.EntityUtils.TEST_TENANT; import static org.folio.dcb.utils.EntityUtils.dcbItem; import static org.folio.dcb.utils.EntityUtils.dcbPatron; import static org.folio.dcb.utils.EntityUtils.dcbTransactionUpdate; +import static org.folio.dcb.utils.EntityUtils.lenderDcbTransaction; import static org.folio.dcb.utils.EntityUtils.pickupDcbTransaction; import static org.folio.dcb.utils.EntityUtils.transactionStatus; +import static org.folio.dcb.utils.EventDataProvider.expiredRequestMessage; +import static org.folio.dcb.utils.EventDataProvider.itemCheckInMessage; import static org.folio.dcb.utils.JsonTestUtils.asJsonString; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.is; @@ -55,6 +61,7 @@ class PickupTransactionIT extends BaseTenantIntegrationTest { "/stubs/mod-users/groups/200-get-by-query(staff).json", "/stubs/mod-circulation-item/201-post(pickup).json", "/stubs/mod-circulation/requests/201-post(any).json", + "/stubs/mod-circulation/check-in-by-barcode/201-post(random SP).json", }) void createTransaction_positive_newDcbItemAndUser() throws Exception { var patron = dcbPatron(NOT_EXISTED_PATRON_ID); @@ -69,6 +76,9 @@ void createTransaction_positive_newDcbItemAndUser() throws Exception { auditEntityVerifier.assertThatLatestEntityIsNotDuplicate(DCB_TRANSACTION_ID); verifyPostCirculationRequestCalledOnce(NOT_EXISTED_PATRON_ID); + + putDcbTransactionStatus(DCB_TRANSACTION_ID, transactionStatus(OPEN)) + .andExpect(jsonPath("$.status").value("OPEN")); } @Test @@ -266,7 +276,7 @@ void updateTransaction_positive_newDcbItemProvidedAsReplacement() throws Excepti .withRequestBody(matchingJsonPath("$.status", equalTo("Closed - Cancelled"))) .withRequestBody(matchingJsonPath("$.instanceId", equalTo(DCBConstants.INSTANCE_ID))) .withRequestBody(matchingJsonPath("$.requesterId", equalTo(PATRON_TYPE_USER_ID))) - .withRequestBody(matchingJsonPath("$.pickupServicePointId", equalTo(BORROWER_SERVICE_POINT_ID))) + .withRequestBody(matchingJsonPath("$.pickupServicePointId", equalTo(VIRTUAL_SERVICE_POINT_ID))) .withRequestBody(matchingJsonPath("$.holdingsRecordId", equalTo(DCBConstants.HOLDING_ID)))); } @@ -294,6 +304,29 @@ void updateTransactionStatus_parameterized_invalidTransitionToExpiredStatus( "Status transition will not be possible from %s to EXPIRED", sourceStatus)))); } + @Test + void updateStatus_positive_awaitingPickupTransactionExpiration() throws Exception { + testJdbcHelper.saveDcbTransaction(DCB_TRANSACTION_ID, AWAITING_PICKUP, pickupDcbTransaction()); + getDcbTransactionStatus(DCB_TRANSACTION_ID) + .andExpect(jsonPath("$.status").value(AWAITING_PICKUP.getValue())); + + testEventHelper.sendMessage(expiredRequestMessage(TEST_TENANT)); + awaitUntilAsserted(() -> getDcbTransactionStatus(DCB_TRANSACTION_ID) + .andExpect(jsonPath("$.status").value(EXPIRED.getValue()))); + } + + @Test + void updateStatus_positive_expiredToClosedTransitionAfterCheckInMessage() throws Exception { + testJdbcHelper.saveDcbTransaction(DCB_TRANSACTION_ID, EXPIRED, pickupDcbTransaction()); + getDcbTransactionStatus(DCB_TRANSACTION_ID) + .andExpect(jsonPath("$.status").value(EXPIRED.getValue())); + + testEventHelper.sendMessage(itemCheckInMessage(TEST_TENANT)); + + awaitUntilAsserted(() -> getDcbTransactionStatus(DCB_TRANSACTION_ID) + .andExpect(jsonPath("$.status").value(CLOSED.getValue()))); + } + private static void verifyPostCirculationRequestCalledOnce(String requesterId) { verifyPostCirculationRequestCalledOnce(ITEM_ID, requesterId); } diff --git a/src/test/java/org/folio/dcb/it/SelfBorrowingTransactionIT.java b/src/test/java/org/folio/dcb/it/SelfBorrowingTransactionIT.java index 9a5488d8..e9e30122 100644 --- a/src/test/java/org/folio/dcb/it/SelfBorrowingTransactionIT.java +++ b/src/test/java/org/folio/dcb/it/SelfBorrowingTransactionIT.java @@ -9,7 +9,7 @@ import static org.folio.dcb.domain.dto.CirculationRequest.RequestTypeEnum.PAGE; import static org.folio.dcb.domain.dto.TransactionStatus.StatusEnum.EXPIRED; import static org.folio.dcb.support.wiremock.WiremockContainerExtension.getWireMockClient; -import static org.folio.dcb.utils.EntityUtils.BORROWER_SERVICE_POINT_ID; +import static org.folio.dcb.utils.EntityUtils.VIRTUAL_SERVICE_POINT_ID; import static org.folio.dcb.utils.EntityUtils.DCB_TRANSACTION_ID; import static org.folio.dcb.utils.EntityUtils.HOLDING_RECORD_ID; import static org.folio.dcb.utils.EntityUtils.INSTANCE_ID; @@ -72,7 +72,7 @@ void createTransaction_positive_selfBorrowingAvailableItem() throws Exception { postDcbTransaction(DCB_TRANSACTION_ID, selfBorrowingTransaction) .andExpect(jsonPath("$.status").value("CREATED")); - verifyPostCirculationRequestCalledOnce(PAGE.getValue(), BORROWER_SERVICE_POINT_ID); + verifyPostCirculationRequestCalledOnce(PAGE.getValue(), VIRTUAL_SERVICE_POINT_ID); auditEntityVerifier.assertThatLatestEntityIsNotDuplicate(DCB_TRANSACTION_ID); } @@ -92,7 +92,7 @@ void createTransaction_positive_selfBorrowingPageRequestItemInTransit() throws E postDcbTransaction(DCB_TRANSACTION_ID, selfBorrowingTransaction) .andExpect(jsonPath("$.status").value("CREATED")); - verifyPostCirculationRequestCalledOnce(HOLD.getValue(), BORROWER_SERVICE_POINT_ID); + verifyPostCirculationRequestCalledOnce(HOLD.getValue(), VIRTUAL_SERVICE_POINT_ID); auditEntityVerifier.assertThatLatestEntityIsNotDuplicate(DCB_TRANSACTION_ID); } diff --git a/src/test/java/org/folio/dcb/it/SettingsIT.java b/src/test/java/org/folio/dcb/it/SettingsIT.java new file mode 100644 index 00000000..9a1ab136 --- /dev/null +++ b/src/test/java/org/folio/dcb/it/SettingsIT.java @@ -0,0 +1,218 @@ +package org.folio.dcb.it; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.folio.dcb.utils.EntityUtils.LENDER_HOLD_SHELF_EXPIRATION_SETTING_ID; +import static org.folio.dcb.utils.EntityUtils.REQUEST_USER_ID; +import static org.folio.dcb.utils.EntityUtils.TEST_TENANT; +import static org.folio.dcb.utils.EntityUtils.lenderHoldShelfExpirationSetting; +import static org.folio.dcb.utils.JsonTestUtils.asJsonString; +import static org.springframework.test.json.JsonCompareMode.LENIENT; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.List; +import java.util.UUID; +import org.folio.dcb.domain.dto.HoldShelfExpiryPeriod; +import org.folio.dcb.domain.dto.IntervalIdEnum; +import org.folio.dcb.domain.dto.Metadata; +import org.folio.dcb.domain.dto.Setting; +import org.folio.dcb.domain.dto.SettingsCollection; +import org.folio.dcb.it.base.BaseTenantIntegrationTest; +import org.folio.dcb.support.types.IntegrationTest; +import org.folio.dcb.utils.EntityUtils; +import org.folio.dcb.utils.SettingsApiHelper; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.beans.factory.annotation.Autowired; +import tools.jackson.databind.json.JsonMapper; + +@IntegrationTest +class SettingsIT extends BaseTenantIntegrationTest { + + @Autowired private JsonMapper jsonMapper; + protected static SettingsApiHelper settingsApiHelper; + + @BeforeAll + static void beforeAll() { + SettingsIT.settingsApiHelper = new SettingsApiHelper(mockMvc); + } + + @AfterAll + static void afterAll() { + SettingsIT.settingsApiHelper = null; + } + + @Test + void getSettingById_positive() throws Exception { + var id = UUID.randomUUID(); + var expectedSetting = lenderHoldShelfExpirationSetting() + .id(id) + .version(1) + .metadata(new Metadata() + .createdByUserId(REQUEST_USER_ID) + .updatedByUserId(REQUEST_USER_ID)); + + testJdbcHelper.saveDcbSetting(TEST_TENANT, expectedSetting); + + settingsApiHelper.getById(id.toString()) + .andExpect(content().json(asJsonString(expectedSetting), LENIENT)) + .andExpect(jsonPath("$._version").value(1)) + .andExpect(jsonPath("$.version").doesNotExist()) + .andExpect(jsonPath("$.metadata.createdByUserId").value(REQUEST_USER_ID)) + .andExpect(jsonPath("$.metadata.createdDate").exists()) + .andExpect(jsonPath("$.metadata.updatedByUserId").value(REQUEST_USER_ID)) + .andExpect(jsonPath("$.metadata.updatedDate").exists()); + } + + @Test + void getSettingById_negative_notFoundById() throws Exception { + var id = UUID.randomUUID().toString(); + settingsApiHelper.getByIdAttempt(id) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.errors[0].message").value("Setting not found by id: " + id)) + .andExpect(jsonPath("$.errors[0].code").value("NOT_FOUND_ERROR")) + .andExpect(jsonPath("$.errors[0].type").value(-1)); + } + + @Test + void findByQuery_positive_emptyResult() throws Exception { + settingsApiHelper.findByQuery("cql.allRecords=1", 100, 0) + .andExpect(jsonPath("$.items").isArray()) + .andExpect(jsonPath("$.items").isEmpty()) + .andExpect(jsonPath("$.totalRecords").value(0)); + } + + @ParameterizedTest + @ValueSource(strings = { + "cql.allRecords=1", + "id==\"d115a0b6-133d-4148-ac1a-b48c2aa57f57\"", + "key==\"lender.hold-shelf-expiry-period\"", + }) + void findByQuery_positive_parameterized(String query) throws Exception { + var expectedSetting = lenderHoldShelfExpirationSetting() + .version(1) + .metadata(new Metadata() + .createdByUserId(REQUEST_USER_ID) + .updatedByUserId(REQUEST_USER_ID)); + + testJdbcHelper.saveDcbSetting(TEST_TENANT, expectedSetting); + var expectedResult = new SettingsCollection() + .items(List.of(expectedSetting)) + .totalRecords(1); + + settingsApiHelper.findByQuery(query, 100, 0) + .andExpect(content().json(asJsonString(expectedResult), LENIENT)) + .andExpect(jsonPath("$.items[0].metadata.createdDate").exists()) + .andExpect(jsonPath("$.items[0].metadata.updatedDate").exists()) + .andExpect(jsonPath("$.totalRecords").value(1)); + } + + @Test + void createAndUpdate_positive() throws Exception { + var setting = EntityUtils.lenderHoldShelfExpirationSetting(); + var createdSettingJson = settingsApiHelper.post(setting) + .andExpect(content().json(asJsonString(setting), LENIENT)) + .andExpect(jsonPath("$._version").value(1)) + .andExpect(jsonPath("$.metadata.createdByUserId").value(REQUEST_USER_ID)) + .andExpect(jsonPath("$.metadata.createdDate").exists()) + .andExpect(jsonPath("$.metadata.updatedByUserId").value(REQUEST_USER_ID)) + .andExpect(jsonPath("$.metadata.updatedDate").exists()) + .andReturn().getResponse().getContentAsString(); + + var createdSetting = jsonMapper.readValue(createdSettingJson, Setting.class); + assertThat(createdSetting.getMetadata()).isNotNull(); + var createdDate = createdSetting.getMetadata().getCreatedDate(); + var updatedDate = createdSetting.getMetadata().getUpdatedDate(); + + assertThat(createdDate).isEqualTo(updatedDate); + + var otherUserId = UUID.randomUUID().toString(); + var newValue = new HoldShelfExpiryPeriod().duration(5).intervalId(IntervalIdEnum.DAYS); + var newSettingValue = lenderHoldShelfExpirationSetting(newValue); + settingsApiHelper.putById(LENDER_HOLD_SHELF_EXPIRATION_SETTING_ID, newSettingValue, otherUserId); + + var updatedSettingJson = settingsApiHelper.getById(LENDER_HOLD_SHELF_EXPIRATION_SETTING_ID) + .andExpect(jsonPath("$._version").value(2)) + .andExpect(jsonPath("$.metadata.createdByUserId").value(REQUEST_USER_ID)) + .andExpect(jsonPath("$.metadata.createdDate").exists()) + .andExpect(jsonPath("$.metadata.updatedByUserId").value(otherUserId)) + .andExpect(jsonPath("$.metadata.updatedDate").exists()) + .andReturn().getResponse().getContentAsString(); + + var updatedSetting = jsonMapper.readValue(updatedSettingJson, Setting.class); + var updatedSettingMetadata = updatedSetting.getMetadata(); + assertThat(updatedSettingMetadata).isNotNull(); + assertThat(updatedSettingMetadata.getCreatedDate()).isEqualTo(createdDate); + assertThat(updatedSettingMetadata.getUpdatedDate()).isAfter(updatedSettingMetadata.getCreatedDate()); + assertThat(updatedSettingMetadata.getUpdatedDate()).isAfter(createdDate); + } + + @Test + void updateSetting_positive() throws Exception { + var setting = lenderHoldShelfExpirationSetting(); + testJdbcHelper.saveDcbSetting(TEST_TENANT, setting); + + var otherUserId = UUID.randomUUID().toString(); + var newValue = new HoldShelfExpiryPeriod().duration(1).intervalId(IntervalIdEnum.DAYS); + var newSetting = lenderHoldShelfExpirationSetting(newValue); + settingsApiHelper.putById(LENDER_HOLD_SHELF_EXPIRATION_SETTING_ID, newSetting, otherUserId); + + settingsApiHelper.getByIdAttempt(LENDER_HOLD_SHELF_EXPIRATION_SETTING_ID) + .andExpect(content().json(asJsonString(newSetting), LENIENT)) + .andExpect(jsonPath("$.metadata.updatedByUserId").value(otherUserId)); + } + + @Test + void updateSetting_negative_notFoundEntity() throws Exception { + var id = UUID.randomUUID(); + var newValue = new HoldShelfExpiryPeriod().duration(1).intervalId(IntervalIdEnum.DAYS); + var newSetting = lenderHoldShelfExpirationSetting(newValue).id(id); + settingsApiHelper.putByIdAttempt(id.toString(), newSetting, REQUEST_USER_ID) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.errors[0].message").value("Setting not found by id: " + id)) + .andExpect(jsonPath("$.errors[0].code").value("NOT_FOUND_ERROR")) + .andExpect(jsonPath("$.errors[0].type").value(-1)); + } + + @Test + void updateSetting_negative_keyModificationNotAllowed() throws Exception { + var setting = lenderHoldShelfExpirationSetting(); + testJdbcHelper.saveDcbSetting(TEST_TENANT, setting); + + var otherUserId = UUID.randomUUID().toString(); + var newValue = new HoldShelfExpiryPeriod().duration(1).intervalId(IntervalIdEnum.DAYS); + var newSetting = lenderHoldShelfExpirationSetting(newValue).key("modifiedKey"); + settingsApiHelper.putByIdAttempt(LENDER_HOLD_SHELF_EXPIRATION_SETTING_ID, newSetting, otherUserId) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errors[0].message").value("Setting key cannot be modified: " + setting.getKey())) + .andExpect(jsonPath("$.errors[0].code").value("VALIDATION_ERROR")) + .andExpect(jsonPath("$.errors[0].type").value(-1)); + + settingsApiHelper.getByIdAttempt(LENDER_HOLD_SHELF_EXPIRATION_SETTING_ID) + .andExpect(content().json(asJsonString(setting), LENIENT)) + .andExpect(jsonPath("$.metadata.updatedByUserId").value(REQUEST_USER_ID)); + } + + @Test + void deleteSetting_positive() throws Exception { + var expectedSetting = lenderHoldShelfExpirationSetting(); + testJdbcHelper.saveDcbSetting(TEST_TENANT, expectedSetting); + settingsApiHelper.deleteById(LENDER_HOLD_SHELF_EXPIRATION_SETTING_ID); + settingsApiHelper.getByIdAttempt(LENDER_HOLD_SHELF_EXPIRATION_SETTING_ID) + .andExpect(status().isNotFound()); + } + + @Test + void deleteSetting_negative_notFound() throws Exception { + var id = UUID.randomUUID().toString(); + settingsApiHelper.deleteByIdAttempt(id) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.errors[0].message").value("Setting not found by id: " + id)) + .andExpect(jsonPath("$.errors[0].code").value("NOT_FOUND_ERROR")) + .andExpect(jsonPath("$.errors[0].type").value(-1)); + } +} diff --git a/src/test/java/org/folio/dcb/it/base/BaseIntegrationTest.java b/src/test/java/org/folio/dcb/it/base/BaseIntegrationTest.java index c44b37ba..f5ae74f8 100644 --- a/src/test/java/org/folio/dcb/it/base/BaseIntegrationTest.java +++ b/src/test/java/org/folio/dcb/it/base/BaseIntegrationTest.java @@ -85,14 +85,18 @@ protected static void purgeTenant(String tenantId) { .andExpect(status().isNoContent()); } - protected static HttpHeaders defaultHeaders() { + public static HttpHeaders defaultHeaders() { + return defaultHeaders(REQUEST_USER_ID); + } + + public static HttpHeaders defaultHeaders(String userId) { final HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.setContentType(APPLICATION_JSON); httpHeaders.put(TENANT, List.of(TEST_TENANT)); httpHeaders.add(URL, getWiremockUrl()); httpHeaders.add(TOKEN, TEST_TOKEN); - httpHeaders.add(USER_ID, REQUEST_USER_ID); + httpHeaders.add(USER_ID, userId); return httpHeaders; } diff --git a/src/test/java/org/folio/dcb/it/base/BaseTenantIntegrationTest.java b/src/test/java/org/folio/dcb/it/base/BaseTenantIntegrationTest.java index 810d0c10..72d07226 100644 --- a/src/test/java/org/folio/dcb/it/base/BaseTenantIntegrationTest.java +++ b/src/test/java/org/folio/dcb/it/base/BaseTenantIntegrationTest.java @@ -29,6 +29,7 @@ import org.folio.dcb.repository.TransactionAuditRepository; import org.folio.dcb.support.AuditEntityTestVerifier; import org.folio.dcb.support.wiremock.WiremockStubExtension; +import org.folio.dcb.utils.SettingsApiHelper; import org.folio.dcb.utils.TestCirculationEventHelper; import org.folio.dcb.utils.TestJdbcHelper; import org.folio.spring.FolioModuleMetadata; diff --git a/src/test/java/org/folio/dcb/listener/CirculationCheckInEventListenerTest.java b/src/test/java/org/folio/dcb/listener/CirculationCheckInEventListenerTest.java index 71ccd6ae..b47d0c9b 100644 --- a/src/test/java/org/folio/dcb/listener/CirculationCheckInEventListenerTest.java +++ b/src/test/java/org/folio/dcb/listener/CirculationCheckInEventListenerTest.java @@ -11,7 +11,7 @@ import static org.folio.dcb.domain.dto.TransactionStatus.StatusEnum.CLOSED; import static org.folio.dcb.domain.dto.TransactionStatus.StatusEnum.EXPIRED; import static org.folio.dcb.utils.CqlQuery.exactMatchById; -import static org.folio.dcb.utils.EntityUtils.BORROWER_SERVICE_POINT_ID; +import static org.folio.dcb.utils.EntityUtils.VIRTUAL_SERVICE_POINT_ID; import static org.folio.dcb.utils.EntityUtils.DCB_TRANSACTION_ID; import static org.folio.dcb.utils.EntityUtils.EXISTED_PATRON_ID; import static org.folio.dcb.utils.EntityUtils.ITEM_ID; @@ -69,9 +69,9 @@ void handleCheckInEvent_positive_expiredDcbTransactionFound() { var validItem = new InventoryItem() .id(ITEM_ID) .status(new ItemStatus().name(AVAILABLE)) - .lastCheckIn(new ItemLastCheckIn().servicePointId(BORROWER_SERVICE_POINT_ID)); + .lastCheckIn(new ItemLastCheckIn().servicePointId(VIRTUAL_SERVICE_POINT_ID)); - when(repository.findExpiredLenderTransactionsByItemId(ITEM_UUID)).thenReturn(List.of(transactionEntity())); + when(repository.findExpiredTransactionsByItemId(ITEM_UUID)).thenReturn(List.of(transactionEntity())); when(repository.save(transactionEntityArgumentCaptor.capture())).then(v -> v.getArgument(0)); when(itemStorageClient.findByQuery(exactMatchById(ITEM_ID).getQuery())) .thenReturn(asSinglePage(prevItem)) @@ -92,7 +92,7 @@ void handleCheckInEvent_positive_itemWithValidServicePointNotFound() { .status(new ItemStatus().name(AWAITING_PICKUP)) .lastCheckIn(new ItemLastCheckIn().servicePointId(PICKUP_SERVICE_POINT_ID)); - when(repository.findExpiredLenderTransactionsByItemId(ITEM_UUID)).thenReturn(List.of(transactionEntity())); + when(repository.findExpiredTransactionsByItemId(ITEM_UUID)).thenReturn(List.of(transactionEntity())); when(itemStorageClient.findByQuery(exactMatchById(ITEM_ID).getQuery())).thenReturn(asSinglePage(item)); eventListener.handleCheckInEvent(CHECK_IN_EVENT_SAMPLE, messageHeaders()); @@ -105,9 +105,9 @@ void handleCheckInEvent_positive_itemWithValidServicePointNotFound() { void handleCheckInEvent_positive_expiredDcbTransactionFoundWithNotAvailableItem() { var item = new InventoryItem().id(ITEM_ID) .status(new ItemStatus().name(IN_TRANSIT)) - .lastCheckIn(new ItemLastCheckIn().servicePointId(BORROWER_SERVICE_POINT_ID)); + .lastCheckIn(new ItemLastCheckIn().servicePointId(VIRTUAL_SERVICE_POINT_ID)); - when(repository.findExpiredLenderTransactionsByItemId(ITEM_UUID)).thenReturn(List.of(transactionEntity())); + when(repository.findExpiredTransactionsByItemId(ITEM_UUID)).thenReturn(List.of(transactionEntity())); when(repository.save(transactionEntityArgumentCaptor.capture())).then(v -> v.getArgument(0)); when(itemStorageClient.findByQuery(exactMatchById(ITEM_ID).getQuery())).thenReturn(asSinglePage(item)); @@ -139,7 +139,7 @@ void handleCheckInEvent_parameterized_emptyMessageHeaders() { void handleCheckInEvent_positive_expiredDcbTransactionNotFound() { var item = new InventoryItem().id(ITEM_ID).status(new ItemStatus().name(AVAILABLE)); - when(repository.findExpiredLenderTransactionsByItemId(ITEM_UUID)).thenReturn(emptyList()); + when(repository.findExpiredTransactionsByItemId(ITEM_UUID)).thenReturn(emptyList()); when(itemStorageClient.findByQuery(exactMatchById(ITEM_ID).getQuery())).thenReturn(asSinglePage(item)); eventListener.handleCheckInEvent(CHECK_IN_EVENT_SAMPLE, messageHeaders()); diff --git a/src/test/java/org/folio/dcb/listener/CirculationRequestEventListenerTest.java b/src/test/java/org/folio/dcb/listener/CirculationRequestEventListenerTest.java index fccfc7ac..6984c1a2 100644 --- a/src/test/java/org/folio/dcb/listener/CirculationRequestEventListenerTest.java +++ b/src/test/java/org/folio/dcb/listener/CirculationRequestEventListenerTest.java @@ -13,6 +13,7 @@ import static org.mockito.Mockito.anyString; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.util.HashMap; @@ -20,6 +21,7 @@ import java.util.Optional; import java.util.UUID; import java.util.stream.Stream; +import org.folio.dcb.domain.dto.DcbTransaction.RoleEnum; import org.folio.dcb.domain.dto.ItemStatus; import org.folio.dcb.domain.dto.TransactionStatus; import org.folio.dcb.domain.entity.TransactionEntity; @@ -31,6 +33,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.MethodSource; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; @@ -232,22 +235,24 @@ void handleExpiredRequestEventTest() { assertEquals(TransactionStatus.StatusEnum.EXPIRED, savedValue.getStatus()); } - @Test - void handleExpiredRequestEventForNonLenderRoleTest() { + @ParameterizedTest + @EnumSource(value = RoleEnum.class, names = {"LENDER"}, mode = EnumSource.Mode.EXCLUDE) + void handleExpiredRequestEventForNonLenderRoleTest(RoleEnum role) { var transactionEntity = createTransactionEntity(); + transactionEntity.setRole(role); transactionEntity.setItemId("5b95877e-86c0-4cb7-a0cd-7660b348ae5d"); transactionEntity.setStatus(TransactionStatus.StatusEnum.AWAITING_PICKUP); - transactionEntity.setRole(BORROWER); - - var circulationItem = createCirculationItem(); - circulationItem.setStatus(ItemStatus.builder().name(ItemStatus.NameEnum.IN_TRANSIT).build()); + var transactionEntityCaptor = ArgumentCaptor.captor(); MessageHeaders messageHeaders = getMessageHeaders(); when(transactionRepository.findTransactionByRequestIdAndStatusNotInClosed(any())).thenReturn(Optional.of(transactionEntity)); - when(circulationItemService.fetchItemById(anyString())).thenReturn(circulationItem); + when(transactionRepository.save(transactionEntityCaptor.capture())).then(v -> v.getArgument(0)); eventListener.handleRequestEvent(REQUEST_EXPIRED_EVENT_FOR_DCB_SAMPLE, messageHeaders); - Mockito.verify(transactionRepository, never()).save(any()); + Mockito.verify(transactionRepository).save(any()); + var savedValue = transactionEntityCaptor.getValue(); + assertEquals(TransactionStatus.StatusEnum.EXPIRED, savedValue.getStatus()); + verify(circulationItemService, never()).fetchItemById(anyString()); } @Test diff --git a/src/test/java/org/folio/dcb/service/BaseLibraryServiceTest.java b/src/test/java/org/folio/dcb/service/BaseLibraryServiceTest.java index c9915883..8a3761e5 100644 --- a/src/test/java/org/folio/dcb/service/BaseLibraryServiceTest.java +++ b/src/test/java/org/folio/dcb/service/BaseLibraryServiceTest.java @@ -1,17 +1,25 @@ package org.folio.dcb.service; +import java.util.UUID; import org.folio.dcb.domain.ResultList; +import org.folio.dcb.domain.dto.DcbTransaction.RoleEnum; +import org.folio.dcb.domain.dto.ItemStatus; +import org.folio.dcb.domain.dto.ItemStatus.NameEnum; import org.folio.dcb.domain.dto.TransactionStatus; import org.folio.dcb.domain.dto.TransactionStatusResponse; import org.folio.dcb.domain.entity.TransactionEntity; import org.folio.dcb.domain.mapper.TransactionMapper; +import org.folio.dcb.exception.InventoryItemNotFound; import org.folio.dcb.exception.ResourceAlreadyExistException; import org.folio.dcb.repository.TransactionRepository; import org.folio.dcb.service.impl.BaseLibraryService; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.EnumSource; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @@ -20,6 +28,7 @@ import static org.folio.dcb.domain.dto.DcbTransaction.RoleEnum.BORROWER; import static org.folio.dcb.domain.dto.DcbTransaction.RoleEnum.BORROWING_PICKUP; +import static org.folio.dcb.domain.dto.DcbTransaction.RoleEnum.LENDER; import static org.folio.dcb.domain.dto.TransactionStatus.StatusEnum.CANCELLED; import static org.folio.dcb.domain.dto.TransactionStatus.StatusEnum.CLOSED; import static org.folio.dcb.domain.dto.TransactionStatus.StatusEnum.OPEN; @@ -53,22 +62,14 @@ @ExtendWith(MockitoExtension.class) class BaseLibraryServiceTest { - @InjectMocks - private BaseLibraryService baseLibraryService; - @Mock - private TransactionRepository transactionRepository; - @Mock - private UserService userService; - @Mock - private RequestService requestService; - @Mock - private CirculationItemService circulationItemService; - @Mock - private CirculationService circulationService; - @Mock - private TransactionMapper transactionMapper; - @Mock - private ItemService itemService; + @InjectMocks private BaseLibraryService baseLibraryService; + @Mock private TransactionRepository transactionRepository; + @Mock private UserService userService; + @Mock private RequestService requestService; + @Mock private CirculationItemService circulationItemService; + @Mock private CirculationService circulationService; + @Mock private TransactionMapper transactionMapper; + @Mock private ItemService itemService; @Test void updateTransactionWithWrongStatusTest() { @@ -184,7 +185,7 @@ void checkItemIfExistsInInventory() { void checkItemIfNotExistsInInventory() { var item = createDcbItem(); - when(itemService.fetchItemByBarcode(item.getBarcode())).thenReturn(ResultList.of(0, List.of())); + when(itemService.fetchItemByBarcode(item.getBarcode())).thenReturn(ResultList.empty()); baseLibraryService.checkItemExistsInInventoryAndThrow(item.getBarcode()); verify(itemService).fetchItemByBarcode(item.getBarcode()); @@ -217,4 +218,53 @@ void saveTransactionTest() { verify(transactionRepository).save(any()); } + @ParameterizedTest + @EnumSource(value = RoleEnum.class, names = "LENDER", mode = EnumSource.Mode.EXCLUDE) + void closeExpiredTransactionEntity_positive_parameterized(RoleEnum role) { + var entity = createTransactionEntity(role); + var servicePointId = UUID.randomUUID().toString(); + + baseLibraryService.closeExpiredTransactionEntity(entity, servicePointId); + + verify(transactionRepository).save(any()); + } + + @Test + void closeExpiredTransactionEntity_positive_lenderRole() { + var entity = createTransactionEntity(LENDER); + var item = createInventoryItem().status(new ItemStatus().name(NameEnum.AVAILABLE)); + var servicePointId = UUID.randomUUID().toString(); + when(itemService.findItemByIdAfterCheckIn(entity.getItemId(), servicePointId)).thenReturn(item); + + baseLibraryService.closeExpiredTransactionEntity(entity, servicePointId); + + verify(transactionRepository).save(any()); + } + + @ParameterizedTest + @CsvSource(nullValues = "null", value = {"LENDER, null", "LENDER, UNAVAILABLE", "LENDER, IN_TRANSIT"}) + void closeExpiredTransactionEntity_negative_parameterized(RoleEnum role, ItemStatus.NameEnum itemStatus) { + var entity = createTransactionEntity(role); + var itemStatusValue = itemStatus != null ? new ItemStatus().name(itemStatus) : null; + var item = createInventoryItem().status(itemStatusValue); + var servicePointId = UUID.randomUUID().toString(); + when(itemService.findItemByIdAfterCheckIn(entity.getItemId(), servicePointId)).thenReturn(item); + + baseLibraryService.closeExpiredTransactionEntity(entity, servicePointId); + + verify(transactionRepository, never()).save(any()); + } + + @ParameterizedTest + @EnumSource(value = RoleEnum.class, names = "LENDER", mode = EnumSource.Mode.INCLUDE) + void closeExpiredTransactionEntity_negative_itemNotFound(RoleEnum role) { + var entity = createTransactionEntity(role); + var servicePointId = UUID.randomUUID().toString(); + when(itemService.findItemByIdAfterCheckIn(entity.getItemId(), servicePointId)) + .thenThrow(new InventoryItemNotFound("not found")); + + baseLibraryService.closeExpiredTransactionEntity(entity, servicePointId); + + verify(transactionRepository, never()).save(any()); + } } diff --git a/src/test/java/org/folio/dcb/service/BorrowingLibraryServiceTest.java b/src/test/java/org/folio/dcb/service/BorrowingLibraryServiceTest.java index 41697ea8..550cc883 100644 --- a/src/test/java/org/folio/dcb/service/BorrowingLibraryServiceTest.java +++ b/src/test/java/org/folio/dcb/service/BorrowingLibraryServiceTest.java @@ -84,7 +84,7 @@ void createTransactionTest() { var servicePointRequest = createServicePointRequest(); var dcbTransaction = createDcbTransactionByRole(BORROWER); servicePointRequest.setId(UUID.randomUUID().toString()); - when(servicePointService.createServicePointIfNotExists(createDcbPickup())).thenReturn(servicePointRequest); + when(servicePointService.createServicePointIfNotExists(dcbTransaction)).thenReturn(servicePointRequest); borrowingLibraryService.createCirculation(DCB_TRANSACTION_ID, dcbTransaction); assertEquals(servicePointRequest.getId(), dcbTransaction.getPickup().getServicePointId()); verify(baseLibraryService).createBorrowingLibraryTransaction(DCB_TRANSACTION_ID, dcbTransaction, servicePointRequest.getId()); diff --git a/src/test/java/org/folio/dcb/service/LendingLibraryServiceTest.java b/src/test/java/org/folio/dcb/service/LendingLibraryServiceTest.java index 04336ce1..d7578bf3 100644 --- a/src/test/java/org/folio/dcb/service/LendingLibraryServiceTest.java +++ b/src/test/java/org/folio/dcb/service/LendingLibraryServiceTest.java @@ -4,7 +4,6 @@ import org.folio.dcb.domain.dto.TransactionStatus; import org.folio.dcb.domain.dto.TransactionStatusResponse; import org.folio.dcb.domain.entity.TransactionEntity; -import org.folio.dcb.exception.InventoryItemNotFound; import org.folio.dcb.repository.TransactionRepository; import org.folio.dcb.service.impl.BaseLibraryService; import org.folio.dcb.service.impl.CirculationServiceImpl; @@ -21,14 +20,12 @@ import java.util.UUID; import static org.folio.dcb.domain.dto.DcbTransaction.RoleEnum.LENDER; -import static org.folio.dcb.domain.dto.ItemStatus.NameEnum.IN_TRANSIT; import static org.folio.dcb.utils.EntityUtils.CIRCULATION_REQUEST_ID; import static org.folio.dcb.utils.EntityUtils.DCB_TRANSACTION_ID; import static org.folio.dcb.utils.EntityUtils.createCirculationRequest; import static org.folio.dcb.utils.EntityUtils.createDcbItem; import static org.folio.dcb.utils.EntityUtils.createDefaultDcbPatron; import static org.folio.dcb.utils.EntityUtils.createDcbTransactionByRole; -import static org.folio.dcb.utils.EntityUtils.createInventoryItem; import static org.folio.dcb.utils.EntityUtils.createServicePointRequest; import static org.folio.dcb.utils.EntityUtils.createTransactionEntity; import static org.folio.dcb.utils.EntityUtils.createTransactionStatus; @@ -38,7 +35,6 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -60,13 +56,12 @@ void createTransactionTest() { var patron = createDefaultDcbPatron(); var user = createUser(); var dcbTransaction = createDcbTransactionByRole(LENDER); - var dcbPickup = dcbTransaction.getPickup(); var servicePoint = createServicePointRequest(); servicePoint.setId(UUID.randomUUID().toString()); when(userService.fetchOrCreateUser(any())) .thenReturn(user); - when(servicePointService.createServicePointIfNotExists(dcbPickup)).thenReturn(servicePoint); + when(servicePointService.createServicePointIfNotExists(dcbTransaction)).thenReturn(servicePoint); when(requestService.createRequestBasedOnItemStatus(any(), any(), anyString())).thenReturn(createCirculationRequest()); doNothing().when(baseLibraryService).saveDcbTransaction(any(), any(), any()); @@ -125,52 +120,4 @@ void updateTransactionWithWrongStatusTest() { TransactionStatus transactionStatus = createTransactionStatus(TransactionStatus.StatusEnum.AWAITING_PICKUP); assertThrows(IllegalArgumentException.class, () -> lendingLibraryService.updateTransactionStatus(transactionEntity, transactionStatus)); } - - @Test - void closeExpiredTransactionEntityAvailableItem() { - var entity = createTransactionEntity(); - var item = createInventoryItem(); - var servicePointId = UUID.randomUUID().toString(); - when(itemService.findItemByIdAfterCheckIn(entity.getItemId(), servicePointId)).thenReturn(item); - - lendingLibraryService.closeExpiredTransactionEntity(entity, servicePointId); - - verify(transactionRepository).save(any()); - } - - @Test - void closeExpiredTransactionEntityUnavailableItem() { - var entity = createTransactionEntity(); - var item = createInventoryItem().status(new ItemStatus().name(IN_TRANSIT)); - var servicePointId = UUID.randomUUID().toString(); - when(itemService.findItemByIdAfterCheckIn(entity.getItemId(), servicePointId)).thenReturn(item); - - lendingLibraryService.closeExpiredTransactionEntity(entity, servicePointId); - - verify(transactionRepository, never()).save(any()); - } - - @Test - void closeExpiredTransactionEntityNullStatus() { - var entity = createTransactionEntity(); - var item = createInventoryItem().status(null); - var servicePointId = UUID.randomUUID().toString(); - when(itemService.findItemByIdAfterCheckIn(entity.getItemId(), servicePointId)).thenReturn(item); - - lendingLibraryService.closeExpiredTransactionEntity(entity, servicePointId); - - verify(transactionRepository, never()).save(any()); - } - - @Test - void closeExpiredTransactionEntityMatchedItemNotFound() { - var entity = createTransactionEntity(); - var servicePointId = UUID.randomUUID().toString(); - when(itemService.findItemByIdAfterCheckIn(entity.getItemId(), servicePointId)) - .thenThrow(new InventoryItemNotFound("not found")); - - lendingLibraryService.closeExpiredTransactionEntity(entity, servicePointId); - - verify(transactionRepository, never()).save(any()); - } } diff --git a/src/test/java/org/folio/dcb/service/ServicePointExpirationPeriodServiceTest.java b/src/test/java/org/folio/dcb/service/ServicePointExpirationPeriodServiceTest.java index 69706e77..cff72acd 100644 --- a/src/test/java/org/folio/dcb/service/ServicePointExpirationPeriodServiceTest.java +++ b/src/test/java/org/folio/dcb/service/ServicePointExpirationPeriodServiceTest.java @@ -1,68 +1,143 @@ package org.folio.dcb.service; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.assertj.core.api.Assertions.assertThat; +import static org.folio.dcb.domain.ResultList.asSinglePage; +import static org.folio.dcb.domain.dto.DcbTransaction.RoleEnum.LENDER; +import static org.folio.dcb.domain.dto.IntervalIdEnum.HOURS; +import static org.folio.dcb.domain.dto.IntervalIdEnum.MINUTES; +import static org.folio.dcb.domain.dto.IntervalIdEnum.MONTHS; +import static org.folio.dcb.service.ServicePointExpirationPeriodService.getSettingsKey; +import static org.junit.jupiter.params.provider.Arguments.arguments; import static org.mockito.Mockito.when; +import java.util.Collections; import java.util.List; +import java.util.Map; +import java.util.UUID; import java.util.stream.Stream; - +import org.folio.dcb.domain.ResultList; import org.folio.dcb.domain.dto.HoldShelfExpiryPeriod; import org.folio.dcb.domain.dto.IntervalIdEnum; +import org.folio.dcb.domain.dto.Setting; +import org.folio.dcb.domain.dto.SettingScope; import org.folio.dcb.domain.entity.ServicePointExpirationPeriodEntity; import org.folio.dcb.repository.ServicePointExpirationPeriodRepository; import org.folio.dcb.service.impl.ServicePointExpirationPeriodServiceImpl; import org.folio.dcb.utils.DCBConstants; +import org.folio.dcb.utils.JsonTestUtils; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.databind.node.ObjectNode; @ExtendWith(MockitoExtension.class) class ServicePointExpirationPeriodServiceTest { - @InjectMocks - private ServicePointExpirationPeriodServiceImpl servicePointExpirationPeriodService; - @Mock - private ServicePointExpirationPeriodRepository servicePointExpirationPeriodRepository; + + private static final String SETTING_KEY = "test.hold-shelf-expiry-period"; + @InjectMocks private ServicePointExpirationPeriodServiceImpl servicePointExpirationPeriodService; + @Mock private ServicePointExpirationPeriodRepository servicePointExpirationPeriodRepository; + @Mock private SettingService settingService; + @Spy private JsonMapper jsonMapper = JsonTestUtils.JSON_MAPPER; + + @Test + void getShelfExpiryPeriod_positive_settingFound() { + var holdShelfExpiryPeriod = new HoldShelfExpiryPeriod().duration(15).intervalId(MINUTES); + var setting = setting(jsonMapper.valueToTree(holdShelfExpiryPeriod)); + when(settingService.findByQuery("key==\"%s\"".formatted(SETTING_KEY), 1, 0)).thenReturn(asSinglePage(setting)); + + var actual = servicePointExpirationPeriodService.getShelfExpiryPeriod(SETTING_KEY); + + assertThat(actual).isEqualTo(holdShelfExpiryPeriod); + } + + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource("invalidSettingDataSource") + void getShelfExpiryPeriod_parameterized_validSettingNotFound(@SuppressWarnings("unused") String name, Setting setting) { + var settingKey = getSettingsKey(LENDER.getValue()); + when(settingService.findByQuery("key==\"%s\"".formatted(settingKey), 1, 0)).thenReturn(asSinglePage(setting)); + when(servicePointExpirationPeriodRepository.findAll()).thenReturn(Collections.emptyList()); + + var actual = servicePointExpirationPeriodService.getShelfExpiryPeriod(settingKey); + + assertThat(settingKey).isEqualTo("lender.hold-shelf-expiry-period"); + assertThat(actual).isEqualTo(DCBConstants.DEFAULT_PERIOD); + } @ParameterizedTest - @MethodSource - void getShelfExpiryPeriodTest(List periods, + @MethodSource("databaseShelfExpiryDataSource") + void getShelfExpiryPeriod_positive_settingFoundInDatabase(List periods, HoldShelfExpiryPeriod expected) { + when(settingService.findByQuery("key==\"%s\"".formatted(SETTING_KEY), 1, 0)).thenReturn(ResultList.empty()); when(servicePointExpirationPeriodRepository.findAll()).thenReturn(periods); - HoldShelfExpiryPeriod actual = servicePointExpirationPeriodService.getShelfExpiryPeriod(); - assertEquals(expected, actual); + var actual = servicePointExpirationPeriodService.getShelfExpiryPeriod(SETTING_KEY); + assertThat(actual).isEqualTo(expected); } - private static Stream getShelfExpiryPeriodTest() { + private static Stream databaseShelfExpiryDataSource() { return Stream.of( - Arguments.of(List.of(), DCBConstants.DEFAULT_PERIOD), - Arguments.of(buildServicePointExpirationPeriodList(2, IntervalIdEnum.MONTHS), - buildExpectedHoldShelfPeriod(2, IntervalIdEnum.MONTHS)), - Arguments.of(buildServicePointExpirationPeriodList(3, IntervalIdEnum.HOURS), - buildExpectedHoldShelfPeriod(3, IntervalIdEnum.HOURS)), - Arguments.of(buildServicePointExpirationPeriodList(4, IntervalIdEnum.MINUTES), - buildExpectedHoldShelfPeriod(4, IntervalIdEnum.MINUTES)) + arguments(List.of(), DCBConstants.DEFAULT_PERIOD), + arguments(servicePointExpirationPeriodList(2, MONTHS), holdShelfPeriod(2, MONTHS)), + arguments(servicePointExpirationPeriodList(3, HOURS), holdShelfPeriod(3, HOURS)), + arguments(servicePointExpirationPeriodList(4, MINUTES), holdShelfPeriod(4, MINUTES)) + ); + } + private static Stream invalidSettingDataSource() { + var invalidContentNode = JsonTestUtils.JSON_MAPPER.createObjectNode().put("invalidKey", "invalidValue"); + return Stream.of( + arguments("null value", null), + arguments("duration is null and intervalId is null", setting(holdShelfPeriodNode(null, null))), + arguments("intervalId is null", setting(holdShelfPeriodNode(2L, null))), + arguments("duration is null", setting(holdShelfPeriodNode(null, "Minutes"))), + arguments("intervalId in lowercase", setting(holdShelfPeriodNode(10L, "minutes"))), + arguments("intervalId in uppercase", setting(holdShelfPeriodNode(10L, "MINUTES"))), + arguments("duration is max long value", setting(holdShelfPeriodNode(Long.MAX_VALUE, "Minutes"))), + arguments("node with invalid content", setting(invalidContentNode)), + arguments("null in value", setting(null)), + arguments("boolean in value", setting(false)), + arguments("empty string in value", setting("")), + arguments("string in value", setting("invalid-content")), + arguments("integer in value", setting(100)), + arguments("json string in value", setting("{\"duration\":\"10\",\"intervalId\":\"Minutes\"}")), + arguments("long in value", setting(Long.MIN_VALUE)), + arguments("invalid map in value", setting(Map.of("invalidKey", "invalidValue"))), + arguments("array in value", setting(List.of(holdShelfPeriodNode(1L, "Days"), holdShelfPeriodNode(2L, "Days")))) ); } - private static HoldShelfExpiryPeriod buildExpectedHoldShelfPeriod(int duration, - IntervalIdEnum intervalId) { + private static HoldShelfExpiryPeriod holdShelfPeriod(int duration, IntervalIdEnum intervalId) { return HoldShelfExpiryPeriod.builder() .duration(duration) .intervalId(intervalId) .build(); } - private static List buildServicePointExpirationPeriodList( - int duration, - IntervalIdEnum intervalId) { + private static ObjectNode holdShelfPeriodNode(Long duration, String intervalId) { + return JsonTestUtils.JSON_MAPPER.createObjectNode() + .put("duration", duration) + .put("intervalId", intervalId); + } + + private static List servicePointExpirationPeriodList( + int duration, IntervalIdEnum intervalId) { return List.of(ServicePointExpirationPeriodEntity.builder() .duration(duration) .intervalId(intervalId) .build()); } + + public static Setting setting(Object value) { + return new Setting() + .id(UUID.randomUUID()) + .scope(SettingScope.MOD_DCB) + .key(SETTING_KEY) + .value(value); + } } diff --git a/src/test/java/org/folio/dcb/service/ServicePointServiceTest.java b/src/test/java/org/folio/dcb/service/ServicePointServiceTest.java index f843dfa2..fd2b4113 100644 --- a/src/test/java/org/folio/dcb/service/ServicePointServiceTest.java +++ b/src/test/java/org/folio/dcb/service/ServicePointServiceTest.java @@ -1,6 +1,7 @@ package org.folio.dcb.service; import static org.folio.dcb.utils.EntityUtils.createDcbPickup; +import static org.folio.dcb.utils.EntityUtils.createDcbTransactionByRole; import static org.folio.dcb.utils.EntityUtils.createServicePointRequest; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; @@ -12,6 +13,8 @@ import java.util.List; import java.util.UUID; +import org.folio.dcb.domain.dto.DcbTransaction; +import org.folio.dcb.domain.dto.DcbTransaction.RoleEnum; import org.folio.dcb.integration.invstorage.ServicePointClient; import org.folio.dcb.domain.ResultList; import org.folio.dcb.domain.dto.HoldShelfExpiryPeriod; @@ -27,25 +30,19 @@ @ExtendWith(MockitoExtension.class) class ServicePointServiceTest { - @InjectMocks - private ServicePointServiceImpl servicePointService; + private static final String SETTING_KEY = "lender.hold-shelf-expiry-period"; - @Mock - private ServicePointClient servicePointClient; - - @Mock - private CalendarService calendarService; - - @Mock - private static ServicePointExpirationPeriodService servicePointExpirationPeriodService; + @InjectMocks private ServicePointServiceImpl servicePointService; + @Mock private ServicePointClient servicePointClient; + @Mock private CalendarService calendarService; + @Mock private static ServicePointExpirationPeriodService servicePointExpirationPeriodService; @Test void createServicePointIfNotExistsTest() { - when(servicePointClient.findByQuery(any())).thenReturn(ResultList.of(0, List.of())); - when(servicePointClient.createServicePoint(any())) - .thenReturn(createServicePointRequest()); - when(servicePointExpirationPeriodService.getShelfExpiryPeriod()).thenReturn(DCBConstants.DEFAULT_PERIOD); - var response = servicePointService.createServicePointIfNotExists(createDcbPickup()); + when(servicePointClient.findByQuery(any())).thenReturn(ResultList.empty()); + when(servicePointClient.createServicePoint(any())).thenReturn(createServicePointRequest()); + when(servicePointExpirationPeriodService.getShelfExpiryPeriod(SETTING_KEY)).thenReturn(DCBConstants.DEFAULT_PERIOD); + var response = servicePointService.createServicePointIfNotExists(createDcbTransactionByRole(RoleEnum.LENDER)); verify(servicePointClient).createServicePoint(any()); verify(servicePointClient).findByQuery(any()); verify(calendarService).addServicePointIdToDefaultCalendar(UUID.fromString(response.getId())); @@ -58,13 +55,13 @@ void createServicePointIfExistsTest() { var servicePointId = UUID.randomUUID().toString(); servicePointRequest.setId(servicePointId); when(servicePointClient.findByQuery(any())).thenReturn(ResultList.of(0, List.of(servicePointRequest))); - when(servicePointExpirationPeriodService.getShelfExpiryPeriod()).thenReturn( + when(servicePointExpirationPeriodService.getShelfExpiryPeriod(SETTING_KEY)).thenReturn( HoldShelfExpiryPeriod.builder() .duration(2) .intervalId(IntervalIdEnum.MONTHS) .build() ); - var response = servicePointService.createServicePointIfNotExists(createDcbPickup()); + var response = servicePointService.createServicePointIfNotExists(createDcbTransactionByRole(RoleEnum.LENDER)); assertEquals(servicePointId, response.getId()); assertEquals(2, response.getHoldShelfExpiryPeriod().getDuration()); assertEquals(IntervalIdEnum.MONTHS, response.getHoldShelfExpiryPeriod().getIntervalId()); diff --git a/src/test/java/org/folio/dcb/service/SettingServiceTest.java b/src/test/java/org/folio/dcb/service/SettingServiceTest.java new file mode 100644 index 00000000..8af801e7 --- /dev/null +++ b/src/test/java/org/folio/dcb/service/SettingServiceTest.java @@ -0,0 +1,233 @@ +package org.folio.dcb.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import org.folio.dcb.domain.ResultList; +import org.folio.dcb.domain.dto.Metadata; +import org.folio.dcb.domain.dto.Setting; +import org.folio.dcb.domain.dto.SettingScope; +import org.folio.dcb.domain.entity.SettingEntity; +import org.folio.dcb.domain.mapper.SettingMapper; +import org.folio.dcb.repository.SettingRepository; +import org.folio.dcb.support.types.UnitTest; +import org.folio.spring.data.OffsetRequest; +import org.folio.spring.exception.NotFoundException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.PageImpl; + +@UnitTest +@ExtendWith(MockitoExtension.class) +class SettingServiceTest { + + private static final UUID SETTING_ID_1 = UUID.randomUUID(); + private static final UUID SETTING_ID_2 = UUID.randomUUID(); + private static final String SETTING_KEY_1 = "dcb.setting#1"; + private static final String SETTING_KEY_2 = "dcb.setting#2"; + private static final String SETTING_VALUE_1 = "value#1"; + private static final String SETTING_VALUE_2 = "value#2"; + private static final String USER_ID = UUID.randomUUID().toString(); + + @InjectMocks private SettingService settingService; + @Mock private SettingMapper settingMapper; + @Mock private SettingRepository settingRepository; + + @Test + void createSetting_positive() { + var setting = setting(); + when(settingRepository.existsById(SETTING_ID_1)).thenReturn(false); + when(settingRepository.existsByKey(SETTING_KEY_1)).thenReturn(false); + when(settingMapper.convert(setting)).thenReturn(settingEntity()); + when(settingRepository.save(settingEntity())).thenReturn(settingEntity()); + when(settingMapper.convert(settingEntity())).thenReturn(setting()); + + var result = settingService.createSetting(setting); + + assertThat(result).isEqualTo(setting()); + } + + @Test + void createSetting_negative_existsById() { + var setting = setting(); + when(settingRepository.existsById(SETTING_ID_1)).thenReturn(true); + + assertThatThrownBy(() -> settingService.createSetting(setting)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Setting already exists with id: " + SETTING_ID_1); + verify(settingRepository, never()).save(any()); + } + + @Test + void createSetting_negative_existsByKey() { + var setting = setting(); + when(settingRepository.existsById(SETTING_ID_1)).thenReturn(false); + when(settingRepository.existsByKey(SETTING_KEY_1)).thenReturn(true); + + assertThatThrownBy(() -> settingService.createSetting(setting)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Setting with key already exists: " + SETTING_KEY_1); + verify(settingRepository, never()).save(any()); + } + + @Test + void getSettingById_positive_returnsSetting() { + var entity = settingEntity(); + var expectedSetting = setting(); + when(settingRepository.findById(SETTING_ID_1)).thenReturn(Optional.of(entity)); + when(settingMapper.convert(entity)).thenReturn(expectedSetting); + + var result = settingService.getSettingById(SETTING_ID_1); + + assertThat(result).isEqualTo(expectedSetting); + } + + @Test + void getSettingById_negative_settingNotFound() { + when(settingRepository.findById(SETTING_ID_1)).thenReturn(Optional.empty()); + assertThatThrownBy(() -> settingService.getSettingById(SETTING_ID_1)) + .isInstanceOf(NotFoundException.class) + .hasMessage("Setting not found by id: " + SETTING_ID_1); + } + + @Test + void findByQuery_positive_returnsSettingsCollection() { + var id1 = UUID.randomUUID(); + var id2 = UUID.randomUUID(); + var query = "cql.allRecords=1"; + var entity1 = settingEntity(id1, "dcb.setting#1", "value#1"); + var entity2 = settingEntity(id2, "dcb.setting#2", "value#2"); + var setting1 = setting(SETTING_ID_1, SETTING_KEY_1, SETTING_VALUE_1); + var setting2 = setting(SETTING_ID_2, SETTING_KEY_2, SETTING_VALUE_2); + var pageable = OffsetRequest.of(0, 100); + var pageResult = new PageImpl<>(List.of(entity1, entity2), pageable, 2); + + when(settingRepository.findByQuery(query, pageable)).thenCallRealMethod(); + when(settingRepository.findByCql(query, pageable)).thenReturn(pageResult); + when(settingMapper.convert(entity1)).thenReturn(setting1); + when(settingMapper.convert(entity2)).thenReturn(setting2); + + var result = settingService.findByQuery(query, 100, 0); + + assertThat(result).isEqualTo(ResultList.asSinglePage(setting1, setting2)); + } + + @Test + void findByQuery_positive_emptyQuery() { + var id1 = UUID.randomUUID(); + var id2 = UUID.randomUUID(); + var entity1 = settingEntity(id1, "dcb.setting#1", "value#1"); + var entity2 = settingEntity(id2, "dcb.setting#2", "value#2"); + var setting1 = setting(SETTING_ID_1, SETTING_KEY_1, SETTING_VALUE_1); + var setting2 = setting(SETTING_ID_2, SETTING_KEY_2, SETTING_VALUE_2); + var pageable = OffsetRequest.of(0, 100); + var pageResult = new PageImpl<>(List.of(entity1, entity2), pageable, 2); + + when(settingRepository.findByQuery(null, pageable)).thenCallRealMethod(); + when(settingRepository.findAll(pageable)).thenReturn(pageResult); + when(settingMapper.convert(entity1)).thenReturn(setting1); + when(settingMapper.convert(entity2)).thenReturn(setting2); + + var result = settingService.findByQuery(null, 100, 0); + + assertThat(result).isEqualTo(ResultList.asSinglePage(setting1, setting2)); + } + + @Test + void updateSetting_positive_updatesSetting() { + var updatedSetting = setting(SETTING_ID_1, SETTING_KEY_1, SETTING_VALUE_1); + var updatedEntity = settingEntity(SETTING_ID_1, SETTING_KEY_1, SETTING_VALUE_1); + when(settingRepository.findById(SETTING_ID_1)).thenReturn(Optional.of(settingEntity())); + when(settingMapper.convert(updatedSetting)).thenReturn(updatedEntity); + + settingService.updateSetting(updatedSetting); + + verify(settingRepository).save(updatedEntity); + } + + @Test + void updateSetting_negative_settingNotFound() { + var updatedSetting = setting(SETTING_ID_1, SETTING_KEY_1, SETTING_VALUE_1); + when(settingRepository.findById(SETTING_ID_1)).thenReturn(Optional.empty()); + assertThatThrownBy(() -> settingService.updateSetting(updatedSetting)) + .isInstanceOf(NotFoundException.class) + .hasMessage("Setting not found by id: " + SETTING_ID_1); + } + + @Test + void updateSetting_negative_keyModification() { + var updatedSetting = setting(SETTING_ID_1, SETTING_KEY_2, SETTING_VALUE_2); + when(settingRepository.findById(SETTING_ID_1)).thenReturn(Optional.of(settingEntity())); + assertThatThrownBy(() -> settingService.updateSetting(updatedSetting)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Setting key cannot be modified: " + SETTING_KEY_1); + } + + @Test + void deleteSettingById_positive() { + when(settingRepository.findById(SETTING_ID_1)).thenReturn(Optional.of(settingEntity())); + settingService.deleteSettingById(SETTING_ID_1); + verify(settingRepository).deleteById(SETTING_ID_1); + } + + @Test + void deleteSettingById_negative_notFoundById() { + when(settingRepository.findById(SETTING_ID_1)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> settingService.deleteSettingById(SETTING_ID_1)) + .isInstanceOf(NotFoundException.class) + .hasMessage("Setting not found by id: " + SETTING_ID_1); + } + + private static Setting setting() { + return setting(SETTING_ID_1, SETTING_KEY_1, SETTING_VALUE_1); + } + + private static Setting setting(UUID id, String key, String value) { + var updatedDate = OffsetDateTime.of(LocalDate.ofYearDay(2025, 1), LocalTime.MIDNIGHT, ZoneOffset.UTC); + return new Setting() + .id(id) + .scope(SettingScope.MOD_DCB) + .key(key) + .value(value) + .version(1) + .metadata(new Metadata() + .createdByUserId(USER_ID) + .createdDate(updatedDate) + .updatedByUserId(USER_ID) + .updatedDate(updatedDate)); + } + + private static SettingEntity settingEntity() { + return settingEntity(SETTING_ID_1, SETTING_KEY_1, SETTING_VALUE_1); + } + + private static SettingEntity settingEntity(UUID id, String key, String value) { + var updatedDate = OffsetDateTime.of(LocalDate.ofYearDay(2025, 1), LocalTime.MIDNIGHT, ZoneOffset.UTC); + var entity = new SettingEntity(); + entity.setId(id); + entity.setScope(SettingScope.MOD_DCB.getValue()); + entity.setKey(key); + entity.setValue(value); + entity.setVersion(1); + entity.setCreatedBy(UUID.fromString(USER_ID)); + entity.setCreatedDate(updatedDate); + entity.setUpdatedBy(UUID.fromString(USER_ID)); + entity.setUpdatedDate(updatedDate); + return entity; + } +} diff --git a/src/test/java/org/folio/dcb/service/entities/DcbServicePointServiceTest.java b/src/test/java/org/folio/dcb/service/entities/DcbServicePointServiceTest.java index cabfdbe7..6d3da724 100644 --- a/src/test/java/org/folio/dcb/service/entities/DcbServicePointServiceTest.java +++ b/src/test/java/org/folio/dcb/service/entities/DcbServicePointServiceTest.java @@ -25,6 +25,7 @@ @ExtendWith(MockitoExtension.class) class DcbServicePointServiceTest { + private static final String DCB_SETTING_KEY = "dcb.hold-shelf-expiry-period"; private static final String TEST_SERVICE_POINT_ID = "9d1b77e8-f02e-4b7f-b296-3f2042ddac54"; private static final String TEST_NAME = "DCB"; private static final String TEST_CODE = "000"; @@ -62,13 +63,13 @@ void createDcbEntity_positive_shouldCreateServicePointWithExpiryPeriod() { var expiryPeriod = dcbExpiryPeriod(); var expectedServicePoint = dcbServicePoint(); - when(servicePointExpirationPeriodService.getShelfExpiryPeriod()).thenReturn(expiryPeriod); + when(servicePointExpirationPeriodService.getShelfExpiryPeriod(DCB_SETTING_KEY)).thenReturn(expiryPeriod); when(servicePointClient.createServicePoint(expectedServicePoint)).thenReturn(expectedServicePoint); var result = dcbServicePointService.createDcbEntity(); assertThat(result).isEqualTo(dcbServicePoint()); - verify(servicePointExpirationPeriodService).getShelfExpiryPeriod(); + verify(servicePointExpirationPeriodService).getShelfExpiryPeriod(DCB_SETTING_KEY); verify(servicePointClient).createServicePoint(expectedServicePoint); } @@ -87,7 +88,7 @@ void findOrCreateEntity_positive_shouldReturnExistingServicePoint() { var result = dcbServicePointService.findOrCreateEntity(); assertThat(result).isEqualTo(dcbServicePoint()); - verify(servicePointExpirationPeriodService, never()).getShelfExpiryPeriod(); + verify(servicePointExpirationPeriodService, never()).getShelfExpiryPeriod(DCB_SETTING_KEY); verify(servicePointClient, never()).createServicePoint(any()); } diff --git a/src/test/java/org/folio/dcb/utils/EntityUtils.java b/src/test/java/org/folio/dcb/utils/EntityUtils.java index 252c165b..4d80c60f 100644 --- a/src/test/java/org/folio/dcb/utils/EntityUtils.java +++ b/src/test/java/org/folio/dcb/utils/EntityUtils.java @@ -3,6 +3,11 @@ import lombok.SneakyThrows; import org.apache.commons.io.IOUtils; import org.folio.dcb.DcbApplication; +import org.folio.dcb.domain.dto.DcbTransaction.RoleEnum; +import org.folio.dcb.domain.dto.HoldShelfExpiryPeriod; +import org.folio.dcb.domain.dto.IntervalIdEnum; +import org.folio.dcb.domain.dto.Setting; +import org.folio.dcb.domain.dto.SettingScope; import org.folio.dcb.integration.invstorage.model.InventoryHolding; import org.folio.dcb.domain.dto.Calendar; import org.folio.dcb.domain.dto.CalendarCollection; @@ -23,7 +28,6 @@ import org.folio.dcb.domain.dto.InventoryItem; import org.folio.dcb.domain.dto.UserGroupCollection; import org.folio.dcb.domain.dto.UserGroup; -import org.folio.dcb.domain.dto.UserCollection; import java.io.InputStream; import java.nio.charset.StandardCharsets; @@ -63,7 +67,7 @@ public class EntityUtils { * */ public static final String EXISTED_PATRON_ID = "284056f5-0670-4e1e-9e2f-61b9f1ee2d18"; public static final String PICKUP_SERVICE_POINT_ID = "0da8c1e4-1c1f-4dd9-b189-70ba978b7d94"; - public static final String BORROWER_SERVICE_POINT_ID = "9d1b77e8-f02e-4b7f-b296-3f2042ddac55"; + public static final String VIRTUAL_SERVICE_POINT_ID = "9d1b77e8-f02e-4b7f-b296-3f2042ddac55"; public static final String DCB_TRANSACTION_ID = "571b0a2c-8883-40b5-a449-d41fe6017082"; public static final String CIRCULATION_REQUEST_ID = "571b0a2c-8883-40b5-a449-d41fe6017083"; @@ -80,6 +84,8 @@ public class EntityUtils { public static final String HOLDING_RECORD_ID = "fcee331d-2b50-49de-9395-a76a6ff4e385"; public static final String INSTANCE_ID = "a9350401-f2f2-4804-9701-ca813c70e322"; + public static final String LENDER_HOLD_SHELF_EXPIRATION_SETTING_ID = "d115a0b6-133d-4148-ac1a-b48c2aa57f57"; + public static DcbTransaction createDcbTransactionByRole(DcbTransaction.RoleEnum role) { return DcbTransaction.builder() .item(createDcbItem()) @@ -251,17 +257,15 @@ public static User createUser() { .build(); } - public static UserCollection createUserCollection() { - return UserCollection.builder() - .users(List.of(createUser())) - .totalRecords(1) - .build(); + public static TransactionEntity createTransactionEntity() { + return createTransactionEntity(null); } - public static TransactionEntity createTransactionEntity() { + public static TransactionEntity createTransactionEntity(RoleEnum role) { return TransactionEntity.builder() .id(DCB_TRANSACTION_ID) .itemId(ITEM_ID) + .role(role) .itemTitle("ITEM TITLE") .itemBarcode("DCB_ITEM") .patronId(NOT_EXISTED_PATRON_ID) @@ -392,7 +396,7 @@ public static DcbTransaction borrowerDcbTransaction(DcbPatron dcbPatron, Boolean .item(dcbItem()) .patron(dcbPatron) .role(BORROWER) - .pickup(dcbPickup().servicePointId(BORROWER_SERVICE_POINT_ID)) + .pickup(dcbPickup().servicePointId(VIRTUAL_SERVICE_POINT_ID)) .selfBorrowing(selfBorrowing); } @@ -461,4 +465,17 @@ public static DcbUpdateTransaction dcbTransactionUpdate() { .build()) .build(); } + + public static Setting lenderHoldShelfExpirationSetting() { + var value = new HoldShelfExpiryPeriod().duration(10).intervalId(IntervalIdEnum.DAYS); + return lenderHoldShelfExpirationSetting(value); + } + + public static Setting lenderHoldShelfExpirationSetting(HoldShelfExpiryPeriod value) { + return new Setting() + .id(UUID.fromString(LENDER_HOLD_SHELF_EXPIRATION_SETTING_ID)) + .scope(SettingScope.MOD_DCB) + .key("lender.hold-shelf-expiry-period") + .value(value); + } } diff --git a/src/test/java/org/folio/dcb/utils/EventDataProvider.java b/src/test/java/org/folio/dcb/utils/EventDataProvider.java index c18134ac..792a25a0 100644 --- a/src/test/java/org/folio/dcb/utils/EventDataProvider.java +++ b/src/test/java/org/folio/dcb/utils/EventDataProvider.java @@ -7,7 +7,7 @@ import static org.folio.dcb.utils.EntityUtils.PICKUP_SERVICE_POINT_ID; import static org.folio.dcb.utils.EntityUtils.REQUEST_ID; import static org.folio.dcb.utils.EntityUtils.TEST_TENANT; -import static org.folio.dcb.utils.JsonTestUtils.objectMapper; +import static org.folio.dcb.utils.JsonTestUtils.JSON_MAPPER; import static org.folio.dcb.utils.JsonTestUtils.toJsonNode; import java.time.OffsetDateTime; @@ -38,13 +38,13 @@ public static ProducerRecord itemCheckInMessage(String tenantId) } private static JsonNode expiredRequestPayload() { - var objectNode = objectMapper.createObjectNode(); + var objectNode = JSON_MAPPER.createObjectNode(); objectNode.put("id", REQUEST_ID); objectNode.put("type", "UPDATED"); objectNode.put("tenant", TEST_TENANT); objectNode.put("timestamp", System.currentTimeMillis()); - var dataNode = objectMapper.createObjectNode(); + var dataNode = JSON_MAPPER.createObjectNode(); dataNode.set("new", toJsonNode(circulationRequest(CLOSED_PICKUP_EXPIRED))); dataNode.set("old", toJsonNode(circulationRequest(OPEN_AWAITING_PICKUP))); @@ -53,13 +53,13 @@ private static JsonNode expiredRequestPayload() { } private static JsonNode itemCheckInPayload() { - var objectNode = objectMapper.createObjectNode(); + var objectNode = JSON_MAPPER.createObjectNode(); objectNode.put("id", UUID.randomUUID().toString()); objectNode.put("type", "CREATED"); objectNode.put("tenant", TEST_TENANT); objectNode.put("timestamp", System.currentTimeMillis()); - var dataNode = objectMapper.createObjectNode(); + var dataNode = JSON_MAPPER.createObjectNode(); dataNode.set("new", toJsonNode(checkInRequest())); objectNode.set("data", dataNode); @@ -75,7 +75,7 @@ private static CirculationRequest circulationRequest(RequestStatus status) { } private static JsonNode checkInRequest() { - var checkInBody = objectMapper.createObjectNode(); + var checkInBody = JSON_MAPPER.createObjectNode(); checkInBody.put("id", UUID.randomUUID().toString()); checkInBody.put("occurredDateTime", OffsetDateTime.now().toString()); checkInBody.put("itemId", ITEM_ID); diff --git a/src/test/java/org/folio/dcb/utils/JsonTestUtils.java b/src/test/java/org/folio/dcb/utils/JsonTestUtils.java index 87fa0cb6..1745c189 100644 --- a/src/test/java/org/folio/dcb/utils/JsonTestUtils.java +++ b/src/test/java/org/folio/dcb/utils/JsonTestUtils.java @@ -3,24 +3,23 @@ import com.fasterxml.jackson.annotation.JsonInclude; import tools.jackson.databind.DeserializationFeature; import tools.jackson.databind.JsonNode; -import tools.jackson.databind.ObjectMapper; import lombok.SneakyThrows; import tools.jackson.databind.json.JsonMapper; public class JsonTestUtils { - protected static ObjectMapper objectMapper = JsonMapper.builder() + public static JsonMapper JSON_MAPPER = JsonMapper.builder() .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) .changeDefaultPropertyInclusion(incl -> incl.withValueInclusion(JsonInclude.Include.NON_NULL)) .build(); @SneakyThrows public static String asJsonString(Object value) { - return objectMapper.writeValueAsString(value); + return JSON_MAPPER.writeValueAsString(value); } @SneakyThrows public static JsonNode toJsonNode(Object value) { - return objectMapper.valueToTree(value); + return JSON_MAPPER.valueToTree(value); } } diff --git a/src/test/java/org/folio/dcb/utils/SettingsApiHelper.java b/src/test/java/org/folio/dcb/utils/SettingsApiHelper.java new file mode 100644 index 00000000..72e86dd0 --- /dev/null +++ b/src/test/java/org/folio/dcb/utils/SettingsApiHelper.java @@ -0,0 +1,94 @@ +package org.folio.dcb.utils; + +import static org.folio.dcb.it.base.BaseIntegrationTest.defaultHeaders; +import static org.folio.dcb.utils.JsonTestUtils.asJsonString; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import org.folio.dcb.domain.dto.Setting; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +@RequiredArgsConstructor +public class SettingsApiHelper { + + private final MockMvc mockMvc; + + @SneakyThrows + public ResultActions getById(String id) { + return getByIdAttempt(id).andExpect(status().isOk()); + } + + @SneakyThrows + public ResultActions getByIdAttempt(String id) { + return mockMvc.perform( + MockMvcRequestBuilders.get("/dcb/settings/{id}", id) + .headers(defaultHeaders()) + .contentType(APPLICATION_JSON) + .accept(APPLICATION_JSON)); + } + + @SneakyThrows + public ResultActions findByQuery(String query, int limit, int offset) { + return findByQueryAttempt(query, limit, offset).andExpect(status().isOk()); + } + + @SneakyThrows + public ResultActions findByQueryAttempt(String query, int limit, int offset) { + return mockMvc.perform( + MockMvcRequestBuilders.get("/dcb/settings") + .queryParam("query", query) + .queryParam("limit", String.valueOf(limit)) + .queryParam("offset", String.valueOf(offset)) + .headers(defaultHeaders()) + .contentType(APPLICATION_JSON) + .accept(APPLICATION_JSON)); + } + + @SneakyThrows + public ResultActions post(Setting setting) { + return postAttempt(setting).andExpect(status().isCreated()); + } + + @SneakyThrows + public ResultActions postAttempt(Setting setting) { + return mockMvc.perform( + MockMvcRequestBuilders.post("/dcb/settings") + .content(asJsonString(setting)) + .headers(defaultHeaders()) + .contentType(APPLICATION_JSON) + .accept(APPLICATION_JSON)); + } + + @SneakyThrows + public void putById(String id, Setting setting, String userId) { + putByIdAttempt(id, setting, userId).andExpect(status().isNoContent()); + } + + @SneakyThrows + public ResultActions putByIdAttempt(String id, Setting setting, String userId) { + return mockMvc.perform( + MockMvcRequestBuilders.put("/dcb/settings/{id}", id) + .content(asJsonString(setting)) + .headers(defaultHeaders(userId)) + .contentType(APPLICATION_JSON) + .accept(APPLICATION_JSON)); + } + + @SneakyThrows + public void deleteById(String id) { + deleteByIdAttempt(id).andExpect(status().isNoContent()); + } + + @SneakyThrows + public ResultActions deleteByIdAttempt(String id) { + return mockMvc.perform( + MockMvcRequestBuilders.delete("/dcb/settings/{id}", id) + .headers(defaultHeaders()) + .contentType(APPLICATION_JSON) + .accept(APPLICATION_JSON)); + } +} diff --git a/src/test/java/org/folio/dcb/utils/TestJdbcHelper.java b/src/test/java/org/folio/dcb/utils/TestJdbcHelper.java index b8458985..6a099fdd 100644 --- a/src/test/java/org/folio/dcb/utils/TestJdbcHelper.java +++ b/src/test/java/org/folio/dcb/utils/TestJdbcHelper.java @@ -1,25 +1,29 @@ package org.folio.dcb.utils; -import static java.sql.Types.BOOLEAN; -import static java.sql.Types.TIMESTAMP; -import static java.sql.Types.VARCHAR; import static java.util.Objects.requireNonNull; import static org.folio.dcb.utils.EntityUtils.REQUEST_ID; import static org.folio.dcb.utils.EntityUtils.REQUEST_USER_ID; import static org.folio.dcb.utils.EntityUtils.TEST_TENANT; +import static org.folio.dcb.utils.JsonTestUtils.asJsonString; import jakarta.transaction.Transactional; import java.sql.Timestamp; import java.sql.Types; import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; import org.folio.dcb.domain.dto.DcbTransaction; +import org.folio.dcb.domain.dto.Setting; import org.folio.dcb.domain.dto.TransactionStatus.StatusEnum; import org.intellij.lang.annotations.Language; +import org.jspecify.annotations.NonNull; import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.stereotype.Service; +@Log4j2 @Service @RequiredArgsConstructor public class TestJdbcHelper { @@ -34,11 +38,61 @@ public void saveDcbTransaction(String transactionId, StatusEnum status, DcbTrans @Transactional public void saveDcbTransaction(String tenantId, String transactionId, StatusEnum txStatus, DcbTransaction tx) { + log.debug("saveDcbTransaction:: providing db entity: id={}, tenant={}, tx={}, status={}", + () -> transactionId, () -> tenantId, () -> asJsonString(tx), () -> txStatus); var txItem = requireNonNull(tx.getItem()); var txPickup = requireNonNull(tx.getPickup()); var txPatron = requireNonNull(tx.getPatron()); - @Language("SQL") var dcbTransactionSqlTemplate = """ + var params = new MapSqlParameterSource() + .addValue("id", transactionId, Types.VARCHAR) + .addValue("requestId", REQUEST_ID, Types.OTHER) + .addValue("itemId", txItem.getId(), Types.OTHER) + .addValue("itemTitle", txItem.getTitle(), Types.VARCHAR) + .addValue("itemBarcode", txItem.getBarcode(), Types.VARCHAR) + .addValue("servicePointId", txPickup.getServicePointId(), Types.VARCHAR) + .addValue("servicePointName", txPickup.getServicePointName(), Types.VARCHAR) + .addValue("pickupLibraryCode", txPickup.getLibraryCode(), Types.VARCHAR) + .addValue("materialType", txItem.getMaterialType(), Types.VARCHAR) + .addValue("lendingLibraryCode", txItem.getLendingLibraryCode(), Types.VARCHAR) + .addValue("patronId", txPatron.getId(), Types.OTHER) + .addValue("patronGroup", txPatron.getGroup(), Types.VARCHAR) + .addValue("patronBarcode", txPatron.getBarcode(), Types.VARCHAR) + .addValue("status", txStatus.getValue(), Types.VARCHAR) + .addValue("role", tx.getRole() != null ? tx.getRole().name() : null, Types.VARCHAR) + .addValue("selfBorrowing", Boolean.TRUE.equals(tx.getSelfBorrowing()), Types.BOOLEAN) + .addValue("itemLocationCode", txItem.getLocationCode(), Types.VARCHAR) + .addValue("createdBy", REQUEST_USER_ID, Types.OTHER) + .addValue("createdDate", Timestamp.from(Instant.now().minusSeconds(600)), Types.TIMESTAMP) + .addValue("updatedBy", REQUEST_USER_ID, Types.OTHER) + .addValue("updatedDate", null, Types.TIMESTAMP); + + //noinspection SqlSourceToSinkFlow + jdbcTemplate.update(getDcbTransactionSql(tenantId), params); + } + + @Transactional + public void saveDcbSetting(String tenantId, Setting setting) { + log.debug("saveDcbSetting:: providing db entity: tenant={}, setting={}", () -> tenantId, () -> asJsonString(setting)); + + var createdDate = OffsetDateTime.now(ZoneOffset.UTC).minusSeconds(600); + var params = new MapSqlParameterSource() + .addValue("id", setting.getId(), Types.OTHER) + .addValue("scope", "mod-dcb", Types.VARCHAR) + .addValue("key", setting.getKey(), Types.VARCHAR) + .addValue("value", asJsonString(setting.getValue()), Types.OTHER) + .addValue("version", 1, Types.INTEGER) + .addValue("createdBy", REQUEST_USER_ID, Types.OTHER) + .addValue("createdDate", createdDate, Types.TIMESTAMP_WITH_TIMEZONE) + .addValue("updatedBy", REQUEST_USER_ID, Types.OTHER) + .addValue("updatedDate", createdDate, Types.TIMESTAMP_WITH_TIMEZONE); + + //noinspection SqlSourceToSinkFlow + jdbcTemplate.update(getDcbSettingSql(tenantId), params); + } + + private static @NonNull String getDcbTransactionSql(String tenantId) { + @Language("PostgreSQL") var dcbTransactionSqlTemplate = """ INSERT INTO %s_mod_dcb.transactions (id, request_id, item_id, item_title, item_barcode, service_point_id, service_point_name, pickup_library_code, material_type, lending_library_code, patron_id, patron_group, patron_barcode, status, role, @@ -48,32 +102,15 @@ public void saveDcbTransaction(String tenantId, :patronGroup, :patronBarcode, :status, :role, :selfBorrowing, :itemLocationCode, :createdBy, :createdDate, :updatedBy, :updatedDate)"""; - var dcbTransactionSql = dcbTransactionSqlTemplate.formatted(tenantId); + return dcbTransactionSqlTemplate.formatted(tenantId); + } - var params = new MapSqlParameterSource() - .addValue("id", transactionId, VARCHAR) - .addValue("requestId", REQUEST_ID, Types.OTHER) - .addValue("itemId", txItem.getId(), Types.OTHER) - .addValue("itemTitle", txItem.getTitle(), VARCHAR) - .addValue("itemBarcode", txItem.getBarcode(), VARCHAR) - .addValue("servicePointId", txPickup.getServicePointId(), VARCHAR) - .addValue("servicePointName", txPickup.getServicePointName(), VARCHAR) - .addValue("pickupLibraryCode", txPickup.getLibraryCode(), VARCHAR) - .addValue("materialType", txItem.getMaterialType(), VARCHAR) - .addValue("lendingLibraryCode", txItem.getLendingLibraryCode(), VARCHAR) - .addValue("patronId", txPatron.getId(), Types.OTHER) - .addValue("patronGroup", txPatron.getGroup(), VARCHAR) - .addValue("patronBarcode", txPatron.getBarcode(), VARCHAR) - .addValue("status", txStatus.getValue(), VARCHAR) - .addValue("role", tx.getRole() != null ? tx.getRole().name() : null, VARCHAR) - .addValue("selfBorrowing", Boolean.TRUE.equals(tx.getSelfBorrowing()), BOOLEAN) - .addValue("itemLocationCode", txItem.getLocationCode(), VARCHAR) - .addValue("createdBy", REQUEST_USER_ID, Types.OTHER) - .addValue("createdDate", Timestamp.from(Instant.now().minusSeconds(600)), TIMESTAMP) - .addValue("updatedBy", REQUEST_USER_ID, Types.OTHER) - .addValue("updatedDate", null, TIMESTAMP); + private static @NonNull String getDcbSettingSql(String tenantId) { + @Language("PostgreSQL") var dcbSettingSqlTemplate = """ + INSERT INTO %s_mod_dcb.settings (id, key, scope, value, version, created_by, created_date, updated_by, updated_date) + VALUES (:id, :key, :scope, :value::jsonb, :version, :createdBy, :createdDate, :updatedBy, :updatedDate) + """; - //noinspection SqlSourceToSinkFlow - jdbcTemplate.update(dcbTransactionSql, params); + return dcbSettingSqlTemplate.formatted(tenantId); } } diff --git a/src/test/resources/db/scripts/cleanup_dcb_tables.sql b/src/test/resources/db/scripts/cleanup_dcb_tables.sql index b9090202..3f631d5c 100644 --- a/src/test/resources/db/scripts/cleanup_dcb_tables.sql +++ b/src/test/resources/db/scripts/cleanup_dcb_tables.sql @@ -3,6 +3,7 @@ CREATE OR REPLACE FUNCTION truncate_dcb_tables() RETURNS void AS ' BEGIN IF EXISTS (SELECT 1 FROM information_schema.schemata WHERE schema_name = ''test_tenant_mod_dcb'') THEN EXECUTE ''TRUNCATE TABLE test_tenant_mod_dcb.transactions CASCADE''; + EXECUTE ''TRUNCATE TABLE test_tenant_mod_dcb.settings CASCADE''; EXECUTE ''TRUNCATE TABLE test_tenant_mod_dcb.transactions_audit CASCADE''; EXECUTE ''TRUNCATE TABLE test_tenant_mod_dcb.service_point_expiration_period CASCADE''; END IF; diff --git a/src/test/resources/stubs/mod-inventory-storage/service-points/204-put(Virtual+custom).json b/src/test/resources/stubs/mod-inventory-storage/service-points/204-put(Virtual+custom).json new file mode 100644 index 00000000..14632a24 --- /dev/null +++ b/src/test/resources/stubs/mod-inventory-storage/service-points/204-put(Virtual+custom).json @@ -0,0 +1,36 @@ +{ + "request": { + "method": "PUT", + "urlPath": "/service-points/9d1b77e8-f02e-4b7f-b296-3f2042ddac55", + "bodyPatterns": [ + { + "equalToJson": { + "id": "9d1b77e8-f02e-4b7f-b296-3f2042ddac55", + "name": "DCB_TestLibraryCode_TestServicePointCode", + "code": "DCB_TestLibraryCode_TestServicePointCode", + "discoveryDisplayName": "DCB_TestLibraryCode_TestServicePointCode", + "pickupLocation": true, + "holdShelfExpiryPeriod": { + "duration": 25, + "intervalId": "Minutes" + }, + "holdShelfClosedLibraryDateManagement": "Keep_the_current_due_date" + } + } + ], + "headers": { + "x-okapi-tenant": { + "equalTo": "test_tenant" + }, + "x-okapi-token": { + "equalTo": "dGVzdF9qd3RfdG9rZW4=" + } + } + }, + "response": { + "status": 204, + "headers": { + "Content-Type": "application/json" + } + } +} From 9d71365f488b7127b4a21a4612d79c0ee845b9ad Mon Sep 17 00:00:00 2001 From: Pavel Filippov Date: Fri, 20 Feb 2026 20:22:06 +0200 Subject: [PATCH 2/8] MODDCB-257: Add spectral rules --- .spectral.yaml | 157 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 .spectral.yaml diff --git a/.spectral.yaml b/.spectral.yaml new file mode 100644 index 00000000..a263bff9 --- /dev/null +++ b/.spectral.yaml @@ -0,0 +1,157 @@ +extends: [ "spectral:oas" ] +aliases: + PathItem: + description: '' + targets: + - formats: + - oas2 + given: + - "$.paths[*]" + - formats: + - oas3 + given: + - "$.paths[*]" + OperationObject: + description: 'The complete operation object. Use it in combo with field object.' + targets: + - formats: + - oas2 + given: + - "#PathItem[get,put,post,delete,options,head,patch,trace]" + - formats: + - oas3 + given: + - "#PathItem[get,put,post,delete,options,head,patch,trace]" + DescribableObjects: + description: '' + targets: + - formats: + - oas2 + given: + - "$.info" + - "$.tags[*]" + - "#OperationObject" + - "$.paths[*][*].responses[*]" + - "$..parameters[?(@ && @.in)]" + - "$.definitions[*]" + - formats: + - oas3 + given: + - "$.info" + - "$.tags[*]" + - "#OperationObject" + - "$.paths[*][*].responses[*]" + - "$..parameters[?(@ && @.in)]" + - "$.components.schemas[*]" + - "$.servers[*]" + MediaTypeObjects: + description: '' + targets: + - formats: + - oas2 + given: + - $.paths[*][*]..parameters[?(@ && @.in == "body")] + - "$.paths[*][*].responses[*]" + - formats: + - oas3 + given: + - "$.paths[*][*].requestBody.content[*]" + - "$.paths[*][*].responses[*].content[*]" +rules: + docs-descriptions: + given: + - "#DescribableObjects" + severity: warn + then: + - function: truthy + field: description + - function: length + functionOptions: + min: 10 + field: description + - function: pattern + functionOptions: + match: "/^[A-Z]/" + field: description + description: "Descriptions should be provided for describable objects, such as `info`, `tags`, `operations`, `parameters`, and more." + message: "{{error}}." + docs-info-contact: + given: + - "$" + severity: warn + then: + function: truthy + field: info.contact + description: "`Info` object should include contact information." + docs-parameters-examples-or-schema: + given: + - "$.paths.parameters[*]" + severity: info + then: + function: schema + functionOptions: + schema: + type: object + anyOf: + - required: + - examples + - required: + - schema + description: "Path parameter must contain a defined schema or examples." + message: No example or schema provided for {{property}} + formats: + - oas3 + docs-summary: + given: + - "#PathItem[*]" + severity: error + then: + - function: truthy + field: summary + description: "Path parameter must contain a defined schema or examples." + message: No summary provided for {{property}} + formats: + - oas3 + docs-media-types-examples-or-schema: + given: + - "#MediaTypeObjects" + severity: info + then: + function: schema + functionOptions: + schema: + type: object + anyOf: + - required: + - examples + - required: + - schema + description: "Media object must contain a defined schema or examples." + message: No example or schema provided for {{property}} + formats: + - oas3 + docs-tags-alphabetical: + given: + - "$" + severity: warn + then: + function: alphabetical + functionOptions: + keyedBy: name + field: tags + description: "Tags are not in alphabetical order." + message: Tags should be defined in alphabetical order + docs-operation-tags: + given: + - "#OperationObject" + severity: warn + then: + function: schema + functionOptions: + schema: + type: array + minItems: 1 + field: tags + description: "Operation must have at least one tag." + message: Operation should have non-empty `tags` array. + From 23f7c4e17df0a937b03c7d6b648e08355bb0010e Mon Sep 17 00:00:00 2001 From: Pavel Filippov Date: Fri, 20 Feb 2026 20:35:41 +0200 Subject: [PATCH 3/8] MODDCB-257: Remove spectral linter (Highlights too many existing issues) --- .github/workflows/spectral-lint.yml | 22 ---- .spectral.yaml | 157 ---------------------------- 2 files changed, 179 deletions(-) delete mode 100644 .github/workflows/spectral-lint.yml delete mode 100644 .spectral.yaml diff --git a/.github/workflows/spectral-lint.yml b/.github/workflows/spectral-lint.yml deleted file mode 100644 index fecd44cf..00000000 --- a/.github/workflows/spectral-lint.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Run Spectral on Pull Requests - -on: - push: - paths: - - 'src/main/resources/swagger.api/**' - pull_request: - paths: - - 'src/main/resources/swagger.api/**' - -jobs: - build: - name: Run Spectral - runs-on: ubuntu-latest - steps: - # Check out the repository - - uses: actions/checkout@v3 - - # Run Spectral - - uses: stoplightio/spectral-action@latest - with: - file_glob: 'src/main/resources/swagger.api/*.yaml' diff --git a/.spectral.yaml b/.spectral.yaml deleted file mode 100644 index a263bff9..00000000 --- a/.spectral.yaml +++ /dev/null @@ -1,157 +0,0 @@ -extends: [ "spectral:oas" ] -aliases: - PathItem: - description: '' - targets: - - formats: - - oas2 - given: - - "$.paths[*]" - - formats: - - oas3 - given: - - "$.paths[*]" - OperationObject: - description: 'The complete operation object. Use it in combo with field object.' - targets: - - formats: - - oas2 - given: - - "#PathItem[get,put,post,delete,options,head,patch,trace]" - - formats: - - oas3 - given: - - "#PathItem[get,put,post,delete,options,head,patch,trace]" - DescribableObjects: - description: '' - targets: - - formats: - - oas2 - given: - - "$.info" - - "$.tags[*]" - - "#OperationObject" - - "$.paths[*][*].responses[*]" - - "$..parameters[?(@ && @.in)]" - - "$.definitions[*]" - - formats: - - oas3 - given: - - "$.info" - - "$.tags[*]" - - "#OperationObject" - - "$.paths[*][*].responses[*]" - - "$..parameters[?(@ && @.in)]" - - "$.components.schemas[*]" - - "$.servers[*]" - MediaTypeObjects: - description: '' - targets: - - formats: - - oas2 - given: - - $.paths[*][*]..parameters[?(@ && @.in == "body")] - - "$.paths[*][*].responses[*]" - - formats: - - oas3 - given: - - "$.paths[*][*].requestBody.content[*]" - - "$.paths[*][*].responses[*].content[*]" -rules: - docs-descriptions: - given: - - "#DescribableObjects" - severity: warn - then: - - function: truthy - field: description - - function: length - functionOptions: - min: 10 - field: description - - function: pattern - functionOptions: - match: "/^[A-Z]/" - field: description - description: "Descriptions should be provided for describable objects, such as `info`, `tags`, `operations`, `parameters`, and more." - message: "{{error}}." - docs-info-contact: - given: - - "$" - severity: warn - then: - function: truthy - field: info.contact - description: "`Info` object should include contact information." - docs-parameters-examples-or-schema: - given: - - "$.paths.parameters[*]" - severity: info - then: - function: schema - functionOptions: - schema: - type: object - anyOf: - - required: - - examples - - required: - - schema - description: "Path parameter must contain a defined schema or examples." - message: No example or schema provided for {{property}} - formats: - - oas3 - docs-summary: - given: - - "#PathItem[*]" - severity: error - then: - - function: truthy - field: summary - description: "Path parameter must contain a defined schema or examples." - message: No summary provided for {{property}} - formats: - - oas3 - docs-media-types-examples-or-schema: - given: - - "#MediaTypeObjects" - severity: info - then: - function: schema - functionOptions: - schema: - type: object - anyOf: - - required: - - examples - - required: - - schema - description: "Media object must contain a defined schema or examples." - message: No example or schema provided for {{property}} - formats: - - oas3 - docs-tags-alphabetical: - given: - - "$" - severity: warn - then: - function: alphabetical - functionOptions: - keyedBy: name - field: tags - description: "Tags are not in alphabetical order." - message: Tags should be defined in alphabetical order - docs-operation-tags: - given: - - "#OperationObject" - severity: warn - then: - function: schema - functionOptions: - schema: - type: array - minItems: 1 - field: tags - description: "Operation must have at least one tag." - message: Operation should have non-empty `tags` array. - From 0a4ab676a43f560c91cb272899afdac0bfb92301 Mon Sep 17 00:00:00 2001 From: Pavel Filippov Date: Mon, 23 Feb 2026 13:02:29 +0200 Subject: [PATCH 4/8] MODDCB-257: Fix sonarcloud issues --- .../entities/DcbServicePointService.java | 4 +++- .../org/folio/dcb/it/PickupTransactionIT.java | 1 - .../dcb/it/base/BaseTenantIntegrationTest.java | 1 - .../CirculationRequestEventListenerTest.java | 1 - .../service/BorrowingLibraryServiceTest.java | 17 +++++------------ .../dcb/service/LendingLibraryServiceTest.java | 1 - .../dcb/service/ServicePointServiceTest.java | 2 -- .../java/org/folio/dcb/utils/JsonTestUtils.java | 2 +- 8 files changed, 9 insertions(+), 20 deletions(-) diff --git a/src/main/java/org/folio/dcb/service/entities/DcbServicePointService.java b/src/main/java/org/folio/dcb/service/entities/DcbServicePointService.java index a68e48d6..cc1ae3ce 100644 --- a/src/main/java/org/folio/dcb/service/entities/DcbServicePointService.java +++ b/src/main/java/org/folio/dcb/service/entities/DcbServicePointService.java @@ -4,6 +4,7 @@ import static org.folio.dcb.service.impl.ServicePointServiceImpl.HOLD_SHELF_CLOSED_LIBRARY_DATE_MANAGEMENT; import static org.folio.dcb.utils.CqlQuery.exactMatchByName; import static org.folio.dcb.utils.DCBConstants.CODE; +import static org.folio.dcb.utils.DCBConstants.DCB_TYPE; import static org.folio.dcb.utils.DCBConstants.DEFAULT_PERIOD; import static org.folio.dcb.utils.DCBConstants.NAME; import static org.folio.dcb.utils.DCBConstants.SERVICE_POINT_ID; @@ -34,7 +35,8 @@ public Optional findDcbEntity() { @Override public ServicePointRequest createDcbEntity() { log.debug("createDcbEntity:: Creating a new DCB Service Point"); - var shelfExpiryPeriod = servicePointExpirationPeriodService.getShelfExpiryPeriod(getSettingsKey("dcb")); + var settingsKey = getSettingsKey(DCB_TYPE); + var shelfExpiryPeriod = servicePointExpirationPeriodService.getShelfExpiryPeriod(settingsKey); var dcbServicePoint = getDcbServicePoint(shelfExpiryPeriod); var createdServicePoint = servicePointClient.createServicePoint(dcbServicePoint); log.info("createDcbEntity:: DCB Service Point created"); diff --git a/src/test/java/org/folio/dcb/it/PickupTransactionIT.java b/src/test/java/org/folio/dcb/it/PickupTransactionIT.java index 5c9a26e4..5e843ac4 100644 --- a/src/test/java/org/folio/dcb/it/PickupTransactionIT.java +++ b/src/test/java/org/folio/dcb/it/PickupTransactionIT.java @@ -25,7 +25,6 @@ import static org.folio.dcb.utils.EntityUtils.dcbItem; import static org.folio.dcb.utils.EntityUtils.dcbPatron; import static org.folio.dcb.utils.EntityUtils.dcbTransactionUpdate; -import static org.folio.dcb.utils.EntityUtils.lenderDcbTransaction; import static org.folio.dcb.utils.EntityUtils.pickupDcbTransaction; import static org.folio.dcb.utils.EntityUtils.transactionStatus; import static org.folio.dcb.utils.EventDataProvider.expiredRequestMessage; diff --git a/src/test/java/org/folio/dcb/it/base/BaseTenantIntegrationTest.java b/src/test/java/org/folio/dcb/it/base/BaseTenantIntegrationTest.java index 72d07226..810d0c10 100644 --- a/src/test/java/org/folio/dcb/it/base/BaseTenantIntegrationTest.java +++ b/src/test/java/org/folio/dcb/it/base/BaseTenantIntegrationTest.java @@ -29,7 +29,6 @@ import org.folio.dcb.repository.TransactionAuditRepository; import org.folio.dcb.support.AuditEntityTestVerifier; import org.folio.dcb.support.wiremock.WiremockStubExtension; -import org.folio.dcb.utils.SettingsApiHelper; import org.folio.dcb.utils.TestCirculationEventHelper; import org.folio.dcb.utils.TestJdbcHelper; import org.folio.spring.FolioModuleMetadata; diff --git a/src/test/java/org/folio/dcb/listener/CirculationRequestEventListenerTest.java b/src/test/java/org/folio/dcb/listener/CirculationRequestEventListenerTest.java index 6984c1a2..1e631c42 100644 --- a/src/test/java/org/folio/dcb/listener/CirculationRequestEventListenerTest.java +++ b/src/test/java/org/folio/dcb/listener/CirculationRequestEventListenerTest.java @@ -1,6 +1,5 @@ package org.folio.dcb.listener; -import static org.folio.dcb.domain.dto.DcbTransaction.RoleEnum.BORROWER; import static org.folio.dcb.domain.dto.DcbTransaction.RoleEnum.BORROWING_PICKUP; import static org.folio.dcb.domain.dto.DcbTransaction.RoleEnum.LENDER; import static org.folio.dcb.domain.dto.DcbTransaction.RoleEnum.PICKUP; diff --git a/src/test/java/org/folio/dcb/service/BorrowingLibraryServiceTest.java b/src/test/java/org/folio/dcb/service/BorrowingLibraryServiceTest.java index 550cc883..75ec031b 100644 --- a/src/test/java/org/folio/dcb/service/BorrowingLibraryServiceTest.java +++ b/src/test/java/org/folio/dcb/service/BorrowingLibraryServiceTest.java @@ -21,7 +21,6 @@ import static org.folio.dcb.domain.dto.TransactionStatus.StatusEnum.ITEM_CHECKED_OUT; import static org.folio.dcb.domain.dto.TransactionStatus.StatusEnum.OPEN; import static org.folio.dcb.utils.EntityUtils.DCB_TRANSACTION_ID; -import static org.folio.dcb.utils.EntityUtils.createDcbPickup; import static org.folio.dcb.utils.EntityUtils.createDcbTransactionByRole; import static org.folio.dcb.utils.EntityUtils.createServicePointRequest; import static org.folio.dcb.utils.EntityUtils.createTransactionEntity; @@ -35,18 +34,12 @@ @ExtendWith(MockitoExtension.class) class BorrowingLibraryServiceTest { - @InjectMocks - private BorrowingLibraryServiceImpl borrowingLibraryService; - @Mock - private CirculationService circulationService; - @Mock - private TransactionRepository transactionRepository; - @Mock - private BaseLibraryService baseLibraryService; - - @Mock - private ServicePointService servicePointService; + @InjectMocks private BorrowingLibraryServiceImpl borrowingLibraryService; + @Mock private CirculationService circulationService; + @Mock private BaseLibraryService baseLibraryService; + @Mock private ServicePointService servicePointService; + @Mock private TransactionRepository transactionRepository; @Test void testTransactionStatusUpdateFromOpenToAwaitingPickup() { diff --git a/src/test/java/org/folio/dcb/service/LendingLibraryServiceTest.java b/src/test/java/org/folio/dcb/service/LendingLibraryServiceTest.java index d7578bf3..4dc28b90 100644 --- a/src/test/java/org/folio/dcb/service/LendingLibraryServiceTest.java +++ b/src/test/java/org/folio/dcb/service/LendingLibraryServiceTest.java @@ -1,6 +1,5 @@ package org.folio.dcb.service; -import org.folio.dcb.domain.dto.ItemStatus; import org.folio.dcb.domain.dto.TransactionStatus; import org.folio.dcb.domain.dto.TransactionStatusResponse; import org.folio.dcb.domain.entity.TransactionEntity; diff --git a/src/test/java/org/folio/dcb/service/ServicePointServiceTest.java b/src/test/java/org/folio/dcb/service/ServicePointServiceTest.java index fd2b4113..0112a122 100644 --- a/src/test/java/org/folio/dcb/service/ServicePointServiceTest.java +++ b/src/test/java/org/folio/dcb/service/ServicePointServiceTest.java @@ -1,6 +1,5 @@ package org.folio.dcb.service; -import static org.folio.dcb.utils.EntityUtils.createDcbPickup; import static org.folio.dcb.utils.EntityUtils.createDcbTransactionByRole; import static org.folio.dcb.utils.EntityUtils.createServicePointRequest; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -13,7 +12,6 @@ import java.util.List; import java.util.UUID; -import org.folio.dcb.domain.dto.DcbTransaction; import org.folio.dcb.domain.dto.DcbTransaction.RoleEnum; import org.folio.dcb.integration.invstorage.ServicePointClient; import org.folio.dcb.domain.ResultList; diff --git a/src/test/java/org/folio/dcb/utils/JsonTestUtils.java b/src/test/java/org/folio/dcb/utils/JsonTestUtils.java index 1745c189..ccae7cbe 100644 --- a/src/test/java/org/folio/dcb/utils/JsonTestUtils.java +++ b/src/test/java/org/folio/dcb/utils/JsonTestUtils.java @@ -8,7 +8,7 @@ public class JsonTestUtils { - public static JsonMapper JSON_MAPPER = JsonMapper.builder() + public static final JsonMapper JSON_MAPPER = JsonMapper.builder() .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) .changeDefaultPropertyInclusion(incl -> incl.withValueInclusion(JsonInclude.Include.NON_NULL)) .build(); From 4fd8468f598703d9ff2b0d0c67c486ea11746634 Mon Sep 17 00:00:00 2001 From: Pavel Filippov Date: Mon, 23 Feb 2026 17:06:05 +0200 Subject: [PATCH 5/8] MODDCB-257: Improve code coverage --- .../dcb/domain/mapper/SettingMapper.java | 5 +++ .../dcb/domain/mapper/SettingMapperTest.java | 33 +++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 src/test/java/org/folio/dcb/domain/mapper/SettingMapperTest.java diff --git a/src/main/java/org/folio/dcb/domain/mapper/SettingMapper.java b/src/main/java/org/folio/dcb/domain/mapper/SettingMapper.java index edb01b1e..8d100338 100644 --- a/src/main/java/org/folio/dcb/domain/mapper/SettingMapper.java +++ b/src/main/java/org/folio/dcb/domain/mapper/SettingMapper.java @@ -36,6 +36,11 @@ private void setJsonMapper(JsonMapper jsonMapper) { this.jsonMapper = jsonMapper; } + /** + * Parses a string value to its corresponding {@link SettingScope} enum. Returns null if no matching value is found. + * + * @param scope the string representation of the setting scope + */ public static SettingScope parseSettingScopeFromString(String scope) { for (var value : SettingScope.values()) { if (Strings.CI.equals(value.getValue(), scope)) { diff --git a/src/test/java/org/folio/dcb/domain/mapper/SettingMapperTest.java b/src/test/java/org/folio/dcb/domain/mapper/SettingMapperTest.java new file mode 100644 index 00000000..59cfbefc --- /dev/null +++ b/src/test/java/org/folio/dcb/domain/mapper/SettingMapperTest.java @@ -0,0 +1,33 @@ +package org.folio.dcb.domain.mapper; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.folio.dcb.domain.dto.SettingScope; +import org.folio.dcb.support.types.UnitTest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +@UnitTest +class SettingMapperTest { + + @ParameterizedTest + @EnumSource(SettingScope.class) + void parseSettingScopeFromString_parameterized_lowerCase(SettingScope scope) { + var result = SettingMapper.parseSettingScopeFromString(scope.getValue().toLowerCase()); + assertThat(result).isEqualTo(scope); + } + + @ParameterizedTest + @EnumSource(SettingScope.class) + void parseSettingScopeFromString_parameterized_upperCase(SettingScope scope) { + var result = SettingMapper.parseSettingScopeFromString(scope.getValue().toUpperCase()); + assertThat(result).isEqualTo(scope); + } + + @Test + void parseSettingScopeFromString_negative_invalidValue() { + var result = SettingMapper.parseSettingScopeFromString("invalid"); + assertThat(result).isNull(); + } +} From 3cef7ef99028c8a63e88700e52d98c55ff51b422 Mon Sep 17 00:00:00 2001 From: Pavel Filippov Date: Tue, 24 Feb 2026 11:32:34 +0200 Subject: [PATCH 6/8] MODDCB-257: Fix issues after performing code review using copilot --- .../dcb/controller/SettingsController.java | 10 ++--- .../dcb/domain/entity/SettingEntity.java | 4 +- .../org/folio/dcb/service/SettingService.java | 18 ++++++--- .../dcb/service/impl/BaseLibraryService.java | 3 -- .../swagger.api/dcb_transaction.yaml | 4 +- .../java/org/folio/dcb/it/SettingsIT.java | 10 ++--- .../folio/dcb/service/SettingServiceTest.java | 40 +++++++++++-------- .../org/folio/dcb/utils/TestJdbcHelper.java | 2 +- 8 files changed, 51 insertions(+), 40 deletions(-) diff --git a/src/main/java/org/folio/dcb/controller/SettingsController.java b/src/main/java/org/folio/dcb/controller/SettingsController.java index a6898465..d38e516a 100644 --- a/src/main/java/org/folio/dcb/controller/SettingsController.java +++ b/src/main/java/org/folio/dcb/controller/SettingsController.java @@ -18,7 +18,7 @@ public class SettingsController implements SettingApi { @Override public ResponseEntity createDcbSetting(Setting setting) { - var createdSetting = settingService.createSetting(setting); + var createdSetting = settingService.create(setting); return ResponseEntity.status(HttpStatus.CREATED).body(createdSetting); } @@ -37,14 +37,14 @@ public ResponseEntity getDcbSettingById(UUID id) { } @Override - public ResponseEntity deleteDcbSettingById(UUID id) { - settingService.deleteSettingById(id); + public ResponseEntity updateDcbSettingById(UUID id, Setting setting) { + settingService.update(id, setting); return ResponseEntity.noContent().build(); } @Override - public ResponseEntity updateDcbSettingById(UUID id, Setting setting) { - settingService.updateSetting(setting); + public ResponseEntity deleteDcbSettingById(UUID id) { + settingService.deleteById(id); return ResponseEntity.noContent().build(); } } diff --git a/src/main/java/org/folio/dcb/domain/entity/SettingEntity.java b/src/main/java/org/folio/dcb/domain/entity/SettingEntity.java index a07a0bd2..b1255c4e 100644 --- a/src/main/java/org/folio/dcb/domain/entity/SettingEntity.java +++ b/src/main/java/org/folio/dcb/domain/entity/SettingEntity.java @@ -26,7 +26,7 @@ public class SettingEntity extends AuditableEntity implements Persistable @Column(nullable = false) private String key; - @Column + @Column(nullable = false) private String scope; @Column(columnDefinition = "jsonb") @@ -45,6 +45,6 @@ public boolean isNew() { @PrePersist private void initVersion() { - version = 1; + version = 0; } } diff --git a/src/main/java/org/folio/dcb/service/SettingService.java b/src/main/java/org/folio/dcb/service/SettingService.java index 27fbaf74..2f042ef8 100644 --- a/src/main/java/org/folio/dcb/service/SettingService.java +++ b/src/main/java/org/folio/dcb/service/SettingService.java @@ -1,5 +1,8 @@ package org.folio.dcb.service; +import static org.springframework.beans.BeanUtils.copyProperties; + +import java.util.Objects; import java.util.UUID; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; @@ -10,7 +13,6 @@ import org.folio.dcb.repository.SettingRepository; import org.folio.spring.data.OffsetRequest; import org.folio.spring.exception.NotFoundException; -import org.springframework.beans.BeanUtils; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -29,7 +31,7 @@ public class SettingService { * @return created setting DTO with any generated fields populated (for example id) */ @Transactional - public Setting createSetting(Setting setting) { + public Setting create(Setting setting) { if (settingRepository.existsById(setting.getId())) { throw new IllegalArgumentException("Setting already exists with id: " + setting.getId()); } @@ -80,8 +82,12 @@ public ResultList findByQuery(String query, int limit, int offset) { * @throws NotFoundException when no setting with the given id exists */ @Transactional - public void updateSetting(Setting updatedSetting) { - var existingEntity = settingRepository.findById(updatedSetting.getId()) + public void update(UUID id, Setting updatedSetting) { + if (!Objects.equals(updatedSetting.getId(), id)) { + throw new IllegalArgumentException("Id cannot be modified: " + id); + } + + var existingEntity = settingRepository.findById(id) .orElseThrow(() -> new NotFoundException("Setting not found by id: " + updatedSetting.getId())); if (!existingEntity.getKey().equals(updatedSetting.getKey())) { @@ -89,7 +95,7 @@ public void updateSetting(Setting updatedSetting) { } var updatedEntity = settingMapper.convert(updatedSetting); - BeanUtils.copyProperties(updatedEntity, existingEntity, "id", "key", "metadata"); + copyProperties(updatedEntity, existingEntity, "id", "key", "createdDate", "createdBy", "updatedDate", "updatedBy"); settingRepository.save(existingEntity); log.debug("updateSetting:: Setting was updated for key: {}", updatedEntity.getKey()); } @@ -101,7 +107,7 @@ public void updateSetting(Setting updatedSetting) { * @throws NotFoundException when no setting with the given id exists */ @Transactional - public void deleteSettingById(UUID settingId) { + public void deleteById(UUID settingId) { var entityToDelete = settingRepository.findById(settingId) .orElseThrow(() -> new NotFoundException("Setting not found by id: " + settingId)); diff --git a/src/main/java/org/folio/dcb/service/impl/BaseLibraryService.java b/src/main/java/org/folio/dcb/service/impl/BaseLibraryService.java index ae207008..c738fe5c 100644 --- a/src/main/java/org/folio/dcb/service/impl/BaseLibraryService.java +++ b/src/main/java/org/folio/dcb/service/impl/BaseLibraryService.java @@ -3,7 +3,6 @@ import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.apache.commons.lang3.BooleanUtils; -import org.apache.commons.lang3.ObjectUtils; import org.folio.dcb.domain.dto.CirculationItem; import org.folio.dcb.domain.dto.CirculationRequest; import org.folio.dcb.domain.dto.DcbItem; @@ -11,7 +10,6 @@ import org.folio.dcb.domain.dto.DcbTransaction; import org.folio.dcb.domain.dto.DcbTransaction.RoleEnum; import org.folio.dcb.domain.dto.DcbUpdateItem; -import org.folio.dcb.domain.dto.ItemStatus; import org.folio.dcb.domain.dto.TransactionStatus; import org.folio.dcb.domain.dto.TransactionStatusResponse; import org.folio.dcb.domain.entity.TransactionEntity; @@ -32,7 +30,6 @@ import java.util.UUID; import static org.folio.dcb.domain.dto.ItemStatus.NameEnum.AVAILABLE; -import static org.folio.dcb.domain.dto.ItemStatus.NameEnum.IN_TRANSIT; import static org.folio.dcb.domain.dto.TransactionStatus.StatusEnum.CANCELLED; import static org.folio.dcb.domain.dto.TransactionStatus.StatusEnum.CLOSED; import static org.folio.dcb.domain.dto.TransactionStatus.StatusEnum.CREATED; diff --git a/src/main/resources/swagger.api/dcb_transaction.yaml b/src/main/resources/swagger.api/dcb_transaction.yaml index d0d26910..8fd22d5d 100644 --- a/src/main/resources/swagger.api/dcb_transaction.yaml +++ b/src/main/resources/swagger.api/dcb_transaction.yaml @@ -237,7 +237,7 @@ paths: schema: $ref: '#/components/schemas/Setting' responses: - '200': + '201': description: created DCB setting content: application/json: @@ -275,7 +275,7 @@ paths: tags: - setting requestBody: - description: DCB setting to create + description: DCB setting to update required: true content: application/json: diff --git a/src/test/java/org/folio/dcb/it/SettingsIT.java b/src/test/java/org/folio/dcb/it/SettingsIT.java index 9a1ab136..40dbe408 100644 --- a/src/test/java/org/folio/dcb/it/SettingsIT.java +++ b/src/test/java/org/folio/dcb/it/SettingsIT.java @@ -51,7 +51,7 @@ void getSettingById_positive() throws Exception { var id = UUID.randomUUID(); var expectedSetting = lenderHoldShelfExpirationSetting() .id(id) - .version(1) + .version(0) .metadata(new Metadata() .createdByUserId(REQUEST_USER_ID) .updatedByUserId(REQUEST_USER_ID)); @@ -60,7 +60,7 @@ void getSettingById_positive() throws Exception { settingsApiHelper.getById(id.toString()) .andExpect(content().json(asJsonString(expectedSetting), LENIENT)) - .andExpect(jsonPath("$._version").value(1)) + .andExpect(jsonPath("$._version").value(0)) .andExpect(jsonPath("$.version").doesNotExist()) .andExpect(jsonPath("$.metadata.createdByUserId").value(REQUEST_USER_ID)) .andExpect(jsonPath("$.metadata.createdDate").exists()) @@ -94,7 +94,7 @@ void findByQuery_positive_emptyResult() throws Exception { }) void findByQuery_positive_parameterized(String query) throws Exception { var expectedSetting = lenderHoldShelfExpirationSetting() - .version(1) + .version(0) .metadata(new Metadata() .createdByUserId(REQUEST_USER_ID) .updatedByUserId(REQUEST_USER_ID)); @@ -116,7 +116,7 @@ void createAndUpdate_positive() throws Exception { var setting = EntityUtils.lenderHoldShelfExpirationSetting(); var createdSettingJson = settingsApiHelper.post(setting) .andExpect(content().json(asJsonString(setting), LENIENT)) - .andExpect(jsonPath("$._version").value(1)) + .andExpect(jsonPath("$._version").value(0)) .andExpect(jsonPath("$.metadata.createdByUserId").value(REQUEST_USER_ID)) .andExpect(jsonPath("$.metadata.createdDate").exists()) .andExpect(jsonPath("$.metadata.updatedByUserId").value(REQUEST_USER_ID)) @@ -136,7 +136,7 @@ void createAndUpdate_positive() throws Exception { settingsApiHelper.putById(LENDER_HOLD_SHELF_EXPIRATION_SETTING_ID, newSettingValue, otherUserId); var updatedSettingJson = settingsApiHelper.getById(LENDER_HOLD_SHELF_EXPIRATION_SETTING_ID) - .andExpect(jsonPath("$._version").value(2)) + .andExpect(jsonPath("$._version").value(1)) .andExpect(jsonPath("$.metadata.createdByUserId").value(REQUEST_USER_ID)) .andExpect(jsonPath("$.metadata.createdDate").exists()) .andExpect(jsonPath("$.metadata.updatedByUserId").value(otherUserId)) diff --git a/src/test/java/org/folio/dcb/service/SettingServiceTest.java b/src/test/java/org/folio/dcb/service/SettingServiceTest.java index 8af801e7..6d987a94 100644 --- a/src/test/java/org/folio/dcb/service/SettingServiceTest.java +++ b/src/test/java/org/folio/dcb/service/SettingServiceTest.java @@ -48,7 +48,7 @@ class SettingServiceTest { @Mock private SettingRepository settingRepository; @Test - void createSetting_positive() { + void create_positive() { var setting = setting(); when(settingRepository.existsById(SETTING_ID_1)).thenReturn(false); when(settingRepository.existsByKey(SETTING_KEY_1)).thenReturn(false); @@ -56,29 +56,29 @@ void createSetting_positive() { when(settingRepository.save(settingEntity())).thenReturn(settingEntity()); when(settingMapper.convert(settingEntity())).thenReturn(setting()); - var result = settingService.createSetting(setting); + var result = settingService.create(setting); assertThat(result).isEqualTo(setting()); } @Test - void createSetting_negative_existsById() { + void create_negative_existsById() { var setting = setting(); when(settingRepository.existsById(SETTING_ID_1)).thenReturn(true); - assertThatThrownBy(() -> settingService.createSetting(setting)) + assertThatThrownBy(() -> settingService.create(setting)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Setting already exists with id: " + SETTING_ID_1); verify(settingRepository, never()).save(any()); } @Test - void createSetting_negative_existsByKey() { + void create_negative_existsByKey() { var setting = setting(); when(settingRepository.existsById(SETTING_ID_1)).thenReturn(false); when(settingRepository.existsByKey(SETTING_KEY_1)).thenReturn(true); - assertThatThrownBy(() -> settingService.createSetting(setting)) + assertThatThrownBy(() -> settingService.create(setting)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Setting with key already exists: " + SETTING_KEY_1); verify(settingRepository, never()).save(any()); @@ -148,47 +148,55 @@ void findByQuery_positive_emptyQuery() { } @Test - void updateSetting_positive_updatesSetting() { + void updateSetting_positive_updates() { var updatedSetting = setting(SETTING_ID_1, SETTING_KEY_1, SETTING_VALUE_1); var updatedEntity = settingEntity(SETTING_ID_1, SETTING_KEY_1, SETTING_VALUE_1); when(settingRepository.findById(SETTING_ID_1)).thenReturn(Optional.of(settingEntity())); when(settingMapper.convert(updatedSetting)).thenReturn(updatedEntity); - settingService.updateSetting(updatedSetting); + settingService.update(SETTING_ID_1, updatedSetting); verify(settingRepository).save(updatedEntity); } @Test - void updateSetting_negative_settingNotFound() { + void updateSetting_negative_NotFound() { var updatedSetting = setting(SETTING_ID_1, SETTING_KEY_1, SETTING_VALUE_1); when(settingRepository.findById(SETTING_ID_1)).thenReturn(Optional.empty()); - assertThatThrownBy(() -> settingService.updateSetting(updatedSetting)) + assertThatThrownBy(() -> settingService.update(SETTING_ID_1, updatedSetting)) .isInstanceOf(NotFoundException.class) .hasMessage("Setting not found by id: " + SETTING_ID_1); } @Test - void updateSetting_negative_keyModification() { + void update_negative_unmodifiableId() { + var updatedSetting = setting(SETTING_ID_1, SETTING_KEY_1, SETTING_VALUE_2); + assertThatThrownBy(() -> settingService.update(SETTING_ID_2, updatedSetting)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Id cannot be modified: " + SETTING_ID_2); + } + + @Test + void update_negative_keyModification() { var updatedSetting = setting(SETTING_ID_1, SETTING_KEY_2, SETTING_VALUE_2); when(settingRepository.findById(SETTING_ID_1)).thenReturn(Optional.of(settingEntity())); - assertThatThrownBy(() -> settingService.updateSetting(updatedSetting)) + assertThatThrownBy(() -> settingService.update(SETTING_ID_1, updatedSetting)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Setting key cannot be modified: " + SETTING_KEY_1); } @Test - void deleteSettingById_positive() { + void deleteById_positive() { when(settingRepository.findById(SETTING_ID_1)).thenReturn(Optional.of(settingEntity())); - settingService.deleteSettingById(SETTING_ID_1); + settingService.deleteById(SETTING_ID_1); verify(settingRepository).deleteById(SETTING_ID_1); } @Test - void deleteSettingById_negative_notFoundById() { + void deleteById_negative_notFoundById() { when(settingRepository.findById(SETTING_ID_1)).thenReturn(Optional.empty()); - assertThatThrownBy(() -> settingService.deleteSettingById(SETTING_ID_1)) + assertThatThrownBy(() -> settingService.deleteById(SETTING_ID_1)) .isInstanceOf(NotFoundException.class) .hasMessage("Setting not found by id: " + SETTING_ID_1); } diff --git a/src/test/java/org/folio/dcb/utils/TestJdbcHelper.java b/src/test/java/org/folio/dcb/utils/TestJdbcHelper.java index 6a099fdd..6aa5f7aa 100644 --- a/src/test/java/org/folio/dcb/utils/TestJdbcHelper.java +++ b/src/test/java/org/folio/dcb/utils/TestJdbcHelper.java @@ -81,7 +81,7 @@ public void saveDcbSetting(String tenantId, Setting setting) { .addValue("scope", "mod-dcb", Types.VARCHAR) .addValue("key", setting.getKey(), Types.VARCHAR) .addValue("value", asJsonString(setting.getValue()), Types.OTHER) - .addValue("version", 1, Types.INTEGER) + .addValue("version", 0, Types.INTEGER) .addValue("createdBy", REQUEST_USER_ID, Types.OTHER) .addValue("createdDate", createdDate, Types.TIMESTAMP_WITH_TIMEZONE) .addValue("updatedBy", REQUEST_USER_ID, Types.OTHER) From a3a45cf04940059f17d8d0bd517f90aa5162c81b Mon Sep 17 00:00:00 2001 From: Pavel Filippov Date: Tue, 24 Feb 2026 13:27:15 +0200 Subject: [PATCH 7/8] MODDCB-257: Extend README.md about parsing failures for setting values --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 1bb1e792..978946ca 100644 --- a/README.md +++ b/README.md @@ -188,6 +188,12 @@ Request Body: As a fallback, `service_point_expiration_period` table is used to determine the hold shelf expiration period for the service point. if it's empty, the default value of 10 days will be used. +> **_NOTE:_** +> * Duration is always an integer value. +> * IntervalId is an enum value that can be "Minutes", "Hours", "Days", "Weeks", or "Month".
_The value is +> case-sensitive. Parsing failures will be detected and logged, and in case of error - the fallback +> approach will be used._ + #### Table: service_point_expiration_period - If the table is empty, the **hold shelf expiration period** will be set to the default value of **10 Days**. From 440511611681aa28c307827ec0311db510ba4cc5 Mon Sep 17 00:00:00 2001 From: Pavel Filippov Date: Tue, 24 Feb 2026 17:03:05 +0200 Subject: [PATCH 8/8] MODDCB-257: Fix review issues --- .../dcb/integration/kafka/CirculationEventListener.java | 3 --- src/main/java/org/folio/dcb/service/SettingService.java | 2 +- src/test/java/org/folio/dcb/service/SettingServiceTest.java | 5 +++-- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/folio/dcb/integration/kafka/CirculationEventListener.java b/src/main/java/org/folio/dcb/integration/kafka/CirculationEventListener.java index dbffeaaf..49eb71e9 100644 --- a/src/main/java/org/folio/dcb/integration/kafka/CirculationEventListener.java +++ b/src/main/java/org/folio/dcb/integration/kafka/CirculationEventListener.java @@ -9,12 +9,9 @@ import org.folio.dcb.domain.entity.TransactionEntity; import org.folio.dcb.integration.kafka.model.EventData; import org.folio.dcb.repository.TransactionRepository; -import org.folio.dcb.service.LibraryService; import org.folio.dcb.service.impl.BaseLibraryService; -import org.folio.dcb.service.impl.LendingLibraryServiceImpl; import org.folio.spring.integration.XOkapiHeaders; import org.folio.spring.service.SystemUserScopedExecutionService; -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.kafka.annotation.KafkaListener; import org.springframework.messaging.MessageHeaders; import org.springframework.stereotype.Component; diff --git a/src/main/java/org/folio/dcb/service/SettingService.java b/src/main/java/org/folio/dcb/service/SettingService.java index 2f042ef8..87673b5f 100644 --- a/src/main/java/org/folio/dcb/service/SettingService.java +++ b/src/main/java/org/folio/dcb/service/SettingService.java @@ -111,7 +111,7 @@ public void deleteById(UUID settingId) { var entityToDelete = settingRepository.findById(settingId) .orElseThrow(() -> new NotFoundException("Setting not found by id: " + settingId)); - settingRepository.deleteById(settingId); + settingRepository.delete(entityToDelete); log.debug("deleteSettingById:: Setting was deleted for key: {}", entityToDelete.getKey()); } } diff --git a/src/test/java/org/folio/dcb/service/SettingServiceTest.java b/src/test/java/org/folio/dcb/service/SettingServiceTest.java index 6d987a94..b545be1b 100644 --- a/src/test/java/org/folio/dcb/service/SettingServiceTest.java +++ b/src/test/java/org/folio/dcb/service/SettingServiceTest.java @@ -187,9 +187,10 @@ void update_negative_keyModification() { @Test void deleteById_positive() { - when(settingRepository.findById(SETTING_ID_1)).thenReturn(Optional.of(settingEntity())); + var entity = settingEntity(); + when(settingRepository.findById(SETTING_ID_1)).thenReturn(Optional.of(entity)); settingService.deleteById(SETTING_ID_1); - verify(settingRepository).deleteById(SETTING_ID_1); + verify(settingRepository).delete(entity); } @Test