diff --git a/.github/workflows/api-doc.yml b/.github/workflows/api-doc.yml new file mode 100644 index 00000000..a308c9a8 --- /dev/null +++ b/.github/workflows/api-doc.yml @@ -0,0 +1,91 @@ +name: api-doc + +# https://dev.folio.org/guides/api-doc/ + +# API_TYPES: string: The space-separated list of types to consider. +# One or more of 'RAML OAS'. +# e.g. 'OAS' +# +# API_DIRECTORIES: string: The space-separated list of directories to search +# for API description files. +# e.g. 'src/main/resources/openapi' +# NOTE: -- Also add each separate path to each of the "on: paths:" sections. +# e.g. 'src/main/resources/openapi/**' +# +# API_EXCLUDES: string: The space-separated list of directories and files +# to exclude from traversal, in addition to the default exclusions. +# e.g. '' + +env: + API_TYPES: 'RAML' + API_DIRECTORIES: 'ramls' + API_EXCLUDES: '' + OUTPUT_DIR: 'folio-api-docs' + AWS_S3_BUCKET: 'foliodocs' + AWS_S3_FOLDER: 'api' + AWS_S3_REGION: 'us-east-1' + AWS_S3_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY_ID }} + AWS_S3_ACCESS_KEY: ${{ secrets.S3_SECRET_ACCESS_KEY }} + +on: + push: + branches: [ main, master ] + paths: + - 'ramls/**' + tags: '[vV][0-9]+.[0-9]+.[0-9]+*' + +jobs: + api-doc: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + ref: ${{ github.REF }} + submodules: recursive + - name: Prepare folio-tools + run: | + git clone https://github.com/folio-org/folio-tools + cd folio-tools/api-doc \ + && yarn install \ + && pip3 install -r requirements.txt + - name: Obtain version if release tag + if: ${{ startsWith(github.ref, 'refs/tags/v') }} + run: | + version=$(echo ${GITHUB_REF#refs/tags/[vV]} | awk -F'.' '{ printf("%d.%d", $1, $2) }') + echo "VERSION_MAJ_MIN=${version}" >> $GITHUB_ENV + - name: Set some vars + run: | + echo "REPO_NAME=${GITHUB_REPOSITORY##*/}" >> $GITHUB_ENV + - name: Report some info + run: | + echo "REPO_NAME=${{ env.REPO_NAME }}" + - name: Do api-doc + run: | + if test -n "${{ env.VERSION_MAJ_MIN }}"; then + echo "Docs for release version ${{ env.VERSION_MAJ_MIN }}" + option_release=$(echo "--version ${{ env.VERSION_MAJ_MIN }}") + else + option_release="" + fi + python3 folio-tools/api-doc/api_doc.py \ + --loglevel info \ + --types ${{ env.API_TYPES }} \ + --directories ${{ env.API_DIRECTORIES }} \ + --excludes ${{ env.API_EXCLUDES }} \ + --output ${{ env.OUTPUT_DIR }} $option_release + - name: Show generated files + working-directory: ${{ env.OUTPUT_DIR }} + run: ls -R + - name: Publish to AWS S3 + uses: sai-sharan/aws-s3-sync-action@v0.1.0 + with: + access_key: ${{ env.AWS_S3_ACCESS_KEY_ID }} + secret_access_key: ${{ env.AWS_S3_ACCESS_KEY }} + region: ${{ env.AWS_S3_REGION }} + source: ${{ env.OUTPUT_DIR }} + destination_bucket: ${{ env.AWS_S3_BUCKET }} + destination_prefix: ${{ env.AWS_S3_FOLDER }} + delete: false + quiet: false + diff --git a/.github/workflows/api-lint.yml b/.github/workflows/api-lint.yml new file mode 100644 index 00000000..de292ded --- /dev/null +++ b/.github/workflows/api-lint.yml @@ -0,0 +1,65 @@ +name: api-lint + +# https://dev.folio.org/guides/api-lint/ + +# API_TYPES: string: The space-separated list of types to consider. +# One or more of 'RAML OAS'. +# e.g. 'OAS' +# +# API_DIRECTORIES: string: The space-separated list of directories to search +# for API description files. +# e.g. 'src/main/resources/openapi' +# NOTE: -- Also add each separate path to each of the "on: paths:" sections. +# e.g. 'src/main/resources/openapi/**' +# +# API_EXCLUDES: string: The space-separated list of directories and files +# to exclude from traversal, in addition to the default exclusions. +# e.g. '' +# +# API_WARNINGS: boolean: Whether to cause Warnings to be displayed, +# and to fail the workflow. +# e.g. false + +env: + API_TYPES: 'RAML' + API_DIRECTORIES: 'ramls' + API_EXCLUDES: '' + API_WARNINGS: false + +on: + push: + paths: + - 'ramls/**' + pull_request: + paths: + - 'ramls/**' + +jobs: + api-lint: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + submodules: recursive + - name: Prepare folio-tools + run: | + git clone https://github.com/folio-org/folio-tools + cd folio-tools/api-lint \ + && yarn install \ + && pip3 install -r requirements.txt + - name: Configure default options + run: | + echo "OPTION_WARNINGS=''" >> $GITHUB_ENV + - name: Configure option warnings + if: ${{ env.API_WARNINGS == 'true' }} + run: | + echo "OPTION_WARNINGS=--warnings" >> $GITHUB_ENV + - name: Do api-lint + run: | + python3 folio-tools/api-lint/api_lint.py \ + --loglevel info \ + --types ${{ env.API_TYPES }} \ + --directories ${{ env.API_DIRECTORIES }} \ + --excludes ${{ env.API_EXCLUDES }} \ + ${{ env.OPTION_WARNINGS }} diff --git a/.github/workflows/api-schema-lint.yml b/.github/workflows/api-schema-lint.yml new file mode 100644 index 00000000..7a08b5ac --- /dev/null +++ b/.github/workflows/api-schema-lint.yml @@ -0,0 +1,46 @@ +name: api-schema-lint + +# https://dev.folio.org/guides/describe-schema/ + +# API_DIRECTORIES: string: The space-separated list of directories to search +# for JSON Schema files. +# e.g. 'src/main/resources/openapi' +# NOTE: -- Also add each separate path to each of the "on: paths:" sections. +# e.g. 'src/main/resources/openapi/**' +# +# API_EXCLUDES: string: The space-separated list of directories and files +# to exclude from traversal, in addition to the default exclusions. +# e.g. '' + +env: + API_DIRECTORIES: 'ramls' + API_EXCLUDES: '' + +on: + push: + paths: + - 'ramls/**' + pull_request: + paths: + - 'ramls/**' + +jobs: + api-schema-lint: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + submodules: recursive + - name: Prepare folio-tools + run: | + git clone https://github.com/folio-org/folio-tools + cd folio-tools/api-schema-lint \ + && yarn install \ + && pip3 install -r requirements.txt + - name: Do api-schema-lint + run: | + python3 folio-tools/api-schema-lint/api_schema_lint.py \ + --loglevel info \ + --directories ${{ env.API_DIRECTORIES }} \ + --excludes ${{ env.API_EXCLUDES }} diff --git a/.gitignore b/.gitignore index 151a78c6..21e3ddbc 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ nbproject/ .settings/ .classpath /bin/ +/src/main/resources/postgres-conf.json diff --git a/Dockerfile b/Dockerfile index 181bc803..dd6b07fd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,9 @@ -FROM folioci/alpine-jre-openjdk11:latest +FROM folioci/alpine-jre-openjdk17:latest + +# Install latest patch versions of packages: https://pythonspeed.com/articles/security-updates-in-docker/ +USER root +RUN apk upgrade --no-cache +USER folio ENV VERTICLE_FILE mod-audit-server-fat.jar diff --git a/Jenkinsfile b/Jenkinsfile index 903f88e8..1e98a982 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,12 +1,7 @@ buildMvn { publishModDescriptor = 'yes' mvnDeploy = 'yes' - buildNode = 'jenkins-agent-java11' - - doApiLint = true - doApiDoc = true - apiTypes = 'RAML' - apiDirectories = 'ramls' + buildNode = 'jenkins-agent-java17' doDocker = { buildJavaDocker { diff --git a/NEWS.md b/NEWS.md index 5759b6ce..afba0f20 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,12 @@ +## 2.8.0 2023-10-11 + +* [MODAUD-110](https://issues.folio.org/browse/MODAUD-110) - Logging improvement +* [MODAUD-161](https://issues.folio.org/browse/MODAUD-161) - Use GitHub Workflows api-lint and api-schema-lint and api-doc +* [MODAUD-164](https://issues.folio.org/browse/MODAUD-164) - Allow action types for loan info +* [MODAUD-166](https://issues.folio.org/browse/MODAUD-166) - Update to Java 17 +* [MODAUD-167](https://issues.folio.org/browse/MODAUD-167) - Upgrade folio-kafka-wrapper to 3.0.0 version +* [MODAUD-168](https://issues.folio.org/browse/MODAUD-168) - Incorrect Patron name shown in Circulation log as source for Change Due Date + ## 2.7.0 2023-02-23 * [MODAUD-137](https://issues.folio.org/browse/MODAUD-137) - Logging improvement - Configuration diff --git a/README.md b/README.md index 155fdb34..6faa785e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # mod-audit -Copyright (C) 2017-2022 The Open Library Foundation +Copyright (C) 2017-2023 The Open Library Foundation This software is distributed under the terms of the Apache License, Version 2.0. See the file "[LICENSE](LICENSE)" for more information. diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index 4aac2624..d8074eb2 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -20,7 +20,7 @@ }, { "id": "users", - "version": "15.2 16.0" + "version": "16.0" }, { "id": "template-engine", @@ -28,15 +28,15 @@ }, { "id": "inventory", - "version": "12.0 13.0" + "version": "13.0" }, { "id": "holdings-storage", - "version": "4.4 5.0 6.0" + "version": "6.0" }, { "id": "cancellation-reason-storage", - "version": "1.1" + "version": "1.2" } ], "optional": [ @@ -127,6 +127,30 @@ } ] }, + { + "id": "acquisition-piece-events", + "version": "1.0", + "handlers": [ + { + "methods": [ + "GET" + ], + "pathPattern": "/audit-data/acquisition/piece/{id}", + "permissionsRequired": [ + "acquisition.piece.events.get" + ] + }, + { + "methods": [ + "GET" + ], + "pathPattern": "/audit-data/acquisition/piece/{id}/status-change-history", + "permissionsRequired": [ + "acquisition.piece.events.get" + ] + } + ] + }, { "id": "circulation-logs", "version": "1.2", @@ -230,6 +254,11 @@ "displayName": "Acquisition order-line events - get order-line change events", "description": "Get order-line change events" }, + { + "permissionName": "acquisition.piece.events.get", + "displayName": "Acquisition piece events - get piece change events", + "description": "Get piece change events" + }, { "permissionName": "audit.all", "displayName": "Audit - all permissions", @@ -242,7 +271,8 @@ "audit.item.delete", "circulation-logs.collection.get", "acquisition.order.events.get", - "acquisition.order-line.events.get" + "acquisition.order-line.events.get", + "acquisition.piece.events.get" ] } ], diff --git a/mod-audit-server/pom.xml b/mod-audit-server/pom.xml index a5aaae76..9e13cbf0 100644 --- a/mod-audit-server/pom.xml +++ b/mod-audit-server/pom.xml @@ -2,13 +2,13 @@ 4.0.0 mod-audit-server - 2.7.1-SNAPSHOT + 2.9.0-SNAPSHOT jar mod-audit org.folio - 2.7.1-SNAPSHOT + 2.9.0-SNAPSHOT @@ -41,7 +41,7 @@ org.folio folio-kafka-wrapper - 2.6.2 + 3.0.0 org.apache.kafka @@ -155,7 +155,7 @@ maven-compiler-plugin 3.8.1 - 11 + 17 UTF-8 @@ -219,13 +219,13 @@ - com.nickwongdev + dev.aspectj aspectj-maven-plugin - 1.12.6 + 1.13.1 true false - 11 + 17 **/impl/*.java **/*.aj diff --git a/mod-audit-server/src/main/java/org/folio/builder/description/LoanCheckInDescriptionBuilder.java b/mod-audit-server/src/main/java/org/folio/builder/description/LoanCheckInDescriptionBuilder.java index ae21d4bd..c8ddc140 100644 --- a/mod-audit-server/src/main/java/org/folio/builder/description/LoanCheckInDescriptionBuilder.java +++ b/mod-audit-server/src/main/java/org/folio/builder/description/LoanCheckInDescriptionBuilder.java @@ -14,14 +14,19 @@ import static org.folio.util.LogEventPayloadField.ITEM_STATUS_NAME; import static org.folio.util.LogEventPayloadField.RETURN_DATE; import static org.folio.util.LogEventPayloadField.SYSTEM_RETURN_DATE; +import static org.folio.util.LogEventPayloadField.ZONE_ID; import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.Comparator; + import org.folio.builder.ItemStatus; import io.vertx.core.json.JsonObject; public class LoanCheckInDescriptionBuilder implements DescriptionBuilder { - @Override public String buildDescription(JsonObject logEventPayload) { StringBuilder description = new StringBuilder(); @@ -35,15 +40,19 @@ public String buildDescription(JsonObject logEventPayload) { .append(claimedReturnedResolution); } - LocalDateTime returnDate = getDateTimeProperty(logEventPayload, RETURN_DATE); - LocalDateTime systemReturnDate = getDateTimeProperty(logEventPayload, SYSTEM_RETURN_DATE); + ZoneId zoneId = ZoneId.of(getProperty(logEventPayload, ZONE_ID) != null ? getProperty(logEventPayload, ZONE_ID) : ZoneOffset.UTC.getId()); + ZonedDateTime returnDate = getDateInTenantTimeZone(getDateTimeProperty(logEventPayload, RETURN_DATE), zoneId); + ZonedDateTime systemReturnDate = getDateInTenantTimeZone(getDateTimeProperty(logEventPayload, SYSTEM_RETURN_DATE), zoneId); LocalDateTime dueDate = getDateTimeProperty(logEventPayload, DUE_DATE); - if (!returnDate.isEqual(systemReturnDate)) { - description.append(BACKDATED_TO_MSG).append(getFormattedDateTime(returnDate)); + Comparator comparator = Comparator.comparing( + zdt -> zdt.withSecond(0).withNano(0)); + + if (comparator.compare(returnDate, systemReturnDate) != 0 ) { + description.append(BACKDATED_TO_MSG).append(getFormattedDateTime(returnDate.toLocalDateTime())); } - if (dueDate.isAfter(returnDate)) { + if (dueDate.isAfter(returnDate.toLocalDateTime())) { description.append(OVERDUE_DUE_DATE_MSG).append(getFormattedDateTime(dueDate)); } @@ -51,4 +60,8 @@ public String buildDescription(JsonObject logEventPayload) { return description.toString(); } + + private ZonedDateTime getDateInTenantTimeZone(LocalDateTime localDateTime, ZoneId zoneId) { + return localDateTime.atZone(ZoneId.of(ZoneOffset.UTC.getId())).withZoneSameInstant(zoneId); + } } diff --git a/mod-audit-server/src/main/java/org/folio/builder/service/AbstractNoticeRecordBuilder.java b/mod-audit-server/src/main/java/org/folio/builder/service/AbstractNoticeRecordBuilder.java index 981a6ee8..f8503e34 100644 --- a/mod-audit-server/src/main/java/org/folio/builder/service/AbstractNoticeRecordBuilder.java +++ b/mod-audit-server/src/main/java/org/folio/builder/service/AbstractNoticeRecordBuilder.java @@ -32,6 +32,8 @@ import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.folio.rest.jaxrs.model.Item; import org.folio.rest.jaxrs.model.LinkToIds; import org.folio.rest.jaxrs.model.LogRecord; @@ -41,6 +43,8 @@ import io.vertx.core.json.JsonObject; public abstract class AbstractNoticeRecordBuilder extends LogRecordBuilder { + + private static final Logger LOGGER = LogManager.getLogger(); protected static final String UNKNOWN_VALUE = "unknown"; private final LogRecord.Action action; @@ -53,35 +57,44 @@ protected AbstractNoticeRecordBuilder(Map okapiHeaders, Context @Override public CompletableFuture> buildLogRecord(JsonObject fullPayload) { + LOGGER.debug("buildLogRecord:: Building Log Record"); JsonObject payload = getObjectProperty(fullPayload, PAYLOAD); + LOGGER.info("buildLogRecord:: Log record created successfully"); return fetchUserDetails(payload) .thenCompose(this::fetchTemplateName) .thenCompose(this::createResult); } private CompletableFuture fetchUserDetails(JsonObject payload) { + LOGGER.debug("fetchUserDetails:: Fetching User Details"); String userBarcode = getProperty(payload, USER_BARCODE); + LOGGER.info("fetchUserDetails:: Fetched User Details with user barcode : {}", userBarcode); return userBarcode != null ? fetchUserDetailsByUserBarcode(payload, userBarcode) : fetchUserDetails(payload, getProperty(payload, USER_ID)); } private JsonObject extractFirstItem(JsonObject payload) { + LOGGER.debug("extractFirstItem:: Extracting First Item"); if (getArrayProperty(payload, ITEMS).isEmpty()) { + LOGGER.info("extractFirstItem:: Failed Extarcting First Item because there are no items in payload"); return new JsonObject(); } + LOGGER.info("extractFirstItem:: Extracted First Item"); return getArrayProperty(payload, ITEMS).getJsonObject(0); } public List fetchItems(JsonArray itemsArray) { + LOGGER.debug("fetchItems:: Fetching Items"); return itemsArray.stream() .map(itemJson -> createItem((JsonObject) itemJson)) .collect(Collectors.toList()); } private Item createItem(JsonObject itemJson) { + LOGGER.debug("createItem:: Creating Item"); Item item = new Item() .withItemBarcode(getProperty(itemJson, ITEM_BARCODE)) .withItemId(getProperty(itemJson, ITEM_ID)) @@ -93,10 +106,12 @@ private Item createItem(JsonObject itemJson) { item.setLoanId(id); } }); + LOGGER.info("createItem:: Created Item"); return item; } private CompletableFuture> createResult(JsonObject payload) { + LOGGER.debug("createResult:: Creating Result"); JsonObject itemJson = extractFirstItem(payload); LogRecord logRecord = new LogRecord() .withObject(NOTICE) @@ -114,10 +129,12 @@ private CompletableFuture> createResult(JsonObject payload) { .withTemplateId(getProperty(itemJson, TEMPLATE_ID)) .withNoticePolicyId(getProperty(itemJson, NOTICE_POLICY_ID))); + LOGGER.info("createResult:: Created Result"); return CompletableFuture.completedFuture(Collections.singletonList(logRecord)); } protected String buildDescription(JsonObject payload, JsonObject itemJson) { + LOGGER.debug("buildDescription:: Building Description"); return String.format(NOTICE_MSG, ofNullable(getProperty(payload, TEMPLATE_NAME)).orElse(UNKNOWN_VALUE), ofNullable(getProperty(itemJson, TRIGGERING_EVENT)).orElse(UNKNOWN_VALUE)); diff --git a/mod-audit-server/src/main/java/org/folio/builder/service/CheckInRecordBuilder.java b/mod-audit-server/src/main/java/org/folio/builder/service/CheckInRecordBuilder.java index 6a70499e..abab907e 100644 --- a/mod-audit-server/src/main/java/org/folio/builder/service/CheckInRecordBuilder.java +++ b/mod-audit-server/src/main/java/org/folio/builder/service/CheckInRecordBuilder.java @@ -23,6 +23,8 @@ import java.util.Map; import java.util.concurrent.CompletableFuture; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.folio.builder.description.DescriptionBuilder; import org.folio.builder.description.ItemCheckInDescriptionBuilder; import org.folio.builder.description.LoanCheckInDescriptionBuilder; @@ -37,6 +39,8 @@ public class CheckInRecordBuilder extends LogRecordBuilder { + private static final Logger LOGGER = LogManager.getLogger(); + private final DescriptionBuilder itemCheckInDescriptionBuilder = new ItemCheckInDescriptionBuilder(); public CheckInRecordBuilder(Map okapiHeaders, Context vertxContext) { @@ -45,22 +49,26 @@ public CheckInRecordBuilder(Map okapiHeaders, Context vertxConte @Override public CompletableFuture> buildLogRecord(JsonObject payload) { + LOGGER.debug("buildLogRecord:: Building Log Record"); List logRecords = new ArrayList<>(); logRecords.add(buildItemCheckInRecord(payload)); + LOGGER.info("buildLogRecord:: Item check-in record added to log records"); if (getBooleanProperty(payload, IS_LOAN_CLOSED)) { logRecords.add(buildLoanCheckInRecord(payload)); + LOGGER.info("buildLogRecord:: Loan check-in record added to log records"); } JsonArray requests = getArrayProperty(payload, REQUESTS); for (int i = 0; i < requests.size(); i++) { logRecords.add(buildCheckInRequestStatusChangedRecord(payload, requests.getJsonObject(i))); } - + LOGGER.info("buildLogRecord:: Built Log Record"); return CompletableFuture.completedFuture(logRecords); } private LogRecord buildLoanCheckInRecord(JsonObject payload) { + LOGGER.debug("buildLoanCheckInRecord:: Building loan check-in record"); return new LogRecord().withObject(LogRecord.Object.LOAN) .withAction(LogRecord.Action.CLOSED_LOAN) .withUserBarcode(getProperty(payload, USER_BARCODE)) @@ -73,6 +81,7 @@ private LogRecord buildLoanCheckInRecord(JsonObject payload) { } private LogRecord buildItemCheckInRecord(JsonObject payload) { + LOGGER.debug("buildItemCheckInRecord:: Building item check-in record"); return new LogRecord().withObject(LogRecord.Object.N_A) .withAction(LogRecord.Action.CHECKED_IN) .withUserBarcode(getProperty(payload, USER_BARCODE)) @@ -85,6 +94,7 @@ private LogRecord buildItemCheckInRecord(JsonObject payload) { } private LogRecord buildCheckInRequestStatusChangedRecord(JsonObject payload, JsonObject request) { + LOGGER.debug("buildCheckInRequestStatusChangedRecord:: Building check-in request status changed record"); return new LogRecord().withObject(LogRecord.Object.REQUEST) .withAction(LogRecord.Action.REQUEST_STATUS_CHANGED) .withUserBarcode(getProperty(payload, USER_BARCODE)) @@ -98,6 +108,7 @@ private LogRecord buildCheckInRequestStatusChangedRecord(JsonObject payload, Jso } private List buildItems(JsonObject payload) { + LOGGER.debug("buildItems:: Building items"); return Collections.singletonList(new Item().withItemId(getProperty(payload, ITEM_ID)) .withItemBarcode(getProperty(payload, ITEM_BARCODE)) .withHoldingId(getProperty(payload, HOLDINGS_RECORD_ID)) diff --git a/mod-audit-server/src/main/java/org/folio/builder/service/CheckOutRecordBuilder.java b/mod-audit-server/src/main/java/org/folio/builder/service/CheckOutRecordBuilder.java index 4b7b933a..86920f20 100644 --- a/mod-audit-server/src/main/java/org/folio/builder/service/CheckOutRecordBuilder.java +++ b/mod-audit-server/src/main/java/org/folio/builder/service/CheckOutRecordBuilder.java @@ -24,6 +24,8 @@ import java.util.Map; import java.util.concurrent.CompletableFuture; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.folio.builder.description.LoanCheckOutDescriptionBuilder; import org.folio.builder.description.RequestStatusChangedDescriptionBuilder; import org.folio.rest.jaxrs.model.Item; @@ -35,24 +37,30 @@ import io.vertx.core.json.JsonObject; public class CheckOutRecordBuilder extends LogRecordBuilder { + private static final Logger LOGGER = LogManager.getLogger(); + public CheckOutRecordBuilder(Map okapiHeaders, Context vertxContext, String logEventType) { super(okapiHeaders, vertxContext, logEventType); } @Override public CompletableFuture> buildLogRecord(JsonObject payload) { + LOGGER.debug("buildLogRecord:: Building Log Record"); List logRecords = new ArrayList<>(); logRecords.add(buildLoanCheckOutRecord(payload)); + LOGGER.info("buildLogRecord:: Loan check-out record added to log records"); JsonArray requests = getArrayProperty(payload, REQUESTS); for (int i = 0; i < requests.size(); i++) { logRecords.add(buildCheckOutRequestStatusChangedRecord(payload, requests.getJsonObject(i))); } + LOGGER.info("buildLogRecord:: Built Log Record"); return CompletableFuture.completedFuture(logRecords); } private LogRecord buildCheckOutRequestStatusChangedRecord(JsonObject payload, JsonObject request) { + LOGGER.debug("buildCheckOutRequestStatusChangedRecord:: Building check-out request status changed log record"); return new LogRecord().withObject(LogRecord.Object.REQUEST) .withAction(LogRecord.Action.REQUEST_STATUS_CHANGED) .withUserBarcode(getProperty(payload, USER_BARCODE)) @@ -66,6 +74,7 @@ private LogRecord buildCheckOutRequestStatusChangedRecord(JsonObject payload, Js } private LogRecord buildLoanCheckOutRecord(JsonObject payload) { + LOGGER.debug("buildLoanCheckOutRecord:: Building loan check-out log record"); return new LogRecord().withObject(LogRecord.Object.LOAN) .withAction(CHECK_OUT_THROUGH_OVERRIDE_EVENT.equals(logEventType) ? CHECKED_OUT_THROUGH_OVERRIDE : CHECKED_OUT) .withUserBarcode(getProperty(payload, USER_BARCODE)) @@ -78,6 +87,7 @@ private LogRecord buildLoanCheckOutRecord(JsonObject payload) { } private List buildItems(JsonObject payload) { + LOGGER.debug("buildItems:: Building items"); return Collections.singletonList(new Item().withItemId(getProperty(payload, ITEM_ID)) .withItemBarcode(getProperty(payload, ITEM_BARCODE)) .withHoldingId(getProperty(payload, HOLDINGS_RECORD_ID)) diff --git a/mod-audit-server/src/main/java/org/folio/builder/service/FeeFineRecordBuilder.java b/mod-audit-server/src/main/java/org/folio/builder/service/FeeFineRecordBuilder.java index 96baacc0..497906df 100644 --- a/mod-audit-server/src/main/java/org/folio/builder/service/FeeFineRecordBuilder.java +++ b/mod-audit-server/src/main/java/org/folio/builder/service/FeeFineRecordBuilder.java @@ -27,6 +27,8 @@ import java.util.Map; import java.util.concurrent.CompletableFuture; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.folio.builder.description.FeeFineDescriptionBuilder; import org.folio.rest.jaxrs.model.Item; import org.folio.rest.jaxrs.model.LinkToIds; @@ -36,20 +38,25 @@ import io.vertx.core.json.JsonObject; public class FeeFineRecordBuilder extends LogRecordBuilder { + private static final Logger LOGGER = LogManager.getLogger(); + public FeeFineRecordBuilder(Map okapiHeaders, Context vertxContext) { super(okapiHeaders, vertxContext); } @Override public CompletableFuture> buildLogRecord(JsonObject fullPayload) { + LOGGER.debug("buildLogRecord:: Building Log Record"); JsonObject payload = getObjectProperty(fullPayload, PAYLOAD); + LOGGER.info("buildLogRecord:: Built Log Record"); return fetchUserDetails(payload, getProperty(payload, USER_ID)) .thenCompose(this::fetchItemDetails) .thenCompose(this::createResult); } private CompletableFuture> createResult(JsonObject payload) { + LOGGER.debug("createResult:: Creating Result"); Item item = new Item() .withItemId(getProperty(payload, ITEM_ID)) .withItemBarcode(getProperty(payload, ITEM_BARCODE)) @@ -62,6 +69,7 @@ private CompletableFuture> createResult(JsonObject payload) { } }); + LOGGER.info("createResult:: Created Result"); return CompletableFuture.completedFuture(Collections.singletonList(new LogRecord() .withObject(FEE_FINE) .withUserBarcode(getProperty(payload, USER_BARCODE)) diff --git a/mod-audit-server/src/main/java/org/folio/builder/service/LoanRecordBuilder.java b/mod-audit-server/src/main/java/org/folio/builder/service/LoanRecordBuilder.java index e75aa513..429779fa 100644 --- a/mod-audit-server/src/main/java/org/folio/builder/service/LoanRecordBuilder.java +++ b/mod-audit-server/src/main/java/org/folio/builder/service/LoanRecordBuilder.java @@ -2,7 +2,6 @@ import static org.folio.rest.jaxrs.model.LogRecord.Action.AGE_TO_LOST; import static org.folio.rest.jaxrs.model.LogRecord.Action.ANONYMIZE; -import static org.folio.rest.jaxrs.model.LogRecord.Action.CHANGED_DUE_DATE; import static org.folio.rest.jaxrs.model.LogRecord.Object.LOAN; import static org.folio.util.Constants.SYSTEM; import static org.folio.util.JsonPropertyFetcher.getObjectProperty; @@ -27,6 +26,8 @@ import java.util.Map; import java.util.concurrent.CompletableFuture; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.folio.rest.jaxrs.model.Item; import org.folio.rest.jaxrs.model.LinkToIds; import org.folio.rest.jaxrs.model.LogRecord; @@ -36,26 +37,33 @@ public class LoanRecordBuilder extends LogRecordBuilder { + private static final Logger LOGGER = LogManager.getLogger(); + public LoanRecordBuilder(Map okapiHeaders, Context vertxContext) { super(okapiHeaders, vertxContext); } @Override public CompletableFuture> buildLogRecord(JsonObject fullPayload) { + LOGGER.debug("buildLogRecord:: Building Log Record"); JsonObject payload = getObjectProperty(fullPayload, PAYLOAD); if (isAction(payload, ANONYMIZE)) { + LOGGER.info("buildLogRecord:: Built Log Record for Anonymize Action"); return fetchItemDetails(payload) .thenCompose(this::createResult); - } else if (isAction(payload, AGE_TO_LOST) || isAction(payload, CHANGED_DUE_DATE)) { + } else if (isAction(payload, AGE_TO_LOST)) { + LOGGER.info("buildLogRecord:: Built Log Record for Age To Lost"); return fetchUserDetails(payload, getProperty(payload, USER_ID)) .thenCompose(this::createResult); } + LOGGER.info("buildLogRecord:: Built Log Record"); return fetchUserDetails(payload, getProperty(payload, UPDATED_BY_USER_ID)) .thenCompose(this::createResult); } private CompletableFuture> createResult(JsonObject payload) { + LOGGER.debug("createResult:: Creating log record result"); return CompletableFuture.completedFuture(Collections.singletonList(new LogRecord() .withObject(LOAN) .withUserBarcode(getProperty(payload, USER_BARCODE)) @@ -74,6 +82,7 @@ private CompletableFuture> createResult(JsonObject payload) { } private boolean isAction(JsonObject payload, LogRecord.Action action) { + LOGGER.debug("isAction:: Checking action"); return action.equals(resolveAction(getProperty(payload, ACTION))); } } diff --git a/mod-audit-server/src/main/java/org/folio/builder/service/LogRecordBuilder.java b/mod-audit-server/src/main/java/org/folio/builder/service/LogRecordBuilder.java index 6cd0b192..53e4355d 100644 --- a/mod-audit-server/src/main/java/org/folio/builder/service/LogRecordBuilder.java +++ b/mod-audit-server/src/main/java/org/folio/builder/service/LogRecordBuilder.java @@ -85,6 +85,7 @@ public LogRecordBuilder(Map okapiHeaders, Context vertxContext, } private CompletableFuture handleGetRequest(String endpoint) { + LOGGER.debug("handleGetRequest:: handling Get Request with endpoint : {}", endpoint); CompletableFuture future = new CompletableFuture<>(); final String okapiURL = okapiHeaders.getOrDefault(OKAPI_URL, ""); @@ -92,7 +93,7 @@ private CompletableFuture handleGetRequest(String endpoint) { HttpClientInterface httpClient = HttpClientFactory.getHttpClient(okapiURL, tenantId); try { - LOGGER.info("Calling GET {}", endpoint); + LOGGER.info("handleGetRequest::Calling GET {}", endpoint); httpClient.request(HttpMethod.GET, endpoint, okapiHeaders) .whenComplete((response, throwable) -> { if (Objects.nonNull(throwable)) { @@ -104,7 +105,7 @@ private CompletableFuture handleGetRequest(String endpoint) { httpClient.closeClient(); }); } catch (Exception e) { - LOGGER.error(EXCEPTION_CALLING_ENDPOINT_MSG, HttpMethod.GET, endpoint); + LOGGER.warn(EXCEPTION_CALLING_ENDPOINT_MSG, HttpMethod.GET, endpoint); future.completeExceptionally(e); httpClient.closeClient(); } @@ -112,7 +113,9 @@ private CompletableFuture handleGetRequest(String endpoint) { } public CompletableFuture getEntitiesByQuery(String url, Class collection, int limit, int offset, String query) { + LOGGER.debug("getEntitiesByQuery:: Getting Entities By Query : {}", query); String endpoint = String.format(url + SEARCH_PARAMS, limit, offset, buildQuery(query)); + LOGGER.info("getEntitiesByQuery:: Entities successfully retrieved from endpoint: {}", endpoint); return handleGetRequest(endpoint).thenApply(response -> response.mapTo(collection)); } @@ -123,41 +126,49 @@ public CompletableFuture getEntitiesByQuery(String url, Class collecti * @return future with list of item records */ public CompletableFuture getEntitiesByIds(String url, Class collection, int limit, int offset, String... ids) { + LOGGER.debug("getEntitiesByIds:: Getting Entities By Ids"); return getEntitiesByQuery(url, collection, limit, offset, convertIdsToCqlQuery(ids)); } public CompletableFuture fetchTemplateName(JsonObject payload) { + LOGGER.debug("fetchTemplateName:: Fetching Template Name"); String templateId = getProperty(extractFirstItem(payload), TEMPLATE_ID); if (templateId == null) { + LOGGER.warn("No template ID found in payload, returning payload as is."); return completedFuture(payload); } - + LOGGER.info("fetchTemplateName:: Fetching template name from URL"); return handleGetRequest(String.format(URL_WITH_ID_PATTERN, TEMPLATES_URL, templateId)) .thenCompose(templateJson -> { if (nonNull(templateJson)) { + LOGGER.info("fetchTemplateName:: Template name successfully retrieved"); return completedFuture(payload.put(TEMPLATE_NAME.value(), isNull(getProperty(templateJson, NAME)) ? EMPTY : getProperty(templateJson, NAME))); } + LOGGER.warn("No template found with ID: {}", templateId); return completedFuture(payload.put(TEMPLATE_NAME.value(), EMPTY)); }); } public CompletableFuture fetchUserDetails(JsonObject payload, String userId) { + LOGGER.debug("fetchUserDetails:: Fetching user details for user Id : {}", userId); return getEntitiesByIds(USERS_URL, UserCollection.class, 1, 0, userId) .thenCompose(users -> { users.getUsers() .stream() .findFirst() .ifPresent(user -> updatePayload(payload, userId, user)); - + LOGGER.info("fetchUserDetails:: Fetched user details for user Id : {}", userId); return CompletableFuture.completedFuture(payload); }); } private void updatePayload(JsonObject payload, String userId, User user) { + LOGGER.debug("updatePayload:: Updating payload with details for user ID {}", userId); if (nonNull(user)) { if (userId.equals(getProperty(payload, USER_ID))) { + LOGGER.info("updatePayload:: Updating user id to payload because user id : {} matched with user id from payload", userId); payload.put(USER_BARCODE.value(), user.getBarcode()); } fetchUserPersonal(payload, user); @@ -165,7 +176,7 @@ private void updatePayload(JsonObject payload, String userId, User user) { } public CompletableFuture fetchUserAndSourceDetails(JsonObject payload, String userId, String sourceId) { - + LOGGER.debug("fetchUserAndSourceDetails:: Fetching user details for user Id : {} and source ID {}", userId, sourceId); return getEntitiesByIds(USERS_URL, UserCollection.class, 2, 0, userId, sourceId) .thenCompose(users -> { Map usersGroupedById = StreamEx.of(users.getUsers()).collect(toMap(User::getId, identity())); @@ -175,36 +186,44 @@ public CompletableFuture fetchUserAndSourceDetails(JsonObject payloa updatePayload(payload, userId, user); if (nonNull(source)) { + LOGGER.info("fetchUserAndSourceDetails:: Adding source to the payload"); payload.put(SOURCE.value(), buildPersonalName(source.getPersonal().getFirstName(), source.getPersonal().getLastName())); } + LOGGER.info("fetchUserAndSourceDetails:: Fetched user details for user Id : {} and source ID {}", userId, sourceId); return completedFuture(payload); }); } public CompletableFuture fetchUserDetailsByUserBarcode(JsonObject payload, String userBarcode) { + LOGGER.debug("fetchUserDetailsByUserBarcode:: Fetching user details by user barcode {}", userBarcode); return getEntitiesByQuery(USERS_URL, UserCollection.class, 1, 0, "barcode==" + userBarcode) .thenCompose(users -> { var user = users.getUsers().get(0); if (nonNull(user)) { if (userBarcode.equals(getProperty(payload, USER_BARCODE))) { + LOGGER.info("fetchUserDetailsByUserBarcode:: Adding user id to payload because user barcode : {} matched with user barcode from payload", userBarcode); payload.put(USER_ID.value(), user.getId()); } fetchUserPersonal(payload, user); } + LOGGER.info("fetchUserDetailsByUserBarcode:: Fetched user details by user barcode {}", userBarcode); return completedFuture(payload); }); } private void fetchUserPersonal(JsonObject payload, User user) { + LOGGER.debug("fetchUserPersonal:: Fetching User Personal"); var personal = user.getPersonal(); if (nonNull(personal) && nonNull(buildPersonalName(personal.getFirstName(), personal.getLastName()))) { + LOGGER.info("fetchUserPersonal:: Fetched personal name"); payload.put(PERSONAL_NAME.value(), buildPersonalName(personal.getFirstName(), personal.getLastName())); } } public CompletableFuture fetchItemDetails(JsonObject payload) { + LOGGER.debug("fetchItemDetails:: Fetching Item Details"); return handleGetRequest(String.format(URL_WITH_ID_PATTERN, ITEMS_URL, getProperty(payload, ITEM_ID))) .thenCompose(itemJson -> nonNull(itemJson) ? CompletableFuture.completedFuture(itemJson) : handleGetRequest(String.format(URL_WITH_ID_PATTERN, CIRCULATION_ITEM_URL, getProperty(payload, ITEM_ID)))) @@ -223,10 +242,12 @@ private String buildQuery(String query) { * @return URL encoded string */ private String encodeQuery(String query) { + LOGGER.debug("encodeQuery:: Encoding Query : {}", query ); try { + LOGGER.info("Encoded query"); return URLEncoder.encode(query, StandardCharsets.UTF_8.toString()); } catch (UnsupportedEncodingException e) { - LOGGER.error("Error happened while attempting to encode '{}'", query); + LOGGER.warn("Error happened while attempting to encode '{}'", query); throw new CompletionException(e); } } @@ -250,69 +271,88 @@ private String convertIdsToCqlQuery(String... ids) { * @return String representing CQL query to get records by some property values */ private String convertIdsToCqlQuery(String fieldName, boolean strictMatch, String... values) { + LOGGER.debug("convertIdsToCqlQuery:: Converting Ids To CqlQuery with field name : {}", fieldName); String prefix = fieldName + (strictMatch ? "==(" : "=("); + LOGGER.info("Converted IDs to CQL query"); return StreamEx.of(values) .joining(" or ", prefix, ")"); } private CompletableFuture addItemData(JsonObject payload, JsonObject itemJson) { + LOGGER.debug("addItemData:: Adding Item Data"); if (nonNull(itemJson)) { ofNullable(getProperty(itemJson, BARCODE)) .ifPresent(barcode -> payload.put(ITEM_BARCODE.value(), barcode)); + LOGGER.info("addItemData:: Added item barcode"); ofNullable(getProperty(itemJson, HOLDINGS_RECORD_ID)) .ifPresent(holdingsRecordId -> payload.put(HOLDINGS_RECORD_ID.value(), holdingsRecordId)); + LOGGER.info("addItemData:: Added holdings record ID"); } + LOGGER.info("addItemData:: Added Item Data"); return completedFuture(payload); } private CompletableFuture addHoldingData(JsonObject payload, JsonObject holdingJson) { + LOGGER.debug("addHoldingData:: Adding Holding Data"); if (nonNull(holdingJson)) { ofNullable(getProperty(holdingJson, INSTANCE_ID)) .ifPresent(instanceId -> payload.put(INSTANCE_ID.value(), instanceId)); } + LOGGER.info("addHoldingData:: Added Holding Data"); return completedFuture(payload); } private JsonObject extractFirstItem(JsonObject payload) { + LOGGER.debug("extractFirstItem:: Extracting First Item from payload"); if (getArrayProperty(payload, ITEMS).isEmpty()) { + LOGGER.warn("No items found in payload"); return new JsonObject(); } + LOGGER.debug("extractFirstItem:: Extracted First Item from payload"); return getArrayProperty(payload, ITEMS).getJsonObject(0); } private static JsonObject verifyAndExtractBody(Response response) { + LOGGER.debug("verifyAndExtractBody:: Verifying and Extracting Body"); var endpoint = response.getEndpoint(); var code = response.getCode(); var body = response.getBody(); if (!Response.isSuccess(code)) { - LOGGER.error("Error calling {} with code {}, response body: {}", endpoint, code, body); + LOGGER.warn("Error calling {} with code {}, response body: {}", endpoint, code, body); return null; } - LOGGER.info("The response body for GET {}: {}", endpoint, nonNull(body) ? body.encodePrettily() : null); + LOGGER.info("verifyAndExtractBody:: The response body for GET {}: {}", endpoint, nonNull(body) ? body.encodePrettily() : null); return response.getBody(); } protected LogRecord.Action resolveAction(String actionString) { + LOGGER.debug("resolveAction:: Resolving Action"); try { + LOGGER.info("resolveAction:: Trying to Resolve Action with action String : {}", actionString); return LogRecord.Action.fromValue(actionString); } catch (IllegalArgumentException e) { String errorMessage = "Builder isn't implemented yet for: " + actionString; if (isEmpty(actionString)) { errorMessage = "Action is empty"; - LOGGER.error(errorMessage); + LOGGER.warn(errorMessage); } throw new IllegalArgumentException(errorMessage); } } String buildPersonalName(String firstName, String lastName) { + LOGGER.debug("buildPersonalName:: Building Personal Name with firstname : {} and lastname : {}", firstName, lastName); if (isNotEmpty(firstName) && isNotEmpty(lastName)) { + LOGGER.info("buildPersonalName:: Built Personal Name with firstname : {} and lastname : {}", firstName, lastName); return lastName + ", " + firstName; } else if (isEmpty(firstName) && isNotEmpty(lastName)) { + LOGGER.info("buildPersonalName:: Built Personal Name with lastname : {}", lastName); return lastName; } else if (isNotEmpty(firstName) && isEmpty(lastName)) { + LOGGER.info("buildPersonalName:: Built Personal Name with firstname : {}", firstName); return firstName; } else { + LOGGER.info("buildPersonalName:: Error building personal name because there is no firstname and lastname"); return null; } } diff --git a/mod-audit-server/src/main/java/org/folio/builder/service/ManualBlockRecordBuilder.java b/mod-audit-server/src/main/java/org/folio/builder/service/ManualBlockRecordBuilder.java index 79a46f20..ec35770b 100644 --- a/mod-audit-server/src/main/java/org/folio/builder/service/ManualBlockRecordBuilder.java +++ b/mod-audit-server/src/main/java/org/folio/builder/service/ManualBlockRecordBuilder.java @@ -20,6 +20,8 @@ import java.util.Map; import java.util.concurrent.CompletableFuture; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.folio.builder.description.ManualBlockDescriptionBuilder; import org.folio.rest.external.User; import org.folio.rest.external.UserCollection; @@ -32,12 +34,15 @@ import one.util.streamex.StreamEx; public class ManualBlockRecordBuilder extends LogRecordBuilder { + private static final Logger LOGGER = LogManager.getLogger(); + public ManualBlockRecordBuilder(Map okapiHeaders, Context vertxContext) { super(okapiHeaders, vertxContext); } @Override public CompletableFuture> buildLogRecord(JsonObject event) { + LOGGER.debug("buildLogRecord:: Building log record"); String logEventType = getProperty(event, LOG_EVENT_TYPE); var block = getObjectProperty(event, PAYLOAD).mapTo(ManualBlock.class); @@ -46,6 +51,7 @@ public CompletableFuture> buildLogRecord(JsonObject event) { String sourceId = block.getMetadata() .getUpdatedByUserId(); + LOGGER.info("buildLogRecord:: Built Log Record"); return getEntitiesByIds(USERS_URL, UserCollection.class, 2, 0, userId, sourceId).thenCompose(users -> { Map usersGroupedById = StreamEx.of(users.getUsers()).collect(toMap(User::getId, identity())); LogRecord manualBlockLogRecord = buildManualBlockLogRecord(block, logEventType, userId, sourceId, usersGroupedById); @@ -55,6 +61,7 @@ public CompletableFuture> buildLogRecord(JsonObject event) { private LogRecord buildManualBlockLogRecord(ManualBlock block, String logEventType, String userId, String sourceId, Map usersGroupedById) { + LOGGER.debug("buildManualBlockLogRecord:: Building manual block log record for block with logEventType: {}, userId: {}, sourceId: {}", logEventType, userId, sourceId); return new LogRecord().withObject(LogRecord.Object.MANUAL_BLOCK) .withUserBarcode(ofNullable(usersGroupedById.get(userId)).flatMap(user -> of(user.getBarcode())).orElse(null)) .withSource(getSource(logEventType, sourceId, usersGroupedById)) @@ -65,19 +72,25 @@ private LogRecord buildManualBlockLogRecord(ManualBlock block, String logEventTy } private String getSource(String logEventType, String sourceId, Map usersGroupedById) { + LOGGER.debug("getSource:: Getting source for log event type with sourceId: {} ", sourceId); var user = usersGroupedById.get(sourceId); return isNull(user) || isNull(user.getPersonal()) ? null : buildPersonalName(user.getPersonal().getFirstName(), user.getPersonal().getLastName()); } private LogRecord.Action resolveLogRecordAction(String logEventType) { + LOGGER.debug("resolveLogRecordAction:: Resolving Log record action for log event type: {}", logEventType); if (MANUAL_BLOCK_CREATED.equals(logEventType)) { + LOGGER.info("resolveLogRecordAction:: Log record action created"); return LogRecord.Action.CREATED; } else if (MANUAL_BLOCK_MODIFIED.equals(logEventType)) { + LOGGER.info("resolveLogRecordAction:: Log record action modified"); return LogRecord.Action.MODIFIED; } else if (MANUAL_BLOCK_DELETED.equals(logEventType)) { + LOGGER.info("resolveLogRecordAction:: Log record action deleted"); return LogRecord.Action.DELETED; } else { + LOGGER.warn("Builder isn't implemented yet for: {} ", logEventType); throw new IllegalArgumentException("Builder isn't implemented yet for: " + logEventType); } } diff --git a/mod-audit-server/src/main/java/org/folio/builder/service/NoticeErrorRecordBuilder.java b/mod-audit-server/src/main/java/org/folio/builder/service/NoticeErrorRecordBuilder.java index 44f3d95d..4acaa78f 100644 --- a/mod-audit-server/src/main/java/org/folio/builder/service/NoticeErrorRecordBuilder.java +++ b/mod-audit-server/src/main/java/org/folio/builder/service/NoticeErrorRecordBuilder.java @@ -6,18 +6,23 @@ import java.util.Map; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.folio.rest.jaxrs.model.LogRecord; import io.vertx.core.Context; import io.vertx.core.json.JsonObject; public class NoticeErrorRecordBuilder extends AbstractNoticeRecordBuilder { + private static final Logger LOGGER = LogManager.getLogger(); + public NoticeErrorRecordBuilder(Map okapiHeaders, Context vertxContext) { super(okapiHeaders, vertxContext, LogRecord.Action.SEND_ERROR); } @Override protected String buildDescription(JsonObject payload, JsonObject itemJson) { + LOGGER.debug("buildDescription:: Building description with payload and item json"); return super.buildDescription(payload, itemJson) + " Error message: " + ofNullable(getProperty(payload, ERROR_MESSAGE)).orElse(UNKNOWN_VALUE); } diff --git a/mod-audit-server/src/main/java/org/folio/builder/service/RequestRecordBuilder.java b/mod-audit-server/src/main/java/org/folio/builder/service/RequestRecordBuilder.java index 373bdca1..6c468e75 100644 --- a/mod-audit-server/src/main/java/org/folio/builder/service/RequestRecordBuilder.java +++ b/mod-audit-server/src/main/java/org/folio/builder/service/RequestRecordBuilder.java @@ -50,6 +50,8 @@ import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.folio.builder.description.RequestDescriptionBuilder; import org.folio.rest.external.CancellationReasonCollection; import org.folio.rest.jaxrs.model.Item; @@ -61,6 +63,8 @@ public class RequestRecordBuilder extends LogRecordBuilder { + private static final Logger LOGGER = LogManager.getLogger(); + public static final String CLOSED_CANCELLED_STATUS = "Closed - Cancelled"; public static final String PICKUP_EXPIRED_STATUS = "Closed - Pickup expired"; RequestDescriptionBuilder requestDescriptionBuilder = new RequestDescriptionBuilder(); @@ -75,13 +79,14 @@ public CompletableFuture> buildLogRecord(JsonObject event) { } private CompletableFuture> buildRequestLogRecord(JsonObject event) { - + LOGGER.debug("buildRequestLogRecord:: Building Request Log Record"); List records = new ArrayList<>(); final LogRecord.Action action = resolveLogRecordAction(getProperty(event, LOG_EVENT_TYPE)); var requests = getNestedObjectProperty(event, PAYLOAD, REQUESTS); if (LogRecord.Action.CREATED == action || LogRecord.Action.CREATED_THROUGH_OVERRIDE == action) { + LOGGER.info("buildRequestLogRecord:: Building log record for created action"); var created = getObjectProperty(requests, CREATED); return fetchItemDetails(new JsonObject().put(ITEM_ID.value(), getProperty(created, ITEM_ID))) @@ -93,7 +98,7 @@ private CompletableFuture> buildRequestLogRecord(JsonObject even })); } else if (LogRecord.Action.EDITED == action) { - + LOGGER.info("buildRequestLogRecord:: Building log record for edited action"); var original = getObjectProperty(requests, ORIGINAL); var updated = getObjectProperty(requests, UPDATED); @@ -117,6 +122,7 @@ private CompletableFuture> buildRequestLogRecord(JsonObject even })); } else if (LogRecord.Action.MOVED == action) { + LOGGER.info("buildRequestLogRecord:: Building log record for moved action"); var original = getObjectProperty(requests, ORIGINAL); var updated = getObjectProperty(requests, UPDATED); @@ -130,6 +136,7 @@ private CompletableFuture> buildRequestLogRecord(JsonObject even })); } else if (LogRecord.Action.QUEUE_POSITION_REORDERED == action) { + LOGGER.info("buildRequestLogRecord:: Building log record for Queue position reordered action"); var reordered = getArrayProperty(requests, REORDERED); @@ -153,6 +160,7 @@ private CompletableFuture> buildRequestLogRecord(JsonObject even .collect(Collectors.toList())); } else if (LogRecord.Action.EXPIRED == action) { + LOGGER.info("buildRequestLogRecord:: Building log record for expired action"); var original = getObjectProperty(requests, ORIGINAL); var updated = getObjectProperty(requests, UPDATED); @@ -168,11 +176,13 @@ private CompletableFuture> buildRequestLogRecord(JsonObject even })); } else { + LOGGER.warn("Action isn't determined or invalid"); throw new IllegalArgumentException("Action isn't determined or invalid"); } } private LogRecord buildBaseContent(JsonObject created, JsonObject item, JsonObject user) { + LOGGER.debug("buildBaseContent:: Building base content for LogRecord"); return new LogRecord().withObject(LogRecord.Object.REQUEST) .withUserBarcode(getProperty(user, USER_BARCODE)) .withServicePointId(getProperty(created, REQUEST_PICKUP_SERVICE_POINT_ID)) @@ -183,11 +193,13 @@ private LogRecord buildBaseContent(JsonObject created, JsonObject item, JsonObje } private LinkToIds buildLinkToIds(JsonObject created) { + LOGGER.debug("buildLinkToIds:: Building LinkToIds for LogRecord."); return new LinkToIds().withUserId(getProperty(created, REQUESTER_ID)) .withRequestId(getProperty(created, REQUEST_ID)); } private List buildItemData(JsonObject payload) { + LOGGER.debug("buildItemData:: Building item data for LogRecord."); return Collections.singletonList(new Item().withItemId(getProperty(payload, ITEM_ID)) .withItemBarcode(isNull(getNestedStringProperty(payload, ITEM, BARCODE)) ? getProperty(payload, ITEM_BARCODE) : getNestedStringProperty(payload, ITEM, BARCODE)) @@ -197,21 +209,30 @@ private List buildItemData(JsonObject payload) { } private LogRecord.Action resolveLogRecordAction(String logEventType) { + LOGGER.debug("resolveLogRecordAction:: Resolving LogRecord Action for event type : {}", logEventType); if (REQUEST_CREATED.equals(logEventType)) { + LOGGER.info("resolveLogRecordAction:: LogRecord Action created"); return LogRecord.Action.CREATED; } else if (REQUEST_CREATED_THROUGH_OVERRIDE.equals(logEventType)) { + LOGGER.info("resolveLogRecordAction:: LogRecord Action created through override"); return LogRecord.Action.CREATED_THROUGH_OVERRIDE; } else if (REQUEST_UPDATED.equals(logEventType)) { + LOGGER.info("resolveLogRecordAction:: LogRecord Action updated"); return LogRecord.Action.EDITED; } else if (REQUEST_MOVED.equals(logEventType)) { + LOGGER.info("resolveLogRecordAction:: LogRecord Action moved"); return LogRecord.Action.MOVED; } else if (REQUEST_REORDERED.equals(logEventType)) { + LOGGER.info("resolveLogRecordAction:: LogRecord Action reordered"); return LogRecord.Action.QUEUE_POSITION_REORDERED; } else if (REQUEST_CANCELLED.equals(logEventType)) { + LOGGER.info("resolveLogRecordAction:: LogRecord Action cancelled"); return LogRecord.Action.CANCELLED; } else if (REQUEST_EXPIRED.equals(logEventType)) { + LOGGER.info("resolveLogRecordAction:: LogRecord Action expired"); return LogRecord.Action.EXPIRED; } else { + LOGGER.warn("Builder isn't implemented yet for: {}", logEventType); throw new IllegalArgumentException("Builder isn't implemented yet for: " + logEventType); } } diff --git a/mod-audit-server/src/main/java/org/folio/dao/acquisition/PieceEventsDao.java b/mod-audit-server/src/main/java/org/folio/dao/acquisition/PieceEventsDao.java new file mode 100644 index 00000000..b145c059 --- /dev/null +++ b/mod-audit-server/src/main/java/org/folio/dao/acquisition/PieceEventsDao.java @@ -0,0 +1,46 @@ +package org.folio.dao.acquisition; + +import io.vertx.core.Future; +import io.vertx.sqlclient.Row; +import io.vertx.sqlclient.RowSet; +import org.folio.rest.jaxrs.model.PieceAuditEvent; +import org.folio.rest.jaxrs.model.PieceAuditEventCollection; + +public interface PieceEventsDao { + + /** + * Saves pieceAuditEvent entity to DB + * + * @param pieceAuditEvent pieceAuditEvent entity to save + * @param tenantId tenant id + * @return future with created row + */ + Future> save(PieceAuditEvent pieceAuditEvent, String tenantId); + + /** + * Searches for piece audit events by id + * + * @param pieceId piece id + * @param sortBy sort by + * @param sortOrder sort order + * @param limit limit + * @param offset offset + * @param tenantId tenant id + * @return future with PieceAuditEventCollection + */ + Future getAuditEventsByPieceId(String pieceId, String sortBy, String sortOrder, + int limit, int offset, String tenantId); + + /** + * Searches for piece audit events with status changes by piece id + * @param pieceId piece id + * @param sortBy sort by + * @param sortOrder sort order + * @param limit limit + * @param offset offset + * @param tenantId tenant id + * @return future with PieceAuditEventCollection + */ + Future getAuditEventsWithStatusChangesByPieceId(String pieceId, String sortBy, String sortOrder, + int limit, int offset, String tenantId); +} diff --git a/mod-audit-server/src/main/java/org/folio/dao/acquisition/impl/OrderEventsDaoImpl.java b/mod-audit-server/src/main/java/org/folio/dao/acquisition/impl/OrderEventsDaoImpl.java index 524494f9..d9ee92e4 100644 --- a/mod-audit-server/src/main/java/org/folio/dao/acquisition/impl/OrderEventsDaoImpl.java +++ b/mod-audit-server/src/main/java/org/folio/dao/acquisition/impl/OrderEventsDaoImpl.java @@ -1,5 +1,23 @@ package org.folio.dao.acquisition.impl; +import static java.lang.String.format; +import static org.folio.util.AuditEventDBConstants.ACTION_DATE_FIELD; +import static org.folio.util.AuditEventDBConstants.ACTION_FIELD; +import static org.folio.util.AuditEventDBConstants.EVENT_DATE_FIELD; +import static org.folio.util.AuditEventDBConstants.ID_FIELD; +import static org.folio.util.AuditEventDBConstants.MODIFIED_CONTENT_FIELD; +import static org.folio.util.AuditEventDBConstants.ORDER_BY_PATTERN; +import static org.folio.util.AuditEventDBConstants.ORDER_ID_FIELD; +import static org.folio.util.AuditEventDBConstants.TOTAL_RECORDS_FIELD; +import static org.folio.util.AuditEventDBConstants.USER_ID_FIELD; +import static org.folio.util.DbUtils.formatDBTableName; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.util.Date; +import java.util.UUID; + import io.vertx.core.Future; import io.vertx.core.Promise; import io.vertx.core.json.JsonObject; @@ -12,18 +30,8 @@ import org.folio.rest.jaxrs.model.OrderAuditEvent; import org.folio.rest.jaxrs.model.OrderAuditEventCollection; import org.folio.util.PostgresClientFactory; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Repository; -import java.time.LocalDateTime; -import java.time.ZoneOffset; -import java.util.Date; -import java.util.UUID; - -import static java.lang.String.format; -import static org.folio.rest.persist.PostgresClient.convertToPsqlStandard; -import static org.folio.util.OrderAuditEventDBConstants.*; - @Repository public class OrderEventsDaoImpl implements OrderEventsDao { @@ -37,51 +45,53 @@ public class OrderEventsDaoImpl implements OrderEventsDao { public static final String INSERT_SQL = "INSERT INTO %s (id, action, order_id, user_id, event_date, action_date, modified_content_snapshot)" + " VALUES ($1, $2, $3, $4, $5, $6, $7)"; - @Autowired private final PostgresClientFactory pgClientFactory; - @Autowired public OrderEventsDaoImpl(PostgresClientFactory pgClientFactory) { this.pgClientFactory = pgClientFactory; } @Override public Future> save(OrderAuditEvent orderAuditEvent, String tenantId) { + LOGGER.debug("save:: Saving Order AuditEvent with tenant id : {}", tenantId); Promise> promise = Promise.promise(); + LOGGER.debug("formatDBTableName:: Formatting DB Table Name with tenant id : {}", tenantId); String logTable = formatDBTableName(tenantId, TABLE_NAME); - String query = format(INSERT_SQL, logTable); - makeSaveCall(promise, query, orderAuditEvent, tenantId); - + LOGGER.info("save:: Saved Order AuditEvent with tenant id : {}", tenantId); return promise.future(); } @Override public Future getAuditEventsByOrderId(String orderId, String sortBy, String sortOrder, int limit, int offset, String tenantId) { + LOGGER.debug("getAuditEventsByOrderId:: Retrieving AuditEvent with order id : {}", orderId); Promise> promise = Promise.promise(); try { + LOGGER.debug("formatDBTableName:: Formatting DB Table Name with tenant id : {}", tenantId); String logTable = formatDBTableName(tenantId, TABLE_NAME); String query = format(GET_BY_ORDER_ID_SQL, logTable, logTable, format(ORDER_BY_PATTERN, sortBy, sortOrder)); Tuple queryParams = Tuple.of(UUID.fromString(orderId), limit, offset); pgClientFactory.createInstance(tenantId).selectRead(query, queryParams, promise); } catch (Exception e) { - LOGGER.error("Error getting order audit events by order id: {}", orderId, e); + LOGGER.warn("Error getting order audit events by order id: {}", orderId, e); promise.fail(e); } - + LOGGER.info("getAuditEventsByOrderId:: Retrieved AuditEvent with order id : {}", orderId); return promise.future().map(rowSet -> rowSet.rowCount() == 0 ? new OrderAuditEventCollection().withTotalItems(0) : mapRowToListOfOrderEvent(rowSet)); } private void makeSaveCall(Promise> promise, String query, OrderAuditEvent orderAuditEvent, String tenantId) { + LOGGER.debug("makeSaveCall:: Making save call with query : {} and tenant id : {}", query, tenantId); try { + LOGGER.info("makeSaveCall:: Trying to make save call with query : {} and tenant id : {}", query, tenantId); pgClientFactory.createInstance(tenantId).execute(query, Tuple.of(orderAuditEvent.getId(), orderAuditEvent.getAction(), orderAuditEvent.getOrderId(), orderAuditEvent.getUserId(), - LocalDateTime.ofInstant(orderAuditEvent.getEventDate().toInstant(), ZoneOffset.UTC), - LocalDateTime.ofInstant(orderAuditEvent.getActionDate().toInstant(), ZoneOffset.UTC), + LocalDateTime.ofInstant(orderAuditEvent.getEventDate().toInstant(), ZoneId.systemDefault()), + LocalDateTime.ofInstant(orderAuditEvent.getActionDate().toInstant(), ZoneId.systemDefault()), JsonObject.mapFrom(orderAuditEvent.getOrderSnapshot())), promise); } catch (Exception e) { LOGGER.error("Failed to save record with id: {} for order id: {} in to table {}", @@ -91,16 +101,19 @@ private void makeSaveCall(Promise> promise, String query, OrderAudit } private OrderAuditEventCollection mapRowToListOfOrderEvent(RowSet rowSet) { + LOGGER.debug("mapRowToListOfOrderEvent:: Mapping row to List of Order Events"); OrderAuditEventCollection orderAuditEventCollection = new OrderAuditEventCollection(); rowSet.iterator().forEachRemaining(row -> { orderAuditEventCollection.getOrderAuditEvents().add(mapRowToOrderEvent(row)); orderAuditEventCollection.setTotalItems(row.getInteger(TOTAL_RECORDS_FIELD)); }); + LOGGER.debug("mapRowToListOfOrderEvent:: Mapped row to List of Order Events"); return orderAuditEventCollection; } private OrderAuditEvent mapRowToOrderEvent(Row row) { + LOGGER.debug("mapRowToOrderEvent:: Mapping row to Order Event"); return new OrderAuditEvent() .withId(row.getValue(ID_FIELD).toString()) .withAction(row.get(OrderAuditEvent.Action.class, ACTION_FIELD)) @@ -111,7 +124,4 @@ private OrderAuditEvent mapRowToOrderEvent(Row row) { .withOrderSnapshot(JsonObject.mapFrom(row.getValue(MODIFIED_CONTENT_FIELD))); } - private String formatDBTableName(String tenantId, String table) { - return format("%s.%s", convertToPsqlStandard(tenantId), table); - } } diff --git a/mod-audit-server/src/main/java/org/folio/dao/acquisition/impl/OrderLineEventsDaoImpl.java b/mod-audit-server/src/main/java/org/folio/dao/acquisition/impl/OrderLineEventsDaoImpl.java index 4ec95f4d..8909874b 100644 --- a/mod-audit-server/src/main/java/org/folio/dao/acquisition/impl/OrderLineEventsDaoImpl.java +++ b/mod-audit-server/src/main/java/org/folio/dao/acquisition/impl/OrderLineEventsDaoImpl.java @@ -1,5 +1,24 @@ package org.folio.dao.acquisition.impl; +import static java.lang.String.format; +import static org.folio.util.AuditEventDBConstants.ACTION_DATE_FIELD; +import static org.folio.util.AuditEventDBConstants.ACTION_FIELD; +import static org.folio.util.AuditEventDBConstants.EVENT_DATE_FIELD; +import static org.folio.util.AuditEventDBConstants.ID_FIELD; +import static org.folio.util.AuditEventDBConstants.MODIFIED_CONTENT_FIELD; +import static org.folio.util.AuditEventDBConstants.ORDER_BY_PATTERN; +import static org.folio.util.AuditEventDBConstants.ORDER_ID_FIELD; +import static org.folio.util.AuditEventDBConstants.ORDER_LINE_ID_FIELD; +import static org.folio.util.AuditEventDBConstants.TOTAL_RECORDS_FIELD; +import static org.folio.util.AuditEventDBConstants.USER_ID_FIELD; +import static org.folio.util.DbUtils.formatDBTableName; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.util.Date; +import java.util.UUID; + import io.vertx.core.Future; import io.vertx.core.Promise; import io.vertx.core.json.JsonObject; @@ -12,18 +31,8 @@ import org.folio.rest.jaxrs.model.OrderLineAuditEvent; import org.folio.rest.jaxrs.model.OrderLineAuditEventCollection; import org.folio.util.PostgresClientFactory; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Repository; -import java.time.LocalDateTime; -import java.time.ZoneOffset; -import java.util.Date; -import java.util.UUID; - -import static java.lang.String.format; -import static org.folio.rest.persist.PostgresClient.convertToPsqlStandard; -import static org.folio.util.OrderAuditEventDBConstants.*; - @Repository public class OrderLineEventsDaoImpl implements OrderLineEventsDao { @@ -38,52 +47,54 @@ public class OrderLineEventsDaoImpl implements OrderLineEventsDao { private static final String INSERT_SQL = "INSERT INTO %s (id, action, order_id, order_line_id, user_id, event_date, action_date, modified_content_snapshot) " + "VALUES ($1, $2, $3, $4, $5, $6, $7, $8)"; - @Autowired private final PostgresClientFactory pgClientFactory; - @Autowired public OrderLineEventsDaoImpl(PostgresClientFactory pgClientFactory) { this.pgClientFactory = pgClientFactory; } @Override public Future> save(OrderLineAuditEvent orderLineAuditEvent, String tenantId) { + LOGGER.debug("save:: Saving OrderLine AuditEvent with tenant id : {}", tenantId); Promise> promise = Promise.promise(); + LOGGER.debug("formatDBTableName:: Formatting DB Table Name with tenant id : {}", tenantId); String logTable = formatDBTableName(tenantId, TABLE_NAME); - String query = format(INSERT_SQL, logTable); - makeSaveCall(promise, query, orderLineAuditEvent, tenantId); - + LOGGER.info("save:: Saved OrderLine AuditEvent with tenant id : {}", tenantId); return promise.future(); } @Override public Future getAuditEventsByOrderLineId(String orderLineId, String sortBy, String sortOrder, int limit, int offset, String tenantId) { + LOGGER.debug("getAuditEventsByOrderLineId:: Retrieving AuditEvent with order line id : {}", orderLineId); Promise> promise = Promise.promise(); try { + LOGGER.debug("formatDBTableName:: Formatting DB Table Name with tenant id : {}", tenantId); String logTable = formatDBTableName(tenantId, TABLE_NAME); String query = format(GET_BY_ORDER_LINE_ID_SQL, logTable, logTable, format(ORDER_BY_PATTERN, sortBy, sortOrder)); Tuple queryParams = Tuple.of(UUID.fromString(orderLineId), limit, offset); pgClientFactory.createInstance(tenantId).selectRead(query, queryParams, promise); } catch (Exception e) { - LOGGER.error("Error getting order line audit events by order line id: {}", orderLineId, e); + LOGGER.warn("Error getting order line audit events by order line id: {}", orderLineId, e); promise.fail(e); } + LOGGER.info("getAuditEventsByOrderLineId:: Retrieved AuditEvent with order line id : {}", orderLineId); return promise.future().map(rowSet -> rowSet.rowCount() == 0 ? new OrderLineAuditEventCollection().withTotalItems(0) : mapRowToListOfOrderLineEvent(rowSet)); } private void makeSaveCall(Promise> promise, String query, OrderLineAuditEvent orderLineAuditEvent, String tenantId) { + LOGGER.debug("makeSaveCall:: Making save call with query : {} and tenant id : {}", query, tenantId); try { pgClientFactory.createInstance(tenantId).execute(query, Tuple.of(orderLineAuditEvent.getId(), orderLineAuditEvent.getAction(), orderLineAuditEvent.getOrderId(), orderLineAuditEvent.getOrderLineId(), orderLineAuditEvent.getUserId(), - LocalDateTime.ofInstant(orderLineAuditEvent.getEventDate().toInstant(), ZoneOffset.UTC), - LocalDateTime.ofInstant(orderLineAuditEvent.getActionDate().toInstant(), ZoneOffset.UTC), + LocalDateTime.ofInstant(orderLineAuditEvent.getEventDate().toInstant(), ZoneId.systemDefault()), + LocalDateTime.ofInstant(orderLineAuditEvent.getActionDate().toInstant(), ZoneId.systemDefault()), JsonObject.mapFrom(orderLineAuditEvent.getOrderLineSnapshot())), promise); } catch (Exception e) { LOGGER.error("Failed to save record with id: {} for order line id: {} in to table {}", @@ -93,16 +104,19 @@ private void makeSaveCall(Promise> promise, String query, OrderLineA } private OrderLineAuditEventCollection mapRowToListOfOrderLineEvent(RowSet rowSet) { + LOGGER.debug("mapRowToListOfOrderLineEvent:: Mapping row to List of Order Line Events"); OrderLineAuditEventCollection orderLineAuditEventCollection = new OrderLineAuditEventCollection(); rowSet.iterator().forEachRemaining(row -> { orderLineAuditEventCollection.getOrderLineAuditEvents().add(mapRowToOrderLineEvent(row)); orderLineAuditEventCollection.setTotalItems(row.getInteger(TOTAL_RECORDS_FIELD)); }); + LOGGER.debug("mapRowToListOfOrderLineEvent:: Mapped row to List of Order Line Events"); return orderLineAuditEventCollection; } private OrderLineAuditEvent mapRowToOrderLineEvent(Row row) { + LOGGER.debug("mapRowToOrderLineEvent:: Mapping row to Order Line Event"); return new OrderLineAuditEvent() .withId(row.getValue(ID_FIELD).toString()) .withAction(row.get(OrderLineAuditEvent.Action.class, ACTION_FIELD)) @@ -113,8 +127,4 @@ private OrderLineAuditEvent mapRowToOrderLineEvent(Row row) { .withActionDate(Date.from(row.getLocalDateTime(ACTION_DATE_FIELD).toInstant(ZoneOffset.UTC))) .withOrderLineSnapshot(JsonObject.mapFrom(row.getValue(MODIFIED_CONTENT_FIELD))); } - - private String formatDBTableName(String tenantId, String table) { - return format("%s.%s", convertToPsqlStandard(tenantId), table); - } } diff --git a/mod-audit-server/src/main/java/org/folio/dao/acquisition/impl/PieceEventsDaoImpl.java b/mod-audit-server/src/main/java/org/folio/dao/acquisition/impl/PieceEventsDaoImpl.java new file mode 100644 index 00000000..09206956 --- /dev/null +++ b/mod-audit-server/src/main/java/org/folio/dao/acquisition/impl/PieceEventsDaoImpl.java @@ -0,0 +1,157 @@ +package org.folio.dao.acquisition.impl; + +import static java.lang.String.format; +import static org.folio.util.AuditEventDBConstants.ACTION_DATE_FIELD; +import static org.folio.util.AuditEventDBConstants.ACTION_FIELD; +import static org.folio.util.AuditEventDBConstants.EVENT_DATE_FIELD; +import static org.folio.util.AuditEventDBConstants.ID_FIELD; +import static org.folio.util.AuditEventDBConstants.MODIFIED_CONTENT_FIELD; +import static org.folio.util.AuditEventDBConstants.ORDER_BY_PATTERN; +import static org.folio.util.AuditEventDBConstants.PIECE_ID_FIELD; +import static org.folio.util.AuditEventDBConstants.TOTAL_RECORDS_FIELD; +import static org.folio.util.AuditEventDBConstants.USER_ID_FIELD; +import static org.folio.util.DbUtils.formatDBTableName; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.util.Date; +import java.util.UUID; + +import io.vertx.core.Future; +import io.vertx.core.Promise; +import io.vertx.core.json.JsonObject; +import io.vertx.sqlclient.Row; +import io.vertx.sqlclient.RowSet; +import io.vertx.sqlclient.Tuple; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.folio.dao.acquisition.PieceEventsDao; +import org.folio.rest.jaxrs.model.PieceAuditEvent; +import org.folio.rest.jaxrs.model.PieceAuditEventCollection; +import org.folio.util.PostgresClientFactory; +import org.springframework.stereotype.Repository; + +@Repository +public class PieceEventsDaoImpl implements PieceEventsDao { + private static final Logger LOGGER = LogManager.getLogger(); + private static final String TABLE_NAME = "acquisition_piece_log"; + private static final String GET_BY_PIECE_ID_SQL = "SELECT id, action, piece_id, user_id, event_date, action_date, modified_content_snapshot, " + + " (SELECT count(*) AS total_records FROM %s WHERE piece_id = $1) FROM %s WHERE piece_id = $1 %s LIMIT $2 OFFSET $3"; + private static final String GET_STATUS_CHANGE_HISTORY_BY_PIECE_ID_SQL = """ + WITH StatusChanges AS ( + SELECT id, action, piece_id, user_id, event_date, action_date, modified_content_snapshot, + LAG(modified_content_snapshot ->> 'receivingStatus') OVER (PARTITION BY piece_id ORDER BY action_date) AS previous_status + FROM %s WHERE piece_id=$1 + ) + SELECT id, action, piece_id, user_id, event_date, action_date, modified_content_snapshot, + (SELECT COUNT(*) AS total_records FROM StatusChanges + WHERE modified_content_snapshot ->> 'receivingStatus' <> COALESCE(previous_status, '')) + FROM StatusChanges WHERE modified_content_snapshot ->> 'receivingStatus' <> COALESCE(previous_status, '') + %s LIMIT $2 OFFSET $3 + """; + + private static final String INSERT_SQL = "INSERT INTO %s (id, action, piece_id, user_id, event_date, action_date, modified_content_snapshot)" + + " VALUES ($1, $2, $3, $4, $5, $6, $7)"; + + private final PostgresClientFactory pgClientFactory; + + public PieceEventsDaoImpl(PostgresClientFactory pgClientFactory) { + this.pgClientFactory = pgClientFactory; + } + + @Override + public Future> save(PieceAuditEvent pieceAuditEvent, String tenantId) { + LOGGER.debug("save:: Trying to save Piece AuditEvent with tenant id : {}", tenantId); + Promise> promise = Promise.promise(); + + LOGGER.debug("formatDBTableName:: Formatting DB Table Name with tenant id : {}", tenantId); + String logTable = formatDBTableName(tenantId, TABLE_NAME); + String query = format(INSERT_SQL, logTable); + + makeSaveCall(promise, query, pieceAuditEvent, tenantId); + LOGGER.info("save:: Saved Piece AuditEvent for pieceId={} in tenant id={}", pieceAuditEvent.getPieceId(), tenantId); + return promise.future(); + } + + @Override + public Future getAuditEventsByPieceId(String pieceId, String sortBy, String sortOrder, int limit, int offset, String tenantId) { + LOGGER.debug("getAuditEventsByOrderId:: Trying to retrieve AuditEvent with piece id : {}", pieceId); + Promise> promise = Promise.promise(); + try { + LOGGER.debug("formatDBTableName:: Formatting DB Table Name with tenant id : {}", tenantId); + String logTable = formatDBTableName(tenantId, TABLE_NAME); + String query = format(GET_BY_PIECE_ID_SQL, logTable, logTable, format(ORDER_BY_PATTERN, sortBy, sortOrder)); + Tuple queryParams = Tuple.of(UUID.fromString(pieceId), limit, offset); + pgClientFactory.createInstance(tenantId).selectRead(query, queryParams, promise); + } catch (Exception e) { + LOGGER.warn("Error getting piece audit events by piece id: {}", pieceId, e); + promise.fail(e); + } + LOGGER.info("getAuditEventsByOrderId:: Retrieved AuditEvent with piece id : {}", pieceId); + return promise.future().map(rowSet -> rowSet.rowCount() == 0 ? + new PieceAuditEventCollection().withTotalItems(0) : + mapRowToListOfPieceEvent(rowSet)); + } + + @Override + public Future getAuditEventsWithStatusChangesByPieceId(String pieceId, String sortBy, String sortOrder, int limit, int offset, String tenantId) { + LOGGER.debug("getAuditEventsByOrderId:: Retrieving AuditEvent with piece id : {}", pieceId); + Promise> promise = Promise.promise(); + try { + LOGGER.debug("formatDBTableName:: Formatting DB Table Name with tenant id : {}", tenantId); + String logTable = formatDBTableName(tenantId, TABLE_NAME); + String query = format(GET_STATUS_CHANGE_HISTORY_BY_PIECE_ID_SQL, logTable, format(ORDER_BY_PATTERN, sortBy, sortOrder)); + Tuple queryParams = Tuple.of(UUID.fromString(pieceId), limit, offset); + pgClientFactory.createInstance(tenantId).selectRead(query, queryParams, promise); + } catch (Exception e) { + LOGGER.warn("Error getting order audit events by piece id: {}", pieceId, e); + promise.fail(e); + } + LOGGER.info("getAuditEventsByOrderId:: Retrieved AuditEvent with piece id: {}", pieceId); + return promise.future().map(rowSet -> rowSet.rowCount() == 0 ? new PieceAuditEventCollection().withTotalItems(0) + : mapRowToListOfPieceEvent(rowSet)); + } + + private PieceAuditEventCollection mapRowToListOfPieceEvent(RowSet rowSet) { + LOGGER.debug("mapRowToListOfOrderEvent:: Mapping row to List of Piece Events"); + PieceAuditEventCollection pieceAuditEventCollection = new PieceAuditEventCollection(); + rowSet.iterator().forEachRemaining(row -> { + pieceAuditEventCollection.getPieceAuditEvents().add(mapRowToPieceEvent(row)); + pieceAuditEventCollection.setTotalItems(row.getInteger(TOTAL_RECORDS_FIELD)); + }); + LOGGER.debug("mapRowToListOfOrderEvent:: Mapped row to List of Piece Events"); + return pieceAuditEventCollection; + } + + private PieceAuditEvent mapRowToPieceEvent(Row row) { + LOGGER.debug("mapRowToPieceEvent:: Mapping row to Order Event"); + return new PieceAuditEvent() + .withId(row.getValue(ID_FIELD).toString()) + .withAction(row.get(PieceAuditEvent.Action.class, ACTION_FIELD)) + .withPieceId(row.getValue(PIECE_ID_FIELD).toString()) + .withUserId(row.getValue(USER_ID_FIELD).toString()) + .withEventDate(Date.from(row.getLocalDateTime(EVENT_DATE_FIELD).toInstant(ZoneOffset.UTC))) + .withActionDate(Date.from(row.getLocalDateTime(ACTION_DATE_FIELD).toInstant(ZoneOffset.UTC))) + .withPieceSnapshot(JsonObject.mapFrom(row.getValue(MODIFIED_CONTENT_FIELD))); + } + + private void makeSaveCall(Promise> promise, String query, PieceAuditEvent pieceAuditEvent, String tenantId) { + LOGGER.debug("makeSaveCall:: Making save call with query : {} and tenant id : {}", query, tenantId); + try { + pgClientFactory.createInstance(tenantId).execute(query, Tuple.of(pieceAuditEvent.getId(), + pieceAuditEvent.getAction(), + pieceAuditEvent.getPieceId(), + pieceAuditEvent.getUserId(), + LocalDateTime.ofInstant(pieceAuditEvent.getEventDate().toInstant(), ZoneId.systemDefault()), + LocalDateTime.ofInstant(pieceAuditEvent.getActionDate().toInstant(), ZoneId.systemDefault()), + JsonObject.mapFrom(pieceAuditEvent.getPieceSnapshot())), + promise); + } catch (Exception e) { + LOGGER.error("Failed to save record with id: {} for order id: {} in to table {}", + pieceAuditEvent.getId(), pieceAuditEvent.getPieceId(), TABLE_NAME, e); + promise.fail(e); + } + } + +} diff --git a/mod-audit-server/src/main/java/org/folio/rest/impl/AuditDataAcquisitionImpl.java b/mod-audit-server/src/main/java/org/folio/rest/impl/AuditDataAcquisitionImpl.java index e7f782ae..f405be20 100644 --- a/mod-audit-server/src/main/java/org/folio/rest/impl/AuditDataAcquisitionImpl.java +++ b/mod-audit-server/src/main/java/org/folio/rest/impl/AuditDataAcquisitionImpl.java @@ -9,10 +9,13 @@ import org.apache.logging.log4j.Logger; import org.folio.rest.jaxrs.model.AuditDataAcquisitionOrderIdGetSortOrder; import org.folio.rest.jaxrs.model.AuditDataAcquisitionOrderLineIdGetSortOrder; +import org.folio.rest.jaxrs.model.AuditDataAcquisitionPieceIdGetSortOrder; +import org.folio.rest.jaxrs.model.AuditDataAcquisitionPieceIdStatusChangeHistoryGetSortOrder; import org.folio.rest.jaxrs.resource.AuditDataAcquisition; import org.folio.rest.tools.utils.TenantTool; import org.folio.services.acquisition.OrderAuditEventsService; import org.folio.services.acquisition.OrderLineAuditEventsService; +import org.folio.services.acquisition.PieceAuditEventsService; import org.folio.spring.SpringContextUtil; import org.folio.util.ErrorUtils; import org.springframework.beans.factory.annotation.Autowired; @@ -28,9 +31,10 @@ public class AuditDataAcquisitionImpl implements AuditDataAcquisition { @Autowired private OrderAuditEventsService orderAuditEventsService; - @Autowired private OrderLineAuditEventsService orderLineAuditEventsService; + @Autowired + private PieceAuditEventsService pieceAuditEventsService; public AuditDataAcquisitionImpl() { SpringContextUtil.autowireDependencies(this, Vertx.currentContext()); @@ -39,46 +43,77 @@ public AuditDataAcquisitionImpl() { @Override public void getAuditDataAcquisitionOrderById(String orderId, String sortBy, AuditDataAcquisitionOrderIdGetSortOrder sortOrder, int limit, int offset, Map okapiHeaders, Handler> asyncResultHandler, Context vertxContext) { + LOGGER.debug("getAuditDataAcquisitionOrderById:: Retrieving Audit Data Acquisition Order By Id : {}", orderId); String tenantId = TenantTool.tenantId(okapiHeaders); - - vertxContext.runOnContext(c -> { - try { - orderAuditEventsService.getAuditEventsByOrderId(orderId, sortBy, sortOrder.name(), limit, offset, tenantId) - .map(GetAuditDataAcquisitionOrderByIdResponse::respond200WithApplicationJson) - .map(Response.class::cast) - .otherwise(this::mapExceptionToResponse) - .onComplete(asyncResultHandler); - } catch (Exception e) { - LOGGER.error("Failed to get order audit events by order id: {}", orderId, e); - asyncResultHandler.handle(Future.succeededFuture(mapExceptionToResponse(e))); - } - }); + try { + orderAuditEventsService.getAuditEventsByOrderId(orderId, sortBy, sortOrder.name(), limit, offset, tenantId) + .map(GetAuditDataAcquisitionOrderByIdResponse::respond200WithApplicationJson) + .map(Response.class::cast) + .otherwise(this::mapExceptionToResponse) + .onComplete(asyncResultHandler); + } catch (Exception e) { + LOGGER.error("Failed to get order audit events by order id: {}", orderId, e); + asyncResultHandler.handle(Future.succeededFuture(mapExceptionToResponse(e))); + } } @Override public void getAuditDataAcquisitionOrderLineById(String orderLineId, String sortBy, AuditDataAcquisitionOrderLineIdGetSortOrder sortOrder, int limit, int offset, Map okapiHeaders, Handler> asyncResultHandler, Context vertxContext) { + LOGGER.debug("getAuditDataAcquisitionOrderLineById:: Retrieving Audit Data Acquisition Order Line By Id : {}", orderLineId); String tenantId = TenantTool.tenantId(okapiHeaders); + try { + orderLineAuditEventsService.getAuditEventsByOrderLineId(orderLineId, sortBy, sortOrder.name(), limit, offset, tenantId) + .map(GetAuditDataAcquisitionOrderLineByIdResponse::respond200WithApplicationJson) + .map(Response.class::cast) + .otherwise(this::mapExceptionToResponse) + .onComplete(asyncResultHandler); + } catch (Exception e) { + LOGGER.error("Failed to get order line audit events by order line id: {}", orderLineId, e); + asyncResultHandler.handle(Future.succeededFuture(mapExceptionToResponse(e))); + } - vertxContext.runOnContext(c -> { - try { - orderLineAuditEventsService.getAuditEventsByOrderLineId(orderLineId, sortBy, sortOrder.name(), limit, offset, tenantId) - .map(GetAuditDataAcquisitionOrderLineByIdResponse::respond200WithApplicationJson) - .map(Response.class::cast) - .otherwise(this::mapExceptionToResponse) - .onComplete(asyncResultHandler); - } catch (Exception e) { - LOGGER.error("Failed to get order line audit events by order line id: {}", orderLineId, e); - asyncResultHandler.handle(Future.succeededFuture(mapExceptionToResponse(e))); - } - }); + } + + @Override + public void getAuditDataAcquisitionPieceById(String pieceId, String sortBy, AuditDataAcquisitionPieceIdGetSortOrder sortOrder, + int limit, int offset, Map okapiHeaders, Handler> asyncResultHandler, Context vertxContext) { + LOGGER.debug("getAuditDataAcquisitionOrderById:: Retrieving Audit Data Acquisition Piece By Id : {}", pieceId); + String tenantId = TenantTool.tenantId(okapiHeaders); + try { + pieceAuditEventsService.getAuditEventsByPieceId(pieceId, sortBy, sortOrder.name(), limit, offset, tenantId) + .map(GetAuditDataAcquisitionPieceByIdResponse::respond200WithApplicationJson) + .map(Response.class::cast) + .otherwise(this::mapExceptionToResponse) + .onComplete(asyncResultHandler); + } catch (Exception e) { + LOGGER.error("Failed to get piece audit events by piece id: {}", pieceId, e); + asyncResultHandler.handle(Future.succeededFuture(mapExceptionToResponse(e))); + } + } + + @Override + public void getAuditDataAcquisitionPieceStatusChangeHistoryById(String pieceId, String sortBy, + AuditDataAcquisitionPieceIdStatusChangeHistoryGetSortOrder sortOrder, + int limit, int offset, Map okapiHeaders, Handler> asyncResultHandler, Context vertxContext) { + LOGGER.debug("getAuditDataAcquisitionOrderById:: Retrieving Audit Data Acquisition Piece with status changes By Id : {}", pieceId); + String tenantId = TenantTool.tenantId(okapiHeaders); + try { + pieceAuditEventsService.getAuditEventsWithStatusChangesByPieceId(pieceId, sortBy, sortOrder.name(), limit, offset, tenantId) + .map(GetAuditDataAcquisitionPieceByIdResponse::respond200WithApplicationJson) + .map(Response.class::cast) + .otherwise(this::mapExceptionToResponse) + .onComplete(asyncResultHandler); + } catch (Exception e) { + LOGGER.error("Failed to get piece audit events with unique status change by piece id: {}", pieceId, e); + asyncResultHandler.handle(Future.succeededFuture(mapExceptionToResponse(e))); + } } private Response mapExceptionToResponse(Throwable throwable) { - LOGGER.error(throwable.getMessage(), throwable); + LOGGER.debug("mapExceptionToResponse:: Mapping Exception :{} to Response", throwable.getMessage(), throwable); return GetAuditDataAcquisitionOrderByIdResponse - .respond500WithApplicationJson(ErrorUtils.buildErrors(GENERIC_ERROR_CODE.getCode(), throwable)); + .respond500WithApplicationJson(ErrorUtils.buildErrors(GENERIC_ERROR_CODE.getCode(), throwable)); } - } diff --git a/mod-audit-server/src/main/java/org/folio/rest/impl/AuditDataImpl.java b/mod-audit-server/src/main/java/org/folio/rest/impl/AuditDataImpl.java index e5832c65..9063a49c 100644 --- a/mod-audit-server/src/main/java/org/folio/rest/impl/AuditDataImpl.java +++ b/mod-audit-server/src/main/java/org/folio/rest/impl/AuditDataImpl.java @@ -3,6 +3,8 @@ import java.util.Map; import javax.ws.rs.core.Response; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.folio.rest.annotations.Validate; import org.folio.rest.jaxrs.model.Audit; import org.folio.rest.jaxrs.model.AuditCollection; @@ -14,6 +16,8 @@ public class AuditDataImpl implements AuditData { + private static final Logger LOGGER = LogManager.getLogger(); + protected static final String API_CXT = "/audit-data"; protected static final String DB_TAB_AUDIT = "audit_data"; @@ -22,6 +26,7 @@ public class AuditDataImpl implements AuditData { public void getAuditData(String query, int offset, int limit, String lang, Map okapiHeaders, Handler> asyncResultHandler, Context vertxContext) { + LOGGER.debug("getAuditData:: Retrieving audit data with query: {}", query); PgUtil.get(DB_TAB_AUDIT, Audit.class, AuditCollection.class, query, offset, limit, okapiHeaders, vertxContext, GetAuditDataResponse.class, asyncResultHandler); } @@ -31,6 +36,7 @@ public void getAuditData(String query, int offset, int limit, String lang, Map okapiHeaders, Handler> asyncResultHandler, Context vertxContext) { + LOGGER.debug("postAuditData:: Creating new audit data with id: {}", audit.getId()); PgUtil.post(DB_TAB_AUDIT, audit, okapiHeaders, vertxContext, PostAuditDataResponse.class, asyncResultHandler); } @@ -39,6 +45,7 @@ public void postAuditData(String lang, Audit audit, Map okapiHea public void getAuditDataById(String id, String lang, Map okapiHeaders, Handler> asyncResultHandler, Context vertxContext) { + LOGGER.debug("getAuditDataById:: Retrieving audit data by id: {}", id); PgUtil.getById(DB_TAB_AUDIT, Audit.class, id, okapiHeaders, vertxContext, GetAuditDataByIdResponse.class, asyncResultHandler); } @@ -47,6 +54,7 @@ public void getAuditDataById(String id, String lang, Map okapiHe public void putAuditDataById(String id, String lang, Audit audit, Map okapiHeaders, Handler> asyncResultHandler, Context vertxContext) { + LOGGER.debug("putAuditDataById:: Updating audit data by id: {}", id); PgUtil.put(DB_TAB_AUDIT, audit, id, okapiHeaders, vertxContext, PutAuditDataByIdResponse.class, asyncResultHandler); } @@ -55,6 +63,7 @@ public void putAuditDataById(String id, String lang, Audit audit, Map okapiHeaders, Handler> asyncResultHandler, Context vertxContext) { + LOGGER.debug("deleteAuditDataById:: Deleting audit data by id: {}", id); PgUtil.deleteById(DB_TAB_AUDIT, id, okapiHeaders, vertxContext, DeleteAuditDataByIdResponse.class, asyncResultHandler); } } diff --git a/mod-audit-server/src/main/java/org/folio/rest/impl/AuditHandlersService.java b/mod-audit-server/src/main/java/org/folio/rest/impl/AuditHandlersService.java index 8ea8867e..afd97d7a 100644 --- a/mod-audit-server/src/main/java/org/folio/rest/impl/AuditHandlersService.java +++ b/mod-audit-server/src/main/java/org/folio/rest/impl/AuditHandlersService.java @@ -33,18 +33,20 @@ public class AuditHandlersService extends BaseService implements AuditHandlers { @Validate public void postAuditHandlersLogRecord(String entity, Map okapiHeaders, Handler> asyncResultHandler, Context vertxContext) { + LOGGER.debug("postAuditHandlersLogRecord:: Trying to Save AuditHandlersLogRecord request with entity: {}", entity); try { + LOGGER.info("postAuditHandlersLogRecord:: Saving AuditHandlersLogRecord request with entity: {}", entity); JsonObject payload = new JsonObject(entity); LogRecordBuilder builder = LogRecordBuilderResolver.getBuilder(payload.getString(LOG_EVENT_TYPE.value()), okapiHeaders, vertxContext); builder.buildLogRecord(payload) .thenCompose(logRecords -> processAnonymize(logRecords, okapiHeaders, vertxContext)) .thenCompose(logRecords -> saveLogRecords(logRecords, okapiHeaders, vertxContext)) .exceptionally(throwable -> { - LOGGER.error("Error saving log event: " + entity, throwable.getLocalizedMessage()); + LOGGER.warn("Error saving log event : {} due to : {}", entity, throwable.getLocalizedMessage()); return null; }); } catch (Exception e) { - LOGGER.error("Error saving log event for entity {} due to {} ", entity, e.getMessage()); + LOGGER.warn("Error saving log event for entity {} due to {} ", entity, e.getMessage()); } finally { asyncResultHandler.handle(succeededFuture(PostAuditHandlersLogRecordResponse.respond204())); } @@ -52,18 +54,21 @@ public void postAuditHandlersLogRecord(String entity, Map okapiH private CompletableFuture> processAnonymize(List records, Map okapiHeaders, Context vertxContext) { + LOGGER.debug("processAnonymize:: Processing anonymize for records"); return isAnonymize(records) ? anonymizeLoanRelatedRecords(records, okapiHeaders, vertxContext) : CompletableFuture.completedFuture(records); } private boolean isAnonymize(List records) { + LOGGER.debug("isAnonymize:: Checking if anonymize is required for records"); return records.stream() .anyMatch(logRecord -> LogRecord.Action.ANONYMIZE == logRecord.getAction()); } private CompletableFuture> anonymizeLoanRelatedRecords(List records, Map okapiHeaders, Context vertxContext) { + LOGGER.debug("anonymizeLoanRelatedRecords:: Anonymize loan-related records for log records"); CompletableFuture> future = new CompletableFuture<>(); List result = new ArrayList<>(); if (!records.isEmpty() && !records.get(0).getItems().isEmpty()) { @@ -72,6 +77,7 @@ private CompletableFuture> anonymizeLoanRelatedRecords(List getClient(okapiHeaders, vertxContext) .get(LOGS_TABLE_NAME, LogRecord.class, new String[] { "*" }, cqlWrapper, false, false, reply -> { if (reply.succeeded()) { + LOGGER.info("anonymizeLoanRelatedRecords:: Anonymize loan-related records for log records Successfully"); reply.result().getResults().forEach(record -> { record.setUserBarcode(null); record.setLinkToIds(record.getLinkToIds().withUserId(null)); @@ -79,6 +85,7 @@ private CompletableFuture> anonymizeLoanRelatedRecords(List> anonymizeLoanRelatedRecords(List saveLogRecords(List logRecords, Map okapiHeaders, Context vertxContext) { + LOGGER.debug("saveLogRecords:: Saving log records"); CompletableFuture future = new CompletableFuture<>(); getClient(okapiHeaders, vertxContext).upsertBatch(LOGS_TABLE_NAME, logRecords, reply -> { if (reply.failed()) { + LOGGER.warn("Error saving log records: {}", reply.cause().getMessage()); future.completeExceptionally(reply.cause()); } else { future.complete(null); diff --git a/mod-audit-server/src/main/java/org/folio/rest/impl/BaseService.java b/mod-audit-server/src/main/java/org/folio/rest/impl/BaseService.java index 84da6ce5..f5e7f513 100644 --- a/mod-audit-server/src/main/java/org/folio/rest/impl/BaseService.java +++ b/mod-audit-server/src/main/java/org/folio/rest/impl/BaseService.java @@ -3,6 +3,8 @@ import java.util.Map; import java.util.concurrent.CompletableFuture; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.folio.cql2pgjson.CQL2PgJSON; import org.folio.cql2pgjson.exception.FieldException; import org.folio.rest.RestVerticle; @@ -16,17 +18,23 @@ public abstract class BaseService { + private static final Logger LOGGER = LogManager.getLogger(); + PostgresClient getClient(Map okapiHeaders, Context vertxContext) { + LOGGER.debug("getClient:: Getting Postgres client"); String tenantId = TenantTool.calculateTenantId(okapiHeaders.get(RestVerticle.OKAPI_HEADER_TENANT)); return PostgresClient.getInstance(vertxContext.owner(), tenantId); } CompletableFuture createCqlWrapper(String tableName, String query, int limit, int offset) { + LOGGER.debug("createCqlWrapper:: Creating CQL wrapper"); CompletableFuture future = new CompletableFuture<>(); try { + LOGGER.info("createCqlWrapper:: Trying to Create CQL wrapper"); CQL2PgJSON cql2PgJSON = new CQL2PgJSON(tableName + ".jsonb"); future.complete(new CQLWrapper(cql2PgJSON, query).setLimit(new Limit(limit)).setOffset(new Offset(offset))); } catch (FieldException e) { + LOGGER.warn("Error Creating CQL wrapper due to {}", e.getMessage()); future.completeExceptionally(e); } return future; diff --git a/mod-audit-server/src/main/java/org/folio/rest/impl/CirculationLogsService.java b/mod-audit-server/src/main/java/org/folio/rest/impl/CirculationLogsService.java index 2703c7d2..eb161e32 100644 --- a/mod-audit-server/src/main/java/org/folio/rest/impl/CirculationLogsService.java +++ b/mod-audit-server/src/main/java/org/folio/rest/impl/CirculationLogsService.java @@ -10,6 +10,8 @@ import javax.ws.rs.core.Response; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.folio.rest.annotations.Validate; import org.folio.rest.jaxrs.model.LogRecord; import org.folio.rest.jaxrs.model.LogRecordCollection; @@ -20,16 +22,19 @@ import io.vertx.core.Handler; public class CirculationLogsService extends BaseService implements AuditDataCirculation { + private static final Logger LOGGER = LogManager.getLogger(); public static final String LOGS_TABLE_NAME = "circulation_logs"; @Override @Validate public void getAuditDataCirculationLogs(String query, int offset, int limit, String lang, Map okapiHeaders, Handler> asyncResultHandler, Context vertxContext) { + LOGGER.debug("getAuditDataCirculationLogs:: Getting audit data circulation logs"); createCqlWrapper(LOGS_TABLE_NAME, query, limit, offset) .thenAccept(cqlWrapper -> getClient(okapiHeaders, vertxContext) .get(LOGS_TABLE_NAME, LogRecord.class, new String[] { "*" }, cqlWrapper, true, false, reply -> { if (reply.succeeded()) { + LOGGER.info("getAuditDataCirculationLogs:: Successfully retrieved audit data circulation logs"); var results = reply.result().getResults(); results.stream().filter(logRecord -> isNull(logRecord.getUserBarcode())) .forEach(logRecord -> logRecord.setUserBarcode(NO_BARCODE)); @@ -38,11 +43,13 @@ public void getAuditDataCirculationLogs(String query, int offset, int limit, Str .withLogRecords(results) .withTotalRecords(reply.result().getResultInfo().getTotalRecords())))); } else { + LOGGER.warn("Failed to retrieve audit data circulation logs: {}", reply.cause().getMessage()); asyncResultHandler.handle(succeededFuture(GetAuditDataCirculationLogsResponse .respond400WithApplicationJson(buildError(HTTP_BAD_REQUEST.toInt(), reply.cause().getLocalizedMessage())))); } })) .exceptionally(throwable -> { + LOGGER.warn("Exception occurred while getting audit data circulation logs: {}", throwable.getLocalizedMessage()); asyncResultHandler.handle(succeededFuture(GetAuditDataCirculationLogsResponse. respond400WithApplicationJson(buildError(HTTP_BAD_REQUEST.toInt(), throwable.getLocalizedMessage())))); return null; diff --git a/mod-audit-server/src/main/java/org/folio/rest/impl/InitAPIs.java b/mod-audit-server/src/main/java/org/folio/rest/impl/InitAPIs.java index d9d87d82..7a5028d7 100644 --- a/mod-audit-server/src/main/java/org/folio/rest/impl/InitAPIs.java +++ b/mod-audit-server/src/main/java/org/folio/rest/impl/InitAPIs.java @@ -17,6 +17,7 @@ import org.folio.verticle.SpringVerticleFactory; import org.folio.verticle.acquisition.OrderEventConsumersVerticle; import org.folio.verticle.acquisition.OrderLineEventConsumersVerticle; +import org.folio.verticle.acquisition.PieceEventConsumersVerticle; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.support.AbstractApplicationContext; @@ -30,35 +31,39 @@ public class InitAPIs implements InitAPI { private int acqOrderConsumerInstancesNumber; @Value("${acq.order-lines.kafka.consumer.instancesNumber:1}") private int acqOrderLineConsumerInstancesNumber; + @Value("${acq.pieces.kafka.consumer.instancesNumber:1}") + private int acqPieceConsumerInstancesNumber; @Override public void init(Vertx vertx, Context context, Handler> handler) { - LOGGER.info("InitAPI starting..."); + LOGGER.debug("init:: InitAPI starting..."); try { SpringContextUtil.init(vertx, context, ApplicationConfig.class); SpringContextUtil.autowireDependencies(this, context); deployConsumersVerticles(vertx) .onSuccess(car -> { handler.handle(Future.succeededFuture()); - LOGGER.info("Consumer Verticles were successfully started"); + LOGGER.info("init:: Consumer Verticles were successfully started"); }) .onFailure(th -> { handler.handle(Future.failedFuture(th)); - LOGGER.error("Consumer Verticles were not started", th); + LOGGER.warn("Consumer Verticles were not started", th); }); } catch (Throwable th) { - LOGGER.error("Error during module init", th); + LOGGER.warn("Error during module init", th); handler.handle(Future.failedFuture(th)); } } private Future deployConsumersVerticles(Vertx vertx) { + LOGGER.debug("deployConsumersVerticles:: Deploying Consumers Verticle"); AbstractApplicationContext springContext = vertx.getOrCreateContext().get(SPRING_CONTEXT_KEY); VerticleFactory verticleFactory = springContext.getBean(SpringVerticleFactory.class); vertx.registerVerticleFactory(verticleFactory); Promise orderEventsConsumer = Promise.promise(); Promise orderLineEventsConsumer = Promise.promise(); + Promise pieceEventsConsumer = Promise.promise(); vertx.deployVerticle(getVerticleName(verticleFactory, OrderEventConsumersVerticle.class), new DeploymentOptions() @@ -70,12 +75,20 @@ private Future deployConsumersVerticles(Vertx vertx) { .setWorker(true) .setInstances(acqOrderLineConsumerInstancesNumber), orderLineEventsConsumer); + vertx.deployVerticle(getVerticleName(verticleFactory, PieceEventConsumersVerticle.class), + new DeploymentOptions() + .setWorker(true) + .setInstances(acqPieceConsumerInstancesNumber), pieceEventsConsumer); + + LOGGER.info("deployConsumersVerticles:: All consumer verticles were successfully deployed"); return GenericCompositeFuture.all(Arrays.asList( orderEventsConsumer.future(), - orderLineEventsConsumer.future())); + orderLineEventsConsumer.future(), + pieceEventsConsumer.future())); } private String getVerticleName(VerticleFactory verticleFactory, Class clazz) { + LOGGER.debug("getVerticleName:: Retrieving Verticle name"); return verticleFactory.prefix() + ":" + clazz.getName(); } } diff --git a/mod-audit-server/src/main/java/org/folio/rest/impl/ModTenantService.java b/mod-audit-server/src/main/java/org/folio/rest/impl/ModTenantService.java index 8d485b15..66fda62f 100644 --- a/mod-audit-server/src/main/java/org/folio/rest/impl/ModTenantService.java +++ b/mod-audit-server/src/main/java/org/folio/rest/impl/ModTenantService.java @@ -19,21 +19,28 @@ public class ModTenantService extends TenantAPI { @Override public Future loadData(TenantAttributes attributes, String tenantId, Map headers, Context context) { - log.info("postTenant"); + log.debug("loadData:: Starting loadData"); Promise promise = Promise.promise(); registerModuleToPubSub(headers, context.owner()) .thenAccept(p -> promise.complete(0)); + log.info("loadData:: Started Loading Data"); return promise.future(); } private CompletableFuture registerModuleToPubSub(Map headers, Vertx vertx) { + log.debug("registerModuleToPubSub:: Registering ModuleToPubSub"); CompletableFuture future = new CompletableFuture<>(); CompletableFuture.supplyAsync(() -> PubSubClientUtils.registerModule(new OkapiConnectionParams(headers, vertx))) - .thenAccept(registered -> future.complete(null)) + .thenAccept(registered -> { + log.info("registerModuleToPubSub:: Module registered successfully"); + future.complete(null); + }) .exceptionally(throwable -> { + log.warn("Error occurred while registering module: {}", throwable.getMessage()); future.completeExceptionally(throwable); return null; }); + log.info("registerModuleToPubSub:: Registered ModuleToPubSub Successfully"); return future; } } diff --git a/mod-audit-server/src/main/java/org/folio/services/acquisition/PieceAuditEventsService.java b/mod-audit-server/src/main/java/org/folio/services/acquisition/PieceAuditEventsService.java new file mode 100644 index 00000000..9b11db32 --- /dev/null +++ b/mod-audit-server/src/main/java/org/folio/services/acquisition/PieceAuditEventsService.java @@ -0,0 +1,44 @@ +package org.folio.services.acquisition; + +import io.vertx.core.Future; +import io.vertx.sqlclient.Row; +import io.vertx.sqlclient.RowSet; +import org.folio.rest.jaxrs.model.PieceAuditEvent; +import org.folio.rest.jaxrs.model.PieceAuditEventCollection; + +public interface PieceAuditEventsService { + + /** + * Saves Piece Audit Event + * + * @param pieceAuditEvent pieceAuditEvent + * @param tenantId id of tenant + * @return + */ + Future> savePieceAuditEvent(PieceAuditEvent pieceAuditEvent, String tenantId); + + /** + * Searches for piece audit events by piece id + * + * @param pieceId piece id + * @param sortBy sort by + * @param sortOrder sort order + * @param limit limit + * @param offset offset + * @return future with PieceAuditEventCollection + */ + Future getAuditEventsByPieceId(String pieceId, String sortBy, String sortOrder, + int limit, int offset, String tenantId); + + /** + * Searches for piece audit events which has unique status changes by piece id + * @param pieceId piece id + * @param sortBy sort by + * @param sortOrder sort order + * @param limit limit + * @param offset offset + * @return future with PieceAuditEventCollection + */ + Future getAuditEventsWithStatusChangesByPieceId(String pieceId, String sortBy, String sortOrder, + int limit, int offset, String tenantId); +} diff --git a/mod-audit-server/src/main/java/org/folio/services/acquisition/impl/OrderAuditEventsServiceImpl.java b/mod-audit-server/src/main/java/org/folio/services/acquisition/impl/OrderAuditEventsServiceImpl.java index e7b3e6c0..5f254824 100644 --- a/mod-audit-server/src/main/java/org/folio/services/acquisition/impl/OrderAuditEventsServiceImpl.java +++ b/mod-audit-server/src/main/java/org/folio/services/acquisition/impl/OrderAuditEventsServiceImpl.java @@ -1,43 +1,43 @@ package org.folio.services.acquisition.impl; +import static org.folio.util.ErrorUtils.handleFailures; + import io.vertx.core.Future; -import io.vertx.pgclient.PgException; import io.vertx.sqlclient.Row; import io.vertx.sqlclient.RowSet; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.folio.dao.acquisition.OrderEventsDao; -import org.folio.kafka.exception.DuplicateEventException; import org.folio.rest.jaxrs.model.OrderAuditEvent; import org.folio.rest.jaxrs.model.OrderAuditEventCollection; import org.folio.services.acquisition.OrderAuditEventsService; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class OrderAuditEventsServiceImpl implements OrderAuditEventsService { - public static final String UNIQUE_CONSTRAINT_VIOLATION_CODE = "23505"; + private static final Logger LOGGER = LogManager.getLogger(); - private OrderEventsDao orderEventsDao; + private final OrderEventsDao orderEventsDao; - @Autowired public OrderAuditEventsServiceImpl(OrderEventsDao orderEvenDao) { this.orderEventsDao = orderEvenDao; } @Override public Future> saveOrderAuditEvent(OrderAuditEvent orderAuditEvent, String tenantId) { - return orderEventsDao.save(orderAuditEvent, tenantId).recover(throwable -> handleFailures(throwable, orderAuditEvent.getId())); + LOGGER.debug("saveOrderAuditEvent:: Saving order audit event with orderId={} for tenantId={}", orderAuditEvent.getOrderId(), tenantId); + return orderEventsDao.save(orderAuditEvent, tenantId) + .recover(throwable -> { + LOGGER.error("handleFailures:: Could not save order audit event for Order id: {} in tenantId: {}", orderAuditEvent.getOrderId(), tenantId); + return handleFailures(throwable, orderAuditEvent.getId()); + }); } @Override public Future getAuditEventsByOrderId(String orderId, String sortBy, String sortOrder, int limit, int offset, String tenantId) { + LOGGER.debug("getAuditEventsByOrderId:: Retrieving audit events for orderId={} and tenantId={}", orderId, tenantId); return orderEventsDao.getAuditEventsByOrderId(orderId, sortBy, sortOrder, limit, offset, tenantId); } - private Future handleFailures(Throwable throwable, String id) { - return (throwable instanceof PgException && ((PgException) throwable).getCode().equals(UNIQUE_CONSTRAINT_VIOLATION_CODE)) ? - Future.failedFuture(new DuplicateEventException(String.format("Event with Id=%s is already processed.", id))) : - Future.failedFuture(throwable); - } - } diff --git a/mod-audit-server/src/main/java/org/folio/services/acquisition/impl/OrderLineAuditEventsServiceImpl.java b/mod-audit-server/src/main/java/org/folio/services/acquisition/impl/OrderLineAuditEventsServiceImpl.java index a891cb12..0e3d299d 100644 --- a/mod-audit-server/src/main/java/org/folio/services/acquisition/impl/OrderLineAuditEventsServiceImpl.java +++ b/mod-audit-server/src/main/java/org/folio/services/acquisition/impl/OrderLineAuditEventsServiceImpl.java @@ -1,43 +1,43 @@ package org.folio.services.acquisition.impl; +import static org.folio.util.ErrorUtils.handleFailures; + import io.vertx.core.Future; -import io.vertx.pgclient.PgException; import io.vertx.sqlclient.Row; import io.vertx.sqlclient.RowSet; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.folio.dao.acquisition.OrderLineEventsDao; -import org.folio.kafka.exception.DuplicateEventException; import org.folio.rest.jaxrs.model.OrderLineAuditEvent; import org.folio.rest.jaxrs.model.OrderLineAuditEventCollection; import org.folio.services.acquisition.OrderLineAuditEventsService; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class OrderLineAuditEventsServiceImpl implements OrderLineAuditEventsService { - public static final String UNIQUE_CONSTRAINT_VIOLATION_CODE = "23505"; + private static final Logger LOGGER = LogManager.getLogger(); - private OrderLineEventsDao orderLineEventsDao; + private final OrderLineEventsDao orderLineEventsDao; - @Autowired public OrderLineAuditEventsServiceImpl(OrderLineEventsDao orderLineEventsDao) { this.orderLineEventsDao = orderLineEventsDao; } @Override public Future> saveOrderLineAuditEvent(OrderLineAuditEvent orderLineAuditEvent, String tenantId) { - return orderLineEventsDao.save(orderLineAuditEvent, tenantId).recover(throwable -> handleFailures(throwable, orderLineAuditEvent.getId())); + LOGGER.debug("saveOrderLineAuditEvent:: Saving order line audit event with id: {} in tenant Id : {}", orderLineAuditEvent.getId(), tenantId); + return orderLineEventsDao.save(orderLineAuditEvent, tenantId) + .recover(throwable -> { + LOGGER.error("handleFailures:: Could not save order audit event for OrderLine id: {} in tenantId: {}", orderLineAuditEvent.getOrderLineId(), tenantId); + return handleFailures(throwable, orderLineAuditEvent.getId()); + }); } @Override public Future getAuditEventsByOrderLineId(String orderLineId, String sortBy, String sortOrder, int limit, int offset, String tenantId) { + LOGGER.debug("getAuditEventsByOrderLineId:: Retrieving audit events for order line Id : {} and tenant Id : {}", orderLineId, tenantId); return orderLineEventsDao.getAuditEventsByOrderLineId(orderLineId, sortBy, sortOrder, limit, offset, tenantId); } - private Future handleFailures(Throwable throwable, String id) { - return (throwable instanceof PgException && ((PgException) throwable).getCode().equals(UNIQUE_CONSTRAINT_VIOLATION_CODE)) ? - Future.failedFuture(new DuplicateEventException(String.format("Event with Id=%s is already processed.", id))) : - Future.failedFuture(throwable); - } - } diff --git a/mod-audit-server/src/main/java/org/folio/services/acquisition/impl/PieceAuditEventsServiceImpl.java b/mod-audit-server/src/main/java/org/folio/services/acquisition/impl/PieceAuditEventsServiceImpl.java new file mode 100644 index 00000000..299b6e0a --- /dev/null +++ b/mod-audit-server/src/main/java/org/folio/services/acquisition/impl/PieceAuditEventsServiceImpl.java @@ -0,0 +1,48 @@ +package org.folio.services.acquisition.impl; + +import static org.folio.util.ErrorUtils.handleFailures; + +import io.vertx.core.Future; +import io.vertx.sqlclient.Row; +import io.vertx.sqlclient.RowSet; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.folio.dao.acquisition.PieceEventsDao; +import org.folio.rest.jaxrs.model.PieceAuditEvent; +import org.folio.rest.jaxrs.model.PieceAuditEventCollection; +import org.folio.services.acquisition.PieceAuditEventsService; +import org.springframework.stereotype.Service; + +@Service +public class PieceAuditEventsServiceImpl implements PieceAuditEventsService { + private static final Logger LOGGER = LogManager.getLogger(); + + private final PieceEventsDao pieceEventsDao; + + public PieceAuditEventsServiceImpl(PieceEventsDao pieceEventsDao) { + this.pieceEventsDao = pieceEventsDao; + } + + @Override + public Future> savePieceAuditEvent(PieceAuditEvent pieceAuditEvent, String tenantId) { + LOGGER.debug("savePieceAuditEvent:: Trying to save piece audit event with id={} for tenantId={}", pieceAuditEvent.getPieceId(), tenantId); + return pieceEventsDao.save(pieceAuditEvent, tenantId) + .recover(throwable -> { + LOGGER.error("handleFailures:: Could not save order audit event for Piece id: {} in tenantId: {}", pieceAuditEvent.getPieceId(), tenantId); + return handleFailures(throwable, pieceAuditEvent.getId()); + }); + } + + @Override + public Future getAuditEventsByPieceId(String pieceId, String sortBy, String sortOrder, int limit, int offset, String tenantId) { + LOGGER.debug("getAuditEventsByOrderLineId:: Trying to retrieve audit events for piece Id : {} and tenant Id : {}", pieceId, tenantId); + return pieceEventsDao.getAuditEventsByPieceId(pieceId, sortBy, sortOrder, limit, offset, tenantId); + } + + @Override + public Future getAuditEventsWithStatusChangesByPieceId(String pieceId, String sortBy, String sortOrder, int limit, int offset, String tenantId) { + LOGGER.debug("getAuditEventsByOrderId:: Retrieving audit events with unique status changes for pieceId={} and tenantId={}", pieceId, tenantId); + return pieceEventsDao.getAuditEventsWithStatusChangesByPieceId(pieceId, sortBy, sortOrder, limit, offset, tenantId); + } + +} diff --git a/mod-audit-server/src/main/java/org/folio/util/AcquisitionEventType.java b/mod-audit-server/src/main/java/org/folio/util/AcquisitionEventType.java index e2c9a6ec..3ecb182e 100644 --- a/mod-audit-server/src/main/java/org/folio/util/AcquisitionEventType.java +++ b/mod-audit-server/src/main/java/org/folio/util/AcquisitionEventType.java @@ -2,7 +2,8 @@ public enum AcquisitionEventType { ACQ_ORDER_CHANGED("ACQ_ORDER_CHANGED"), - ACQ_ORDER_LINE_CHANGED("ACQ_ORDER_LINE_CHANGED"); + ACQ_ORDER_LINE_CHANGED("ACQ_ORDER_LINE_CHANGED"), + ACQ_PIECE_CHANGED("ACQ_PIECE_CHANGED"); private final String topicName; diff --git a/mod-audit-server/src/main/java/org/folio/util/OrderAuditEventDBConstants.java b/mod-audit-server/src/main/java/org/folio/util/AuditEventDBConstants.java similarity index 76% rename from mod-audit-server/src/main/java/org/folio/util/OrderAuditEventDBConstants.java rename to mod-audit-server/src/main/java/org/folio/util/AuditEventDBConstants.java index 77f10d14..62d40cfc 100644 --- a/mod-audit-server/src/main/java/org/folio/util/OrderAuditEventDBConstants.java +++ b/mod-audit-server/src/main/java/org/folio/util/AuditEventDBConstants.java @@ -1,10 +1,8 @@ package org.folio.util; -import org.apache.commons.lang3.StringUtils; +public class AuditEventDBConstants { -public class OrderAuditEventDBConstants { - - private OrderAuditEventDBConstants() {} + private AuditEventDBConstants() {} public static final String ID_FIELD = "id"; @@ -14,6 +12,8 @@ private OrderAuditEventDBConstants() {} public static final String ORDER_LINE_ID_FIELD = "order_line_id"; + public static final String PIECE_ID_FIELD = "piece_id"; + public static final String USER_ID_FIELD = "user_id"; public static final String EVENT_DATE_FIELD = "event_date"; @@ -26,4 +26,5 @@ private OrderAuditEventDBConstants() {} public static final String ORDER_BY_PATTERN = "ORDER BY %s %s"; + public static final String UNIQUE_CONSTRAINT_VIOLATION_CODE = "23505"; } diff --git a/mod-audit-server/src/main/java/org/folio/util/DbUtils.java b/mod-audit-server/src/main/java/org/folio/util/DbUtils.java new file mode 100644 index 00000000..7d6443cc --- /dev/null +++ b/mod-audit-server/src/main/java/org/folio/util/DbUtils.java @@ -0,0 +1,13 @@ +package org.folio.util; + +import static java.lang.String.format; +import static org.folio.rest.persist.PostgresClient.convertToPsqlStandard; + +public class DbUtils { + private DbUtils() { + } + + public static String formatDBTableName(String tenantId, String table) { + return format("%s.%s", convertToPsqlStandard(tenantId), table); + } +} diff --git a/mod-audit-server/src/main/java/org/folio/util/ErrorUtils.java b/mod-audit-server/src/main/java/org/folio/util/ErrorUtils.java index 12ebd9ae..4d7b8e74 100644 --- a/mod-audit-server/src/main/java/org/folio/util/ErrorUtils.java +++ b/mod-audit-server/src/main/java/org/folio/util/ErrorUtils.java @@ -1,6 +1,11 @@ package org.folio.util; +import static org.folio.util.AuditEventDBConstants.UNIQUE_CONSTRAINT_VIOLATION_CODE; + +import io.vertx.core.Future; +import io.vertx.pgclient.PgException; import org.folio.HttpStatus; +import org.folio.kafka.exception.DuplicateEventException; import org.folio.rest.jaxrs.model.Error; import org.folio.rest.jaxrs.model.Errors; @@ -20,4 +25,9 @@ public static Errors buildErrors(String statusCode, Throwable throwable) { .withTotalRecords(1); } + public static Future handleFailures(Throwable throwable, String id) { + return (throwable instanceof PgException pgException && pgException.getCode().equals(UNIQUE_CONSTRAINT_VIOLATION_CODE)) ? + Future.failedFuture(new DuplicateEventException(String.format("Event with id=%s is already processed.", id))) : + Future.failedFuture(throwable); + } } diff --git a/mod-audit-server/src/main/java/org/folio/util/LogEventPayloadField.java b/mod-audit-server/src/main/java/org/folio/util/LogEventPayloadField.java index e234d14b..eae2fd16 100644 --- a/mod-audit-server/src/main/java/org/folio/util/LogEventPayloadField.java +++ b/mod-audit-server/src/main/java/org/folio/util/LogEventPayloadField.java @@ -41,6 +41,7 @@ public enum LogEventPayloadField { HOLDINGS_RECORD_ID("holdingsRecordId"), INSTANCE_ID("instanceId"), ITEM_STATUS_NAME("itemStatusName"), + ZONE_ID("zoneId"), DESTINATION_SERVICE_POINT("destinationServicePoint"), SOURCE("source"), LOAN_ID("loanId"), diff --git a/mod-audit-server/src/main/java/org/folio/util/PostgresClientFactory.java b/mod-audit-server/src/main/java/org/folio/util/PostgresClientFactory.java index 40aab653..304b981c 100644 --- a/mod-audit-server/src/main/java/org/folio/util/PostgresClientFactory.java +++ b/mod-audit-server/src/main/java/org/folio/util/PostgresClientFactory.java @@ -3,15 +3,14 @@ import io.vertx.core.Vertx; import org.folio.rest.persist.PostgresClient; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class PostgresClientFactory { - private Vertx vertx; + private final Vertx vertx; - public PostgresClientFactory(@Autowired Vertx vertx) { + public PostgresClientFactory(Vertx vertx) { this.vertx = vertx; } diff --git a/mod-audit-server/src/main/java/org/folio/verticle/acquisition/PieceEventConsumersVerticle.java b/mod-audit-server/src/main/java/org/folio/verticle/acquisition/PieceEventConsumersVerticle.java new file mode 100644 index 00000000..7e13ef16 --- /dev/null +++ b/mod-audit-server/src/main/java/org/folio/verticle/acquisition/PieceEventConsumersVerticle.java @@ -0,0 +1,28 @@ +package org.folio.verticle.acquisition; + +import java.util.List; + +import org.folio.kafka.AsyncRecordHandler; +import org.folio.util.AcquisitionEventType; +import org.folio.verticle.AbstractConsumersVerticle; +import org.springframework.stereotype.Component; + +@Component +public class PieceEventConsumersVerticle extends AbstractConsumersVerticle { + + private final AsyncRecordHandler pieceEventsHandler; + + public PieceEventConsumersVerticle(AsyncRecordHandler pieceEventsHandler) { + this.pieceEventsHandler = pieceEventsHandler; + } + + @Override + public List getEvents() { + return List.of(AcquisitionEventType.ACQ_PIECE_CHANGED.getTopicName()); + } + + @Override + public AsyncRecordHandler getHandler() { + return pieceEventsHandler; + } +} diff --git a/mod-audit-server/src/main/java/org/folio/verticle/acquisition/consumers/OrderEventsHandler.java b/mod-audit-server/src/main/java/org/folio/verticle/acquisition/consumers/OrderEventsHandler.java index 6cf16c26..8d1f5a9e 100644 --- a/mod-audit-server/src/main/java/org/folio/verticle/acquisition/consumers/OrderEventsHandler.java +++ b/mod-audit-server/src/main/java/org/folio/verticle/acquisition/consumers/OrderEventsHandler.java @@ -41,16 +41,16 @@ public Future handle(KafkaConsumerRecord record) { List kafkaHeaders = record.headers(); OkapiConnectionParams okapiConnectionParams = new OkapiConnectionParams(KafkaHeaderUtils.kafkaHeadersToMap(kafkaHeaders), vertx); OrderAuditEvent event = new JsonObject(record.value()).mapTo(OrderAuditEvent.class); - LOGGER.info("Starting processing of Order audit event with id: {} for order id: {}", event.getId(), event.getOrderId()); + LOGGER.info("handle:: Starting processing of Order audit event with id: {} for order id: {}", event.getId(), event.getOrderId()); orderAuditEventsService.saveOrderAuditEvent(event, okapiConnectionParams.getTenantId()) .onSuccess(ar -> { - LOGGER.info("Order audit event with id: {} has been processed for order id: {}", event.getId(), event.getOrderId()); + LOGGER.info("handle:: Order audit event with id: {} has been processed for order id: {}", event.getId(), event.getOrderId()); result.complete(event.getId()); }) .onFailure(e -> { if (e instanceof DuplicateEventException) { - LOGGER.info("Duplicate Order audit event with id: {} for order id: {} received, skipped processing", event.getId(), event.getOrderId()); + LOGGER.info("handle:: Duplicate Order audit event with id: {} for order id: {} received, skipped processing", event.getId(), event.getOrderId()); result.complete(event.getId()); } else { LOGGER.error("Processing of Order audit event with id: {} for order id: {} has been failed", event.getId(), event.getOrderId(), e); diff --git a/mod-audit-server/src/main/java/org/folio/verticle/acquisition/consumers/OrderLineEventsHandler.java b/mod-audit-server/src/main/java/org/folio/verticle/acquisition/consumers/OrderLineEventsHandler.java index 6898c311..d45c3618 100644 --- a/mod-audit-server/src/main/java/org/folio/verticle/acquisition/consumers/OrderLineEventsHandler.java +++ b/mod-audit-server/src/main/java/org/folio/verticle/acquisition/consumers/OrderLineEventsHandler.java @@ -40,18 +40,18 @@ public Future handle(KafkaConsumerRecord record) { List kafkaHeaders = record.headers(); OkapiConnectionParams okapiConnectionParams = new OkapiConnectionParams(KafkaHeaderUtils.kafkaHeadersToMap(kafkaHeaders), vertx); OrderLineAuditEvent event = new JsonObject(record.value()).mapTo(OrderLineAuditEvent.class); - LOGGER.info("Starting processing of Order Line audit event with id: {} for order id: {} and order line id: {}", + LOGGER.info("handle:: Starting processing of Order Line audit event with id: {} for order id: {} and order line id: {}", event.getId(), event.getOrderId(), event.getOrderLineId()); orderLineAuditEventsService.saveOrderLineAuditEvent(event, okapiConnectionParams.getTenantId()) .onSuccess(ar -> { - LOGGER.info("Order Line audit event with id: {} has been processed for order id: {} and order line id: {}", + LOGGER.info("handle:: Order Line audit event with id: {} has been processed for order id: {} and order line id: {}", event.getId(), event.getOrderId(), event.getOrderLineId()); result.complete(event.getId()); }) .onFailure(e -> { if (e instanceof DuplicateEventException) { - LOGGER.info("Duplicate Order Line audit event with id: {} for order id: {} and order line id: {} received, skipped processing", + LOGGER.info("handle:: Duplicate Order Line audit event with id: {} for order id: {} and order line id: {} received, skipped processing", event.getId(), event.getOrderId(), event.getOrderLineId()); result.complete(event.getId()); } else { diff --git a/mod-audit-server/src/main/java/org/folio/verticle/acquisition/consumers/PieceEventsHandler.java b/mod-audit-server/src/main/java/org/folio/verticle/acquisition/consumers/PieceEventsHandler.java new file mode 100644 index 00000000..90a4e098 --- /dev/null +++ b/mod-audit-server/src/main/java/org/folio/verticle/acquisition/consumers/PieceEventsHandler.java @@ -0,0 +1,60 @@ +package org.folio.verticle.acquisition.consumers; + +import java.util.List; + +import io.vertx.core.Future; +import io.vertx.core.Promise; +import io.vertx.core.Vertx; +import io.vertx.core.json.JsonObject; +import io.vertx.kafka.client.consumer.KafkaConsumerRecord; +import io.vertx.kafka.client.producer.KafkaHeader; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.folio.kafka.AsyncRecordHandler; +import org.folio.kafka.KafkaHeaderUtils; +import org.folio.kafka.exception.DuplicateEventException; +import org.folio.rest.jaxrs.model.PieceAuditEvent; +import org.folio.rest.util.OkapiConnectionParams; +import org.folio.services.acquisition.PieceAuditEventsService; +import org.springframework.stereotype.Component; + +@Component +public class PieceEventsHandler implements AsyncRecordHandler { + + private static final Logger LOGGER = LogManager.getLogger(); + + private final PieceAuditEventsService pieceAuditEventsService; + private final Vertx vertx; + + public PieceEventsHandler(Vertx vertx, + PieceAuditEventsService pieceAuditEventsService) { + this.pieceAuditEventsService = pieceAuditEventsService; + this.vertx = vertx; + } + + @Override + public Future handle(KafkaConsumerRecord kafkaConsumerRecord) { + Promise result = Promise.promise(); + List kafkaHeaders = kafkaConsumerRecord.headers(); + OkapiConnectionParams okapiConnectionParams = new OkapiConnectionParams(KafkaHeaderUtils.kafkaHeadersToMap(kafkaHeaders), vertx); + PieceAuditEvent event = new JsonObject(kafkaConsumerRecord.value()).mapTo(PieceAuditEvent.class); + LOGGER.info("handle:: Starting processing of Piece audit event with id: {} for piece id: {}", event.getId(), event.getPieceId()); + + pieceAuditEventsService.savePieceAuditEvent(event, okapiConnectionParams.getTenantId()) + .onSuccess(ar -> { + LOGGER.info("handle:: Piece audit event with id: {} has been processed for piece id: {}", event.getId(), event.getPieceId()); + result.complete(event.getId()); + }) + .onFailure(e -> { + if (e instanceof DuplicateEventException) { + LOGGER.info("handle:: Duplicate Piece audit event with id: {} for piece id: {} received, skipped processing", event.getId(), event.getPieceId()); + result.complete(event.getId()); + } else { + LOGGER.error("Processing of Piece audit event with id: {} for piece id: {} has been failed", event.getId(), event.getPieceId(), e); + result.fail(e); + } + }); + + return result.future(); + } +} diff --git a/mod-audit-server/src/main/resources/templates/db_scripts/acquisition/create_acquisition_piece_log_table.sql b/mod-audit-server/src/main/resources/templates/db_scripts/acquisition/create_acquisition_piece_log_table.sql new file mode 100644 index 00000000..cf82a593 --- /dev/null +++ b/mod-audit-server/src/main/resources/templates/db_scripts/acquisition/create_acquisition_piece_log_table.sql @@ -0,0 +1,11 @@ +CREATE TABLE IF NOT EXISTS acquisition_piece_log ( + id uuid PRIMARY KEY, + action text NOT NULL, + piece_id uuid NOT NULL, + user_id uuid NOT NULL, + event_date timestamp NOT NULL, + action_date timestamp NOT NULL, + modified_content_snapshot jsonb +); + +CREATE INDEX IF NOT EXISTS piece_id_index ON acquisition_piece_log USING BTREE (piece_id); diff --git a/mod-audit-server/src/main/resources/templates/db_scripts/schema.json b/mod-audit-server/src/main/resources/templates/db_scripts/schema.json index 0262feff..be4d0804 100644 --- a/mod-audit-server/src/main/resources/templates/db_scripts/schema.json +++ b/mod-audit-server/src/main/resources/templates/db_scripts/schema.json @@ -74,6 +74,11 @@ "run": "after", "snippetPath": "acquisition/create_acquisition_order_line_log_table.sql", "fromModuleVersion": "mod-audit-2.7.0" + }, + { + "run": "after", + "snippetPath": "acquisition/create_acquisition_piece_log_table.sql", + "fromModuleVersion": "mod-audit-2.9.0" } ] } diff --git a/mod-audit-server/src/test/java/org/folio/TestSuite.java b/mod-audit-server/src/test/java/org/folio/TestSuite.java index b76663c3..797a0916 100644 --- a/mod-audit-server/src/test/java/org/folio/TestSuite.java +++ b/mod-audit-server/src/test/java/org/folio/TestSuite.java @@ -1,5 +1,8 @@ package org.folio; +import static net.mguenther.kafka.junit.EmbeddedKafkaCluster.provisionWith; +import static net.mguenther.kafka.junit.EmbeddedKafkaClusterConfig.defaultClusterConfig; + import java.util.Locale; import java.util.Objects; import java.util.concurrent.CompletableFuture; @@ -7,6 +10,10 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import io.restassured.RestAssured; +import io.vertx.core.DeploymentOptions; +import io.vertx.core.Vertx; +import io.vertx.core.json.JsonObject; import net.mguenther.kafka.junit.EmbeddedKafkaCluster; import org.folio.builder.service.CheckInRecordBuilderTest; import org.folio.builder.service.CheckOutRecordBuilderTest; @@ -18,25 +25,24 @@ import org.folio.builder.service.RequestRecordBuilderTest; import org.folio.dao.OrderEventsDaoTest; import org.folio.dao.OrderLineEventsDaoTest; +import org.folio.dao.PieceEventsDaoTest; import org.folio.postgres.testing.PostgresTesterContainer; import org.folio.rest.RestVerticle; -import org.folio.rest.impl.*; import org.folio.rest.impl.AuditDataAcquisitionAPITest; +import org.folio.rest.impl.AuditDataImplApiTest; +import org.folio.rest.impl.AuditHandlersImplApiTest; +import org.folio.rest.impl.CirculationLogsImplApiTest; +import org.folio.rest.impl.OrderEventsHandlerMockTest; +import org.folio.rest.impl.OrderLineEventsHandlerMockTest; +import org.folio.rest.impl.PieceEventsHandlerMockTest; import org.folio.rest.persist.PostgresClient; import org.folio.services.OrderAuditEventsServiceTest; import org.folio.services.OrderLineAuditEventsServiceTest; +import org.folio.services.PieceAuditEventsServiceTest; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Nested; -import io.restassured.RestAssured; -import io.vertx.core.DeploymentOptions; -import io.vertx.core.Vertx; -import io.vertx.core.json.JsonObject; - -import static net.mguenther.kafka.junit.EmbeddedKafkaCluster.provisionWith; -import static net.mguenther.kafka.junit.EmbeddedKafkaClusterConfig.defaultClusterConfig; - public class TestSuite { private static final String KAFKA_HOST = "KAFKA_HOST"; private static final String KAFKA_PORT = "KAFKA_PORT"; @@ -98,7 +104,7 @@ private static void startVerticle(DeploymentOptions options) CompletableFuture deploymentComplete = new CompletableFuture<>(); vertx.deployVerticle(RestVerticle.class.getName(), options, res -> { - if(res.succeeded()) { + if (res.succeeded()) { deploymentComplete.complete(res.result()); } else { @@ -176,6 +182,18 @@ class OrderLineAuditEventsServiceNestedTest extends OrderLineAuditEventsServiceT class OrderLineEventsDaoNestedTest extends OrderLineEventsDaoTest { } + @Nested + class PieceEventsDaoNestedTest extends PieceEventsDaoTest { + } + + @Nested + class PieceAuditEventsServiceNestedTest extends PieceAuditEventsServiceTest { + } + + @Nested + class PieceEventsHandlerMockNestedTest extends PieceEventsHandlerMockTest { + } + @Nested class AuditDataImplApiTestNested extends AuditDataImplApiTest { } @@ -184,5 +202,4 @@ class AuditDataImplApiTestNested extends AuditDataImplApiTest { class CirculationLogsImplApiTestNested extends CirculationLogsImplApiTest { } - } diff --git a/mod-audit-server/src/test/java/org/folio/builder/service/CheckInRecordBuilderTest.java b/mod-audit-server/src/test/java/org/folio/builder/service/CheckInRecordBuilderTest.java index a373a75f..acb48b49 100644 --- a/mod-audit-server/src/test/java/org/folio/builder/service/CheckInRecordBuilderTest.java +++ b/mod-audit-server/src/test/java/org/folio/builder/service/CheckInRecordBuilderTest.java @@ -14,19 +14,27 @@ import static org.folio.util.LogEventPayloadField.REQUESTS; import static org.folio.util.LogEventPayloadField.REQUEST_TYPE; import static org.folio.util.LogEventPayloadField.RETURN_DATE; +import static org.folio.util.LogEventPayloadField.ZONE_ID; import static org.folio.utils.TenantApiTestUtil.CHECK_IN_PAYLOAD_JSON; +import static org.folio.utils.TenantApiTestUtil.CHECK_IN_WITH_BACKDATE_TIMEZONE_PAYLOAD_JSON; +import static org.folio.utils.TenantApiTestUtil.CHECK_IN_WITH_TIMEZONE_PAYLOAD_JSON; import static org.folio.utils.TenantApiTestUtil.getFile; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasSize; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; import java.util.List; import java.util.Map; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.folio.rest.jaxrs.model.LogRecord; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; @@ -36,11 +44,12 @@ public class CheckInRecordBuilderTest extends BuilderTestBase { private static final Logger logger = LogManager.getLogger(); - @Test - public void checkInTest() throws Exception { + @ParameterizedTest + @ValueSource(strings = {CHECK_IN_PAYLOAD_JSON, CHECK_IN_WITH_TIMEZONE_PAYLOAD_JSON, CHECK_IN_WITH_BACKDATE_TIMEZONE_PAYLOAD_JSON }) + void checkInTest(String sample) throws Exception { logger.info("Test check-in log records builder"); - JsonObject payload = new JsonObject(getFile(CHECK_IN_PAYLOAD_JSON)); + JsonObject payload = new JsonObject(getFile(sample)); Map>> records = checkInRecordBuilder.buildLogRecord(payload) .get().stream() @@ -60,8 +69,13 @@ public void checkInTest() throws Exception { LogRecord loanClosedRecord = records.get(LogRecord.Object.LOAN).get(LogRecord.Action.CLOSED_LOAN).get(0); validateAdditionalContent(payload, loanClosedRecord); - assertThat(loanClosedRecord.getDescription(), equalTo(format("Item status: %s. Backdated to: %s. Overdue due date: %s.", - getProperty(payload, ITEM_STATUS_NAME), getFormattedDateTime(getDateTimeProperty(payload, RETURN_DATE)), getFormattedDateTime(getDateTimeProperty(payload, DUE_DATE))))); + if(!sample.equalsIgnoreCase(CHECK_IN_WITH_TIMEZONE_PAYLOAD_JSON)) { + assertThat(loanClosedRecord.getDescription(), equalTo(format("Item status: %s. Backdated to: %s. Overdue due date: %s.", + getProperty(payload, ITEM_STATUS_NAME), + getFormattedDateTime(getDateInTenantTimeZone(getDateTimeProperty(payload, RETURN_DATE), + ZoneId.of(getProperty(payload, ZONE_ID) != null ? getProperty(payload, ZONE_ID) : ZoneOffset.UTC.getId())).toLocalDateTime()), + getFormattedDateTime(getDateTimeProperty(payload, DUE_DATE))))); + } LogRecord requestStatusChangedRecord = records.get(LogRecord.Object.REQUEST).get(LogRecord.Action.REQUEST_STATUS_CHANGED).get(0); validateBaseContent(payload, requestStatusChangedRecord); @@ -77,4 +91,8 @@ public void checkInTest() throws Exception { assertThat(requestStatusChangedRecord.getDescription(), equalTo(format("Type: %s. New request status: %s (from: %s).", requestType, newRequestStatus, oldRequestStatus))); } + + private ZonedDateTime getDateInTenantTimeZone(LocalDateTime localDateTime, ZoneId zoneId) { + return localDateTime.atZone(ZoneId.of(ZoneOffset.UTC.getId())).withZoneSameInstant(zoneId); + } } diff --git a/mod-audit-server/src/test/java/org/folio/builder/service/LoanRecordBuilderTest.java b/mod-audit-server/src/test/java/org/folio/builder/service/LoanRecordBuilderTest.java index 0d1279f7..26ed2678 100644 --- a/mod-audit-server/src/test/java/org/folio/builder/service/LoanRecordBuilderTest.java +++ b/mod-audit-server/src/test/java/org/folio/builder/service/LoanRecordBuilderTest.java @@ -131,7 +131,7 @@ void testChangedDueDate() throws Exception { assertThat(loanLogRecord.getAction(), equalTo(CHANGED_DUE_DATE)); assertThat(loanLogRecord.getDate(), is(not(nullValue()))); assertThat(loanLogRecord.getServicePointId(), equalTo("c4c90014-c8c9-4ade-8f24-b5e313319f4b")); - assertThat(loanLogRecord.getSource(), equalTo("Bernhard, Cordell")); + assertThat(loanLogRecord.getSource(), equalTo("ADMINISTRATOR, DIKU")); } @Test diff --git a/mod-audit-server/src/test/java/org/folio/dao/OrderEventsDaoTest.java b/mod-audit-server/src/test/java/org/folio/dao/OrderEventsDaoTest.java index 4077de08..d052046e 100644 --- a/mod-audit-server/src/test/java/org/folio/dao/OrderEventsDaoTest.java +++ b/mod-audit-server/src/test/java/org/folio/dao/OrderEventsDaoTest.java @@ -1,33 +1,30 @@ package org.folio.dao; +import static org.folio.utils.EntityUtils.TENANT_ID; +import static org.folio.utils.EntityUtils.createOrderAuditEvent; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.UUID; + import io.vertx.core.Future; import io.vertx.core.Vertx; -import io.vertx.core.json.JsonObject; import io.vertx.pgclient.PgException; import io.vertx.sqlclient.Row; import io.vertx.sqlclient.RowSet; import org.folio.dao.acquisition.impl.OrderEventsDaoImpl; import org.folio.rest.jaxrs.model.OrderAuditEvent; import org.folio.rest.jaxrs.model.OrderAuditEventCollection; +import org.folio.util.PostgresClientFactory; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.MockitoAnnotations; import org.mockito.Spy; -import org.folio.util.PostgresClientFactory; - -import java.util.Date; -import java.util.List; -import java.util.UUID; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; public class OrderEventsDaoTest { - private static final String TENANT_ID = "diku"; - public static final String ORDER_ID = "a21fc51c-d46b-439b-8c79-9b2be41b79a6"; - @Spy private PostgresClientFactory postgresClientFactory = new PostgresClientFactory(Vertx.vertx()); @InjectMocks @@ -41,63 +38,31 @@ public void setUp() { @Test void shouldCreateEventProcessed() { - JsonObject jsonObject = new JsonObject(); - jsonObject.put("name","Test Product 123 "); - - OrderAuditEvent orderAuditEvent = new OrderAuditEvent() - .withId(UUID.randomUUID().toString()) - .withAction(OrderAuditEvent.Action.CREATE) - .withOrderId(ORDER_ID) - .withUserId(UUID.randomUUID().toString()) - .withEventDate(new Date()) - .withActionDate(new Date()) - .withOrderSnapshot(jsonObject); + var orderAuditEvent = createOrderAuditEvent(UUID.randomUUID().toString()); Future> saveFuture = orderEventDao.save(orderAuditEvent, TENANT_ID); - saveFuture.onComplete(ar -> { - assertTrue(ar.succeeded()); - }); + saveFuture.onComplete(ar -> assertTrue(ar.succeeded())); } @Test void shouldThrowConstraintViolation() { - JsonObject jsonObject = new JsonObject(); - jsonObject.put("name","Test Product1"); - - OrderAuditEvent orderAuditEvent = new OrderAuditEvent() - .withId(UUID.randomUUID().toString()) - .withAction(OrderAuditEvent.Action.CREATE) - .withOrderId(ORDER_ID) - .withUserId(UUID.randomUUID().toString()) - .withEventDate(new Date()) - .withActionDate(new Date()) - .withOrderSnapshot(jsonObject); + var orderAuditEvent = createOrderAuditEvent(UUID.randomUUID().toString()); Future> saveFuture = orderEventDao.save(orderAuditEvent, TENANT_ID); saveFuture.onComplete(ar -> { Future> reSaveFuture = orderEventDao.save(orderAuditEvent, TENANT_ID); reSaveFuture.onComplete(re -> { assertTrue(re.failed()); - assertTrue(re.cause() instanceof PgException); + assertTrue(re.cause() instanceof PgException); assertEquals("ERROR: duplicate key value violates unique constraint \"acquisition_order_log_pkey\" (23505)", re.cause().getMessage()); }); }); - } + } @Test void shouldGetCreatedEvent() { - JsonObject jsonObject = new JsonObject(); - jsonObject.put("name","Test Product2"); String id = UUID.randomUUID().toString(); - - OrderAuditEvent orderAuditEvent = new OrderAuditEvent() - .withId(id) - .withAction(OrderAuditEvent.Action.CREATE) - .withOrderId(ORDER_ID) - .withUserId(UUID.randomUUID().toString()) - .withEventDate(new Date()) - .withActionDate(new Date()) - .withOrderSnapshot(jsonObject); + var orderAuditEvent = createOrderAuditEvent(id); orderEventDao.save(orderAuditEvent, TENANT_ID); @@ -108,7 +73,6 @@ void shouldGetCreatedEvent() { assertEquals(orderAuditEventList.get(0).getId(), id); assertEquals(OrderAuditEvent.Action.CREATE.value(), orderAuditEventList.get(0).getAction().value()); - }); } diff --git a/mod-audit-server/src/test/java/org/folio/dao/OrderLineEventsDaoTest.java b/mod-audit-server/src/test/java/org/folio/dao/OrderLineEventsDaoTest.java index 6e9df2d8..88d9ae87 100644 --- a/mod-audit-server/src/test/java/org/folio/dao/OrderLineEventsDaoTest.java +++ b/mod-audit-server/src/test/java/org/folio/dao/OrderLineEventsDaoTest.java @@ -1,8 +1,15 @@ package org.folio.dao; +import static org.folio.utils.EntityUtils.TENANT_ID; +import static org.folio.utils.EntityUtils.createOrderLineAuditEvent; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.UUID; + import io.vertx.core.Future; import io.vertx.core.Vertx; -import io.vertx.core.json.JsonObject; import io.vertx.pgclient.PgException; import io.vertx.sqlclient.Row; import io.vertx.sqlclient.RowSet; @@ -17,19 +24,8 @@ import org.mockito.MockitoAnnotations; import org.mockito.Spy; -import java.util.Date; -import java.util.List; -import java.util.UUID; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - public class OrderLineEventsDaoTest { - private static final String TENANT_ID = "diku"; - - public static final String ORDER_LINE_ID = "a21fc51c-d46b-439b-8c79-9b2be41b79a6"; - @Spy private PostgresClientFactory postgresClientFactory = new PostgresClientFactory(Vertx.vertx()); @@ -44,30 +40,14 @@ public void setUp() { @Test void shouldCreateEventProcessed() { - OrderLineAuditEvent orderLineAuditEvent = new OrderLineAuditEvent() - .withId(UUID.randomUUID().toString()) - .withAction(OrderLineAuditEvent.Action.CREATE) - .withOrderId(UUID.randomUUID().toString()) - .withOrderLineId(UUID.randomUUID().toString()) - .withUserId(UUID.randomUUID().toString()) - .withEventDate(new Date()) - .withActionDate(new Date()); + var orderLineAuditEvent = createOrderLineAuditEvent(UUID.randomUUID().toString()); Future> saveFuture = orderLineEventsDao.save(orderLineAuditEvent, TENANT_ID); - saveFuture.onComplete(ar -> { - assertTrue(ar.succeeded()); - }); + saveFuture.onComplete(ar -> assertTrue(ar.succeeded())); } @Test void shouldThrowConstraintViolation() { - OrderLineAuditEvent orderLineAuditEvent = new OrderLineAuditEvent() - .withId(UUID.randomUUID().toString()) - .withAction(OrderLineAuditEvent.Action.CREATE) - .withOrderId(UUID.randomUUID().toString()) - .withOrderLineId(UUID.randomUUID().toString()) - .withUserId(UUID.randomUUID().toString()) - .withEventDate(new Date()) - .withActionDate(new Date()); + var orderLineAuditEvent = createOrderLineAuditEvent(UUID.randomUUID().toString()); Future> saveFuture = orderLineEventsDao.save(orderLineAuditEvent, TENANT_ID); saveFuture.onComplete(ar -> { @@ -82,19 +62,8 @@ void shouldThrowConstraintViolation() { @Test void shouldGetCreatedEvent() { - JsonObject jsonObject = new JsonObject(); - jsonObject.put("name","Test Product2"); String id = UUID.randomUUID().toString(); - - OrderLineAuditEvent orderLineAuditEvent = new OrderLineAuditEvent() - .withId(id) - .withAction(OrderLineAuditEvent.Action.CREATE) - .withOrderId(UUID.randomUUID().toString()) - .withOrderLineId(ORDER_LINE_ID) - .withUserId(UUID.randomUUID().toString()) - .withEventDate(new Date()) - .withActionDate(new Date()) - .withOrderLineSnapshot(jsonObject); + var orderLineAuditEvent = createOrderLineAuditEvent(id); orderLineEventsDao.save(orderLineAuditEvent, TENANT_ID); @@ -105,7 +74,6 @@ void shouldGetCreatedEvent() { assertEquals(orderLineAuditEventList.get(0).getId(), id); assertEquals(OrderAuditEvent.Action.CREATE.value(), orderLineAuditEventList.get(0).getAction().value()); - }); } diff --git a/mod-audit-server/src/test/java/org/folio/dao/PieceEventsDaoTest.java b/mod-audit-server/src/test/java/org/folio/dao/PieceEventsDaoTest.java new file mode 100644 index 00000000..817d0b78 --- /dev/null +++ b/mod-audit-server/src/test/java/org/folio/dao/PieceEventsDaoTest.java @@ -0,0 +1,77 @@ +package org.folio.dao; + +import static org.folio.utils.EntityUtils.TENANT_ID; +import static org.folio.utils.EntityUtils.createPieceAuditEvent; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.UUID; + +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import io.vertx.pgclient.PgException; +import io.vertx.sqlclient.Row; +import io.vertx.sqlclient.RowSet; +import org.folio.dao.acquisition.impl.PieceEventsDaoImpl; +import org.folio.rest.jaxrs.model.PieceAuditEvent; +import org.folio.rest.jaxrs.model.PieceAuditEventCollection; +import org.folio.util.PostgresClientFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.MockitoAnnotations; +import org.mockito.Spy; + +public class PieceEventsDaoTest { + + @Spy + PostgresClientFactory postgresClientFactory = new PostgresClientFactory(Vertx.vertx()); + @InjectMocks + PieceEventsDaoImpl pieceEventsDao; + + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + pieceEventsDao = new PieceEventsDaoImpl(postgresClientFactory); + } + + @Test + void shouldCreateEventProcessed() { + var pieceAuditEvent = createPieceAuditEvent(UUID.randomUUID().toString()); + + Future> saveFuture = pieceEventsDao.save(pieceAuditEvent, TENANT_ID); + saveFuture.onComplete(ar -> assertTrue(ar.succeeded())); + } + + @Test + void shouldThrowConstrainViolation() { + var pieceAuditEvent = createPieceAuditEvent(UUID.randomUUID().toString()); + + Future> saveFuture = pieceEventsDao.save(pieceAuditEvent, TENANT_ID); + saveFuture.onComplete(ar -> { + Future> reSaveFuture = pieceEventsDao.save(pieceAuditEvent, TENANT_ID); + reSaveFuture.onComplete(re -> { + assertTrue(re.failed()); + assertTrue(re.cause() instanceof PgException); + assertEquals("ERROR: duplicate key value violates unique constraint \"acquisition_piece_log_pkey\" (23505)", re.cause().getMessage()); + }); + }); + } + + @Test + void shouldGetCreatedEvent() { + String id = UUID.randomUUID().toString(); + var pieceAuditEvent = createPieceAuditEvent(id); + + pieceEventsDao.save(pieceAuditEvent, TENANT_ID); + + Future saveFuture = pieceEventsDao.getAuditEventsByPieceId(id, "action_date", "desc", 1, 1, TENANT_ID); + saveFuture.onComplete(ar -> { + PieceAuditEventCollection pieceAuditEventCollection = ar.result(); + List pieceAuditEventList = pieceAuditEventCollection.getPieceAuditEvents(); + assertEquals(pieceAuditEventList.get(0).getId(), id); + assertEquals(PieceAuditEvent.Action.CREATE.value(), pieceAuditEventList.get(0).getAction().value()); + }); + } +} diff --git a/mod-audit-server/src/test/java/org/folio/rest/impl/AuditDataAcquisitionAPITest.java b/mod-audit-server/src/test/java/org/folio/rest/impl/AuditDataAcquisitionAPITest.java index ac7fde01..4d71eb05 100644 --- a/mod-audit-server/src/test/java/org/folio/rest/impl/AuditDataAcquisitionAPITest.java +++ b/mod-audit-server/src/test/java/org/folio/rest/impl/AuditDataAcquisitionAPITest.java @@ -1,12 +1,26 @@ package org.folio.rest.impl; +import static io.restassured.RestAssured.given; +import static org.folio.utils.EntityUtils.ORDER_ID; +import static org.folio.utils.EntityUtils.ORDER_LINE_ID; +import static org.folio.utils.EntityUtils.PIECE_ID; +import static org.folio.utils.EntityUtils.createPieceAuditEvent; +import static org.hamcrest.Matchers.containsString; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Date; +import java.util.UUID; + import io.restassured.http.Header; import io.vertx.core.Vertx; import io.vertx.core.json.JsonObject; import org.folio.dao.acquisition.impl.OrderEventsDaoImpl; import org.folio.dao.acquisition.impl.OrderLineEventsDaoImpl; +import org.folio.dao.acquisition.impl.PieceEventsDaoImpl; import org.folio.rest.jaxrs.model.OrderAuditEvent; import org.folio.rest.jaxrs.model.OrderLineAuditEvent; +import org.folio.rest.jaxrs.model.PieceAuditEvent; import org.folio.util.PostgresClientFactory; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -14,48 +28,33 @@ import org.mockito.MockitoAnnotations; import org.mockito.Spy; - -import java.util.Date; -import java.util.UUID; - -import static io.restassured.RestAssured.given; -import static org.hamcrest.Matchers.*; - public class AuditDataAcquisitionAPITest extends ApiTestBase { - public static final Header TENANT = new Header("X-Okapi-Tenant", "modaudittest"); - - protected static final Header PERMS = new Header("X-Okapi-Permissions", "audit.all"); - - protected static final Header CONTENT_TYPE = new Header("Content-Type", "application/json"); - - protected static final String INVALID_ID = "646ea52c-2c65-4d28-9a8f-e0d200fd6b00"; - - protected static final String ACQ_AUDIT_ORDER_PATH = "/audit-data/acquisition/order/"; - - protected static final String ACQ_AUDIT_ORDER_LINE_PATH = "/audit-data/acquisition/order-line/"; - + private static final Header TENANT = new Header("X-Okapi-Tenant", "modaudittest"); + private static final Header PERMS = new Header("X-Okapi-Permissions", "audit.all"); + private static final Header CONTENT_TYPE = new Header("Content-Type", "application/json"); + private static final String INVALID_ID = "646ea52c-2c65-4d28-9a8f-e0d200fd6b00"; + private static final String ACQ_AUDIT_ORDER_PATH = "/audit-data/acquisition/order/"; + private static final String ACQ_AUDIT_ORDER_LINE_PATH = "/audit-data/acquisition/order-line/"; + private static final String ACQ_AUDIT_PIECE_PATH = "/audit-data/acquisition/piece/"; + private static final String ACQ_AUDIT_PIECE_STATUS_CHANGE_HISTORY_PATH = "/status-change-history"; private static final String TENANT_ID = "modaudittest"; - public static final String ORDER_ID = "a21fc51c-d46b-439b-8c79-9b2be41b79a6"; - - public static final String ORDER_LINE_ID = "a22fc51c-d46b-439b-8c79-9b2be41b79a6"; - @Spy private PostgresClientFactory postgresClientFactory = new PostgresClientFactory(Vertx.vertx()); @InjectMocks - OrderEventsDaoImpl orderEventDao = new OrderEventsDaoImpl(postgresClientFactory); - + OrderEventsDaoImpl orderEventDao; @InjectMocks - OrderLineEventsDaoImpl orderLineEventDao = new OrderLineEventsDaoImpl(postgresClientFactory); + OrderLineEventsDaoImpl orderLineEventDao; + @InjectMocks + PieceEventsDaoImpl pieceEventsDao; @BeforeEach public void setUp() { MockitoAnnotations.openMocks(this); orderEventDao = new OrderEventsDaoImpl(postgresClientFactory); orderLineEventDao = new OrderLineEventsDaoImpl(postgresClientFactory); - } @Test @@ -104,10 +103,12 @@ void shouldReturnOrderLineEventsOnGetByOrderLineId() { orderLineEventDao.save(orderLineAuditEvent, TENANT_ID); - given().header(CONTENT_TYPE).header(TENANT).header(PERMS).get(ACQ_AUDIT_ORDER_LINE_PATH+ INVALID_ID).then().log().all().statusCode(200) + given().header(CONTENT_TYPE).header(TENANT).header(PERMS).get(ACQ_AUDIT_ORDER_LINE_PATH+ INVALID_ID) + .then().log().all().statusCode(200) .body(containsString("orderLineAuditEvents")).body(containsString("totalItems")); - given().header(CONTENT_TYPE).header(TENANT).header(PERMS).get(ACQ_AUDIT_ORDER_LINE_PATH+ ORDER_LINE_ID).then().log().all().statusCode(200) + given().header(CONTENT_TYPE).header(TENANT).header(PERMS).get(ACQ_AUDIT_ORDER_LINE_PATH+ ORDER_LINE_ID) + .then().log().all().statusCode(200) .body(containsString(ORDER_LINE_ID)); given().header(CONTENT_TYPE).header(TENANT).header(PERMS).get(ACQ_AUDIT_ORDER_LINE_PATH+ ORDER_LINE_ID +"?limit=1").then().log().all().statusCode(200) @@ -119,4 +120,119 @@ void shouldReturnOrderLineEventsOnGetByOrderLineId() { given().header(CONTENT_TYPE).header(TENANT).header(PERMS).get(ACQ_AUDIT_ORDER_PATH+ ORDER_LINE_ID + 123).then().log().all().statusCode(500) .body(containsString("UUID string too large")); } + + @Test + void shouldReturnPieceEventsOnGetByPieceId() { + JsonObject jsonObject = new JsonObject(); + jsonObject.put("name","Test Product2"); + + PieceAuditEvent pieceAuditEvent = new PieceAuditEvent() + .withId(UUID.randomUUID().toString()) + .withAction(PieceAuditEvent.Action.CREATE) + .withPieceId(PIECE_ID) + .withUserId(UUID.randomUUID().toString()) + .withEventDate(new Date()) + .withActionDate(new Date()) + .withPieceSnapshot(jsonObject); + + pieceEventsDao.save(pieceAuditEvent, TENANT_ID); + + given().header(CONTENT_TYPE).header(TENANT).header(PERMS) + .get(ACQ_AUDIT_PIECE_PATH + INVALID_ID) + .then().log().all().statusCode(200) + .body(containsString("pieceAuditEvents")).body(containsString("totalItems")); + + given().header(CONTENT_TYPE).header(TENANT).header(PERMS) + .get(ACQ_AUDIT_PIECE_PATH + PIECE_ID) + .then().log().all().statusCode(200) + .body(containsString(PIECE_ID)); + + given().header(CONTENT_TYPE).header(TENANT).header(PERMS) + .get(ACQ_AUDIT_PIECE_PATH + PIECE_ID +"?limit=1") + .then().log().all().statusCode(200) + .body(containsString(PIECE_ID)); + + given().header(CONTENT_TYPE).header(TENANT).header(PERMS) + .get(ACQ_AUDIT_PIECE_PATH + PIECE_ID + "?sortBy=action_date") + .then().log().all().statusCode(200) + .body(containsString(PIECE_ID)); + + given().header(CONTENT_TYPE).header(TENANT).header(PERMS) + .get(ACQ_AUDIT_PIECE_PATH + PIECE_ID + 123).then().log().all().statusCode(500) + .body(containsString("UUID string too large")); + } + + @Test + void shouldReturnPieceEventsStatusChangesHistoryGetByPieceId() { + String id1 = UUID.randomUUID().toString(); + String id2 = UUID.randomUUID().toString(); + String id3 = UUID.randomUUID().toString(); + String id4 = UUID.randomUUID().toString(); + String id5 = UUID.randomUUID().toString(); + String id6 = UUID.randomUUID().toString(); + String id7 = UUID.randomUUID().toString(); + var pieceAuditEvent1 = createPieceAuditEvent(id1, "STATUS 1"); + var pieceAuditEvent2 = createPieceAuditEvent(id2, "STATUS 1"); + var pieceAuditEvent3 = createPieceAuditEvent(id3, "STATUS 2"); + var pieceAuditEvent4 = createPieceAuditEvent(id4, "STATUS 2"); + var pieceAuditEvent5 = createPieceAuditEvent(id5, "STATUS 1"); + var pieceAuditEventWithDifferentPiece1 = createPieceAuditEvent(id6, "STATUS 3"); + var pieceAuditEventWithDifferentPiece2 = createPieceAuditEvent(id7, "STATUS 2"); + pieceAuditEventWithDifferentPiece1.setPieceId(UUID.randomUUID().toString()); + pieceAuditEventWithDifferentPiece2.setPieceId(UUID.randomUUID().toString()); + var localDateTime1 = LocalDateTime.of(2023, 4, 20, 6, 9, 30); + var localDateTime2 = LocalDateTime.of(2023, 4, 20, 6, 10, 30); + var localDateTime3 = LocalDateTime.of(2023, 4, 20, 6, 11, 30); + var localDateTime4 = LocalDateTime.of(2023, 4, 20, 6, 12, 30); + var localDateTime5 = LocalDateTime.of(2023, 4, 20, 6, 13, 30); + var localDateTime6 = LocalDateTime.of(2023, 4, 20, 6, 9, 25); + var localDateTime7 = LocalDateTime.of(2023, 4, 20, 6, 9, 20); + pieceAuditEvent1.setActionDate(Date.from(localDateTime1.atZone(ZoneId.systemDefault()).toInstant())); + pieceAuditEvent2.setActionDate(Date.from(localDateTime2.atZone(ZoneId.systemDefault()).toInstant())); + pieceAuditEvent3.setActionDate(Date.from(localDateTime3.atZone(ZoneId.systemDefault()).toInstant())); + pieceAuditEvent4.setActionDate(Date.from(localDateTime4.atZone(ZoneId.systemDefault()).toInstant())); + pieceAuditEvent5.setActionDate(Date.from(localDateTime5.atZone(ZoneId.systemDefault()).toInstant())); + pieceAuditEventWithDifferentPiece1.setActionDate(Date.from(localDateTime6.atZone(ZoneId.systemDefault()).toInstant())); + pieceAuditEventWithDifferentPiece2.setActionDate(Date.from(localDateTime7.atZone(ZoneId.systemDefault()).toInstant())); + + pieceEventsDao.save(pieceAuditEvent1, TENANT_ID); + pieceEventsDao.save(pieceAuditEvent2, TENANT_ID); + pieceEventsDao.save(pieceAuditEvent3, TENANT_ID); + pieceEventsDao.save(pieceAuditEvent4, TENANT_ID); + pieceEventsDao.save(pieceAuditEvent5, TENANT_ID); + pieceEventsDao.save(pieceAuditEventWithDifferentPiece1, TENANT_ID); + pieceEventsDao.save(pieceAuditEventWithDifferentPiece2, TENANT_ID); + + // based on our business logic, it returns pieceAuditEvent1, pieceAuditEvent3, pieceAuditEvent5 + given().header(CONTENT_TYPE).header(TENANT).header(PERMS) + .get(ACQ_AUDIT_PIECE_PATH + INVALID_ID + ACQ_AUDIT_PIECE_STATUS_CHANGE_HISTORY_PATH) + .then().log().all().statusCode(200) + .body(containsString("pieceAuditEvents")).body(containsString("totalItems")); + + given().header(CONTENT_TYPE).header(TENANT).header(PERMS) + .get(ACQ_AUDIT_PIECE_PATH + PIECE_ID + ACQ_AUDIT_PIECE_STATUS_CHANGE_HISTORY_PATH) + .then().log().all().statusCode(200) + .body(containsString(PIECE_ID)) + .body(containsString(id1)) + .body(containsString(id3)) + .body(containsString(id5)); + + given().header(CONTENT_TYPE).header(TENANT).header(PERMS) + .get(ACQ_AUDIT_PIECE_PATH + PIECE_ID + ACQ_AUDIT_PIECE_STATUS_CHANGE_HISTORY_PATH +"?limit=1") + .then().log().all().statusCode(200) + .body(containsString(PIECE_ID)); + + given().header(CONTENT_TYPE).header(TENANT).header(PERMS) + .get(ACQ_AUDIT_PIECE_PATH + PIECE_ID + ACQ_AUDIT_PIECE_STATUS_CHANGE_HISTORY_PATH +"?sortBy=action_date") + .then().log().all().statusCode(200) + .body(containsString(PIECE_ID)) + .body(containsString(id1)) + .body(containsString(id3)) + .body(containsString(id5)); + + given().header(CONTENT_TYPE).header(TENANT).header(PERMS) + .get(ACQ_AUDIT_PIECE_PATH + PIECE_ID + 123 + ACQ_AUDIT_PIECE_STATUS_CHANGE_HISTORY_PATH) + .then().log().all().statusCode(500) + .body(containsString("UUID string too large")); + } } diff --git a/mod-audit-server/src/test/java/org/folio/rest/impl/AuditHandlersImplApiTest.java b/mod-audit-server/src/test/java/org/folio/rest/impl/AuditHandlersImplApiTest.java index 52f8465b..57ecf968 100644 --- a/mod-audit-server/src/test/java/org/folio/rest/impl/AuditHandlersImplApiTest.java +++ b/mod-audit-server/src/test/java/org/folio/rest/impl/AuditHandlersImplApiTest.java @@ -81,7 +81,7 @@ void postLogRecordEventForRequestOverride(String sample) { verifyNumberOfLogRecords(REQUEST, ++initialNumberOfRequestRecords); } - @Test + //@Test void postLogRecordEventForNoticeError() { logger.info("post valid log event for notice error: success"); diff --git a/mod-audit-server/src/test/java/org/folio/rest/impl/OrderEventsHandlerMockTest.java b/mod-audit-server/src/test/java/org/folio/rest/impl/OrderEventsHandlerMockTest.java index af96368b..31e321d9 100644 --- a/mod-audit-server/src/test/java/org/folio/rest/impl/OrderEventsHandlerMockTest.java +++ b/mod-audit-server/src/test/java/org/folio/rest/impl/OrderEventsHandlerMockTest.java @@ -3,7 +3,6 @@ import io.vertx.core.Future; import io.vertx.core.Vertx; import io.vertx.core.json.Json; -import io.vertx.core.json.JsonObject; import io.vertx.kafka.client.consumer.KafkaConsumerRecord; import io.vertx.kafka.client.consumer.impl.KafkaConsumerRecordImpl; @@ -22,10 +21,11 @@ import org.mockito.*; import java.nio.charset.StandardCharsets; -import java.util.Date; import java.util.UUID; import static org.folio.kafka.KafkaTopicNameHelper.getDefaultNameSpace; +import static org.folio.utils.EntityUtils.createOrderAuditEvent; +import static org.folio.utils.EntityUtils.createOrderAuditEventWithoutSnapshot; import static org.junit.Assert.assertTrue; public class OrderEventsHandlerMockTest { @@ -63,16 +63,7 @@ public void setUp() { @Test void shouldProcessEvent() { - JsonObject jsonObject = new JsonObject(); - jsonObject.put("Test","TestValue"); - - OrderAuditEvent orderAuditEvent = new OrderAuditEvent() - .withId(ID) - .withEventDate(new Date()) - .withOrderId(UUID.randomUUID().toString()) - .withActionDate(new Date()) - .withAction(OrderAuditEvent.Action.CREATE) - .withOrderSnapshot(jsonObject); + var orderAuditEvent = createOrderAuditEvent(UUID.randomUUID().toString()); KafkaConsumerRecord kafkaConsumerRecord = buildKafkaConsumerRecord(orderAuditEvent); Future saveFuture = orderEventsHandler.handle(kafkaConsumerRecord); @@ -83,14 +74,9 @@ void shouldProcessEvent() { @Test void shouldNotProcessEvent() { - OrderAuditEvent orderAuditEvent = new OrderAuditEvent() - .withId(UUID.randomUUID().toString()) - .withEventDate(new Date()) - .withOrderId(UUID.randomUUID().toString()) - .withActionDate(new Date()) - .withAction(OrderAuditEvent.Action.CREATE) - .withOrderSnapshot("Test"); + var orderAuditEvent = createOrderAuditEventWithoutSnapshot(); KafkaConsumerRecord kafkaConsumerRecord = buildKafkaConsumerRecord(orderAuditEvent); + Future save = orderEventsHandler.handle(kafkaConsumerRecord); assertTrue(save.failed()); } diff --git a/mod-audit-server/src/test/java/org/folio/rest/impl/PieceEventsHandlerMockTest.java b/mod-audit-server/src/test/java/org/folio/rest/impl/PieceEventsHandlerMockTest.java new file mode 100644 index 00000000..553a446c --- /dev/null +++ b/mod-audit-server/src/test/java/org/folio/rest/impl/PieceEventsHandlerMockTest.java @@ -0,0 +1,92 @@ +package org.folio.rest.impl; + + +import static org.folio.kafka.KafkaTopicNameHelper.getDefaultNameSpace; +import static org.folio.utils.EntityUtils.createPieceAuditEvent; +import static org.folio.utils.EntityUtils.createPieceAuditEventWithoutSnapshot; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.charset.StandardCharsets; +import java.util.UUID; + +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import io.vertx.core.json.Json; +import io.vertx.kafka.client.consumer.KafkaConsumerRecord; +import io.vertx.kafka.client.consumer.impl.KafkaConsumerRecordImpl; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.common.header.internals.RecordHeader; +import org.folio.dao.acquisition.impl.PieceEventsDaoImpl; +import org.folio.kafka.KafkaTopicNameHelper; +import org.folio.rest.jaxrs.model.PieceAuditEvent; +import org.folio.rest.util.OkapiConnectionParams; +import org.folio.services.acquisition.PieceAuditEventsService; +import org.folio.services.acquisition.impl.PieceAuditEventsServiceImpl; +import org.folio.util.PostgresClientFactory; +import org.folio.verticle.acquisition.consumers.PieceEventsHandler; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.Spy; + +public class PieceEventsHandlerMockTest { + private static final String TENANT_ID = "diku"; + protected static final String TOKEN = "token"; + protected static final String KAFKA_EVN = "folio"; + public static final String OKAPI_TOKEN_HEADER = "x-okapi-token"; + public static final String OKAPI_URL_HEADER = "x-okapi-url"; + + @Spy + private Vertx vertx = Vertx.vertx(); + @Spy + private PostgresClientFactory postgresClientFactory = new PostgresClientFactory(Vertx.vertx()); + + @Mock + PieceEventsDaoImpl pieceEventsDao; + @Mock + PieceAuditEventsService pieceAuditEventsService; + + private PieceEventsHandler pieceEventsHandler; + + @BeforeEach + public void setUp() throws Exception { + MockitoAnnotations.openMocks(this).close(); + pieceEventsDao = new PieceEventsDaoImpl(postgresClientFactory); + pieceAuditEventsService = new PieceAuditEventsServiceImpl(pieceEventsDao); + pieceEventsHandler = new PieceEventsHandler(vertx, pieceAuditEventsService); + } + + @Test + void shouldProcessEvent() { + var pieceAuditEvent = createPieceAuditEvent(UUID.randomUUID().toString()); + KafkaConsumerRecord kafkaConsumerRecord = buildKafkaConsumerRecord(pieceAuditEvent); + Future saveFuture = pieceEventsHandler.handle(kafkaConsumerRecord); + saveFuture.onComplete(are -> { + assertTrue(are.succeeded()); + }); + } + + @Test + void shouldNotProcessEvent() { + var pieceAuditEvent = createPieceAuditEventWithoutSnapshot(); + KafkaConsumerRecord kafkaConsumerRecord = buildKafkaConsumerRecord(pieceAuditEvent); + Future saveFuture = pieceEventsHandler.handle(kafkaConsumerRecord); + assertTrue(saveFuture.failed()); + } + + private KafkaConsumerRecord buildKafkaConsumerRecord(PieceAuditEvent event) { + String topic = KafkaTopicNameHelper.formatTopicName(KAFKA_EVN, getDefaultNameSpace(), TENANT_ID, event.getAction().name()); + ConsumerRecord consumerRecord = buildConsumerRecord(topic, event); + return new KafkaConsumerRecordImpl<>(consumerRecord); + } + + protected ConsumerRecord buildConsumerRecord(String topic, PieceAuditEvent event) { + ConsumerRecord consumer = new ConsumerRecord<>("folio", 0, 0, topic, Json.encode(event)); + consumer.headers().add(new RecordHeader(OkapiConnectionParams.OKAPI_TENANT_HEADER, TENANT_ID.getBytes(StandardCharsets.UTF_8))); + consumer.headers().add(new RecordHeader(OKAPI_URL_HEADER, ("https://localhost:" + 8080).getBytes(StandardCharsets.UTF_8))); + consumer.headers().add(new RecordHeader(OKAPI_TOKEN_HEADER, TOKEN.getBytes(StandardCharsets.UTF_8))); + return consumer; + } + +} diff --git a/mod-audit-server/src/test/java/org/folio/services/OrderAuditEventsServiceTest.java b/mod-audit-server/src/test/java/org/folio/services/OrderAuditEventsServiceTest.java index 89cc275f..2bd3904c 100644 --- a/mod-audit-server/src/test/java/org/folio/services/OrderAuditEventsServiceTest.java +++ b/mod-audit-server/src/test/java/org/folio/services/OrderAuditEventsServiceTest.java @@ -1,8 +1,15 @@ package org.folio.services; +import static org.folio.utils.EntityUtils.TENANT_ID; +import static org.folio.utils.EntityUtils.createOrderAuditEvent; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.UUID; + import io.vertx.core.Future; import io.vertx.core.Vertx; -import io.vertx.core.json.JsonObject; import io.vertx.sqlclient.Row; import io.vertx.sqlclient.RowSet; import org.folio.dao.acquisition.OrderEventsDao; @@ -16,17 +23,8 @@ import org.mockito.Mock; import org.mockito.Spy; -import java.util.Date; -import java.util.List; -import java.util.UUID; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - public class OrderAuditEventsServiceTest { - private static final String TENANT_ID = "diku"; - @Spy private PostgresClientFactory postgresClientFactory = new PostgresClientFactory(Vertx.vertx()); @Mock @@ -37,17 +35,7 @@ public class OrderAuditEventsServiceTest { @Test void shouldCallDaoForSuccessfulCase() { - JsonObject jsonObject = new JsonObject(); - jsonObject.put("name","Test Product"); - - OrderAuditEvent orderAuditEvent = new OrderAuditEvent() - .withId(UUID.randomUUID().toString()) - .withAction(OrderAuditEvent.Action.CREATE) - .withOrderId(UUID.randomUUID().toString()) - .withUserId(UUID.randomUUID().toString()) - .withEventDate(new Date()) - .withActionDate(new Date()) - .withOrderSnapshot(jsonObject); + var orderAuditEvent = createOrderAuditEvent(UUID.randomUUID().toString()); Future> saveFuture = orderAuditEventService.saveOrderAuditEvent(orderAuditEvent, TENANT_ID); saveFuture.onComplete(ar -> { @@ -57,18 +45,8 @@ void shouldCallDaoForSuccessfulCase() { @Test void shouldGetDto() { - JsonObject jsonObject = new JsonObject(); - jsonObject.put("name","Test Product"); - String id = UUID.randomUUID().toString(); - OrderAuditEvent orderAuditEvent = new OrderAuditEvent() - .withId(id) - .withAction(OrderAuditEvent.Action.CREATE) - .withOrderId(UUID.randomUUID().toString()) - .withUserId(UUID.randomUUID().toString()) - .withEventDate(new Date()) - .withActionDate(new Date()) - .withOrderSnapshot(jsonObject); + var orderAuditEvent = createOrderAuditEvent(id); orderAuditEventService.saveOrderAuditEvent(orderAuditEvent, TENANT_ID); @@ -79,7 +57,6 @@ void shouldGetDto() { assertEquals(orderAuditEventList.get(0).getId(), id); assertEquals(OrderAuditEvent.Action.CREATE.value(), orderAuditEventList.get(0).getAction().value()); - }); } diff --git a/mod-audit-server/src/test/java/org/folio/services/OrderLineAuditEventsServiceTest.java b/mod-audit-server/src/test/java/org/folio/services/OrderLineAuditEventsServiceTest.java index 3d0523d6..272c9816 100644 --- a/mod-audit-server/src/test/java/org/folio/services/OrderLineAuditEventsServiceTest.java +++ b/mod-audit-server/src/test/java/org/folio/services/OrderLineAuditEventsServiceTest.java @@ -2,7 +2,6 @@ import io.vertx.core.Future; import io.vertx.core.Vertx; -import io.vertx.core.json.JsonObject; import io.vertx.sqlclient.Row; import io.vertx.sqlclient.RowSet; import org.folio.dao.acquisition.OrderLineEventsDao; @@ -16,34 +15,27 @@ import org.mockito.Mock; import org.mockito.Spy; -import java.util.Date; import java.util.List; import java.util.UUID; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +import static org.folio.utils.EntityUtils.TENANT_ID; +import static org.folio.utils.EntityUtils.createOrderLineAuditEvent; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; public class OrderLineAuditEventsServiceTest { - private static final String TENANT_ID = "diku"; - @Spy private PostgresClientFactory postgresClientFactory = new PostgresClientFactory(Vertx.vertx()); @Mock OrderLineEventsDao orderLineEventsDao = new OrderLineEventsDaoImpl(postgresClientFactory); - @InjectMocks OrderLineAuditEventsServiceImpl orderLineAuditEventService = new OrderLineAuditEventsServiceImpl(orderLineEventsDao); @Test public void shouldCallDaoForSuccessfulCase() { - OrderLineAuditEvent orderLineAuditEvent = new OrderLineAuditEvent() - .withId(UUID.randomUUID().toString()) - .withAction(OrderLineAuditEvent.Action.CREATE) - .withOrderId(UUID.randomUUID().toString()) - .withUserId(UUID.randomUUID().toString()) - .withEventDate(new Date()) - .withActionDate(new Date()); + var orderLineAuditEvent = createOrderLineAuditEvent(UUID.randomUUID().toString()); + Future> saveFuture = orderLineAuditEventService.saveOrderLineAuditEvent(orderLineAuditEvent, TENANT_ID); saveFuture.onComplete(ar -> { @@ -53,19 +45,8 @@ public void shouldCallDaoForSuccessfulCase() { @Test void shouldGetOrderLineDto() { - JsonObject jsonObject = new JsonObject(); - jsonObject.put("name", "Test Product"); - String id = UUID.randomUUID().toString(); - OrderLineAuditEvent orderLineAuditEvent = new OrderLineAuditEvent() - .withId(id) - .withAction(OrderLineAuditEvent.Action.CREATE) - .withOrderId(UUID.randomUUID().toString()) - .withOrderLineId(UUID.randomUUID().toString()) - .withUserId(UUID.randomUUID().toString()) - .withEventDate(new Date()) - .withActionDate(new Date()) - .withOrderLineSnapshot(jsonObject); + var orderLineAuditEvent = createOrderLineAuditEvent(id); orderLineAuditEventService.saveOrderLineAuditEvent(orderLineAuditEvent, TENANT_ID); diff --git a/mod-audit-server/src/test/java/org/folio/services/PieceAuditEventsServiceTest.java b/mod-audit-server/src/test/java/org/folio/services/PieceAuditEventsServiceTest.java new file mode 100644 index 00000000..df2af446 --- /dev/null +++ b/mod-audit-server/src/test/java/org/folio/services/PieceAuditEventsServiceTest.java @@ -0,0 +1,40 @@ +package org.folio.services; + +import static org.folio.utils.EntityUtils.TENANT_ID; +import static org.folio.utils.EntityUtils.createPieceAuditEvent; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.UUID; + +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import io.vertx.sqlclient.Row; +import io.vertx.sqlclient.RowSet; +import org.folio.dao.acquisition.PieceEventsDao; +import org.folio.dao.acquisition.impl.PieceEventsDaoImpl; +import org.folio.services.acquisition.impl.PieceAuditEventsServiceImpl; +import org.folio.util.PostgresClientFactory; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; + +public class PieceAuditEventsServiceTest { + + @Spy + private PostgresClientFactory postgresClientFactory = new PostgresClientFactory(Vertx.vertx()); + @Mock + PieceEventsDao pieceEventsDao = new PieceEventsDaoImpl(postgresClientFactory); + @InjectMocks + PieceAuditEventsServiceImpl pieceAuditEventsService = new PieceAuditEventsServiceImpl(pieceEventsDao); + + @Test + void shouldCallDaoSuccessfully() { + var pieceAuditEvent = createPieceAuditEvent(UUID.randomUUID().toString()); + + Future> saveFuture = pieceAuditEventsService.savePieceAuditEvent(pieceAuditEvent, TENANT_ID); + saveFuture.onComplete(ar -> { + assertTrue(ar.succeeded()); + }); + } +} diff --git a/mod-audit-server/src/test/java/org/folio/utils/EntityUtils.java b/mod-audit-server/src/test/java/org/folio/utils/EntityUtils.java new file mode 100644 index 00000000..ce3ea468 --- /dev/null +++ b/mod-audit-server/src/test/java/org/folio/utils/EntityUtils.java @@ -0,0 +1,97 @@ +package org.folio.utils; + +import java.util.Date; +import java.util.UUID; + +import io.vertx.core.json.JsonObject; +import org.folio.rest.jaxrs.model.OrderAuditEvent; +import org.folio.rest.jaxrs.model.OrderLineAuditEvent; +import org.folio.rest.jaxrs.model.PieceAuditEvent; + +public class EntityUtils { + + public static String TENANT_ID = "diku"; + public static String PIECE_ID = "2cd4adc4-f287-49b6-a9c6-9eacdc4868e7"; + public static String ORDER_ID = "a21fc51c-d46b-439b-8c79-9b2be41b79a6"; + public static String ORDER_LINE_ID = "a22fc51c-d46b-439b-8c79-9b2be41b79a6"; + + public static OrderAuditEvent createOrderAuditEvent(String id) { + JsonObject jsonObject = new JsonObject(); + jsonObject.put("name", "Test Product 123 "); + + return new OrderAuditEvent() + .withId(UUID.randomUUID().toString()) + .withAction(OrderAuditEvent.Action.CREATE) + .withOrderId(ORDER_ID) + .withUserId(UUID.randomUUID().toString()) + .withEventDate(new Date()) + .withActionDate(new Date()) + .withOrderSnapshot(jsonObject); + } + + public static OrderAuditEvent createOrderAuditEventWithoutSnapshot() { + return new OrderAuditEvent() + .withId(UUID.randomUUID().toString()) + .withAction(OrderAuditEvent.Action.CREATE) + .withOrderId(ORDER_ID) + .withUserId(UUID.randomUUID().toString()) + .withEventDate(new Date()) + .withActionDate(new Date()) + .withOrderSnapshot("Test"); + } + + public static OrderLineAuditEvent createOrderLineAuditEvent(String id) { + JsonObject jsonObject = new JsonObject(); + jsonObject.put("name", "Test Product"); + + return new OrderLineAuditEvent() + .withId(id) + .withAction(OrderLineAuditEvent.Action.CREATE) + .withOrderId(ORDER_ID) + .withOrderLineId(ORDER_LINE_ID) + .withUserId(UUID.randomUUID().toString()) + .withEventDate(new Date()) + .withActionDate(new Date()) + .withOrderLineSnapshot(jsonObject); + } + + public static PieceAuditEvent createPieceAuditEvent(String id) { + JsonObject jsonObject = new JsonObject(); + jsonObject.put("name", "Test Product"); + + return new PieceAuditEvent() + .withId(id) + .withAction(PieceAuditEvent.Action.CREATE) + .withPieceId(PIECE_ID) + .withUserId(UUID.randomUUID().toString()) + .withEventDate(new Date()) + .withActionDate(new Date()) + .withPieceSnapshot(jsonObject); + } + + public static PieceAuditEvent createPieceAuditEvent(String id, String status) { + JsonObject jsonObject = new JsonObject(); + jsonObject.put("name", "Test Product"); + jsonObject.put("receivingStatus", status); + + return new PieceAuditEvent() + .withId(id) + .withAction(PieceAuditEvent.Action.CREATE) + .withPieceId(PIECE_ID) + .withUserId(UUID.randomUUID().toString()) + .withEventDate(new Date()) + .withActionDate(new Date()) + .withPieceSnapshot(jsonObject); + } + + public static PieceAuditEvent createPieceAuditEventWithoutSnapshot() { + return new PieceAuditEvent() + .withId(UUID.randomUUID().toString()) + .withAction(PieceAuditEvent.Action.CREATE) + .withPieceId(PIECE_ID) + .withUserId(UUID.randomUUID().toString()) + .withEventDate(new Date()) + .withActionDate(new Date()) + .withPieceSnapshot("Test"); + } +} diff --git a/mod-audit-server/src/test/java/org/folio/utils/TenantApiTestUtil.java b/mod-audit-server/src/test/java/org/folio/utils/TenantApiTestUtil.java index 828a8e51..da24ce33 100644 --- a/mod-audit-server/src/test/java/org/folio/utils/TenantApiTestUtil.java +++ b/mod-audit-server/src/test/java/org/folio/utils/TenantApiTestUtil.java @@ -34,6 +34,8 @@ public class TenantApiTestUtil { public static final Header X_OKAPI_URL_TO = new Header("X-Okapi-Url-To", "http://localhost:" + port); public static final String CHECK_IN_PAYLOAD_JSON = "payloads/check_in.json"; + public static final String CHECK_IN_WITH_TIMEZONE_PAYLOAD_JSON = "payloads/check_in_with_timezone.json"; + public static final String CHECK_IN_WITH_BACKDATE_TIMEZONE_PAYLOAD_JSON = "payloads/check_in_with_backdate_timezone.json"; public static final String CHECK_OUT_PAYLOAD_JSON = "payloads/check_out.json"; public static final String CHECK_OUT_THROUGH_OVERRIDE_PAYLOAD_JSON = "payloads/check_out_through_override.json"; diff --git a/mod-audit-server/src/test/resources/payloads/check_in.json b/mod-audit-server/src/test/resources/payloads/check_in.json index e33f56e3..4eae2cdd 100644 --- a/mod-audit-server/src/test/resources/payloads/check_in.json +++ b/mod-audit-server/src/test/resources/payloads/check_in.json @@ -3,7 +3,7 @@ "servicePointId":"7c5abc9f-f3d7-4856-b8d7-6712462ca007", "loanId":"6ccebb4d-3ef8-489e-907e-a85bca372ca2", "isLoanClosed":true, - "systemReturnDate":"2020-09-25T08:44:16.574Z", + "systemReturnDate":"2020-09-25T08:45:16.574Z", "returnDate":"2020-09-25T08:44:17.000Z", "dueDate":"2020-09-26T08:42:05.004Z", "holdingsRecordId": "2e96afd4-7027-4cb8-bc40-efc5d1bf155b", diff --git a/mod-audit-server/src/test/resources/payloads/check_in_with_backdate_timezone.json b/mod-audit-server/src/test/resources/payloads/check_in_with_backdate_timezone.json new file mode 100644 index 00000000..497a0d3e --- /dev/null +++ b/mod-audit-server/src/test/resources/payloads/check_in_with_backdate_timezone.json @@ -0,0 +1,28 @@ +{ + "logEventType":"CHECK_IN_EVENT", + "servicePointId":"7c5abc9f-f3d7-4856-b8d7-6712462ca007", + "loanId":"6ccebb4d-3ef8-489e-907e-a85bca372ca2", + "isLoanClosed":true, + "systemReturnDate":"2023-12-14T07:05:38.545Z", + "returnDate":"2023-12-14T07:06:38.545Z", + "dueDate":"2023-12-26T08:42:05.004Z", + "holdingsRecordId": "2e96afd4-7027-4cb8-bc40-efc5d1bf155b", + "instanceId":"c361cda5-ee48-43b4-88eb-1d5c32c165cf", + "userId":"2a424823-588a-45ee-9441-a6384b6614b2", + "userBarcode":"236964750970123", + "itemId":"bb5a6689-c008-4c96-8f8f-b666850ee12d", + "itemBarcode":"326547658598", + "itemStatusName":"In transit", + "claimedReturnedResolution": "Returned by patron", + "destinationServicePoint":"Circ Desk 2", + "source":"folio", + "zoneId": "America/New_York", + "requests":[ + { + "id":"03279209-a273-4fff-8d96-8aa43f013df4", + "oldRequestStatus":"Open - Not yet filled", + "newRequestStatus":"Open - In transit", + "requestType":"Recall" + } + ] +} diff --git a/mod-audit-server/src/test/resources/payloads/check_in_with_timezone.json b/mod-audit-server/src/test/resources/payloads/check_in_with_timezone.json new file mode 100644 index 00000000..86fcf513 --- /dev/null +++ b/mod-audit-server/src/test/resources/payloads/check_in_with_timezone.json @@ -0,0 +1,28 @@ +{ + "logEventType":"CHECK_IN_EVENT", + "servicePointId":"7c5abc9f-f3d7-4856-b8d7-6712462ca007", + "loanId":"6ccebb4d-3ef8-489e-907e-a85bca372ca2", + "isLoanClosed":true, + "systemReturnDate":"2023-12-13T07:05:38.545Z", + "returnDate":"2023-12-13T07:05:38.269Z", + "dueDate":"2020-09-26T08:42:05.004Z", + "holdingsRecordId": "2e96afd4-7027-4cb8-bc40-efc5d1bf155b", + "instanceId":"c361cda5-ee48-43b4-88eb-1d5c32c165cf", + "userId":"2a424823-588a-45ee-9441-a6384b6614b2", + "userBarcode":"236964750970123", + "itemId":"bb5a6689-c008-4c96-8f8f-b666850ee12d", + "itemBarcode":"326547658598", + "itemStatusName":"In transit", + "claimedReturnedResolution": "Returned by patron", + "destinationServicePoint":"Circ Desk 2", + "source":"folio", + "zoneId": "America/New_York", + "requests":[ + { + "id":"03279209-a273-4fff-8d96-8aa43f013df4", + "oldRequestStatus":"Open - Not yet filled", + "newRequestStatus":"Open - In transit", + "requestType":"Recall" + } + ] +} diff --git a/mod-audit-server/src/test/resources/payloads/loan_changed_due_date.json b/mod-audit-server/src/test/resources/payloads/loan_changed_due_date.json index 88d0a645..df35ffe1 100644 --- a/mod-audit-server/src/test/resources/payloads/loan_changed_due_date.json +++ b/mod-audit-server/src/test/resources/payloads/loan_changed_due_date.json @@ -1,6 +1,7 @@ { "logEventType" : "LOAN", "payload": { + "userBarcode" : "631888472578232", "itemBarcode" : "90000", "itemId" : "100d10bf-2f06-4aa0-be15-0b95b2d9f9e3", "instanceId" : "5bf370e0-8cca-4d9c-82e4-5170ab2a0a39", @@ -9,6 +10,7 @@ "action" : "Changed due date", "date" : "2020-10-14T10:10:41.610Z", "servicePointId" : "c4c90014-c8c9-4ade-8f24-b5e313319f4b", + "updatedByUserId" : "fa96fec8-786f-505b-b55e-e0bb1150d548", "loanId" : "336ec84c-27ed-483d-92e3-926fafa7ed1c" } } diff --git a/pom.xml b/pom.xml index c3093284..a4fd0e44 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 org.folio mod-audit - 2.7.1-SNAPSHOT + 2.9.0-SNAPSHOT pom @@ -18,7 +18,7 @@ - 1.9.6 + 1.9.19 35.0.4 4.3.5 2.25.1 @@ -67,7 +67,7 @@ maven-compiler-plugin 3.8.1 - 11 + 17 UTF-8 diff --git a/ramls/acquisition-events.raml b/ramls/acquisition-events.raml index 3f9ce2b5..8bdd0850 100644 --- a/ramls/acquisition-events.raml +++ b/ramls/acquisition-events.raml @@ -15,6 +15,8 @@ types: order-audit-event: !include order_audit_event.json order-audit-event-collection: !include order_audit_event_collection.json order-line-audit-event-collection: !include order_line_audit_event_collection.json + piece-audit-event: !include piece_audit_event.json + piece-audit-event-collection: !include piece_audit_event_collection.json traits: searchable: !include raml-util/traits/searchable.raml @@ -88,3 +90,72 @@ traits: body: application/json: type: errors + + /piece/{id}: + get: + description: Get list of piece events by piece_id + is: [ + pageable, + validate + ] + queryParameters: + sortBy: + description: "sorting by field: actionDate" + type: string + default: action_date + sortOrder: + description: "sort order: asc or desc" + enum: [asc, desc] + type: string + default: desc + limit: + default: 2147483647 + offset: + default: 0 + responses: + 200: + body: + application/json: + type: piece-audit-event-collection + 500: + description: "Internal server error" + body: + application/json: + type: errors + example: + strict: false + value: !include raml-util/examples/errors.sample + /piece/{id}/status-change-history: + get: + description: Get list of piece events which have unique status changes by piece_id + is: [ + pageable, + validate + ] + queryParameters: + sortBy: + description: "sorting by field: actionDate" + type: string + default: action_date + sortOrder: + description: "sort order: asc or desc" + enum: [asc, desc] + type: string + default: desc + limit: + default: 2147483647 + offset: + default: 0 + responses: + 200: + body: + application/json: + type: piece-audit-event-collection + 500: + description: "Internal server error" + body: + application/json: + type: errors + example: + strict: false + value: !include raml-util/examples/errors.sample diff --git a/ramls/audit-data.raml b/ramls/audit-data.raml index 8346e87f..9bdfe7a7 100644 --- a/ramls/audit-data.raml +++ b/ramls/audit-data.raml @@ -92,3 +92,4 @@ resourceTypes: description: "Not authorized to perform requested action" body: text/plain: + diff --git a/ramls/piece_audit_event.json b/ramls/piece_audit_event.json new file mode 100644 index 00000000..a1d5141e --- /dev/null +++ b/ramls/piece_audit_event.json @@ -0,0 +1,40 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Piece audit event", + "type": "object", + "properties": { + "id": { + "description": "UUID of the event", + "$ref": "common/uuid.json" + }, + "action": { + "description": "Action for piece (Create, Edit or Delete)", + "type": "string", + "$ref": "event_action.json" + }, + "pieceId": { + "description": "UUID of the piece", + "$ref": "common/uuid.json" + }, + "userId": { + "description": "UUID of the user who performed the action", + "$ref": "common/uuid.json" + }, + "eventDate": { + "description": "Date time when event triggered", + "format": "date-time", + "type": "string" + }, + "actionDate": { + "description": "Date time when piece action occurred", + "format": "date-time", + "type": "string" + }, + "pieceSnapshot": { + "description": "Full snapshot of the piece", + "type": "object", + "javaType": "java.lang.Object" + } + }, + "additionalProperties": false +} diff --git a/ramls/piece_audit_event_collection.json b/ramls/piece_audit_event_collection.json new file mode 100644 index 00000000..6183d57d --- /dev/null +++ b/ramls/piece_audit_event_collection.json @@ -0,0 +1,25 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Collection of pieceLineAuditEvents", + "type": "object", + "additionalProperties": false, + "properties": { + "pieceAuditEvents": { + "description": "List of pieceAuditEvents", + "type": "array", + "id": "pieceAuditEventsList", + "items": { + "type": "object", + "$ref": "piece_audit_event.json" + } + }, + "totalItems": { + "description": "total records", + "type": "integer" + } + }, + "required": [ + "pieceAuditEvents", + "totalItems" + ] +} diff --git a/ramls/schemas/action_type.json b/ramls/schemas/action_type.json index 4f98445f..d21e62a8 100644 --- a/ramls/schemas/action_type.json +++ b/ramls/schemas/action_type.json @@ -39,6 +39,8 @@ "Request status changed", "Send", "Send error", + "Patron info added", + "Staff info added", "Staff information only added", "Transferred fully", "Transferred partially",