From 31d575e4d8a65a6f68ad00ce9abfee59a1ca18be Mon Sep 17 00:00:00 2001 From: Alessio Cialini Date: Tue, 23 Apr 2024 19:13:30 +0200 Subject: [PATCH 01/19] [VAS-834] feat: initial commit --- Dockerfile | 11 +- pom.xml | 378 ++++++++++-------- .../microservice/config/OpenApiConfig.java | 129 ------ .../microservice/config/RequestFilter.java | 58 --- .../microservice/exception/AppError.java | 30 -- .../microservice/exception/ErrorHandler.java | 220 ---------- .../microservice/model/ProblemJson.java | 40 -- .../notice/generator}/Application.java | 2 +- .../generator}/config/LoggingAspect.java | 14 +- .../config/MappingsConfiguration.java | 2 +- .../generator/config/OpenApiConfig.java | 130 ++++++ .../generator/config/RequestFilter.java | 54 +++ .../generator}/config/ResponseValidator.java | 15 +- .../config/WebMvcConfiguration.java | 4 +- .../generator}/controller/HomeController.java | 2 +- .../NoticeGenerationController.java | 65 +++ .../generator/exception/Aes256Exception.java | 35 ++ .../notice/generator/exception/AppError.java | 38 ++ .../generator}/exception/AppException.java | 7 +- .../generator/exception/ErrorHandler.java | 179 +++++++++ .../model/AppCorsConfiguration.java | 2 +- .../notice/generator}/model/AppInfo.java | 2 +- .../model/NoticeGenerationRequestItem.java | 18 + .../notice/generator/model/ProblemJson.java | 42 ++ .../enums/PaymentGenerationRequestStatus.java | 13 + .../model/notice/CreditorInstitution.java | 38 ++ .../notice/generator/model/notice/Debtor.java | 19 + .../model/notice/InstallmentData.java | 14 + .../generator/model/notice/Installments.java | 16 + .../notice/generator/model/notice/Notice.java | 38 ++ .../model/notice/NoticeRequestData.java | 22 + ...ymentGenerationRequestErrorRepository.java | 16 + .../PaymentGenerationRequestRepository.java | 17 + .../service/NoticeGenerationService.java | 10 + .../service/NoticeGenerationServiceImpl.java | 105 +++++ .../storage/InstitutionsStorageClient.java | 119 ++++++ .../storage/NoticeStorageClient.java | 119 ++++++ .../storage/NoticeTemplateStorageClient.java | 120 ++++++ .../notice/generator/util/Aes256Utils.java | 98 +++++ .../notice/generator}/util/CommonUtility.java | 2 +- .../notice/generator}/util/Constants.java | 2 +- .../notice/generator}/ApplicationTest.java | 2 +- .../generator}/OpenApiGenerationTest.java | 2 +- 43 files changed, 1568 insertions(+), 681 deletions(-) delete mode 100644 src/main/java/it/gov/pagopa/microservice/config/OpenApiConfig.java delete mode 100644 src/main/java/it/gov/pagopa/microservice/config/RequestFilter.java delete mode 100644 src/main/java/it/gov/pagopa/microservice/exception/AppError.java delete mode 100644 src/main/java/it/gov/pagopa/microservice/exception/ErrorHandler.java delete mode 100644 src/main/java/it/gov/pagopa/microservice/model/ProblemJson.java rename src/main/java/it/gov/pagopa/{microservice => payment/notice/generator}/Application.java (77%) rename src/main/java/it/gov/pagopa/{microservice => payment/notice/generator}/config/LoggingAspect.java (93%) rename src/main/java/it/gov/pagopa/{microservice => payment/notice/generator}/config/MappingsConfiguration.java (89%) create mode 100644 src/main/java/it/gov/pagopa/payment/notice/generator/config/OpenApiConfig.java create mode 100644 src/main/java/it/gov/pagopa/payment/notice/generator/config/RequestFilter.java rename src/main/java/it/gov/pagopa/{microservice => payment/notice/generator}/config/ResponseValidator.java (80%) rename src/main/java/it/gov/pagopa/{microservice => payment/notice/generator}/config/WebMvcConfiguration.java (87%) rename src/main/java/it/gov/pagopa/{microservice => payment/notice/generator}/controller/HomeController.java (92%) create mode 100644 src/main/java/it/gov/pagopa/payment/notice/generator/controller/NoticeGenerationController.java create mode 100644 src/main/java/it/gov/pagopa/payment/notice/generator/exception/Aes256Exception.java create mode 100644 src/main/java/it/gov/pagopa/payment/notice/generator/exception/AppError.java rename src/main/java/it/gov/pagopa/{microservice => payment/notice/generator}/exception/AppException.java (96%) create mode 100644 src/main/java/it/gov/pagopa/payment/notice/generator/exception/ErrorHandler.java rename src/main/java/it/gov/pagopa/{microservice => payment/notice/generator}/model/AppCorsConfiguration.java (91%) rename src/main/java/it/gov/pagopa/{microservice => payment/notice/generator}/model/AppInfo.java (90%) create mode 100644 src/main/java/it/gov/pagopa/payment/notice/generator/model/NoticeGenerationRequestItem.java create mode 100644 src/main/java/it/gov/pagopa/payment/notice/generator/model/ProblemJson.java create mode 100644 src/main/java/it/gov/pagopa/payment/notice/generator/model/enums/PaymentGenerationRequestStatus.java create mode 100644 src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/CreditorInstitution.java create mode 100644 src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/Debtor.java create mode 100644 src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/InstallmentData.java create mode 100644 src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/Installments.java create mode 100644 src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/Notice.java create mode 100644 src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/NoticeRequestData.java create mode 100644 src/main/java/it/gov/pagopa/payment/notice/generator/repository/PaymentGenerationRequestErrorRepository.java create mode 100644 src/main/java/it/gov/pagopa/payment/notice/generator/repository/PaymentGenerationRequestRepository.java create mode 100644 src/main/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationService.java create mode 100644 src/main/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationServiceImpl.java create mode 100644 src/main/java/it/gov/pagopa/payment/notice/generator/storage/InstitutionsStorageClient.java create mode 100644 src/main/java/it/gov/pagopa/payment/notice/generator/storage/NoticeStorageClient.java create mode 100644 src/main/java/it/gov/pagopa/payment/notice/generator/storage/NoticeTemplateStorageClient.java create mode 100644 src/main/java/it/gov/pagopa/payment/notice/generator/util/Aes256Utils.java rename src/main/java/it/gov/pagopa/{microservice => payment/notice/generator}/util/CommonUtility.java (96%) rename src/main/java/it/gov/pagopa/{microservice => payment/notice/generator}/util/Constants.java (74%) rename src/test/java/it/gov/pagopa/{microservice => payment/notice/generator}/ApplicationTest.java (86%) rename src/test/java/it/gov/pagopa/{microservice => payment/notice/generator}/OpenApiGenerationTest.java (97%) diff --git a/Dockerfile b/Dockerfile index 65e6a12..7585a11 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,19 +1,18 @@ # # Build # -FROM maven:3.8.4-jdk-11-slim as buildtime +FROM maven:3.9.6-amazoncorretto-17-al2023@sha256:4c8bd9ec72b372f587f7b9d92564a307e4f5180b7ec08455fb346617bae1757e as buildtime WORKDIR /build COPY . . -RUN mvn clean package +RUN mvn clean package -Dmaven.test.skip=true -FROM adoptopenjdk/openjdk11:alpine-jre as builder +FROM amazoncorretto:17.0.10-alpine3.19@sha256:180e9c91bdbaad3599fedd2f492bf0d0335a9382835aa64669b2c2a8de7c9a22 as builder COPY --from=buildtime /build/target/*.jar application.jar RUN java -Djarmode=layertools -jar application.jar extract -FROM ghcr.io/pagopa/docker-base-springboot-openjdk11:v1.0.1@sha256:bbbe948e91efa0a3e66d8f308047ec255f64898e7f9250bdb63985efd3a95dbf -ADD --chown=spring:spring https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/download/v1.25.1/opentelemetry-javaagent.jar . +FROM ghcr.io/pagopa/docker-base-springboot-openjdk17:v1.1.3@sha256:a4e970ef05ecf2081424a64707e7c20856bbc40ddb3e99b32a24cd74591817c4 COPY --chown=spring:spring --from=builder dependencies/ ./ COPY --chown=spring:spring --from=builder snapshot-dependencies/ ./ @@ -24,4 +23,4 @@ COPY --chown=spring:spring --from=builder application/ ./ EXPOSE 8080 -ENTRYPOINT ["java","-javaagent:opentelemetry-javaagent.jar","--enable-preview","org.springframework.boot.loader.JarLauncher"] +ENTRYPOINT ["java","--enable-preview","org.springframework.boot.loader.launch.JarLauncher"] diff --git a/pom.xml b/pom.xml index 2c6ae01..52c15a2 100644 --- a/pom.xml +++ b/pom.xml @@ -1,175 +1,213 @@ - 4.0.0 - - - org.springframework.boot - spring-boot-starter-parent - 2.7.3 - - - it.gov.pagopa - microservice - 0.0.0 - Your Name - Your description - - - 11 - - - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-starter-validation - - - - - org.springframework.boot - spring-boot-devtools - runtime - true - - - org.springframework.boot - spring-boot-configuration-processor - true - - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.boot - spring-boot-starter-actuator - - - - org.springframework.boot - spring-boot-starter-data-jpa - - - - org.springframework.data - spring-data-jpa - - - - org.springframework.boot - spring-boot-starter-cache - - - com.github.ben-manes.caffeine - caffeine - - - - - org.springdoc - springdoc-openapi-ui - 1.6.11 - - - - com.h2database - h2 - runtime - - - - org.hibernate.orm - hibernate-core - 6.1.3.Final - - - - org.springframework.cloud - spring-cloud-starter-openfeign - 4.0.3 - - - - - org.modelmapper - modelmapper - 3.1.0 - - - org.projectlombok - lombok - true - - - - junit - junit - test - - - co.elastic.logging - logback-ecs-encoder - 1.5.0 - - - - - - + xmlns="http://maven.apache.org/POM/4.0.0" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + 4.0.0 + + org.springframework.boot - spring-boot-maven-plugin - - - - org.jacoco - jacoco-maven-plugin - 0.8.7 - - - - - - - - prepare-agent - - - - report - test - - report - - - - - - org.sonarsource.scanner.maven - sonar-maven-plugin - 3.3.0.603 - - - verify - - sonar - - - - - - - - src/test/resources - true - - - + spring-boot-starter-parent + 3.2.3 + + + it.gov.pagopa + print-payment-notice-generator + 0.0.0 + pagopa-print-payment-notice-generator + PagoPA Print Payment Notices Generator + + + 17 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-validation + + + + + org.springframework.boot + spring-boot-devtools + runtime + true + + + org.springframework.boot + spring-boot-configuration-processor + true + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-starter-actuator + + + com.azure.spring + spring-cloud-azure-starter-storage-blob + + + org.springframework.cloud + spring-cloud-starter-stream-kafka + + + org.springframework.boot + spring-boot-starter-cache + + + com.github.ben-manes.caffeine + caffeine + + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.3.0 + + + + + + io.swagger + swagger-annotations + 1.6.12 + + + + + org.hibernate.orm + hibernate-core + 6.4.0.Final + + + org.springframework.boot + spring-boot-starter-data-mongodb + + + de.flapdoodle.embed + de.flapdoodle.embed.mongo + 4.12.2 + test + + + + org.springframework.cloud + spring-cloud-starter-openfeign + 4.1.1 + + + + + org.modelmapper + modelmapper + 3.1.0 + + + org.projectlombok + lombok + true + + + + junit + junit + test + + + co.elastic.logging + logback-ecs-encoder + 1.5.0 + + + + + + + + org.springframework.cloud + spring-cloud-dependencies + 2023.0.1 + pom + import + + + com.azure.spring + spring-cloud-azure-dependencies + 5.11.0 + pom + import + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.jacoco + jacoco-maven-plugin + 0.8.7 + + + + + + + + prepare-agent + + + + report + test + + report + + + + + + org.sonarsource.scanner.maven + sonar-maven-plugin + 3.7.0.1746 + + + verify + + sonar + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 17 + 17 + + + + + + src/test/resources + true + + + diff --git a/src/main/java/it/gov/pagopa/microservice/config/OpenApiConfig.java b/src/main/java/it/gov/pagopa/microservice/config/OpenApiConfig.java deleted file mode 100644 index 6b7ec18..0000000 --- a/src/main/java/it/gov/pagopa/microservice/config/OpenApiConfig.java +++ /dev/null @@ -1,129 +0,0 @@ -package it.gov.pagopa.microservice.config; - -import static it.gov.pagopa.microservice.util.Constants.HEADER_REQUEST_ID; - -import io.swagger.v3.oas.models.Components; -import io.swagger.v3.oas.models.OpenAPI; -import io.swagger.v3.oas.models.Paths; -import io.swagger.v3.oas.models.headers.Header; -import io.swagger.v3.oas.models.info.Info; -import io.swagger.v3.oas.models.media.StringSchema; -import io.swagger.v3.oas.models.parameters.Parameter; -import io.swagger.v3.oas.models.responses.ApiResponses; -import io.swagger.v3.oas.models.security.SecurityScheme; -import java.util.Collections; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import org.springdoc.core.customizers.OpenApiCustomiser; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -public class OpenApiConfig { - - @Bean - public OpenAPI customOpenAPI( - @Value("${info.application.artifactId}") String appName, - @Value("${info.application.description}") String appDescription, - @Value("${info.application.version}") String appVersion) { - return new OpenAPI() - .components( - new Components() - .addSecuritySchemes( - "ApiKey", - new SecurityScheme() - .type(SecurityScheme.Type.APIKEY) - .description("The API key to access this function app.") - .name("Ocp-Apim-Subscription-Key") - .in(SecurityScheme.In.HEADER))) - .info( - new Info() - .title(appName) - .version(appVersion) - .description(appDescription) - .termsOfService("https://www.pagopa.gov.it/")); - } - - @Bean - public OpenApiCustomiser sortOperationsAlphabetically() { - return openApi -> { - Paths paths = - openApi - .getPaths() - .entrySet() - .stream() - .sorted(Map.Entry.comparingByKey()) - .collect( - Paths::new, - (map, item) -> map.addPathItem(item.getKey(), item.getValue()), - Paths::putAll); - - paths.forEach( - (key, value) -> - value - .readOperations() - .forEach( - operation -> { - var responses = - operation - .getResponses() - .entrySet() - .stream() - .sorted(Map.Entry.comparingByKey()) - .collect( - ApiResponses::new, - (map, item) -> - map.addApiResponse(item.getKey(), item.getValue()), - ApiResponses::putAll); - operation.setResponses(responses); - })); - openApi.setPaths(paths); - }; - } - - @Bean - public OpenApiCustomiser addCommonHeaders() { - return openApi -> - openApi - .getPaths() - .forEach( - (key, value) -> { - - // add Request-ID as request header - var header = - Optional.ofNullable(value.getParameters()) - .orElse(Collections.emptyList()) - .parallelStream() - .filter(Objects::nonNull) - .anyMatch(elem -> HEADER_REQUEST_ID.equals(elem.getName())); - if (!header) { - value.addParametersItem( - new Parameter() - .in("header") - .name(HEADER_REQUEST_ID) - .schema(new StringSchema()) - .description( - "This header identifies the call, if not passed it is self-generated. This ID is returned in the response.")); - } - - // add Request-ID as response header - value - .readOperations() - .forEach( - operation -> - operation - .getResponses() - .values() - .forEach( - response -> - response.addHeaderObject( - HEADER_REQUEST_ID, - new Header() - .schema(new StringSchema()) - .description( - "This header identifies the call")))); - }); - } -} diff --git a/src/main/java/it/gov/pagopa/microservice/config/RequestFilter.java b/src/main/java/it/gov/pagopa/microservice/config/RequestFilter.java deleted file mode 100644 index 63726e3..0000000 --- a/src/main/java/it/gov/pagopa/microservice/config/RequestFilter.java +++ /dev/null @@ -1,58 +0,0 @@ -package it.gov.pagopa.microservice.config; - -import static it.gov.pagopa.microservice.util.Constants.HEADER_REQUEST_ID; - -import java.io.IOException; -import java.util.UUID; -import javax.servlet.Filter; -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import lombok.extern.slf4j.Slf4j; -import org.slf4j.MDC; -import org.springframework.core.Ordered; -import org.springframework.core.annotation.Order; -import org.springframework.stereotype.Component; - -@Component -@Order(Ordered.HIGHEST_PRECEDENCE) -@Slf4j -public class RequestFilter implements Filter { - - /** - * Get the request ID from the custom header "X-Request-Id" if present, otherwise it generates - * one. Set the X-Request-Id value in the {@code response} and in the MDC - * - * @param request http request - * @param response http response - * @param chain next filter - * @throws IOException if an I/O error occurs during this filter's processing of the request - * @throws ServletException if the processing fails for any other reason - */ - @Override - public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) - throws IOException, ServletException { - try { - HttpServletRequest httRequest = (HttpServletRequest) request; - - // get requestId from header or generate one - String requestId = httRequest.getHeader(HEADER_REQUEST_ID); - if (requestId == null || requestId.isEmpty()) { - requestId = UUID.randomUUID().toString(); - } - - // set requestId in MDC - MDC.put("requestId", requestId); - - // set requestId in the response header - ((HttpServletResponse) response).setHeader(HEADER_REQUEST_ID, requestId); - chain.doFilter(request, response); - } finally { - MDC.clear(); - } - } - -} diff --git a/src/main/java/it/gov/pagopa/microservice/exception/AppError.java b/src/main/java/it/gov/pagopa/microservice/exception/AppError.java deleted file mode 100644 index d9ccb08..0000000 --- a/src/main/java/it/gov/pagopa/microservice/exception/AppError.java +++ /dev/null @@ -1,30 +0,0 @@ -package it.gov.pagopa.microservice.exception; - -import lombok.Getter; -import org.springframework.http.HttpStatus; - - -@Getter -public enum AppError { - INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "Internal Server Error", "Something was wrong"), - BAD_REQUEST(HttpStatus.INTERNAL_SERVER_ERROR, "Bad Request", "%s"), - UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "Unauthorized", "Error during authentication"), - FORBIDDEN(HttpStatus.FORBIDDEN, "Forbidden", "This method is forbidden"), - RESPONSE_NOT_READABLE(HttpStatus.BAD_GATEWAY, "Response Not Readable", "The response body is not readable"), - - UNKNOWN(null, null, null); - - - public final HttpStatus httpStatus; - public final String title; - public final String details; - - - AppError(HttpStatus httpStatus, String title, String details) { - this.httpStatus = httpStatus; - this.title = title; - this.details = details; - } -} - - diff --git a/src/main/java/it/gov/pagopa/microservice/exception/ErrorHandler.java b/src/main/java/it/gov/pagopa/microservice/exception/ErrorHandler.java deleted file mode 100644 index 3fd7e49..0000000 --- a/src/main/java/it/gov/pagopa/microservice/exception/ErrorHandler.java +++ /dev/null @@ -1,220 +0,0 @@ -package it.gov.pagopa.microservice.exception; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import feign.FeignException; -import it.gov.pagopa.microservice.model.ProblemJson; -import java.util.ArrayList; -import java.util.List; -import lombok.extern.slf4j.Slf4j; -import org.hibernate.exception.ConstraintViolationException; -import org.springframework.beans.TypeMismatchException; -import org.springframework.dao.DataIntegrityViolationException; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.http.converter.HttpMessageNotReadableException; -import org.springframework.validation.FieldError; -import org.springframework.web.bind.MethodArgumentNotValidException; -import org.springframework.web.bind.MissingServletRequestParameterException; -import org.springframework.web.bind.annotation.ControllerAdvice; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.context.request.WebRequest; -import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; -import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; - -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.List; - -/** - * All Exceptions are handled by this class - */ -@ControllerAdvice -@Slf4j -public class ErrorHandler extends ResponseEntityExceptionHandler { - - /** - * Handle if the input request is not a valid JSON - * - * @param ex {@link HttpMessageNotReadableException} exception raised - * @param headers of the response - * @param status of the response - * @param request from frontend - * @return a {@link ProblemJson} as response with the cause and with a 400 as HTTP status - */ - @Override - public ResponseEntity handleHttpMessageNotReadable(HttpMessageNotReadableException ex, - HttpHeaders headers, HttpStatus status, WebRequest request) { - log.warn("Input not readable: ", ex); - var errorResponse = ProblemJson.builder() - .status(HttpStatus.BAD_REQUEST.value()) - .title(AppError.BAD_REQUEST.getTitle()) - .detail("Invalid input format") - .build(); - return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); - } - - /** - * Handle if missing some request parameters in the request - * - * @param ex {@link MissingServletRequestParameterException} exception raised - * @param headers of the response - * @param status of the response - * @param request from frontend - * @return a {@link ProblemJson} as response with the cause and with a 400 as HTTP status - */ - @Override - public ResponseEntity handleMissingServletRequestParameter( - MissingServletRequestParameterException ex, HttpHeaders headers, HttpStatus status, - WebRequest request) { - log.warn("Missing request parameter: ", ex); - var errorResponse = ProblemJson.builder() - .status(HttpStatus.BAD_REQUEST.value()) - .title(AppError.BAD_REQUEST.getTitle()) - .detail(ex.getMessage()) - .build(); - return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); - } - - - /** - * Customize the response for TypeMismatchException. - * - * @param ex the exception - * @param headers the headers to be written to the response - * @param status the selected response status - * @param request the current request - * @return a {@code ResponseEntity} instance - */ - @Override - protected ResponseEntity handleTypeMismatch(TypeMismatchException ex, HttpHeaders headers, - HttpStatus status, WebRequest request) { - log.warn("Type mismatch: ", ex); - var errorResponse = ProblemJson.builder() - .status(HttpStatus.BAD_REQUEST.value()) - .title(AppError.BAD_REQUEST.getTitle()) - .detail(String.format("Invalid value %s for property %s", ex.getValue(), - ((MethodArgumentTypeMismatchException) ex).getName())) - .build(); - return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); - } - - /** - * Handle if validation constraints are unsatisfied - * - * @param ex {@link MethodArgumentNotValidException} exception raised - * @param headers of the response - * @param status of the response - * @param request from frontend - * @return a {@link ProblemJson} as response with the cause and with a 400 as HTTP status - */ - @Override - protected ResponseEntity handleMethodArgumentNotValid(MethodArgumentNotValidException ex, - HttpHeaders headers, HttpStatus status, WebRequest request) { - List details = new ArrayList<>(); - for (FieldError error : ex.getBindingResult().getFieldErrors()) { - details.add(error.getField() + ": " + error.getDefaultMessage()); - } - var detailsMessage = String.join(", ", details); - log.warn("Input not valid: " + detailsMessage); - var errorResponse = ProblemJson.builder() - .status(HttpStatus.BAD_REQUEST.value()) - .title(AppError.BAD_REQUEST.getTitle()) - .detail(detailsMessage) - .build(); - return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); - } - - @ExceptionHandler({javax.validation.ConstraintViolationException.class}) - public ResponseEntity handleConstraintViolationException( - final javax.validation.ConstraintViolationException ex, final WebRequest request) { - log.warn("Validation Error raised:", ex); - var errorResponse = ProblemJson.builder() - .status(HttpStatus.BAD_REQUEST.value()) - .title(AppError.BAD_REQUEST.getTitle()) - .detail(ex.getMessage()) - .build(); - return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); - } - - - /** - * Handle if a {@link FeignException} is raised - * - * @param ex {@link FeignException} exception raised - * @param request from frontend - * @return a {@link ProblemJson} as response with the cause and with an appropriated HTTP status - */ - @ExceptionHandler({FeignException.class}) - public ResponseEntity handleFeignException(final FeignException ex, final WebRequest request) { - log.warn("FeignException raised: ", ex); - - ProblemJson problem; - if(ex.responseBody().isPresent()) { - var body = new String(ex.responseBody().get().array(), StandardCharsets.UTF_8); - try { - problem = new ObjectMapper().readValue(body, ProblemJson.class); - } catch (JsonProcessingException e) { - problem = ProblemJson.builder() - .status(HttpStatus.BAD_GATEWAY.value()) - .title(AppError.RESPONSE_NOT_READABLE.getTitle()) - .detail(AppError.RESPONSE_NOT_READABLE.getDetails()) - .build(); - } - } else { - problem = ProblemJson.builder() - .status(HttpStatus.BAD_GATEWAY.value()) - .title("No Response Body") - .detail("Error with external dependency") - .build(); - } - - return new ResponseEntity<>(problem, HttpStatus.valueOf(problem.getStatus())); - } - - - /** - * Handle if a {@link AppException} is raised - * - * @param ex {@link AppException} exception raised - * @param request from frontend - * @return a {@link ProblemJson} as response with the cause and with an appropriated HTTP status - */ - @ExceptionHandler({AppException.class}) - public ResponseEntity handleAppException(final AppException ex, - final WebRequest request) { - if (ex.getCause() != null) { - log.warn("App Exception raised: " + ex.getMessage() + "\nCause of the App Exception: ", - ex.getCause()); - } else { - log.warn("App Exception raised: ", ex); - } - var errorResponse = ProblemJson.builder() - .status(ex.getHttpStatus().value()) - .title(ex.getTitle()) - .detail(ex.getMessage()) - .build(); - return new ResponseEntity<>(errorResponse, ex.getHttpStatus()); - } - - - /** - * Handle if a {@link Exception} is raised - * - * @param ex {@link Exception} exception raised - * @param request from frontend - * @return a {@link ProblemJson} as response with the cause and with 500 as HTTP status - */ - @ExceptionHandler({Exception.class}) - public ResponseEntity handleGenericException(final Exception ex, - final WebRequest request) { - log.error("Generic Exception raised:", ex); - var errorResponse = ProblemJson.builder() - .status(HttpStatus.INTERNAL_SERVER_ERROR.value()) - .title(AppError.INTERNAL_SERVER_ERROR.getTitle()) - .detail(ex.getMessage()) - .build(); - return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR); - } -} diff --git a/src/main/java/it/gov/pagopa/microservice/model/ProblemJson.java b/src/main/java/it/gov/pagopa/microservice/model/ProblemJson.java deleted file mode 100644 index db11a37..0000000 --- a/src/main/java/it/gov/pagopa/microservice/model/ProblemJson.java +++ /dev/null @@ -1,40 +0,0 @@ -package it.gov.pagopa.microservice.model; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; -import io.swagger.v3.oas.annotations.media.Schema; -import javax.validation.constraints.Max; -import javax.validation.constraints.Min; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.ToString; - -/** - * Object returned as response in case of an error. - *

See {@link it.pagopa.microservice.exception.ErrorHandler} - */ -@Data -@Builder(toBuilder = true) -@NoArgsConstructor -@AllArgsConstructor -@ToString -@JsonIgnoreProperties(ignoreUnknown = true) -public class ProblemJson { - - @JsonProperty("title") - @Schema(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") - private String title; - - @JsonProperty("status") - @Schema(example = "200", description = "The HTTP status code generated by the origin server for this occurrence of the problem.") - @Min(100) - @Max(600) - private Integer status; - - @JsonProperty("detail") - @Schema(example = "There was an error processing the request", description = "A human readable explanation specific to this occurrence of the problem.") - private String detail; - -} diff --git a/src/main/java/it/gov/pagopa/microservice/Application.java b/src/main/java/it/gov/pagopa/payment/notice/generator/Application.java similarity index 77% rename from src/main/java/it/gov/pagopa/microservice/Application.java rename to src/main/java/it/gov/pagopa/payment/notice/generator/Application.java index 465b6cb..3bda102 100644 --- a/src/main/java/it/gov/pagopa/microservice/Application.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/Application.java @@ -1,4 +1,4 @@ -package it.gov.pagopa.microservice; // TODO: refactor the package +package it.gov.pagopa.payment.notice.generator; // TODO: refactor the package import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; diff --git a/src/main/java/it/gov/pagopa/microservice/config/LoggingAspect.java b/src/main/java/it/gov/pagopa/payment/notice/generator/config/LoggingAspect.java similarity index 93% rename from src/main/java/it/gov/pagopa/microservice/config/LoggingAspect.java rename to src/main/java/it/gov/pagopa/payment/notice/generator/config/LoggingAspect.java index 8990632..4e699c2 100644 --- a/src/main/java/it/gov/pagopa/microservice/config/LoggingAspect.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/config/LoggingAspect.java @@ -1,7 +1,10 @@ -package it.gov.pagopa.microservice.config; +package it.gov.pagopa.payment.notice.generator.config; -import it.gov.pagopa.microservice.exception.AppError; -import it.gov.pagopa.microservice.model.ProblemJson; +import it.gov.pagopa.payment.notice.generator.exception.AppError; +import it.gov.pagopa.payment.notice.generator.model.ProblemJson; +import jakarta.annotation.PostConstruct; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; @@ -16,14 +19,11 @@ import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; -import javax.annotation.PostConstruct; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; import java.util.HashMap; import java.util.Map; import java.util.UUID; -import static it.gov.pagopa.microservice.util.CommonUtility.deNull; +import static it.gov.pagopa.payment.notice.generator.util.CommonUtility.deNull; @Aspect diff --git a/src/main/java/it/gov/pagopa/microservice/config/MappingsConfiguration.java b/src/main/java/it/gov/pagopa/payment/notice/generator/config/MappingsConfiguration.java similarity index 89% rename from src/main/java/it/gov/pagopa/microservice/config/MappingsConfiguration.java rename to src/main/java/it/gov/pagopa/payment/notice/generator/config/MappingsConfiguration.java index b403822..805ce6f 100644 --- a/src/main/java/it/gov/pagopa/microservice/config/MappingsConfiguration.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/config/MappingsConfiguration.java @@ -1,4 +1,4 @@ -package it.gov.pagopa.microservice.config; +package it.gov.pagopa.payment.notice.generator.config; import org.modelmapper.ModelMapper; diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/config/OpenApiConfig.java b/src/main/java/it/gov/pagopa/payment/notice/generator/config/OpenApiConfig.java new file mode 100644 index 0000000..1622704 --- /dev/null +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/config/OpenApiConfig.java @@ -0,0 +1,130 @@ +package it.gov.pagopa.payment.notice.generator.config; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.Paths; +import io.swagger.v3.oas.models.headers.Header; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.media.StringSchema; +import io.swagger.v3.oas.models.parameters.Parameter; +import io.swagger.v3.oas.models.responses.ApiResponses; +import io.swagger.v3.oas.models.security.SecurityScheme; +import org.springdoc.core.customizers.GlobalOpenApiCustomizer; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.Collections; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +import static it.gov.pagopa.payment.notice.generator.util.Constants.HEADER_REQUEST_ID; + +@Configuration +public class OpenApiConfig { + + @Bean + public OpenAPI customOpenAPI( + @Value("${info.application.name}") String appName, + @Value("${info.application.description}") String appDescription, + @Value("${info.application.version}") String appVersion) { + return new OpenAPI() + .components( + new Components() + .addSecuritySchemes( + "ApiKey", + new SecurityScheme() + .type(SecurityScheme.Type.APIKEY) + .description("The API key to access this function app.") + .name("Ocp-Apim-Subscription-Key") + .in(SecurityScheme.In.HEADER))) + .info( + new Info() + .title(appName) + .version(appVersion) + .description(appDescription) + .termsOfService("https://www.pagopa.gov.it/")); + } + + @Bean + public GlobalOpenApiCustomizer sortOperationsAlphabetically() { + return openApi -> { + Paths paths = + openApi + .getPaths() + .entrySet() + .stream() + .sorted(Map.Entry.comparingByKey()) + .collect( + Paths::new, + (map, item) -> map.addPathItem(item.getKey(), item.getValue()), + Paths::putAll); + + paths.forEach( + (key, value) -> + value + .readOperations() + .forEach( + operation -> { + var responses = + operation + .getResponses() + .entrySet() + .stream() + .sorted(Map.Entry.comparingByKey()) + .collect( + ApiResponses::new, + (map, item) -> + map.addApiResponse(item.getKey(), item.getValue()), + ApiResponses::putAll); + operation.setResponses(responses); + })); + openApi.setPaths(paths); + }; + } + + @Bean + public GlobalOpenApiCustomizer addCommonHeaders() { + return openApi -> + openApi + .getPaths() + .forEach( + (key, value) -> { + + // add Request-ID as request header + var header = + Optional.ofNullable(value.getParameters()) + .orElse(Collections.emptyList()) + .parallelStream() + .filter(Objects::nonNull) + .anyMatch(elem -> HEADER_REQUEST_ID.equals(elem.getName())); + if(!header) { + value.addParametersItem( + new Parameter() + .in("header") + .name(HEADER_REQUEST_ID) + .schema(new StringSchema()) + .description( + "This header identifies the call, if not passed it is self-generated. This ID is returned in the response.")); + } + + // add Request-ID as response header + value + .readOperations() + .forEach( + operation -> + operation + .getResponses() + .values() + .forEach( + response -> + response.addHeaderObject( + HEADER_REQUEST_ID, + new Header() + .schema(new StringSchema()) + .description( + "This header identifies the call")))); + }); + } +} diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/config/RequestFilter.java b/src/main/java/it/gov/pagopa/payment/notice/generator/config/RequestFilter.java new file mode 100644 index 0000000..cd39605 --- /dev/null +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/config/RequestFilter.java @@ -0,0 +1,54 @@ +package it.gov.pagopa.payment.notice.generator.config; + +import it.gov.pagopa.payment.notice.generator.util.Constants; +import jakarta.servlet.*; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.slf4j.MDC; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.util.UUID; + +@Component +@Order(Ordered.HIGHEST_PRECEDENCE) +@Slf4j +public class RequestFilter implements Filter { + + /** + * Get the request ID from the custom header "X-Request-Id" if present, otherwise it generates + * one. Set the X-Request-Id value in the {@code response} and in the MDC + * + * @param request http request + * @param response http response + * @param chain next filter + * @throws IOException if an I/O error occurs during this filter's processing of the request + * @throws ServletException if the processing fails for any other reason + */ + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + try { + HttpServletRequest httRequest = (HttpServletRequest) request; + + // get requestId from header or generate one + String requestId = httRequest.getHeader(Constants.HEADER_REQUEST_ID); + if(requestId == null || requestId.isEmpty()) { + requestId = UUID.randomUUID().toString(); + } + + // set requestId in MDC + MDC.put("requestId", requestId); + + // set requestId in the response header + ((HttpServletResponse) response).setHeader(Constants.HEADER_REQUEST_ID, requestId); + chain.doFilter(request, response); + } finally { + MDC.clear(); + } + } + +} diff --git a/src/main/java/it/gov/pagopa/microservice/config/ResponseValidator.java b/src/main/java/it/gov/pagopa/payment/notice/generator/config/ResponseValidator.java similarity index 80% rename from src/main/java/it/gov/pagopa/microservice/config/ResponseValidator.java rename to src/main/java/it/gov/pagopa/payment/notice/generator/config/ResponseValidator.java index 67cc3af..c1a5b87 100644 --- a/src/main/java/it/gov/pagopa/microservice/config/ResponseValidator.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/config/ResponseValidator.java @@ -1,9 +1,8 @@ -package it.gov.pagopa.microservice.config; +package it.gov.pagopa.payment.notice.generator.config; -import it.gov.pagopa.microservice.exception.AppException; -import java.util.Set; -import javax.validation.ConstraintViolation; -import javax.validation.Validator; +import it.gov.pagopa.payment.notice.generator.exception.AppException; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validator; import org.apache.commons.lang3.StringUtils; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.AfterReturning; @@ -13,6 +12,8 @@ import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; +import java.util.Set; + @Aspect @Component public class ResponseValidator { @@ -22,13 +23,13 @@ public class ResponseValidator { /** - * This method validates the response annotated with the {@link javax.validation.constraints} + * This method validates the response annotated with the {@link jakarta.validation.constraints} * * @param joinPoint not used * @param result the response to validate */ // TODO: set your package - @AfterReturning(pointcut = "execution(* it.gov.pagopa.microservice.controller.*.*(..))", returning = "result") + @AfterReturning(pointcut = "execution(* it.gov.pagopa.payment.notices.service.controller.*.*(..))", returning = "result") public void validateResponse(JoinPoint joinPoint, Object result) { if (result instanceof ResponseEntity) { validateResponse((ResponseEntity) result); diff --git a/src/main/java/it/gov/pagopa/microservice/config/WebMvcConfiguration.java b/src/main/java/it/gov/pagopa/payment/notice/generator/config/WebMvcConfiguration.java similarity index 87% rename from src/main/java/it/gov/pagopa/microservice/config/WebMvcConfiguration.java rename to src/main/java/it/gov/pagopa/payment/notice/generator/config/WebMvcConfiguration.java index c61db33..64d6ce4 100644 --- a/src/main/java/it/gov/pagopa/microservice/config/WebMvcConfiguration.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/config/WebMvcConfiguration.java @@ -1,7 +1,7 @@ -package it.gov.pagopa.microservice.config; +package it.gov.pagopa.payment.notice.generator.config; import com.fasterxml.jackson.databind.ObjectMapper; -import it.gov.pagopa.microservice.model.AppCorsConfiguration; +import it.gov.pagopa.payment.notice.generator.model.AppCorsConfiguration; import lombok.SneakyThrows; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; diff --git a/src/main/java/it/gov/pagopa/microservice/controller/HomeController.java b/src/main/java/it/gov/pagopa/payment/notice/generator/controller/HomeController.java similarity index 92% rename from src/main/java/it/gov/pagopa/microservice/controller/HomeController.java rename to src/main/java/it/gov/pagopa/payment/notice/generator/controller/HomeController.java index 70b6dd1..5c1964d 100644 --- a/src/main/java/it/gov/pagopa/microservice/controller/HomeController.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/controller/HomeController.java @@ -1,4 +1,4 @@ -package it.gov.pagopa.microservice.controller; +package it.gov.pagopa.payment.notice.generator.controller; import io.swagger.v3.oas.annotations.Hidden; import org.springframework.beans.factory.annotation.Value; diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/controller/NoticeGenerationController.java b/src/main/java/it/gov/pagopa/payment/notice/generator/controller/NoticeGenerationController.java new file mode 100644 index 0000000..9b83c04 --- /dev/null +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/controller/NoticeGenerationController.java @@ -0,0 +1,65 @@ +package it.gov.pagopa.payment.notice.generator.controller; + +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import it.gov.pagopa.payment.notice.generator.model.NoticeGenerationRequestItem; +import it.gov.pagopa.payment.notice.generator.service.NoticeGenerationService; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.FileUtils; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; + +@RestController +@RequestMapping(value = "/notices", produces = MediaType.APPLICATION_JSON_VALUE) +@Validated +@Slf4j +@Tag(name = "Notice Generation APIs") +public class NoticeGenerationController { + + private final NoticeGenerationService noticeGenerationService; + + public NoticeGenerationController(NoticeGenerationService noticeGenerationService) { + this.noticeGenerationService = noticeGenerationService; + } + + @PostMapping("/generate") + public ResponseEntity generateNotice( + @Valid @NotNull @RequestParam("folderId") String folderId, + @Parameter(description = "templateId to use for retrieval") + @Valid @NotNull @RequestBody NoticeGenerationRequestItem noticeGenerationRequestItem) { + File file = noticeGenerationService.generateNotice(noticeGenerationRequestItem, folderId); + try (BufferedInputStream inputStream = new BufferedInputStream(new FileInputStream(file))) { + HttpHeaders headers = new HttpHeaders(); + headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + file.getName() + ".pdf\""); + return ResponseEntity.ok().contentType(MediaType.APPLICATION_OCTET_STREAM) + .headers(headers) + .body(new ByteArrayResource(inputStream.readAllBytes())); + } catch (IOException e) { + throw new RuntimeException(e); + } finally { + clearTempDirectory(file.toPath().getParent()); + } + } + + private void clearTempDirectory(java.nio.file.Path workingDirPath) { + try { + FileUtils.deleteDirectory(workingDirPath.toFile()); + } catch (IOException e) { + log.warn("Unable to clear working directory", e); + } + } + + +} diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/exception/Aes256Exception.java b/src/main/java/it/gov/pagopa/payment/notice/generator/exception/Aes256Exception.java new file mode 100644 index 0000000..86c1836 --- /dev/null +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/exception/Aes256Exception.java @@ -0,0 +1,35 @@ +package it.gov.pagopa.payment.notice.generator.exception; + +import lombok.Getter; + +/** + * Thrown in case an error occur when encrypting or decrypting a BizEvent + */ +@Getter +public class Aes256Exception extends Exception{ + private final int statusCode; + + /** + * Constructs new exception with provided message + * + * @param message Detail message + * @param statusCode status code + */ + public Aes256Exception(String message, int statusCode) { + super(message); + this.statusCode = statusCode; + } + + /** + * Constructs new exception with provided message + * + * @param message Detail message + * @param statusCode status code + * @param cause Exception causing the constructed one + */ + public Aes256Exception(String message, int statusCode, Throwable cause) { + super(message, cause); + this.statusCode = statusCode; + } + +} diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/exception/AppError.java b/src/main/java/it/gov/pagopa/payment/notice/generator/exception/AppError.java new file mode 100644 index 0000000..814fb1e --- /dev/null +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/exception/AppError.java @@ -0,0 +1,38 @@ +package it.gov.pagopa.payment.notice.generator.exception; + +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +public enum AppError { + INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "Internal Server Error", "Something was wrong"), + BAD_REQUEST(HttpStatus.INTERNAL_SERVER_ERROR, "Bad Request", "%s"), + UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "Unauthorized", "Error during authentication"), + FORBIDDEN(HttpStatus.FORBIDDEN, "Forbidden", "This method is forbidden"), + RESPONSE_NOT_READABLE(HttpStatus.BAD_GATEWAY, "Response Not Readable", "The response body is not readable"), + + TEMPLATE_NOT_FOUND(HttpStatus.NOT_FOUND, "Template Not Found", + "Required template has not been found on the storage"), + TEMPLATE_CLIENT_UNAVAILABLE(HttpStatus.SERVICE_UNAVAILABLE, + "Template Storage Not Available", + "Template Storage client temporarily not available"), + TEMPLATE_CLIENT_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "Template Client Error", + "Template Client encountered an error"), + + FOLDER_NOT_AVAILABLE(HttpStatus.NOT_FOUND, "Folder Not Available", + "Required folder is either missing or not available to the requirer"), + UNKNOWN(null, null, null); + + public final HttpStatus httpStatus; + public final String title; + public final String details; + + + AppError(HttpStatus httpStatus, String title, String details) { + this.httpStatus = httpStatus; + this.title = title; + this.details = details; + } +} + + diff --git a/src/main/java/it/gov/pagopa/microservice/exception/AppException.java b/src/main/java/it/gov/pagopa/payment/notice/generator/exception/AppException.java similarity index 96% rename from src/main/java/it/gov/pagopa/microservice/exception/AppException.java rename to src/main/java/it/gov/pagopa/payment/notice/generator/exception/AppException.java index e16fd36..43d4489 100644 --- a/src/main/java/it/gov/pagopa/microservice/exception/AppException.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/exception/AppException.java @@ -1,12 +1,13 @@ -package it.gov.pagopa.microservice.exception; +package it.gov.pagopa.payment.notice.generator.exception; -import java.util.Formatter; -import javax.validation.constraints.NotNull; +import jakarta.validation.constraints.NotNull; import lombok.EqualsAndHashCode; import lombok.Value; import org.springframework.http.HttpStatus; import org.springframework.validation.annotation.Validated; +import java.util.Formatter; + /** * Custom exception. *

See {@link ErrorHandler} diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/exception/ErrorHandler.java b/src/main/java/it/gov/pagopa/payment/notice/generator/exception/ErrorHandler.java new file mode 100644 index 0000000..924f613 --- /dev/null +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/exception/ErrorHandler.java @@ -0,0 +1,179 @@ +package it.gov.pagopa.payment.notice.generator.exception; + +import it.gov.pagopa.payment.notice.generator.model.ProblemJson; +import jakarta.validation.ConstraintViolationException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.TypeMismatchException; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingServletRequestParameterException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +import java.util.ArrayList; +import java.util.List; + +/** + * All Exceptions are handled by this class + */ +@ControllerAdvice +@Slf4j +public class ErrorHandler extends ResponseEntityExceptionHandler { + + /** + * Handle if the input request is not a valid JSON + * + * @param ex {@link HttpMessageNotReadableException} exception raised + * @param headers of the response + * @param status of the response + * @param request from frontend + * @return a {@link ProblemJson} as response with the cause and with a 400 as HTTP status + */ + @Override + public ResponseEntity handleHttpMessageNotReadable(HttpMessageNotReadableException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) { + log.warn("Input not readable: ", ex); + var errorResponse = ProblemJson.builder() + .status(HttpStatus.BAD_REQUEST.value()) + .title(AppError.BAD_REQUEST.getTitle()) + .detail("Invalid input format") + .build(); + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } + + /** + * Handle if missing some request parameters in the request + * + * @param ex {@link MissingServletRequestParameterException} exception raised + * @param headers of the response + * @param status of the response + * @param request from frontend + * @return a {@link ProblemJson} as response with the cause and with a 400 as HTTP status + */ + @Override + public ResponseEntity handleMissingServletRequestParameter( + MissingServletRequestParameterException ex, HttpHeaders headers, HttpStatusCode status, + WebRequest request) { + log.warn("Missing request parameter: ", ex); + var errorResponse = ProblemJson.builder() + .status(HttpStatus.BAD_REQUEST.value()) + .title(AppError.BAD_REQUEST.getTitle()) + .detail(ex.getMessage()) + .build(); + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } + + + /** + * Customize the response for TypeMismatchException. + * + * @param ex the exception + * @param headers the headers to be written to the response + * @param status the selected response status + * @param request the current request + * @return a {@code ResponseEntity} instance + */ + @Override + protected ResponseEntity handleTypeMismatch(TypeMismatchException ex, HttpHeaders headers, + HttpStatusCode status, WebRequest request) { + log.warn("Type mismatch: ", ex); + var errorResponse = ProblemJson.builder() + .status(HttpStatus.BAD_REQUEST.value()) + .title(AppError.BAD_REQUEST.getTitle()) + .detail(String.format("Invalid value %s for property %s", ex.getValue(), + ((MethodArgumentTypeMismatchException) ex).getName())) + .build(); + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } + + /** + * Handle if validation constraints are unsatisfied + * + * @param ex {@link MethodArgumentNotValidException} exception raised + * @param headers of the response + * @param status of the response + * @param request from frontend + * @return a {@link ProblemJson} as response with the cause and with a 400 as HTTP status + */ + @Override + protected ResponseEntity handleMethodArgumentNotValid(MethodArgumentNotValidException ex, + HttpHeaders headers, HttpStatusCode status, WebRequest request) { + List details = new ArrayList<>(); + for (FieldError error : ex.getBindingResult().getFieldErrors()) { + details.add(error.getField() + ": " + error.getDefaultMessage()); + } + var detailsMessage = String.join(", ", details); + log.warn("Input not valid: " + detailsMessage); + var errorResponse = ProblemJson.builder() + .status(HttpStatus.BAD_REQUEST.value()) + .title(AppError.BAD_REQUEST.getTitle()) + .detail(detailsMessage) + .build(); + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler({ConstraintViolationException.class}) + public ResponseEntity handleConstraintViolationException( + final ConstraintViolationException ex, final WebRequest request) { + log.warn("Validation Error raised:", ex); + var errorResponse = ProblemJson.builder() + .status(HttpStatus.BAD_REQUEST.value()) + .title(AppError.BAD_REQUEST.getTitle()) + .detail(ex.getMessage()) + .build(); + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } + + + + /** + * Handle if a {@link AppException} is raised + * + * @param ex {@link AppException} exception raised + * @param request from frontend + * @return a {@link ProblemJson} as response with the cause and with an appropriated HTTP status + */ + @ExceptionHandler({AppException.class}) + public ResponseEntity handleAppException(final AppException ex, + final WebRequest request) { + if(ex.getCause() != null) { + log.warn("App Exception raised: " + ex.getMessage() + "\nCause of the App Exception: ", + ex.getCause()); + } else { + log.warn("App Exception raised: ", ex); + } + var errorResponse = ProblemJson.builder() + .status(ex.getHttpStatus().value()) + .title(ex.getTitle()) + .detail(ex.getMessage()) + .build(); + return new ResponseEntity<>(errorResponse, ex.getHttpStatus()); + } + + + /** + * Handle if a {@link Exception} is raised + * + * @param ex {@link Exception} exception raised + * @param request from frontend + * @return a {@link ProblemJson} as response with the cause and with 500 as HTTP status + */ + @ExceptionHandler({Exception.class}) + public ResponseEntity handleGenericException(final Exception ex, + final WebRequest request) { + log.error("Generic Exception raised:", ex); + var errorResponse = ProblemJson.builder() + .status(HttpStatus.INTERNAL_SERVER_ERROR.value()) + .title(AppError.INTERNAL_SERVER_ERROR.getTitle()) + .detail(ex.getMessage()) + .build(); + return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR); + } +} diff --git a/src/main/java/it/gov/pagopa/microservice/model/AppCorsConfiguration.java b/src/main/java/it/gov/pagopa/payment/notice/generator/model/AppCorsConfiguration.java similarity index 91% rename from src/main/java/it/gov/pagopa/microservice/model/AppCorsConfiguration.java rename to src/main/java/it/gov/pagopa/payment/notice/generator/model/AppCorsConfiguration.java index 8d99b89..a92e6fa 100644 --- a/src/main/java/it/gov/pagopa/microservice/model/AppCorsConfiguration.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/model/AppCorsConfiguration.java @@ -1,4 +1,4 @@ -package it.gov.pagopa.microservice.model; +package it.gov.pagopa.payment.notice.generator.model; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; diff --git a/src/main/java/it/gov/pagopa/microservice/model/AppInfo.java b/src/main/java/it/gov/pagopa/payment/notice/generator/model/AppInfo.java similarity index 90% rename from src/main/java/it/gov/pagopa/microservice/model/AppInfo.java rename to src/main/java/it/gov/pagopa/payment/notice/generator/model/AppInfo.java index d381de0..571f949 100644 --- a/src/main/java/it/gov/pagopa/microservice/model/AppInfo.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/model/AppInfo.java @@ -1,4 +1,4 @@ -package it.gov.pagopa.microservice.model; +package it.gov.pagopa.payment.notice.generator.model; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import lombok.AccessLevel; diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/model/NoticeGenerationRequestItem.java b/src/main/java/it/gov/pagopa/payment/notice/generator/model/NoticeGenerationRequestItem.java new file mode 100644 index 0000000..647ca12 --- /dev/null +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/model/NoticeGenerationRequestItem.java @@ -0,0 +1,18 @@ +package it.gov.pagopa.payment.notice.generator.model; + +import it.gov.pagopa.payment.notice.generator.model.notice.NoticeRequestData; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class NoticeGenerationRequestItem { + + private String templateId; + private NoticeRequestData data; + +} diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/model/ProblemJson.java b/src/main/java/it/gov/pagopa/payment/notice/generator/model/ProblemJson.java new file mode 100644 index 0000000..4c9ad83 --- /dev/null +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/model/ProblemJson.java @@ -0,0 +1,42 @@ +package it.gov.pagopa.payment.notice.generator.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; + +import it.gov.pagopa.payment.notice.generator.exception.ErrorHandler; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * Object returned as response in case of an error. + *

See {@link ErrorHandler} + */ +@Data +@Builder(toBuilder = true) +@NoArgsConstructor +@AllArgsConstructor +@ToString +@JsonIgnoreProperties(ignoreUnknown = true) +public class ProblemJson { + + @JsonProperty("title") + @Schema(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") + private String title; + + @JsonProperty("status") + @Schema(example = "200", description = "The HTTP status code generated by the origin server for this occurrence of the problem.") + @Min(100) + @Max(600) + private Integer status; + + @JsonProperty("detail") + @Schema(example = "There was an error processing the request", description = "A human readable explanation specific to this occurrence of the problem.") + private String detail; + +} diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/model/enums/PaymentGenerationRequestStatus.java b/src/main/java/it/gov/pagopa/payment/notice/generator/model/enums/PaymentGenerationRequestStatus.java new file mode 100644 index 0000000..9b5f56b --- /dev/null +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/model/enums/PaymentGenerationRequestStatus.java @@ -0,0 +1,13 @@ +package it.gov.pagopa.payment.notice.generator.model.enums; + +/** + * Enum containing generation request status + */ +public enum PaymentGenerationRequestStatus { + + INSERTED, + PROCESSING, + FAILED, + PROCESSED + +} diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/CreditorInstitution.java b/src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/CreditorInstitution.java new file mode 100644 index 0000000..00c77ca --- /dev/null +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/CreditorInstitution.java @@ -0,0 +1,38 @@ +package it.gov.pagopa.payment.notice.generator.model.notice; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; + +public class CreditorInstitution { + + @Schema(description = "CI tax code", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull + @NotEmpty + private String taxCode; + + @Schema(description = "CI full name", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull + @NotEmpty + private String fullName; + private String organization; + + @Schema(description = "CI info", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull + @NotEmpty + private String info; + + @Schema(description = "Boolean to refer if it has a web channel", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull + private Boolean webChannel; + + @Schema(description = "CI physical channel data") + private String physicalChannel; + + @Schema(description = "CI cbill", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull + @NotEmpty + private String cbill; + + +} diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/Debtor.java b/src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/Debtor.java new file mode 100644 index 0000000..4456907 --- /dev/null +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/Debtor.java @@ -0,0 +1,19 @@ +package it.gov.pagopa.payment.notice.generator.model.notice; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; + +public class Debtor { + + @Schema(description = "Debtor taxCode") + private String taxCode; + @Schema(description = "Debtor full name", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull + @NotEmpty + private String fullName; + @Schema(description = "Debtor address", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull + @NotEmpty + private String address; +} diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/InstallmentData.java b/src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/InstallmentData.java new file mode 100644 index 0000000..be86c63 --- /dev/null +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/InstallmentData.java @@ -0,0 +1,14 @@ +package it.gov.pagopa.payment.notice.generator.model.notice; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +public class InstallmentData { + + @Schema(description = "Installment amount", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer amount; + @Schema(description = "Installment dueDate", requiredMode = Schema.RequiredMode.REQUIRED) + private String dueDate; + +} diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/Installments.java b/src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/Installments.java new file mode 100644 index 0000000..6e3aac3 --- /dev/null +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/Installments.java @@ -0,0 +1,16 @@ +package it.gov.pagopa.payment.notice.generator.model.notice; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Data +public class Installments { + + @Schema(description = "Number of installments") + private Integer number; + @Schema(description = "Installments of the notice") + private List data; + +} diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/Notice.java b/src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/Notice.java new file mode 100644 index 0000000..183f97f --- /dev/null +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/Notice.java @@ -0,0 +1,38 @@ +package it.gov.pagopa.payment.notice.generator.model.notice; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class Notice { + + @Schema(description = "Notice subject", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull + @NotEmpty + private String subject; + + @Schema(description = "Notice total amount to pay", requiredMode = Schema.RequiredMode.REQUIRED) + private Long paymentAmount; + + @Schema(description = "Notice due date", requiredMode = Schema.RequiredMode.REQUIRED) + private String dueDate; + + @Schema(description = "Notice code", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull + @NotEmpty + private String code; + + @Schema(description = "Notice installments (if present)") + private List installments; + +} diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/NoticeRequestData.java b/src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/NoticeRequestData.java new file mode 100644 index 0000000..4f10103 --- /dev/null +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/NoticeRequestData.java @@ -0,0 +1,22 @@ +package it.gov.pagopa.payment.notice.generator.model.notice; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class NoticeRequestData { + + @Schema(description = "Notice data", requiredMode = Schema.RequiredMode.REQUIRED) + private Notice notice; + @Schema(description = "Creditor Institution data", requiredMode = Schema.RequiredMode.REQUIRED) + private CreditorInstitution creditorInstitution; + @Schema(description = "Debtor data", requiredMode = Schema.RequiredMode.REQUIRED) + private Debtor debtor; + +} diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/repository/PaymentGenerationRequestErrorRepository.java b/src/main/java/it/gov/pagopa/payment/notice/generator/repository/PaymentGenerationRequestErrorRepository.java new file mode 100644 index 0000000..db4fbdb --- /dev/null +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/repository/PaymentGenerationRequestErrorRepository.java @@ -0,0 +1,16 @@ +package it.gov.pagopa.payment.notice.generator.repository; + +import it.gov.pagopa.payment.notice.generator.entity.PaymentNoticeGenerationRequestError; +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.data.mongodb.repository.Query; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface PaymentGenerationRequestErrorRepository extends MongoRepository { + + @Query(value = "{ folderId : ?0}", fields = "{}") + List findErrors(String folderId); + +} diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/repository/PaymentGenerationRequestRepository.java b/src/main/java/it/gov/pagopa/payment/notice/generator/repository/PaymentGenerationRequestRepository.java new file mode 100644 index 0000000..13ecddb --- /dev/null +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/repository/PaymentGenerationRequestRepository.java @@ -0,0 +1,17 @@ +package it.gov.pagopa.payment.notice.generator.repository; + +import it.gov.pagopa.payment.notice.generator.entity.PaymentNoticeGenerationRequest; +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.data.mongodb.repository.Update; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface PaymentGenerationRequestRepository extends MongoRepository { + Optional findByIdAndUserId(String folderId, String userId); + + @Update("{ '$inc' : { 'numberOfElementsFailed' : 1 } }") + long findAndIncrementNumberOfElementsFailedById(String folderId); + +} diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationService.java b/src/main/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationService.java new file mode 100644 index 0000000..72bfac3 --- /dev/null +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationService.java @@ -0,0 +1,10 @@ +package it.gov.pagopa.payment.notice.generator.service; + +import it.gov.pagopa.payment.notice.generator.model.NoticeGenerationRequestItem; + +import java.io.File; + +public interface NoticeGenerationService { + File generateNotice(NoticeGenerationRequestItem noticeGenerationRequestItem, + String folderId); +} diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationServiceImpl.java b/src/main/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationServiceImpl.java new file mode 100644 index 0000000..bcfb014 --- /dev/null +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationServiceImpl.java @@ -0,0 +1,105 @@ +package it.gov.pagopa.payment.notice.generator.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import it.gov.pagopa.payment.notice.generator.entity.PaymentNoticeGenerationRequest; +import it.gov.pagopa.payment.notice.generator.entity.PaymentNoticeGenerationRequestError; +import it.gov.pagopa.payment.notice.generator.exception.Aes256Exception; +import it.gov.pagopa.payment.notice.generator.exception.AppError; +import it.gov.pagopa.payment.notice.generator.exception.AppException; +import it.gov.pagopa.payment.notice.generator.model.NoticeGenerationRequestItem; +import it.gov.pagopa.payment.notice.generator.repository.PaymentGenerationRequestErrorRepository; +import it.gov.pagopa.payment.notice.generator.repository.PaymentGenerationRequestRepository; +import it.gov.pagopa.payment.notice.generator.storage.InstitutionsStorageClient; +import it.gov.pagopa.payment.notice.generator.storage.NoticeStorageClient; +import it.gov.pagopa.payment.notice.generator.storage.NoticeTemplateStorageClient; +import it.gov.pagopa.payment.notice.generator.util.Aes256Utils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.io.File; +import java.time.Instant; +import java.util.Optional; + +@Service +@Slf4j +public class NoticeGenerationServiceImpl implements NoticeGenerationService { + + private final PaymentGenerationRequestErrorRepository paymentGenerationRequestErrorRepository; + private final PaymentGenerationRequestRepository paymentGenerationRequestRepository; + + private final InstitutionsStorageClient institutionsStorageClient; + private final NoticeStorageClient noticeStorageClient; + private final NoticeTemplateStorageClient noticeTemplateStorageClient; + + private final Aes256Utils aes256Utils; + + private final ObjectMapper objectMapper; + + public NoticeGenerationServiceImpl( + PaymentGenerationRequestRepository paymentGenerationRequestRepository, + PaymentGenerationRequestErrorRepository paymentGenerationRequestErrorRepository, + InstitutionsStorageClient institutionsStorageClient, + NoticeStorageClient noticeStorageClient, + NoticeTemplateStorageClient noticeTemplateStorageClient, + Aes256Utils aes256Utils, + ObjectMapper objectMapper) { + this.paymentGenerationRequestRepository = paymentGenerationRequestRepository; + this.paymentGenerationRequestErrorRepository = paymentGenerationRequestErrorRepository; + this.institutionsStorageClient = institutionsStorageClient; + this.noticeStorageClient = noticeStorageClient; + this.noticeTemplateStorageClient = noticeTemplateStorageClient; + this.aes256Utils = aes256Utils; + this.objectMapper = objectMapper; + } + + @Override + public File generateNotice(NoticeGenerationRequestItem noticeGenerationRequestItem, + String folderId) { + + if (folderId != null) { + Optional paymentNoticeGenerationRequestOptional = + paymentGenerationRequestRepository.findById(folderId); + if (paymentNoticeGenerationRequestOptional.isEmpty()) { + throw new AppException(AppError.FOLDER_NOT_AVAILABLE); + } + } + + try { + File templateFile = noticeTemplateStorageClient.getTemplate( + noticeGenerationRequestItem.getTemplateId()); + + File noticeFile = null; + return noticeFile; + + } catch (Exception e) { + if (folderId != null) { + saveErrorEvent(folderId, noticeGenerationRequestItem); + } + throw e; + } + + } + + private void saveErrorEvent(String folderId, NoticeGenerationRequestItem noticeGenerationRequestItem) { + try { + paymentGenerationRequestErrorRepository.save( + PaymentNoticeGenerationRequestError.builder() + .errorDescription("Encountered error sending notice on EH") + .folderId(folderId) + .data(aes256Utils.encrypt(objectMapper + .writeValueAsString(noticeGenerationRequestItem))) + .createdAt(Instant.now()) + .numberOfAttempts(0) + .build() + ); + paymentGenerationRequestRepository.findAndIncrementNumberOfElementsFailedById(folderId); + } catch (JsonProcessingException | Aes256Exception e) { + log.error( + "Unable to save notice data into error repository for notice with folder " + folderId + + " and noticeId " + noticeGenerationRequestItem.getData().getNotice().getCode() + ); + } + } + +} diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/storage/InstitutionsStorageClient.java b/src/main/java/it/gov/pagopa/payment/notice/generator/storage/InstitutionsStorageClient.java new file mode 100644 index 0000000..d9b9934 --- /dev/null +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/storage/InstitutionsStorageClient.java @@ -0,0 +1,119 @@ +package it.gov.pagopa.payment.notice.generator.storage; + +import com.azure.core.util.Context; +import com.azure.storage.blob.BlobContainerClient; +import com.azure.storage.blob.BlobServiceClient; +import com.azure.storage.blob.BlobServiceClientBuilder; +import com.azure.storage.blob.models.BlobStorageException; +import com.azure.storage.blob.models.DownloadRetryOptions; +import com.azure.storage.blob.options.BlobDownloadToFileOptions; +import it.gov.pagopa.payment.notice.generator.exception.AppError; +import it.gov.pagopa.payment.notice.generator.exception.AppException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.time.Duration; +import java.util.Arrays; +import java.util.HashSet; + +@Component +public class InstitutionsStorageClient { + + private BlobContainerClient blobContainerClient; + + private Integer maxRetry; + + private Integer timeout; + + @Autowired + public InstitutionsStorageClient( + @Value("${spring.cloud.azure.storage.blob.institutions.enabled}") String enabled, + @Value("${spring.cloud.azure.storage.blob.institutions.connection_string}") String connectionString, + @Value("${spring.cloud.azure.storage.blob.institutions.containerName}") String containerName, + @Value("${spring.cloud.azure.storage.blob.institutions.retry}") Integer maxRetry, + @Value("${spring.cloud.azure.storage.blob.institutions.timeout}") Integer timeout) { + if (Boolean.TRUE.toString().equals(enabled)) { + BlobServiceClient blobServiceClient = new BlobServiceClientBuilder() + .connectionString(connectionString).buildClient(); + blobContainerClient = blobServiceClient.getBlobContainerClient(containerName); + this.maxRetry = maxRetry; + this.timeout = timeout; + } + } + public InstitutionsStorageClient( + Boolean enabled, + BlobContainerClient blobContainerClient) { + if (Boolean.TRUE.equals(enabled)) { + this.blobContainerClient = blobContainerClient; + this.maxRetry=3; + this.timeout=10; + } + } + + /** + * Retrieve the template from the Blob Storage + * + * @param institutionCode the name of the institution to be retrieved + * @return the File with the reference to the downloaded data + * @throws AppException thrown for error when retrieving the data + */ + public File getTemplate(String institutionCode) { + + if (blobContainerClient == null) { + throw new AppException(AppError.TEMPLATE_CLIENT_UNAVAILABLE); + } + String filePath = createTempDirectory(institutionCode); + try { + blobContainerClient.getBlobClient(institutionCode.concat("/data.zip")) + .downloadToFileWithResponse( + getBlobDownloadToFileOptions(filePath), + Duration.ofSeconds(timeout), + Context.NONE); + return new File(filePath); + } catch (BlobStorageException blobStorageException) { + throw new AppException(AppError.TEMPLATE_NOT_FOUND, blobStorageException); + } + } + + private BlobDownloadToFileOptions getBlobDownloadToFileOptions(String filePath) { + return new BlobDownloadToFileOptions(filePath) + .setDownloadRetryOptions(new DownloadRetryOptions().setMaxRetryRequests(maxRetry)) + .setOpenOptions(new HashSet<>( + Arrays.asList( + StandardOpenOption.CREATE_NEW, + StandardOpenOption.WRITE, + StandardOpenOption.READ + )) + ); + } + + private String createTempDirectory(String templateId) { + try { + File workingDirectory = createWorkingDirectory(); + Path tempDirectory = Files.createTempDirectory(workingDirectory.toPath(), "notice-generator") + .normalize().toAbsolutePath(); + Path filePath = tempDirectory.resolve(templateId + ".zip").normalize().toAbsolutePath(); + if (!filePath.startsWith(tempDirectory + File.separator)) { + throw new IllegalArgumentException("Invalid filename"); + } + return filePath.toFile().getAbsolutePath(); + } catch (IOException e) { + throw new AppException(AppError.TEMPLATE_CLIENT_ERROR, e); + } + } + + private File createWorkingDirectory() throws IOException { + File workingDirectory = new File("temp"); + if (!workingDirectory.exists()) { + Files.createDirectory(workingDirectory.toPath()); + } + return workingDirectory; + } + +} diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/storage/NoticeStorageClient.java b/src/main/java/it/gov/pagopa/payment/notice/generator/storage/NoticeStorageClient.java new file mode 100644 index 0000000..9bd8ca9 --- /dev/null +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/storage/NoticeStorageClient.java @@ -0,0 +1,119 @@ +package it.gov.pagopa.payment.notice.generator.storage; + +import com.azure.core.util.Context; +import com.azure.storage.blob.BlobContainerClient; +import com.azure.storage.blob.BlobServiceClient; +import com.azure.storage.blob.BlobServiceClientBuilder; +import com.azure.storage.blob.models.BlobStorageException; +import com.azure.storage.blob.models.DownloadRetryOptions; +import com.azure.storage.blob.options.BlobDownloadToFileOptions; +import it.gov.pagopa.payment.notice.generator.exception.AppError; +import it.gov.pagopa.payment.notice.generator.exception.AppException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.time.Duration; +import java.util.Arrays; +import java.util.HashSet; + +@Component +public class NoticeStorageClient { + + private BlobContainerClient blobContainerClient; + + private Integer maxRetry; + + private Integer timeout; + + @Autowired + public NoticeStorageClient( + @Value("${spring.cloud.azure.storage.blob.notices.enabled}") String enabled, + @Value("${spring.cloud.azure.storage.blob.notices.connection_string}") String connectionString, + @Value("${spring.cloud.azure.storage.blob.notices.containerName}") String containerName, + @Value("${spring.cloud.azure.storage.blob.notices.retry}") Integer maxRetry, + @Value("${spring.cloud.azure.storage.blob.notices.timeout}") Integer timeout) { + if (Boolean.TRUE.toString().equals(enabled)) { + BlobServiceClient blobServiceClient = new BlobServiceClientBuilder() + .connectionString(connectionString).buildClient(); + blobContainerClient = blobServiceClient.getBlobContainerClient(containerName); + this.maxRetry = maxRetry; + this.timeout = timeout; + } + } + public NoticeStorageClient( + Boolean enabled, + BlobContainerClient blobContainerClient) { + if (Boolean.TRUE.equals(enabled)) { + this.blobContainerClient = blobContainerClient; + this.maxRetry=3; + this.timeout=10; + } + } + + /** + * Retrieve the template from the Blob Storage + * + * @param institutionCode the name of the institution to be retrieved + * @return the File with the reference to the downloaded data + * @throws AppException thrown for error when retrieving the data + */ + public File getTemplate(String institutionCode) { + + if (blobContainerClient == null) { + throw new AppException(AppError.TEMPLATE_CLIENT_UNAVAILABLE); + } + String filePath = createTempDirectory(institutionCode); + try { + blobContainerClient.getBlobClient(institutionCode.concat("/data.zip")) + .downloadToFileWithResponse( + getBlobDownloadToFileOptions(filePath), + Duration.ofSeconds(timeout), + Context.NONE); + return new File(filePath); + } catch (BlobStorageException blobStorageException) { + throw new AppException(AppError.TEMPLATE_NOT_FOUND, blobStorageException); + } + } + + private BlobDownloadToFileOptions getBlobDownloadToFileOptions(String filePath) { + return new BlobDownloadToFileOptions(filePath) + .setDownloadRetryOptions(new DownloadRetryOptions().setMaxRetryRequests(maxRetry)) + .setOpenOptions(new HashSet<>( + Arrays.asList( + StandardOpenOption.CREATE_NEW, + StandardOpenOption.WRITE, + StandardOpenOption.READ + )) + ); + } + + private String createTempDirectory(String templateId) { + try { + File workingDirectory = createWorkingDirectory(); + Path tempDirectory = Files.createTempDirectory(workingDirectory.toPath(), "notice-generator") + .normalize().toAbsolutePath(); + Path filePath = tempDirectory.resolve(templateId + ".zip").normalize().toAbsolutePath(); + if (!filePath.startsWith(tempDirectory + File.separator)) { + throw new IllegalArgumentException("Invalid filename"); + } + return filePath.toFile().getAbsolutePath(); + } catch (IOException e) { + throw new AppException(AppError.TEMPLATE_CLIENT_ERROR, e); + } + } + + private File createWorkingDirectory() throws IOException { + File workingDirectory = new File("temp"); + if (!workingDirectory.exists()) { + Files.createDirectory(workingDirectory.toPath()); + } + return workingDirectory; + } + +} diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/storage/NoticeTemplateStorageClient.java b/src/main/java/it/gov/pagopa/payment/notice/generator/storage/NoticeTemplateStorageClient.java new file mode 100644 index 0000000..c6ba6c9 --- /dev/null +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/storage/NoticeTemplateStorageClient.java @@ -0,0 +1,120 @@ +package it.gov.pagopa.payment.notice.generator.storage; + +import com.azure.core.util.Context; +import com.azure.storage.blob.BlobContainerClient; +import com.azure.storage.blob.BlobServiceClient; +import com.azure.storage.blob.BlobServiceClientBuilder; +import com.azure.storage.blob.models.BlobStorageException; +import com.azure.storage.blob.models.DownloadRetryOptions; +import com.azure.storage.blob.options.BlobDownloadToFileOptions; +import it.gov.pagopa.payment.notice.generator.exception.AppError; +import it.gov.pagopa.payment.notice.generator.exception.AppException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.time.Duration; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; + +@Component +public class NoticeTemplateStorageClient { + + private BlobContainerClient blobContainerClient; + + private Integer maxRetry; + + private Integer timeout; + + @Autowired + public NoticeTemplateStorageClient( + @Value("${spring.cloud.azure.storage.blob.templates.enabled}") String enabled, + @Value("${spring.cloud.azure.storage.blob.templates.connection_string}") String connectionString, + @Value("${spring.cloud.azure.storage.blob.templates.containerName}") String containerName, + @Value("${spring.cloud.azure.storage.blob.templates.retry}") Integer maxRetry, + @Value("${spring.cloud.azure.storage.blob.templates.timeout}") Integer timeout) { + if (Boolean.TRUE.toString().equals(enabled)) { + BlobServiceClient blobServiceClient = new BlobServiceClientBuilder() + .connectionString(connectionString).buildClient(); + blobContainerClient = blobServiceClient.getBlobContainerClient(containerName); + this.maxRetry = maxRetry; + this.timeout = timeout; + } + } + public NoticeTemplateStorageClient( + Boolean enabled, + BlobContainerClient blobContainerClient) { + if (Boolean.TRUE.equals(enabled)) { + this.blobContainerClient = blobContainerClient; + this.maxRetry=3; + this.timeout=10; + } + } + + /** + * Retrieve the template from the Blob Storage + * + * @param templateId the name of the file to be retrieved + * @return the File with the reference to the downloaded template + * @throws AppException thrown for error when retrieving the template + */ + public File getTemplate(String templateId) { + + if (blobContainerClient == null) { + throw new AppException(AppError.TEMPLATE_CLIENT_UNAVAILABLE); + } + String filePath = createTempDirectory(templateId); + try { + blobContainerClient.getBlobClient(templateId.concat("/template.zip")) + .downloadToFileWithResponse( + getBlobDownloadToFileOptions(filePath), + Duration.ofSeconds(timeout), + Context.NONE); + return new File(filePath); + } catch (BlobStorageException blobStorageException) { + throw new AppException(AppError.TEMPLATE_NOT_FOUND, blobStorageException); + } + } + + private BlobDownloadToFileOptions getBlobDownloadToFileOptions(String filePath) { + return new BlobDownloadToFileOptions(filePath) + .setDownloadRetryOptions(new DownloadRetryOptions().setMaxRetryRequests(maxRetry)) + .setOpenOptions(new HashSet<>( + Arrays.asList( + StandardOpenOption.CREATE_NEW, + StandardOpenOption.WRITE, + StandardOpenOption.READ + )) + ); + } + + private String createTempDirectory(String templateId) { + try { + File workingDirectory = createWorkingDirectory(); + Path tempDirectory = Files.createTempDirectory(workingDirectory.toPath(), "notice-generator") + .normalize().toAbsolutePath(); + Path filePath = tempDirectory.resolve(templateId + ".zip").normalize().toAbsolutePath(); + if (!filePath.startsWith(tempDirectory + File.separator)) { + throw new IllegalArgumentException("Invalid filename"); + } + return filePath.toFile().getAbsolutePath(); + } catch (IOException e) { + throw new AppException(AppError.TEMPLATE_CLIENT_ERROR, e); + } + } + + private File createWorkingDirectory() throws IOException { + File workingDirectory = new File("temp"); + if (!workingDirectory.exists()) { + Files.createDirectory(workingDirectory.toPath()); + } + return workingDirectory; + } + +} diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/util/Aes256Utils.java b/src/main/java/it/gov/pagopa/payment/notice/generator/util/Aes256Utils.java new file mode 100644 index 0000000..69bc111 --- /dev/null +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/util/Aes256Utils.java @@ -0,0 +1,98 @@ +package it.gov.pagopa.payment.notice.generator.util; + +import it.gov.pagopa.payment.notice.generator.exception.Aes256Exception; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; +import java.security.spec.KeySpec; +import java.util.Base64; + +@Component +public class Aes256Utils { + + private String aesSecretKey; + + private String aesSalt; + + private static final int KEY_LENGTH = 256; + private static final int ITERATION_COUNT = 65536; + public static final String PBKDF_2_WITH_HMAC_SHA_256 = "PBKDF2WithHmacSHA256"; + public static final String AES_CBC_PKCS_5_PADDING = "AES/CBC/PKCS5Padding"; + + public static final String ALGORITHM = "AES"; + + private static final int AES_UNEXPECTED_ERROR = 701; + + @Autowired + public Aes256Utils( + @Value("${aes.secret.key}") String aesSecretKey, + @Value("${aes.salt}") String aesSalt) { + this.aesSecretKey = aesSecretKey; + this.aesSalt = aesSalt; + } + + public String encrypt(String strToEncrypt) throws Aes256Exception { + + try { + + SecureRandom secureRandom = new SecureRandom(); + byte[] iv = new byte[16]; + secureRandom.nextBytes(iv); + IvParameterSpec ivspec = new IvParameterSpec(iv); + + SecretKeyFactory factory = SecretKeyFactory.getInstance(PBKDF_2_WITH_HMAC_SHA_256); + KeySpec spec = new PBEKeySpec(aesSecretKey.toCharArray(), aesSalt.getBytes(), ITERATION_COUNT, KEY_LENGTH); + SecretKey tmp = factory.generateSecret(spec); + SecretKeySpec secretKeySpec = new SecretKeySpec(tmp.getEncoded(), ALGORITHM); + + //Padding vulnerability rule java:S5542 ignored because encryption is used inside application workflow + Cipher cipher = Cipher.getInstance(AES_CBC_PKCS_5_PADDING); + cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivspec); + + byte[] cipherText = cipher.doFinal(strToEncrypt.getBytes(StandardCharsets.UTF_8)); + byte[] encryptedData = new byte[iv.length + cipherText.length]; + System.arraycopy(iv, 0, encryptedData, 0, iv.length); + System.arraycopy(cipherText, 0, encryptedData, iv.length, cipherText.length); + + return Base64.getEncoder().encodeToString(encryptedData); + } catch (Exception e) { + throw new Aes256Exception("Unexpected error when encrypting the given string", AES_UNEXPECTED_ERROR, e); + } + } + + public String decrypt(String strToDecrypt) throws Aes256Exception { + try{ + byte[] encryptedData = Base64.getDecoder().decode(strToDecrypt); + byte[] iv = new byte[16]; + System.arraycopy(encryptedData, 0, iv, 0, iv.length); + IvParameterSpec ivspec = new IvParameterSpec(iv); + + SecretKeyFactory factory = SecretKeyFactory.getInstance(PBKDF_2_WITH_HMAC_SHA_256); + KeySpec spec = new PBEKeySpec(aesSecretKey.toCharArray(), aesSalt.getBytes(), ITERATION_COUNT, KEY_LENGTH); + SecretKey tmp = factory.generateSecret(spec); + SecretKeySpec secretKeySpec = new SecretKeySpec(tmp.getEncoded(), ALGORITHM); + + //Padding vulnerability rule java:S5542 ignored because decryption is used inside application workflow + Cipher cipher = Cipher.getInstance(AES_CBC_PKCS_5_PADDING); + cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivspec); + + byte[] cipherText = new byte[encryptedData.length - 16]; + System.arraycopy(encryptedData, 16, cipherText, 0, cipherText.length); + + byte[] decryptedText = cipher.doFinal(cipherText); + return new String(decryptedText, StandardCharsets.UTF_8); + } catch (Exception e) { + throw new Aes256Exception("Unexpected error when decrypting the given string", AES_UNEXPECTED_ERROR, e); + } + } +} + diff --git a/src/main/java/it/gov/pagopa/microservice/util/CommonUtility.java b/src/main/java/it/gov/pagopa/payment/notice/generator/util/CommonUtility.java similarity index 96% rename from src/main/java/it/gov/pagopa/microservice/util/CommonUtility.java rename to src/main/java/it/gov/pagopa/payment/notice/generator/util/CommonUtility.java index f080c47..9b79a48 100644 --- a/src/main/java/it/gov/pagopa/microservice/util/CommonUtility.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/util/CommonUtility.java @@ -1,4 +1,4 @@ -package it.gov.pagopa.microservice.util; +package it.gov.pagopa.payment.notice.generator.util; import lombok.AccessLevel; import lombok.NoArgsConstructor; diff --git a/src/main/java/it/gov/pagopa/microservice/util/Constants.java b/src/main/java/it/gov/pagopa/payment/notice/generator/util/Constants.java similarity index 74% rename from src/main/java/it/gov/pagopa/microservice/util/Constants.java rename to src/main/java/it/gov/pagopa/payment/notice/generator/util/Constants.java index e5812c7..48da16a 100644 --- a/src/main/java/it/gov/pagopa/microservice/util/Constants.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/util/Constants.java @@ -1,4 +1,4 @@ -package it.gov.pagopa.microservice.util; +package it.gov.pagopa.payment.notice.generator.util; import lombok.experimental.UtilityClass; diff --git a/src/test/java/it/gov/pagopa/microservice/ApplicationTest.java b/src/test/java/it/gov/pagopa/payment/notice/generator/ApplicationTest.java similarity index 86% rename from src/test/java/it/gov/pagopa/microservice/ApplicationTest.java rename to src/test/java/it/gov/pagopa/payment/notice/generator/ApplicationTest.java index 80c6e10..656ae79 100644 --- a/src/test/java/it/gov/pagopa/microservice/ApplicationTest.java +++ b/src/test/java/it/gov/pagopa/payment/notice/generator/ApplicationTest.java @@ -1,4 +1,4 @@ -package it.gov.pagopa.microservice; +package it.gov.pagopa.payment.notice.generator; import static org.junit.jupiter.api.Assertions.assertTrue; diff --git a/src/test/java/it/gov/pagopa/microservice/OpenApiGenerationTest.java b/src/test/java/it/gov/pagopa/payment/notice/generator/OpenApiGenerationTest.java similarity index 97% rename from src/test/java/it/gov/pagopa/microservice/OpenApiGenerationTest.java rename to src/test/java/it/gov/pagopa/payment/notice/generator/OpenApiGenerationTest.java index 07f521b..d47b660 100644 --- a/src/test/java/it/gov/pagopa/microservice/OpenApiGenerationTest.java +++ b/src/test/java/it/gov/pagopa/payment/notice/generator/OpenApiGenerationTest.java @@ -1,4 +1,4 @@ -package it.gov.pagopa.microservice; +package it.gov.pagopa.payment.notice.generator; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; From 4ade8e0c0b5606d3a9e476035216957946c804a3 Mon Sep 17 00:00:00 2001 From: Alessio Cialini Date: Mon, 29 Apr 2024 12:42:49 +0200 Subject: [PATCH 02/19] [VAS-834] feat: Introduced generation for single request (with folderId support) --- pom.xml | 15 ++ .../generator/client/PdfEngineClient.java | 12 ++ .../generator/client/PdfEngineClientImpl.java | 200 ++++++++++++++++++ .../PaymentNoticeGenerationRequest.java | 43 ++++ .../PaymentNoticeGenerationRequestError.java | 37 ++++ .../notice/generator/exception/AppError.java | 8 +- .../generator/mapper/TemplateDataMapper.java | 86 ++++++++ .../model/notice/CreditorInstitution.java | 26 ++- .../notice/generator/model/notice/Debtor.java | 8 + .../model/notice/InstallmentData.java | 4 +- .../notice/generator/model/notice/Notice.java | 2 +- .../model/pdf/PdfEngineErrorMessage.java | 13 ++ .../model/pdf/PdfEngineErrorResponse.java | 19 ++ .../generator/model/pdf/PdfEngineRequest.java | 20 ++ .../model/pdf/PdfEngineResponse.java | 19 ++ .../generator/model/pdf/notice/Channel.java | 16 ++ .../generator/model/pdf/notice/Debtor.java | 22 ++ .../model/pdf/notice/Installment.java | 20 ++ .../generator/model/pdf/notice/Notice.java | 24 +++ .../generator/model/pdf/notice/Online.java | 17 ++ .../generator/model/pdf/notice/Payee.java | 21 ++ .../model/pdf/notice/PaymentNotice.java | 18 ++ ...ymentGenerationRequestErrorRepository.java | 8 +- .../PaymentGenerationRequestRepository.java | 8 +- .../service/NoticeGenerationService.java | 4 +- .../service/NoticeGenerationServiceImpl.java | 69 +++++- .../storage/InstitutionsStorageClient.java | 76 ++----- .../storage/NoticeStorageClient.java | 73 ++----- .../storage/NoticeTemplateStorageClient.java | 41 ++-- .../generator/util/WorkingDirectoryUtils.java | 17 ++ 30 files changed, 799 insertions(+), 147 deletions(-) create mode 100644 src/main/java/it/gov/pagopa/payment/notice/generator/client/PdfEngineClient.java create mode 100644 src/main/java/it/gov/pagopa/payment/notice/generator/client/PdfEngineClientImpl.java create mode 100644 src/main/java/it/gov/pagopa/payment/notice/generator/entity/PaymentNoticeGenerationRequest.java create mode 100644 src/main/java/it/gov/pagopa/payment/notice/generator/entity/PaymentNoticeGenerationRequestError.java create mode 100644 src/main/java/it/gov/pagopa/payment/notice/generator/mapper/TemplateDataMapper.java create mode 100644 src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/PdfEngineErrorMessage.java create mode 100644 src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/PdfEngineErrorResponse.java create mode 100644 src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/PdfEngineRequest.java create mode 100644 src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/PdfEngineResponse.java create mode 100644 src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/notice/Channel.java create mode 100644 src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/notice/Debtor.java create mode 100644 src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/notice/Installment.java create mode 100644 src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/notice/Notice.java create mode 100644 src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/notice/Online.java create mode 100644 src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/notice/Payee.java create mode 100644 src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/notice/PaymentNotice.java create mode 100644 src/main/java/it/gov/pagopa/payment/notice/generator/util/WorkingDirectoryUtils.java diff --git a/pom.xml b/pom.xml index 52c15a2..9fda518 100644 --- a/pom.xml +++ b/pom.xml @@ -68,6 +68,21 @@ caffeine + + + + org.apache.httpcomponents + httpclient + 4.5.14 + + + org.apache.httpcomponents + httpmime + 4.5.14 + + + + org.springdoc diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/client/PdfEngineClient.java b/src/main/java/it/gov/pagopa/payment/notice/generator/client/PdfEngineClient.java new file mode 100644 index 0000000..3782510 --- /dev/null +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/client/PdfEngineClient.java @@ -0,0 +1,12 @@ +package it.gov.pagopa.payment.notice.generator.client; + +import it.gov.pagopa.payment.notice.generator.model.pdf.PdfEngineRequest; +import it.gov.pagopa.payment.notice.generator.model.pdf.PdfEngineResponse; + +import java.nio.file.Path; + +public interface PdfEngineClient { + + PdfEngineResponse generatePDF(PdfEngineRequest pdfEngineRequest, Path workingDirPath); + +} diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/client/PdfEngineClientImpl.java b/src/main/java/it/gov/pagopa/payment/notice/generator/client/PdfEngineClientImpl.java new file mode 100644 index 0000000..b57f02f --- /dev/null +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/client/PdfEngineClientImpl.java @@ -0,0 +1,200 @@ +package it.gov.pagopa.payment.notice.generator.client; + +import com.fasterxml.jackson.databind.ObjectMapper; +import it.gov.pagopa.payment.notice.generator.model.pdf.PdfEngineErrorResponse; +import it.gov.pagopa.payment.notice.generator.model.pdf.PdfEngineRequest; +import it.gov.pagopa.payment.notice.generator.model.pdf.PdfEngineResponse; +import org.apache.commons.io.FileUtils; +import org.apache.http.HttpEntity; +import org.apache.http.HttpStatus; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.mime.HttpMultipartMode; +import org.apache.http.entity.mime.MultipartEntityBuilder; +import org.apache.http.entity.mime.content.StringBody; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.util.EntityUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; + +/** + * Client for the PDF Engine + */ +public class PdfEngineClientImpl implements PdfEngineClient { + + private final String pdfEngineEndpoint; + private final String ocpAimSubKey; + + private static final String HEADER_AUTH_KEY = "Ocp-Apim-Subscription-Key"; + private static final String ZIP_FILE_NAME = "template.zip"; + private static final String TEMPLATE_KEY = "template"; + private static final String DATA_KEY = "data"; + + private final HttpClientBuilder httpClientBuilder; + private final ObjectMapper objectMapper; + + @Autowired + private PdfEngineClientImpl(ObjectMapper objectMapper, + @Value("pdf.engine.endpoint") String pdfEngineEndpoint, + @Value("pdf.engine.ocpaim.subkey") String ocpAimSubKey) { + this.objectMapper = objectMapper; + this.httpClientBuilder = HttpClientBuilder.create(); + this.ocpAimSubKey = ocpAimSubKey; + this.pdfEngineEndpoint = pdfEngineEndpoint; + } + + PdfEngineClientImpl(ObjectMapper objectMapper, HttpClientBuilder clientBuilder, + String pdfEngineEndpoint, String ocpAimSubKey) { + this.objectMapper = objectMapper; + this.httpClientBuilder = clientBuilder; + this.pdfEngineEndpoint = pdfEngineEndpoint; + this.ocpAimSubKey = ocpAimSubKey; + } + + /** + * Generate the client, builds the request and returns the response + * + * @param pdfEngineRequest Request to the client + * @return response with the PDF or error message and the status + */ + @Override + public PdfEngineResponse generatePDF(PdfEngineRequest pdfEngineRequest, Path workingDirPath) { + + PdfEngineResponse pdfEngineResponse = new PdfEngineResponse(); + + //Generate client + try (CloseableHttpClient client = this.httpClientBuilder.build(); InputStream is = pdfEngineRequest.getTemplate().openStream()) { + //Encode template and data + + StringBody dataBody = new StringBody(pdfEngineRequest.getData(), ContentType.APPLICATION_JSON); + + //Build the multipart request + MultipartEntityBuilder builder = MultipartEntityBuilder.create(); + builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE); + builder.addBinaryBody(TEMPLATE_KEY, is.readAllBytes(), ContentType.create("application/zip"), ZIP_FILE_NAME); + builder.addPart(DATA_KEY, dataBody); + HttpEntity entity = builder.build(); + + //Set endpoint and auth key + HttpPost request = new HttpPost(pdfEngineEndpoint); + request.setHeader(HEADER_AUTH_KEY, ocpAimSubKey); + request.setEntity(entity); + + pdfEngineResponse = handlePdfEngineResponse(client, request, workingDirPath); + } catch (IOException e) { + handleExceptionErrorMessage(pdfEngineResponse, e); + } + + return pdfEngineResponse; + } + + /** + * Calls the PDF Engine and handles its response, updating the PdfEngineResponse accordingly + * + * @param client The previously generated client + * @param request The request to the PDF engine + * @return pdf engine response + */ + private PdfEngineResponse handlePdfEngineResponse(CloseableHttpClient client, HttpPost request, Path workingDirPath) { + PdfEngineResponse pdfEngineResponse = new PdfEngineResponse(); + //Execute call + try (CloseableHttpResponse response = client.execute(request)) { + //Retrieve response + HttpEntity entityResponse = response.getEntity(); + + //Handles response + if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK && entityResponse != null) { + try (InputStream inputStream = entityResponse.getContent()) { + pdfEngineResponse.setStatusCode(HttpStatus.SC_OK); + + saveTempPdf(pdfEngineResponse, inputStream, workingDirPath); + } + } else { + pdfEngineResponse.setStatusCode(HttpStatus.SC_INTERNAL_SERVER_ERROR); + + handleErrorResponse(pdfEngineResponse, response, entityResponse); + } + } catch (Exception e) { + handleExceptionErrorMessage(pdfEngineResponse, e); + } + + return pdfEngineResponse; + } + + /** + * Saves pdf as temporary file + * + * @param pdfEngineResponse Pdf engine response + * @param inputStream InputStream pdf + * @throws IOException In case of error to save + */ + private void saveTempPdf( + PdfEngineResponse pdfEngineResponse, InputStream inputStream, Path workingDirPath) throws IOException { + File targetFile = File.createTempFile("tempFile", ".pdf", workingDirPath.toFile()); + + FileUtils.copyInputStreamToFile(inputStream, targetFile); + + pdfEngineResponse.setTempPdfPath(targetFile.getAbsolutePath()); + } + + /** + * Handles error message in case of error thrown + * + * @param pdfEngineResponse Pdf engine respone + * @param e Error thrown + */ + private void handleExceptionErrorMessage(PdfEngineResponse pdfEngineResponse, Exception e) { + pdfEngineResponse.setStatusCode(HttpStatus.SC_INTERNAL_SERVER_ERROR); + pdfEngineResponse.setErrorMessage(String.format("Exception thrown during pdf generation process: %s", e)); + } + + /** + * Handles error response from the PDF Engine + * + * @param pdfEngineResponse Response to update + * @param response Response from the PDF engine + * @param entityResponse Response content from the PDF Engine + * @throws IOException in case of error encoding to string + */ + private void handleErrorResponse( + PdfEngineResponse pdfEngineResponse, + CloseableHttpResponse response, + HttpEntity entityResponse + ) throws IOException { + //Verify if unauthorized + if (response != null && + response.getStatusLine() != null && + response.getStatusLine().getStatusCode() == HttpStatus.SC_UNAUTHORIZED + ) { + pdfEngineResponse.setErrorMessage("Unauthorized call to PDF engine function"); + + } else if (entityResponse != null) { + //Handle JSON response + String jsonString = EntityUtils.toString(entityResponse, StandardCharsets.UTF_8); + + if (!jsonString.isEmpty()) { + PdfEngineErrorResponse errorResponse = objectMapper.readValue(jsonString, PdfEngineErrorResponse.class); + + if (errorResponse != null && + errorResponse.getErrors() != null && + !errorResponse.getErrors().isEmpty() && + errorResponse.getErrors().get(0) != null + ) { + pdfEngineResponse.setErrorMessage(errorResponse.getErrors().get(0).getMessage()); + } + } + } + + if (pdfEngineResponse.getErrorMessage() == null) { + pdfEngineResponse.setErrorMessage("Unknown error in PDF engine function"); + } + } +} diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/entity/PaymentNoticeGenerationRequest.java b/src/main/java/it/gov/pagopa/payment/notice/generator/entity/PaymentNoticeGenerationRequest.java new file mode 100644 index 0000000..89f81fc --- /dev/null +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/entity/PaymentNoticeGenerationRequest.java @@ -0,0 +1,43 @@ +package it.gov.pagopa.payment.notice.generator.entity; + +import it.gov.pagopa.payment.notice.generator.model.enums.PaymentGenerationRequestStatus; +import lombok.*; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.index.Indexed; +import org.springframework.data.mongodb.core.mapping.Document; + +import java.time.Instant; +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(of = "id") +@Document("payment_notice_generation_request") +@ToString +public class PaymentNoticeGenerationRequest { + + @Id + private String id; + + @Indexed() + private String userId; + + @CreatedDate + private Instant createdAt; + + private Instant requestDate; + + private PaymentGenerationRequestStatus status; + + private List items; + + private Integer numberOfElementsProcessed; + + private Integer numberOfElementsFailed; + + private Integer numberOfElementsTotal; + +} diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/entity/PaymentNoticeGenerationRequestError.java b/src/main/java/it/gov/pagopa/payment/notice/generator/entity/PaymentNoticeGenerationRequestError.java new file mode 100644 index 0000000..f4a1537 --- /dev/null +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/entity/PaymentNoticeGenerationRequestError.java @@ -0,0 +1,37 @@ +package it.gov.pagopa.payment.notice.generator.entity; + +import lombok.*; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.index.Indexed; +import org.springframework.data.mongodb.core.mapping.Document; + +import java.time.Instant; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(of = "folderId") +@Document("payment_notice_generation_request_error") +@ToString +public class PaymentNoticeGenerationRequestError { + + @Id + private String id; + + @Indexed() + private String folderId; + + @CreatedDate + private Instant createdAt; + + private String errorCode; + + private String errorDescription; + + private String data; + + private Integer numberOfAttempts; + +} diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/exception/AppError.java b/src/main/java/it/gov/pagopa/payment/notice/generator/exception/AppError.java index 814fb1e..f6fc734 100644 --- a/src/main/java/it/gov/pagopa/payment/notice/generator/exception/AppError.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/exception/AppError.java @@ -20,7 +20,13 @@ public enum AppError { "Template Client encountered an error"), FOLDER_NOT_AVAILABLE(HttpStatus.NOT_FOUND, "Folder Not Available", - "Required folder is either missing or not available to the requirer"), + "Required folder is either missing or not available to require"), + + INSTITUTION_NOT_FOUND(HttpStatus.NOT_FOUND, "Institution Not Found", + "Required institution data has not been found on the storage"), + + INSTITUTION_PARSING_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "Parsing Error for Institution Data", + "Exception thrown while parsing institution data retrieve from storage"), UNKNOWN(null, null, null); public final HttpStatus httpStatus; diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/mapper/TemplateDataMapper.java b/src/main/java/it/gov/pagopa/payment/notice/generator/mapper/TemplateDataMapper.java new file mode 100644 index 0000000..a243e28 --- /dev/null +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/mapper/TemplateDataMapper.java @@ -0,0 +1,86 @@ +package it.gov.pagopa.payment.notice.generator.mapper; + +import it.gov.pagopa.payment.notice.generator.model.notice.InstallmentData; +import it.gov.pagopa.payment.notice.generator.model.notice.NoticeRequestData; +import it.gov.pagopa.payment.notice.generator.model.pdf.notice.*; +import org.apache.commons.lang3.StringUtils; + +import java.util.Collections; + +public class TemplateDataMapper { + + public static PaymentNotice mapTemplate(NoticeRequestData noticeRequestData) { + + String noticeCode = noticeRequestData.getNotice().getCode(); + String cbill = noticeRequestData.getCreditorInstitution().getCbill(); + String ciTaxCode = noticeRequestData.getCreditorInstitution().getTaxCode(); + String noticeAmount = String.valueOf(noticeRequestData.getNotice().getPaymentAmount()); + + return PaymentNotice.builder() + .payee(Payee.builder() + .taxCode(ciTaxCode) + .name(noticeRequestData.getDebtor().getFullName()) + .channel(Channel.builder().online( + Online.builder() + .website(noticeRequestData.getCreditorInstitution().getWebChannel()) + .build() + ).build()) + .build() + ) + .debtor(Debtor + .builder() + .buildingNumber("") //TODO + .province("") //TODO + .city("") //TODO + .postalCode("") //TODO + .address("") //TODO + .fullName(noticeRequestData.getDebtor().getFullName()) + .taxCode(noticeRequestData.getDebtor().getTaxCode()) + .build() + ) + .notice(Notice.builder() + .refNumber(noticeCode) + .cbillCode(cbill) + .qrCode( + generateQrCode(noticeCode, + ciTaxCode, + noticeAmount) + ) + .subject(noticeRequestData.getNotice().getSubject()) + .amount(noticeAmount) + .expiryDate(noticeRequestData.getNotice().getDueDate()) + .instalments(noticeRequestData.getNotice().getInstallments() != null ? + noticeRequestData.getNotice().getInstallments().stream().map(item -> + mapInstallment( + noticeCode, + cbill, + item + )).toList() : + Collections.emptyList()) + .build()) + .build(); + } + + public static String generateQrCode(String code, String taxCode, String amount) { + return String.join("|", + "PAGOPA","002", + StringUtils.leftPad(code, 18, "0"), + StringUtils.leftPad(code, 11, "0"), + taxCode, + amount + ); + } + + public static Installment mapInstallment( + String cbill, String taxCode, InstallmentData installmentData) { + String amount = String.valueOf(installmentData.getAmount()); + return Installment.builder() + .refNumber(installmentData.getCode()) + .cbillCode(cbill) + .qrCode(generateQrCode(installmentData.getCode(), taxCode, amount)) + .amount(amount) + .expiryDate(installmentData.getDueDate()) + .build(); + } + +} diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/CreditorInstitution.java b/src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/CreditorInstitution.java index 00c77ca..1c89887 100644 --- a/src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/CreditorInstitution.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/CreditorInstitution.java @@ -3,7 +3,15 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; - +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder public class CreditorInstitution { @Schema(description = "CI tax code", requiredMode = Schema.RequiredMode.REQUIRED) @@ -11,27 +19,23 @@ public class CreditorInstitution { @NotEmpty private String taxCode; - @Schema(description = "CI full name", requiredMode = Schema.RequiredMode.REQUIRED) - @NotNull - @NotEmpty + @Schema(description = "CI full name") private String fullName; + + @Schema(description = "CI Organization") private String organization; - @Schema(description = "CI info", requiredMode = Schema.RequiredMode.REQUIRED) - @NotNull - @NotEmpty + @Schema(description = "CI info") private String info; - @Schema(description = "Boolean to refer if it has a web channel", requiredMode = Schema.RequiredMode.REQUIRED) + @Schema(description = "Boolean to refer if it has a web channel") @NotNull private Boolean webChannel; @Schema(description = "CI physical channel data") private String physicalChannel; - @Schema(description = "CI cbill", requiredMode = Schema.RequiredMode.REQUIRED) - @NotNull - @NotEmpty + @Schema(description = "CI cbill") private String cbill; diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/Debtor.java b/src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/Debtor.java index 4456907..fa2b140 100644 --- a/src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/Debtor.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/Debtor.java @@ -3,7 +3,15 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder public class Debtor { @Schema(description = "Debtor taxCode") diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/InstallmentData.java b/src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/InstallmentData.java index be86c63..dba810a 100644 --- a/src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/InstallmentData.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/InstallmentData.java @@ -6,8 +6,10 @@ @Data public class InstallmentData { + @Schema(description = "Installment code", requiredMode = Schema.RequiredMode.REQUIRED) + private String code; @Schema(description = "Installment amount", requiredMode = Schema.RequiredMode.REQUIRED) - private Integer amount; + private Long amount; @Schema(description = "Installment dueDate", requiredMode = Schema.RequiredMode.REQUIRED) private String dueDate; diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/Notice.java b/src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/Notice.java index 183f97f..508afd1 100644 --- a/src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/Notice.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/Notice.java @@ -33,6 +33,6 @@ public class Notice { private String code; @Schema(description = "Notice installments (if present)") - private List installments; + private List installments; } diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/PdfEngineErrorMessage.java b/src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/PdfEngineErrorMessage.java new file mode 100644 index 0000000..b9555ac --- /dev/null +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/PdfEngineErrorMessage.java @@ -0,0 +1,13 @@ +package it.gov.pagopa.payment.notice.generator.model.pdf; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Getter; + +/** + * Model class for PDF engine HTTP error messages + */ +@Getter +@JsonIgnoreProperties(ignoreUnknown = true) +public class PdfEngineErrorMessage { + private String message; +} diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/PdfEngineErrorResponse.java b/src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/PdfEngineErrorResponse.java new file mode 100644 index 0000000..0eed639 --- /dev/null +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/PdfEngineErrorResponse.java @@ -0,0 +1,19 @@ +package it.gov.pagopa.payment.notice.generator.model.pdf; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +/** + * Model class for PDF engine HTTP error response + */ +@Getter +@Setter +@JsonIgnoreProperties(ignoreUnknown = true) +public class PdfEngineErrorResponse { + + private List errors; + +} diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/PdfEngineRequest.java b/src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/PdfEngineRequest.java new file mode 100644 index 0000000..b304176 --- /dev/null +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/PdfEngineRequest.java @@ -0,0 +1,20 @@ +package it.gov.pagopa.payment.notice.generator.model.pdf; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.net.URL; + +/** + * Model class for PDF engine request + */ +@Getter +@Setter +@NoArgsConstructor +public class PdfEngineRequest { + + URL template; + String data; + boolean applySignature; +} diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/PdfEngineResponse.java b/src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/PdfEngineResponse.java new file mode 100644 index 0000000..3a85ffd --- /dev/null +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/PdfEngineResponse.java @@ -0,0 +1,19 @@ +package it.gov.pagopa.payment.notice.generator.model.pdf; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +/** + * Model class for PDF Engine client's response + */ +@Getter +@Setter +@NoArgsConstructor +public class PdfEngineResponse { + + String tempPdfPath; + int statusCode; + String errorMessage; + +} diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/notice/Channel.java b/src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/notice/Channel.java new file mode 100644 index 0000000..306285b --- /dev/null +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/notice/Channel.java @@ -0,0 +1,16 @@ +package it.gov.pagopa.payment.notice.generator.model.pdf.notice; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class Channel { + + private Online online; + +} diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/notice/Debtor.java b/src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/notice/Debtor.java new file mode 100644 index 0000000..2e69d1d --- /dev/null +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/notice/Debtor.java @@ -0,0 +1,22 @@ +package it.gov.pagopa.payment.notice.generator.model.pdf.notice; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class Debtor { + + private String fullName; + private String taxCode; + private String address; + private String buildingNumber; + private String postalCode; + private String city; + private String province; + +} diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/notice/Installment.java b/src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/notice/Installment.java new file mode 100644 index 0000000..e4edd15 --- /dev/null +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/notice/Installment.java @@ -0,0 +1,20 @@ +package it.gov.pagopa.payment.notice.generator.model.pdf.notice; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class Installment { + + private String refNumber; + private String amount; + private String expiryDate; + private String cbillCode; + private String qrCode; + +} diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/notice/Notice.java b/src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/notice/Notice.java new file mode 100644 index 0000000..8f9d01f --- /dev/null +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/notice/Notice.java @@ -0,0 +1,24 @@ +package it.gov.pagopa.payment.notice.generator.model.pdf.notice; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class Notice { + + private String subject; + private String amount; + private String expiryDate; + private String qrCode; + private String refNumber; + private String cbillCode; + private List instalments; + +} diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/notice/Online.java b/src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/notice/Online.java new file mode 100644 index 0000000..f851e0c --- /dev/null +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/notice/Online.java @@ -0,0 +1,17 @@ +package it.gov.pagopa.payment.notice.generator.model.pdf.notice; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class Online { + + private boolean website; + private boolean app; + +} diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/notice/Payee.java b/src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/notice/Payee.java new file mode 100644 index 0000000..866fc9d --- /dev/null +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/notice/Payee.java @@ -0,0 +1,21 @@ +package it.gov.pagopa.payment.notice.generator.model.pdf.notice; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class Payee { + + private String logo; + private String name; + private String taxCode; + private String sector; + private String additionalInfo; + private Channel channel; + +} diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/notice/PaymentNotice.java b/src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/notice/PaymentNotice.java new file mode 100644 index 0000000..2bfb8ec --- /dev/null +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/notice/PaymentNotice.java @@ -0,0 +1,18 @@ +package it.gov.pagopa.payment.notice.generator.model.pdf.notice; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class PaymentNotice { + + private Debtor debtor; + private Payee payee; + private Notice notice; + +} diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/repository/PaymentGenerationRequestErrorRepository.java b/src/main/java/it/gov/pagopa/payment/notice/generator/repository/PaymentGenerationRequestErrorRepository.java index db4fbdb..a79af23 100644 --- a/src/main/java/it/gov/pagopa/payment/notice/generator/repository/PaymentGenerationRequestErrorRepository.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/repository/PaymentGenerationRequestErrorRepository.java @@ -2,15 +2,11 @@ import it.gov.pagopa.payment.notice.generator.entity.PaymentNoticeGenerationRequestError; import org.springframework.data.mongodb.repository.MongoRepository; -import org.springframework.data.mongodb.repository.Query; import org.springframework.stereotype.Repository; -import java.util.List; - @Repository -public interface PaymentGenerationRequestErrorRepository extends MongoRepository { +public interface PaymentGenerationRequestErrorRepository + extends MongoRepository { - @Query(value = "{ folderId : ?0}", fields = "{}") - List findErrors(String folderId); } diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/repository/PaymentGenerationRequestRepository.java b/src/main/java/it/gov/pagopa/payment/notice/generator/repository/PaymentGenerationRequestRepository.java index 13ecddb..13a5c1a 100644 --- a/src/main/java/it/gov/pagopa/payment/notice/generator/repository/PaymentGenerationRequestRepository.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/repository/PaymentGenerationRequestRepository.java @@ -2,6 +2,7 @@ import it.gov.pagopa.payment.notice.generator.entity.PaymentNoticeGenerationRequest; import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.data.mongodb.repository.Query; import org.springframework.data.mongodb.repository.Update; import org.springframework.stereotype.Repository; @@ -9,7 +10,12 @@ @Repository public interface PaymentGenerationRequestRepository extends MongoRepository { - Optional findByIdAndUserId(String folderId, String userId); + + @Query("{'id' : ?0}") + @Update("{ '$set': { 'status' : 'PROCESSING' }, " + + "'$inc' : { 'numberOfElementsProcessed' : 1 }, " + + "'$addToSet' : { 'items' : '?1'} }") + long findAndAddItemById(String folderId, String noticeId); @Update("{ '$inc' : { 'numberOfElementsFailed' : 1 } }") long findAndIncrementNumberOfElementsFailedById(String folderId); diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationService.java b/src/main/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationService.java index 72bfac3..6bb15ae 100644 --- a/src/main/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationService.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationService.java @@ -1,10 +1,12 @@ package it.gov.pagopa.payment.notice.generator.service; +import com.fasterxml.jackson.core.JsonProcessingException; import it.gov.pagopa.payment.notice.generator.model.NoticeGenerationRequestItem; import java.io.File; +import java.net.MalformedURLException; public interface NoticeGenerationService { File generateNotice(NoticeGenerationRequestItem noticeGenerationRequestItem, - String folderId); + String folderId) ; } diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationServiceImpl.java b/src/main/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationServiceImpl.java index bcfb014..40ebb67 100644 --- a/src/main/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationServiceImpl.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationServiceImpl.java @@ -2,25 +2,40 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import it.gov.pagopa.payment.notice.generator.client.PdfEngineClient; import it.gov.pagopa.payment.notice.generator.entity.PaymentNoticeGenerationRequest; import it.gov.pagopa.payment.notice.generator.entity.PaymentNoticeGenerationRequestError; import it.gov.pagopa.payment.notice.generator.exception.Aes256Exception; import it.gov.pagopa.payment.notice.generator.exception.AppError; import it.gov.pagopa.payment.notice.generator.exception.AppException; +import it.gov.pagopa.payment.notice.generator.mapper.TemplateDataMapper; import it.gov.pagopa.payment.notice.generator.model.NoticeGenerationRequestItem; +import it.gov.pagopa.payment.notice.generator.model.notice.CreditorInstitution; +import it.gov.pagopa.payment.notice.generator.model.pdf.PdfEngineRequest; +import it.gov.pagopa.payment.notice.generator.model.pdf.PdfEngineResponse; import it.gov.pagopa.payment.notice.generator.repository.PaymentGenerationRequestErrorRepository; import it.gov.pagopa.payment.notice.generator.repository.PaymentGenerationRequestRepository; import it.gov.pagopa.payment.notice.generator.storage.InstitutionsStorageClient; import it.gov.pagopa.payment.notice.generator.storage.NoticeStorageClient; import it.gov.pagopa.payment.notice.generator.storage.NoticeTemplateStorageClient; import it.gov.pagopa.payment.notice.generator.util.Aes256Utils; +import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; +import org.apache.http.HttpStatus; import org.springframework.stereotype.Service; +import java.io.BufferedInputStream; import java.io.File; +import java.io.FileInputStream; +import java.nio.file.Files; +import java.nio.file.Path; import java.time.Instant; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; import java.util.Optional; +import static it.gov.pagopa.payment.notice.generator.util.WorkingDirectoryUtils.createWorkingDirectory; + @Service @Slf4j public class NoticeGenerationServiceImpl implements NoticeGenerationService { @@ -28,10 +43,14 @@ public class NoticeGenerationServiceImpl implements NoticeGenerationService { private final PaymentGenerationRequestErrorRepository paymentGenerationRequestErrorRepository; private final PaymentGenerationRequestRepository paymentGenerationRequestRepository; + private final PdfEngineClient pdfEngineClient; + private final InstitutionsStorageClient institutionsStorageClient; private final NoticeStorageClient noticeStorageClient; private final NoticeTemplateStorageClient noticeTemplateStorageClient; + private final TemplateDataMapper templateDataMapper; + private final Aes256Utils aes256Utils; private final ObjectMapper objectMapper; @@ -42,6 +61,8 @@ public NoticeGenerationServiceImpl( InstitutionsStorageClient institutionsStorageClient, NoticeStorageClient noticeStorageClient, NoticeTemplateStorageClient noticeTemplateStorageClient, + TemplateDataMapper templateDataMapper, + PdfEngineClient pdfEngineClient, Aes256Utils aes256Utils, ObjectMapper objectMapper) { this.paymentGenerationRequestRepository = paymentGenerationRequestRepository; @@ -49,10 +70,13 @@ public NoticeGenerationServiceImpl( this.institutionsStorageClient = institutionsStorageClient; this.noticeStorageClient = noticeStorageClient; this.noticeTemplateStorageClient = noticeTemplateStorageClient; + this.templateDataMapper = templateDataMapper; + this.pdfEngineClient = pdfEngineClient; this.aes256Utils = aes256Utils; this.objectMapper = objectMapper; } + @SneakyThrows @Override public File generateNotice(NoticeGenerationRequestItem noticeGenerationRequestItem, String folderId) { @@ -66,13 +90,54 @@ public File generateNotice(NoticeGenerationRequestItem noticeGenerationRequestIt } try { + + File workingDirectory = createWorkingDirectory(); + Path tempDirectory = Files.createTempDirectory(workingDirectory.toPath(), "notice-generator") + .normalize().toAbsolutePath(); + File templateFile = noticeTemplateStorageClient.getTemplate( noticeGenerationRequestItem.getTemplateId()); - File noticeFile = null; - return noticeFile; + CreditorInstitution creditorInstitution = institutionsStorageClient.getInstitutionData( + noticeGenerationRequestItem.getData().getCreditorInstitution().getTaxCode()); + noticeGenerationRequestItem.getData().setCreditorInstitution(creditorInstitution); + + PdfEngineRequest request = new PdfEngineRequest(); + + //Build the request + request.setTemplate(templateFile.toURI().toURL()); + request.setData(objectMapper.writeValueAsString(noticeGenerationRequestItem.getData())); + request.setApplySignature(false); + + PdfEngineResponse pdfEngineResponse = pdfEngineClient.generatePDF(request, tempDirectory); + + if (pdfEngineResponse.getStatusCode() != HttpStatus.SC_OK) { + String errMsg = String.format("PDF-Engine response KO (%s): %s", + pdfEngineResponse.getStatusCode(), pdfEngineResponse.getErrorMessage()); + log.error(errMsg); + throw new AppException(AppError.UNKNOWN); + } + + if (folderId != null) { + try (BufferedInputStream pdfStream = new BufferedInputStream( + new FileInputStream(pdfEngineResponse.getTempPdfPath()))) { + + String dateFormatted = LocalDate.now().format(DateTimeFormatter.ofPattern("yyMMdd")); + String blobName = String.format("%s-%s-%s", "pagopa-avviso", dateFormatted, + noticeGenerationRequestItem.getData().getNotice().getCode()); + noticeStorageClient.savePdfToBlobStorage(pdfStream, blobName); + paymentGenerationRequestRepository.findAndAddItemById(folderId, blobName); + + } catch (Exception e) { + log.error(e.getMessage(), e); + throw new AppException(AppError.UNKNOWN); + } + } + + return new File(pdfEngineResponse.getTempPdfPath()); } catch (Exception e) { + log.error(e.getMessage(), e); if (folderId != null) { saveErrorEvent(folderId, noticeGenerationRequestItem); } diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/storage/InstitutionsStorageClient.java b/src/main/java/it/gov/pagopa/payment/notice/generator/storage/InstitutionsStorageClient.java index d9b9934..a3d23aa 100644 --- a/src/main/java/it/gov/pagopa/payment/notice/generator/storage/InstitutionsStorageClient.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/storage/InstitutionsStorageClient.java @@ -1,5 +1,6 @@ package it.gov.pagopa.payment.notice.generator.storage; +import com.azure.core.util.BinaryData; import com.azure.core.util.Context; import com.azure.storage.blob.BlobContainerClient; import com.azure.storage.blob.BlobServiceClient; @@ -7,8 +8,11 @@ import com.azure.storage.blob.models.BlobStorageException; import com.azure.storage.blob.models.DownloadRetryOptions; import com.azure.storage.blob.options.BlobDownloadToFileOptions; +import com.fasterxml.jackson.databind.ObjectMapper; import it.gov.pagopa.payment.notice.generator.exception.AppError; import it.gov.pagopa.payment.notice.generator.exception.AppException; +import it.gov.pagopa.payment.notice.generator.model.notice.CreditorInstitution; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @@ -23,97 +27,59 @@ import java.util.HashSet; @Component +@Slf4j public class InstitutionsStorageClient { private BlobContainerClient blobContainerClient; - private Integer maxRetry; - - private Integer timeout; + private ObjectMapper objectMapper; @Autowired public InstitutionsStorageClient( @Value("${spring.cloud.azure.storage.blob.institutions.enabled}") String enabled, @Value("${spring.cloud.azure.storage.blob.institutions.connection_string}") String connectionString, @Value("${spring.cloud.azure.storage.blob.institutions.containerName}") String containerName, - @Value("${spring.cloud.azure.storage.blob.institutions.retry}") Integer maxRetry, - @Value("${spring.cloud.azure.storage.blob.institutions.timeout}") Integer timeout) { + ObjectMapper objectMapper) { if (Boolean.TRUE.toString().equals(enabled)) { BlobServiceClient blobServiceClient = new BlobServiceClientBuilder() .connectionString(connectionString).buildClient(); blobContainerClient = blobServiceClient.getBlobContainerClient(containerName); - this.maxRetry = maxRetry; - this.timeout = timeout; + this.objectMapper = objectMapper; } } public InstitutionsStorageClient( Boolean enabled, - BlobContainerClient blobContainerClient) { + BlobContainerClient blobContainerClient, + ObjectMapper objectMapper) { if (Boolean.TRUE.equals(enabled)) { this.blobContainerClient = blobContainerClient; - this.maxRetry=3; - this.timeout=10; + this.objectMapper = objectMapper; } } /** - * Retrieve the template from the Blob Storage + * Retrieve the institutionData from the Blob Storage * * @param institutionCode the name of the institution to be retrieved * @return the File with the reference to the downloaded data * @throws AppException thrown for error when retrieving the data */ - public File getTemplate(String institutionCode) { - + public CreditorInstitution getInstitutionData(String institutionCode) { if (blobContainerClient == null) { throw new AppException(AppError.TEMPLATE_CLIENT_UNAVAILABLE); } - String filePath = createTempDirectory(institutionCode); try { - blobContainerClient.getBlobClient(institutionCode.concat("/data.zip")) - .downloadToFileWithResponse( - getBlobDownloadToFileOptions(filePath), - Duration.ofSeconds(timeout), - Context.NONE); - return new File(filePath); + BinaryData jsonData = blobContainerClient.getBlobClient(institutionCode.concat("/data.json")) + .downloadContent(); + return objectMapper.readValue(jsonData.toBytes(), CreditorInstitution.class); } catch (BlobStorageException blobStorageException) { - throw new AppException(AppError.TEMPLATE_NOT_FOUND, blobStorageException); - } - } - - private BlobDownloadToFileOptions getBlobDownloadToFileOptions(String filePath) { - return new BlobDownloadToFileOptions(filePath) - .setDownloadRetryOptions(new DownloadRetryOptions().setMaxRetryRequests(maxRetry)) - .setOpenOptions(new HashSet<>( - Arrays.asList( - StandardOpenOption.CREATE_NEW, - StandardOpenOption.WRITE, - StandardOpenOption.READ - )) - ); - } - - private String createTempDirectory(String templateId) { - try { - File workingDirectory = createWorkingDirectory(); - Path tempDirectory = Files.createTempDirectory(workingDirectory.toPath(), "notice-generator") - .normalize().toAbsolutePath(); - Path filePath = tempDirectory.resolve(templateId + ".zip").normalize().toAbsolutePath(); - if (!filePath.startsWith(tempDirectory + File.separator)) { - throw new IllegalArgumentException("Invalid filename"); - } - return filePath.toFile().getAbsolutePath(); - } catch (IOException e) { - throw new AppException(AppError.TEMPLATE_CLIENT_ERROR, e); - } - } + log.error(blobStorageException.getMessage(), blobStorageException); + throw new AppException(AppError.INSTITUTION_NOT_FOUND, blobStorageException); + } catch (IOException ioException) { + log.error(ioException.getMessage(), ioException); + throw new AppException(AppError.INSTITUTION_PARSING_ERROR, ioException); - private File createWorkingDirectory() throws IOException { - File workingDirectory = new File("temp"); - if (!workingDirectory.exists()) { - Files.createDirectory(workingDirectory.toPath()); } - return workingDirectory; } } diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/storage/NoticeStorageClient.java b/src/main/java/it/gov/pagopa/payment/notice/generator/storage/NoticeStorageClient.java index 9bd8ca9..8aa35bc 100644 --- a/src/main/java/it/gov/pagopa/payment/notice/generator/storage/NoticeStorageClient.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/storage/NoticeStorageClient.java @@ -1,20 +1,26 @@ package it.gov.pagopa.payment.notice.generator.storage; +import com.azure.core.http.rest.Response; import com.azure.core.util.Context; +import com.azure.storage.blob.BlobClient; import com.azure.storage.blob.BlobContainerClient; import com.azure.storage.blob.BlobServiceClient; import com.azure.storage.blob.BlobServiceClientBuilder; import com.azure.storage.blob.models.BlobStorageException; +import com.azure.storage.blob.models.BlockBlobItem; import com.azure.storage.blob.models.DownloadRetryOptions; import com.azure.storage.blob.options.BlobDownloadToFileOptions; +import com.azure.storage.blob.options.BlobParallelUploadOptions; import it.gov.pagopa.payment.notice.generator.exception.AppError; import it.gov.pagopa.payment.notice.generator.exception.AppException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; @@ -57,63 +63,28 @@ public NoticeStorageClient( } /** - * Retrieve the template from the Blob Storage + * Handles saving the PDF to the blob storage * - * @param institutionCode the name of the institution to be retrieved - * @return the File with the reference to the downloaded data - * @throws AppException thrown for error when retrieving the data + * @param pdf PDF file + * @param fileName Filename to save the PDF with + * @return blob storage response with PDF metadata or error message and status */ - public File getTemplate(String institutionCode) { + public boolean savePdfToBlobStorage(InputStream pdf, String fileName) { - if (blobContainerClient == null) { - throw new AppException(AppError.TEMPLATE_CLIENT_UNAVAILABLE); - } - String filePath = createTempDirectory(institutionCode); - try { - blobContainerClient.getBlobClient(institutionCode.concat("/data.zip")) - .downloadToFileWithResponse( - getBlobDownloadToFileOptions(filePath), - Duration.ofSeconds(timeout), - Context.NONE); - return new File(filePath); - } catch (BlobStorageException blobStorageException) { - throw new AppException(AppError.TEMPLATE_NOT_FOUND, blobStorageException); - } - } + //Get a reference to a blob + BlobClient blobClient = blobContainerClient.getBlobClient(fileName); - private BlobDownloadToFileOptions getBlobDownloadToFileOptions(String filePath) { - return new BlobDownloadToFileOptions(filePath) - .setDownloadRetryOptions(new DownloadRetryOptions().setMaxRetryRequests(maxRetry)) - .setOpenOptions(new HashSet<>( - Arrays.asList( - StandardOpenOption.CREATE_NEW, - StandardOpenOption.WRITE, - StandardOpenOption.READ - )) - ); - } + //Upload the blob + Response blockBlobItemResponse = blobClient.uploadWithResponse( + new BlobParallelUploadOptions( + pdf + ), null, null); - private String createTempDirectory(String templateId) { - try { - File workingDirectory = createWorkingDirectory(); - Path tempDirectory = Files.createTempDirectory(workingDirectory.toPath(), "notice-generator") - .normalize().toAbsolutePath(); - Path filePath = tempDirectory.resolve(templateId + ".zip").normalize().toAbsolutePath(); - if (!filePath.startsWith(tempDirectory + File.separator)) { - throw new IllegalArgumentException("Invalid filename"); - } - return filePath.toFile().getAbsolutePath(); - } catch (IOException e) { - throw new AppException(AppError.TEMPLATE_CLIENT_ERROR, e); - } - } + //Build response accordingly + int statusCode = blockBlobItemResponse.getStatusCode(); + + return statusCode == HttpStatus.CREATED.value(); - private File createWorkingDirectory() throws IOException { - File workingDirectory = new File("temp"); - if (!workingDirectory.exists()) { - Files.createDirectory(workingDirectory.toPath()); - } - return workingDirectory; } } diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/storage/NoticeTemplateStorageClient.java b/src/main/java/it/gov/pagopa/payment/notice/generator/storage/NoticeTemplateStorageClient.java index c6ba6c9..7deae31 100644 --- a/src/main/java/it/gov/pagopa/payment/notice/generator/storage/NoticeTemplateStorageClient.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/storage/NoticeTemplateStorageClient.java @@ -9,8 +9,10 @@ import com.azure.storage.blob.options.BlobDownloadToFileOptions; import it.gov.pagopa.payment.notice.generator.exception.AppError; import it.gov.pagopa.payment.notice.generator.exception.AppException; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import java.io.File; @@ -21,9 +23,11 @@ import java.time.Duration; import java.util.Arrays; import java.util.HashSet; -import java.util.List; + +import static it.gov.pagopa.payment.notice.generator.util.WorkingDirectoryUtils.createWorkingDirectory; @Component +@Slf4j public class NoticeTemplateStorageClient { private BlobContainerClient blobContainerClient; @@ -69,13 +73,15 @@ public File getTemplate(String templateId) { if (blobContainerClient == null) { throw new AppException(AppError.TEMPLATE_CLIENT_UNAVAILABLE); } - String filePath = createTempDirectory(templateId); + String filePath = createTemplatesDirectory(templateId); try { - blobContainerClient.getBlobClient(templateId.concat("/template.zip")) - .downloadToFileWithResponse( - getBlobDownloadToFileOptions(filePath), - Duration.ofSeconds(timeout), - Context.NONE); + if (!new File(filePath).exists()) { + blobContainerClient.getBlobClient(templateId.concat("/template.zip")) + .downloadToFileWithResponse( + getBlobDownloadToFileOptions(filePath), + Duration.ofSeconds(timeout), + Context.NONE); + } return new File(filePath); } catch (BlobStorageException blobStorageException) { throw new AppException(AppError.TEMPLATE_NOT_FOUND, blobStorageException); @@ -94,13 +100,12 @@ private BlobDownloadToFileOptions getBlobDownloadToFileOptions(String filePath) ); } - private String createTempDirectory(String templateId) { + private String createTemplatesDirectory(String templateId) { try { File workingDirectory = createWorkingDirectory(); - Path tempDirectory = Files.createTempDirectory(workingDirectory.toPath(), "notice-generator") - .normalize().toAbsolutePath(); - Path filePath = tempDirectory.resolve(templateId + ".zip").normalize().toAbsolutePath(); - if (!filePath.startsWith(tempDirectory + File.separator)) { + Path filePath = workingDirectory.toPath().resolve( + "templates/"+templateId + ".zip").normalize().toAbsolutePath(); + if (!filePath.startsWith(workingDirectory + File.separator)) { throw new IllegalArgumentException("Invalid filename"); } return filePath.toFile().getAbsolutePath(); @@ -109,12 +114,14 @@ private String createTempDirectory(String templateId) { } } - private File createWorkingDirectory() throws IOException { - File workingDirectory = new File("temp"); - if (!workingDirectory.exists()) { - Files.createDirectory(workingDirectory.toPath()); + @Scheduled(cron = "${spring.cloud.azure.storage.blob.templates.timeout}") + private void refreshTemplates() { + File templateFiles = new File("temp/templates"); + if (templateFiles.exists()) { + if (!templateFiles.delete()) { + log.warn("Error while deleting template directory"); + } } - return workingDirectory; } } diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/util/WorkingDirectoryUtils.java b/src/main/java/it/gov/pagopa/payment/notice/generator/util/WorkingDirectoryUtils.java new file mode 100644 index 0000000..042b218 --- /dev/null +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/util/WorkingDirectoryUtils.java @@ -0,0 +1,17 @@ +package it.gov.pagopa.payment.notice.generator.util; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; + +public class WorkingDirectoryUtils { + + public static File createWorkingDirectory() throws IOException { + File workingDirectory = new File("temp"); + if (!workingDirectory.exists()) { + Files.createDirectory(workingDirectory.toPath()); + } + return workingDirectory; + } + +} From 7d1519f66ab26d11370c5f84aeb5ac04ef0169ee Mon Sep 17 00:00:00 2001 From: Alessio Cialini Date: Mon, 29 Apr 2024 17:23:32 +0200 Subject: [PATCH 03/19] [VAS-834][VAS-973] feat: Updated mapping, introduced Kafka listener for generation --- .../NoticeGenerationConsumerConfig.java | 17 ++++++ .../NoticeGenerationController.java | 12 +--- .../generator/mapper/TemplateDataMapper.java | 22 ++++--- .../generator/model/NoticeRequestEH.java | 11 ++++ .../model/notice/CreditorInstitution.java | 2 + .../notice/generator/model/notice/Debtor.java | 23 +++++++ .../generator/model/pdf/notice/Channel.java | 1 + .../generator/model/pdf/notice/Physical.java | 16 +++++ .../service/NoticeGenerationService.java | 4 +- .../service/NoticeGenerationServiceImpl.java | 54 +++++++++++++++-- .../generator/util/WorkingDirectoryUtils.java | 12 ++++ src/main/resources/application.properties | 60 ++++++++++++++++++- 12 files changed, 208 insertions(+), 26 deletions(-) create mode 100644 src/main/java/it/gov/pagopa/payment/notice/generator/consumer/NoticeGenerationConsumerConfig.java create mode 100644 src/main/java/it/gov/pagopa/payment/notice/generator/model/NoticeRequestEH.java create mode 100644 src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/notice/Physical.java diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/consumer/NoticeGenerationConsumerConfig.java b/src/main/java/it/gov/pagopa/payment/notice/generator/consumer/NoticeGenerationConsumerConfig.java new file mode 100644 index 0000000..73e2b06 --- /dev/null +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/consumer/NoticeGenerationConsumerConfig.java @@ -0,0 +1,17 @@ +package it.gov.pagopa.payment.notice.generator.consumer; + +import it.gov.pagopa.payment.notice.generator.service.NoticeGenerationService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.function.Consumer; + +@Configuration +@Slf4j +public class NoticeGenerationConsumerConfig { + @Bean + public Consumer noticeGeneration(NoticeGenerationService noticeGenerationService){ + return noticeGenerationService::processNoticeGenerationEH; + } +} diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/controller/NoticeGenerationController.java b/src/main/java/it/gov/pagopa/payment/notice/generator/controller/NoticeGenerationController.java index 9b83c04..d85e648 100644 --- a/src/main/java/it/gov/pagopa/payment/notice/generator/controller/NoticeGenerationController.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/controller/NoticeGenerationController.java @@ -7,7 +7,6 @@ import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.io.FileUtils; import org.springframework.core.io.ByteArrayResource; import org.springframework.core.io.Resource; import org.springframework.http.HttpHeaders; @@ -21,6 +20,8 @@ import java.io.FileInputStream; import java.io.IOException; +import static it.gov.pagopa.payment.notice.generator.util.WorkingDirectoryUtils.clearTempDirectory; + @RestController @RequestMapping(value = "/notices", produces = MediaType.APPLICATION_JSON_VALUE) @Validated @@ -53,13 +54,4 @@ public ResponseEntity generateNotice( } } - private void clearTempDirectory(java.nio.file.Path workingDirPath) { - try { - FileUtils.deleteDirectory(workingDirPath.toFile()); - } catch (IOException e) { - log.warn("Unable to clear working directory", e); - } - } - - } diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/mapper/TemplateDataMapper.java b/src/main/java/it/gov/pagopa/payment/notice/generator/mapper/TemplateDataMapper.java index a243e28..7b5fd9e 100644 --- a/src/main/java/it/gov/pagopa/payment/notice/generator/mapper/TemplateDataMapper.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/mapper/TemplateDataMapper.java @@ -23,17 +23,25 @@ public static PaymentNotice mapTemplate(NoticeRequestData noticeRequestData) { .channel(Channel.builder().online( Online.builder() .website(noticeRequestData.getCreditorInstitution().getWebChannel()) - .build() - ).build()) + .app(!noticeRequestData.getCreditorInstitution().getWebChannel()) + .build()) + .physical(Physical.builder() + .data(noticeRequestData.getCreditorInstitution().getPhysicalChannel()) + .build()) + .build() + ) + .logo(noticeRequestData.getCreditorInstitution().getLogo()) + .additionalInfo(noticeRequestData.getCreditorInstitution().getInfo()) + .sector(noticeRequestData.getCreditorInstitution().getOrganization()) .build() ) .debtor(Debtor .builder() - .buildingNumber("") //TODO - .province("") //TODO - .city("") //TODO - .postalCode("") //TODO - .address("") //TODO + .buildingNumber(noticeRequestData.getDebtor().getBuildingNumber()) + .province(noticeRequestData.getDebtor().getProvince()) + .city(noticeRequestData.getDebtor().getCity()) + .postalCode(noticeRequestData.getDebtor().getPostalCode()) + .address(noticeRequestData.getDebtor().getAddress()) .fullName(noticeRequestData.getDebtor().getFullName()) .taxCode(noticeRequestData.getDebtor().getTaxCode()) .build() diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/model/NoticeRequestEH.java b/src/main/java/it/gov/pagopa/payment/notice/generator/model/NoticeRequestEH.java new file mode 100644 index 0000000..4518d3e --- /dev/null +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/model/NoticeRequestEH.java @@ -0,0 +1,11 @@ +package it.gov.pagopa.payment.notice.generator.model; + +import lombok.Data; + +@Data +public class NoticeRequestEH { + + private String folderId; + private NoticeGenerationRequestItem noticeGenerationRequestItem; + +} diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/CreditorInstitution.java b/src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/CreditorInstitution.java index 1c89887..835b828 100644 --- a/src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/CreditorInstitution.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/CreditorInstitution.java @@ -38,5 +38,7 @@ public class CreditorInstitution { @Schema(description = "CI cbill") private String cbill; + private String logo; + } diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/Debtor.java b/src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/Debtor.java index fa2b140..45510e8 100644 --- a/src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/Debtor.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/Debtor.java @@ -16,12 +16,35 @@ public class Debtor { @Schema(description = "Debtor taxCode") private String taxCode; + @Schema(description = "Debtor full name", requiredMode = Schema.RequiredMode.REQUIRED) @NotNull @NotEmpty private String fullName; + @Schema(description = "Debtor address", requiredMode = Schema.RequiredMode.REQUIRED) @NotNull @NotEmpty private String address; + + @Schema(description = "Debtor postal code", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull + @NotEmpty + private String postalCode; + + @Schema(description = "Debtor city", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull + @NotEmpty + private String city; + + @Schema(description = "Debtor building number", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull + @NotEmpty + private String buildingNumber; + + @Schema(description = "Debtor province", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull + @NotEmpty + private String province; + } diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/notice/Channel.java b/src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/notice/Channel.java index 306285b..34e6b82 100644 --- a/src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/notice/Channel.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/notice/Channel.java @@ -12,5 +12,6 @@ public class Channel { private Online online; + private Physical physical; } diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/notice/Physical.java b/src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/notice/Physical.java new file mode 100644 index 0000000..0a7d1ee --- /dev/null +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/notice/Physical.java @@ -0,0 +1,16 @@ +package it.gov.pagopa.payment.notice.generator.model.pdf.notice; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class Physical { + + private String data; + +} diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationService.java b/src/main/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationService.java index 6bb15ae..aef4823 100644 --- a/src/main/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationService.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationService.java @@ -1,12 +1,12 @@ package it.gov.pagopa.payment.notice.generator.service; -import com.fasterxml.jackson.core.JsonProcessingException; import it.gov.pagopa.payment.notice.generator.model.NoticeGenerationRequestItem; import java.io.File; -import java.net.MalformedURLException; public interface NoticeGenerationService { File generateNotice(NoticeGenerationRequestItem noticeGenerationRequestItem, String folderId) ; + + void processNoticeGenerationEH(String message); } diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationServiceImpl.java b/src/main/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationServiceImpl.java index 40ebb67..be65085 100644 --- a/src/main/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationServiceImpl.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationServiceImpl.java @@ -10,6 +10,7 @@ import it.gov.pagopa.payment.notice.generator.exception.AppException; import it.gov.pagopa.payment.notice.generator.mapper.TemplateDataMapper; import it.gov.pagopa.payment.notice.generator.model.NoticeGenerationRequestItem; +import it.gov.pagopa.payment.notice.generator.model.NoticeRequestEH; import it.gov.pagopa.payment.notice.generator.model.notice.CreditorInstitution; import it.gov.pagopa.payment.notice.generator.model.pdf.PdfEngineRequest; import it.gov.pagopa.payment.notice.generator.model.pdf.PdfEngineResponse; @@ -34,6 +35,7 @@ import java.time.format.DateTimeFormatter; import java.util.Optional; +import static it.gov.pagopa.payment.notice.generator.util.WorkingDirectoryUtils.clearTempDirectory; import static it.gov.pagopa.payment.notice.generator.util.WorkingDirectoryUtils.createWorkingDirectory; @Service @@ -89,10 +91,12 @@ public File generateNotice(NoticeGenerationRequestItem noticeGenerationRequestIt } } + Path tempDirectory = null; + try { File workingDirectory = createWorkingDirectory(); - Path tempDirectory = Files.createTempDirectory(workingDirectory.toPath(), "notice-generator") + tempDirectory = Files.createTempDirectory(workingDirectory.toPath(), "notice-generator") .normalize().toAbsolutePath(); File templateFile = noticeTemplateStorageClient.getTemplate( @@ -139,18 +143,58 @@ public File generateNotice(NoticeGenerationRequestItem noticeGenerationRequestIt } catch (Exception e) { log.error(e.getMessage(), e); if (folderId != null) { - saveErrorEvent(folderId, noticeGenerationRequestItem); + saveErrorEvent(folderId, noticeGenerationRequestItem, e.getMessage()); } throw e; + } finally { + if (tempDirectory != null) { + clearTempDirectory(tempDirectory); + } } } - private void saveErrorEvent(String folderId, NoticeGenerationRequestItem noticeGenerationRequestItem) { + @Override + public void processNoticeGenerationEH(String message) { + + String folderId = null; + NoticeGenerationRequestItem noticeGenerationRequestItem = null; + + try { + + NoticeRequestEH noticeRequestEH = objectMapper.readValue(message, NoticeRequestEH.class); + folderId = noticeRequestEH.getFolderId(); + noticeGenerationRequestItem = noticeRequestEH.getNoticeGenerationRequestItem(); + + } catch (Exception e) { + try { + paymentGenerationRequestErrorRepository.save( + PaymentNoticeGenerationRequestError.builder() + .errorDescription("Unable to read EH message content") + .folderId("UNKNOWN") + .data(aes256Utils.encrypt(message)) + .createdAt(Instant.now()) + .numberOfAttempts(0) + .build()); + } catch (Exception cryptException) { + log.error( + "Unable to save unparsable data to error" + ); + } + } + + if (noticeGenerationRequestItem != null && folderId != null) { + generateNotice(noticeGenerationRequestItem, folderId); + } + + } + + private void saveErrorEvent( + String folderId, NoticeGenerationRequestItem noticeGenerationRequestItem, String error) { try { paymentGenerationRequestErrorRepository.save( PaymentNoticeGenerationRequestError.builder() - .errorDescription("Encountered error sending notice on EH") + .errorDescription(error) .folderId(folderId) .data(aes256Utils.encrypt(objectMapper .writeValueAsString(noticeGenerationRequestItem))) @@ -159,7 +203,7 @@ private void saveErrorEvent(String folderId, NoticeGenerationRequestItem noticeG .build() ); paymentGenerationRequestRepository.findAndIncrementNumberOfElementsFailedById(folderId); - } catch (JsonProcessingException | Aes256Exception e) { + } catch (Exception e) { log.error( "Unable to save notice data into error repository for notice with folder " + folderId + " and noticeId " + noticeGenerationRequestItem.getData().getNotice().getCode() diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/util/WorkingDirectoryUtils.java b/src/main/java/it/gov/pagopa/payment/notice/generator/util/WorkingDirectoryUtils.java index 042b218..f922745 100644 --- a/src/main/java/it/gov/pagopa/payment/notice/generator/util/WorkingDirectoryUtils.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/util/WorkingDirectoryUtils.java @@ -1,9 +1,13 @@ package it.gov.pagopa.payment.notice.generator.util; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.FileUtils; + import java.io.File; import java.io.IOException; import java.nio.file.Files; +@Slf4j public class WorkingDirectoryUtils { public static File createWorkingDirectory() throws IOException { @@ -14,4 +18,12 @@ public static File createWorkingDirectory() throws IOException { return workingDirectory; } + public static void clearTempDirectory(java.nio.file.Path workingDirPath) { + try { + FileUtils.deleteDirectory(workingDirPath.toFile()); + } catch (IOException e) { + log.warn("Unable to clear working directory", e); + } + } + } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 5ad4771..0a0a2c9 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,5 +1,6 @@ # Info info.application.artifactId=@project.artifactId@ +info.application.name=@project.name@ info.application.version=@project.version@ info.application.description=@project.description@ info.properties.environment=${ENV:azure} @@ -14,12 +15,67 @@ management.health.readinessState.enabled=true springdoc.writer-with-order-by-keys=true springdoc.writer-with-default-pretty-printer=true # Server -# TODO: set your base path server.servlet.context-path=/ server.port=8080 +server.shutdown=GRACEFUL # Logging logging.level.root=${DEFAULT_LOGGING_LEVEL:INFO} logging.level.it.gov.pagopa=${APP_LOGGING_LEVEL:INFO} # CORS configuration -cors.configuration=${CORS_CONFIGURATION:'{"origins": ["*"], "methods": ["*"]}'} +cors.configuration=${CORS_CONFIGURATION:{"origins": ["*"], "methods": ["*"]}} +# Mongo Configuration +spring.data.mongodb.uri=${MONGODB_CONNECTION_URI:} +spring.data.mongodb.database=${MONGODB_NAME:noticesMongoDb} +# Cache configuration +spring.cache.type=caffeine +spring.cache.caffeine.spec=maximumSize=${CACHE_SIZE:1000}, expireAfterWrite=${CACHE_EXPIRATION_TIME:720m} +cache.enabled=${CACHE_ENABLED:true} +# Jackson serialization +spring.jackson.default-property-inclusion=NON_NULL +spring.jackson.serialization.write-dates-as-timestamps=false +spring.jackson.serialization.fail-on-empty-beans=false +spring.jackson.deserialization.fail-on-unknown-properties=false + +spring.cloud.azure.storage.blob.templates.enabled=${TEMPLATE_STORAGE_ENABLED:true} +spring.cloud.azure.storage.blob.templates.connection_string=${TEMPLATE_STORAGE_CONN_STRING:} +spring.cloud.azure.storage.blob.templates.containerName=${TEMPLATE_STORAGE_CONTAINER_NAME:noticetemplateblob} +spring.cloud.azure.storage.blob.templates.tableName=${TEMPLATE_STORAGE_CONTAINER_NAME:noticetemplatedatatable} +spring.cloud.azure.storage.blob.templates.retry=${TEMPLATE_STORAGE_RETRY:3} +spring.cloud.azure.storage.blob.templates.timeout=${TEMPLATE_STORAGE_TIMEOUT:10} + +# EH Kafka Configuration +spring.cloud.function.definition=noticeGeneration +spring.cloud.stream.bindings.noticeGeneration-in-0.destination=${KAFKA_NOTICE_GENERATION_TOPIC:pagopa-notice-evt-rx} +spring.cloud.stream.bindings.noticeGeneration-in-0.group=${KAFKA_NOTICE_GENERATION_GROUP_ID:notice-generation-group} +spring.cloud.stream.bindings.noticeGeneration-in-0.content-type=${KAFKA_CONTENT_TYPE:application/json} +spring.cloud.stream.bindings.noticeGeneration-in-0.binder=notice-generation +spring.cloud.stream.bindings.noticeGeneration-in-0.consumer.autoStartup=false +spring.cloud.stream.binders.notice-generation.type=kafka +spring.cloud.stream.binders.notice-generation.environment.spring.cloud.stream.kafka.binder.brokers=${KAFKA_BROKER:localhost:9092} +spring.cloud.stream.binders.notice-generation.environment.spring.cloud.stream.kafka.binder.configuration.sasl.jaas.config=${KAFKA_SASL_JAAS_CONFIG:} +spring.cloud.stream.binders.notice-generation.environment.spring.cloud.stream.kafka.binder.configuration.key.serializer=org.apache.kafka.common.serialization.StringSerializer +spring.cloud.stream.kafka.binder.auto-create-topics=false +spring.cloud.stream.kafka.binder.configuration.heartbeat.interval.ms=${KAFKA_CONFIG_HEARTBEAT_INTERVAL_MS:3000} +spring.cloud.stream.kafka.binder.configuration.session.timeout.ms=${KAFKA_CONFIG_SESSION_TIMEOUT_MS:60000} +spring.cloud.stream.kafka.binder.configuration.request.timeout.ms=${KAFKA_CONFIG_REQUEST_TIMEOUT_MS:60000} +spring.cloud.stream.kafka.binder.configuration.sasl.mechanism=${KAFKA_CONFIG_SASL_MECHANISM:PLAIN} +spring.cloud.stream.kafka.binder.configuration.security.protocol=${KAFKA_CONFIG_SECURITY_PROTOCOL:SASL_SSL} +spring.cloud.stream.kafka.binder.configuration.connections.max.idle.ms=${KAFKA_CONFIG_CONNECTION_MAX_IDLE_TIME:180000} +spring.cloud.stream.kafka.binder.configuration.metadata.max.idle.ms=${KAFKA_CONFIG_METADATA_MAX_IDLE_MS:180000} +spring.cloud.stream.kafka.binder.configuration.metadata.max.age.ms=${KAFKA_CONFIG_METADATA_MAX_AGE_INTERVAL:179000} +spring.cloud.stream.kafka.binder.configuration.max.request.size=${KAFKA_CONFIG_METADATA_MAX_REQUEST_SIZE:1000000} +spring.cloud.stream.kafka.bindings.noticeGeneration-in-0.consumer.startOffset=${KAFKA_CONSUMER_CONFIG_START_OFFSET:earliest} +spring.cloud.stream.kafka.bindings.noticeGeneration-in-0.consumer.autoCommitOffset=false +spring.cloud.stream.kafka.bindings.noticeGeneration-in-0.consumer.ackMode=MANUAL_IMMEDIATE +spring.cloud.stream.kafka.bindings.noticeGeneration-in-0.consumer.ackTime=${KAFKA_TRANSACTION_USER_ID_SPLITTER_ACK_MILLIS:500} +spring.cloud.stream.kafka.bindings.noticeGeneration-in-0.consumer.standardHeaders=${KAFKA_CONSUMER_CONFIG_STANDARD_HEADERS:both} +spring.cloud.stream.kafka.bindings.noticeGeneration-in-0.consumer.configuration.max.poll.records=${KAFKA_CONSUMER_CONFIG_MAX_POLL_SIZE:500} +spring.cloud.stream.kafka.bindings.noticeGeneration-in-0.consumer.configuration.max.poll.interval.ms=${KAFKA_CONFIG_MAX_POLL_INTERVAL_TIMEOUT_MS:300000} +spring.cloud.stream.kafka.bindings.noticeGeneration-in-0.consumer.configuration.connections.max.idle.ms=${KAFKA_CONSUMER_CONFIG_CONNECTIONS_MAX_IDLE_MS:180000} +spring.cloud.stream.kafka.bindings.noticeGeneration-in-0.consumer.configuration.socket.connection.setup.timeout.max.ms=${KAFKA_CONSUMER_CONFIG_CONNECTION_TIMEOUT_MAX_MS:200000} +spring.cloud.stream.kafka.bindings.noticeGeneration-in-0.consumer.configuration.socket.connection.setup.timeout.ms=${KAFKA_CONSUMER_CONFIG_CONNECTION_TIMEOUT_MS:100000} + +#Other Configs +aes.secret.key=${AES_SECRET_KEY:} +aes.salt=${AES_SALT:} From 948a8dad2aa36ced7effd3b95407248dccc9c725 Mon Sep 17 00:00:00 2001 From: Alessio Cialini Date: Mon, 29 Apr 2024 19:03:46 +0200 Subject: [PATCH 04/19] [VAS-834][VAS-973] feat: Updated helm charts and infra --- .github/workflows/code_review.yml | 152 +++++++++--------- .../workflows/deploy_with_github_runner.yml | 2 +- .identity/99_variables.tf | 6 +- .identity/env/dev/backend.tfvars | 2 +- .identity/env/dev/terraform.tfvars | 2 +- .identity/env/prod/backend.tfvars | 2 +- .identity/env/prod/terraform.tfvars | 2 +- .identity/env/uat/backend.tfvars | 2 +- .identity/env/uat/terraform.tfvars | 2 +- .opex/env/prod/backend.tfvars | 2 +- .opex/env/prod/config.yaml | 4 +- .opex/env/prod/terraform.tfvars | 2 +- helm/Chart.yaml | 4 +- helm/values-dev.yaml | 24 +-- helm/values-prod.yaml | 28 ++-- helm/values-uat.yaml | 27 ++-- infra/env/weu-dev/backend.tfvars | 2 +- infra/env/weu-prod/backend.tfvars | 2 +- infra/env/weu-uat/backend.tfvars | 2 +- .../storage/NoticeStorageClient.java | 12 +- src/main/resources/application.properties | 12 +- 21 files changed, 157 insertions(+), 136 deletions(-) diff --git a/.github/workflows/code_review.yml b/.github/workflows/code_review.yml index 3ea4730..8cc79fe 100644 --- a/.github/workflows/code_review.yml +++ b/.github/workflows/code_review.yml @@ -18,7 +18,7 @@ on: workflow_dispatch: env: - PROJECT_KEY: # TODO + PROJECT_KEY: pagopa_pagopa-print-payment-notice-generator permissions: id-token: write @@ -35,84 +35,86 @@ jobs: # Steps represent a sequence of tasks that will be executed as part of the job steps: - name: Code Review - uses: pagopa/github-actions-template/maven-code-review@v1.4.2 + uses: pagopa/github-actions-template/maven-code-review@v1.10.6 with: github_token: ${{ secrets.GITHUB_TOKEN }} sonar_token: ${{ secrets.SONAR_TOKEN }} project_key: ${{env.PROJECT_KEY}} - coverage_exclusions: "**/config/*,**/*Mock*,**/model/**,**/entity/*" + coverage_exclusions: "**/config/*,**/*Mock*,**/model/**,**/entity/*,**/exception/*" cpd_exclusions: "**/model/**,**/entity/*" + java_version: 17 - smoke-test: - name: Smoke Test - runs-on: ubuntu-latest - environment: - name: dev - steps: - - name: Checkout - id: checkout - uses: actions/checkout@1f9a0c22da41e6ebfa534300ef656657ea2c6707 - - - name: Login - id: login - # from https://github.com/Azure/login/commits/master - uses: azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 - with: - client-id: ${{ secrets.CLIENT_ID }} - tenant-id: ${{ secrets.TENANT_ID }} - subscription-id: ${{ secrets.SUBSCRIPTION_ID }} - - - name: Run Service on Docker - shell: bash - id: run_service_docker - run: | - cd ./docker - chmod +x ./run_docker.sh - ./run_docker.sh local - - - name: Run Integration Tests - shell: bash - id: run_integration_test - run: | - export SUBKEY=${{ secrets.SUBKEY }} - export CANARY=${{ inputs.canary }} - export CUCUMBER_PUBLISH_TOKEN=${{ secrets.CUCUMBER_PUBLISH_TOKEN }} - - cd ./integration-test - chmod +x ./run_integration_test.sh - ./run_integration_test.sh local - - - delete_github_deployments: - runs-on: ubuntu-latest - needs: smoke-test - if: ${{ always() }} - steps: - - name: Delete Previous deployments - uses: actions/github-script@v6 - env: - SHA_HEAD: ${{ (github.event_name == 'pull_request' && github.event.pull_request.head.sha) || github.sha}} - with: - script: | - const { SHA_HEAD } = process.env - const deployments = await github.rest.repos.listDeployments({ - owner: context.repo.owner, - repo: context.repo.repo, - sha: SHA_HEAD - }); - await Promise.all( - deployments.data.map(async (deployment) => { - await github.rest.repos.createDeploymentStatus({ - owner: context.repo.owner, - repo: context.repo.repo, - deployment_id: deployment.id, - state: 'inactive' - }); - return github.rest.repos.deleteDeployment({ - owner: context.repo.owner, - repo: context.repo.repo, - deployment_id: deployment.id - }); - }) - ); +# smoke-test: +# name: Smoke Test +# runs-on: ubuntu-latest +# environment: +# name: dev +# steps: +# - name: Checkout +# id: checkout +# uses: actions/checkout@1f9a0c22da41e6ebfa534300ef656657ea2c6707 +# +# - name: Login +# id: login +# # from https://github.com/Azure/login/commits/master +# uses: azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 +# with: +# client-id: ${{ secrets.CLIENT_ID }} +# tenant-id: ${{ secrets.TENANT_ID }} +# subscription-id: ${{ secrets.SUBSCRIPTION_ID }} +# +# - name: Run Service on Docker +# shell: bash +# id: run_service_docker +# run: | +# cd ./docker +# chmod +x ./run_docker.sh +# ./run_docker.sh local +# +# - name: Run Integration Tests +# shell: bash +# id: run_integration_test +# run: | +# export SUBKEY=${{ secrets.SUBKEY }} +# export CANARY=${{ inputs.canary }} +# export CUCUMBER_PUBLISH_TOKEN=${{ secrets.CUCUMBER_PUBLISH_TOKEN }} +# +# cd ./integration-test +# chmod +x ./run_integration_test.sh +# ./run_integration_test.sh local +# +# +# delete_github_deployments: +# runs-on: ubuntu-latest +# needs: smoke-test +# if: ${{ always() }} +# steps: +# - name: Delete Previous deployments +# uses: actions/github-script@v6 +# env: +# SHA_HEAD: ${{ (github.event_name == 'pull_request' && github.event.pull_request.head.sha) || github.sha}} +# with: +# script: | +# const { SHA_HEAD } = process.env +# +# const deployments = await github.rest.repos.listDeployments({ +# owner: context.repo.owner, +# repo: context.repo.repo, +# sha: SHA_HEAD +# }); +# await Promise.all( +# deployments.data.map(async (deployment) => { +# await github.rest.repos.createDeploymentStatus({ +# owner: context.repo.owner, +# repo: context.repo.repo, +# deployment_id: deployment.id, +# state: 'inactive' +# }); +# return github.rest.repos.deleteDeployment({ +# owner: context.repo.owner, +# repo: context.repo.repo, +# deployment_id: deployment.id +# }); +# }) +# ); diff --git a/.github/workflows/deploy_with_github_runner.yml b/.github/workflows/deploy_with_github_runner.yml index 58fe083..d425edc 100644 --- a/.github/workflows/deploy_with_github_runner.yml +++ b/.github/workflows/deploy_with_github_runner.yml @@ -9,7 +9,7 @@ on: type: string env: - APP_NAME: # TODO + APP_NAME: pagopa-print-payment-notice-generator permissions: diff --git a/.identity/99_variables.tf b/.identity/99_variables.tf index b57e1b5..01729c1 100644 --- a/.identity/99_variables.tf +++ b/.identity/99_variables.tf @@ -1,12 +1,12 @@ locals { github = { org = "pagopa" - repository = "TODO" #TODO + repository = "pagopa-print-payment-notice-generator" } prefix = "pagopa" - domain = "TODO" #TODO - location_short = "weu" + domain = "printit" + location_short = "itn" product = "${var.prefix}-${var.env_short}" app_name = "github-${local.github.org}-${local.github.repository}-${var.prefix}-${local.domain}-${var.env}-aks" diff --git a/.identity/env/dev/backend.tfvars b/.identity/env/dev/backend.tfvars index b5dbac8..edb59fa 100644 --- a/.identity/env/dev/backend.tfvars +++ b/.identity/env/dev/backend.tfvars @@ -1,4 +1,4 @@ resource_group_name = "io-infra-rg" storage_account_name = "pagopainfraterraformdev" container_name = "azurermstate" -key = ".tfstate" # TODO +key = "pagopa-print-payment-notice-generator.tfstate" diff --git a/.identity/env/dev/terraform.tfvars b/.identity/env/dev/terraform.tfvars index 3345e07..e8feaf4 100644 --- a/.identity/env/dev/terraform.tfvars +++ b/.identity/env/dev/terraform.tfvars @@ -6,6 +6,6 @@ tags = { CreatedBy = "Terraform" Environment = "Dev" Owner = "pagoPA" - Source = "https://github.com/pagopa/your-repository" # TODO + Source = "https://github.com/pagopa/pagopa-print-payment-notice-generator" CostCenter = "TS310 - PAGAMENTI & SERVIZI" } diff --git a/.identity/env/prod/backend.tfvars b/.identity/env/prod/backend.tfvars index d8d402c..3196711 100644 --- a/.identity/env/prod/backend.tfvars +++ b/.identity/env/prod/backend.tfvars @@ -1,4 +1,4 @@ resource_group_name = "io-infra-rg" storage_account_name = "pagopainfraterraformprod" container_name = "azurermstate" -key = ".tfstate" # TODO +key = "pagopa-print-payment-notice-generator.tfstate" diff --git a/.identity/env/prod/terraform.tfvars b/.identity/env/prod/terraform.tfvars index ee41cf5..66ec96f 100644 --- a/.identity/env/prod/terraform.tfvars +++ b/.identity/env/prod/terraform.tfvars @@ -6,6 +6,6 @@ tags = { CreatedBy = "Terraform" Environment = "Prod" Owner = "pagoPA" - Source = "https://github.com/pagopa/your-repository" # TODO + Source = "https://github.com/pagopa/pagopa-print-payment-notice-generator" CostCenter = "TS310 - PAGAMENTI & SERVIZI" } diff --git a/.identity/env/uat/backend.tfvars b/.identity/env/uat/backend.tfvars index 502eaf6..10336a6 100644 --- a/.identity/env/uat/backend.tfvars +++ b/.identity/env/uat/backend.tfvars @@ -1,4 +1,4 @@ resource_group_name = "io-infra-rg" storage_account_name = "pagopainfraterraformuat" container_name = "azurermstate" -key = ".tfstate" # TODO +key = "pagopa-print-payment-notice-generator.tfstate" diff --git a/.identity/env/uat/terraform.tfvars b/.identity/env/uat/terraform.tfvars index 86ec8fc..48a3fd3 100644 --- a/.identity/env/uat/terraform.tfvars +++ b/.identity/env/uat/terraform.tfvars @@ -6,6 +6,6 @@ tags = { CreatedBy = "Terraform" Environment = "Uat" Owner = "pagoPA" - Source = "https://github.com/pagopa/your-repository" # TODO + Source = "https://github.com/pagopa/pagopa-print-payment-notice-generator" CostCenter = "TS310 - PAGAMENTI & SERVIZI" } diff --git a/.opex/env/prod/backend.tfvars b/.opex/env/prod/backend.tfvars index ae0e6e4..4215f5a 100644 --- a/.opex/env/prod/backend.tfvars +++ b/.opex/env/prod/backend.tfvars @@ -1,4 +1,4 @@ resource_group_name = "io-infra-rg" storage_account_name = "pagopainfraterraformprod" container_name = "azurermstate" -key = "opex..terraform.tfstate" #TODO +key = "opex.pagopa-print-payment-notice-generator.terraform.tfstate" diff --git a/.opex/env/prod/config.yaml b/.opex/env/prod/config.yaml index 80a9d54..beffd59 100644 --- a/.opex/env/prod/config.yaml +++ b/.opex/env/prod/config.yaml @@ -1,6 +1,6 @@ oa3_spec: ./openapi/openapi.json # If start with http the file would be downloaded from the internet -name: opex_ # TODO -location: West Europe +name: opex_pagopa-print-payment-notice-generator +location: Italy North timespan: 5m # Default, a number or a timespan https://docs.microsoft.com/en-us/azure/data-explorer/kusto/query/scalar-data-types/timespan data_source: /subscriptions/b9fc9419-6097-45fe-9f74-ba0641c91912/resourceGroups/pagopa-p-vnet-rg/providers/Microsoft.Network/applicationGateways/pagopa-p-app-gw #data_source: /subscriptions/b9fc9419-6097-45fe-9f74-ba0641c91912/resourceGroups/pagopa-p-api-rg/providers/Microsoft.ApiManagement/service/pagopa-p-apim diff --git a/.opex/env/prod/terraform.tfvars b/.opex/env/prod/terraform.tfvars index 1445485..5458add 100644 --- a/.opex/env/prod/terraform.tfvars +++ b/.opex/env/prod/terraform.tfvars @@ -6,6 +6,6 @@ tags = { CreatedBy = "Terraform" Environment = "Prod" Owner = "pagoPA" - Source = "https://github.com/pagopa/your-repository" # TODO + Source = "https://github.com/pagopa/pagopa-print-payment-notice-generator" CostCenter = "TS310 - PAGAMENTI & SERVIZI" } diff --git a/helm/Chart.yaml b/helm/Chart.yaml index b608062..06da112 100644 --- a/helm/Chart.yaml +++ b/helm/Chart.yaml @@ -1,6 +1,6 @@ apiVersion: v2 -name: pagopa-afm-calculator -description: Microservice that handles calculation for pagoPA Advanced Fees Management +name: pagopa-print-payment-notice-generator +description: Microservice that handles services for notice print generation type: application version: 0.0.0 appVersion: 0.0.0 diff --git a/helm/values-dev.yaml b/helm/values-dev.yaml index ec62f6d..9eae604 100644 --- a/helm/values-dev.yaml +++ b/helm/values-dev.yaml @@ -1,9 +1,9 @@ microservice-chart: - namespace: "your-namespace" # TODO: set your AKS namespace + namespace: "printit" nameOverride: "" fullnameOverride: "" image: - repository: ghcr.io/pagopa/yourname # TODO + repository: ghcr.io/pagopa/pagopa-print-payment-notice-generator tag: "0.0.0" pullPolicy: Always livenessProbe: @@ -29,8 +29,8 @@ microservice-chart: - 8080 ingress: create: true - host: "your.host" # TODO: set the host - path: /your-path-here/(.*) # TODO: set your path + host: "itndev.printit.internal.dev.platform.pagopa.it" + path: /pagopa-print-payment-notice-generator/(.*) servicePort: 8080 serviceAccount: create: false @@ -62,26 +62,32 @@ microservice-chart: type: Utilization # Allowed types are 'Utilization' or 'AverageValue' value: "75" envConfig: - # TODO: set your name - WEBSITE_SITE_NAME: 'yourProjectName' # required to show cloud role name in application insights + WEBSITE_SITE_NAME: 'print-payment-notice-generator' # required to show cloud role name in application insights ENV: 'azure-dev' APP_LOGGING_LEVEL: 'DEBUG' DEFAULT_LOGGING_LEVEL: 'INFO' CORS_CONFIGURATION: '{"origins": ["*"], "methods": ["*"]}' - OTEL_SERVICE_NAME: # TODO + OTEL_SERVICE_NAME: 'print-payment-notice-generator-otl' OTEL_RESOURCE_ATTRIBUTES: "deployment.environment=dev" OTEL_EXPORTER_OTLP_ENDPOINT: "http://otel-collector.elastic-system.svc:4317" OTEL_TRACES_EXPORTER: otlp OTEL_METRICS_EXPORTER: otlp OTEL_LOGS_EXPORTER: none OTEL_TRACES_SAMPLER: "always_on" + KAFKA_BROKER: "pagopa-d-printit-evh-ns01.servicebus.windows.net:9092" envSecret: # required APPLICATIONINSIGHTS_CONNECTION_STRING: 'ai-d-connection-string' OTEL_EXPORTER_OTLP_HEADERS: elastic-apm-secret-token + MONGODB_CONNECTION_URI: 'notices_mongo_connection_string' + TEMPLATE_STORAGE_CONN_STRING: 'templates_storage_account_connection_string' + INSTITUTION_STORAGE_CONN_STRING: 'institutions_storage_account_connection_string' + NOTICE_STORAGE_CONN_STRING: 'notices_storage_account_connection_string' + KAFKA_SASL_JAAS_CONFIG: 'ehub_notice_connection_string' + PDF_ENGINE_SUBKEY: 'pdf_engine_node_subkey' keyvault: - name: "pagopa-d-name-kv" #TODO + name: "pagopa-d-itn-printit-kv" tenantId: "7788edaf-0346-4068-9d79-c868aed15b3d" nodeSelector: { } tolerations: [ ] @@ -108,7 +114,7 @@ microservice-chart: deployment: create: true image: - repository: ghcr.io/pagopa/yourname # TODO + repository: ghcr.io/pagopa/pagopa-print-payment-notice-generator tag: "0.0.0" pullPolicy: Always envConfig: { } diff --git a/helm/values-prod.yaml b/helm/values-prod.yaml index 7a751aa..7d21ba2 100644 --- a/helm/values-prod.yaml +++ b/helm/values-prod.yaml @@ -1,9 +1,9 @@ microservice-chart: - namespace: "your-namespace" # TODO: set your AKS namespace + namespace: "printit" nameOverride: "" fullnameOverride: "" image: - repository: ghcr.io/pagopa/yourname # TODO + repository: ghcr.io/pagopa/pagopa-print-payment-notice-generator tag: "0.0.0" pullPolicy: Always livenessProbe: @@ -29,8 +29,8 @@ microservice-chart: - 8080 ingress: create: true - host: "your.host" # TODO: set the host - path: /your-path-here/(.*) # TODO: set your path + host: "itnprod.printit.internal.platform.pagopa.it" + path: /pagopa-print-payment-notice-generator/(.*) servicePort: 8080 serviceAccount: create: false @@ -51,7 +51,7 @@ microservice-chart: cpu: "0.25" autoscaling: enable: true - minReplica: 3 + minReplica: 1 maxReplica: 10 pollingInterval: 10 # seconds cooldownPeriod: 50 # seconds @@ -62,26 +62,32 @@ microservice-chart: type: Utilization # Allowed types are 'Utilization' or 'AverageValue' value: "75" envConfig: - # TODO: set your name - WEBSITE_SITE_NAME: 'yourProjectName' # required to show cloud role name in application insights + WEBSITE_SITE_NAME: 'print-payment-notice-generator' # required to show cloud role name in application insights ENV: 'azure-prod' - APP_LOGGING_LEVEL: 'INFO' + APP_LOGGING_LEVEL: 'DEBUG' DEFAULT_LOGGING_LEVEL: 'INFO' CORS_CONFIGURATION: '{"origins": ["*"], "methods": ["*"]}' - OTEL_SERVICE_NAME: # TODO + OTEL_SERVICE_NAME: 'print-payment-notice-generator-otl' OTEL_RESOURCE_ATTRIBUTES: "deployment.environment=prod" OTEL_EXPORTER_OTLP_ENDPOINT: "http://otel-collector.elastic-system.svc:4317" OTEL_TRACES_EXPORTER: otlp OTEL_METRICS_EXPORTER: otlp OTEL_LOGS_EXPORTER: none OTEL_TRACES_SAMPLER: "always_on" + KAFKA_BROKER: "pagopa-รจ-printit-evh-ns01.servicebus.windows.net:9092" envSecret: # required APPLICATIONINSIGHTS_CONNECTION_STRING: 'ai-p-connection-string' OTEL_EXPORTER_OTLP_HEADERS: elastic-apm-secret-token + MONGODB_CONNECTION_URI: 'notices_mongo_connection_string' + TEMPLATE_STORAGE_CONN_STRING: 'templates_storage_account_connection_string' + INSTITUTION_STORAGE_CONN_STRING: 'institutions_storage_account_connection_string' + NOTICE_STORAGE_CONN_STRING: 'notices_storage_account_connection_string' + KAFKA_SASL_JAAS_CONFIG: 'ehub_notice_connection_string' + PDF_ENGINE_SUBKEY: 'pdf_engine_node_subkey' keyvault: - name: "pagopa-p-name-kv" #TODO + name: "pagopa-p-itn-printit-kv" tenantId: "7788edaf-0346-4068-9d79-c868aed15b3d" nodeSelector: { } tolerations: [ ] @@ -108,7 +114,7 @@ microservice-chart: deployment: create: true image: - repository: ghcr.io/pagopa/yourname # TODO + repository: ghcr.io/pagopa/pagopa-print-payment-notice-generator tag: "0.0.0" pullPolicy: Always envConfig: { } diff --git a/helm/values-uat.yaml b/helm/values-uat.yaml index 80e74af..7605dbe 100644 --- a/helm/values-uat.yaml +++ b/helm/values-uat.yaml @@ -1,9 +1,9 @@ microservice-chart: - namespace: "your-namespace" # TODO: set your AKS namespace + namespace: "printit" nameOverride: "" fullnameOverride: "" image: - repository: ghcr.io/pagopa/yourname # TODO + repository: ghcr.io/pagopa/pagopa-print-payment-notice-generator tag: "0.0.0" pullPolicy: Always livenessProbe: @@ -29,8 +29,8 @@ microservice-chart: - 8080 ingress: create: true - host: "your.host" # TODO: set the host - path: /your-path-here/(.*) # TODO: set your path + host: "itnuat.printit.internal.uat.platform.pagopa.it" + path: /pagopa-print-payment-notice-generator/(.*) servicePort: 8080 serviceAccount: create: false @@ -51,7 +51,7 @@ microservice-chart: cpu: "0.25" autoscaling: enable: true - minReplica: 3 + minReplica: 1 maxReplica: 10 pollingInterval: 10 # seconds cooldownPeriod: 50 # seconds @@ -62,25 +62,32 @@ microservice-chart: type: Utilization # Allowed types are 'Utilization' or 'AverageValue' value: "75" envConfig: - # TODO: set your name - WEBSITE_SITE_NAME: 'yourProjectName' # required to show cloud role name in application insights + WEBSITE_SITE_NAME: 'print-payment-notice-generator' # required to show cloud role name in application insights ENV: 'azure-uat' APP_LOGGING_LEVEL: 'DEBUG' DEFAULT_LOGGING_LEVEL: 'INFO' CORS_CONFIGURATION: '{"origins": ["*"], "methods": ["*"]}' - OTEL_SERVICE_NAME: # TODO + + OTEL_SERVICE_NAME: 'print-payment-notice-generator-otl' OTEL_RESOURCE_ATTRIBUTES: "deployment.environment=uat" OTEL_EXPORTER_OTLP_ENDPOINT: "http://otel-collector.elastic-system.svc:4317" OTEL_TRACES_EXPORTER: otlp OTEL_METRICS_EXPORTER: otlp OTEL_LOGS_EXPORTER: none OTEL_TRACES_SAMPLER: "always_on" + KAFKA_BROKER: "pagopa-u-printit-evh-ns01.servicebus.windows.net:9092" envSecret: # required APPLICATIONINSIGHTS_CONNECTION_STRING: 'ai-u-connection-string' OTEL_EXPORTER_OTLP_HEADERS: elastic-apm-secret-token + MONGODB_CONNECTION_URI: 'notices_mongo_connection_string' + TEMPLATE_STORAGE_CONN_STRING: 'templates_storage_account_connection_string' + INSTITUTION_STORAGE_CONN_STRING: 'institutions_storage_account_connection_string' + NOTICE_STORAGE_CONN_STRING: 'notices_storage_account_connection_string' + KAFKA_SASL_JAAS_CONFIG: 'ehub_notice_connection_string' + PDF_ENGINE_SUBKEY: 'pdf_engine_node_subkey' keyvault: - name: "pagopa-u-name-kv" #TODO + name: "pagopa-u-itn-printit-kv" tenantId: "7788edaf-0346-4068-9d79-c868aed15b3d" nodeSelector: { } tolerations: [ ] @@ -107,7 +114,7 @@ microservice-chart: deployment: create: true image: - repository: ghcr.io/pagopa/yourname # TODO + repository: ghcr.io/pagopa/pagopa-print-payment-notice-generator tag: "0.0.0" pullPolicy: Always envConfig: { } diff --git a/infra/env/weu-dev/backend.tfvars b/infra/env/weu-dev/backend.tfvars index 619395b..edb59fa 100644 --- a/infra/env/weu-dev/backend.tfvars +++ b/infra/env/weu-dev/backend.tfvars @@ -1,4 +1,4 @@ resource_group_name = "io-infra-rg" storage_account_name = "pagopainfraterraformdev" container_name = "azurermstate" -key = ".infra.tfstate" # TODO +key = "pagopa-print-payment-notice-generator.tfstate" diff --git a/infra/env/weu-prod/backend.tfvars b/infra/env/weu-prod/backend.tfvars index dac1727..3196711 100644 --- a/infra/env/weu-prod/backend.tfvars +++ b/infra/env/weu-prod/backend.tfvars @@ -1,4 +1,4 @@ resource_group_name = "io-infra-rg" storage_account_name = "pagopainfraterraformprod" container_name = "azurermstate" -key = ".infra.tfstate" # TODO +key = "pagopa-print-payment-notice-generator.tfstate" diff --git a/infra/env/weu-uat/backend.tfvars b/infra/env/weu-uat/backend.tfvars index 6f406b1..10336a6 100644 --- a/infra/env/weu-uat/backend.tfvars +++ b/infra/env/weu-uat/backend.tfvars @@ -1,4 +1,4 @@ resource_group_name = "io-infra-rg" storage_account_name = "pagopainfraterraformuat" container_name = "azurermstate" -key = ".infra.tfstate" # TODO +key = "pagopa-print-payment-notice-generator.tfstate" diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/storage/NoticeStorageClient.java b/src/main/java/it/gov/pagopa/payment/notice/generator/storage/NoticeStorageClient.java index 8aa35bc..bfd8736 100644 --- a/src/main/java/it/gov/pagopa/payment/notice/generator/storage/NoticeStorageClient.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/storage/NoticeStorageClient.java @@ -33,23 +33,15 @@ public class NoticeStorageClient { private BlobContainerClient blobContainerClient; - private Integer maxRetry; - - private Integer timeout; - @Autowired public NoticeStorageClient( @Value("${spring.cloud.azure.storage.blob.notices.enabled}") String enabled, @Value("${spring.cloud.azure.storage.blob.notices.connection_string}") String connectionString, - @Value("${spring.cloud.azure.storage.blob.notices.containerName}") String containerName, - @Value("${spring.cloud.azure.storage.blob.notices.retry}") Integer maxRetry, - @Value("${spring.cloud.azure.storage.blob.notices.timeout}") Integer timeout) { + @Value("${spring.cloud.azure.storage.blob.notices.containerName}") String containerName) { if (Boolean.TRUE.toString().equals(enabled)) { BlobServiceClient blobServiceClient = new BlobServiceClientBuilder() .connectionString(connectionString).buildClient(); blobContainerClient = blobServiceClient.getBlobContainerClient(containerName); - this.maxRetry = maxRetry; - this.timeout = timeout; } } public NoticeStorageClient( @@ -57,8 +49,6 @@ public NoticeStorageClient( BlobContainerClient blobContainerClient) { if (Boolean.TRUE.equals(enabled)) { this.blobContainerClient = blobContainerClient; - this.maxRetry=3; - this.timeout=10; } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 0a0a2c9..3632423 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -36,13 +36,23 @@ spring.jackson.serialization.write-dates-as-timestamps=false spring.jackson.serialization.fail-on-empty-beans=false spring.jackson.deserialization.fail-on-unknown-properties=false +pdf.engine.endpoint=${PDF_ENGINE_ENDPOINT:} +pdf.engine.ocpaim.subkey=${PDF_ENGINE_SUBKEY:} + spring.cloud.azure.storage.blob.templates.enabled=${TEMPLATE_STORAGE_ENABLED:true} spring.cloud.azure.storage.blob.templates.connection_string=${TEMPLATE_STORAGE_CONN_STRING:} spring.cloud.azure.storage.blob.templates.containerName=${TEMPLATE_STORAGE_CONTAINER_NAME:noticetemplateblob} -spring.cloud.azure.storage.blob.templates.tableName=${TEMPLATE_STORAGE_CONTAINER_NAME:noticetemplatedatatable} spring.cloud.azure.storage.blob.templates.retry=${TEMPLATE_STORAGE_RETRY:3} spring.cloud.azure.storage.blob.templates.timeout=${TEMPLATE_STORAGE_TIMEOUT:10} +spring.cloud.azure.storage.blob.notices.enabled=${NOTICE_STORAGE_ENABLED:true} +spring.cloud.azure.storage.blob.notices.connection_string=${NOTICE_STORAGE_CONN_STRING:} +spring.cloud.azure.storage.blob.notices.containerName=${NOTICE_STORAGE_CONTAINER_NAME:notices} + +spring.cloud.azure.storage.blob.institutions.enabled=${INSTITUTION_STORAGE_ENABLED:true} +spring.cloud.azure.storage.blob.institutions.connection_string=${INSTITUTION_STORAGE_CONN_STRING:} +spring.cloud.azure.storage.blob.institutions.containerName=${INSTITUTION_STORAGE_CONTAINER_NAME:institutionsdatablob} + # EH Kafka Configuration spring.cloud.function.definition=noticeGeneration spring.cloud.stream.bindings.noticeGeneration-in-0.destination=${KAFKA_NOTICE_GENERATION_TOPIC:pagopa-notice-evt-rx} From bbaa40bb160eeecd73ee24d544a27872742761b9 Mon Sep 17 00:00:00 2001 From: Alessio Cialini Date: Tue, 30 Apr 2024 15:01:49 +0200 Subject: [PATCH 05/19] [VAS-834][VAS-973] feat: Updated terraform configs and updated unit tests --- infra/04_apim_api.tf | 8 +- infra/99_locals.tf | 2 +- infra/policy/_base_policy.xml | 3 +- openapi/openapi.json | 232 ++++++++++++++- .../generator/client/PdfEngineClientImpl.java | 2 + .../config/LocalValidatorConfig.java | 15 + .../NoticeGenerationController.java | 6 +- .../notice/generator/exception/AppError.java | 7 + .../generator/mapper/TemplateDataMapper.java | 4 +- .../generator/model/NoticeRequestEH.java | 8 + .../model/notice/InstallmentData.java | 6 + .../service/NoticeGenerationServiceImpl.java | 32 +- .../storage/NoticeStorageClient.java | 14 - .../storage/NoticeTemplateStorageClient.java | 6 +- .../notice/generator/util/Aes256Utils.java | 25 -- .../client/PdfEngineClientImplTest.java | 176 +++++++++++ .../NoticeGenerationControllerTest.java | 128 ++++++++ .../NoticeGenerationServiceImplTest.java | 277 ++++++++++++++++++ .../InstitutionsStorageClientTest.java | 71 +++++ .../storage/NoticeStorageClientTest.java | 71 +++++ .../NoticeTemplateStorageClientTest.java | 78 +++++ src/test/resources/application.properties | 95 +++++- 22 files changed, 1196 insertions(+), 70 deletions(-) create mode 100644 src/main/java/it/gov/pagopa/payment/notice/generator/config/LocalValidatorConfig.java create mode 100644 src/test/java/it/gov/pagopa/payment/notice/generator/client/PdfEngineClientImplTest.java create mode 100644 src/test/java/it/gov/pagopa/payment/notice/generator/controller/NoticeGenerationControllerTest.java create mode 100644 src/test/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationServiceImplTest.java create mode 100644 src/test/java/it/gov/pagopa/payment/notice/generator/storage/InstitutionsStorageClientTest.java create mode 100644 src/test/java/it/gov/pagopa/payment/notice/generator/storage/NoticeStorageClientTest.java create mode 100644 src/test/java/it/gov/pagopa/payment/notice/generator/storage/NoticeTemplateStorageClientTest.java diff --git a/infra/04_apim_api.tf b/infra/04_apim_api.tf index b24ae4d..05509ca 100644 --- a/infra/04_apim_api.tf +++ b/infra/04_apim_api.tf @@ -1,9 +1,9 @@ locals { - repo_name = "TODO" # TODO add the name of the repository + repo_name = "pagopa-print-payment-notice-generator" - display_name = "TODO" # TODO - description = "TODO" # TODO - path = "TODO" # TODO add your base path + display_name = "Payment Notices Print Generator APIs" + description = "Payment Notices Print Generator APIs" + path = "payment/notice/generator" host = "api.${var.apim_dns_zone_prefix}.${var.external_domain}" hostname = var.hostname diff --git a/infra/99_locals.tf b/infra/99_locals.tf index 5ed42d0..1c55b26 100644 --- a/infra/99_locals.tf +++ b/infra/99_locals.tf @@ -4,7 +4,7 @@ locals { apim = { name = "${local.product}-apim" rg = "${local.product}-api-rg" - product_id = "TODO" # TODO product id to import from pagopa-infra + product_id = "pagopa_notices_generator" } } diff --git a/infra/policy/_base_policy.xml b/infra/policy/_base_policy.xml index e3b583c..21ffd26 100644 --- a/infra/policy/_base_policy.xml +++ b/infra/policy/_base_policy.xml @@ -1,7 +1,8 @@ - + + diff --git a/openapi/openapi.json b/openapi/openapi.json index 0967ef4..b432270 100644 --- a/openapi/openapi.json +++ b/openapi/openapi.json @@ -1 +1,231 @@ -{} +{ + "openapi" : "3.0.1", + "info" : { + "description" : "PagoPA Print Payment Notices Generator", + "termsOfService" : "https://www.pagopa.gov.it/", + "title" : "pagopa-print-payment-notice-generator", + "version" : "0.0.0" + }, + "servers" : [ { + "url" : "http://localhost", + "description" : "Generated server url" + } ], + "paths" : { + "/notices/generate" : { + "parameters" : [ { + "description" : "This header identifies the call, if not passed it is self-generated. This ID is returned in the response.", + "in" : "header", + "name" : "X-Request-Id", + "schema" : { + "type" : "string" + } + } ], + "post" : { + "operationId" : "generateNotice", + "parameters" : [ { + "in" : "query", + "name" : "folderId", + "required" : true, + "schema" : { + "type" : "string" + } + } ], + "requestBody" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/NoticeGenerationRequestItem" + } + } + }, + "required" : true + }, + "responses" : { + "200" : { + "content" : { + "application/json" : { + "schema" : { + "type" : "string", + "format" : "binary" + } + } + }, + "description" : "OK", + "headers" : { + "X-Request-Id" : { + "description" : "This header identifies the call", + "schema" : { + "type" : "string" + } + } + } + } + }, + "tags" : [ "Notice Generation APIs" ] + } + } + }, + "components" : { + "schemas" : { + "CreditorInstitution" : { + "required" : [ "taxCode", "webChannel" ], + "type" : "object", + "properties" : { + "cbill" : { + "type" : "string", + "description" : "CI cbill" + }, + "fullName" : { + "type" : "string", + "description" : "CI full name" + }, + "info" : { + "type" : "string", + "description" : "CI info" + }, + "logo" : { + "type" : "string" + }, + "organization" : { + "type" : "string", + "description" : "CI Organization" + }, + "physicalChannel" : { + "type" : "string", + "description" : "CI physical channel data" + }, + "taxCode" : { + "type" : "string", + "description" : "CI tax code" + }, + "webChannel" : { + "type" : "boolean", + "description" : "Boolean to refer if it has a web channel" + } + }, + "description" : "Creditor Institution data" + }, + "Debtor" : { + "required" : [ "address", "buildingNumber", "city", "fullName", "postalCode", "province" ], + "type" : "object", + "properties" : { + "address" : { + "type" : "string", + "description" : "Debtor address" + }, + "buildingNumber" : { + "type" : "string", + "description" : "Debtor building number" + }, + "city" : { + "type" : "string", + "description" : "Debtor city" + }, + "fullName" : { + "type" : "string", + "description" : "Debtor full name" + }, + "postalCode" : { + "type" : "string", + "description" : "Debtor postal code" + }, + "province" : { + "type" : "string", + "description" : "Debtor province" + }, + "taxCode" : { + "type" : "string", + "description" : "Debtor taxCode" + } + }, + "description" : "Debtor data" + }, + "InstallmentData" : { + "required" : [ "amount", "code", "dueDate" ], + "type" : "object", + "properties" : { + "amount" : { + "type" : "integer", + "description" : "Installment amount", + "format" : "int64" + }, + "code" : { + "type" : "string", + "description" : "Installment code" + }, + "dueDate" : { + "type" : "string", + "description" : "Installment dueDate" + } + }, + "description" : "Notice installments (if present)" + }, + "Notice" : { + "required" : [ "code", "dueDate", "paymentAmount", "subject" ], + "type" : "object", + "properties" : { + "code" : { + "type" : "string", + "description" : "Notice code" + }, + "dueDate" : { + "type" : "string", + "description" : "Notice due date" + }, + "installments" : { + "type" : "array", + "description" : "Notice installments (if present)", + "items" : { + "$ref" : "#/components/schemas/InstallmentData" + } + }, + "paymentAmount" : { + "type" : "integer", + "description" : "Notice total amount to pay", + "format" : "int64" + }, + "subject" : { + "type" : "string", + "description" : "Notice subject" + } + }, + "description" : "Notice data" + }, + "NoticeGenerationRequestItem" : { + "type" : "object", + "properties" : { + "data" : { + "$ref" : "#/components/schemas/NoticeRequestData" + }, + "templateId" : { + "type" : "string" + } + }, + "description" : "templateId to use for retrieval" + }, + "NoticeRequestData" : { + "required" : [ "creditorInstitution", "debtor", "notice" ], + "type" : "object", + "properties" : { + "creditorInstitution" : { + "$ref" : "#/components/schemas/CreditorInstitution" + }, + "debtor" : { + "$ref" : "#/components/schemas/Debtor" + }, + "notice" : { + "$ref" : "#/components/schemas/Notice" + } + } + } + }, + "securitySchemes" : { + "ApiKey" : { + "description" : "The API key to access this function app.", + "in" : "header", + "name" : "Ocp-Apim-Subscription-Key", + "type" : "apiKey" + } + } + } +} \ No newline at end of file diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/client/PdfEngineClientImpl.java b/src/main/java/it/gov/pagopa/payment/notice/generator/client/PdfEngineClientImpl.java index b57f02f..d323043 100644 --- a/src/main/java/it/gov/pagopa/payment/notice/generator/client/PdfEngineClientImpl.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/client/PdfEngineClientImpl.java @@ -18,6 +18,7 @@ import org.apache.http.util.EntityUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; import java.io.File; import java.io.IOException; @@ -25,6 +26,7 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Path; +@Component /** * Client for the PDF Engine */ diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/config/LocalValidatorConfig.java b/src/main/java/it/gov/pagopa/payment/notice/generator/config/LocalValidatorConfig.java new file mode 100644 index 0000000..17e469f --- /dev/null +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/config/LocalValidatorConfig.java @@ -0,0 +1,15 @@ +package it.gov.pagopa.payment.notice.generator.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; + +@Configuration +public class LocalValidatorConfig { + + @Bean + public LocalValidatorFactoryBean validator() { + return new LocalValidatorFactoryBean(); + } + +} diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/controller/NoticeGenerationController.java b/src/main/java/it/gov/pagopa/payment/notice/generator/controller/NoticeGenerationController.java index d85e648..37d4885 100644 --- a/src/main/java/it/gov/pagopa/payment/notice/generator/controller/NoticeGenerationController.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/controller/NoticeGenerationController.java @@ -2,6 +2,8 @@ import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; +import it.gov.pagopa.payment.notice.generator.exception.AppError; +import it.gov.pagopa.payment.notice.generator.exception.AppException; import it.gov.pagopa.payment.notice.generator.model.NoticeGenerationRequestItem; import it.gov.pagopa.payment.notice.generator.service.NoticeGenerationService; import jakarta.validation.Valid; @@ -47,8 +49,8 @@ public ResponseEntity generateNotice( return ResponseEntity.ok().contentType(MediaType.APPLICATION_OCTET_STREAM) .headers(headers) .body(new ByteArrayResource(inputStream.readAllBytes())); - } catch (IOException e) { - throw new RuntimeException(e); + } catch (Exception e) { + throw new AppException(AppError.INTERNAL_SERVER_ERROR, e); } finally { clearTempDirectory(file.toPath().getParent()); } diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/exception/AppError.java b/src/main/java/it/gov/pagopa/payment/notice/generator/exception/AppError.java index f6fc734..5e80898 100644 --- a/src/main/java/it/gov/pagopa/payment/notice/generator/exception/AppError.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/exception/AppError.java @@ -27,6 +27,13 @@ public enum AppError { INSTITUTION_PARSING_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "Parsing Error for Institution Data", "Exception thrown while parsing institution data retrieve from storage"), + + MESSAGE_VALIDATION_ERROR(HttpStatus.BAD_REQUEST, "Message Validation Error" , "EH Message content is not valid"), + + PDF_ENGINE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "PDF Engine Error", "Encountered an error calling the PDF Engine"), + + NOTICE_SAVE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "Notice save error", "Exception while saving notice"), + UNKNOWN(null, null, null); public final HttpStatus httpStatus; diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/mapper/TemplateDataMapper.java b/src/main/java/it/gov/pagopa/payment/notice/generator/mapper/TemplateDataMapper.java index 7b5fd9e..c0a84a0 100644 --- a/src/main/java/it/gov/pagopa/payment/notice/generator/mapper/TemplateDataMapper.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/mapper/TemplateDataMapper.java @@ -69,7 +69,7 @@ public static PaymentNotice mapTemplate(NoticeRequestData noticeRequestData) { .build(); } - public static String generateQrCode(String code, String taxCode, String amount) { + private static String generateQrCode(String code, String taxCode, String amount) { return String.join("|", "PAGOPA","002", StringUtils.leftPad(code, 18, "0"), @@ -79,7 +79,7 @@ public static String generateQrCode(String code, String taxCode, String amount) ); } - public static Installment mapInstallment( + private static Installment mapInstallment( String cbill, String taxCode, InstallmentData installmentData) { String amount = String.valueOf(installmentData.getAmount()); return Installment.builder() diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/model/NoticeRequestEH.java b/src/main/java/it/gov/pagopa/payment/notice/generator/model/NoticeRequestEH.java index 4518d3e..63f30fb 100644 --- a/src/main/java/it/gov/pagopa/payment/notice/generator/model/NoticeRequestEH.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/model/NoticeRequestEH.java @@ -1,10 +1,18 @@ package it.gov.pagopa.payment.notice.generator.model; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; @Data +@AllArgsConstructor +@NoArgsConstructor +@Builder public class NoticeRequestEH { + @NotNull private String folderId; private NoticeGenerationRequestItem noticeGenerationRequestItem; diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/InstallmentData.java b/src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/InstallmentData.java index dba810a..b8db9e5 100644 --- a/src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/InstallmentData.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/InstallmentData.java @@ -1,9 +1,15 @@ package it.gov.pagopa.payment.notice.generator.model.notice; import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; @Data +@AllArgsConstructor +@NoArgsConstructor +@Builder public class InstallmentData { @Schema(description = "Installment code", requiredMode = Schema.RequiredMode.REQUIRED) diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationServiceImpl.java b/src/main/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationServiceImpl.java index be65085..51e7ccb 100644 --- a/src/main/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationServiceImpl.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationServiceImpl.java @@ -1,11 +1,9 @@ package it.gov.pagopa.payment.notice.generator.service; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import it.gov.pagopa.payment.notice.generator.client.PdfEngineClient; import it.gov.pagopa.payment.notice.generator.entity.PaymentNoticeGenerationRequest; import it.gov.pagopa.payment.notice.generator.entity.PaymentNoticeGenerationRequestError; -import it.gov.pagopa.payment.notice.generator.exception.Aes256Exception; import it.gov.pagopa.payment.notice.generator.exception.AppError; import it.gov.pagopa.payment.notice.generator.exception.AppException; import it.gov.pagopa.payment.notice.generator.mapper.TemplateDataMapper; @@ -20,6 +18,7 @@ import it.gov.pagopa.payment.notice.generator.storage.NoticeStorageClient; import it.gov.pagopa.payment.notice.generator.storage.NoticeTemplateStorageClient; import it.gov.pagopa.payment.notice.generator.util.Aes256Utils; +import jakarta.validation.Validator; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.apache.http.HttpStatus; @@ -51,31 +50,31 @@ public class NoticeGenerationServiceImpl implements NoticeGenerationService { private final NoticeStorageClient noticeStorageClient; private final NoticeTemplateStorageClient noticeTemplateStorageClient; - private final TemplateDataMapper templateDataMapper; - private final Aes256Utils aes256Utils; private final ObjectMapper objectMapper; + private final Validator validator; + public NoticeGenerationServiceImpl( PaymentGenerationRequestRepository paymentGenerationRequestRepository, PaymentGenerationRequestErrorRepository paymentGenerationRequestErrorRepository, InstitutionsStorageClient institutionsStorageClient, NoticeStorageClient noticeStorageClient, NoticeTemplateStorageClient noticeTemplateStorageClient, - TemplateDataMapper templateDataMapper, PdfEngineClient pdfEngineClient, Aes256Utils aes256Utils, - ObjectMapper objectMapper) { + ObjectMapper objectMapper, + Validator validator) { this.paymentGenerationRequestRepository = paymentGenerationRequestRepository; this.paymentGenerationRequestErrorRepository = paymentGenerationRequestErrorRepository; this.institutionsStorageClient = institutionsStorageClient; this.noticeStorageClient = noticeStorageClient; this.noticeTemplateStorageClient = noticeTemplateStorageClient; - this.templateDataMapper = templateDataMapper; this.pdfEngineClient = pdfEngineClient; this.aes256Utils = aes256Utils; this.objectMapper = objectMapper; + this.validator = validator; } @SneakyThrows @@ -110,7 +109,8 @@ public File generateNotice(NoticeGenerationRequestItem noticeGenerationRequestIt //Build the request request.setTemplate(templateFile.toURI().toURL()); - request.setData(objectMapper.writeValueAsString(noticeGenerationRequestItem.getData())); + request.setData(objectMapper.writeValueAsString( + TemplateDataMapper.mapTemplate(noticeGenerationRequestItem.getData()))); request.setApplySignature(false); PdfEngineResponse pdfEngineResponse = pdfEngineClient.generatePDF(request, tempDirectory); @@ -119,7 +119,7 @@ public File generateNotice(NoticeGenerationRequestItem noticeGenerationRequestIt String errMsg = String.format("PDF-Engine response KO (%s): %s", pdfEngineResponse.getStatusCode(), pdfEngineResponse.getErrorMessage()); log.error(errMsg); - throw new AppException(AppError.UNKNOWN); + throw new AppException(AppError.PDF_ENGINE_ERROR, errMsg); } if (folderId != null) { @@ -129,12 +129,14 @@ public File generateNotice(NoticeGenerationRequestItem noticeGenerationRequestIt String dateFormatted = LocalDate.now().format(DateTimeFormatter.ofPattern("yyMMdd")); String blobName = String.format("%s-%s-%s", "pagopa-avviso", dateFormatted, noticeGenerationRequestItem.getData().getNotice().getCode()); - noticeStorageClient.savePdfToBlobStorage(pdfStream, blobName); + if (!noticeStorageClient.savePdfToBlobStorage(pdfStream, blobName)) { + throw new RuntimeException("Encountered error during blob saving"); + } paymentGenerationRequestRepository.findAndAddItemById(folderId, blobName); } catch (Exception e) { log.error(e.getMessage(), e); - throw new AppException(AppError.UNKNOWN); + throw new AppException(AppError.NOTICE_SAVE_ERROR, e); } } @@ -163,16 +165,22 @@ public void processNoticeGenerationEH(String message) { try { NoticeRequestEH noticeRequestEH = objectMapper.readValue(message, NoticeRequestEH.class); + + if (!validator.validate(noticeRequestEH).isEmpty()) { + throw new AppException(AppError.MESSAGE_VALIDATION_ERROR); + } + folderId = noticeRequestEH.getFolderId(); noticeGenerationRequestItem = noticeRequestEH.getNoticeGenerationRequestItem(); } catch (Exception e) { + log.error(e.getMessage(), e); try { paymentGenerationRequestErrorRepository.save( PaymentNoticeGenerationRequestError.builder() .errorDescription("Unable to read EH message content") .folderId("UNKNOWN") - .data(aes256Utils.encrypt(message)) + .data(message != null ? aes256Utils.encrypt(message) : "EMPTY") .createdAt(Instant.now()) .numberOfAttempts(0) .build()); diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/storage/NoticeStorageClient.java b/src/main/java/it/gov/pagopa/payment/notice/generator/storage/NoticeStorageClient.java index bfd8736..4bb9a25 100644 --- a/src/main/java/it/gov/pagopa/payment/notice/generator/storage/NoticeStorageClient.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/storage/NoticeStorageClient.java @@ -1,32 +1,18 @@ package it.gov.pagopa.payment.notice.generator.storage; import com.azure.core.http.rest.Response; -import com.azure.core.util.Context; import com.azure.storage.blob.BlobClient; import com.azure.storage.blob.BlobContainerClient; import com.azure.storage.blob.BlobServiceClient; import com.azure.storage.blob.BlobServiceClientBuilder; -import com.azure.storage.blob.models.BlobStorageException; import com.azure.storage.blob.models.BlockBlobItem; -import com.azure.storage.blob.models.DownloadRetryOptions; -import com.azure.storage.blob.options.BlobDownloadToFileOptions; import com.azure.storage.blob.options.BlobParallelUploadOptions; -import it.gov.pagopa.payment.notice.generator.exception.AppError; -import it.gov.pagopa.payment.notice.generator.exception.AppException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; -import java.io.File; -import java.io.IOException; import java.io.InputStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardOpenOption; -import java.time.Duration; -import java.util.Arrays; -import java.util.HashSet; @Component public class NoticeStorageClient { diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/storage/NoticeTemplateStorageClient.java b/src/main/java/it/gov/pagopa/payment/notice/generator/storage/NoticeTemplateStorageClient.java index 7deae31..9b652af 100644 --- a/src/main/java/it/gov/pagopa/payment/notice/generator/storage/NoticeTemplateStorageClient.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/storage/NoticeTemplateStorageClient.java @@ -102,14 +102,14 @@ private BlobDownloadToFileOptions getBlobDownloadToFileOptions(String filePath) private String createTemplatesDirectory(String templateId) { try { - File workingDirectory = createWorkingDirectory(); - Path filePath = workingDirectory.toPath().resolve( + Path workingDirectory = createWorkingDirectory().toPath().normalize().toAbsolutePath(); + Path filePath = workingDirectory.resolve( "templates/"+templateId + ".zip").normalize().toAbsolutePath(); if (!filePath.startsWith(workingDirectory + File.separator)) { throw new IllegalArgumentException("Invalid filename"); } return filePath.toFile().getAbsolutePath(); - } catch (IOException e) { + } catch (Exception e) { throw new AppException(AppError.TEMPLATE_CLIENT_ERROR, e); } } diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/util/Aes256Utils.java b/src/main/java/it/gov/pagopa/payment/notice/generator/util/Aes256Utils.java index 69bc111..41f10f3 100644 --- a/src/main/java/it/gov/pagopa/payment/notice/generator/util/Aes256Utils.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/util/Aes256Utils.java @@ -69,30 +69,5 @@ public String encrypt(String strToEncrypt) throws Aes256Exception { } } - public String decrypt(String strToDecrypt) throws Aes256Exception { - try{ - byte[] encryptedData = Base64.getDecoder().decode(strToDecrypt); - byte[] iv = new byte[16]; - System.arraycopy(encryptedData, 0, iv, 0, iv.length); - IvParameterSpec ivspec = new IvParameterSpec(iv); - - SecretKeyFactory factory = SecretKeyFactory.getInstance(PBKDF_2_WITH_HMAC_SHA_256); - KeySpec spec = new PBEKeySpec(aesSecretKey.toCharArray(), aesSalt.getBytes(), ITERATION_COUNT, KEY_LENGTH); - SecretKey tmp = factory.generateSecret(spec); - SecretKeySpec secretKeySpec = new SecretKeySpec(tmp.getEncoded(), ALGORITHM); - - //Padding vulnerability rule java:S5542 ignored because decryption is used inside application workflow - Cipher cipher = Cipher.getInstance(AES_CBC_PKCS_5_PADDING); - cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivspec); - - byte[] cipherText = new byte[encryptedData.length - 16]; - System.arraycopy(encryptedData, 16, cipherText, 0, cipherText.length); - - byte[] decryptedText = cipher.doFinal(cipherText); - return new String(decryptedText, StandardCharsets.UTF_8); - } catch (Exception e) { - throw new Aes256Exception("Unexpected error when decrypting the given string", AES_UNEXPECTED_ERROR, e); - } - } } diff --git a/src/test/java/it/gov/pagopa/payment/notice/generator/client/PdfEngineClientImplTest.java b/src/test/java/it/gov/pagopa/payment/notice/generator/client/PdfEngineClientImplTest.java new file mode 100644 index 0000000..fa4ca60 --- /dev/null +++ b/src/test/java/it/gov/pagopa/payment/notice/generator/client/PdfEngineClientImplTest.java @@ -0,0 +1,176 @@ +package it.gov.pagopa.payment.notice.generator.client; + +import com.fasterxml.jackson.databind.ObjectMapper; +import it.gov.pagopa.payment.notice.generator.model.pdf.PdfEngineRequest; +import it.gov.pagopa.payment.notice.generator.model.pdf.PdfEngineResponse; +import org.apache.http.HttpEntity; +import org.apache.http.HttpStatus; +import org.apache.http.StatusLine; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + + +import java.io.*; +import java.nio.file.Files; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class PdfEngineClientImplTest { + + @Test + void runOk() throws Exception { + + File tempDirectory = new File("temp"); + if (!tempDirectory.exists()) { + Files.createDirectory(tempDirectory.toPath()); + } + + File targetFile = File.createTempFile("tempFile", ".txt", tempDirectory); + + + byte[] template; + PdfEngineRequest pdfEngineRequest = new PdfEngineRequest(); + try (InputStream inputStream = FileInputStream.nullInputStream()) { + template = inputStream.readAllBytes(); + + pdfEngineRequest.setTemplate(targetFile.toURI().toURL()); + pdfEngineRequest.setData(new String(template)); + } finally { + targetFile.deleteOnExit(); + tempDirectory.deleteOnExit(); + } + + HttpClientBuilder mockBuilder = mock(HttpClientBuilder.class); + CloseableHttpClient mockClient = mock(CloseableHttpClient.class); + + CloseableHttpResponse mockResponse = mock(CloseableHttpResponse.class); + StatusLine mockStatusLine = mock(StatusLine.class); + when(mockStatusLine.getStatusCode()).thenReturn(HttpStatus.SC_OK); + when(mockResponse.getStatusLine()).thenReturn(mockStatusLine); + + HttpEntity mockEntity = mock(HttpEntity.class); + when(mockEntity.getContent()).thenReturn(InputStream.nullInputStream()); + when(mockResponse.getEntity()).thenReturn(mockEntity); + + when(mockClient.execute(any())).thenReturn(mockResponse); + when(mockBuilder.build()).thenReturn(mockClient); + + PdfEngineClientImpl client = new PdfEngineClientImpl( + new ObjectMapper(), mockBuilder, "test", "test"); + PdfEngineResponse pdfEngineResponse = client.generatePDF(pdfEngineRequest, tempDirectory.toPath()); + + File tempPdf = new File(pdfEngineResponse.getTempPdfPath()); + Assertions.assertTrue(tempPdf.delete()); + Assertions.assertEquals(HttpStatus.SC_OK, pdfEngineResponse.getStatusCode()); + } + + @Test + void runKoUnauthorized() throws IOException { + + File tempDirectory = new File("temp"); + if (!tempDirectory.exists()) { + Files.createDirectory(tempDirectory.toPath()); + } + + File targetFile = File.createTempFile("tempFile", ".txt", tempDirectory); + + byte[] template; + PdfEngineRequest pdfEngineRequest = new PdfEngineRequest(); + try (InputStream inputStream = FileInputStream.nullInputStream()) { + template = inputStream.readAllBytes(); + + pdfEngineRequest.setTemplate(targetFile.toURI().toURL()); + pdfEngineRequest.setData(new String(template)); + } finally { + targetFile.deleteOnExit(); + tempDirectory.deleteOnExit(); + } + + HttpClientBuilder mockBuilder = mock(HttpClientBuilder.class); + CloseableHttpClient mockClient = mock(CloseableHttpClient.class); + + CloseableHttpResponse mockResponse = mock(CloseableHttpResponse.class); + StatusLine mockStatusLine = mock(StatusLine.class); + when(mockStatusLine.getStatusCode()).thenReturn(HttpStatus.SC_UNAUTHORIZED); + when(mockResponse.getStatusLine()).thenReturn(mockStatusLine); + + HttpEntity mockEntity = mock(HttpEntity.class); + lenient().when(mockEntity.getContent()).thenReturn(InputStream.nullInputStream()); + when(mockResponse.getEntity()).thenReturn(mockEntity); + + when(mockClient.execute(any())).thenReturn(mockResponse); + when(mockBuilder.build()).thenReturn(mockClient); + + PdfEngineClientImpl client = new PdfEngineClientImpl( + new ObjectMapper(), mockBuilder, "test", "test"); + ; + PdfEngineResponse pdfEngineResponse = client.generatePDF(pdfEngineRequest, tempDirectory.toPath()); + + Assertions.assertEquals(HttpStatus.SC_INTERNAL_SERVER_ERROR, pdfEngineResponse.getStatusCode()); + Assertions.assertNotNull(pdfEngineResponse.getErrorMessage()); + + } + + @Test + void runKo400() throws IOException { + File tempDirectory = new File("temp"); + if (!tempDirectory.exists()) { + Files.createDirectory(tempDirectory.toPath()); + } + + File targetFile = File.createTempFile("tempFile", ".txt", tempDirectory); + byte[] template; + PdfEngineRequest pdfEngineRequest = new PdfEngineRequest(); + try (InputStream inputStream = FileInputStream.nullInputStream()) { + template = inputStream.readAllBytes(); + pdfEngineRequest.setTemplate(targetFile.toURI().toURL()); + pdfEngineRequest.setData(new String(template)); + } finally { + targetFile.deleteOnExit(); + tempDirectory.deleteOnExit(); + } + + HttpClientBuilder mockBuilder = mock(HttpClientBuilder.class); + CloseableHttpClient mockClient = mock(CloseableHttpClient.class); + + CloseableHttpResponse mockResponse = mock(CloseableHttpResponse.class); + StatusLine mockStatusLine = mock(StatusLine.class); + when(mockStatusLine.getStatusCode()).thenReturn(HttpStatus.SC_BAD_REQUEST); + when(mockResponse.getStatusLine()).thenReturn(mockStatusLine); + + HttpEntity mockEntity = mock(HttpEntity.class); + String ERROR_MESSAGE = "\"Invalid request\""; + String ERROR_400 = "{\n" + + " \"errorId\": \"a3779a25-9c8a-4a6f-9272-a052119cfd2e\",\n" + + " \"httpStatusCode\": \"BAD_REQUEST\",\n" + + " \"httpStatusDescription\": \"Bad Request\",\n" + + " \"appErrorCode\": \"PDFE_898\",\n" + + " \"errors\": [\n" + + " {\n" + + " \"message\": " + ERROR_MESSAGE + + " }\n" + + " ]\n" + + "}"; + when(mockEntity.getContent()).thenReturn(new ByteArrayInputStream(ERROR_400.getBytes())); + when(mockResponse.getEntity()).thenReturn(mockEntity); + + when(mockClient.execute(any())).thenReturn(mockResponse); + when(mockBuilder.build()).thenReturn(mockClient); + + PdfEngineClientImpl client = new PdfEngineClientImpl( + new ObjectMapper(), mockBuilder, "test", "test"); + + PdfEngineResponse pdfEngineResponse = client.generatePDF(pdfEngineRequest, tempDirectory.toPath()); + + Assertions.assertEquals(HttpStatus.SC_INTERNAL_SERVER_ERROR, pdfEngineResponse.getStatusCode()); + Assertions.assertEquals(ERROR_MESSAGE.replace("\"", ""), pdfEngineResponse.getErrorMessage()); + + } +} diff --git a/src/test/java/it/gov/pagopa/payment/notice/generator/controller/NoticeGenerationControllerTest.java b/src/test/java/it/gov/pagopa/payment/notice/generator/controller/NoticeGenerationControllerTest.java new file mode 100644 index 0000000..1462cd5 --- /dev/null +++ b/src/test/java/it/gov/pagopa/payment/notice/generator/controller/NoticeGenerationControllerTest.java @@ -0,0 +1,128 @@ +package it.gov.pagopa.payment.notice.generator.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import it.gov.pagopa.payment.notice.generator.model.NoticeGenerationRequestItem; +import it.gov.pagopa.payment.notice.generator.model.notice.*; +import it.gov.pagopa.payment.notice.generator.service.NoticeGenerationServiceImpl; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import java.io.File; +import java.nio.file.Files; +import java.util.Collections; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +class NoticeGenerationControllerTest { + + @Autowired + private MockMvc mvc; + + @MockBean + private NoticeGenerationServiceImpl noticeGenerationService; + + @Autowired + private ObjectMapper objectMapper; + + @BeforeEach + void setUp() { + Mockito.reset(noticeGenerationService); + } + + @Test + void generateNoticeShouldReturnFileOnOk() throws Exception { + File tempDirectory = Files.createTempDirectory("test").toFile(); + File file = Files.createTempFile(tempDirectory.toPath(), "test", ".zip").toFile(); + when(noticeGenerationService.generateNotice(any(),any())) + .thenReturn(file); + String url = "/notices/generate"; + mvc.perform(post(url) + .param("folderId", "test") + .content(objectMapper.writeValueAsString( + getNoticeGenerationRequestItem())) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_OCTET_STREAM)); + verify(noticeGenerationService).generateNotice(any(),any()); + } + + @Test + void generateNoticeShouldReturnBadRequestOnMissingFolder() throws Exception { + File tempDirectory = Files.createTempDirectory("test").toFile(); + File file = Files.createTempFile(tempDirectory.toPath(), "test", ".zip").toFile(); + when(noticeGenerationService.generateNotice(any(),any())) + .thenReturn(file); + String url = "/notices/generate"; + mvc.perform(post(url) + .content(objectMapper.writeValueAsString( + getNoticeGenerationRequestItem())) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isBadRequest()); + verifyNoInteractions(noticeGenerationService); + } + + @Test + void generateNoticeShouldReturnKOonErrorFile() throws Exception { + when(noticeGenerationService.generateNotice(any(),any())) + .thenReturn(null); + String url = "/notices/generate"; + mvc.perform(post(url) + .param("folderId", "test") + .content(objectMapper.writeValueAsString( + getNoticeGenerationRequestItem())) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().is5xxServerError()); + verify(noticeGenerationService).generateNotice(any(),any()); + } + + private static NoticeGenerationRequestItem getNoticeGenerationRequestItem() { + return NoticeGenerationRequestItem.builder() + .templateId("template") + .data(NoticeRequestData.builder() + .notice(Notice.builder() + .code("code") + .dueDate("24/10/2024") + .subject("subject") + .paymentAmount(100L) + .installments(Collections.singletonList( + InstallmentData.builder() + .amount(100L) + .code("codeRate") + .dueDate("24/10/2024") + .build() + )) + .build()) + .creditorInstitution(CreditorInstitution.builder() + .taxCode("taxCode") + .build()) + .debtor(Debtor.builder() + .taxCode("taxCode") + .address("address") + .city("city") + .buildingNumber("101") + .postalCode("00135") + .province("RM") + .fullName("Test Name") + .build()) + .build()) + .build(); + } + + +} diff --git a/src/test/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationServiceImplTest.java b/src/test/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationServiceImplTest.java new file mode 100644 index 0000000..3f32d67 --- /dev/null +++ b/src/test/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationServiceImplTest.java @@ -0,0 +1,277 @@ +package it.gov.pagopa.payment.notice.generator.service; + +import com.fasterxml.jackson.databind.ObjectMapper; +import it.gov.pagopa.payment.notice.generator.client.PdfEngineClient; +import it.gov.pagopa.payment.notice.generator.entity.PaymentNoticeGenerationRequest; +import it.gov.pagopa.payment.notice.generator.exception.AppException; +import it.gov.pagopa.payment.notice.generator.model.NoticeGenerationRequestItem; +import it.gov.pagopa.payment.notice.generator.model.NoticeRequestEH; +import it.gov.pagopa.payment.notice.generator.model.notice.*; +import it.gov.pagopa.payment.notice.generator.model.pdf.PdfEngineResponse; +import it.gov.pagopa.payment.notice.generator.repository.PaymentGenerationRequestErrorRepository; +import it.gov.pagopa.payment.notice.generator.repository.PaymentGenerationRequestRepository; +import it.gov.pagopa.payment.notice.generator.storage.InstitutionsStorageClient; +import it.gov.pagopa.payment.notice.generator.storage.NoticeStorageClient; +import it.gov.pagopa.payment.notice.generator.storage.NoticeTemplateStorageClient; +import it.gov.pagopa.payment.notice.generator.util.Aes256Utils; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import lombok.SneakyThrows; +import org.apache.http.HttpStatus; +import org.junit.Assert; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.io.File; +import java.nio.file.Files; +import java.util.Collections; +import java.util.Optional; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class NoticeGenerationServiceImplTest { + + @Mock + public PaymentGenerationRequestRepository paymentGenerationRequestRepository; + + @Mock + public PaymentGenerationRequestErrorRepository paymentGenerationRequestErrorRepository; + + @Mock + InstitutionsStorageClient institutionsStorageClient; + + @Mock + NoticeStorageClient noticeStorageClient; + + @Mock + NoticeTemplateStorageClient noticeTemplateStorageClient; + + @Mock + PdfEngineClient pdfEngineClient; + + ObjectMapper objectMapper = new ObjectMapper(); + + NoticeGenerationServiceImpl noticeGenerationService; + + File templateFile; + + File noticeFile; + + Validator validator; + + + @SneakyThrows + public NoticeGenerationServiceImplTest() { + ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); + validator = factory.getValidator(); + + File tempDirectory = new File("temp"); + if (!tempDirectory.exists()) { + Files.createDirectory(tempDirectory.toPath()); + } + + templateFile = File.createTempFile("tempFile", ".txt", tempDirectory); + noticeFile = File.createTempFile("notice", ".tmp", tempDirectory); + + } + + @BeforeEach + public void init() { + Mockito.reset(paymentGenerationRequestErrorRepository, paymentGenerationRequestRepository, + institutionsStorageClient, noticeStorageClient, noticeTemplateStorageClient, pdfEngineClient); + noticeGenerationService = new NoticeGenerationServiceImpl( + paymentGenerationRequestRepository, paymentGenerationRequestErrorRepository, + institutionsStorageClient, noticeStorageClient, noticeTemplateStorageClient, + pdfEngineClient, new Aes256Utils("test","test"), objectMapper, + validator); + } + + @SneakyThrows + @Test + void processNoticeGenerationShouldReturnOkOnValidData() { + + doReturn(Optional.of(PaymentNoticeGenerationRequest.builder().build())) + .when(paymentGenerationRequestRepository).findById(any()); + doReturn(templateFile).when(noticeTemplateStorageClient).getTemplate(any()); + doReturn(CreditorInstitution.builder() + .webChannel(true) + .physicalChannel("Test") + .fullName("Test") + .logo("logo") + .cbill("Cbill") + .organization("ORG") + .build() + ).when(institutionsStorageClient).getInstitutionData(any()); + doReturn(getPdfEngineResponse(HttpStatus.SC_OK, noticeFile.getPath())) + .when(pdfEngineClient).generatePDF(any(), any()); + doReturn(true).when(noticeStorageClient).savePdfToBlobStorage(any(), any()); + doReturn(1L).when(paymentGenerationRequestRepository).findAndAddItemById(any(),any()); + + NoticeRequestEH noticeRequestEH = NoticeRequestEH + .builder() + .folderId("test") + .noticeGenerationRequestItem(NoticeGenerationRequestItem.builder() + .templateId("template") + .data(NoticeRequestData.builder() + .notice(Notice.builder() + .code("code") + .dueDate("24/10/2024") + .subject("subject") + .paymentAmount(100L) + .installments(Collections.singletonList( + InstallmentData.builder() + .amount(100L) + .code("codeRate") + .dueDate("24/10/2024") + .build() + )) + .build()) + .creditorInstitution(CreditorInstitution.builder() + .taxCode("taxCode") + .build()) + .debtor(Debtor.builder() + .taxCode("taxCode") + .address("address") + .city("city") + .buildingNumber("101") + .postalCode("00135") + .province("RM") + .fullName("Test Name") + .build()) + .build()) + .build()) + .build(); + noticeGenerationService.processNoticeGenerationEH(objectMapper.writeValueAsString(noticeRequestEH)); + verify(paymentGenerationRequestRepository).findById(any()); + verify(paymentGenerationRequestRepository).findAndAddItemById(any(), any()); + verify(noticeStorageClient).savePdfToBlobStorage(any(),any()); + verify(institutionsStorageClient).getInstitutionData(any()); + verify(noticeTemplateStorageClient).getTemplate(any()); + verify(pdfEngineClient).generatePDF(any(),any()); + verifyNoInteractions(paymentGenerationRequestErrorRepository); + } + + @SneakyThrows + @Test + void processNoticeGenerationShouldReturnKOOnPDfEngineBadRequest() { + + doReturn(Optional.of(PaymentNoticeGenerationRequest.builder().build())) + .when(paymentGenerationRequestRepository).findById(any()); + doReturn(templateFile).when(noticeTemplateStorageClient).getTemplate(any()); + doReturn(CreditorInstitution.builder() + .webChannel(true) + .physicalChannel("Test") + .fullName("Test") + .logo("logo") + .cbill("Cbill") + .organization("ORG") + .build() + ).when(institutionsStorageClient).getInstitutionData(any()); + doReturn(getPdfEngineResponse(HttpStatus.SC_INTERNAL_SERVER_ERROR, noticeFile.getPath())) + .when(pdfEngineClient).generatePDF(any(), any()); + + NoticeRequestEH noticeRequestEH = NoticeRequestEH + .builder() + .folderId("test") + .noticeGenerationRequestItem(NoticeGenerationRequestItem.builder() + .templateId("template") + .data(NoticeRequestData.builder() + .notice(Notice.builder() + .code("code") + .dueDate("24/10/2024") + .subject("subject") + .paymentAmount(100L) + .installments(Collections.singletonList( + InstallmentData.builder() + .amount(100L) + .code("codeRate") + .dueDate("24/10/2024") + .build() + )) + .build()) + .creditorInstitution(CreditorInstitution.builder() + .taxCode("taxCode") + .build()) + .debtor(Debtor.builder() + .taxCode("taxCode") + .address("address") + .city("city") + .buildingNumber("101") + .postalCode("00135") + .province("RM") + .fullName("Test Name") + .build()) + .build()) + .build()) + .build(); + Assert.assertThrows(AppException.class, () -> + noticeGenerationService.processNoticeGenerationEH( + objectMapper.writeValueAsString(noticeRequestEH))); + verify(paymentGenerationRequestRepository).findById(any()); + verify(institutionsStorageClient).getInstitutionData(any()); + verify(noticeTemplateStorageClient).getTemplate(any()); + verify(pdfEngineClient).generatePDF(any(),any()); + verify(paymentGenerationRequestErrorRepository).save(any()); + verify(paymentGenerationRequestRepository).findAndIncrementNumberOfElementsFailedById(any()); + verifyNoInteractions(noticeStorageClient); + } + + @SneakyThrows + @Test + void processNoticeGenerationShouldReturnKoOnInvalidData() { + + NoticeRequestEH noticeRequestEH = NoticeRequestEH + .builder() + .noticeGenerationRequestItem(NoticeGenerationRequestItem.builder() + .templateId("template") + .data(NoticeRequestData.builder() + .notice(Notice.builder() + .code("code") + .dueDate("24/10/2024") + .subject("subject") + .paymentAmount(100L) + .installments(Collections.singletonList( + InstallmentData.builder() + .amount(100L) + .code("codeRate") + .dueDate("24/10/2024") + .build() + )) + .build()) + .creditorInstitution(CreditorInstitution.builder() + .taxCode("taxCode") + .build()) + .debtor(Debtor.builder() + .taxCode("taxCode") + .address("address") + .city("city") + .buildingNumber("101") + .postalCode("00135") + .province("RM") + .fullName("Test Name") + .build()) + .build()) + .build()) + .build(); + noticeGenerationService.processNoticeGenerationEH(objectMapper.writeValueAsString(noticeRequestEH)); + verify(paymentGenerationRequestErrorRepository).save(any()); + } + + private PdfEngineResponse getPdfEngineResponse(int status, String pdfPath) { + PdfEngineResponse pdfEngineResponse = new PdfEngineResponse(); + pdfEngineResponse.setTempPdfPath(pdfPath); + if (status != HttpStatus.SC_OK) { + pdfEngineResponse.setErrorMessage("error"); + } + pdfEngineResponse.setStatusCode(status); + return pdfEngineResponse; + } + +} diff --git a/src/test/java/it/gov/pagopa/payment/notice/generator/storage/InstitutionsStorageClientTest.java b/src/test/java/it/gov/pagopa/payment/notice/generator/storage/InstitutionsStorageClientTest.java new file mode 100644 index 0000000..230c23b --- /dev/null +++ b/src/test/java/it/gov/pagopa/payment/notice/generator/storage/InstitutionsStorageClientTest.java @@ -0,0 +1,71 @@ +package it.gov.pagopa.payment.notice.generator.storage; + +import com.azure.core.util.BinaryData; +import com.azure.core.util.Context; +import com.azure.storage.blob.BlobClient; +import com.azure.storage.blob.BlobContainerClient; +import com.azure.storage.blob.models.BlobStorageException; +import com.azure.storage.blob.options.BlobDownloadToFileOptions; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import it.gov.pagopa.payment.notice.generator.exception.AppException; +import it.gov.pagopa.payment.notice.generator.model.notice.CreditorInstitution; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.io.File; +import java.time.Duration; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class InstitutionsStorageClientTest { + + private BlobContainerClient blobContainerClient; + + private static BlobClient blobClientMock; + + private ObjectMapper objectMapper = new ObjectMapper(); + + private InstitutionsStorageClient institutionsStorageClient; + + @BeforeEach + public void init() { + blobContainerClient = mock(BlobContainerClient.class); + institutionsStorageClient = new InstitutionsStorageClient( + true, blobContainerClient, objectMapper); + blobClientMock = mock(BlobClient.class); + lenient().doReturn(blobClientMock).when(blobContainerClient).getBlobClient(anyString()); + } + + @Test + void shouldReturnCreditorInstitutions() throws JsonProcessingException { + doReturn(BinaryData.fromBytes(objectMapper.writeValueAsString( + CreditorInstitution.builder().build()).getBytes())) + .when(blobClientMock) + .downloadContent(); + CreditorInstitution result = institutionsStorageClient.getInstitutionData("testFile"); + assertNotNull(result); + } + + @Test + void shouldReturnException() { + doThrow(new BlobStorageException("test", null, null)).when(blobClientMock) + .downloadContent(); + assertThrows(AppException.class, () -> institutionsStorageClient.getInstitutionData("testFile")); + } + + @Test + void shouldReturnExceptionOnMissingClient() { + assertThrows(AppException.class, () -> + new InstitutionsStorageClient(false, null, null) + .getInstitutionData("testFile")); + } + +} diff --git a/src/test/java/it/gov/pagopa/payment/notice/generator/storage/NoticeStorageClientTest.java b/src/test/java/it/gov/pagopa/payment/notice/generator/storage/NoticeStorageClientTest.java new file mode 100644 index 0000000..cc33a26 --- /dev/null +++ b/src/test/java/it/gov/pagopa/payment/notice/generator/storage/NoticeStorageClientTest.java @@ -0,0 +1,71 @@ + +package it.gov.pagopa.payment.notice.generator.storage; + +import com.azure.core.http.rest.Response; +import com.azure.storage.blob.BlobClient; +import com.azure.storage.blob.BlobContainerClient; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpStatus; + +import java.io.InputStream; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class NoticeStorageClientTest { + + @Test + void runOk() { + BlobContainerClient mockContainer = mock(BlobContainerClient.class); + BlobClient mockClient = mock(BlobClient.class); + + Response mockBlockItem = mock(Response.class); + + when(mockBlockItem.getStatusCode()).thenReturn(HttpStatus.CREATED.value()); + + when(mockClient.uploadWithResponse(any(), eq(null), eq(null))).thenReturn( + mockBlockItem + ); + + when(mockContainer.getBlobClient(any())).thenReturn(mockClient); + + NoticeStorageClient noticeStorageClient = new NoticeStorageClient(true, mockContainer); + + boolean result = noticeStorageClient.savePdfToBlobStorage( + InputStream.nullInputStream(), "filename"); + + assertTrue(result); + + } + + @Test + void runKo() { + BlobContainerClient mockContainer = mock(BlobContainerClient.class); + BlobClient mockClient = mock(BlobClient.class); + + Response mockBlockItem = mock(Response.class); + + when(mockBlockItem.getStatusCode()).thenReturn(HttpStatus.NO_CONTENT.value()); + + when(mockClient.uploadWithResponse(any(), eq(null), eq(null))).thenReturn( + mockBlockItem + ); + + when(mockContainer.getBlobClient(any())).thenReturn(mockClient); + + NoticeStorageClient receiptBlobClient = new NoticeStorageClient(true, mockContainer); + + boolean response = receiptBlobClient.savePdfToBlobStorage(InputStream.nullInputStream(), "filename"); + + Assertions.assertFalse(response); + + } + +} diff --git a/src/test/java/it/gov/pagopa/payment/notice/generator/storage/NoticeTemplateStorageClientTest.java b/src/test/java/it/gov/pagopa/payment/notice/generator/storage/NoticeTemplateStorageClientTest.java new file mode 100644 index 0000000..9062115 --- /dev/null +++ b/src/test/java/it/gov/pagopa/payment/notice/generator/storage/NoticeTemplateStorageClientTest.java @@ -0,0 +1,78 @@ +package it.gov.pagopa.payment.notice.generator.storage; + +import com.azure.core.util.Context; +import com.azure.storage.blob.BlobClient; +import com.azure.storage.blob.BlobContainerClient; +import com.azure.storage.blob.models.BlobStorageException; +import com.azure.storage.blob.options.BlobDownloadToFileOptions; +import it.gov.pagopa.payment.notice.generator.exception.AppException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.io.File; +import java.time.Duration; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class NoticeTemplateStorageClientTest { + + private BlobContainerClient blobContainerClient; + + private static BlobClient blobClientMock; + + private NoticeTemplateStorageClient noticeTemplateStorageClient; + + @BeforeEach + public void init() { + blobContainerClient = mock(BlobContainerClient.class); + + noticeTemplateStorageClient = new NoticeTemplateStorageClient( + true, blobContainerClient); + blobClientMock = mock(BlobClient.class); + lenient().doReturn(blobClientMock).when(blobContainerClient).getBlobClient(anyString()); + } + + @Test + void shouldReturnTemplate() { + doReturn(null).when(blobClientMock) + .downloadToFileWithResponse( + any(BlobDownloadToFileOptions.class), + any(Duration.class), + any(Context.class) + ); + File result = noticeTemplateStorageClient.getTemplate("testFile"); + assertNotNull(result); + } + + @Test + void shouldReturnException() { + doThrow(new BlobStorageException("test", null, null)).when(blobClientMock) + .downloadToFileWithResponse( + any(BlobDownloadToFileOptions.class), + any(Duration.class), + any(Context.class) + ); + assertThrows(AppException.class, () -> noticeTemplateStorageClient.getTemplate("testFile")); + } + + @Test + void shouldReturnExceptionOnMissingClient() { + assertThrows(AppException.class, () -> + new NoticeTemplateStorageClient(false, null) + .getTemplate("testFile")); + } + + @Test + void shouldReturnKOOnWrongFile() { + assertThrows(AppException.class, () -> + noticeTemplateStorageClient.getTemplate("../../testFile")); + } + +} diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties index cd4c92f..e974b68 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -1,7 +1,92 @@ # Info -info.properties.environment=test -# logging -logging.level.root=INFO -logging.level.it.gov.pagopa=INFO +info.application.artifactId=@project.artifactId@ +info.application.name=@project.name@ +info.application.version=@project.version@ +info.application.description=@project.description@ +info.properties.environment=${ENV:azure} +# Actuator +management.endpoints.web.exposure.include=health,info +management.endpoints.jmx.exposure.include=health,info +management.info.env.enabled=true +management.endpoint.health.probes.enabled=true +management.health.livenessState.enabled=true +management.health.readinessState.enabled=true +# Openapi +springdoc.writer-with-order-by-keys=true +springdoc.writer-with-default-pretty-printer=true +# Server +server.servlet.context-path=/ +server.port=8080 +server.shutdown=GRACEFUL +# Logging +logging.level.root=${DEFAULT_LOGGING_LEVEL:INFO} +logging.level.it.gov.pagopa=${APP_LOGGING_LEVEL:INFO} # CORS configuration -cors.configuration={"origins": ["*"], "methods": ["*"]} +cors.configuration=${CORS_CONFIGURATION:{"origins": ["*"], "methods": ["*"]}} +# Mongo Configuration +spring.data.mongodb.uri=mongodb://localhost:27017/personDB +spring.data.mongodb.database=noticesMongoDb +spring.mongodb.embedded.version=3.4.3 +# Cache configuration +spring.cache.type=caffeine +spring.cache.caffeine.spec=maximumSize=${CACHE_SIZE:1000}, expireAfterWrite=${CACHE_EXPIRATION_TIME:720m} +cache.enabled=${CACHE_ENABLED:true} +# Jackson serialization +spring.jackson.default-property-inclusion=NON_NULL +spring.jackson.serialization.write-dates-as-timestamps=false +spring.jackson.serialization.fail-on-empty-beans=false +spring.jackson.deserialization.fail-on-unknown-properties=false + +pdf.engine.endpoint=${PDF_ENGINE_ENDPOINT:} +pdf.engine.ocpaim.subkey=${PDF_ENGINE_SUBKEY:} + +spring.cloud.azure.storage.blob.templates.enabled=${TEMPLATE_STORAGE_ENABLED:false} +spring.cloud.azure.storage.blob.templates.connection_string=${TEMPLATE_STORAGE_CONN_STRING:} +spring.cloud.azure.storage.blob.templates.containerName=${TEMPLATE_STORAGE_CONTAINER_NAME:noticetemplateblob} +spring.cloud.azure.storage.blob.templates.retry=${TEMPLATE_STORAGE_RETRY:3} +spring.cloud.azure.storage.blob.templates.timeout=${TEMPLATE_STORAGE_TIMEOUT:10} + +spring.cloud.azure.storage.blob.notices.enabled=${NOTICE_STORAGE_ENABLED:false} +spring.cloud.azure.storage.blob.notices.connection_string=${NOTICE_STORAGE_CONN_STRING:} +spring.cloud.azure.storage.blob.notices.containerName=${NOTICE_STORAGE_CONTAINER_NAME:notices} + +spring.cloud.azure.storage.blob.institutions.enabled=${INSTITUTION_STORAGE_ENABLED:false} +spring.cloud.azure.storage.blob.institutions.connection_string=${INSTITUTION_STORAGE_CONN_STRING:} +spring.cloud.azure.storage.blob.institutions.containerName=${INSTITUTION_STORAGE_CONTAINER_NAME:institutionsdatablob} + +# EH Kafka Configuration +spring.cloud.function.definition=noticeGeneration +spring.cloud.stream.bindings.noticeGeneration-in-0.destination=${KAFKA_NOTICE_GENERATION_TOPIC:pagopa-notice-evt-rx} +spring.cloud.stream.bindings.noticeGeneration-in-0.group=${KAFKA_NOTICE_GENERATION_GROUP_ID:notice-generation-group} +spring.cloud.stream.bindings.noticeGeneration-in-0.content-type=${KAFKA_CONTENT_TYPE:application/json} +spring.cloud.stream.bindings.noticeGeneration-in-0.binder=notice-generation +spring.cloud.stream.bindings.noticeGeneration-in-0.consumer.autoStartup=false +spring.cloud.stream.binders.notice-generation.type=kafka +spring.cloud.stream.binders.notice-generation.environment.spring.cloud.stream.kafka.binder.brokers=${KAFKA_BROKER:localhost:9092} +spring.cloud.stream.binders.notice-generation.environment.spring.cloud.stream.kafka.binder.configuration.sasl.jaas.config=${KAFKA_SASL_JAAS_CONFIG:} +spring.cloud.stream.binders.notice-generation.environment.spring.cloud.stream.kafka.binder.configuration.key.serializer=org.apache.kafka.common.serialization.StringSerializer +spring.cloud.stream.kafka.binder.auto-create-topics=false +spring.cloud.stream.kafka.binder.configuration.heartbeat.interval.ms=${KAFKA_CONFIG_HEARTBEAT_INTERVAL_MS:3000} +spring.cloud.stream.kafka.binder.configuration.session.timeout.ms=${KAFKA_CONFIG_SESSION_TIMEOUT_MS:60000} +spring.cloud.stream.kafka.binder.configuration.request.timeout.ms=${KAFKA_CONFIG_REQUEST_TIMEOUT_MS:60000} +spring.cloud.stream.kafka.binder.configuration.sasl.mechanism=${KAFKA_CONFIG_SASL_MECHANISM:PLAIN} +spring.cloud.stream.kafka.binder.configuration.security.protocol=${KAFKA_CONFIG_SECURITY_PROTOCOL:SASL_SSL} +spring.cloud.stream.kafka.binder.configuration.connections.max.idle.ms=${KAFKA_CONFIG_CONNECTION_MAX_IDLE_TIME:180000} +spring.cloud.stream.kafka.binder.configuration.metadata.max.idle.ms=${KAFKA_CONFIG_METADATA_MAX_IDLE_MS:180000} +spring.cloud.stream.kafka.binder.configuration.metadata.max.age.ms=${KAFKA_CONFIG_METADATA_MAX_AGE_INTERVAL:179000} +spring.cloud.stream.kafka.binder.configuration.max.request.size=${KAFKA_CONFIG_METADATA_MAX_REQUEST_SIZE:1000000} +spring.cloud.stream.kafka.bindings.noticeGeneration-in-0.consumer.startOffset=${KAFKA_CONSUMER_CONFIG_START_OFFSET:earliest} +spring.cloud.stream.kafka.bindings.noticeGeneration-in-0.consumer.autoCommitOffset=false +spring.cloud.stream.kafka.bindings.noticeGeneration-in-0.consumer.ackMode=MANUAL_IMMEDIATE +spring.cloud.stream.kafka.bindings.noticeGeneration-in-0.consumer.ackTime=${KAFKA_TRANSACTION_USER_ID_SPLITTER_ACK_MILLIS:500} +spring.cloud.stream.kafka.bindings.noticeGeneration-in-0.consumer.standardHeaders=${KAFKA_CONSUMER_CONFIG_STANDARD_HEADERS:both} +spring.cloud.stream.kafka.bindings.noticeGeneration-in-0.consumer.configuration.max.poll.records=${KAFKA_CONSUMER_CONFIG_MAX_POLL_SIZE:500} +spring.cloud.stream.kafka.bindings.noticeGeneration-in-0.consumer.configuration.max.poll.interval.ms=${KAFKA_CONFIG_MAX_POLL_INTERVAL_TIMEOUT_MS:300000} +spring.cloud.stream.kafka.bindings.noticeGeneration-in-0.consumer.configuration.connections.max.idle.ms=${KAFKA_CONSUMER_CONFIG_CONNECTIONS_MAX_IDLE_MS:180000} +spring.cloud.stream.kafka.bindings.noticeGeneration-in-0.consumer.configuration.socket.connection.setup.timeout.max.ms=${KAFKA_CONSUMER_CONFIG_CONNECTION_TIMEOUT_MAX_MS:200000} +spring.cloud.stream.kafka.bindings.noticeGeneration-in-0.consumer.configuration.socket.connection.setup.timeout.ms=${KAFKA_CONSUMER_CONFIG_CONNECTION_TIMEOUT_MS:100000} + +#Other Configs +aes.secret.key=${AES_SECRET_KEY:} +aes.salt=${AES_SALT:} + From 213e7438e0c9f83e58533e8c599b13f4fa1c13db Mon Sep 17 00:00:00 2001 From: Alessio Cialini Date: Tue, 30 Apr 2024 17:40:45 +0200 Subject: [PATCH 06/19] [VAS-834][VAS-973] feat: Updated notice model and mapper to contain poste related data --- .gitignore | 2 + .../NoticeGenerationController.java | 11 ++- .../generator/mapper/TemplateDataMapper.java | 83 +++++++++++++++++-- .../model/notice/CreditorInstitution.java | 4 + .../model/notice/InstallmentData.java | 4 + .../notice/generator/model/notice/Notice.java | 6 ++ .../model/pdf/notice/Installment.java | 3 + .../generator/model/pdf/notice/Notice.java | 4 + .../service/NoticeGenerationServiceImpl.java | 14 ++++ 9 files changed, 125 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 79d7c43..1d4be02 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,5 @@ hs_err_pid* # Helm /helm/charts/* **/.terraform/ + +/temp diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/controller/NoticeGenerationController.java b/src/main/java/it/gov/pagopa/payment/notice/generator/controller/NoticeGenerationController.java index 37d4885..646f209 100644 --- a/src/main/java/it/gov/pagopa/payment/notice/generator/controller/NoticeGenerationController.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/controller/NoticeGenerationController.java @@ -20,10 +20,12 @@ import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; -import java.io.IOException; import static it.gov.pagopa.payment.notice.generator.util.WorkingDirectoryUtils.clearTempDirectory; +/** + * Controller containing APIs to generate notice + */ @RestController @RequestMapping(value = "/notices", produces = MediaType.APPLICATION_JSON_VALUE) @Validated @@ -37,6 +39,13 @@ public NoticeGenerationController(NoticeGenerationService noticeGenerationServic this.noticeGenerationService = noticeGenerationService; } + /** + * POST method to generate a single notice, if a folderId is provided the content will be saved inside the provided + * folder + * @param folderId optional parameter to use if the content generates has to be saved + * @param noticeGenerationRequestItem data containing notice generation request + * @return generated pdf + */ @PostMapping("/generate") public ResponseEntity generateNotice( @Valid @NotNull @RequestParam("folderId") String folderId, diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/mapper/TemplateDataMapper.java b/src/main/java/it/gov/pagopa/payment/notice/generator/mapper/TemplateDataMapper.java index c0a84a0..99c33fc 100644 --- a/src/main/java/it/gov/pagopa/payment/notice/generator/mapper/TemplateDataMapper.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/mapper/TemplateDataMapper.java @@ -7,14 +7,28 @@ import java.util.Collections; +/** + * Class containing methods to map paymentNotice data to use in notice generation + */ public class TemplateDataMapper { + /** + * Map notice generation dat + * @param noticeRequestData request data + * @return mapped notice data + */ public static PaymentNotice mapTemplate(NoticeRequestData noticeRequestData) { String noticeCode = noticeRequestData.getNotice().getCode(); String cbill = noticeRequestData.getCreditorInstitution().getCbill(); String ciTaxCode = noticeRequestData.getCreditorInstitution().getTaxCode(); String noticeAmount = String.valueOf(noticeRequestData.getNotice().getPaymentAmount()); + String debtorTaxCode = noticeRequestData.getDebtor().getTaxCode(); + String fullName = noticeRequestData.getDebtor().getFullName(); + String subject = noticeRequestData.getNotice().getSubject(); + String posteAuthCode = noticeRequestData.getNotice().getPosteAuth(); + String posteAccountNumber = noticeRequestData.getCreditorInstitution().getPosteAccountNumber(); + return PaymentNotice.builder() .payee(Payee.builder() @@ -42,8 +56,8 @@ public static PaymentNotice mapTemplate(NoticeRequestData noticeRequestData) { .city(noticeRequestData.getDebtor().getCity()) .postalCode(noticeRequestData.getDebtor().getPostalCode()) .address(noticeRequestData.getDebtor().getAddress()) - .fullName(noticeRequestData.getDebtor().getFullName()) - .taxCode(noticeRequestData.getDebtor().getTaxCode()) + .fullName(fullName) + .taxCode(debtorTaxCode) .build() ) .notice(Notice.builder() @@ -57,11 +71,26 @@ public static PaymentNotice mapTemplate(NoticeRequestData noticeRequestData) { .subject(noticeRequestData.getNotice().getSubject()) .amount(noticeAmount) .expiryDate(noticeRequestData.getNotice().getDueDate()) + .posteDataMatrix(posteAuthCode != null ? + generatePosteDataMatrix( + ciTaxCode, + debtorTaxCode, + fullName, + subject, + posteAuthCode, + posteAccountNumber, + noticeAmount, + noticeRequestData.getNotice().getPosteDocumentType() + ) : null) .instalments(noticeRequestData.getNotice().getInstallments() != null ? noticeRequestData.getNotice().getInstallments().stream().map(item -> mapInstallment( - noticeCode, + ciTaxCode, cbill, + debtorTaxCode, + fullName, + subject, + posteAccountNumber, item )).toList() : Collections.emptyList()) @@ -69,6 +98,33 @@ public static PaymentNotice mapTemplate(NoticeRequestData noticeRequestData) { .build(); } + private static String generatePosteDataMatrix( + String ciTaxCode, String debtorTaxCode, String fullName, String subject, String authCode, + String posteAccountNumber, String amount, String posteTypeCode + ) { + return String.join("", + "codfase=NBPA;", + generateCodeline(authCode, posteAccountNumber, amount, posteTypeCode), + "1P1", + StringUtils.rightPad(ciTaxCode, 11, " "), + StringUtils.rightPad(debtorTaxCode, 16, " "), + StringUtils.rightPad(fullName, 40, " "), + StringUtils.rightPad(subject, 110, " "), + StringUtils.rightPad("", 12, " "), + "A"); + } + + private static String generateCodeline(String posteAuthCode, String posteAccountNumber, + String amount, String posteTypeCode) { + return String.join("","18", + StringUtils.leftPad(posteAuthCode, 18, "0"), + "12", + StringUtils.leftPad(posteAccountNumber, 12, "0"), + "10", + StringUtils.leftPad(amount, 10, "0"), "3", + posteTypeCode); + } + private static String generateQrCode(String code, String taxCode, String amount) { return String.join("|", "PAGOPA","002", @@ -80,14 +136,31 @@ private static String generateQrCode(String code, String taxCode, String amount) } private static Installment mapInstallment( - String cbill, String taxCode, InstallmentData installmentData) { + String cbill, String ciTaxCode, String debtorTaxCode, + String fullname, String subject, String accountNumber, + InstallmentData installmentData) { String amount = String.valueOf(installmentData.getAmount()); return Installment.builder() .refNumber(installmentData.getCode()) .cbillCode(cbill) - .qrCode(generateQrCode(installmentData.getCode(), taxCode, amount)) + .qrCode(generateQrCode(installmentData.getCode(), ciTaxCode, amount)) .amount(amount) .expiryDate(installmentData.getDueDate()) + .posteDocumentType(installmentData.getPosteDocumentType()) + .posteAuth(installmentData.getPosteAuth()) + .posteDataMatrix(installmentData.getPosteAuth() != null ? + generatePosteDataMatrix( + ciTaxCode, + debtorTaxCode, + fullname, + subject, + installmentData.getPosteAuth(), + accountNumber, + String.valueOf(installmentData.getAmount()), + installmentData.getPosteDocumentType() + ) : + null + ) .build(); } diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/CreditorInstitution.java b/src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/CreditorInstitution.java index 835b828..8a27d4c 100644 --- a/src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/CreditorInstitution.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/CreditorInstitution.java @@ -40,5 +40,9 @@ public class CreditorInstitution { private String logo; + @Schema(description = "Poste account number") + private String posteAccountNumber; + + } diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/InstallmentData.java b/src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/InstallmentData.java index b8db9e5..8b471c3 100644 --- a/src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/InstallmentData.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/InstallmentData.java @@ -18,5 +18,9 @@ public class InstallmentData { private Long amount; @Schema(description = "Installment dueDate", requiredMode = Schema.RequiredMode.REQUIRED) private String dueDate; + @Schema(description = "Installment poste auth code") + private String posteAuth; + @Schema(description = "Poste Document Type") + private String posteDocumentType; } diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/Notice.java b/src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/Notice.java index 508afd1..22a088e 100644 --- a/src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/Notice.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/model/notice/Notice.java @@ -32,6 +32,12 @@ public class Notice { @NotEmpty private String code; + @Schema(description = "Poste auth code") + private String posteAuth; + + @Schema(description = "Poste Document Type") + private String posteDocumentType; + @Schema(description = "Notice installments (if present)") private List installments; diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/notice/Installment.java b/src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/notice/Installment.java index e4edd15..561bf14 100644 --- a/src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/notice/Installment.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/notice/Installment.java @@ -16,5 +16,8 @@ public class Installment { private String expiryDate; private String cbillCode; private String qrCode; + private String posteAuth; + private String posteDataMatrix; + private String posteDocumentType; } diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/notice/Notice.java b/src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/notice/Notice.java index 8f9d01f..a296fae 100644 --- a/src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/notice/Notice.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/notice/Notice.java @@ -19,6 +19,10 @@ public class Notice { private String qrCode; private String refNumber; private String cbillCode; + private String posteAccountNumber; + private String posteAuth; + private String posteDocumentType; + private String posteDataMatrix; private List instalments; } diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationServiceImpl.java b/src/main/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationServiceImpl.java index 51e7ccb..c327787 100644 --- a/src/main/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationServiceImpl.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationServiceImpl.java @@ -37,6 +37,9 @@ import static it.gov.pagopa.payment.notice.generator.util.WorkingDirectoryUtils.clearTempDirectory; import static it.gov.pagopa.payment.notice.generator.util.WorkingDirectoryUtils.createWorkingDirectory; +/** + * Services regarding the notice generation flow + */ @Service @Slf4j public class NoticeGenerationServiceImpl implements NoticeGenerationService { @@ -77,6 +80,13 @@ public NoticeGenerationServiceImpl( this.validator = validator; } + /** + * Generate a notice, if required content is provided and valid, and saves the content to a folderId if provided + * and valid + * @param noticeGenerationRequestItem request data to use for the notice generation + * @param folderId optional parameter to generate folderId + * @return generated notice + */ @SneakyThrows @Override public File generateNotice(NoticeGenerationRequestItem noticeGenerationRequestItem, @@ -156,6 +166,10 @@ public File generateNotice(NoticeGenerationRequestItem noticeGenerationRequestIt } + /** + * Generate a notice provided as a EH message + * @param message content to use for generation process + */ @Override public void processNoticeGenerationEH(String message) { From fda89b73eb00992ff514d69d8fc62a99fe8911dd Mon Sep 17 00:00:00 2001 From: Alessio Cialini Date: Tue, 7 May 2024 17:39:52 +0200 Subject: [PATCH 07/19] [VAS-834][VAS-973] feat: Updated helm chart, updated client and services --- helm/values-dev.yaml | 4 ++-- helm/values-prod.yaml | 3 ++- helm/values-uat.yaml | 3 ++- .../payment/notice/generator/client/PdfEngineClientImpl.java | 4 ++-- .../generator/controller/NoticeGenerationController.java | 2 +- .../generator/service/NoticeGenerationServiceImpl.java | 4 ---- .../generator/storage/NoticeTemplateStorageClient.java | 5 +++++ 7 files changed, 14 insertions(+), 11 deletions(-) diff --git a/helm/values-dev.yaml b/helm/values-dev.yaml index 9eae604..1dc4080 100644 --- a/helm/values-dev.yaml +++ b/helm/values-dev.yaml @@ -67,7 +67,6 @@ microservice-chart: APP_LOGGING_LEVEL: 'DEBUG' DEFAULT_LOGGING_LEVEL: 'INFO' CORS_CONFIGURATION: '{"origins": ["*"], "methods": ["*"]}' - OTEL_SERVICE_NAME: 'print-payment-notice-generator-otl' OTEL_RESOURCE_ATTRIBUTES: "deployment.environment=dev" OTEL_EXPORTER_OTLP_ENDPOINT: "http://otel-collector.elastic-system.svc:4317" @@ -75,7 +74,8 @@ microservice-chart: OTEL_METRICS_EXPORTER: otlp OTEL_LOGS_EXPORTER: none OTEL_TRACES_SAMPLER: "always_on" - KAFKA_BROKER: "pagopa-d-printit-evh-ns01.servicebus.windows.net:9092" + KAFKA_BROKER: "pagopa-d-itn-core-evh-meucci.servicebus.windows.net:9093" + KAFKA_NOTICE_GENERATION_TOPIC: pagopa-printit-evh envSecret: # required APPLICATIONINSIGHTS_CONNECTION_STRING: 'ai-d-connection-string' diff --git a/helm/values-prod.yaml b/helm/values-prod.yaml index 7d21ba2..b08e8c5 100644 --- a/helm/values-prod.yaml +++ b/helm/values-prod.yaml @@ -75,7 +75,8 @@ microservice-chart: OTEL_METRICS_EXPORTER: otlp OTEL_LOGS_EXPORTER: none OTEL_TRACES_SAMPLER: "always_on" - KAFKA_BROKER: "pagopa-รจ-printit-evh-ns01.servicebus.windows.net:9092" + KAFKA_BROKER: "pagopa-p-itn-core-evh-meucci.servicebus.windows.net:9093" + KAFKA_NOTICE_GENERATION_TOPIC: pagopa-printit-evh envSecret: # required APPLICATIONINSIGHTS_CONNECTION_STRING: 'ai-p-connection-string' diff --git a/helm/values-uat.yaml b/helm/values-uat.yaml index 7605dbe..e8291ff 100644 --- a/helm/values-uat.yaml +++ b/helm/values-uat.yaml @@ -75,7 +75,8 @@ microservice-chart: OTEL_METRICS_EXPORTER: otlp OTEL_LOGS_EXPORTER: none OTEL_TRACES_SAMPLER: "always_on" - KAFKA_BROKER: "pagopa-u-printit-evh-ns01.servicebus.windows.net:9092" + KAFKA_BROKER: "pagopa-u-itn-core-evh-meucci.servicebus.windows.net:9093" + KAFKA_NOTICE_GENERATION_TOPIC: pagopa-printit-evh envSecret: # required APPLICATIONINSIGHTS_CONNECTION_STRING: 'ai-u-connection-string' diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/client/PdfEngineClientImpl.java b/src/main/java/it/gov/pagopa/payment/notice/generator/client/PdfEngineClientImpl.java index d323043..c050e24 100644 --- a/src/main/java/it/gov/pagopa/payment/notice/generator/client/PdfEngineClientImpl.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/client/PdfEngineClientImpl.java @@ -45,8 +45,8 @@ public class PdfEngineClientImpl implements PdfEngineClient { @Autowired private PdfEngineClientImpl(ObjectMapper objectMapper, - @Value("pdf.engine.endpoint") String pdfEngineEndpoint, - @Value("pdf.engine.ocpaim.subkey") String ocpAimSubKey) { + @Value("${pdf.engine.endpoint}") String pdfEngineEndpoint, + @Value("${pdf.engine.ocpaim.subkey}") String ocpAimSubKey) { this.objectMapper = objectMapper; this.httpClientBuilder = HttpClientBuilder.create(); this.ocpAimSubKey = ocpAimSubKey; diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/controller/NoticeGenerationController.java b/src/main/java/it/gov/pagopa/payment/notice/generator/controller/NoticeGenerationController.java index 646f209..1df7eb4 100644 --- a/src/main/java/it/gov/pagopa/payment/notice/generator/controller/NoticeGenerationController.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/controller/NoticeGenerationController.java @@ -48,7 +48,7 @@ public NoticeGenerationController(NoticeGenerationService noticeGenerationServic */ @PostMapping("/generate") public ResponseEntity generateNotice( - @Valid @NotNull @RequestParam("folderId") String folderId, + @RequestParam(value = "folderId", required = false) String folderId, @Parameter(description = "templateId to use for retrieval") @Valid @NotNull @RequestBody NoticeGenerationRequestItem noticeGenerationRequestItem) { File file = noticeGenerationService.generateNotice(noticeGenerationRequestItem, folderId); diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationServiceImpl.java b/src/main/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationServiceImpl.java index c327787..8cc9af1 100644 --- a/src/main/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationServiceImpl.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationServiceImpl.java @@ -158,10 +158,6 @@ public File generateNotice(NoticeGenerationRequestItem noticeGenerationRequestIt saveErrorEvent(folderId, noticeGenerationRequestItem, e.getMessage()); } throw e; - } finally { - if (tempDirectory != null) { - clearTempDirectory(tempDirectory); - } } } diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/storage/NoticeTemplateStorageClient.java b/src/main/java/it/gov/pagopa/payment/notice/generator/storage/NoticeTemplateStorageClient.java index 9b652af..59b17b6 100644 --- a/src/main/java/it/gov/pagopa/payment/notice/generator/storage/NoticeTemplateStorageClient.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/storage/NoticeTemplateStorageClient.java @@ -103,8 +103,13 @@ private BlobDownloadToFileOptions getBlobDownloadToFileOptions(String filePath) private String createTemplatesDirectory(String templateId) { try { Path workingDirectory = createWorkingDirectory().toPath().normalize().toAbsolutePath(); + File templatesDirectory = new File(workingDirectory+"/templates"); + if (!templatesDirectory.exists()) { + Files.createDirectory(templatesDirectory.toPath()); + } Path filePath = workingDirectory.resolve( "templates/"+templateId + ".zip").normalize().toAbsolutePath(); + if (!filePath.startsWith(workingDirectory + File.separator)) { throw new IllegalArgumentException("Invalid filename"); } From 3f1bf2d08ef0ab55298c3797aa52f839f80a5a4f Mon Sep 17 00:00:00 2001 From: Alessio Cialini Date: Wed, 8 May 2024 10:27:36 +0200 Subject: [PATCH 08/19] [VAS-834][VAS-973] feat: Updated generation service and tests --- openapi/openapi.json | 22 ++++++++++++++++++- .../service/NoticeGenerationServiceImpl.java | 4 ++-- .../storage/NoticeStorageClient.java | 4 ++-- .../NoticeGenerationControllerTest.java | 16 -------------- .../NoticeGenerationServiceImplTest.java | 4 ++-- .../storage/NoticeStorageClientTest.java | 4 ++-- 6 files changed, 29 insertions(+), 25 deletions(-) diff --git a/openapi/openapi.json b/openapi/openapi.json index b432270..4e08a77 100644 --- a/openapi/openapi.json +++ b/openapi/openapi.json @@ -25,7 +25,7 @@ "parameters" : [ { "in" : "query", "name" : "folderId", - "required" : true, + "required" : false, "schema" : { "type" : "string" } @@ -94,6 +94,10 @@ "type" : "string", "description" : "CI physical channel data" }, + "posteAccountNumber" : { + "type" : "string", + "description" : "Poste account number" + }, "taxCode" : { "type" : "string", "description" : "CI tax code" @@ -156,6 +160,14 @@ "dueDate" : { "type" : "string", "description" : "Installment dueDate" + }, + "posteAuth" : { + "type" : "string", + "description" : "Installment poste auth code" + }, + "posteDocumentType" : { + "type" : "string", + "description" : "Poste Document Type" } }, "description" : "Notice installments (if present)" @@ -184,6 +196,14 @@ "description" : "Notice total amount to pay", "format" : "int64" }, + "posteAuth" : { + "type" : "string", + "description" : "Poste auth code" + }, + "posteDocumentType" : { + "type" : "string", + "description" : "Poste Document Type" + }, "subject" : { "type" : "string", "description" : "Notice subject" diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationServiceImpl.java b/src/main/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationServiceImpl.java index 8cc9af1..4d2270e 100644 --- a/src/main/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationServiceImpl.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationServiceImpl.java @@ -100,7 +100,7 @@ public File generateNotice(NoticeGenerationRequestItem noticeGenerationRequestIt } } - Path tempDirectory = null; + Path tempDirectory; try { @@ -139,7 +139,7 @@ public File generateNotice(NoticeGenerationRequestItem noticeGenerationRequestIt String dateFormatted = LocalDate.now().format(DateTimeFormatter.ofPattern("yyMMdd")); String blobName = String.format("%s-%s-%s", "pagopa-avviso", dateFormatted, noticeGenerationRequestItem.getData().getNotice().getCode()); - if (!noticeStorageClient.savePdfToBlobStorage(pdfStream, blobName)) { + if (!noticeStorageClient.savePdfToBlobStorage(pdfStream, folderId, blobName)) { throw new RuntimeException("Encountered error during blob saving"); } paymentGenerationRequestRepository.findAndAddItemById(folderId, blobName); diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/storage/NoticeStorageClient.java b/src/main/java/it/gov/pagopa/payment/notice/generator/storage/NoticeStorageClient.java index 4bb9a25..cbd57cc 100644 --- a/src/main/java/it/gov/pagopa/payment/notice/generator/storage/NoticeStorageClient.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/storage/NoticeStorageClient.java @@ -45,10 +45,10 @@ public NoticeStorageClient( * @param fileName Filename to save the PDF with * @return blob storage response with PDF metadata or error message and status */ - public boolean savePdfToBlobStorage(InputStream pdf, String fileName) { + public boolean savePdfToBlobStorage(InputStream pdf, String folderId, String fileName) { //Get a reference to a blob - BlobClient blobClient = blobContainerClient.getBlobClient(fileName); + BlobClient blobClient = blobContainerClient.getBlobClient(String.join("/", folderId, fileName)); //Upload the blob Response blockBlobItemResponse = blobClient.uploadWithResponse( diff --git a/src/test/java/it/gov/pagopa/payment/notice/generator/controller/NoticeGenerationControllerTest.java b/src/test/java/it/gov/pagopa/payment/notice/generator/controller/NoticeGenerationControllerTest.java index 1462cd5..de8807d 100644 --- a/src/test/java/it/gov/pagopa/payment/notice/generator/controller/NoticeGenerationControllerTest.java +++ b/src/test/java/it/gov/pagopa/payment/notice/generator/controller/NoticeGenerationControllerTest.java @@ -60,22 +60,6 @@ void generateNoticeShouldReturnFileOnOk() throws Exception { verify(noticeGenerationService).generateNotice(any(),any()); } - @Test - void generateNoticeShouldReturnBadRequestOnMissingFolder() throws Exception { - File tempDirectory = Files.createTempDirectory("test").toFile(); - File file = Files.createTempFile(tempDirectory.toPath(), "test", ".zip").toFile(); - when(noticeGenerationService.generateNotice(any(),any())) - .thenReturn(file); - String url = "/notices/generate"; - mvc.perform(post(url) - .content(objectMapper.writeValueAsString( - getNoticeGenerationRequestItem())) - .contentType(MediaType.APPLICATION_JSON) - ) - .andExpect(status().isBadRequest()); - verifyNoInteractions(noticeGenerationService); - } - @Test void generateNoticeShouldReturnKOonErrorFile() throws Exception { when(noticeGenerationService.generateNotice(any(),any())) diff --git a/src/test/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationServiceImplTest.java b/src/test/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationServiceImplTest.java index 3f32d67..05d54e3 100644 --- a/src/test/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationServiceImplTest.java +++ b/src/test/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationServiceImplTest.java @@ -111,7 +111,7 @@ void processNoticeGenerationShouldReturnOkOnValidData() { ).when(institutionsStorageClient).getInstitutionData(any()); doReturn(getPdfEngineResponse(HttpStatus.SC_OK, noticeFile.getPath())) .when(pdfEngineClient).generatePDF(any(), any()); - doReturn(true).when(noticeStorageClient).savePdfToBlobStorage(any(), any()); + doReturn(true).when(noticeStorageClient).savePdfToBlobStorage(any(), any(), any()); doReturn(1L).when(paymentGenerationRequestRepository).findAndAddItemById(any(),any()); NoticeRequestEH noticeRequestEH = NoticeRequestEH @@ -151,7 +151,7 @@ void processNoticeGenerationShouldReturnOkOnValidData() { noticeGenerationService.processNoticeGenerationEH(objectMapper.writeValueAsString(noticeRequestEH)); verify(paymentGenerationRequestRepository).findById(any()); verify(paymentGenerationRequestRepository).findAndAddItemById(any(), any()); - verify(noticeStorageClient).savePdfToBlobStorage(any(),any()); + verify(noticeStorageClient).savePdfToBlobStorage(any(),any(),any()); verify(institutionsStorageClient).getInstitutionData(any()); verify(noticeTemplateStorageClient).getTemplate(any()); verify(pdfEngineClient).generatePDF(any(),any()); diff --git a/src/test/java/it/gov/pagopa/payment/notice/generator/storage/NoticeStorageClientTest.java b/src/test/java/it/gov/pagopa/payment/notice/generator/storage/NoticeStorageClientTest.java index cc33a26..3e5e126 100644 --- a/src/test/java/it/gov/pagopa/payment/notice/generator/storage/NoticeStorageClientTest.java +++ b/src/test/java/it/gov/pagopa/payment/notice/generator/storage/NoticeStorageClientTest.java @@ -39,7 +39,7 @@ void runOk() { NoticeStorageClient noticeStorageClient = new NoticeStorageClient(true, mockContainer); boolean result = noticeStorageClient.savePdfToBlobStorage( - InputStream.nullInputStream(), "filename"); + InputStream.nullInputStream(), "folderId","filename"); assertTrue(result); @@ -62,7 +62,7 @@ void runKo() { NoticeStorageClient receiptBlobClient = new NoticeStorageClient(true, mockContainer); - boolean response = receiptBlobClient.savePdfToBlobStorage(InputStream.nullInputStream(), "filename"); + boolean response = receiptBlobClient.savePdfToBlobStorage(InputStream.nullInputStream(), "folderId","filename"); Assertions.assertFalse(response); From e52f1a2faf1a58233997107f30143530cb7e44aa Mon Sep 17 00:00:00 2001 From: Alessio Cialini Date: Wed, 8 May 2024 10:47:24 +0200 Subject: [PATCH 09/19] [VAS-834][VAS-973] feat: Updated NoticeStorageClient --- .../payment/notice/generator/storage/NoticeStorageClient.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/storage/NoticeStorageClient.java b/src/main/java/it/gov/pagopa/payment/notice/generator/storage/NoticeStorageClient.java index cbd57cc..6386d11 100644 --- a/src/main/java/it/gov/pagopa/payment/notice/generator/storage/NoticeStorageClient.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/storage/NoticeStorageClient.java @@ -48,7 +48,8 @@ public NoticeStorageClient( public boolean savePdfToBlobStorage(InputStream pdf, String folderId, String fileName) { //Get a reference to a blob - BlobClient blobClient = blobContainerClient.getBlobClient(String.join("/", folderId, fileName)); + BlobClient blobClient = blobContainerClient.getBlobClient(String.join("/", folderId, + fileName.concat(".pdf"))); //Upload the blob Response blockBlobItemResponse = blobClient.uploadWithResponse( From 8afe6f33033160066df1dbcd59215697c24bada9 Mon Sep 17 00:00:00 2001 From: Alessio Cialini <63233981+alessio-cialini@users.noreply.github.com> Date: Thu, 9 May 2024 09:52:02 +0200 Subject: [PATCH 10/19] Update helm/values-prod.yaml Co-authored-by: Jacopo Carlini --- helm/values-prod.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/values-prod.yaml b/helm/values-prod.yaml index b08e8c5..f3b2f68 100644 --- a/helm/values-prod.yaml +++ b/helm/values-prod.yaml @@ -29,7 +29,7 @@ microservice-chart: - 8080 ingress: create: true - host: "itnprod.printit.internal.platform.pagopa.it" + host: "printit.itn.internal.platform.pagopa.it" path: /pagopa-print-payment-notice-generator/(.*) servicePort: 8080 serviceAccount: From 80f44454b94995616ebc34381a951b92e4c57e47 Mon Sep 17 00:00:00 2001 From: Alessio Cialini <63233981+alessio-cialini@users.noreply.github.com> Date: Thu, 9 May 2024 09:52:15 +0200 Subject: [PATCH 11/19] Update helm/values-dev.yaml Co-authored-by: Jacopo Carlini --- helm/values-dev.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/values-dev.yaml b/helm/values-dev.yaml index 1dc4080..d1e14f9 100644 --- a/helm/values-dev.yaml +++ b/helm/values-dev.yaml @@ -29,7 +29,7 @@ microservice-chart: - 8080 ingress: create: true - host: "itndev.printit.internal.dev.platform.pagopa.it" + host: "printit.itn.internal.dev.platform.pagopa.it" path: /pagopa-print-payment-notice-generator/(.*) servicePort: 8080 serviceAccount: From 464e63c59c9d7391331b40c00df338dfed3a0c34 Mon Sep 17 00:00:00 2001 From: Alessio Cialini <63233981+alessio-cialini@users.noreply.github.com> Date: Thu, 9 May 2024 09:52:45 +0200 Subject: [PATCH 12/19] Update infra/04_apim_api.tf Co-authored-by: Jacopo Carlini --- infra/04_apim_api.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/04_apim_api.tf b/infra/04_apim_api.tf index 05509ca..12adb3e 100644 --- a/infra/04_apim_api.tf +++ b/infra/04_apim_api.tf @@ -3,7 +3,7 @@ locals { display_name = "Payment Notices Print Generator APIs" description = "Payment Notices Print Generator APIs" - path = "payment/notice/generator" + path = "print-payment-notice-generator" host = "api.${var.apim_dns_zone_prefix}.${var.external_domain}" hostname = var.hostname From f9971fdaff93b23df4578758f512574227803353 Mon Sep 17 00:00:00 2001 From: Alessio Cialini <63233981+alessio-cialini@users.noreply.github.com> Date: Thu, 9 May 2024 09:53:49 +0200 Subject: [PATCH 13/19] Update helm/values-uat.yaml Co-authored-by: Jacopo Carlini --- helm/values-uat.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/values-uat.yaml b/helm/values-uat.yaml index e8291ff..1f71e4f 100644 --- a/helm/values-uat.yaml +++ b/helm/values-uat.yaml @@ -29,7 +29,7 @@ microservice-chart: - 8080 ingress: create: true - host: "itnuat.printit.internal.uat.platform.pagopa.it" + host: "printit.itn.internal.uat.platform.pagopa.it" path: /pagopa-print-payment-notice-generator/(.*) servicePort: 8080 serviceAccount: From ae6d459ca57bdc6c79b0ac207c801982d095a79d Mon Sep 17 00:00:00 2001 From: Alessio Cialini <63233981+alessio-cialini@users.noreply.github.com> Date: Thu, 9 May 2024 09:55:36 +0200 Subject: [PATCH 14/19] Update src/main/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationServiceImpl.java Co-authored-by: Jacopo Carlini --- .../generator/service/NoticeGenerationServiceImpl.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationServiceImpl.java b/src/main/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationServiceImpl.java index 4d2270e..76f35bd 100644 --- a/src/main/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationServiceImpl.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationServiceImpl.java @@ -93,11 +93,9 @@ public File generateNotice(NoticeGenerationRequestItem noticeGenerationRequestIt String folderId) { if (folderId != null) { - Optional paymentNoticeGenerationRequestOptional = - paymentGenerationRequestRepository.findById(folderId); - if (paymentNoticeGenerationRequestOptional.isEmpty()) { - throw new AppException(AppError.FOLDER_NOT_AVAILABLE); - } + PaymentNoticeGenerationRequest paymentNoticeGenerationRequestOptional = + paymentGenerationRequestRepository.findById(folderId) + .orElseThrow(()->throw new AppException(AppError.FOLDER_NOT_AVAILABLE)); } Path tempDirectory; From c5ae8ff28f1728bd4735e2ee1b8725ead2989ff9 Mon Sep 17 00:00:00 2001 From: Alessio Cialini Date: Thu, 9 May 2024 09:58:35 +0200 Subject: [PATCH 15/19] [VAS-834][VAS-973] feat: Updated terraform domains from weu to itn --- infra/env/{weu-dev => itn-dev}/backend.ini | 0 infra/env/{weu-dev => itn-dev}/backend.tfvars | 0 infra/env/{weu-dev => itn-dev}/terraform.tfvars | 0 infra/env/{weu-prod => itn-prod}/backend.ini | 0 infra/env/{weu-prod => itn-prod}/backend.tfvars | 0 infra/env/{weu-prod => itn-prod}/terraform.tfvars | 0 infra/env/{weu-uat => itn-uat}/backend.ini | 0 infra/env/{weu-uat => itn-uat}/backend.tfvars | 0 infra/env/{weu-uat => itn-uat}/terraform.tfvars | 0 9 files changed, 0 insertions(+), 0 deletions(-) rename infra/env/{weu-dev => itn-dev}/backend.ini (100%) rename infra/env/{weu-dev => itn-dev}/backend.tfvars (100%) rename infra/env/{weu-dev => itn-dev}/terraform.tfvars (100%) rename infra/env/{weu-prod => itn-prod}/backend.ini (100%) rename infra/env/{weu-prod => itn-prod}/backend.tfvars (100%) rename infra/env/{weu-prod => itn-prod}/terraform.tfvars (100%) rename infra/env/{weu-uat => itn-uat}/backend.ini (100%) rename infra/env/{weu-uat => itn-uat}/backend.tfvars (100%) rename infra/env/{weu-uat => itn-uat}/terraform.tfvars (100%) diff --git a/infra/env/weu-dev/backend.ini b/infra/env/itn-dev/backend.ini similarity index 100% rename from infra/env/weu-dev/backend.ini rename to infra/env/itn-dev/backend.ini diff --git a/infra/env/weu-dev/backend.tfvars b/infra/env/itn-dev/backend.tfvars similarity index 100% rename from infra/env/weu-dev/backend.tfvars rename to infra/env/itn-dev/backend.tfvars diff --git a/infra/env/weu-dev/terraform.tfvars b/infra/env/itn-dev/terraform.tfvars similarity index 100% rename from infra/env/weu-dev/terraform.tfvars rename to infra/env/itn-dev/terraform.tfvars diff --git a/infra/env/weu-prod/backend.ini b/infra/env/itn-prod/backend.ini similarity index 100% rename from infra/env/weu-prod/backend.ini rename to infra/env/itn-prod/backend.ini diff --git a/infra/env/weu-prod/backend.tfvars b/infra/env/itn-prod/backend.tfvars similarity index 100% rename from infra/env/weu-prod/backend.tfvars rename to infra/env/itn-prod/backend.tfvars diff --git a/infra/env/weu-prod/terraform.tfvars b/infra/env/itn-prod/terraform.tfvars similarity index 100% rename from infra/env/weu-prod/terraform.tfvars rename to infra/env/itn-prod/terraform.tfvars diff --git a/infra/env/weu-uat/backend.ini b/infra/env/itn-uat/backend.ini similarity index 100% rename from infra/env/weu-uat/backend.ini rename to infra/env/itn-uat/backend.ini diff --git a/infra/env/weu-uat/backend.tfvars b/infra/env/itn-uat/backend.tfvars similarity index 100% rename from infra/env/weu-uat/backend.tfvars rename to infra/env/itn-uat/backend.tfvars diff --git a/infra/env/weu-uat/terraform.tfvars b/infra/env/itn-uat/terraform.tfvars similarity index 100% rename from infra/env/weu-uat/terraform.tfvars rename to infra/env/itn-uat/terraform.tfvars From c14ba6a29cc4d3a9e23db6dd827374e3d35a119c Mon Sep 17 00:00:00 2001 From: Alessio Cialini Date: Thu, 9 May 2024 10:35:57 +0200 Subject: [PATCH 16/19] [VAS-834][VAS-973] feat: Updated unit tests --- .../service/NoticeGenerationServiceImpl.java | 7 ++++--- .../storage/NoticeTemplateStorageClient.java | 2 +- .../service/NoticeGenerationServiceImplTest.java | 5 +++++ .../storage/InstitutionsStorageClientTest.java | 12 ++++++++++++ .../storage/NoticeTemplateStorageClientTest.java | 1 + 5 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationServiceImpl.java b/src/main/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationServiceImpl.java index 76f35bd..7040f4b 100644 --- a/src/main/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationServiceImpl.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationServiceImpl.java @@ -93,9 +93,10 @@ public File generateNotice(NoticeGenerationRequestItem noticeGenerationRequestIt String folderId) { if (folderId != null) { - PaymentNoticeGenerationRequest paymentNoticeGenerationRequestOptional = - paymentGenerationRequestRepository.findById(folderId) - .orElseThrow(()->throw new AppException(AppError.FOLDER_NOT_AVAILABLE)); + paymentGenerationRequestRepository.findById(folderId) + .orElseThrow(() -> { + throw new AppException(AppError.FOLDER_NOT_AVAILABLE); + }); } Path tempDirectory; diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/storage/NoticeTemplateStorageClient.java b/src/main/java/it/gov/pagopa/payment/notice/generator/storage/NoticeTemplateStorageClient.java index 59b17b6..4f285c9 100644 --- a/src/main/java/it/gov/pagopa/payment/notice/generator/storage/NoticeTemplateStorageClient.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/storage/NoticeTemplateStorageClient.java @@ -120,7 +120,7 @@ private String createTemplatesDirectory(String templateId) { } @Scheduled(cron = "${spring.cloud.azure.storage.blob.templates.timeout}") - private void refreshTemplates() { + protected void refreshTemplates() { File templateFiles = new File("temp/templates"); if (templateFiles.exists()) { if (!templateFiles.delete()) { diff --git a/src/test/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationServiceImplTest.java b/src/test/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationServiceImplTest.java index 05d54e3..43c09c2 100644 --- a/src/test/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationServiceImplTest.java +++ b/src/test/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationServiceImplTest.java @@ -172,6 +172,7 @@ void processNoticeGenerationShouldReturnKOOnPDfEngineBadRequest() { .logo("logo") .cbill("Cbill") .organization("ORG") + .posteAccountNumber("131213") .build() ).when(institutionsStorageClient).getInstitutionData(any()); doReturn(getPdfEngineResponse(HttpStatus.SC_INTERNAL_SERVER_ERROR, noticeFile.getPath())) @@ -188,11 +189,15 @@ void processNoticeGenerationShouldReturnKOOnPDfEngineBadRequest() { .dueDate("24/10/2024") .subject("subject") .paymentAmount(100L) + .posteDocumentType("0121") + .posteAuth("0232323") .installments(Collections.singletonList( InstallmentData.builder() .amount(100L) .code("codeRate") .dueDate("24/10/2024") + .posteAuth("02323") + .posteAuth("322323") .build() )) .build()) diff --git a/src/test/java/it/gov/pagopa/payment/notice/generator/storage/InstitutionsStorageClientTest.java b/src/test/java/it/gov/pagopa/payment/notice/generator/storage/InstitutionsStorageClientTest.java index 230c23b..da7189b 100644 --- a/src/test/java/it/gov/pagopa/payment/notice/generator/storage/InstitutionsStorageClientTest.java +++ b/src/test/java/it/gov/pagopa/payment/notice/generator/storage/InstitutionsStorageClientTest.java @@ -16,6 +16,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import java.io.File; +import java.io.IOException; import java.time.Duration; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -61,6 +62,17 @@ void shouldReturnException() { assertThrows(AppException.class, () -> institutionsStorageClient.getInstitutionData("testFile")); } + @Test + void shouldReturnExceptionOnIoError() { + doAnswer(item -> { + throw new IOException("test"); + }).when(blobClientMock) + .downloadContent(); + assertThrows(AppException.class, () -> + institutionsStorageClient.getInstitutionData("testFile")); + } + + @Test void shouldReturnExceptionOnMissingClient() { assertThrows(AppException.class, () -> diff --git a/src/test/java/it/gov/pagopa/payment/notice/generator/storage/NoticeTemplateStorageClientTest.java b/src/test/java/it/gov/pagopa/payment/notice/generator/storage/NoticeTemplateStorageClientTest.java index 9062115..88943e1 100644 --- a/src/test/java/it/gov/pagopa/payment/notice/generator/storage/NoticeTemplateStorageClientTest.java +++ b/src/test/java/it/gov/pagopa/payment/notice/generator/storage/NoticeTemplateStorageClientTest.java @@ -49,6 +49,7 @@ void shouldReturnTemplate() { ); File result = noticeTemplateStorageClient.getTemplate("testFile"); assertNotNull(result); + noticeTemplateStorageClient.refreshTemplates(); } @Test From 24f743bc1b257afb0a81bc338a87b1981dad405e Mon Sep 17 00:00:00 2001 From: Alessio Cialini Date: Thu, 9 May 2024 10:45:44 +0200 Subject: [PATCH 17/19] [VAS-834][VAS-973] feat: Updated unit tests --- .../notice/generator/service/NoticeGenerationServiceImpl.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationServiceImpl.java b/src/main/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationServiceImpl.java index 7040f4b..f8e34d1 100644 --- a/src/main/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationServiceImpl.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationServiceImpl.java @@ -93,7 +93,9 @@ public File generateNotice(NoticeGenerationRequestItem noticeGenerationRequestIt String folderId) { if (folderId != null) { - paymentGenerationRequestRepository.findById(folderId) + // Unused value set to avoid sonar scan code gate block + PaymentNoticeGenerationRequest ignored = + paymentGenerationRequestRepository.findById(folderId) .orElseThrow(() -> { throw new AppException(AppError.FOLDER_NOT_AVAILABLE); }); From 7d98ebc6fa5e2e1bc757ed3580c529a51d91264f Mon Sep 17 00:00:00 2001 From: Alessio Cialini Date: Thu, 9 May 2024 16:09:25 +0200 Subject: [PATCH 18/19] [VAS-834][VAS-973] feat: Updated EH model --- .../generator/mapper/TemplateDataMapper.java | 15 +++++++++++++-- .../notice/generator/model/NoticeRequestEH.java | 2 +- .../service/NoticeGenerationServiceImpl.java | 4 +--- .../service/NoticeGenerationServiceImplTest.java | 6 +++--- 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/mapper/TemplateDataMapper.java b/src/main/java/it/gov/pagopa/payment/notice/generator/mapper/TemplateDataMapper.java index 99c33fc..caf6308 100644 --- a/src/main/java/it/gov/pagopa/payment/notice/generator/mapper/TemplateDataMapper.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/mapper/TemplateDataMapper.java @@ -5,7 +5,10 @@ import it.gov.pagopa.payment.notice.generator.model.pdf.notice.*; import org.apache.commons.lang3.StringUtils; +import java.math.BigDecimal; +import java.text.NumberFormat; import java.util.Collections; +import java.util.Locale; /** * Class containing methods to map paymentNotice data to use in notice generation @@ -69,7 +72,7 @@ public static PaymentNotice mapTemplate(NoticeRequestData noticeRequestData) { noticeAmount) ) .subject(noticeRequestData.getNotice().getSubject()) - .amount(noticeAmount) + .amount(currencyFormat(noticeAmount)) .expiryDate(noticeRequestData.getNotice().getDueDate()) .posteDataMatrix(posteAuthCode != null ? generatePosteDataMatrix( @@ -144,7 +147,7 @@ private static Installment mapInstallment( .refNumber(installmentData.getCode()) .cbillCode(cbill) .qrCode(generateQrCode(installmentData.getCode(), ciTaxCode, amount)) - .amount(amount) + .amount(currencyFormat(amount)) .expiryDate(installmentData.getDueDate()) .posteDocumentType(installmentData.getPosteDocumentType()) .posteAuth(installmentData.getPosteAuth()) @@ -164,4 +167,12 @@ private static Installment mapInstallment( .build(); } + private static String currencyFormat(String value) { + BigDecimal valueToFormat = new BigDecimal(value); + NumberFormat numberFormat = NumberFormat.getInstance(Locale.ITALY); + numberFormat.setMaximumFractionDigits(2); + numberFormat.setMinimumFractionDigits(2); + return numberFormat.format(valueToFormat); + } + } diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/model/NoticeRequestEH.java b/src/main/java/it/gov/pagopa/payment/notice/generator/model/NoticeRequestEH.java index 63f30fb..80e03ce 100644 --- a/src/main/java/it/gov/pagopa/payment/notice/generator/model/NoticeRequestEH.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/model/NoticeRequestEH.java @@ -14,6 +14,6 @@ public class NoticeRequestEH { @NotNull private String folderId; - private NoticeGenerationRequestItem noticeGenerationRequestItem; + private NoticeGenerationRequestItem noticeData; } diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationServiceImpl.java b/src/main/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationServiceImpl.java index f8e34d1..ccee71d 100644 --- a/src/main/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationServiceImpl.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationServiceImpl.java @@ -32,9 +32,7 @@ import java.time.Instant; import java.time.LocalDate; import java.time.format.DateTimeFormatter; -import java.util.Optional; -import static it.gov.pagopa.payment.notice.generator.util.WorkingDirectoryUtils.clearTempDirectory; import static it.gov.pagopa.payment.notice.generator.util.WorkingDirectoryUtils.createWorkingDirectory; /** @@ -182,7 +180,7 @@ public void processNoticeGenerationEH(String message) { } folderId = noticeRequestEH.getFolderId(); - noticeGenerationRequestItem = noticeRequestEH.getNoticeGenerationRequestItem(); + noticeGenerationRequestItem = noticeRequestEH.getNoticeData(); } catch (Exception e) { log.error(e.getMessage(), e); diff --git a/src/test/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationServiceImplTest.java b/src/test/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationServiceImplTest.java index 43c09c2..5e8c797 100644 --- a/src/test/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationServiceImplTest.java +++ b/src/test/java/it/gov/pagopa/payment/notice/generator/service/NoticeGenerationServiceImplTest.java @@ -117,7 +117,7 @@ void processNoticeGenerationShouldReturnOkOnValidData() { NoticeRequestEH noticeRequestEH = NoticeRequestEH .builder() .folderId("test") - .noticeGenerationRequestItem(NoticeGenerationRequestItem.builder() + .noticeData(NoticeGenerationRequestItem.builder() .templateId("template") .data(NoticeRequestData.builder() .notice(Notice.builder() @@ -181,7 +181,7 @@ void processNoticeGenerationShouldReturnKOOnPDfEngineBadRequest() { NoticeRequestEH noticeRequestEH = NoticeRequestEH .builder() .folderId("test") - .noticeGenerationRequestItem(NoticeGenerationRequestItem.builder() + .noticeData(NoticeGenerationRequestItem.builder() .templateId("template") .data(NoticeRequestData.builder() .notice(Notice.builder() @@ -234,7 +234,7 @@ void processNoticeGenerationShouldReturnKoOnInvalidData() { NoticeRequestEH noticeRequestEH = NoticeRequestEH .builder() - .noticeGenerationRequestItem(NoticeGenerationRequestItem.builder() + .noticeData(NoticeGenerationRequestItem.builder() .templateId("template") .data(NoticeRequestData.builder() .notice(Notice.builder() From 6985e08a5098ef548b487829280db7f126325e28 Mon Sep 17 00:00:00 2001 From: Alessio Cialini Date: Fri, 10 May 2024 14:37:09 +0200 Subject: [PATCH 19/19] [VAS-834][VAS-973] feat: Updated EH model --- .../controller/NoticeGenerationController.java | 2 +- .../generator/mapper/TemplateDataMapper.java | 6 +++--- .../model/pdf/notice/Installments.java | 18 ++++++++++++++++++ .../generator/model/pdf/notice/Notice.java | 4 +--- 4 files changed, 23 insertions(+), 7 deletions(-) create mode 100644 src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/notice/Installments.java diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/controller/NoticeGenerationController.java b/src/main/java/it/gov/pagopa/payment/notice/generator/controller/NoticeGenerationController.java index 1df7eb4..6b21a0e 100644 --- a/src/main/java/it/gov/pagopa/payment/notice/generator/controller/NoticeGenerationController.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/controller/NoticeGenerationController.java @@ -54,7 +54,7 @@ public ResponseEntity generateNotice( File file = noticeGenerationService.generateNotice(noticeGenerationRequestItem, folderId); try (BufferedInputStream inputStream = new BufferedInputStream(new FileInputStream(file))) { HttpHeaders headers = new HttpHeaders(); - headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + file.getName() + ".pdf\""); + headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + file.getName() + "\""); return ResponseEntity.ok().contentType(MediaType.APPLICATION_OCTET_STREAM) .headers(headers) .body(new ByteArrayResource(inputStream.readAllBytes())); diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/mapper/TemplateDataMapper.java b/src/main/java/it/gov/pagopa/payment/notice/generator/mapper/TemplateDataMapper.java index caf6308..843996f 100644 --- a/src/main/java/it/gov/pagopa/payment/notice/generator/mapper/TemplateDataMapper.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/mapper/TemplateDataMapper.java @@ -36,7 +36,7 @@ public static PaymentNotice mapTemplate(NoticeRequestData noticeRequestData) { return PaymentNotice.builder() .payee(Payee.builder() .taxCode(ciTaxCode) - .name(noticeRequestData.getDebtor().getFullName()) + .name(noticeRequestData.getCreditorInstitution().getFullName()) .channel(Channel.builder().online( Online.builder() .website(noticeRequestData.getCreditorInstitution().getWebChannel()) @@ -85,7 +85,7 @@ public static PaymentNotice mapTemplate(NoticeRequestData noticeRequestData) { noticeAmount, noticeRequestData.getNotice().getPosteDocumentType() ) : null) - .instalments(noticeRequestData.getNotice().getInstallments() != null ? + .instalments(Installments.builder().items(noticeRequestData.getNotice().getInstallments() != null ? noticeRequestData.getNotice().getInstallments().stream().map(item -> mapInstallment( ciTaxCode, @@ -96,7 +96,7 @@ public static PaymentNotice mapTemplate(NoticeRequestData noticeRequestData) { posteAccountNumber, item )).toList() : - Collections.emptyList()) + Collections.emptyList()).build()) .build()) .build(); } diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/notice/Installments.java b/src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/notice/Installments.java new file mode 100644 index 0000000..fa3f35d --- /dev/null +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/notice/Installments.java @@ -0,0 +1,18 @@ +package it.gov.pagopa.payment.notice.generator.model.pdf.notice; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class Installments { + + private List items; + +} diff --git a/src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/notice/Notice.java b/src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/notice/Notice.java index a296fae..0da0666 100644 --- a/src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/notice/Notice.java +++ b/src/main/java/it/gov/pagopa/payment/notice/generator/model/pdf/notice/Notice.java @@ -5,8 +5,6 @@ import lombok.Data; import lombok.NoArgsConstructor; -import java.util.List; - @Data @AllArgsConstructor @NoArgsConstructor @@ -23,6 +21,6 @@ public class Notice { private String posteAuth; private String posteDocumentType; private String posteDataMatrix; - private List instalments; + private Installments instalments; }