diff --git a/README.md b/README.md index 0f6354b..4388b3e 100644 --- a/README.md +++ b/README.md @@ -1,42 +1,147 @@ -# pagoPA Functions template +# pagoPA Receipt-pdf-helpdesk -Java template to create an Azure Function. +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=pagopa_pagopa-receipt-pdf-helpdesk&metric=alert_status)](https://sonarcloud.io/dashboard?id=pagopa_pagopa-receipt-pdf-helpdesk) -## Function examples -There is an example of a Http Trigger function. +Java Azure Functions that exposed the following recover APIs: +- ReceiptToReviewed +- RecoverFailedReceipt +- RecoverNotNotifiedReceipt --- -## Run locally with Docker -`docker build -t pagopa-functions-template .` +## Summary ๐Ÿ“– -`docker run -p 8999:80 pagopa-functions-template` +- [Api Documentation ๐Ÿ“–](#api-documentation-) +- [Start Project Locally ๐Ÿš€](#start-project-locally-) + * [Run locally with Docker](#run-locally-with-docker) + + [Prerequisites](#prerequisites) + + [Run docker container](#run-docker-container) + * [Run locally with Maven](#run-locally-with-maven) + + [Prerequisites](#prerequisites-1) + + [Set environment variables](#set-environment-variables) + + [Run the project](#run-the-project) + * [Test](#test) +- [Develop Locally ๐Ÿ’ป](#develop-locally-) + * [Prerequisites](#prerequisites-2) + * [Testing ๐Ÿงช](#testing-) + + [Unit testing](#unit-testing) + + [Integration testing](#integration-testing) + + [Performance testing](#performance-testing) +- [Contributors ๐Ÿ‘ฅ](#contributors-) + * [Maintainers](#maintainers) -### Test -`curl http://localhost:8999/example` +--- + +## Api Documentation ๐Ÿ“– + +See +the [OpenApi 3 here](https://editor.swagger.io/?url=https://raw.githubusercontent.com/pagopa/pagopa-receipt-pdf-helpdesk/main/openapi/openapi.json) + +## Start Project Locally ๐Ÿš€ + +### Run locally with Docker + +#### Prerequisites + +- docker + +#### Set environment variables + +`docker build -t pagopa-receip-pdf-helpdesk .` + +`cp .env.example .env` + +and replace in `.env` with correct values + +#### Run docker container + +then type : + +`docker run -p 80:80 --env-file=./.env pagopa-receip-pdf-helpdesk` + +### Run locally with Maven + +#### Prerequisites + +- maven + +#### Set environment variables -## Run locally with Maven +On terminal type: + +`cp local.settings.json.example local.settings.json` + +then replace env variables with correct values +(if there is NO default value, the variable HAS to be defined) + +| VARIABLE | USAGE | DEFAULT VALUE | +|----------------------------------------|---------------------------------------------------------------------------------------------------------------------------------|:------------------------------------------------------:| +| `RECEIPT_QUEUE_CONN_STRING` | Connection string to the Receipt Queue | | +| `RECEIPT_QUEUE_TOPIC` | Topic name of the Receipt Queue | | +| `COSMOS_BIZ_EVENT_CONN_STRING` | Connection string to the BizEvent CosmosDB | | +| `COSMOS_BIZ_EVENT_SERVICE_ENDPOINT` | Endpoint to the BizEvent CosmosDB | | +| `COSMOS_BIZ_EVENT_DB_NAME` | Database name of the BizEvent database in CosmosDB | | +| `COSMOS_BIZ_EVENT_CONTAINER_NAME` | Container name of the BizEvent container in CosmosDB | | +| `COSMOS_RECEIPTS_CONN_STRING` | Connection string to the Receipt CosmosDB | | +| `COSMOS_RECEIPT_SERVICE_ENDPOINT` | Endpoint to the Receipt CosmosDB | | +| `COSMOS_RECEIPT_KEY` | Key to the Receipt CosmosDB | | +| `COSMOS_RECEIPT_DB_NAME` | Database name of the Receipt database in CosmosDB | | +| `COSMOS_RECEIPT_CONTAINER_NAME` | Container name of the Receipt container in CosmosDB | | +| `COSMOS_RECEIPT_ERROR_CONTAINER_NAME` | Container name of the Receipt-message-error container in CosmosDB | | +| `PDV_TOKENIZER_BASE_PATH` | PDV Tokenizer API base path | "https://api.uat.tokenizer.pdv.pagopa.it/tokenizer/v1" | +| `PDV_TOKENIZER_SEARCH_TOKEN_ENDPOINT` | PDV Tokenizer API search token endpoint | "/tokens/search" | +| `PDV_TOKENIZER_FIND_PII_ENDPOINT` | PDV Tokenizer API find pii endpoint | "/tokens/%s/pii" | +| `PDV_TOKENIZER_CREATE_TOKEN_ENDPOINT` | PDV Tokenizer API create token endpoint | "/tokens" | +| `PDV_TOKENIZER_SUBSCRIPTION_KEY` | API azure ocp apim subscription key | | +| `PDV_TOKENIZER_INITIAL_INTERVAL` | PDV Tokenizer initial interval for retry a request that fail with 429 status code | 200 | +| `PDV_TOKENIZER_MULTIPLIER` | PDV Tokenizer interval multiplier for subsequent request retry | 2.0 | +| `PDV_TOKENIZER_RANDOMIZATION_FACTOR` | PDV Tokenizer randomization factor for interval retry calculation | 0.6 | +| `PDV_TOKENIZER_MAX_RETRIES` | PDV Tokenizer max request retry | 3 | +| `TOKENIZER_APIM_HEADER_KEY` | Tokenizer APIM header key | x-api-key | +| `MAX_DATE_DIFF_MILLIS` | Difference in millis between the current time and the date from witch the receipts will be fetched in massive recover operation | 360000 | + +> to doc details about AZ fn config +> see [here](https://stackoverflow.com/questions/62669672/azure-functions-what-is-the-purpose-of-having-host-json-and-local-settings-jso) + + +#### Run the project `mvn clean package` `mvn azure-functions:run` ### Test -`curl http://localhost:7071/example` + +`curl http://localhost:8080/info` --- +## Develop Locally ๐Ÿ’ป + +### Prerequisites + +- git +- maven +- jdk-17 + +### Testing ๐Ÿงช + +#### Unit testing + +To run the **Junit** tests: + +`mvn clean verify` + +#### Integration testing + +#### Performance testing + +--- + +## Contributors ๐Ÿ‘ฅ + +Made with โค๏ธ by PagoPa S.p.A. -## TODO -Once cloned the repo, you should: -- to deploy on standard Azure service: - - rename `deploy-pipelines-standard.yml` to `deploy-pipelines.yml` - - remove `helm` folder -- to deploy on Kubernetes: - - rename `deploy-pipelines-aks.yml` to `deploy-pipelines.yml` - - customize `helm` configuration -- configure the following GitHub action in `.github` folder: - - `deploy.yml` - - `sonar_analysis.yml` +### Maintainers -Configure the SonarCloud project :point_right: [guide](https://pagopa.atlassian.net/wiki/spaces/DEVOPS/pages/147193860/SonarCloud+experimental). \ No newline at end of file +See `CODEOWNERS` file \ No newline at end of file diff --git a/openapi/openapi.json b/openapi/openapi.json index 5b7b8d7..e38e583 100644 --- a/openapi/openapi.json +++ b/openapi/openapi.json @@ -233,6 +233,148 @@ } } ] + }, + "/recoverNotNotified": { + "put": { + "tags": [ + "Receipts REST APIs" + ], + "summary": "Recover a receipt, or group of, in IO_ERROR_TO_NOTIFY or GENERATED status", + "operationId": "recoverNotNotified", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotNotifiedRecoveryRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Succesfull Calls.", + "headers": { + "X-Request-Id": { + "description": "This header identifies the call", + "schema": { + "type": "string" + } + } + }, + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + }, + "400": { + "description": "Bad request invalid input.", + "headers": { + "X-Request-Id": { + "description": "This header identifies the call", + "schema": { + "type": "string" + } + } + }, + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + }, + "401": { + "description": "Wrong or missing function key.", + "headers": { + "X-Request-Id": { + "description": "This header identifies the call", + "schema": { + "type": "string" + } + } + } + }, + "404": { + "description": "Requested receipt not found.", + "headers": { + "X-Request-Id": { + "description": "This header identifies the call", + "schema": { + "type": "string" + } + } + }, + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + }, + "429": { + "description": "Too many requests.", + "headers": { + "X-Request-Id": { + "description": "This header identifies the call", + "schema": { + "type": "string" + } + } + } + }, + "500": { + "description": "The requested receipts is not in a status that can be elaborated.", + "headers": { + "X-Request-Id": { + "description": "This header identifies the call", + "schema": { + "type": "string" + } + } + }, + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + }, + "default": { + "description": "Unexpected error.", + "headers": { + "X-Request-Id": { + "description": "This header identifies the call", + "schema": { + "type": "string" + } + } + } + } + }, + "security": [ + { + "ApiKey": [] + } + ] + }, + "parameters": [ + { + "name": "X-Request-Id", + "in": "header", + "description": "This header identifies the call, if not passed it is self-generated. This ID is returned in the response.", + "schema": { + "type": "string" + } + } + ] } }, "components": { @@ -260,6 +402,24 @@ } } }, + "NotNotifiedRecoveryRequest": { + "type": "object", + "description": "The request body for recoverNotNotified API, at least one of generatedStatus or ioErrorToNotifyStatus must be true. The field eventId when not provided enable the massive operation", + "properties": { + "eventId": { + "type": "string", + "description": "Id of the event to start recovering (optional)" + }, + "generatedStatus": { + "type": "boolean", + "description": "True to recover the receipts in GENERATED status, false otherwise" + }, + "ioErrorToNotifyStatus": { + "type": "boolean", + "description": "True to recover the receipts in IO_ERROR_TO_NOTIFY status, false otherwise" + } + } + }, "ProblemJson": { "type": "object", "properties": { diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/RecoverFailedReceipt.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/RecoverFailedReceipt.java index cbbaa67..61d49d8 100644 --- a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/RecoverFailedReceipt.java +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/RecoverFailedReceipt.java @@ -163,7 +163,7 @@ private void getEvent(String eventId, ExecutionContext context, bizEventToReceiptService.handleSendMessageToQueue(bizEvent, receipt); if(receipt.getStatus() != ReceiptStatusType.NOT_QUEUE_SENT){ receipt.setStatus(ReceiptStatusType.INSERTED); - receipt.setInserted_at(System.currentTimeMillis()); + receipt.setInsertedAt(System.currentTimeMillis()); receipt.setReasonErr(null); receipt.setReasonErrPayer(null); } diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/RecoverNotNotifiedReceipt.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/RecoverNotNotifiedReceipt.java new file mode 100644 index 0000000..4204c66 --- /dev/null +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/RecoverNotNotifiedReceipt.java @@ -0,0 +1,185 @@ +package it.gov.pagopa.receipt.pdf.helpdesk; + +import com.azure.cosmos.models.FeedResponse; +import com.microsoft.azure.functions.ExecutionContext; +import com.microsoft.azure.functions.HttpMethod; +import com.microsoft.azure.functions.HttpRequestMessage; +import com.microsoft.azure.functions.HttpResponseMessage; +import com.microsoft.azure.functions.HttpStatus; +import com.microsoft.azure.functions.OutputBinding; +import com.microsoft.azure.functions.annotation.AuthorizationLevel; +import com.microsoft.azure.functions.annotation.CosmosDBOutput; +import com.microsoft.azure.functions.annotation.FunctionName; +import com.microsoft.azure.functions.annotation.HttpTrigger; +import it.gov.pagopa.receipt.pdf.helpdesk.client.ReceiptCosmosClient; +import it.gov.pagopa.receipt.pdf.helpdesk.client.impl.ReceiptCosmosClientImpl; +import it.gov.pagopa.receipt.pdf.helpdesk.entity.receipt.Receipt; +import it.gov.pagopa.receipt.pdf.helpdesk.entity.receipt.enumeration.ReceiptStatusType; +import it.gov.pagopa.receipt.pdf.helpdesk.exception.ReceiptNotFoundException; +import it.gov.pagopa.receipt.pdf.helpdesk.model.NotNotifiedRecoveryRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +/** + * Azure Functions with HTTP Trigger. + */ +public class RecoverNotNotifiedReceipt { + + private final Logger logger = LoggerFactory.getLogger(RecoverNotNotifiedReceipt.class); + + private final ReceiptCosmosClient receiptCosmosClient; + + public RecoverNotNotifiedReceipt() { + this.receiptCosmosClient = ReceiptCosmosClientImpl.getInstance(); + } + + RecoverNotNotifiedReceipt(ReceiptCosmosClient receiptCosmosClient) { + this.receiptCosmosClient = receiptCosmosClient; + } + + /** + * This function will be invoked when a Http Trigger occurs. + *

+ * It recovers the receipt with failed notification ({@link ReceiptStatusType#IO_ERROR_TO_NOTIFY}) or notification + * not triggered ({@link ReceiptStatusType#GENERATED} by clearing the errors and update the status to the + * previous step ({@link ReceiptStatusType#GENERATED}). + *

+ * If invoked with a specific eventId it restore the associated receipt, otherwise it restore all receipt with status + * {@link ReceiptStatusType#IO_ERROR_TO_NOTIFY}. + * + * @return response with {@link HttpStatus#OK} if the notification succeeded + */ + @FunctionName("RecoverNotNotifiedReceipt") + public HttpResponseMessage run( + @HttpTrigger(name = "RecoverNotNotifiedTrigger", + methods = {HttpMethod.PUT}, + route = "recoverNotNotified", + authLevel = AuthorizationLevel.FUNCTION) + HttpRequestMessage> request, + @CosmosDBOutput( + name = "ReceiptDatastore", + databaseName = "db", + collectionName = "receipts", + connectionStringSetting = "COSMOS_RECEIPTS_CONN_STRING") + OutputBinding> documentReceipts, + final ExecutionContext context) { + logger.info("[{}] function called at {}", context.getFunctionName(), LocalDateTime.now()); + + Optional recoveryRequestOptional = request.getBody(); + if (recoveryRequestOptional.isEmpty()) { + return request.createResponseBuilder(HttpStatus.BAD_REQUEST).body("Please pass a valid body").build(); + } + NotNotifiedRecoveryRequest recoveryRequest = recoveryRequestOptional.get(); + + List statusToRestore = getStatusToRestore(recoveryRequest); + if (statusToRestore.isEmpty()) { + return request.createResponseBuilder(HttpStatus.BAD_REQUEST).body("Please select at least one status to recover").build(); + } + + String eventId = recoveryRequest.getEventId(); + if (eventId != null) { + Receipt receipt; + try { + receipt = getReceipt(eventId); + } catch (ReceiptNotFoundException e) { + String responseMsg = String.format("Unable to retrieve the receipt with eventId %s", eventId); + logger.error("[{}] {}", context.getFunctionName(), responseMsg, e); + return request.createResponseBuilder(HttpStatus.NOT_FOUND).body(responseMsg).build(); + } + + if (!statusToRestore.contains(receipt.getStatus())) { + String responseMsg = String.format("The requested receipt with eventId %s is not in the expected status", + receipt.getEventId()); + return request.createResponseBuilder(HttpStatus.INTERNAL_SERVER_ERROR).body(responseMsg).build(); + } + + Receipt restoredReceipt = restoreReceipt(receipt); + + documentReceipts.setValue(Collections.singletonList(restoredReceipt)); + String responseMsg = String.format("Receipt with id %s and eventId %s restored int status %s with success", + receipt.getId(), receipt.getEventId(), ReceiptStatusType.GENERATED); + return request.createResponseBuilder(HttpStatus.OK).body(responseMsg).build(); + } + + List receiptList = receiptMassiveRestore(recoveryRequest); + if (receiptList.isEmpty()) { + return request.createResponseBuilder(HttpStatus.OK).body("No receipts restored").build(); + } + + documentReceipts.setValue(receiptList); + String msg = String.format("Restored %s receipt with success", receiptList.size()); + return request.createResponseBuilder(HttpStatus.OK).body(msg).build(); + } + + private List getStatusToRestore(NotNotifiedRecoveryRequest recoveryRequest) { + List statusToRestore = new ArrayList<>(); + if (recoveryRequest.isGeneratedStatus()) { + statusToRestore.add(ReceiptStatusType.GENERATED); + } + if (recoveryRequest.isIoErrorToNotifyStatus()) { + statusToRestore.add(ReceiptStatusType.IO_ERROR_TO_NOTIFY); + } + return statusToRestore; + } + + private List receiptMassiveRestore(NotNotifiedRecoveryRequest recoveryRequest) { + List receiptList = new ArrayList<>(); + String continuationToken = null; + do { + Iterable> feedResponseIterator = + receiptCosmosClient + .getNotNotifiedReceiptDocuments( + continuationToken, + 100, + recoveryRequest.isIoErrorToNotifyStatus(), + recoveryRequest.isGeneratedStatus() + ); + + for (FeedResponse page : feedResponseIterator) { + for (Receipt receipt : page.getResults()) { + Receipt restoredReceipt = restoreReceipt(receipt); + receiptList.add(restoredReceipt); + } + continuationToken = page.getContinuationToken(); + } + } while (continuationToken != null); + return receiptList; + } + + private Receipt restoreReceipt(Receipt receipt) { + receipt.setStatus(ReceiptStatusType.GENERATED); + receipt.setNotificationNumRetry(0); + receipt.setNotifiedAt(0); + + if (receipt.getReasonErr() != null) { + receipt.setReasonErr(null); + } + if (receipt.getReasonErrPayer() != null) { + receipt.setReasonErrPayer(null); + } + return receipt; + } + + //Retrieve receipt from CosmosDB + private Receipt getReceipt(String eventId) throws ReceiptNotFoundException { + Receipt receipt; + try { + receipt = receiptCosmosClient.getReceiptDocument(eventId); + } catch (ReceiptNotFoundException e) { + String errorMsg = String.format("Receipt not found with the biz-event id %s", eventId); + throw new ReceiptNotFoundException(errorMsg, e); + } + + if (receipt == null) { + String errorMsg = String.format("Receipt retrieved with the biz-event id %s is null", eventId); + throw new ReceiptNotFoundException(errorMsg); + } + return receipt; + } +} diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/client/ReceiptCosmosClient.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/client/ReceiptCosmosClient.java index ff3a251..18067fc 100644 --- a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/client/ReceiptCosmosClient.java +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/client/ReceiptCosmosClient.java @@ -4,11 +4,12 @@ import com.azure.cosmos.models.FeedResponse; import it.gov.pagopa.receipt.pdf.helpdesk.entity.receipt.Receipt; import it.gov.pagopa.receipt.pdf.helpdesk.entity.receipt.ReceiptError; +import it.gov.pagopa.receipt.pdf.helpdesk.entity.receipt.enumeration.ReceiptStatusType; import it.gov.pagopa.receipt.pdf.helpdesk.exception.ReceiptNotFoundException; public interface ReceiptCosmosClient { - Receipt getReceiptDocument(String receiptId) throws ReceiptNotFoundException; + Receipt getReceiptDocument(String eventId) throws ReceiptNotFoundException; Iterable> getFailedReceiptDocuments(String continuationToken, Integer pageSize); @@ -17,4 +18,21 @@ public interface ReceiptCosmosClient { ReceiptError getReceiptError(String bizEventId) throws ReceiptNotFoundException; Iterable> getToReviewReceiptsError(String continuationToken, Integer pageSize); + + /** + * Retrieve the receipt documents with status {@link ReceiptStatusType#IO_ERROR_TO_NOTIFY} + * or {@link ReceiptStatusType#GENERATED} from Cosmos database + * + * @param continuationToken Paged query continuation token + * @param pageSize the page size + * @param ioErrorToNotifyStatus true if the receipts must be in {@link ReceiptStatusType#IO_ERROR_TO_NOTIFY} status, false otherwise + * @param generatedStatus true if the receipts must be in {@link ReceiptStatusType#GENERATED} status, false otherwise + * @return receipt documents + */ + Iterable> getNotNotifiedReceiptDocuments( + String continuationToken, + Integer pageSize, + boolean ioErrorToNotifyStatus, + boolean generatedStatus + ); } diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/client/impl/ReceiptCosmosClientImpl.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/client/impl/ReceiptCosmosClientImpl.java index 76871f9..e0c3f84 100644 --- a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/client/impl/ReceiptCosmosClientImpl.java +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/client/impl/ReceiptCosmosClientImpl.java @@ -4,12 +4,15 @@ import com.azure.cosmos.CosmosClientBuilder; import com.azure.cosmos.CosmosContainer; import com.azure.cosmos.CosmosDatabase; -import com.azure.cosmos.models.*; +import com.azure.cosmos.models.CosmosItemResponse; +import com.azure.cosmos.models.CosmosQueryRequestOptions; +import com.azure.cosmos.models.FeedResponse; import com.azure.cosmos.util.CosmosPagedIterable; import it.gov.pagopa.receipt.pdf.helpdesk.client.ReceiptCosmosClient; import it.gov.pagopa.receipt.pdf.helpdesk.entity.receipt.Receipt; import it.gov.pagopa.receipt.pdf.helpdesk.entity.receipt.ReceiptError; import it.gov.pagopa.receipt.pdf.helpdesk.entity.receipt.enumeration.ReceiptErrorStatusType; +import it.gov.pagopa.receipt.pdf.helpdesk.entity.receipt.enumeration.ReceiptStatusType; import it.gov.pagopa.receipt.pdf.helpdesk.exception.ReceiptNotFoundException; import java.time.OffsetDateTime; @@ -47,7 +50,6 @@ public static ReceiptCosmosClientImpl getInstance() { if (instance == null) { instance = new ReceiptCosmosClientImpl(); } - return instance; } @@ -60,7 +62,6 @@ public static ReceiptCosmosClientImpl getInstance() { */ public Receipt getReceiptDocument(String eventId) throws ReceiptNotFoundException { CosmosDatabase cosmosDatabase = this.cosmosClient.getDatabase(databaseId); - CosmosContainer cosmosContainer = cosmosDatabase.getContainer(containerId); //Build query @@ -75,7 +76,6 @@ public Receipt getReceiptDocument(String eventId) throws ReceiptNotFoundExceptio } else { throw new ReceiptNotFoundException("Document not found in the defined container"); } - } /** @@ -87,7 +87,6 @@ public Receipt getReceiptDocument(String eventId) throws ReceiptNotFoundExceptio @Override public Iterable> getFailedReceiptDocuments(String continuationToken, Integer pageSize) { CosmosDatabase cosmosDatabase = this.cosmosClient.getDatabase(databaseId); - CosmosContainer cosmosContainer = cosmosDatabase.getContainer(containerId); //Build query @@ -99,7 +98,6 @@ public Iterable> getFailedReceiptDocuments(String continua return cosmosContainer .queryItems(query, new CosmosQueryRequestOptions(), Receipt.class) .iterableByPage(continuationToken,pageSize); - } /** @@ -111,7 +109,6 @@ public Iterable> getFailedReceiptDocuments(String continua @Override public CosmosItemResponse saveReceipts(Receipt receipt) { CosmosDatabase cosmosDatabase = this.cosmosClient.getDatabase(databaseId); - CosmosContainer cosmosContainer = cosmosDatabase.getContainer(containerId); return cosmosContainer.createItem(receipt); @@ -165,4 +162,44 @@ public Iterable> getToReviewReceiptsError(String cont .iterableByPage(continuationToken,pageSize); } + /** + * {@inheritDoc} + */ + @Override + public Iterable> getNotNotifiedReceiptDocuments( + String continuationToken, + Integer pageSize, + boolean ioErrorToNotifyStatus, + boolean generatedStatus + ) { + CosmosDatabase cosmosDatabase = this.cosmosClient.getDatabase(databaseId); + CosmosContainer cosmosContainer = cosmosDatabase.getContainer(containerId); + + if (!ioErrorToNotifyStatus && !generatedStatus) { + throw new IllegalArgumentException("at least one param must be true"); + } + + //Build query + String query = buildQuery(ioErrorToNotifyStatus, generatedStatus); + + //Query the container + return cosmosContainer + .queryItems(query, new CosmosQueryRequestOptions(), Receipt.class) + .iterableByPage(continuationToken,pageSize); + } + + private String buildQuery(boolean ioErrorToNotifyStatus, boolean generatedStatus) { + String query = "SELECT *CosmosPagedIterable FROM c WHERE "; + String ioErrorNotifyParam = String.format("c.status = '%s'", ReceiptStatusType.IO_ERROR_TO_NOTIFY); + String generatedParam = String.format("(c.status= = '%s' AND ( %s - c.inserted_at) >= %s)", + ReceiptStatusType.GENERATED, OffsetDateTime.now().toInstant().toEpochMilli(), millisDiff); + + if (ioErrorToNotifyStatus && generatedStatus) { + return query.concat(ioErrorNotifyParam).concat(" AND ").concat(generatedParam); + } + if (ioErrorToNotifyStatus) { + return query.concat(ioErrorNotifyParam); + } + return query.concat(generatedParam); + } } diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/receipt/CartItem.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/receipt/CartItem.java index a9387b8..2eb2431 100644 --- a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/receipt/CartItem.java +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/receipt/CartItem.java @@ -1,5 +1,7 @@ package it.gov.pagopa.receipt.pdf.helpdesk.entity.receipt; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -7,6 +9,8 @@ @Getter @Setter @NoArgsConstructor +@AllArgsConstructor +@Builder public class CartItem { private String subject; private String payeeName; diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/receipt/EventData.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/receipt/EventData.java index 0fd5512..44b9e3b 100644 --- a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/receipt/EventData.java +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/receipt/EventData.java @@ -1,5 +1,7 @@ package it.gov.pagopa.receipt.pdf.helpdesk.entity.receipt; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -9,6 +11,8 @@ @Getter @Setter @NoArgsConstructor +@AllArgsConstructor +@Builder public class EventData { private String payerFiscalCode; private String debtorFiscalCode; diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/receipt/IOMessageData.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/receipt/IOMessageData.java index 2e47de3..fb4f494 100644 --- a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/receipt/IOMessageData.java +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/receipt/IOMessageData.java @@ -1,5 +1,7 @@ package it.gov.pagopa.receipt.pdf.helpdesk.entity.receipt; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -7,6 +9,8 @@ @Getter @Setter @NoArgsConstructor +@AllArgsConstructor +@Builder public class IOMessageData { private String idMessageDebtor; private String idMessagePayer; diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/receipt/ReasonError.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/receipt/ReasonError.java index a81e9bd..e243420 100644 --- a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/receipt/ReasonError.java +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/receipt/ReasonError.java @@ -1,6 +1,7 @@ package it.gov.pagopa.receipt.pdf.helpdesk.entity.receipt; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -9,6 +10,7 @@ @Setter @NoArgsConstructor @AllArgsConstructor +@Builder public class ReasonError { private int code; private String message; diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/receipt/Receipt.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/receipt/Receipt.java index c38cf4d..fbf934e 100644 --- a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/receipt/Receipt.java +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/receipt/Receipt.java @@ -1,6 +1,9 @@ package it.gov.pagopa.receipt.pdf.helpdesk.entity.receipt; +import com.fasterxml.jackson.annotation.JsonProperty; import it.gov.pagopa.receipt.pdf.helpdesk.entity.receipt.enumeration.ReceiptStatusType; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -8,6 +11,8 @@ @Getter @Setter @NoArgsConstructor +@AllArgsConstructor +@Builder public class Receipt { private String eventId; @@ -21,8 +26,12 @@ public class Receipt { private int numRetry; private ReasonError reasonErr; private ReasonError reasonErrPayer; - private long inserted_at; - private long generated_at; - private long notified_at; + private int notificationNumRetry; + @JsonProperty("inserted_at") + private long insertedAt; + @JsonProperty("generated_at") + private long generatedAt; + @JsonProperty("notified_at") + private long notifiedAt; } diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/receipt/ReceiptMetadata.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/receipt/ReceiptMetadata.java index 2554e1c..cd2eda0 100644 --- a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/receipt/ReceiptMetadata.java +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/receipt/ReceiptMetadata.java @@ -1,5 +1,7 @@ package it.gov.pagopa.receipt.pdf.helpdesk.entity.receipt; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -7,6 +9,8 @@ @Getter @Setter @NoArgsConstructor +@AllArgsConstructor +@Builder public class ReceiptMetadata { private String name; diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/model/NotNotifiedRecoveryRequest.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/model/NotNotifiedRecoveryRequest.java new file mode 100644 index 0000000..93058a3 --- /dev/null +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/model/NotNotifiedRecoveryRequest.java @@ -0,0 +1,16 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class NotNotifiedRecoveryRequest { + + private String eventId; + private boolean generatedStatus; + private boolean ioErrorToNotifyStatus; + +} \ No newline at end of file diff --git a/src/test/java/it/gov/pagopa/receipt/pdf/helpdesk/RecoverNotNotifiedReceiptTest.java b/src/test/java/it/gov/pagopa/receipt/pdf/helpdesk/RecoverNotNotifiedReceiptTest.java new file mode 100644 index 0000000..9ea871c --- /dev/null +++ b/src/test/java/it/gov/pagopa/receipt/pdf/helpdesk/RecoverNotNotifiedReceiptTest.java @@ -0,0 +1,422 @@ +package it.gov.pagopa.receipt.pdf.helpdesk; + +import com.azure.cosmos.models.FeedResponse; +import com.microsoft.azure.functions.ExecutionContext; +import com.microsoft.azure.functions.HttpRequestMessage; +import com.microsoft.azure.functions.HttpResponseMessage; +import com.microsoft.azure.functions.HttpStatus; +import com.microsoft.azure.functions.OutputBinding; +import it.gov.pagopa.receipt.pdf.helpdesk.client.ReceiptCosmosClient; +import it.gov.pagopa.receipt.pdf.helpdesk.entity.receipt.ReasonError; +import it.gov.pagopa.receipt.pdf.helpdesk.entity.receipt.Receipt; +import it.gov.pagopa.receipt.pdf.helpdesk.entity.receipt.enumeration.ReceiptStatusType; +import it.gov.pagopa.receipt.pdf.helpdesk.exception.ReceiptNotFoundException; +import it.gov.pagopa.receipt.pdf.helpdesk.model.NotNotifiedRecoveryRequest; +import it.gov.pagopa.receipt.pdf.helpdesk.util.HttpResponseMessageMock; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.stubbing.Answer; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class RecoverNotNotifiedReceiptTest { + + private static final String EVENT_ID = "eventId"; + + private final ExecutionContext executionContextMock = mock(ExecutionContext.class); + + @Mock + private ReceiptCosmosClient receiptCosmosClientMock; + + @Spy + OutputBinding> documentReceipts; + + @Captor + private ArgumentCaptor> receiptCaptor; + + private RecoverNotNotifiedReceipt sut; + + private AutoCloseable closeable; + + @BeforeEach + public void openMocks() { + closeable = MockitoAnnotations.openMocks(this); + sut = spy(new RecoverNotNotifiedReceipt(receiptCosmosClientMock)); + } + + @AfterEach + public void releaseMocks() throws Exception { + closeable.close(); + } + + @Test + void recoverNotNotifiedReceiptWithEventIdSuccess() throws ReceiptNotFoundException { + NotNotifiedRecoveryRequest recoveryRequest = new NotNotifiedRecoveryRequest(EVENT_ID, false, true); + + @SuppressWarnings("unchecked") + HttpRequestMessage> request = mock(HttpRequestMessage.class); + when(request.getBody()).thenReturn(Optional.of(recoveryRequest)); + + Receipt receipt = buildReceipt(); + when(receiptCosmosClientMock.getReceiptDocument(EVENT_ID)).thenReturn(receipt); + + doAnswer((Answer) invocation -> { + HttpStatus status = (HttpStatus) invocation.getArguments()[0]; + return new HttpResponseMessageMock.HttpResponseMessageBuilderMock().status(status); + }).when(request).createResponseBuilder(any(HttpStatus.class)); + + // test execution + HttpResponseMessage response = sut.run(request, documentReceipts, executionContextMock); + + // test assertion + assertNotNull(response); + assertEquals(HttpStatus.OK, response.getStatus()); + assertNotNull(response.getBody()); + + verify(documentReceipts).setValue(receiptCaptor.capture()); + + assertEquals(1, receiptCaptor.getValue().size()); + Receipt captured = receiptCaptor.getValue().get(0); + assertEquals(ReceiptStatusType.GENERATED, captured.getStatus()); + assertEquals(EVENT_ID, captured.getEventId()); + assertEquals(0, captured.getNotificationNumRetry()); + assertNull(captured.getReasonErr()); + assertNull(captured.getReasonErrPayer()); + } + + @Test + void recoverNotNotifiedReceiptWithoutEventIdSuccess() { + NotNotifiedRecoveryRequest recoveryRequest = new NotNotifiedRecoveryRequest(null, false, true); + + @SuppressWarnings("unchecked") + HttpRequestMessage> request = mock(HttpRequestMessage.class); + when(request.getBody()).thenReturn(Optional.of(recoveryRequest)); + + FeedResponse feedResponseMock = mock(FeedResponse.class); + List receiptList = getReceiptList(); + when(feedResponseMock.getResults()).thenReturn(receiptList); + when(receiptCosmosClientMock.getNotNotifiedReceiptDocuments(any(), any(), anyBoolean(), anyBoolean())) + .thenReturn(Collections.singletonList(feedResponseMock)); + + doAnswer((Answer) invocation -> { + HttpStatus status = (HttpStatus) invocation.getArguments()[0]; + return new HttpResponseMessageMock.HttpResponseMessageBuilderMock().status(status); + }).when(request).createResponseBuilder(any(HttpStatus.class)); + + // test execution + HttpResponseMessage response = sut.run(request, documentReceipts, executionContextMock); + + // test assertion + assertNotNull(response); + assertEquals(HttpStatus.OK, response.getStatus()); + assertNotNull(response.getBody()); + + verify(documentReceipts).setValue(receiptCaptor.capture()); + + assertEquals(receiptList.size(), receiptCaptor.getValue().size()); + Receipt captured1 = receiptCaptor.getValue().get(0); + assertEquals(ReceiptStatusType.GENERATED, captured1.getStatus()); + assertEquals(EVENT_ID, captured1.getEventId()); + assertEquals(0, captured1.getNotificationNumRetry()); + assertNull(captured1.getReasonErr()); + assertNull(captured1.getReasonErrPayer()); + Receipt captured2 = receiptCaptor.getValue().get(0); + assertEquals(ReceiptStatusType.GENERATED, captured2.getStatus()); + assertEquals(EVENT_ID, captured2.getEventId()); + assertEquals(0, captured2.getNotificationNumRetry()); + assertNull(captured2.getReasonErr()); + assertNull(captured2.getReasonErrPayer()); + } + + @Test + void recoverNotNotifiedReceiptWithoutEventIdSuccessWithNoReceiptUpdated() { + NotNotifiedRecoveryRequest recoveryRequest = new NotNotifiedRecoveryRequest(null, false, true); + + @SuppressWarnings("unchecked") + HttpRequestMessage> request = mock(HttpRequestMessage.class); + when(request.getBody()).thenReturn(Optional.of(recoveryRequest)); + + FeedResponse feedResponseMock = mock(FeedResponse.class); + when(feedResponseMock.getResults()).thenReturn(Collections.emptyList()); + when(receiptCosmosClientMock.getNotNotifiedReceiptDocuments(any(), any(), anyBoolean(), anyBoolean())) + .thenReturn(Collections.singletonList(feedResponseMock)); + + doAnswer((Answer) invocation -> { + HttpStatus status = (HttpStatus) invocation.getArguments()[0]; + return new HttpResponseMessageMock.HttpResponseMessageBuilderMock().status(status); + }).when(request).createResponseBuilder(any(HttpStatus.class)); + + // test execution + HttpResponseMessage response = sut.run(request, documentReceipts, executionContextMock); + + // test assertion + assertNotNull(response); + assertEquals(HttpStatus.OK, response.getStatus()); + assertNotNull(response.getBody()); + + verify(documentReceipts, never()).setValue(receiptCaptor.capture()); + } + + @Test + void recoverReceiptFailForEmptyBody() { + @SuppressWarnings("unchecked") + HttpRequestMessage> request = mock(HttpRequestMessage.class); + when(request.getBody()).thenReturn(Optional.empty()); + + doAnswer((Answer) invocation -> { + HttpStatus status = (HttpStatus) invocation.getArguments()[0]; + return new HttpResponseMessageMock.HttpResponseMessageBuilderMock().status(status); + }).when(request).createResponseBuilder(any(HttpStatus.class)); + + // test execution + HttpResponseMessage response = sut.run(request, documentReceipts, executionContextMock); + + // test assertion + assertNotNull(response); + assertEquals(HttpStatus.BAD_REQUEST, response.getStatus()); + assertNotNull(response.getBody()); + + verify(documentReceipts, never()).setValue(receiptCaptor.capture()); + } + + @Test + void recoverReceiptFailForInvalidInputParams() { + NotNotifiedRecoveryRequest recoveryRequest = new NotNotifiedRecoveryRequest(EVENT_ID, false, false); + + @SuppressWarnings("unchecked") + HttpRequestMessage> request = mock(HttpRequestMessage.class); + when(request.getBody()).thenReturn(Optional.of(recoveryRequest)); + + doAnswer((Answer) invocation -> { + HttpStatus status = (HttpStatus) invocation.getArguments()[0]; + return new HttpResponseMessageMock.HttpResponseMessageBuilderMock().status(status); + }).when(request).createResponseBuilder(any(HttpStatus.class)); + + // test execution + HttpResponseMessage response = sut.run(request, documentReceipts, executionContextMock); + + // test assertion + assertNotNull(response); + assertEquals(HttpStatus.BAD_REQUEST, response.getStatus()); + assertNotNull(response.getBody()); + + verify(documentReceipts, never()).setValue(receiptCaptor.capture()); + } + + @Test + void recoverReceiptFailReceiptNotFound() throws ReceiptNotFoundException { + NotNotifiedRecoveryRequest recoveryRequest = new NotNotifiedRecoveryRequest(EVENT_ID, false, true); + + @SuppressWarnings("unchecked") + HttpRequestMessage> request = mock(HttpRequestMessage.class); + when(request.getBody()).thenReturn(Optional.of(recoveryRequest)); + + when(receiptCosmosClientMock.getReceiptDocument(EVENT_ID)).thenThrow(ReceiptNotFoundException.class); + + doAnswer((Answer) invocation -> { + HttpStatus status = (HttpStatus) invocation.getArguments()[0]; + return new HttpResponseMessageMock.HttpResponseMessageBuilderMock().status(status); + }).when(request).createResponseBuilder(any(HttpStatus.class)); + + // test execution + HttpResponseMessage response = sut.run(request, documentReceipts, executionContextMock); + + // test assertion + assertNotNull(response); + assertEquals(HttpStatus.NOT_FOUND, response.getStatus()); + assertNotNull(response.getBody()); + + verify(documentReceipts, never()).setValue(receiptCaptor.capture()); + } + + @Test + void recoverReceiptFailReceiptFoundIsNull() throws ReceiptNotFoundException { + NotNotifiedRecoveryRequest recoveryRequest = new NotNotifiedRecoveryRequest(EVENT_ID, false, true); + + @SuppressWarnings("unchecked") + HttpRequestMessage> request = mock(HttpRequestMessage.class); + when(request.getBody()).thenReturn(Optional.of(recoveryRequest)); + + when(receiptCosmosClientMock.getReceiptDocument(EVENT_ID)).thenReturn(null); + + doAnswer((Answer) invocation -> { + HttpStatus status = (HttpStatus) invocation.getArguments()[0]; + return new HttpResponseMessageMock.HttpResponseMessageBuilderMock().status(status); + }).when(request).createResponseBuilder(any(HttpStatus.class)); + + // test execution + HttpResponseMessage response = sut.run(request, documentReceipts, executionContextMock); + + // test assertion + assertNotNull(response); + assertEquals(HttpStatus.NOT_FOUND, response.getStatus()); + assertNotNull(response.getBody()); + + verify(documentReceipts, never()).setValue(receiptCaptor.capture()); + } + + @Test + void recoverReceiptFailReceiptInGeneratedButOnlyIOErrorToNotify() throws ReceiptNotFoundException { + NotNotifiedRecoveryRequest recoveryRequest = new NotNotifiedRecoveryRequest(EVENT_ID, false, true); + + @SuppressWarnings("unchecked") + HttpRequestMessage> request = mock(HttpRequestMessage.class); + when(request.getBody()).thenReturn(Optional.of(recoveryRequest)); + + Receipt receipt = new Receipt(); + receipt.setStatus(ReceiptStatusType.GENERATED); + when(receiptCosmosClientMock.getReceiptDocument(EVENT_ID)).thenReturn(receipt); + + doAnswer((Answer) invocation -> { + HttpStatus status = (HttpStatus) invocation.getArguments()[0]; + return new HttpResponseMessageMock.HttpResponseMessageBuilderMock().status(status); + }).when(request).createResponseBuilder(any(HttpStatus.class)); + + // test execution + HttpResponseMessage response = sut.run(request, documentReceipts, executionContextMock); + + // test assertion + assertNotNull(response); + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatus()); + assertNotNull(response.getBody()); + + verify(documentReceipts, never()).setValue(receiptCaptor.capture()); + } + + @Test + void recoverReceiptFailReceiptInIOErrorToNotifyButOnlyGenerated() throws ReceiptNotFoundException { + NotNotifiedRecoveryRequest recoveryRequest = new NotNotifiedRecoveryRequest(EVENT_ID, true, false); + + @SuppressWarnings("unchecked") + HttpRequestMessage> request = mock(HttpRequestMessage.class); + when(request.getBody()).thenReturn(Optional.of(recoveryRequest)); + + Receipt receipt = new Receipt(); + receipt.setStatus(ReceiptStatusType.IO_ERROR_TO_NOTIFY); + when(receiptCosmosClientMock.getReceiptDocument(EVENT_ID)).thenReturn(receipt); + + doAnswer((Answer) invocation -> { + HttpStatus status = (HttpStatus) invocation.getArguments()[0]; + return new HttpResponseMessageMock.HttpResponseMessageBuilderMock().status(status); + }).when(request).createResponseBuilder(any(HttpStatus.class)); + + // test execution + HttpResponseMessage response = sut.run(request, documentReceipts, executionContextMock); + + // test assertion + assertNotNull(response); + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatus()); + assertNotNull(response.getBody()); + + verify(documentReceipts, never()).setValue(receiptCaptor.capture()); + } + + @Test + void recoverReceiptFailReceiptInInsertedButOnlyGenerated() throws ReceiptNotFoundException { + NotNotifiedRecoveryRequest recoveryRequest = new NotNotifiedRecoveryRequest(EVENT_ID, true, false); + + @SuppressWarnings("unchecked") + HttpRequestMessage> request = mock(HttpRequestMessage.class); + when(request.getBody()).thenReturn(Optional.of(recoveryRequest)); + + Receipt receipt = new Receipt(); + receipt.setStatus(ReceiptStatusType.INSERTED); + when(receiptCosmosClientMock.getReceiptDocument(EVENT_ID)).thenReturn(receipt); + + doAnswer((Answer) invocation -> { + HttpStatus status = (HttpStatus) invocation.getArguments()[0]; + return new HttpResponseMessageMock.HttpResponseMessageBuilderMock().status(status); + }).when(request).createResponseBuilder(any(HttpStatus.class)); + + // test execution + HttpResponseMessage response = sut.run(request, documentReceipts, executionContextMock); + + // test assertion + assertNotNull(response); + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatus()); + assertNotNull(response.getBody()); + + verify(documentReceipts, never()).setValue(receiptCaptor.capture()); + } + + @Test + void recoverReceiptFailReceiptInInsertedButOnlyIOErrorToNotify() throws ReceiptNotFoundException { + NotNotifiedRecoveryRequest recoveryRequest = new NotNotifiedRecoveryRequest(EVENT_ID, false, true); + + @SuppressWarnings("unchecked") + HttpRequestMessage> request = mock(HttpRequestMessage.class); + when(request.getBody()).thenReturn(Optional.of(recoveryRequest)); + + Receipt receipt = new Receipt(); + receipt.setStatus(ReceiptStatusType.INSERTED); + when(receiptCosmosClientMock.getReceiptDocument(EVENT_ID)).thenReturn(receipt); + + doAnswer((Answer) invocation -> { + HttpStatus status = (HttpStatus) invocation.getArguments()[0]; + return new HttpResponseMessageMock.HttpResponseMessageBuilderMock().status(status); + }).when(request).createResponseBuilder(any(HttpStatus.class)); + + // test execution + HttpResponseMessage response = sut.run(request, documentReceipts, executionContextMock); + + // test assertion + assertNotNull(response); + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatus()); + assertNotNull(response.getBody()); + + verify(documentReceipts, never()).setValue(receiptCaptor.capture()); + } + + private Receipt buildReceipt() { + return Receipt.builder() + .eventId(EVENT_ID) + .status(ReceiptStatusType.IO_ERROR_TO_NOTIFY) + .reasonErr(ReasonError.builder() + .code(500) + .message("error message") + .build()) + .reasonErrPayer(ReasonError.builder() + .code(500) + .message("error message") + .build()) + .numRetry(0) + .notificationNumRetry(6) + .insertedAt(0) + .generatedAt(0) + .notifiedAt(0) + .build(); + } + + private List getReceiptList() { + List receiptList = new ArrayList<>(); + Receipt receipt1 = buildReceipt(); + Receipt receipt2 = buildReceipt(); + receiptList.add(receipt1); + receiptList.add(receipt2); + return receiptList; + } +} \ No newline at end of file diff --git a/src/test/java/it/gov/pagopa/receipt/pdf/helpdesk/client/impl/ReceiptCosmosClientImplTest.java b/src/test/java/it/gov/pagopa/receipt/pdf/helpdesk/client/impl/ReceiptCosmosClientImplTest.java index ea640db..fd3b42d 100644 --- a/src/test/java/it/gov/pagopa/receipt/pdf/helpdesk/client/impl/ReceiptCosmosClientImplTest.java +++ b/src/test/java/it/gov/pagopa/receipt/pdf/helpdesk/client/impl/ReceiptCosmosClientImplTest.java @@ -6,34 +6,47 @@ import com.azure.cosmos.util.CosmosPagedIterable; import it.gov.pagopa.receipt.pdf.helpdesk.entity.receipt.Receipt; import it.gov.pagopa.receipt.pdf.helpdesk.exception.ReceiptNotFoundException; -import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.util.Iterator; -import static org.mockito.ArgumentMatchers.*; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static uk.org.webcompere.systemstubs.SystemStubs.withEnvironmentVariables; class ReceiptCosmosClientImplTest { + private static final String RECEIPT_ID = "a valid receipt id"; + + private CosmosClient mockClient; + + private ReceiptCosmosClientImpl client; + + @BeforeEach + void setUp() { + mockClient = mock(CosmosClient.class); + + client = new ReceiptCosmosClientImpl(mockClient); + } + @Test void testSingletonConnectionError() throws Exception { String mockKey = "mockKeymockKeymockKeymockKeymockKeymockKeymockKeymockKeymockKeymockKeymockKeymockKeyMK=="; withEnvironmentVariables( "COSMOS_RECEIPT_KEY", mockKey, "COSMOS_RECEIPT_SERVICE_ENDPOINT", "" - ).execute(() -> Assertions.assertThrows(IllegalArgumentException.class, ReceiptCosmosClientImpl::getInstance) - ); + ).execute(() -> assertThrows(IllegalArgumentException.class, ReceiptCosmosClientImpl::getInstance)); } @Test - void runOk() throws ReceiptNotFoundException { - String RECEIPT_ID = "a valid receipt id"; - - CosmosClient mockClient = mock(CosmosClient.class); - + void runOk() { CosmosDatabase mockDatabase = mock(CosmosDatabase.class); CosmosContainer mockContainer = mock(CosmosContainer.class); @@ -54,18 +67,13 @@ void runOk() throws ReceiptNotFoundException { when(mockDatabase.getContainer(any())).thenReturn(mockContainer); when(mockClient.getDatabase(any())).thenReturn(mockDatabase); - ReceiptCosmosClientImpl client = new ReceiptCosmosClientImpl(mockClient); - - Assertions.assertDoesNotThrow(() -> client.getReceiptDocument(RECEIPT_ID)); + Receipt receiptResponse = assertDoesNotThrow(() -> client.getReceiptDocument(RECEIPT_ID)); - Receipt receiptResponse = client.getReceiptDocument(RECEIPT_ID); - Assertions.assertEquals(RECEIPT_ID, receiptResponse.getId()); + assertEquals(RECEIPT_ID, receiptResponse.getId()); } @Test void runKo() { - CosmosClient mockClient = mock(CosmosClient.class); - CosmosDatabase mockDatabase = mock(CosmosDatabase.class); CosmosContainer mockContainer = mock(CosmosContainer.class); @@ -83,17 +91,11 @@ void runKo() { when(mockDatabase.getContainer(any())).thenReturn(mockContainer); when(mockClient.getDatabase(any())).thenReturn(mockDatabase); - ReceiptCosmosClientImpl client = new ReceiptCosmosClientImpl(mockClient); - - Assertions.assertThrows(ReceiptNotFoundException.class, () -> client.getReceiptDocument("an invalid receipt id")); + assertThrows(ReceiptNotFoundException.class, () -> client.getReceiptDocument("an invalid receipt id")); } @Test -void runOk_FailedQueryClient() throws ReceiptNotFoundException { - String RECEIPT_ID = "a valid receipt id"; - - CosmosClient mockClient = mock(CosmosClient.class); - + void runOk_FailedQueryClient() { CosmosDatabase mockDatabase = mock(CosmosDatabase.class); CosmosContainer mockContainer = mock(CosmosContainer.class); @@ -114,10 +116,73 @@ void runOk_FailedQueryClient() throws ReceiptNotFoundException { when(mockDatabase.getContainer(any())).thenReturn(mockContainer); when(mockClient.getDatabase(any())).thenReturn(mockDatabase); - ReceiptCosmosClientImpl client = new ReceiptCosmosClientImpl(mockClient); + assertDoesNotThrow(() -> client.getFailedReceiptDocuments(null, 100)); + } + + @Test + void getNotNotifiedReceiptDocumentsSuccess() { + CosmosDatabase mockDatabase = mock(CosmosDatabase.class); + CosmosContainer mockContainer = mock(CosmosContainer.class); + + when(mockClient.getDatabase(any())).thenReturn(mockDatabase); + when(mockDatabase.getContainer(any())).thenReturn(mockContainer); + + CosmosPagedIterable mockIterable = mock(CosmosPagedIterable.class); + when(mockContainer.queryItems(anyString(), any(), eq(Receipt.class))).thenReturn(mockIterable); + + Iterator mockIterator = mock(Iterator.class); + when(mockIterable.iterator()).thenReturn(mockIterator); + when(mockIterator.hasNext()).thenReturn(true); + + Receipt receipt = new Receipt(); + receipt.setId(RECEIPT_ID); - Assertions.assertDoesNotThrow(() -> client.getFailedReceiptDocuments(null, 100)); + when(mockIterator.next()).thenReturn(receipt); + assertDoesNotThrow(() -> client.getNotNotifiedReceiptDocuments( + null, + 100, + true, + true)); + assertDoesNotThrow(() -> client.getNotNotifiedReceiptDocuments( + null, + 100, + false, + true)); + assertDoesNotThrow(() -> client.getNotNotifiedReceiptDocuments( + null, + 100, + true, + false)); } + + @Test + void getNotNotifiedReceiptDocumentsFailThrowsIllegalArgumentException() { + CosmosDatabase mockDatabase = mock(CosmosDatabase.class); + CosmosContainer mockContainer = mock(CosmosContainer.class); + + when(mockClient.getDatabase(any())).thenReturn(mockDatabase); + when(mockDatabase.getContainer(any())).thenReturn(mockContainer); + + CosmosPagedIterable mockIterable = mock(CosmosPagedIterable.class); + when(mockContainer.queryItems(anyString(), any(), eq(Receipt.class))).thenReturn(mockIterable); + + Iterator mockIterator = mock(Iterator.class); + when(mockIterable.iterator()).thenReturn(mockIterator); + when(mockIterator.hasNext()).thenReturn(true); + + Receipt receipt = new Receipt(); + receipt.setId(RECEIPT_ID); + + when(mockIterator.next()).thenReturn(receipt); + + assertThrows( + IllegalArgumentException.class, + () -> client.getNotNotifiedReceiptDocuments( + null, + 100, + false, + false)); + } } \ No newline at end of file diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml new file mode 100644 index 0000000..7587362 --- /dev/null +++ b/src/test/resources/logback-test.xml @@ -0,0 +1,10 @@ + + + + %msg%n + + + + + + \ No newline at end of file