From 43fa3b8788d8fc60803f74162da408bd5c783fc0 Mon Sep 17 00:00:00 2001 From: serdimic Date: Tue, 17 Dec 2024 18:45:20 +0100 Subject: [PATCH] P4ADEV-1674 initial draft --- build.gradle.kts | 52 ++- openapi/paCreatePosition.yaml | 302 ++++++++++++++++++ .../config/ApiClientConfig.java | 44 +++ .../config/RestTemplateConfig.java | 30 ++ .../pu/pagopapayments/service/AcaService.java | 93 ++++++ src/main/resources/application.yml | 7 + 6 files changed, 527 insertions(+), 1 deletion(-) create mode 100644 openapi/paCreatePosition.yaml create mode 100644 src/main/java/it/gov/pagopa/pu/pagopapayments/config/ApiClientConfig.java create mode 100644 src/main/java/it/gov/pagopa/pu/pagopapayments/config/RestTemplateConfig.java create mode 100644 src/main/java/it/gov/pagopa/pu/pagopapayments/service/AcaService.java diff --git a/build.gradle.kts b/build.gradle.kts index b9ff333..6656510 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,6 +6,7 @@ plugins { id("org.sonarqube") version "6.0.1.5171" id("com.github.ben-manes.versions") version "0.51.0" id("org.openapi.generator") version "7.10.0" + id("org.ajoberstar.grgit") version "5.3.0" } group = "it.gov.pagopa.payhub" @@ -82,7 +83,7 @@ configurations { } tasks.compileJava { - dependsOn("openApiGenerate") + dependsOn("openApiGenerate","openApiGenerateOrganization","openApiGeneratePaCreatePosition") } configure { @@ -95,6 +96,12 @@ springBoot { mainClass.value("it.gov.pagopa.pu.pagopapayments.PagoPaPaymentsApplication") } +var targetEnv = when (grgit.branch.current().name) { + "uat" -> "uat" + "main" -> "main" + else -> "develop" +} + openApiGenerate { generatorName.set("spring") inputSpec.set("$rootDir/openapi/p4pa-pagopa-payments.openapi.yaml") @@ -112,3 +119,46 @@ openApiGenerate { "additionalModelTypeAnnotations" to "@lombok.Data @lombok.Builder @lombok.AllArgsConstructor" )) } + +tasks.register("openApiGenerateOrganization") { + group = "openapi" + description = "description" + + generatorName.set("java") + remoteInputSpec.set("https://raw.githubusercontent.com/pagopa/p4pa-organization/refs/heads/$targetEnv/openapi/generated.openapi.json") + outputDir.set("$projectDir/build/generated") + apiPackage.set("it.gov.pagopa.pu.p4pa-organization.controller.generated") + modelPackage.set("it.gov.pagopa.pu.p4pa-organization.dto.generated") + configOptions.set(mapOf( + "swaggerAnnotations" to "false", + "openApiNullable" to "false", + "dateLibrary" to "java8", + "useSpringBoot3" to "true", + "useJakartaEe" to "true", + "serializationLibrary" to "jackson", + "generateSupportingFiles" to "true" + )) + library.set("resttemplate") +} + +tasks.register("openApiGeneratePaCreatePosition") { + group = "openapi" + description = "description" + + generatorName.set("java") + inputSpec.set("$rootDir/openapi/paCreatePosition.yaml") + outputDir.set("$projectDir/build/generated") + apiPackage.set("it.gov.pagopa.nodo.paCreatePosition.controller.generated") + modelPackage.set("it.gov.pagopa.nodo.paCreatePosition.dto.generated") + configOptions.set(mapOf( + "swaggerAnnotations" to "false", + "openApiNullable" to "false", + "dateLibrary" to "java8", + "useSpringBoot3" to "true", + "useJakartaEe" to "true", + "serializationLibrary" to "jackson", + "generateSupportingFiles" to "true" + )) + library.set("resttemplate") +} + diff --git a/openapi/paCreatePosition.yaml b/openapi/paCreatePosition.yaml new file mode 100644 index 0000000..2f71ef5 --- /dev/null +++ b/openapi/paCreatePosition.yaml @@ -0,0 +1,302 @@ +openapi: 3.0.3 +info: + version: 0.6.0 + title: Pagopa ACA + description: pagoPA ACA microservice pagoPA ACA microservice contains the api to allow the creation of a new debt position. + contact: + name: pagoPA - Touchpoints team +servers: + - url: https://api.uat.platform.pagopa.it/aca/v1 + description: paCreatePosition Test environment + - url: https://api.platform.pagopa.it/aca/v1 + description: paCreatePosition Prod environment +tags: + - name: ACA + description: API's for performing a debt position census +paths: + /paCreatePosition: + post: + parameters: + - in: query + name: segregationCodes + required: false + schema: + type: string + description: Segregation codes for which broker is authorized + tags: + - ACA + operationId: newDebtPosition + summary: Create a new debt position + description: Create a new debt position. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/NewDebtPositionRequest' + responses: + '200': + description: New debt position successfully created or updated + content: + application/json: + schema: + $ref: '#/components/schemas/DebtPositionResponse' + '400': + description: Formally invalid input + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemJson' + example: + value: + invalidInput: + type: https://example.com/problem/ + title: string + status: 400 + detail: Formally invalid input + '404': + description: Entity not found + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemJson' + examples: + iupd_not_found: + value: + type: https://example.com/problem/ + title: Invocation exception + status: 404 + detail: Error while invalidate debit position. Debit position not found with {iupd} + pa_fiscal_code_not_found: + value: + type: https://example.com/problem/ + title: Invocation exception + status: 404 + detail: No debt position found with Creditor institution code {creditorInstitutionCode} and {iupd} + '422': + description: Can not perform the requested action on debit position + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemJson' + example: + value: + type: https://example.com/problem/ + title: Unprocessable request + status: 422 + detail: Can not perform the requested action on debit position + '409': + description: Conflict into requested action + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemJson' + examples: + create: + value: + type: https://example.com/problem/ + title: Invocation exception + status: 409 + detail: Error while create new debit position conflict into request + update: + value: + type: https://example.com/problem/ + title: Invocation exception + status: 409 + detail: Error while update debit position conflict into request + invalidate: + value: + type: https://example.com/problem/ + title: Invocation exception + status: 409 + detail: Error while invalidate debit position conflict into request + unauthorized_action: + value: + type: https://example.com/problem/ + title: Unauthorized action + status: 409 + detail: Unauthorized action on debt position with iuv {iuv} + '502': + description: Bad gateway + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemJson' + example: + value: + type: https://example.com/problem/ + title: string + status: 502 + detail: Bad gateway, error while execute request + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemJson' + example: + value: + type: https://example.com/problem/ + title: string + status: 500 + detail: Internal server error +components: + schemas: + NewDebtPositionRequest: + type: object + required: + - paFiscalCode + - entityType + - entityFiscalCode + - entityFullName + - iuv + - amount + - description + - expirationDate + description: Request body for creating a new transaction + properties: + paFiscalCode: + type: string + example: "12345678910" + minLength: 11 + maxLength: 11 + entityType: + type: string + example: "G" + enum: + - F + - G + entityFiscalCode: + type: string + example: "12345678910" + minLength: 2 + entityFullName: + type: string + example: "Full Name" + minLength: 1 + maxLength: 255 + iuv: + type: string + example: "00000000000000000" + minLength: 1 + maxLength: 255 + nav: + type: string + example: "30000000000000000" + minLength: 1 + maxLength: 255 + description: |- + notice number, if not specified it will created using the following pattern: 3 + iuv + amount: + $ref: '#/components/schemas/AmountEuroCents' + description: + type: string + example: "ACA Debt Position description" + minLength: 1 + maxLength: 255 + expirationDate: + type: string + format: date-time + iban: + type: string + example: "IT0000000000000000000000000" + minLength: 27 + maxLength: 27 + postalIban: + type: string + example: "IT60X0542811101000000123456" + minLength: 27 + maxLength: 27 + switchToExpired: + type: boolean + example: true + default: false + payStandIn: + type: boolean + example: true + default: true + + DebtPositionResponse: + type: object + properties: + paFiscalCode: + type: string + companyName: + type: string + entityType: + type: string + entityFiscalCode: + type: string + entityFullName: + type: string + iuv: + type: string + nav: + type: string + amount: + type: integer + format: int64 + description: + type: string + expirationDate: + type: string + format: local-date-time + iban: + type: string + postalIban: + type: string + switchToExpired: + type: boolean + status: + type: string + AmountEuroCents: + description: Amount for payments, in eurocents + type: integer + example: 100 + minimum: 0 + maximum: 99999999999 + ProblemJson: + type: object + properties: + type: + type: string + format: uri + description: |- + An absolute URI that identifies the problem type. When dereferenced, + it SHOULD provide human-readable documentation for the problem type + (e.g., using HTML). + default: about:blank + example: https://example.com/problem/ + title: + type: string + description: |- + A short, summary of the problem type. Written in english and readable + for engineers (usually not suited for non technical stakeholders and + not localized); example: Service Unavailable + status: + type: integer + format: int32 + description: |- + The HTTP status code generated by the origin server for this occurrence + of the problem. + minimum: 100 + maximum: 600 + exclusiveMaximum: true + example: 400 + detail: + type: string + description: |- + A human readable explanation specific to this occurrence of the + problem. + instance: + type: string + format: uri + description: |- + An absolute URI that identifies the specific occurrence of the problem. + It may or may not yield further information if dereferenced. + securitySchemes: + ApiKey: + type: apiKey + description: The API key to access this function app. + name: Ocp-Apim-Subscription-Key + in: header diff --git a/src/main/java/it/gov/pagopa/pu/pagopapayments/config/ApiClientConfig.java b/src/main/java/it/gov/pagopa/pu/pagopapayments/config/ApiClientConfig.java new file mode 100644 index 0000000..09a4a0c --- /dev/null +++ b/src/main/java/it/gov/pagopa/pu/pagopapayments/config/ApiClientConfig.java @@ -0,0 +1,44 @@ +package it.gov.pagopa.pu.pagopapayments.config; + +import it.gov.pagopa.pu.p4pa_organization.controller.ApiClient; +import it.gov.pagopa.pu.p4pa_organization.controller.generated.BrokerEntityControllerApi; +import it.gov.pagopa.pu.p4pa_organization.controller.generated.OrganizationEntityControllerApi; +import it.gov.pagopa.pu.p4pa_organization.controller.generated.OrganizationSearchControllerApi; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +@RequiredArgsConstructor +@Slf4j +public class ApiClientConfig { + + @Value("${app.organization.base-url}") + String organizationBaseUrl; + + private final RestTemplate restTemplate; + + @Bean + public OrganizationEntityControllerApi organizationEntityControllerApiClient() { + return new OrganizationEntityControllerApi( + new ApiClient(restTemplate).setBasePath(organizationBaseUrl) + ); + } + + @Bean + public OrganizationSearchControllerApi organizationSearchControllerApi() { + return new OrganizationSearchControllerApi( + new ApiClient(restTemplate).setBasePath(organizationBaseUrl) + ); + } + + @Bean + public BrokerEntityControllerApi brokerEntityControllerApiClient() { + return new BrokerEntityControllerApi( + new ApiClient(restTemplate).setBasePath(organizationBaseUrl) + ); + } +} diff --git a/src/main/java/it/gov/pagopa/pu/pagopapayments/config/RestTemplateConfig.java b/src/main/java/it/gov/pagopa/pu/pagopapayments/config/RestTemplateConfig.java new file mode 100644 index 0000000..2d0bc91 --- /dev/null +++ b/src/main/java/it/gov/pagopa/pu/pagopapayments/config/RestTemplateConfig.java @@ -0,0 +1,30 @@ +package it.gov.pagopa.pu.pagopapayments.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.web.client.RestTemplateBuilderConfigurer; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.time.Duration; + +@Configuration(proxyBeanMethods = false) +public class RestTemplateConfig { + private final int connectTimeoutMillis; + private final int readTimeoutHandlerMillis; + + public RestTemplateConfig( + @Value("${app.rest-client.connect.timeout.millis}") int connectTimeoutMillis, + @Value("${app.rest-client.read.timeout.millis}") int readTimeoutHandlerMillis) { + this.connectTimeoutMillis = connectTimeoutMillis; + this.readTimeoutHandlerMillis = readTimeoutHandlerMillis; + } + + @Bean + public RestTemplateBuilder restTemplateBuilder( + RestTemplateBuilderConfigurer configurer) { + return configurer.configure(new RestTemplateBuilder()) + .connectTimeout(Duration.ofMillis(connectTimeoutMillis)) + .readTimeout(Duration.ofMillis(readTimeoutHandlerMillis)); + } +} diff --git a/src/main/java/it/gov/pagopa/pu/pagopapayments/service/AcaService.java b/src/main/java/it/gov/pagopa/pu/pagopapayments/service/AcaService.java new file mode 100644 index 0000000..ffbd63d --- /dev/null +++ b/src/main/java/it/gov/pagopa/pu/pagopapayments/service/AcaService.java @@ -0,0 +1,93 @@ +package it.gov.pagopa.pu.pagopapayments.service; + +import it.gov.pagopa.nodo.paCreatePosition.controller.ApiClient; +import it.gov.pagopa.nodo.paCreatePosition.controller.generated.AcaApi; +import it.gov.pagopa.nodo.paCreatePosition.dto.generated.DebtPositionResponse; +import it.gov.pagopa.nodo.paCreatePosition.dto.generated.NewDebtPositionRequest; +import it.gov.pagopa.pu.p4pa_organization.controller.generated.BrokerEntityControllerApi; +import it.gov.pagopa.pu.p4pa_organization.controller.generated.OrganizationEntityControllerApi; +import it.gov.pagopa.pu.p4pa_organization.dto.generated.EntityModelBroker; +import it.gov.pagopa.pu.p4pa_organization.dto.generated.EntityModelOrganization; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.client.HttpServerErrorException; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +@Service +@RequiredArgsConstructor +@Slf4j +public class AcaService { + + @Value("${app.aca.base-url}") + private String acaBaseUrl; + + private final BrokerEntityControllerApi brokerEntityApi; + + private final OrganizationEntityControllerApi organizationEntityApi; + + private final RestTemplate restTemplate; + + private ThreadLocal apiClientThreadLocal; + private AcaApi acaApi; + + @PostConstruct + void init(){ + this.apiClientThreadLocal = ThreadLocal.withInitial( () -> new ApiClient(restTemplate).setBasePath(acaBaseUrl)); + this.acaApi = new AcaApi(); + } + + public void create(){ + Long organizationId = 1L; + NewDebtPositionRequest request = new NewDebtPositionRequest() + .nav("301000000000012345") + .amount(1234); + invokeAca(organizationId, request); + } + + public void update(){ + + } + + public void delete(){ + + } + + //TODO implement some caching mechanism for API KEY + private String getApiKeyForOrganization(Long organizationId){ + EntityModelOrganization organization = organizationEntityApi.getItemResourceOrganizationGet(""+organizationId); + EntityModelBroker broker = brokerEntityApi.getItemResourceBrokerGet(""+organization.getBrokerId()); + byte[] encryptedAcaKey = broker.getAcaKey(); + String decryptedAcaKey = "decryptedAcaKey"; //TODO implement decrypt API on broker API + return decryptedAcaKey; + } + + private DebtPositionResponse invokeAca(Long organizationId, NewDebtPositionRequest request){ + String apiKey = getApiKeyForOrganization(organizationId); + ApiClient apiClient = apiClientThreadLocal.get(); + apiClient.setApiKey(apiKey); + acaApi.setApiClient(apiClient); + + long httpCallStart = System.currentTimeMillis(); + try{ + String segregationCodes = StringUtils.substring(request.getNav(),1,3); + DebtPositionResponse response = acaApi.newDebtPosition(request, segregationCodes); + return response; + } catch (HttpServerErrorException he) { + int statusCode = he.getStatusCode().value(); + String body = he.getResponseBodyAsString(); + log.error("HttpServerErrorException on invokeAca - returned code[{}] body[{}]", statusCode, body); + throw he; + } catch (RestClientException e) { + log.error("error on invokeAca[{}]", request, e); + throw e; + } finally { + long elapsed = Math.max(0, System.currentTimeMillis() - httpCallStart); + log.info("elapsed time(ms) for invokeAca: {}", elapsed); + } + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 131e0da..9cdea49 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -16,3 +16,10 @@ management: web: exposure.include: info, health app: + rest-client: + connect.timeout.millis: "\${CONNECT_TIMEOUT_MILLIS:120000}" + read.timeout.millis: "\${READ_TIMEOUT_MILLIS:120000}" + auth: + base-url: "\${AUTH_SERVER_BASE_URL:}" + organization: + base-url: "\${ORGANIZATION_BASE_URL:}"