diff --git a/.github/workflows/code_review.yml b/.github/workflows/code_review.yml index 937d4fd..a819d19 100644 --- a/.github/workflows/code_review.yml +++ b/.github/workflows/code_review.yml @@ -40,8 +40,6 @@ jobs: github_token: ${{ secrets.GITHUB_TOKEN }} sonar_token: ${{ secrets.SONAR_TOKEN }} project_key: ${{env.PROJECT_KEY}} - coverage_exclusions: "**/config/**,**/*Mock*,**/model/**,**/entity/*,**/util/*" - cpd_exclusions: "**/model/**,**/entity/*" java_version: 17 smoke-test: diff --git a/.github/workflows/integration_test.yml b/.github/workflows/integration_test.yml index b0e2e3e..82cc6d0 100644 --- a/.github/workflows/integration_test.yml +++ b/.github/workflows/integration_test.yml @@ -65,7 +65,7 @@ jobs: needs: [ integration_test ] runs-on: ubuntu-latest name: Notify - if: ${{ always() && inputs.notify == 'true' }} + if: ${{ inputs.notify == 'true' }} steps: - name: Report Status if: ${{ inputs.notify }} diff --git a/README.md b/README.md index 1bb87c6..2b38bb2 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,9 @@ It allows the creditor institutions to: ## Api Documentation 📖 -See the [OpenApi 3 here.](https://editor.swagger.io/?url=https://raw.githubusercontent.com/pagopa/pagopa-gpd-upload/main/openapi/openapi.json) +See the external [OpenApi 3 here.](https://editor.swagger.io/?url=https://raw.githubusercontent.com/pagopa/pagopa-gpd-upload/main/openapi/openapi.json) + +See the internal [OpenApi 3 here.](https://editor.swagger.io/?url=https://raw.githubusercontent.com/pagopa/pagopa-gpd-upload/main/openapi/openapi-support-internal.json) --- diff --git a/helm/Chart.yaml b/helm/Chart.yaml index 17342da..7bc7b36 100644 --- a/helm/Chart.yaml +++ b/helm/Chart.yaml @@ -2,8 +2,8 @@ apiVersion: v2 name: pagopa-gpd-upload description: Microservice that handles file upload of massive debt positions JSON object type: application -version: 0.107.0 -appVersion: 0.1.33 +version: 0.108.0 +appVersion: 0.1.33-1-recover-delete-operation-status dependencies: - name: microservice-chart version: 2.8.0 diff --git a/helm/values-dev.yaml b/helm/values-dev.yaml index 0753fdc..5f4bba5 100644 --- a/helm/values-dev.yaml +++ b/helm/values-dev.yaml @@ -4,7 +4,7 @@ microservice-chart: fullnameOverride: "" image: repository: ghcr.io/pagopa/pagopa-gpd-upload - tag: "0.1.33" + tag: "0.1.33-1-recover-delete-operation-status" pullPolicy: Always livenessProbe: httpGet: diff --git a/helm/values-prod.yaml b/helm/values-prod.yaml index cd77ce9..61c9a8c 100644 --- a/helm/values-prod.yaml +++ b/helm/values-prod.yaml @@ -4,7 +4,7 @@ microservice-chart: fullnameOverride: "" image: repository: ghcr.io/pagopa/pagopa-gpd-upload - tag: "0.1.33" + tag: "0.1.33-1-recover-delete-operation-status" pullPolicy: Always livenessProbe: httpGet: diff --git a/helm/values-uat.yaml b/helm/values-uat.yaml index 34463ac..a0acd85 100644 --- a/helm/values-uat.yaml +++ b/helm/values-uat.yaml @@ -4,7 +4,7 @@ microservice-chart: fullnameOverride: "" image: repository: ghcr.io/pagopa/pagopa-gpd-upload - tag: "0.1.33" + tag: "0.1.33-1-recover-delete-operation-status" pullPolicy: Always livenessProbe: httpGet: diff --git a/openapi/openapi-support-internal.json b/openapi/openapi-support-internal.json new file mode 100644 index 0000000..a8dbb71 --- /dev/null +++ b/openapi/openapi-support-internal.json @@ -0,0 +1,916 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "GPD-Upload-Support-API", + "description": "Microservice to manage PagoPA GPD Upload", + "termsOfService": "https://www.pagopa.gov.it/", + "version": "0.1.33-1-recover-delete-operation-status" + }, + "servers": [ + { + "url": "http://localhost:8080" + }, + { + "url": "https://{host}{basePath}", + "variables": { + "basePath": { + "default": "/upload/gpd/debt-positions-service/v1", + "enum": [ + "/upload/gpd/debt-positions-service/v1" + ] + }, + "host": { + "default": "api.dev.platform.pagopa.it", + "enum": [ + "api.dev.platform.pagopa.it", + "api.uat.platform.pagopa.it", + "api.platform.pagopa.it" + ] + } + } + } + ], + "paths": { + "/brokers/{broker-code}/organizations/{organization-fiscal-code}/debtpositions/file": { + "put": { + "tags": [ + "Debt Positions CRUD via file upload API" + ], + "summary": "The Organization updates the debt positions listed in the file.", + "operationId": "update-debt-positions-by-file-upload", + "parameters": [ + { + "name": "broker-code", + "in": "path", + "description": "The broker code", + "required": true, + "schema": { + "minLength": 1, + "type": "string" + } + }, + { + "name": "organization-fiscal-code", + "in": "path", + "description": "The organization fiscal code", + "required": true, + "schema": { + "minLength": 1, + "type": "string" + } + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "file": { + "type": "string", + "description": "File to be uploaded", + "format": "binary" + } + } + }, + "encoding": { + "file": { + "contentType": "application/octet-stream" + } + } + } + }, + "required": true + }, + "responses": { + "202": { + "description": "Request accepted." + }, + "400": { + "description": "Malformed request.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemJson" + } + } + } + }, + "401": { + "description": "Wrong or missing function key.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemJson" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "allOf": [], + "anyOf": [], + "oneOf": [] + } + } + } + }, + "409": { + "description": "Conflict: duplicate file found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemJson" + } + } + } + }, + "429": { + "description": "Too many requests.", + "content": { + "text/json": {} + } + }, + "500": { + "description": "Service unavailable.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemJson" + } + } + } + } + }, + "security": [ + { + "ApiKey": [] + }, + { + "Authorization": [] + } + ] + }, + "post": { + "tags": [ + "Debt Positions CRUD via file upload API" + ], + "summary": "The Organization creates the debt positions listed in the file.", + "operationId": "create-debt-positions-by-file-upload", + "parameters": [ + { + "name": "broker-code", + "in": "path", + "description": "The broker code", + "required": true, + "schema": { + "minLength": 1, + "type": "string" + } + }, + { + "name": "organization-fiscal-code", + "in": "path", + "description": "The organization fiscal code", + "required": true, + "schema": { + "minLength": 1, + "type": "string" + } + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "file": { + "type": "string", + "description": "File to be uploaded", + "format": "binary" + } + } + }, + "encoding": { + "file": { + "contentType": "application/octet-stream" + } + } + } + }, + "required": true + }, + "responses": { + "202": { + "description": "Request accepted." + }, + "400": { + "description": "Malformed request.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemJson" + } + } + } + }, + "401": { + "description": "Wrong or missing function key.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemJson" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "allOf": [], + "anyOf": [], + "oneOf": [] + } + } + } + }, + "409": { + "description": "Conflict: duplicate file found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemJson" + } + } + } + }, + "429": { + "description": "Too many requests.", + "content": { + "text/json": {} + } + }, + "500": { + "description": "Service unavailable.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemJson" + } + } + } + } + }, + "security": [ + { + "ApiKey": [] + }, + { + "Authorization": [] + } + ] + }, + "delete": { + "tags": [ + "Debt Positions CRUD via file upload API" + ], + "summary": "The Organization deletes the debt positions based on IUPD listed in the file.", + "operationId": "delete-debt-positions-by-file-upload", + "parameters": [ + { + "name": "broker-code", + "in": "path", + "description": "The broker code", + "required": true, + "schema": { + "minLength": 1, + "type": "string" + } + }, + { + "name": "organization-fiscal-code", + "in": "path", + "description": "The organization fiscal code", + "required": true, + "schema": { + "minLength": 1, + "type": "string" + } + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "file": { + "type": "string", + "description": "File to be uploaded", + "format": "binary" + } + } + }, + "encoding": { + "file": { + "contentType": "application/octet-stream" + } + } + } + }, + "required": true + }, + "responses": { + "202": { + "description": "Request accepted." + }, + "400": { + "description": "Malformed request.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemJson" + } + } + } + }, + "401": { + "description": "Wrong or missing function key.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemJson" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "allOf": [], + "anyOf": [], + "oneOf": [] + } + } + } + }, + "409": { + "description": "Conflict: duplicate file found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemJson" + } + } + } + }, + "429": { + "description": "Too many requests.", + "content": { + "text/json": {} + } + }, + "500": { + "description": "Service unavailable.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemJson" + } + } + } + } + }, + "security": [ + { + "ApiKey": [] + }, + { + "Authorization": [] + } + ] + } + }, + "/brokers/{broker-code}/organizations/{organization-fiscal-code}/debtpositions/file/{file-id}/report": { + "get": { + "tags": [ + "Upload Status API" + ], + "summary": "Returns the debt positions upload report.", + "operationId": "get-debt-positions-upload-report", + "parameters": [ + { + "name": "broker-code", + "in": "path", + "description": "The broker code", + "required": true, + "schema": { + "minLength": 1, + "type": "string" + } + }, + { + "name": "organization-fiscal-code", + "in": "path", + "description": "The organization fiscal code", + "required": true, + "schema": { + "minLength": 1, + "type": "string" + } + }, + { + "name": "file-id", + "in": "path", + "description": "The unique identifier for file upload", + "required": true, + "schema": { + "minLength": 1, + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Upload report found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UploadReport" + } + } + } + }, + "400": { + "description": "Malformed request.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemJson" + } + } + } + }, + "401": { + "description": "Wrong or missing function key.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemJson" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "allOf": [], + "anyOf": [], + "oneOf": [] + } + } + } + }, + "404": { + "description": "Upload report not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemJson" + } + } + } + }, + "429": { + "description": "Too many requests.", + "content": { + "text/json": {} + } + }, + "500": { + "description": "Service unavailable.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemJson" + } + } + } + } + }, + "security": [ + { + "ApiKey": [] + }, + { + "Authorization": [] + } + ] + } + }, + "/brokers/{broker-code}/organizations/{organization-fiscal-code}/debtpositions/file/{file-id}/status": { + "get": { + "tags": [ + "Upload Status API" + ], + "summary": "Returns the debt positions upload status.", + "operationId": "get-debt-positions-upload-status", + "parameters": [ + { + "name": "broker-code", + "in": "path", + "description": "The broker code", + "required": true, + "schema": { + "minLength": 1, + "type": "string" + } + }, + { + "name": "organization-fiscal-code", + "in": "path", + "description": "The organization fiscal code", + "required": true, + "schema": { + "minLength": 1, + "type": "string" + } + }, + { + "name": "file-id", + "in": "path", + "description": "The unique identifier for file upload", + "required": true, + "schema": { + "minLength": 1, + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Upload found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UploadStatus" + } + } + } + }, + "400": { + "description": "Malformed request.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemJson" + } + } + } + }, + "401": { + "description": "Wrong or missing function key.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemJson" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "allOf": [], + "anyOf": [], + "oneOf": [] + } + } + } + }, + "404": { + "description": "Upload not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemJson" + } + } + } + }, + "429": { + "description": "Too many requests.", + "content": { + "text/json": {} + } + }, + "500": { + "description": "Service unavailable.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemJson" + } + } + } + } + }, + "security": [ + { + "ApiKey": [] + }, + { + "Authorization": [] + } + ] + } + }, + "/info": { + "get": { + "tags": [ + "Health check" + ], + "summary": "health check", + "description": "Return OK if application is started", + "operationId": "healthCheck", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AppInfo" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemJson" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "allOf": [], + "anyOf": [], + "oneOf": [] + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "allOf": [], + "anyOf": [], + "oneOf": [] + } + } + } + }, + "429": { + "description": "Too many requests.", + "content": { + "text/json": {} + } + }, + "500": { + "description": "Service unavailable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemJson" + } + } + } + } + } + } + }, + "/support/uploads/{upload}/status/refresh": { + "get": { + "tags": [ + "Support API" + ], + "summary": "Support API to recover status on CREATE and DELETE operation", + "description": "Returns the debt positions upload report recovered.", + "operationId": "recoverStatus", + "parameters": [ + { + "name": "upload", + "in": "path", + "description": "The unique identifier for file upload", + "required": true, + "schema": { + "minLength": 1, + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AppInfo" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemJson" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "allOf": [], + "anyOf": [], + "oneOf": [] + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "allOf": [], + "anyOf": [], + "oneOf": [] + } + } + } + }, + "429": { + "description": "Too many requests.", + "content": { + "text/json": {} + } + }, + "500": { + "description": "Service unavailable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemJson" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "AppInfo": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "environment": { + "type": "string" + } + } + }, + "ProblemJson": { + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "A short, summary of the problem type. Written in english and readable for engineers (usually not suited for non technical stakeholders and not localized); example: Service Unavailable" + }, + "status": { + "type": "integer", + "description": "The HTTP status code generated by the origin server for this occurrence of the problem.", + "format": "int32", + "example": 200 + }, + "detail": { + "type": "string", + "description": "A human readable explanation specific to this occurrence of the problem.", + "example": "There was an error processing the request" + } + }, + "description": "Object returned as response in case of an error." + }, + "ResponseEntry": { + "type": "object", + "properties": { + "statusCode": { + "type": "integer", + "format": "int32", + "example": 400 + }, + "statusMessage": { + "type": "string", + "example": "Bad request caused by invalid email address" + }, + "requestIDs": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "UploadReport": { + "type": "object", + "properties": { + "uploadID": { + "type": "string" + }, + "processedItem": { + "type": "integer", + "format": "int32" + }, + "submittedItem": { + "type": "integer", + "format": "int32" + }, + "responses": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ResponseEntry" + } + }, + "startTime": { + "type": "string", + "format": "date-time", + "example": "2024-10-08T14:55:16.302Z" + }, + "endTime": { + "type": "string", + "format": "date-time", + "example": "2024-10-08T14:55:16.302Z" + } + } + }, + "UploadStatus": { + "type": "object", + "properties": { + "uploadID": { + "type": "string" + }, + "processedItem": { + "type": "integer", + "format": "int32" + }, + "submittedItem": { + "type": "integer", + "format": "int32" + }, + "startTime": { + "type": "string", + "format": "date-time", + "example": "2024-10-08T14:55:16.302Z" + } + } + } + }, + "securitySchemes": { + "Ocp-Apim-Subscription-Key": { + "type": "apiKey", + "name": "Ocp-Apim-Subscription-Key", + "in": "header" + } + } + } +} diff --git a/openapi/openapi.json b/openapi/openapi.json index 1cc8d54..2ebccc7 100644 --- a/openapi/openapi.json +++ b/openapi/openapi.json @@ -1,10 +1,10 @@ { "openapi": "3.0.1", "info": { - "title": "pagopa-gpd-upload", + "title": "GPD-Upload-API", "description": "Microservice to manage PagoPA GPD Upload", "termsOfService": "https://www.pagopa.gov.it/", - "version": "0.1.33" + "version": "0.1.33-1-recover-delete-operation-status" }, "servers": [ { @@ -708,110 +708,6 @@ } } } - }, - "/support/brokers/{broker}/organizations/{organization}/{upload}/status/created/refresh": { - "get": { - "tags": [ - "Support API" - ], - "summary": "Support API to recover status on CREATE operation", - "description": "Returns the debt positions upload report recovered.", - "operationId": "recoverStatus", - "parameters": [ - { - "name": "broker", - "in": "path", - "description": "The broker code", - "required": true, - "schema": { - "minLength": 1, - "type": "string" - } - }, - { - "name": "organization", - "in": "path", - "description": "The organization fiscal code", - "required": true, - "schema": { - "minLength": 1, - "type": "string" - } - }, - { - "name": "upload", - "in": "path", - "description": "The unique identifier for file upload", - "required": true, - "schema": { - "minLength": 1, - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AppInfo" - } - } - } - }, - "400": { - "description": "Bad Request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemJson" - } - } - } - }, - "401": { - "description": "Unauthorized", - "content": { - "application/json": { - "schema": { - "allOf": [], - "anyOf": [], - "oneOf": [] - } - } - } - }, - "403": { - "description": "Forbidden", - "content": { - "application/json": { - "schema": { - "allOf": [], - "anyOf": [], - "oneOf": [] - } - } - } - }, - "429": { - "description": "Too many requests.", - "content": { - "text/json": {} - } - }, - "500": { - "description": "Service unavailable", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemJson" - } - } - } - } - } - } } }, "components": { diff --git a/pom.xml b/pom.xml index b9ff881..b8a8fb0 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ 4.0.0 it.gov.pagopa.gpd.upload pagopa-gpd-upload - 0.1.33 + 0.1.33-1-recover-delete-operation-status ${packaging} diff --git a/src/main/java/it/gov/pagopa/gpd/upload/Application.java b/src/main/java/it/gov/pagopa/gpd/upload/Application.java index b0c248a..846191f 100644 --- a/src/main/java/it/gov/pagopa/gpd/upload/Application.java +++ b/src/main/java/it/gov/pagopa/gpd/upload/Application.java @@ -9,7 +9,7 @@ @OpenAPIDefinition( info = @Info( - title = "pagopa-gpd-upload", + title = "${info.application.title}", version = "${openapi.application.version}", description = "Microservice to manage PagoPA GPD Upload", termsOfService = "https://www.pagopa.gov.it/" diff --git a/src/main/java/it/gov/pagopa/gpd/upload/config/duplicate/NoDuplicate.java b/src/main/java/it/gov/pagopa/gpd/upload/config/duplicate/NoDuplicate.java new file mode 100644 index 0000000..dbae0ce --- /dev/null +++ b/src/main/java/it/gov/pagopa/gpd/upload/config/duplicate/NoDuplicate.java @@ -0,0 +1,22 @@ +package it.gov.pagopa.gpd.upload.config.duplicate; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Constraint(validatedBy = NoDuplicateValidator.class) +@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER }) +@Retention(RetentionPolicy.RUNTIME) +public @interface NoDuplicate { + String message() default "there are duplicates in the list"; + + Class[] groups() default {}; + + Class[] payload() default {}; + + String fieldName() default ""; // Check duplicate on this field +} \ No newline at end of file diff --git a/src/main/java/it/gov/pagopa/gpd/upload/config/duplicate/NoDuplicateValidator.java b/src/main/java/it/gov/pagopa/gpd/upload/config/duplicate/NoDuplicateValidator.java new file mode 100644 index 0000000..4bf697c --- /dev/null +++ b/src/main/java/it/gov/pagopa/gpd/upload/config/duplicate/NoDuplicateValidator.java @@ -0,0 +1,53 @@ +package it.gov.pagopa.gpd.upload.config.duplicate; + + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import lombok.extern.slf4j.Slf4j; + +import java.lang.reflect.Field; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +@Slf4j +public class NoDuplicateValidator implements ConstraintValidator> { + + private String fieldName; + + @Override + public void initialize(NoDuplicate constraintAnnotation) { + this.fieldName = constraintAnnotation.fieldName(); + } + + @Override + public boolean isValid(List list, ConstraintValidatorContext context) { + if (list == null) { + return true; + } + + if(Objects.equals(fieldName, "")) { + HashSet unique = new HashSet<>(list); + return unique.size() == list.size(); + } else { + Set seenValues = new HashSet<>(); + + for (Object item : list) { + try { + Field field = item.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + Object value = field.get(item); + if (!seenValues.add(value)) { + return false; + } + } catch (NoSuchFieldException | IllegalAccessException e) { + log.error("Exception while detect duplicates: {}, cause: {}", e.getMessage(), e.getCause().getMessage()); + return false; + } + } + + return true; + } + } +} diff --git a/src/main/java/it/gov/pagopa/gpd/upload/controller/BaseController.java b/src/main/java/it/gov/pagopa/gpd/upload/controller/external/BaseController.java similarity index 98% rename from src/main/java/it/gov/pagopa/gpd/upload/controller/BaseController.java rename to src/main/java/it/gov/pagopa/gpd/upload/controller/external/BaseController.java index d9d30c7..62a8d88 100644 --- a/src/main/java/it/gov/pagopa/gpd/upload/controller/BaseController.java +++ b/src/main/java/it/gov/pagopa/gpd/upload/controller/external/BaseController.java @@ -1,4 +1,4 @@ -package it.gov.pagopa.gpd.upload.controller; +package it.gov.pagopa.gpd.upload.controller.external; import io.micronaut.context.annotation.Value; import io.micronaut.http.HttpResponse; diff --git a/src/main/java/it/gov/pagopa/gpd/upload/controller/FileUploadController.java b/src/main/java/it/gov/pagopa/gpd/upload/controller/external/FileUploadController.java similarity index 99% rename from src/main/java/it/gov/pagopa/gpd/upload/controller/FileUploadController.java rename to src/main/java/it/gov/pagopa/gpd/upload/controller/external/FileUploadController.java index e5c6718..6b8267c 100644 --- a/src/main/java/it/gov/pagopa/gpd/upload/controller/FileUploadController.java +++ b/src/main/java/it/gov/pagopa/gpd/upload/controller/external/FileUploadController.java @@ -1,4 +1,4 @@ -package it.gov.pagopa.gpd.upload.controller; +package it.gov.pagopa.gpd.upload.controller.external; import io.micronaut.context.annotation.Value; import io.micronaut.http.HttpHeaders; diff --git a/src/main/java/it/gov/pagopa/gpd/upload/controller/UploadStatusController.java b/src/main/java/it/gov/pagopa/gpd/upload/controller/external/UploadStatusController.java similarity index 99% rename from src/main/java/it/gov/pagopa/gpd/upload/controller/UploadStatusController.java rename to src/main/java/it/gov/pagopa/gpd/upload/controller/external/UploadStatusController.java index 31bc63f..2fddd8a 100644 --- a/src/main/java/it/gov/pagopa/gpd/upload/controller/UploadStatusController.java +++ b/src/main/java/it/gov/pagopa/gpd/upload/controller/external/UploadStatusController.java @@ -1,4 +1,4 @@ -package it.gov.pagopa.gpd.upload.controller; +package it.gov.pagopa.gpd.upload.controller.external; import io.micronaut.http.HttpResponse; import io.micronaut.http.HttpStatus; diff --git a/src/main/java/it/gov/pagopa/gpd/upload/controller/SupportController.java b/src/main/java/it/gov/pagopa/gpd/upload/controller/support/SupportController.java similarity index 82% rename from src/main/java/it/gov/pagopa/gpd/upload/controller/SupportController.java rename to src/main/java/it/gov/pagopa/gpd/upload/controller/support/SupportController.java index f1871e0..0dd6a2b 100644 --- a/src/main/java/it/gov/pagopa/gpd/upload/controller/SupportController.java +++ b/src/main/java/it/gov/pagopa/gpd/upload/controller/support/SupportController.java @@ -1,4 +1,4 @@ -package it.gov.pagopa.gpd.upload.controller; +package it.gov.pagopa.gpd.upload.controller.support; import io.micronaut.http.HttpResponse; import io.micronaut.http.HttpStatus; @@ -15,6 +15,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; +import it.gov.pagopa.gpd.upload.exception.AppException; import it.gov.pagopa.gpd.upload.model.AppInfo; import it.gov.pagopa.gpd.upload.model.ProblemJson; import it.gov.pagopa.gpd.upload.model.UploadReport; @@ -36,7 +37,7 @@ public class SupportController { @Inject StatusService statusService; - @Operation(summary = "Support API to recover status on CREATE operation", description = "Returns the debt positions upload report recovered.", tags = {"Support API"}) + @Operation(summary = "Support API to recover status on CREATE and DELETE operation", description = "Returns the debt positions upload report recovered.", tags = {"Support API"}) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "OK", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = AppInfo.class))), @ApiResponse(responseCode = "400", description = "Bad Request", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = ProblemJson.class))), @@ -44,15 +45,17 @@ public class SupportController { @ApiResponse(responseCode = "403", description = "Forbidden", content = @Content(schema = @Schema())), @ApiResponse(responseCode = "429", description = "Too many requests.", content = @Content(mediaType = MediaType.TEXT_JSON)), @ApiResponse(responseCode = "500", description = "Service unavailable", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = ProblemJson.class)))}) - @Get(value = "brokers/{broker}/organizations/{organization}/{upload}/status/created/refresh") + @Get(value = "uploads/{upload}/status/refresh") public HttpResponse recoverStatus( - @Parameter(description = "The broker code", required = true) - @NotBlank @PathVariable(name = "broker") String broker, - @Parameter(description = "The organization fiscal code", required = true) - @NotBlank @PathVariable(name = "organization") String organization, @Parameter(description = "The unique identifier for file upload", required = true) - @NotBlank @PathVariable(name = "upload") String upload - ) { + @NotBlank @PathVariable(name = "upload") String upload) { + String[] strings = upload.split("_"); + + if(strings.length < 3) + throw new AppException(HttpStatus.BAD_REQUEST, "Recover bad request", "The upload UUID should be formatted as __"); + + String broker = strings[0]; + String organization = strings[1]; recoveryService.recover(broker, organization, upload); log.info("[Support-API] Status {} recovered", upload); UploadReport report = statusService.getReport(organization, upload); diff --git a/src/main/java/it/gov/pagopa/gpd/upload/entity/MatchResult.java b/src/main/java/it/gov/pagopa/gpd/upload/entity/MatchResult.java new file mode 100644 index 0000000..db4bb2b --- /dev/null +++ b/src/main/java/it/gov/pagopa/gpd/upload/entity/MatchResult.java @@ -0,0 +1,8 @@ +package it.gov.pagopa.gpd.upload.entity; + + +import java.util.List; + +public record MatchResult(List matchingIUPD, List nonMatchingIUPD) { + // used by RecoveryService to store 2 list, match and non-match cases +} \ No newline at end of file diff --git a/src/main/java/it/gov/pagopa/gpd/upload/model/UploadInput.java b/src/main/java/it/gov/pagopa/gpd/upload/model/UploadInput.java index 23d14f4..58cffa8 100644 --- a/src/main/java/it/gov/pagopa/gpd/upload/model/UploadInput.java +++ b/src/main/java/it/gov/pagopa/gpd/upload/model/UploadInput.java @@ -1,6 +1,7 @@ package it.gov.pagopa.gpd.upload.model; import com.fasterxml.jackson.annotation.JsonProperty; +import it.gov.pagopa.gpd.upload.config.duplicate.NoDuplicate; import it.gov.pagopa.gpd.upload.model.pd.PaymentPositionModel; import jakarta.validation.Valid; import lombok.AllArgsConstructor; @@ -22,5 +23,6 @@ public class UploadInput { @Valid private List<@Valid PaymentPositionModel> paymentPositions; @Valid + @NoDuplicate private List paymentPositionIUPDs; } diff --git a/src/main/java/it/gov/pagopa/gpd/upload/model/pd/MultipleIUPDModel.java b/src/main/java/it/gov/pagopa/gpd/upload/model/pd/MultipleIUPDModel.java index de970e3..65e47be 100644 --- a/src/main/java/it/gov/pagopa/gpd/upload/model/pd/MultipleIUPDModel.java +++ b/src/main/java/it/gov/pagopa/gpd/upload/model/pd/MultipleIUPDModel.java @@ -1,6 +1,7 @@ package it.gov.pagopa.gpd.upload.model.pd; import io.micronaut.core.annotation.Introspected; +import it.gov.pagopa.gpd.upload.config.duplicate.NoDuplicate; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; @@ -23,5 +24,6 @@ public class MultipleIUPDModel { @NotEmpty @Size(min = 1, max = 100000, message = "The list of payment positions IUPD must contain at least one element and at the most " + MAX_IUPD) @NotNull + @NoDuplicate private List paymentPositionIUPDs; } diff --git a/src/main/java/it/gov/pagopa/gpd/upload/model/pd/PaymentPositionsModel.java b/src/main/java/it/gov/pagopa/gpd/upload/model/pd/PaymentPositionsModel.java index e2b71e2..fbfa554 100644 --- a/src/main/java/it/gov/pagopa/gpd/upload/model/pd/PaymentPositionsModel.java +++ b/src/main/java/it/gov/pagopa/gpd/upload/model/pd/PaymentPositionsModel.java @@ -1,6 +1,7 @@ package it.gov.pagopa.gpd.upload.model.pd; import io.micronaut.core.annotation.Introspected; +import it.gov.pagopa.gpd.upload.config.duplicate.NoDuplicate; import jakarta.validation.Valid; import jakarta.validation.constraints.Size; import lombok.AllArgsConstructor; @@ -20,6 +21,7 @@ public class PaymentPositionsModel { @Size(min = 1, message = "The list of payment positions must contain at least one element") private List<@Valid PaymentPositionModel> paymentPositions; + @NoDuplicate public @Valid List<@Valid PaymentPositionModel> getPaymentPositions() { return this.paymentPositions; } diff --git a/src/main/java/it/gov/pagopa/gpd/upload/repository/BlobStorageRepository.java b/src/main/java/it/gov/pagopa/gpd/upload/repository/BlobStorageRepository.java index 861021a..474237d 100644 --- a/src/main/java/it/gov/pagopa/gpd/upload/repository/BlobStorageRepository.java +++ b/src/main/java/it/gov/pagopa/gpd/upload/repository/BlobStorageRepository.java @@ -43,7 +43,7 @@ public void init() { } @Override - public String upload(String broker, String fiscalCode, File file) throws FileNotFoundException { + public String upload(String broker, String fiscalCode, InputStream inputStream) throws FileNotFoundException { blobServiceClient.createBlobContainerIfNotExists(broker); BlobContainerClient container = blobServiceClient.getBlobContainerClient(broker + "/" + fiscalCode + "/" + INPUT_DIRECTORY); String key = this.createRandomName(broker + "_" + fiscalCode); @@ -57,30 +57,23 @@ public String upload(String broker, String fiscalCode, File file) throws FileNot BlockBlobClient blockBlobClient = blobClient.getBlockBlobClient(); - CompletableFuture uploadFuture = uploadFileAsync(blockBlobClient, file); + CompletableFuture uploadFuture = uploadFileAsync(blockBlobClient, inputStream); uploadFuture.thenAccept(blobName -> { // Handle the result asynchronously - log.debug(String.format("Asynchronous upload completed for blob %s", blobName)); + log.debug("Asynchronous upload completed for blob {}", blobName); }).exceptionally(ex -> { - log.error(String.format("[Error][BlobStorageRepository@upload] Exception while uploading file %s asynchronously: %s", - file.getName(), ex.getMessage())); + log.error("[Error][BlobStorageRepository@upload] Exception while uploading file asynchronously: {}", ex.getMessage()); throw new AppException(HttpStatus.INTERNAL_SERVER_ERROR, "INTERNAL_SERVER_ERROR", "Error uploading file asynchronously", ex); }); return key; } - private CompletableFuture uploadFileAsync(BlockBlobClient blockBlobClient, File file) { + private CompletableFuture uploadFileAsync(BlockBlobClient blockBlobClient, InputStream inputStream) { return CompletableFuture.supplyAsync(() -> { try { - String blobName = this.uploadFileBlocksAsBlockBlob(blockBlobClient, file); - - if(!file.delete()) { - log.error(String.format("[Error][BlobStorageRepository@uploadFileAsync] The file %s was not deleted", file.getName())); - } - - return blobName; + return this.uploadFileBlocksAsBlockBlob(blockBlobClient, inputStream); } catch (IOException e) { throw new AppException(HttpStatus.INTERNAL_SERVER_ERROR, "INTERNAL_SERVER_ERROR", "Error uploading file asynchronously", e); } @@ -91,8 +84,7 @@ private String createRandomName(String namePrefix) { return namePrefix + "_" + UUID.randomUUID().toString().replace("-", ""); } - private String uploadFileBlocksAsBlockBlob(BlockBlobClient blockBlob, File file) throws IOException { - InputStream inputStream = new FileInputStream(file); + private String uploadFileBlocksAsBlockBlob(BlockBlobClient blockBlob, InputStream inputStream) throws IOException { ByteArrayInputStream byteInputStream = null; int blockSize = 1024 * 1024; diff --git a/src/main/java/it/gov/pagopa/gpd/upload/repository/FileRepository.java b/src/main/java/it/gov/pagopa/gpd/upload/repository/FileRepository.java index 45383af..0713db4 100644 --- a/src/main/java/it/gov/pagopa/gpd/upload/repository/FileRepository.java +++ b/src/main/java/it/gov/pagopa/gpd/upload/repository/FileRepository.java @@ -1,8 +1,8 @@ package it.gov.pagopa.gpd.upload.repository; -import java.io.File; import java.io.IOException; +import java.io.InputStream; public interface FileRepository { - String upload(String container, String directory, File file) throws IOException; + String upload(String container, String directory, InputStream inputStream) throws IOException; } diff --git a/src/main/java/it/gov/pagopa/gpd/upload/service/BlobService.java b/src/main/java/it/gov/pagopa/gpd/upload/service/BlobService.java index df9df29..14163ae 100644 --- a/src/main/java/it/gov/pagopa/gpd/upload/service/BlobService.java +++ b/src/main/java/it/gov/pagopa/gpd/upload/service/BlobService.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import io.micronaut.context.annotation.Context; import io.micronaut.context.annotation.Value; @@ -22,8 +23,6 @@ import jakarta.inject.Singleton; import java.io.*; -import java.nio.file.Files; -import java.nio.file.Path; import java.util.List; import java.util.UUID; import java.util.zip.ZipEntry; @@ -70,17 +69,16 @@ public String upsert(String broker, String organizationFiscalCode, UploadOperati File file = this.unzip(fileUpload); if (null == file) throw new AppException(HttpStatus.BAD_REQUEST, "EMPTY FILE", "The JSON file is missing"); - log.debug("File with name " + file.getName() + " has been unzipped, upload operation: " + uploadOperation); + log.debug("File with name {} has been unzipped, upload operation: {}", file.getName(), uploadOperation); try { PaymentPositionsModel paymentPositionsModel = objectMapper.readValue(new FileInputStream(file), PaymentPositionsModel.class); if(!file.delete()) { - log.error(String.format("[Error][BlobService@upsert] The file %s was not deleted", file.getName())); + log.error("[Error][BlobService@upsert] The file {} was not deleted", file.getName()); } if (!paymentPositionsValidator.isValid(paymentPositionsModel)) { - log.error(String.format("[Error][BlobService@upload] Debt-Positions validation failed for upload from broker %s and organization %s", - broker, organizationFiscalCode)); + log.error("[Error][BlobService@upload] Debt-Positions validation failed for upload from broker {} and organization {}", broker, organizationFiscalCode); throw new AppException(HttpStatus.BAD_REQUEST, "INVALID DEBT POSITIONS", "The format of the debt positions in the uploaded file is invalid."); } @@ -94,7 +92,7 @@ public String upsert(String broker, String organizationFiscalCode, UploadOperati } catch (IOException e) { log.error("[Error][BlobService@upload] " + e.getMessage()); if(!file.delete()) - log.error(String.format("[Error][BlobService@upsert] The file %s was not deleted", file.getName())); + log.error("[Error][BlobService@upsert] The file {} was not deleted", file.getName()); if(e instanceof JsonMappingException) throw new AppException(HttpStatus.BAD_REQUEST, "INVALID JSON", "Given JSON is invalid for required API payload: " + e.getMessage()); @@ -163,14 +161,12 @@ public String upload(UploadInput uploadInput, String broker, String organization log.debug(String.format("Upload operation %s was launched for broker %s and organization fiscal code %s", uploadInput.getUploadOperation(), broker, organizationFiscalCode)); - // replace file content - File uploadInputFile = Files.createTempFile(Path.of(DESTINATION_DIRECTORY), "gpd_upload_temp", ".json").toFile(); - FileWriter fileWriter = new FileWriter(uploadInputFile); - fileWriter.write(objectMapper.writeValueAsString(uploadInput)); - fileWriter.close(); + // from UploadInput Object to ByteArrayInputStream + objectMapper.enable(SerializationFeature.INDENT_OUTPUT); + ByteArrayInputStream inputStream = new ByteArrayInputStream(objectMapper.writeValueAsBytes(uploadInput)); // upload blob - String fileId = blobStorageRepository.upload(broker, organizationFiscalCode, uploadInputFile); + String fileId = blobStorageRepository.upload(broker, organizationFiscalCode, inputStream); statusService.createUploadStatus(organizationFiscalCode, broker, fileId, totalItem); return fileId; diff --git a/src/main/java/it/gov/pagopa/gpd/upload/service/RecoveryService.java b/src/main/java/it/gov/pagopa/gpd/upload/service/RecoveryService.java index 439f1f2..2f096bc 100644 --- a/src/main/java/it/gov/pagopa/gpd/upload/service/RecoveryService.java +++ b/src/main/java/it/gov/pagopa/gpd/upload/service/RecoveryService.java @@ -2,6 +2,7 @@ import io.micronaut.http.HttpStatus; import it.gov.pagopa.gpd.upload.client.GPDClient; +import it.gov.pagopa.gpd.upload.entity.MatchResult; import it.gov.pagopa.gpd.upload.entity.ResponseEntry; import it.gov.pagopa.gpd.upload.entity.Status; import it.gov.pagopa.gpd.upload.exception.AppException; @@ -14,9 +15,7 @@ import java.time.LocalDateTime; import java.util.ArrayList; -import java.util.HashSet; import java.util.List; -import java.util.Set; @Singleton @Slf4j @@ -32,58 +31,85 @@ public RecoveryService(StatusService statusService, BlobService blobService, GPD this.gpdClient = gpdClient; } - public Status recover(String brokerId, String organizationFiscalCode, String uploadId) { - Status current = statusService.getStatus(organizationFiscalCode, uploadId); + public boolean recover(String brokerId, String organizationFiscalCode, String uploadId) { UploadInput uploadInput = blobService.getUploadInput(brokerId, organizationFiscalCode, uploadId); + List inputIUPD; + + if(uploadInput.getUploadOperation().equals(UploadOperation.CREATE)) { + inputIUPD = uploadInput.getPaymentPositions().stream().map(PaymentPositionModel::getIupd).toList(); + return recover(organizationFiscalCode, uploadId, inputIUPD, HttpStatus.OK, HttpStatus.CREATED); + } else if(uploadInput.getUploadOperation().equals(UploadOperation.DELETE)) { + inputIUPD = uploadInput.getPaymentPositionIUPDs(); + return recover(organizationFiscalCode, uploadId, inputIUPD, HttpStatus.NOT_FOUND, HttpStatus.NOT_FOUND); + } else { + throw new AppException(HttpStatus.NOT_FOUND, "Upload operation not processable", + String.format("Not exists CREATE or DELETE operation with upload-id %s", uploadId)); + } + } - if(!uploadInput.getUploadOperation().equals(UploadOperation.CREATE)) - throw new AppException(HttpStatus.NOT_FOUND, - "Upload operation not processable", String.format("Not exists create operation with upload-id %s", uploadId)); + private boolean recover(String organizationFiscalCode, String uploadId, List inputIUPD, HttpStatus toGetFromGPD, HttpStatus toWrite) { + Status current = statusService.getStatus(organizationFiscalCode, uploadId); // check if upload is pending - if(current.upload.getCurrent() >= current.upload.getTotal()) - return current; + if(current.upload.getCurrent() >= current.upload.getTotal()) { + if(current.upload.getEnd() != null) + return false; + // update end-upload-time if it is null + current.upload.setEnd(LocalDateTime.now()); + Status updated = statusService.upsert(current); + return updated != null; + } // extract debt position id list - List inputIUPD = uploadInput.getPaymentPositions().stream() - .map(PaymentPositionModel::getIupd).toList(); List processedIUPD = new ArrayList<>(); current.upload.getResponses().forEach( res -> processedIUPD.addAll(res.getRequestIDs()) ); - // sync with core to check if debt positions are already created - List createdIUPD = getAlreadyCreatedIUPD(organizationFiscalCode, inputIUPD, processedIUPD); + // sync with core to check if debt positions are already processed (DELETED or CREATED -> NOT_EXISTS, EXISTS) + MatchResult result = match(organizationFiscalCode, inputIUPD, processedIUPD, toGetFromGPD); // update status and save current.upload.addResponse(ResponseEntry.builder() - .requestIDs(createdIUPD) - .statusCode(HttpStatus.CREATED.getCode()) - .statusMessage(statusService.getDetail(HttpStatus.CREATED)) + .requestIDs(result.matchingIUPD()) + .statusCode(toWrite.getCode()) + .statusMessage(statusService.getDetail(toWrite)) .build()); + + // for non-matching IUPD the code is 500 + if(!result.nonMatchingIUPD().isEmpty()) { + current.upload.addResponse(ResponseEntry.builder() + .requestIDs(result.nonMatchingIUPD()) + .statusCode(HttpStatus.INTERNAL_SERVER_ERROR.getCode()) + .statusMessage(statusService.getDetail(toWrite)) + .build()); + } current.upload.setEnd(LocalDateTime.now()); - return statusService.upsert(current); + Status updated = statusService.upsert(current); + return updated != null; } - private List getAlreadyCreatedIUPD(String organizationFiscalCode, List inputIUPD, List processedIUPD) { - Set inputIUPDSet = new HashSet<>(inputIUPD); - Set processedIUPDSet = new HashSet<>(processedIUPD); - - // diff - if(!inputIUPDSet.removeAll(processedIUPDSet)) - throw new AppException(HttpStatus.INTERNAL_SERVER_ERROR, "Internal Server Error", "Internal Server Error"); + private MatchResult match(String organizationFiscalCode, List inputIUPD, List processedIUPD, HttpStatus target) { + List differenceIUPD = inputIUPD.stream() + .filter(iupd -> !processedIUPD.contains(iupd)) + .toList(); - List createdIUPD = new ArrayList<>(); + List matchingIUPD = new ArrayList<>(); + List nonMatchingIUPD = new ArrayList<>(); // for each check if position is processed - inputIUPDSet.forEach(id -> { + differenceIUPD.forEach(id -> { // request to GPD HttpStatus httpStatus = gpdClient.getDebtPosition(organizationFiscalCode, id).getStatus(); - if(httpStatus.equals(HttpStatus.OK)) { // if request was successful - createdIUPD.add(id); + // if status code match the target + if(httpStatus.equals(target)) { + matchingIUPD.add(id); + } else { + nonMatchingIUPD.add(id); } }); - return createdIUPD; + + return new MatchResult(matchingIUPD, nonMatchingIUPD); } } \ No newline at end of file diff --git a/src/main/java/it/gov/pagopa/gpd/upload/utils/GPDValidator.java b/src/main/java/it/gov/pagopa/gpd/upload/utils/GPDValidator.java index 65a8fc9..b5a7e7f 100644 --- a/src/main/java/it/gov/pagopa/gpd/upload/utils/GPDValidator.java +++ b/src/main/java/it/gov/pagopa/gpd/upload/utils/GPDValidator.java @@ -33,7 +33,7 @@ public boolean isValid(T model) throws IOException { invalidValues.add(cv.getMessage()); } throw new AppException(HttpStatus.BAD_REQUEST, "INVALID DEBT POSITIONS", - "The format of the debt positions in the uploaded file is invalid. Invalid values: " + invalidValues); + "Debt positions format is invalid or duplicates were found. Invalid values: " + invalidValues); } log.debug("[GPDValidator@isValid] PaymentPosition with id {} validated", model.hashCode()); diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index cc21a3e..f46f78b 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,5 +1,6 @@ info.application.artifactId=${project.artifactId} info.application.version=${project.version} +info.application.title=GPD-Upload-API openapi.application.version=0.1.22 info.properties.environment=env micronaut.application.name=GPD-Massive-Upload-service @@ -12,6 +13,18 @@ micronaut.server.multipart.maxFileSize=104857600 micronaut.server.max-request-size=104857600 micronaut.server.max-request-size.multipart.max-file-size=104857600 +# openapi groups +micronaut.openapi.groups.external.primary=true +micronaut.openapi.groups.external.filename=pagopa-gpd-upload-${openapi.application.version} +micronaut.openapi.groups.external.display-name=GPD-Upload-API +micronaut.openapi.groups.external.title=GPD-Upload-API +micronaut.openapi.groups.external.packages=it.gov.pagopa.gpd.upload.controller.external.* +micronaut.openapi.groups.internal.primary=false +micronaut.openapi.groups.internal.display-name=GPD-Upload-Support-API +micronaut.openapi.groups.internal.title=GPD-Upload-Support-API +micronaut.openapi.groups.internal.filename=pagopa-gpd-upload-support +micronaut.openapi.groups.internal.packages=it.gov.pagopa.gpd.upload.controller.* + blob.sas.connection=${BLOB_CONNECTION_STRING} blob.container.input=gpd-upload/input zip.content.size=104857600 diff --git a/src/test/java/it/gov/pagopa/gpd/upload/OpenApiGenerationTest.java b/src/test/java/it/gov/pagopa/gpd/upload/OpenApiGenerationTest.java index 8be6815..8650854 100644 --- a/src/test/java/it/gov/pagopa/gpd/upload/OpenApiGenerationTest.java +++ b/src/test/java/it/gov/pagopa/gpd/upload/OpenApiGenerationTest.java @@ -33,24 +33,30 @@ class OpenApiGenerationTest { @Client("/") HttpClient client; + @Value("${info.application.title}") + String title; + @Test void swaggerSpringPlugin() throws Exception { - boolean result = saveOpenAPI("/swagger/pagopa-gpd-upload-" + version + ".json", "openapi.json"); - + boolean result = saveOpenAPI("/swagger/pagopa-gpd-upload-" + version + ".json", "openapi.json", "GPD-Upload-API"); assertTrue(result); + + boolean resultSupportAPI = saveOpenAPI("/swagger/pagopa-gpd-upload-support.json", "openapi-support-internal.json", "GPD-Upload-Support-API"); + assertTrue(resultSupportAPI); } - private boolean saveOpenAPI(String fromUri, String toFile) throws IOException { - HttpResponse response = client.toBlocking().exchange(fromUri, String.class); - ObjectMapper objectMapper = new ObjectMapper(); - String responseBody = response.getBody().get(); - Object openAPI = objectMapper.readValue(responseBody, Object.class); - String formatted = new ObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(openAPI); - Path basePath = Paths.get("openapi/"); - Files.createDirectories(basePath); - Files.write(basePath.resolve(toFile), formatted.getBytes()); - return true; - } + private boolean saveOpenAPI(String fromUri, String toFile, String newTitle) throws IOException { + HttpResponse response = client.toBlocking().exchange(fromUri, String.class); + ObjectMapper objectMapper = new ObjectMapper(); + String responseBody = response.getBody().get(); + responseBody = responseBody.replace(title, newTitle); + Object openAPI = objectMapper.readValue(responseBody, Object.class); + String formatted = new ObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(openAPI); + Path basePath = Paths.get("openapi/"); + Files.createDirectories(basePath); + Files.write(basePath.resolve(toFile), formatted.getBytes()); + return true; + } @Bean @Primary diff --git a/src/test/java/it/gov/pagopa/gpd/upload/controller/FileUploadControllerTest.java b/src/test/java/it/gov/pagopa/gpd/upload/controller/FileUploadControllerTest.java index 69c2c19..c46d2de 100644 --- a/src/test/java/it/gov/pagopa/gpd/upload/controller/FileUploadControllerTest.java +++ b/src/test/java/it/gov/pagopa/gpd/upload/controller/FileUploadControllerTest.java @@ -109,6 +109,10 @@ void getUploadStatus_KO() throws IOException { assertEquals(OK, response.getStatus()); } + //////////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////MOCK////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////////// + @Bean @Primary public BlobService fileUploadService() throws IOException { @@ -125,7 +129,6 @@ public StatusService statusService() throws IOException { return statusService; } - // real repositories are out of scope for this test, @PostConstruct init routine requires connection-string @Bean @Primary diff --git a/src/test/java/it/gov/pagopa/gpd/upload/controller/SupportControllerTest.java b/src/test/java/it/gov/pagopa/gpd/upload/controller/SupportControllerTest.java new file mode 100644 index 0000000..1cd7245 --- /dev/null +++ b/src/test/java/it/gov/pagopa/gpd/upload/controller/SupportControllerTest.java @@ -0,0 +1,83 @@ +package it.gov.pagopa.gpd.upload.controller; + +import io.micronaut.context.annotation.Bean; +import io.micronaut.context.annotation.Primary; +import io.micronaut.http.HttpMethod; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.client.HttpClient; +import io.micronaut.http.client.annotation.Client; +import io.micronaut.http.client.exceptions.HttpClientException; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import it.gov.pagopa.gpd.upload.model.UploadReport; +import it.gov.pagopa.gpd.upload.repository.BlobStorageRepository; +import it.gov.pagopa.gpd.upload.repository.StatusRepository; +import it.gov.pagopa.gpd.upload.service.RecoveryService; +import it.gov.pagopa.gpd.upload.service.StatusService; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.io.IOException; + +import static io.micronaut.http.HttpStatus.OK; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyString; + +@MicronautTest +public class SupportControllerTest { + + private static final String URI = "support/uploads/broker_organization_uid/status/refresh"; + private static final String BAD_URI = "support/uploads/broker-organization-uid/status/refresh"; + + @Inject + @Client("/") + HttpClient client; + + @Test + void recoverStatus_OK() { + HttpRequest httpRequest = HttpRequest.create(HttpMethod.GET, URI); + HttpResponse response = client.toBlocking().exchange(httpRequest); + + assertNotNull(response); + assertEquals(OK, response.getStatus()); + } + + @Test + void recoverStatus_KO() { + HttpRequest httpRequest = HttpRequest.create(HttpMethod.GET, BAD_URI); + assertThrows(HttpClientException.class, () -> client.toBlocking().exchange(httpRequest)); + } + + //////////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////MOCK////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////////// + + @Bean + @Primary + public StatusService statusService() throws IOException { + StatusService statusService = Mockito.mock(StatusService.class); + Mockito.when(statusService.getReport(anyString(), anyString())).thenReturn(UploadReport.builder().build()); + return statusService; + } + + @Bean + @Primary + public RecoveryService recoveryService() { + RecoveryService recoveryService = Mockito.mock(RecoveryService.class); + Mockito.when(recoveryService.recover(anyString(), anyString(), anyString())).thenReturn(true); + return recoveryService; + } + + // real repositories are out of scope for this test, @PostConstruct init routine requires connection-string + @Bean + @Primary + public StatusRepository statusRepository() { + return Mockito.mock(StatusRepository.class); + } + @Bean + @Primary + public BlobStorageRepository blobStorageRepository() { + return Mockito.mock(BlobStorageRepository.class); + } +} diff --git a/src/test/java/it/gov/pagopa/gpd/upload/service/RecoveryServiceTest.java b/src/test/java/it/gov/pagopa/gpd/upload/service/RecoveryServiceTest.java new file mode 100644 index 0000000..08b3b12 --- /dev/null +++ b/src/test/java/it/gov/pagopa/gpd/upload/service/RecoveryServiceTest.java @@ -0,0 +1,136 @@ +package it.gov.pagopa.gpd.upload.service; + +import io.micronaut.context.annotation.Bean; +import io.micronaut.context.annotation.Primary; +import io.micronaut.http.HttpResponse; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import it.gov.pagopa.gpd.upload.client.GPDClient; +import it.gov.pagopa.gpd.upload.entity.Status; +import it.gov.pagopa.gpd.upload.entity.Upload; +import it.gov.pagopa.gpd.upload.model.UploadInput; +import it.gov.pagopa.gpd.upload.model.UploadOperation; +import it.gov.pagopa.gpd.upload.model.pd.PaymentPositionModel; +import it.gov.pagopa.gpd.upload.repository.BlobStorageRepository; +import it.gov.pagopa.gpd.upload.repository.StatusRepository; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +import static org.mockito.ArgumentMatchers.*; + +@MicronautTest +public class RecoveryServiceTest { + + static final String CREATE_UPLOAD_ID = "upload-id-create"; + static final String DELETE_UPLOAD_ID = "upload-id-delete"; + + @Inject + RecoveryService recoveryService; + + @Test + void recover_CREATE_UPLOAD_OK() { + Assertions.assertTrue( + recoveryService.recover("broker", "organizaition", CREATE_UPLOAD_ID) + ); + } + + @Test + void recover_DELETE_UPLOAD_OK() { + Assertions.assertTrue( + recoveryService.recover("broker", "organizaition", DELETE_UPLOAD_ID) + ); + } + + //////////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////MOCK////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////////// + + // real repositories are out of scope for this test, @PostConstruct init routine requires connection-string + @Bean + @Primary + public static BlobStorageRepository blobStorageRepository() { + return Mockito.mock(BlobStorageRepository.class); + } + @Bean + @Primary + public static StatusRepository statusRepository() { + return Mockito.mock(StatusRepository.class); + } + @Bean + @Primary + public static BlobService blobService() { + BlobService blobService = Mockito.mock(BlobService.class); + UploadInput uploadInputDelete = UploadInput.builder() + .uploadOperation(UploadOperation.DELETE) + .paymentPositionIUPDs(List.of("IUPD-1")) + .build(); + UploadInput uploadInputCreate = UploadInput.builder() + .uploadOperation(UploadOperation.CREATE) + .paymentPositions(List.of(PaymentPositionModel.builder() + .iupd("IUPD-1").build())) + .build(); + Mockito.when(blobService.getUploadInput(anyString(), anyString(), eq(CREATE_UPLOAD_ID))).thenReturn(uploadInputCreate); + Mockito.when(blobService.getUploadInput(anyString(), anyString(), eq(DELETE_UPLOAD_ID))).thenReturn(uploadInputDelete); + return blobService; + } + @Bean + @Primary + public static StatusService statusService() { + StatusService statusService = Mockito.mock(StatusService.class); + Mockito.when(statusService.getStatus(anyString(), eq(DELETE_UPLOAD_ID))).thenReturn( + Status.builder() + .id("UPLOAD_KEY") + .brokerID("broker") + .fiscalCode("organization") + .upload(Upload.builder() + .current(0) + .total(1) + .responses(new ArrayList<>()) + .start(LocalDateTime.now().minusHours(1)) + .end(LocalDateTime.now()) + .build()) + .build() + ); + Mockito.when(statusService.getStatus(anyString(), eq(CREATE_UPLOAD_ID))).thenReturn( + Status.builder() + .id("UPLOAD_KEY") + .brokerID("broker") + .fiscalCode("organization") + .upload(Upload.builder() + .current(0) + .total(1) + .responses(new ArrayList<>()) + .start(LocalDateTime.now().minusHours(1)) + .end(LocalDateTime.now()) + .build()) + .build() + ); + Mockito.when(statusService.upsert(any())).thenReturn( + Status.builder() + .id("UPLOAD_KEY") + .brokerID("broker") + .fiscalCode("organization") + .upload(Upload.builder() + .current(1) + .total(1) + .start(LocalDateTime.now().minusHours(1)) + .end(LocalDateTime.now()) + .build()) + .build() + ); + return statusService; + } + @Bean + @Primary + public static GPDClient gpdClient() { + GPDClient gpdClient = Mockito.mock(GPDClient.class); + HttpResponse response = HttpResponse.notFound(); + Mockito.when(gpdClient.getDebtPosition(anyString(), anyString())).thenReturn(response); + return gpdClient; + } +} diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties index 73e9eb7..90b16b3 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -1,4 +1,5 @@ info.application.artifactId=gpd-massive-upload +info.application.title=GPD-Upload-API info.application.version=0.1.22 info.properties.environment=test openapi.application.version=0.1.22