From f2bf1c26916f13b5e573acb751cc1f768d3e3396 Mon Sep 17 00:00:00 2001 From: Bobby Sharp Date: Tue, 26 Mar 2024 10:42:53 -0400 Subject: [PATCH] MODFQMMGR-202: Include system exchange rate in Lists app --- descriptors/ModuleDescriptor-template.json | 14 +- .../fqm/repository/DataRefreshRepository.java | 155 ++++++ .../MaterializedViewRefreshRepository.java | 34 -- ...roller.java => DataRefreshController.java} | 10 +- .../folio/fqm/service/DataRefreshService.java | 16 + .../MaterializedViewRefreshService.java | 15 - .../db/changelog/changelog-master.xml | 1 + .../changes/v1.1.1/changelog-v1.1.1.xml | 11 + .../create-table-currency-exchange-rates.sql | 4 + ...purchase-order-line-details-definition.xml | 492 ++++++++++++++++++ .../swagger.api/mod-fqm-manager.yaml | 6 +- ...st.java => DataRefreshControllerTest.java} | 14 +- .../repository/DataRefreshRepositoryTest.java | 122 +++++ ...MaterializedViewRefreshRepositoryTest.java | 29 -- .../fqm/service/DataRefreshServiceTest.java | 28 + .../MaterializedViewRefreshServiceTest.java | 28 - 16 files changed, 857 insertions(+), 122 deletions(-) create mode 100644 src/main/java/org/folio/fqm/repository/DataRefreshRepository.java delete mode 100644 src/main/java/org/folio/fqm/repository/MaterializedViewRefreshRepository.java rename src/main/java/org/folio/fqm/resource/{MaterializedViewRefreshController.java => DataRefreshController.java} (54%) create mode 100644 src/main/java/org/folio/fqm/service/DataRefreshService.java delete mode 100644 src/main/java/org/folio/fqm/service/MaterializedViewRefreshService.java create mode 100644 src/main/resources/db/changelog/changes/v1.1.1/changelog-v1.1.1.xml create mode 100644 src/main/resources/db/changelog/changes/v1.1.1/sql/create-table-currency-exchange-rates.sql create mode 100644 src/main/resources/db/changelog/changes/v1.1.1/update-drv-purchase-order-line-details-definition.xml rename src/test/java/org/folio/fqm/controller/{MaterializedViewRefreshControllerTest.java => DataRefreshControllerTest.java} (75%) create mode 100644 src/test/java/org/folio/fqm/repository/DataRefreshRepositoryTest.java delete mode 100644 src/test/java/org/folio/fqm/repository/MaterializedViewRefreshRepositoryTest.java create mode 100644 src/test/java/org/folio/fqm/service/DataRefreshServiceTest.java delete mode 100644 src/test/java/org/folio/fqm/service/MaterializedViewRefreshServiceTest.java diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index 63277c66..0ba40fc8 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -170,7 +170,11 @@ "permissionName": "fqm.materializedViews.post", "displayName": "FQM - Refresh materialized views", "description": "Refresh FQM materialized views", - "visible": true + "visible": true, + "subPermissions": [ + "configuration.entries.collection.get", + "finance.exchange-rate.item.get" + ] }, { "permissionName": "fqm.query.all", @@ -260,6 +264,14 @@ { "id": "acquisitions-units-storage.units", "version": "1.1" + }, + { + "id": "configuration", + "version": "2.0" + }, + { + "id": "finance.exchange-rate", + "version": "1.0" } ], "launchDescriptor": { diff --git a/src/main/java/org/folio/fqm/repository/DataRefreshRepository.java b/src/main/java/org/folio/fqm/repository/DataRefreshRepository.java new file mode 100644 index 00000000..26f28501 --- /dev/null +++ b/src/main/java/org/folio/fqm/repository/DataRefreshRepository.java @@ -0,0 +1,155 @@ +package org.folio.fqm.repository; + +import com.fasterxml.jackson.databind.JsonNode; +import com.jayway.jsonpath.DocumentContext; +import com.jayway.jsonpath.JsonPath; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.folio.fqm.client.SimpleHttpClient; +import org.jooq.DSLContext; +import org.jooq.Field; +import org.jooq.Record2; +import org.jooq.impl.DSL; +import org.springframework.stereotype.Repository; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static org.jooq.impl.DSL.field; +import static org.jooq.impl.DSL.table; + +@Repository +@RequiredArgsConstructor +@Log4j2 +public class DataRefreshRepository { + + public static final Field CURRENCY_FIELD = field("currency", String.class); + public static final Field EXCHANGE_RATE_FIELD = field("exchange_rate", Double.class); + + private static final String REFRESH_MATERIALIZED_VIEW_SQL = "REFRESH MATERIALIZED VIEW CONCURRENTLY "; + private static final String GET_EXCHANGE_RATE_PATH = "finance/exchange-rate"; + private static final String GET_LOCALE_SETTINGS_PATH = "configurations/entries"; + private static final Map GET_LOCALE_SETTINGS_PARAMS = Map.of( + "query", "(module==ORG and configName==localeSettings)" + ); + + private static final List MATERIALIZED_VIEW_NAMES = List.of( + "drv_circulation_loan_status", + "drv_inventory_item_status", + "drv_pol_payment_status", + "drv_pol_receipt_status", + "drv_inventory_statistical_code_full", + "drv_languages" + ); + + static final List SYSTEM_SUPPORTED_CURRENCIES = List.of( + "USD", + "JPY", + "BGN", + "CZK", + "DKK", + "GBP", + "HUF", + "PLN", + "RON", + "SEK", + "CHF", + "ISK", + "NOK", + "HRK", + "RUB", + "TRY", + "AUD", + "BRL", + "CAD", + "CNY", + "HKD", + "IDR", + "ILS", + "INR", + "KRW", + "MXN", + "MYR", + "NZD", + "PHP", + "SGD", + "THB", + "ZAR" + ); + + private final DSLContext jooqContext; + + private final SimpleHttpClient simpleHttpClient; + + public void refreshMaterializedViews(String tenantId) { + for (String matViewName : MATERIALIZED_VIEW_NAMES) { + String fullName = tenantId + "_mod_fqm_manager." + matViewName; + log.info("Refreshing materialized view {}", fullName); + jooqContext.execute(REFRESH_MATERIALIZED_VIEW_SQL + fullName); + } + } + + public void refreshExchangeRates(String tenantId) { + log.info("Refreshing exchange rates"); + String fullTableName = tenantId + "_mod_fqm_manager.currency_exchange_rates"; + String systemCurrency = getSystemCurrencyCode(); + if (!SYSTEM_SUPPORTED_CURRENCIES.contains(systemCurrency)) { + log.info("System currency does not support automatic exchange rate calculation"); + return; + } + + List> exchangeRates = new ArrayList<>(); + for (String currency : SYSTEM_SUPPORTED_CURRENCIES) { + Double exchangeRate = getExchangeRate(currency, systemCurrency); + if (!Double.isNaN(exchangeRate)) { + Record2 currencyExchangeRate = jooqContext + .newRecord(CURRENCY_FIELD, EXCHANGE_RATE_FIELD) + .value1(currency) + .value2(exchangeRate); + exchangeRates.add(currencyExchangeRate); + } + } + + jooqContext.insertInto(table(fullTableName), CURRENCY_FIELD, EXCHANGE_RATE_FIELD) + .valuesOfRecords(exchangeRates) + .onConflict(CURRENCY_FIELD) + .doUpdate() + .set(EXCHANGE_RATE_FIELD, DSL.field("EXCLUDED." + EXCHANGE_RATE_FIELD.getName(), Double.class)) + .execute(); + } + + private String getSystemCurrencyCode() { + log.info("Getting system currency"); + try { + String localeSettingsResponse = simpleHttpClient.get(GET_LOCALE_SETTINGS_PATH, GET_LOCALE_SETTINGS_PARAMS); + JsonNode localeSettings = JsonPath.parse(localeSettingsResponse).read("configs[0].value"); + DocumentContext locale = JsonPath.parse(localeSettings); + return locale.read("currency"); + } catch (Exception e) { + log.info("No system currency defined, defaulting to USD"); + return "USD"; + } + } + + private Double getExchangeRate(String fromCurrency, String toCurrency) { + log.info("Getting currency exchange rate from {} to {}", fromCurrency, toCurrency); + Map exchangeRateParams = Map.of( + "from", fromCurrency, + "to", toCurrency + ); + try { + String exchangeRateResponse = simpleHttpClient.get(GET_EXCHANGE_RATE_PATH, exchangeRateParams); + DocumentContext exchangeRateInfo = JsonPath.parse(exchangeRateResponse); + var exchangeRate = exchangeRateInfo.read("exchangeRate"); + if (exchangeRate instanceof BigDecimal bd) { + return bd.doubleValue(); + } + return (Double) exchangeRate; + } catch (Exception e) { + log.info("Failed to get exchange rate from {} to {}", fromCurrency, toCurrency); + return Double.NaN; + } + } +} diff --git a/src/main/java/org/folio/fqm/repository/MaterializedViewRefreshRepository.java b/src/main/java/org/folio/fqm/repository/MaterializedViewRefreshRepository.java deleted file mode 100644 index 181ada55..00000000 --- a/src/main/java/org/folio/fqm/repository/MaterializedViewRefreshRepository.java +++ /dev/null @@ -1,34 +0,0 @@ -package org.folio.fqm.repository; - -import lombok.RequiredArgsConstructor; -import lombok.extern.log4j.Log4j2; -import org.jooq.DSLContext; -import org.springframework.stereotype.Repository; - -import java.util.List; - -@Repository -@RequiredArgsConstructor -@Log4j2 -public class MaterializedViewRefreshRepository { - private static final String REFRESH_MATERIALIZED_VIEW_SQL = "REFRESH MATERIALIZED VIEW CONCURRENTLY "; - - private static final List materializedViewNames = List.of( - "drv_circulation_loan_status", - "drv_inventory_item_status", - "drv_pol_payment_status", - "drv_pol_receipt_status", - "drv_inventory_statistical_code_full", - "drv_languages" - ); - - private final DSLContext jooqContext; - - public void refreshMaterializedViews(String tenantId) { - for (String matViewName : materializedViewNames) { - String fullName = tenantId + "_mod_fqm_manager." + matViewName; - log.info("Refreshing materialized view {}", fullName); - jooqContext.execute(REFRESH_MATERIALIZED_VIEW_SQL + fullName); - } - } -} diff --git a/src/main/java/org/folio/fqm/resource/MaterializedViewRefreshController.java b/src/main/java/org/folio/fqm/resource/DataRefreshController.java similarity index 54% rename from src/main/java/org/folio/fqm/resource/MaterializedViewRefreshController.java rename to src/main/java/org/folio/fqm/resource/DataRefreshController.java index 7da2c3cb..70d1b9f1 100644 --- a/src/main/java/org/folio/fqm/resource/MaterializedViewRefreshController.java +++ b/src/main/java/org/folio/fqm/resource/DataRefreshController.java @@ -1,7 +1,7 @@ package org.folio.fqm.resource; import lombok.RequiredArgsConstructor; -import org.folio.fqm.service.MaterializedViewRefreshService; +import org.folio.fqm.service.DataRefreshService; import org.folio.spring.FolioExecutionContext; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -9,13 +9,13 @@ @RestController @RequiredArgsConstructor -public class MaterializedViewRefreshController implements MaterializedViewsApi { +public class DataRefreshController implements MaterializedViewsApi { private final FolioExecutionContext executionContext; - private final MaterializedViewRefreshService materializedViewRefreshService; + private final DataRefreshService dataRefreshService; @Override - public ResponseEntity refreshMaterializedViews() { - materializedViewRefreshService.refreshMaterializedViews(executionContext.getTenantId()); + public ResponseEntity refreshData() { + dataRefreshService.refreshData(executionContext.getTenantId()); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } } diff --git a/src/main/java/org/folio/fqm/service/DataRefreshService.java b/src/main/java/org/folio/fqm/service/DataRefreshService.java new file mode 100644 index 00000000..db3037e3 --- /dev/null +++ b/src/main/java/org/folio/fqm/service/DataRefreshService.java @@ -0,0 +1,16 @@ +package org.folio.fqm.service; + +import lombok.RequiredArgsConstructor; +import org.folio.fqm.repository.DataRefreshRepository; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class DataRefreshService { + private final DataRefreshRepository dataRefreshRepository; + + public void refreshData(String tenantId) { + dataRefreshRepository.refreshMaterializedViews(tenantId); + dataRefreshRepository.refreshExchangeRates(tenantId); + } +} diff --git a/src/main/java/org/folio/fqm/service/MaterializedViewRefreshService.java b/src/main/java/org/folio/fqm/service/MaterializedViewRefreshService.java deleted file mode 100644 index a7d1e00f..00000000 --- a/src/main/java/org/folio/fqm/service/MaterializedViewRefreshService.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.folio.fqm.service; - -import lombok.RequiredArgsConstructor; -import org.folio.fqm.repository.MaterializedViewRefreshRepository; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class MaterializedViewRefreshService { - private final MaterializedViewRefreshRepository materializedViewRefreshRepository; - - public void refreshMaterializedViews(String tenantId) { - materializedViewRefreshRepository.refreshMaterializedViews(tenantId); - } -} diff --git a/src/main/resources/db/changelog/changelog-master.xml b/src/main/resources/db/changelog/changelog-master.xml index dd6c98f8..6b166fa9 100644 --- a/src/main/resources/db/changelog/changelog-master.xml +++ b/src/main/resources/db/changelog/changelog-master.xml @@ -10,5 +10,6 @@ + diff --git a/src/main/resources/db/changelog/changes/v1.1.1/changelog-v1.1.1.xml b/src/main/resources/db/changelog/changes/v1.1.1/changelog-v1.1.1.xml new file mode 100644 index 00000000..ddc03fa5 --- /dev/null +++ b/src/main/resources/db/changelog/changes/v1.1.1/changelog-v1.1.1.xml @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/src/main/resources/db/changelog/changes/v1.1.1/sql/create-table-currency-exchange-rates.sql b/src/main/resources/db/changelog/changes/v1.1.1/sql/create-table-currency-exchange-rates.sql new file mode 100644 index 00000000..0dc5fb07 --- /dev/null +++ b/src/main/resources/db/changelog/changes/v1.1.1/sql/create-table-currency-exchange-rates.sql @@ -0,0 +1,4 @@ +CREATE TABLE IF NOT EXISTS currency_exchange_rates( + currency VARCHAR(8) PRIMARY KEY, + exchange_rate FLOAT +); diff --git a/src/main/resources/db/changelog/changes/v1.1.1/update-drv-purchase-order-line-details-definition.xml b/src/main/resources/db/changelog/changes/v1.1.1/update-drv-purchase-order-line-details-definition.xml new file mode 100644 index 00000000..fb40cb64 --- /dev/null +++ b/src/main/resources/db/changelog/changes/v1.1.1/update-drv-purchase-order-line-details-definition.xml @@ -0,0 +1,492 @@ + + + + + { + "id": "90403847-8c47-4f58-b117-9a807b052808", + "name": "drv_purchase_order_line_details", + "private": false, + "fromClause": "src_purchase_order_line JOIN src_purchase_order purchase_order ON purchase_order.id = ((src_purchase_order_line.jsonb ->> 'purchaseOrderId'::text)::uuid) LEFT JOIN src_users_users user_details ON user_details.id = (((purchase_order.jsonb -> 'metadata'::text) ->> 'createdByUserId'::text)::uuid) LEFT JOIN src_users_users user_details_for_order_updated_by ON user_details_for_order_updated_by.id = (((purchase_order.jsonb -> 'metadata'::text) ->> 'updatedByUserId'::text)::uuid) LEFT JOIN src_users_users user_details_for_pol_created_by ON user_details_for_pol_created_by.id = (((src_purchase_order_line.jsonb -> 'metadata'::text) ->> 'createdByUserId'::text)::uuid) LEFT JOIN src_users_users user_details_for_pol_updated_by ON user_details_for_pol_updated_by.id = (((src_purchase_order_line.jsonb -> 'metadata'::text) ->> 'updatedByUserId'::text)::uuid) LEFT JOIN src_organizations organization_details ON organization_details.id = ((purchase_order.jsonb ->> 'vendor'::text)::uuid) LEFT JOIN src_users_users user_details_of_assignee ON user_details_of_assignee.id = ((purchase_order.jsonb ->> 'assignedTo'::text)::uuid) LEFT JOIN currency_exchange_rates rates ON rates.currency = src_purchase_order_line.jsonb -> 'cost' ->> 'currency'", + "columns": [ + { + "name": "acqunit_ids", + "dataType": { + "dataType": "arrayType", + "itemDataType": { + "dataType": "rangedUUIDType" + } + }, + "queryable": false, + "valueGetter": "( SELECT array_agg(acq_id.value::text) FILTER (WHERE (acq_id.value::text) IS NOT NULL) AS array_agg FROM jsonb_array_elements_text(purchase_order.jsonb -> 'acqUnitIds'::text) acq_id(value))", + "filterValueGetter": "( SELECT array_agg(lower(acq_id.value::text)) FILTER (WHERE (acq_id.value::text) IS NOT NULL) AS array_agg FROM jsonb_array_elements_text(purchase_order.jsonb -> 'acqUnitIds'::text) acq_id(value))", + "valueFunction": "lower(:value)", + "visibleByDefault": false + }, + { + "name": "acquisition_unit", + "dataType": { + "dataType": "arrayType", + "itemDataType": { + "dataType": "stringType" + } + }, + "queryable": false, + "valueGetter": "( SELECT array_agg(acq_unit.jsonb ->> 'name'::text) FILTER (WHERE (acq_unit.jsonb ->> 'name'::text) IS NOT NULL) AS array_agg FROM jsonb_array_elements_text((purchase_order.jsonb -> 'acqUnitIds'::text)) record(value) JOIN src_acquisitions_unit acq_unit ON lower(record.value::text) = acq_unit.id::text)", + "filterValueGetter": "( SELECT array_agg(lower(acq_unit.jsonb ->> 'name'::text)) FILTER (WHERE (acq_unit.jsonb ->> 'name'::text) IS NOT NULL) AS array_agg FROM jsonb_array_elements_text((purchase_order.jsonb -> 'acqUnitIds'::text)) record(value) JOIN src_acquisitions_unit acq_unit ON (record.value::text) = acq_unit.id::text)", + "valueFunction": "lower(:value)", + "visibleByDefault": false, + "idColumnName": "acqunit_ids", + "source": { + "entityTypeId": "cc51f042-03e2-43d1-b1d6-11aa6a39bc78", + "columnName": "acquisitions_name" + } + }, + { + "name": "fund_distribution", + "dataType": { + "dataType": "arrayType", + "itemDataType": { + "dataType": "objectType", + "properties": [ + { + "name": "code", + "property": "code", + "dataType": {"dataType": "stringType"}, + "queryable": false, + "valueGetter": "( SELECT array_agg(elems.value ->> 'code') FROM jsonb_array_elements(src_purchase_order_line.jsonb -> 'fundDistribution') AS elems)", + "filterValueGetter": "( SELECT array_agg(lower(elems.value ->> 'code')) FROM jsonb_array_elements(src_purchase_order_line.jsonb -> 'fundDistribution') AS elems)", + "valueFunction": "lower(:value)" + }, + { + "name": "distribution_type", + "property": "distributionType", + "dataType": {"dataType": "stringType"}, + "values": [ + { + "label": "percentage", + "value": "percentage" + }, + { + "label": "amount", + "value": "amount" + } + ], + "queryable": false, + "valueGetter": "( SELECT array_agg(elems.value ->> 'distributionType') FROM jsonb_array_elements(src_purchase_order_line.jsonb -> 'fundDistribution') AS elems)", + "filterValueGetter": "( SELECT array_agg(lower(elems.value ->> 'distributionType')) FROM jsonb_array_elements(src_purchase_order_line.jsonb -> 'fundDistribution') AS elems)", + "valueFunction": "lower(:value)" + }, + { + "name": "encumbrance", + "property": "encumbrance", + "dataType": {"dataType": "rangedUUIDType"}, + "queryable": false, + "valueGetter": "( SELECT array_agg(elems.value ->> 'encumbrance') FROM jsonb_array_elements(src_purchase_order_line.jsonb -> 'fundDistribution') AS elems)", + "filterValueGetter": "( SELECT array_agg(lower(elems.value ->> 'encumbrance')) FROM jsonb_array_elements(src_purchase_order_line.jsonb -> 'fundDistribution') AS elems)", + "valueFunction": "lower(:value)" + }, + { + "name": "fund_id", + "property": "fundId", + "dataType": {"dataType": "rangedUUIDType"}, + "queryable": false, + "valueGetter": "( SELECT array_agg(elems.value ->> 'fundId') FROM jsonb_array_elements(src_purchase_order_line.jsonb -> 'fundDistribution') AS elems)", + "filterValueGetter": "( SELECT array_agg(lower(elems.value ->> 'fundId')) FROM jsonb_array_elements(src_purchase_order_line.jsonb -> 'fundDistribution') AS elems)", + "valueFunction": "lower(:value)" + }, + { + "name": "value", + "property": "value", + "dataType": {"dataType": "numberType"}, + "queryable": false, + "valueGetter": "( SELECT array_agg(elems.value -> 'value') FROM jsonb_array_elements(src_purchase_order_line.jsonb -> 'fundDistribution') AS elems)" + } + ] + } + }, + "valueGetter": "src_purchase_order_line.jsonb ->> 'fundDistribution'", + "visibleByDefault": false + }, + { + "name": "id", + "dataType": { + "dataType": "rangedUUIDType" + }, + "queryable": true, + "valueGetter": "src_purchase_order_line.id", + "isIdColumn": true, + "visibleByDefault": true + }, + { + "name": "po_approved", + "values": [ + { + "label": "True", + "value": "true" + }, + { + "label": "False", + "value": "false" + } + ], + "dataType": { + "dataType": "booleanType" + }, + "queryable": true, + "valueGetter": "purchase_order.jsonb ->> 'approved'", + "filterValueGetter": "\"left\"(lower(${tenant_id}_mod_orders_storage.f_unaccent(purchase_order.jsonb ->> 'approved'::text)), 600)", + "valueFunction": "\"left\"(lower(${tenant_id}_mod_orders_storage.f_unaccent(:value)), 600)", + "visibleByDefault": false + }, + { + "name": "po_assigned_to", + "dataType": { + "dataType": "stringType" + }, + "queryable": true, + "valueGetter": "concat_ws(', '::text, NULLIF((user_details_of_assignee.jsonb -> 'personal'::text) ->> 'lastName', ''::text), NULLIF((user_details_of_assignee.jsonb -> 'personal'::text) ->> 'firstName', ''::text))", + "visibleByDefault": true + }, + { + "name": "po_assigned_to_id", + "dataType": { + "dataType": "rangedUUIDType" + }, + "queryable": true, + "valueGetter": "purchase_order.jsonb ->> 'assignedTo'", + "visibleByDefault": false + }, + { + "name": "po_created_by", + "dataType": { + "dataType": "stringType" + }, + "queryable": true, + "valueGetter": "concat_ws(', '::text, NULLIF((user_details.jsonb -> 'personal'::text) ->> 'lastName', ''::text), NULLIF((user_details.jsonb -> 'personal'::text) ->> 'firstName', ''::text))", + "visibleByDefault": false + }, + { + "name": "po_created_by_id", + "dataType": { + "dataType": "rangedUUIDType" + }, + "queryable": true, + "valueGetter": "purchase_order.jsonb -> 'metadata' ->> 'createdByUserId'", + "filterValueGetter": "\"left\"(lower(${tenant_id}_mod_orders_storage.f_unaccent((purchase_order.jsonb -> 'metadata'::text) ->> 'createdByUserId'::text)), 600)", + "valueFunction": "\"left\"(lower(${tenant_id}_mod_orders_storage.f_unaccent(:value)), 600)", + "visibleByDefault": false + }, + { + "name": "po_created_date", + "dataType": { + "dataType": "dateType" + }, + "queryable": true, + "valueGetter": "purchase_order.jsonb -> 'metadata' ->> 'createdDate'", + "filterValueGetter": "\"left\"(lower(${tenant_id}_mod_orders_storage.f_unaccent((purchase_order.jsonb -> 'metadata'::text) ->> 'createdDate'::text)), 600)", + "valueFunction": "\"left\"(lower(${tenant_id}_mod_orders_storage.f_unaccent(:value)), 600)", + "visibleByDefault": false + }, + { + "name": "po_id", + "dataType": { + "dataType": "rangedUUIDType" + }, + "queryable": true, + "valueGetter": "src_purchase_order_line.jsonb ->> 'purchaseOrderId'", + "visibleByDefault": false + }, + { + "name": "po_notes", + "dataType": { + "dataType": "arrayType", + "itemDataType": { + "dataType": "stringType" + } + }, + "queryable": false, + "valueGetter": "ARRAY(SELECT jsonb_array_elements_text(purchase_order.jsonb -> 'notes'))", + "filterValueGetter": "ARRAY(SELECT lower(jsonb_array_elements_text(purchase_order.jsonb -> 'notes')))", + "valueFunction": "lower(:value)", + "visibleByDefault": false + }, + { + "name": "po_number", + "dataType": { + "dataType": "stringType" + }, + "queryable": true, + "valueGetter": "purchase_order.jsonb ->> 'poNumber'", + "visibleByDefault": false + }, + { + "name": "po_type", + "values": [ + { + "label": "Ongoing", + "value": "Ongoing" + }, + { + "label": "One-Time", + "value": "One-Time" + } + ], + "dataType": { + "dataType": "stringType" + }, + "queryable": true, + "valueGetter": "purchase_order.jsonb ->> 'orderType'", + "filterValueGetter": "\"left\"(lower(${tenant_id}_mod_orders_storage.f_unaccent(purchase_order.jsonb ->> 'orderType'::text)), 600)", + "valueFunction": "\"left\"(lower(${tenant_id}_mod_orders_storage.f_unaccent(:value)), 600)", + "visibleByDefault": false + }, + { + "name": "po_updated_by", + "dataType": { + "dataType": "stringType" + }, + "queryable": true, + "valueGetter": "concat_ws(', '::text, NULLIF((user_details_for_order_updated_by.jsonb -> 'personal'::text) ->> 'lastName', ''::text), NULLIF((user_details_for_order_updated_by.jsonb -> 'personal'::text) ->> 'firstName', ''::text))", + "visibleByDefault": false + }, + { + "name": "po_updated_by_id", + "dataType": { + "dataType": "stringType" + }, + "queryable": true, + "valueGetter": "purchase_order.jsonb -> 'metadata' ->> 'updatedByUserId'", + "visibleByDefault": false + }, + { + "name": "po_updated_date", + "dataType": { + "dataType": "dateType" + }, + "queryable": true, + "valueGetter": "purchase_order.jsonb -> 'metadata' ->> 'updatedDate'", + "visibleByDefault": false + }, + { + "name": "po_workflow_status", + "dataType": { + "dataType": "stringType" + }, + "queryable": true, + "valueGetter": "purchase_order.jsonb ->> 'workflowStatus'", + "filterValueGetter": "\"left\"(lower(${tenant_id}_mod_orders_storage.f_unaccent(purchase_order.jsonb ->> 'workflowStatus'::text)), 600)", + "valueFunction": "\"left\"(lower(${tenant_id}_mod_orders_storage.f_unaccent(:value)), 600)", + "visibleByDefault": true, + "values": [ + { + "value": "closed", + "label": "Closed" + }, + { + "value": "open", + "label": "Open" + }, + { + "value": "pending", + "label": "Pending" + } + ] + }, + { + "name": "pol_created_by", + "dataType": { + "dataType": "stringType" + }, + "queryable": true, + "valueGetter": "concat_ws(', '::text, NULLIF((user_details_for_pol_created_by.jsonb -> 'personal'::text) ->> 'lastName', ''::text), NULLIF((user_details_for_pol_created_by.jsonb -> 'personal'::text) ->> 'firstName', ''::text))", + "visibleByDefault": true + }, + { + "name": "pol_created_by_id", + "dataType": { + "dataType": "rangedUUIDType" + }, + "queryable": true, + "valueGetter": "src_purchase_order_line.jsonb -> 'metadata' ->> 'createdByUserId'", + "visibleByDefault": false + }, + { + "name": "pol_created_date", + "dataType": { + "dataType": "dateType" + }, + "queryable": true, + "valueGetter": "src_purchase_order_line.jsonb -> 'metadata' ->> 'createdDate'", + "filterValueGetter": "\"left\"(lower(${tenant_id}_mod_orders_storage.f_unaccent((src_purchase_order_line.jsonb -> 'metadata'::text) ->> 'createdDate'::text)), 600)", + "valueFunction": "\"left\"(lower(${tenant_id}_mod_orders_storage.f_unaccent(:value)), 600)", + "visibleByDefault": false + }, + { + "name": "pol_currency", + "dataType": { + "dataType": "stringType" + }, + "queryable": true, + "valueGetter": "src_purchase_order_line.jsonb -> 'cost' ->> 'currency'", + "filterValueGetter": "lower(src_purchase_order_line.jsonb -> 'cost' ->> 'currency')", + "valueFunction": "lower(:value)", + "source": { + "entityTypeId": "90403847-8c47-4f58-b117-9a807b052808", + "columnName": "pol_currency" + }, + "visibleByDefault": false + }, + { + "name": "pol_description", + "dataType": { + "dataType": "stringType" + }, + "queryable": true, + "valueGetter": "src_purchase_order_line.jsonb ->> 'poLineDescription'", + "visibleByDefault": false + }, + { + "name": "pol_estimated_price", + "dataType": { + "dataType": "numberType" + }, + "queryable": true, + "valueGetter": "(src_purchase_order_line.jsonb -> 'cost' -> 'poLineEstimatedPrice')::float", + "valueFunction": "(:value)::float", + "visibleByDefault": false + }, + { + "name": "pol_exchange_rate", + "dataType": { + "dataType": "numberType" + }, + "queryable": true, + "valueGetter": "CASE WHEN (src_purchase_order_line.jsonb -> 'cost' ->> 'exchangeRate') IS NOT NULL THEN (src_purchase_order_line.jsonb -> 'cost' -> 'exchangeRate')::float ELSE (rates.exchange_rate) END", + "valueFunction": "(:value)::float", + "visibleByDefault": false + }, + { + "name": "pol_number", + "dataType": { + "dataType": "stringType" + }, + "queryable": true, + "valueGetter": "src_purchase_order_line.jsonb ->> 'poLineNumber'", + "filterValueGetter": "lower(${tenant_id}_mod_orders_storage.f_unaccent(src_purchase_order_line.jsonb ->> 'poLineNumber'::text))", + "valueFunction": "lower(${tenant_id}_mod_orders_storage.f_unaccent(:value))", + "visibleByDefault": true + }, + { + "name": "pol_payment_status", + "dataType": { + "dataType": "stringType" + }, + "queryable": true, + "valueGetter": "src_purchase_order_line.jsonb ->> 'paymentStatus'", + "filterValueGetter": "\"left\"(lower(${tenant_id}_mod_orders_storage.f_unaccent(src_purchase_order_line.jsonb ->> 'paymentStatus'::text)), 600)", + "valueFunction": "\"left\"(lower(${tenant_id}_mod_orders_storage.f_unaccent(:value)), 600)", + "source": { + "entityTypeId": "2168014f-9316-4760-9d82-d0306d5f59e4", + "columnName": "payment_status" + }, + "visibleByDefault": false + }, + { + "name": "pol_receipt_status", + "dataType": { + "dataType": "stringType" + }, + "queryable": true, + "valueGetter": "src_purchase_order_line.jsonb ->> 'receiptStatus'", + "filterValueGetter": "\"left\"(lower(${tenant_id}_mod_orders_storage.f_unaccent(src_purchase_order_line.jsonb ->> 'receiptStatus'::text)), 600)", + "valueFunction": "\"left\"(lower(${tenant_id}_mod_orders_storage.f_unaccent(:value)), 600)", + "source": { + "entityTypeId": "5fefec2a-9d6c-474c-8698-b0ea77186c12", + "columnName": "receipt_status" + }, + "visibleByDefault": true + }, + { + "name": "pol_updated_by", + "dataType": { + "dataType": "stringType" + }, + "queryable": true, + "valueGetter": "concat_ws(', '::text, NULLIF((user_details_for_pol_updated_by.jsonb -> 'personal'::text) ->> 'lastName', ''::text), NULLIF((user_details_for_pol_updated_by.jsonb -> 'personal'::text) ->> 'firstName', ''::text))", + "visibleByDefault": false + }, + { + "name": "pol_updated_by_id", + "dataType": { + "dataType": "rangedUUIDType" + }, + "queryable": true, + "valueGetter": "src_purchase_order_line.jsonb -> 'metadata' ->> 'updatedByUserId'", + "visibleByDefault": false + }, + { + "name": "pol_updated_date", + "dataType": { + "dataType": "dateType" + }, + "queryable": true, + "valueGetter": "src_purchase_order_line.jsonb -> 'metadata' ->> 'updatedDate'", + "visibleByDefault": true + }, + { + "name": "vendor_code", + "dataType": { + "dataType": "stringType" + }, + "queryable": true, + "valueGetter": "organization_details.jsonb ->> 'code'", + "filterValueGetter": "${tenant_id}_mod_organizations_storage.f_unaccent(organization_details.jsonb ->> 'code'::text)", + "valueFunction": "${tenant_id}_mod_organizations_storage.f_unaccent(:value)", + "visibleByDefault": false, + "idColumnName": "vendor_id", + "source": { + "entityTypeId": "489234a9-8703-48cd-85e3-7f84011bafa3", + "columnName": "vendor_code" + } + }, + { + "name": "vendor_id", + "dataType": { + "dataType": "stringType" + }, + "queryable": true, + "valueGetter": "purchase_order.jsonb ->> 'vendor'", + "visibleByDefault": false + }, + { + "name": "vendor_name", + "dataType": { + "dataType": "stringType" + }, + "queryable": true, + "valueGetter": "organization_details.jsonb ->> 'name'", + "filterValueGetter": "${tenant_id}_mod_organizations_storage.f_unaccent(organization_details.jsonb ->> 'name'::text)", + "valueFunction": "${tenant_id}_mod_organizations_storage.f_unaccent(:value)", + "visibleByDefault": false, + "idColumnName": "vendor_id", + "source": { + "entityTypeId": "489234a9-8703-48cd-85e3-7f84011bafa3", + "columnName": "vendor_name" + } + } + ], + "defaultSort": [ + { + "columnName": "id", + "direction": "ASC" + } + ] + } + + id = '90403847-8c47-4f58-b117-9a807b052808' + + + diff --git a/src/main/resources/swagger.api/mod-fqm-manager.yaml b/src/main/resources/swagger.api/mod-fqm-manager.yaml index c38a0739..fb6c23a1 100644 --- a/src/main/resources/swagger.api/mod-fqm-manager.yaml +++ b/src/main/resources/swagger.api/mod-fqm-manager.yaml @@ -46,13 +46,13 @@ paths: $ref: '#/components/responses/internalServerErrorResponse' /entity-types/materialized-views/refresh: post: - operationId: refreshMaterializedViews + operationId: refreshData tags: - materializedViews - description: Refresh all materialized views for a tenant. + description: Refresh all materialized views and similar data for a tenant. responses: '204': - description: 'Views refreshed' + description: 'Data refreshed' '400': $ref: '#/components/responses/badRequestResponse' '500': diff --git a/src/test/java/org/folio/fqm/controller/MaterializedViewRefreshControllerTest.java b/src/test/java/org/folio/fqm/controller/DataRefreshControllerTest.java similarity index 75% rename from src/test/java/org/folio/fqm/controller/MaterializedViewRefreshControllerTest.java rename to src/test/java/org/folio/fqm/controller/DataRefreshControllerTest.java index 27acab39..e054741c 100644 --- a/src/test/java/org/folio/fqm/controller/MaterializedViewRefreshControllerTest.java +++ b/src/test/java/org/folio/fqm/controller/DataRefreshControllerTest.java @@ -1,7 +1,7 @@ package org.folio.fqm.controller; -import org.folio.fqm.resource.MaterializedViewRefreshController; -import org.folio.fqm.service.MaterializedViewRefreshService; +import org.folio.fqm.resource.DataRefreshController; +import org.folio.fqm.service.DataRefreshService; import org.folio.spring.FolioExecutionContext; import org.folio.spring.integration.XOkapiHeaders; import org.junit.jupiter.api.Test; @@ -19,14 +19,14 @@ import static org.springframework.http.MediaType.APPLICATION_JSON; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@WebMvcTest(MaterializedViewRefreshController.class) -class MaterializedViewRefreshControllerTest { +@WebMvcTest(DataRefreshController.class) +class DataRefreshControllerTest { @Autowired private MockMvc mockMvc; @MockBean private FolioExecutionContext executionContext; @MockBean - private MaterializedViewRefreshService materializedViewRefreshService; + private DataRefreshService dataRefreshService; @Test void refreshMaterializedViewsTest() throws Exception { @@ -35,9 +35,9 @@ void refreshMaterializedViewsTest() throws Exception { .header(XOkapiHeaders.TENANT, tenantId) .contentType(APPLICATION_JSON); when(executionContext.getTenantId()).thenReturn(tenantId); - doNothing().when(materializedViewRefreshService).refreshMaterializedViews(tenantId); + doNothing().when(dataRefreshService).refreshData(tenantId); mockMvc.perform(requestBuilder) .andExpect(status().isNoContent()); - verify(materializedViewRefreshService, times(1)).refreshMaterializedViews(tenantId); + verify(dataRefreshService, times(1)).refreshData(tenantId); } } diff --git a/src/test/java/org/folio/fqm/repository/DataRefreshRepositoryTest.java b/src/test/java/org/folio/fqm/repository/DataRefreshRepositoryTest.java new file mode 100644 index 00000000..77934ac7 --- /dev/null +++ b/src/test/java/org/folio/fqm/repository/DataRefreshRepositoryTest.java @@ -0,0 +1,122 @@ +package org.folio.fqm.repository; + +import org.folio.fqm.client.SimpleHttpClient; +import org.jooq.DSLContext; +import org.jooq.InsertOnConflictWhereIndexPredicateStep; +import org.jooq.InsertOnDuplicateSetMoreStep; +import org.jooq.InsertOnDuplicateSetStep; +import org.jooq.InsertValuesStep2; +import org.jooq.Record; +import org.jooq.Record2; +import org.jooq.impl.DSL; +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.mockito.stubbing.Answer; + +import java.util.Collection; +import java.util.Map; +import java.util.stream.Collectors; + +import static org.folio.fqm.repository.DataRefreshRepository.CURRENCY_FIELD; +import static org.folio.fqm.repository.DataRefreshRepository.EXCHANGE_RATE_FIELD; +import static org.folio.fqm.repository.DataRefreshRepository.SYSTEM_SUPPORTED_CURRENCIES; +import static org.jooq.impl.DSL.table; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class DataRefreshRepositoryTest { + @InjectMocks + private DataRefreshRepository dataRefreshRepository; + @Mock + private DSLContext jooqContext; + @Mock + private SimpleHttpClient simpleHttpClient; + + @Test + void refreshMaterializedViewsTest() { + String tenantId = "tenant_01"; + String expectedItemStatusSql = "REFRESH MATERIALIZED VIEW CONCURRENTLY tenant_01_mod_fqm_manager.drv_inventory_item_status"; + String expectedLoanStatusSql = "REFRESH MATERIALIZED VIEW CONCURRENTLY tenant_01_mod_fqm_manager.drv_circulation_loan_status"; + when(jooqContext.execute(anyString())).thenReturn(1); + dataRefreshRepository.refreshMaterializedViews(tenantId); + verify(jooqContext, times(1)).execute(expectedItemStatusSql); + verify(jooqContext, times(1)).execute(expectedLoanStatusSql); + } + + @Test + void shouldRefreshExchangeRates() { + String tenantId = "tenant_01"; + String localeSettingsPath = "configurations/entries"; + String exchangeRatePath = "finance/exchange-rate"; + String fullTableName = "tenant_01_mod_fqm_manager.currency_exchange_rates"; + Map localSettingsParams = Map.of( + "query", "(module==ORG and configName==localeSettings)" + ); + Map expectedExchangeRates = SYSTEM_SUPPORTED_CURRENCIES + .stream() + .collect(Collectors.toMap(currency -> currency, currency -> 1.25)); + when(simpleHttpClient.get(localeSettingsPath, localSettingsParams)).thenReturn(""" + { + "configs": [ + {"id":"2a132a01-623b-4d3a-9d9a-2feb777665c2","module":"ORG","configName":"localeSettings","enabled":true,"value":"{\\"locale\\":\\"en-US\\",\\"timezone\\":\\"UTC\\",\\"currency\\":\\"USD\\"}","metadata":{"createdDate":"2024-03-25T17:37:22.309+00:00","createdByUserId":"db760bf8-e05a-4a5d-a4c3-8d49dc0d4e48"}}], + "totalRecords": 1, + "resultInfo": {"totalRecords":1,"facets":[],"diagnostics":[]} + } + """); + when(simpleHttpClient.get(eq(exchangeRatePath), any())).thenReturn(""" + { + "from": "someCurrency", + "to": "USD", + "exchangeRate": 1.25 + } + """); + + Record2 exchangeRateMock = mock(Record2.class); + when(jooqContext.newRecord(CURRENCY_FIELD, EXCHANGE_RATE_FIELD)) + .thenAnswer((Answer>) invocation -> exchangeRateMock); + when(exchangeRateMock.value1(anyString())).thenReturn(exchangeRateMock); + when(exchangeRateMock.value2(1.25)).thenReturn(exchangeRateMock); + + InsertValuesStep2 insertValuesStep2Mock = mock(InsertValuesStep2.class); + InsertOnConflictWhereIndexPredicateStep insertOnConflictWhereIndexPredicateStep = mock(InsertOnConflictWhereIndexPredicateStep.class); + InsertOnDuplicateSetStep insertOnDuplicateSetStepMock = mock(InsertOnDuplicateSetStep.class); + InsertOnDuplicateSetMoreStep insertOnDuplicateSetMoreStepMock = mock(InsertOnDuplicateSetMoreStep.class); + when(jooqContext.insertInto(table(fullTableName), CURRENCY_FIELD, EXCHANGE_RATE_FIELD)).thenReturn(insertValuesStep2Mock); + when(insertValuesStep2Mock.valuesOfRecords((Collection) any())).thenReturn(insertValuesStep2Mock); + when(insertValuesStep2Mock.onConflict(CURRENCY_FIELD)).thenReturn(insertOnConflictWhereIndexPredicateStep); + when(insertOnConflictWhereIndexPredicateStep.doUpdate()).thenReturn(insertOnDuplicateSetStepMock); + when(insertOnDuplicateSetStepMock.set(EXCHANGE_RATE_FIELD, DSL.field("EXCLUDED." + EXCHANGE_RATE_FIELD.getName(), Double.class))).thenReturn(insertOnDuplicateSetMoreStepMock); + when(insertOnDuplicateSetMoreStepMock.execute()).thenReturn(1); + assertDoesNotThrow(() -> dataRefreshRepository.refreshExchangeRates(tenantId)); + verify(insertOnDuplicateSetMoreStepMock, times(1)).execute(); + } + + @Test + void shouldUseUSDAsDefaultCurrencyIfSystemCurrencyNotDefined() { +// String tenantId = "tenant_01"; +// String localeSettingsPath = "configurations/entries"; +// Map localSettingsParams = Map.of( +// "query", "(module==ORG and configName==localeSettings)" +// ); +// String exchangeRatePath = "finance/exchange-rate"; +// when(simpleHttpClient.get(localeSettingsPath, localSettingsParams)).thenReturn(null); +// when(simpleHttpClient.get(eq(exchangeRatePath), any())).thenReturn(""" +// { +// "from": "someCurrency", +// "to": "USD", +// "exchangeRate": 1.25 +// """); +// assertDoesNotThrow(() -> materializedViewRefreshRepository.refreshExchangeRates(tenantId)); + + } +} diff --git a/src/test/java/org/folio/fqm/repository/MaterializedViewRefreshRepositoryTest.java b/src/test/java/org/folio/fqm/repository/MaterializedViewRefreshRepositoryTest.java deleted file mode 100644 index 565b963c..00000000 --- a/src/test/java/org/folio/fqm/repository/MaterializedViewRefreshRepositoryTest.java +++ /dev/null @@ -1,29 +0,0 @@ -package org.folio.fqm.repository; - -import org.jooq.DSLContext; -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 static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.*; - -@ExtendWith(MockitoExtension.class) -class MaterializedViewRefreshRepositoryTest { - @InjectMocks - private MaterializedViewRefreshRepository materializedViewRefreshRepository; - @Mock - private DSLContext jooqContext; - - @Test - void refreshMaterializedViewsTest() { - String tenantId = "tenant_01"; - String expectedItemStatusSql = "REFRESH MATERIALIZED VIEW CONCURRENTLY tenant_01_mod_fqm_manager.drv_inventory_item_status"; - String expectedLoanStatusSql = "REFRESH MATERIALIZED VIEW CONCURRENTLY tenant_01_mod_fqm_manager.drv_circulation_loan_status"; - when(jooqContext.execute(anyString())).thenReturn(1); - materializedViewRefreshRepository.refreshMaterializedViews(tenantId); - verify(jooqContext, times(1)).execute(expectedItemStatusSql); - verify(jooqContext, times(1)).execute(expectedLoanStatusSql); - } -} diff --git a/src/test/java/org/folio/fqm/service/DataRefreshServiceTest.java b/src/test/java/org/folio/fqm/service/DataRefreshServiceTest.java new file mode 100644 index 00000000..85b4e245 --- /dev/null +++ b/src/test/java/org/folio/fqm/service/DataRefreshServiceTest.java @@ -0,0 +1,28 @@ +package org.folio.fqm.service; + +import org.folio.fqm.repository.DataRefreshRepository; +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 static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class DataRefreshServiceTest { + @InjectMocks + private DataRefreshService dataRefreshService; + @Mock + private DataRefreshRepository dataRefreshRepository; + + @Test + void refreshMaterializedViewsTest() { + String tenantId = "tenant_01"; + doNothing().when(dataRefreshRepository).refreshMaterializedViews(tenantId); + dataRefreshService.refreshData(tenantId); + verify(dataRefreshRepository, times(1)).refreshMaterializedViews(tenantId); + } +} diff --git a/src/test/java/org/folio/fqm/service/MaterializedViewRefreshServiceTest.java b/src/test/java/org/folio/fqm/service/MaterializedViewRefreshServiceTest.java deleted file mode 100644 index 70afa579..00000000 --- a/src/test/java/org/folio/fqm/service/MaterializedViewRefreshServiceTest.java +++ /dev/null @@ -1,28 +0,0 @@ -package org.folio.fqm.service; - -import org.folio.fqm.repository.MaterializedViewRefreshRepository; -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 static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -@ExtendWith(MockitoExtension.class) -class MaterializedViewRefreshServiceTest { - @InjectMocks - private MaterializedViewRefreshService materializedViewRefreshService; - @Mock - private MaterializedViewRefreshRepository materializedViewRefreshRepository; - - @Test - void refreshMaterializedViewsTest() { - String tenantId = "tenant_01"; - doNothing().when(materializedViewRefreshRepository).refreshMaterializedViews(tenantId); - materializedViewRefreshService.refreshMaterializedViews(tenantId); - verify(materializedViewRefreshRepository, times(1)).refreshMaterializedViews(tenantId); - } -}