From 3706aeaafcf9db90408ede39d638cdc75d103a40 Mon Sep 17 00:00:00 2001 From: paologaleotti <45665769+paologaleotti@users.noreply.github.com> Date: Wed, 11 Sep 2024 09:33:08 +0200 Subject: [PATCH] IMN-683 IMN-684 IMN-689 IMN-703 - BFF Agreement enhanceAgreement (#828) --- packages/bff/src/app.ts | 2 +- packages/bff/src/model/api/apiConverter.ts | 36 +- packages/bff/src/model/domain/errors.ts | 11 + packages/bff/src/routers/agreementRouter.ts | 99 +++++- packages/bff/src/services/agreementService.ts | 312 +++++++++++++++++- packages/bff/src/services/attributeService.ts | 13 + packages/bff/src/services/tenantService.ts | 97 +++++- packages/bff/src/utilities/errorMappers.ts | 19 ++ 8 files changed, 569 insertions(+), 20 deletions(-) diff --git a/packages/bff/src/app.ts b/packages/bff/src/app.ts index bb42731bbb..d7bce6af87 100644 --- a/packages/bff/src/app.ts +++ b/packages/bff/src/app.ts @@ -65,7 +65,7 @@ app.use(genericRouter(zodiosCtx)); app.use(catalogRouter(zodiosCtx, clients, fileManager)); app.use(attributeRouter(zodiosCtx, clients)); app.use(purposeRouter(zodiosCtx, clients)); -app.use(agreementRouter(zodiosCtx)); +app.use(agreementRouter(zodiosCtx, clients)); app.use(selfcareRouter(zodiosCtx)); app.use(tenantRouter(zodiosCtx, clients)); app.use(clientRouter(zodiosCtx, clients)); diff --git a/packages/bff/src/model/api/apiConverter.ts b/packages/bff/src/model/api/apiConverter.ts index 193952cfba..7bdd1902e0 100644 --- a/packages/bff/src/model/api/apiConverter.ts +++ b/packages/bff/src/model/api/apiConverter.ts @@ -1,26 +1,27 @@ /* eslint-disable max-params */ + import { DescriptorWithOnlyAttributes, TenantWithOnlyAttributes, } from "pagopa-interop-agreement-lifecycle"; import { - agreementApi, authorizationApi, bffApi, catalogApi, selfcareV2ClientApi, tenantApi, + agreementApi, } from "pagopa-interop-api-clients"; import { match, P } from "ts-pattern"; import { - AttributeId, - CertifiedTenantAttribute, - DeclaredTenantAttribute, EServiceAttribute, + unsafeBrandId, TenantAttribute, + CertifiedTenantAttribute, + AttributeId, tenantAttributeType, - unsafeBrandId, VerifiedTenantAttribute, + DeclaredTenantAttribute, } from "pagopa-interop-models"; import { isAgreementUpgradable } from "../validators.js"; @@ -152,6 +153,31 @@ export function toTenantWithOnlyAttributes( }; } +export function toCompactEservice( + eservice: catalogApi.EService, + producer: tenantApi.Tenant +): bffApi.CompactEService { + return { + id: eservice.id, + name: eservice.name, + producer: { + id: producer.id, + name: producer.name, + kind: producer.kind, + }, + }; +} + +export function toCompactDescriptor( + descriptor: catalogApi.EServiceDescriptor +): bffApi.CompactDescriptor { + return { + id: descriptor.id, + audience: descriptor.audience, + state: descriptor.state, + version: descriptor.version, + }; +} export const toBffApiCompactClient = ( input: authorizationApi.ClientWithKeys ): bffApi.CompactClient => ({ diff --git a/packages/bff/src/model/domain/errors.ts b/packages/bff/src/model/domain/errors.ts index 234bda4e16..4cf4aa2eca 100644 --- a/packages/bff/src/model/domain/errors.ts +++ b/packages/bff/src/model/domain/errors.ts @@ -27,6 +27,7 @@ export const errorCodes = { invalidInterfaceFileDetected: "0019", openapiVersionNotRecognized: "0020", interfaceExtractingInfoError: "0021", + agreementDescriptorNotFound: "0022", }; export type ErrorCodes = keyof typeof errorCodes; @@ -66,6 +67,16 @@ export function purposeNotFound(purposeId: string): ApiError { }); } +export function agreementDescriptorNotFound( + agreementId: string +): ApiError { + return new ApiError({ + detail: `Descriptor of agreement ${agreementId} not found`, + code: "agreementDescriptorNotFound", + title: "Agreement descriptor not found", + }); +} + export function eServiceNotFound(eserviceId: string): ApiError { return new ApiError({ detail: `EService ${eserviceId} not found`, diff --git a/packages/bff/src/routers/agreementRouter.ts b/packages/bff/src/routers/agreementRouter.ts index 76911cbf66..65b6c6ac60 100644 --- a/packages/bff/src/routers/agreementRouter.ts +++ b/packages/bff/src/routers/agreementRouter.ts @@ -1,21 +1,86 @@ import { ZodiosEndpointDefinitions } from "@zodios/core"; import { ZodiosRouter } from "@zodios/express"; +import { bffApi } from "pagopa-interop-api-clients"; import { - ExpressContext, ZodiosContext, + ExpressContext, zodiosValidationErrorToApiProblem, } from "pagopa-interop-commons"; -import { bffApi } from "pagopa-interop-api-clients"; +import { makeApiProblem } from "../model/domain/errors.js"; +import { PagoPAInteropBeClients } from "../providers/clientProvider.js"; +import { fromBffAppContext } from "../utilities/context.js"; +import { + emptyErrorMapper, + getAgreementByIdErrorMapper, + getAgreementsErrorMapper, +} from "../utilities/errorMappers.js"; +import { agreementServiceBuilder } from "../services/agreementService.js"; const agreementRouter = ( - ctx: ZodiosContext + ctx: ZodiosContext, + clients: PagoPAInteropBeClients ): ZodiosRouter => { const agreementRouter = ctx.router(bffApi.agreementsApi.api, { validationErrorHandler: zodiosValidationErrorToApiProblem, }); + + const agreementService = agreementServiceBuilder(clients); + agreementRouter - .get("/agreements", async (_req, res) => res.status(501).send()) - .post("/agreements", async (_req, res) => res.status(501).send()) + .get("/agreements", async (req, res) => { + const ctx = fromBffAppContext(req.ctx, req.headers); + + try { + const { + consumersIds, + eservicesIds, + limit, + offset, + producersIds, + showOnlyUpgradeable, + states, + } = req.query; + + const result = await agreementService.getAgreements( + { + offset, + limit, + producersIds, + eservicesIds, + consumersIds, + states, + showOnlyUpgradeable, + }, + ctx + ); + return res.status(200).json(result).end(); + } catch (error) { + const errorRes = makeApiProblem( + error, + getAgreementsErrorMapper, + ctx.logger, + "Error retrieving agreements" + ); + return res.status(errorRes.status).json(errorRes).end(); + } + }) + + .post("/agreements", async (req, res) => { + const ctx = fromBffAppContext(req.ctx, req.headers); + + try { + const result = await agreementService.createAgreement(req.body, ctx); + return res.status(200).json(result).end(); + } catch (error) { + const errorRes = makeApiProblem( + error, + emptyErrorMapper, + ctx.logger, + `Error creating agreement for EService ${req.body.eserviceId} and Descriptor ${req.body.descriptorId}` + ); + return res.status(errorRes.status).json(errorRes).end(); + } + }) .get("/producers/agreements/eservices", async (_req, res) => res.status(501).send() ) @@ -28,9 +93,27 @@ const agreementRouter = ( .get("/agreements/filter/consumers", async (_req, res) => res.status(501).send() ) - .get("/agreements/:agreementId", async (_req, res) => - res.status(501).send() - ) + + .get("/agreements/:agreementId", async (req, res) => { + const ctx = fromBffAppContext(req.ctx, req.headers); + + try { + const result = await agreementService.getAgreementById( + req.params.agreementId, + ctx + ); + return res.status(200).json(result).end(); + } catch (error) { + const errorRes = makeApiProblem( + error, + getAgreementByIdErrorMapper, + ctx.logger, + `Error retrieving agreement ${req.params.agreementId}` + ); + return res.status(errorRes.status).json(errorRes).end(); + } + }) + .delete("/agreements/:agreementId", async (_req, res) => res.status(501).send() ) diff --git a/packages/bff/src/services/agreementService.ts b/packages/bff/src/services/agreementService.ts index cbbc70fb31..559d20b07a 100644 --- a/packages/bff/src/services/agreementService.ts +++ b/packages/bff/src/services/agreementService.ts @@ -1,9 +1,110 @@ /* eslint-disable functional/immutable-data */ -/* eslint-disable max-params */ -import { agreementApi, catalogApi } from "pagopa-interop-api-clients"; -import { getAllFromPaginated } from "pagopa-interop-commons"; -import { AgreementProcessClient } from "../providers/clientProvider.js"; -import { Headers } from "../utilities/context.js"; +/* eslint-disable @typescript-eslint/explicit-function-return-type */ + +import { + getAllFromPaginated, + toSetToArray, + WithLogger, +} from "pagopa-interop-commons"; +import { + bffApi, + catalogApi, + agreementApi, + tenantApi, + attributeRegistryApi, +} from "pagopa-interop-api-clients"; +import { + AgreementProcessClient, + PagoPAInteropBeClients, +} from "../providers/clientProvider.js"; +import { BffAppContext, Headers } from "../utilities/context.js"; +import { agreementDescriptorNotFound } from "../model/domain/errors.js"; +import { + toCompactEservice, + toCompactDescriptor, +} from "../model/api/apiConverter.js"; +import { isAgreementUpgradable } from "../model/validators.js"; +import { getBulkAttributes } from "./attributeService.js"; +import { enhanceTenantAttributes } from "./tenantService.js"; + +export function agreementServiceBuilder(clients: PagoPAInteropBeClients) { + const { agreementProcessClient } = clients; + return { + async createAgreement( + payload: bffApi.AgreementPayload, + { headers, logger, authData }: WithLogger + ) { + logger.info( + `Creating agreement with consumerId ${authData.organizationId} eserviceId ${payload.eserviceId} descriptorId ${payload.descriptorId}` + ); + return await agreementProcessClient.createAgreement(payload, { + headers, + }); + }, + + async getAgreements( + { + offset, + limit, + producersIds, + eservicesIds, + consumersIds, + states, + showOnlyUpgradeable, + }: { + offset: number; + limit: number; + producersIds: string[]; + eservicesIds: string[]; + consumersIds: string[]; + states: bffApi.AgreementState[]; + showOnlyUpgradeable?: boolean; + }, + ctx: WithLogger + ): Promise { + ctx.logger.info("Retrieving agreements"); + + const { results, totalCount } = + await agreementProcessClient.getAgreements({ + queries: { + offset, + limit, + showOnlyUpgradeable, + eservicesIds, + consumersIds, + producersIds, + states, + }, + headers: ctx.headers, + }); + + const agreements = results.map((a) => + enrichAgreementListEntry(a, clients, ctx) + ); + return { + pagination: { + limit, + offset, + totalCount, + }, + results: await Promise.all(agreements), + }; + }, + + async getAgreementById( + agreementId: string, + ctx: WithLogger + ): Promise { + ctx.logger.info(`Retrieving agreement with id ${agreementId}`); + const agreement = await agreementProcessClient.getAgreementById({ + params: { agreementId }, + headers: ctx.headers, + }); + + return enrichAgreement(agreement, clients, ctx); + }, + }; +} export const getLatestAgreement = async ( agreementProcessClient: AgreementProcessClient, @@ -47,3 +148,204 @@ export const getLatestAgreement = async ( }) .at(0); }; + +async function enrichAgreementListEntry( + agreement: agreementApi.Agreement, + clients: PagoPAInteropBeClients, + ctx: WithLogger +): Promise { + const { consumer, producer, eservice } = await getConsumerProducerEservice( + agreement, + clients, + ctx + ); + + const currentDescriptor = getCurrentDescriptor(eservice, agreement); + + return { + id: agreement.id, + state: agreement.state, + consumer: { + id: consumer.id, + name: consumer.name, + kind: consumer.kind, + }, + eservice: toCompactEservice(eservice, producer), + descriptor: toCompactDescriptor(currentDescriptor), + canBeUpgraded: isAgreementUpgradable(eservice, agreement), + suspendedByConsumer: agreement.suspendedByConsumer, + suspendedByProducer: agreement.suspendedByProducer, + suspendedByPlatform: agreement.suspendedByPlatform, + }; +} + +export async function enrichAgreement( + agreement: agreementApi.Agreement, + clients: PagoPAInteropBeClients, + ctx: WithLogger +): Promise { + const { consumer, producer, eservice } = await getConsumerProducerEservice( + agreement, + clients, + ctx + ); + + const currentDescriptior = getCurrentDescriptor(eservice, agreement); + + const activeDescriptor = eservice.descriptors + .toSorted((a, b) => Number(a.version) - Number(b.version)) + .at(-1); + const activeDescriptorAttributes = activeDescriptor + ? descriptorAttributesIds(activeDescriptor) + : []; + const allAttributesIds = toSetToArray([ + ...activeDescriptorAttributes, + ...tenantAttributesIds(consumer), + ]); + + const attributes = await getBulkAttributes( + allAttributesIds, + clients.attributeProcessClient, + ctx + ); + + const agreementVerifiedAttrs = filterAttributes( + attributes, + agreement.verifiedAttributes.map((attr) => attr.id) + ); + const agreementCertifiedAttrs = filterAttributes( + attributes, + agreement.certifiedAttributes.map((attr) => attr.id) + ); + const agreementDeclaredAttrs = filterAttributes( + attributes, + agreement.declaredAttributes.map((attr) => attr.id) + ); + const tenantAttributes = enhanceTenantAttributes( + consumer.attributes, + attributes + ); + return { + id: agreement.id, + descriptorId: agreement.descriptorId, + producer: { + id: agreement.producerId, + name: producer.name, + kind: producer.kind, + contactMail: producer.mails.find( + (m) => m.kind === tenantApi.MailKind.Values.CONTACT_EMAIL + ), + }, + consumer: { + id: agreement.consumerId, + selfcareId: consumer.selfcareId, + externalId: consumer.externalId, + createdAt: consumer.createdAt, + updatedAt: consumer.updatedAt, + name: consumer.name, + attributes: tenantAttributes, + contactMail: consumer.mails.find( + (m) => m.kind === tenantApi.MailKind.Values.CONTACT_EMAIL + ), + features: consumer.features, + }, + eservice: { + id: agreement.eserviceId, + name: eservice.name, + version: currentDescriptior.version, + activeDescriptor, + }, + state: agreement.state, + verifiedAttributes: agreementVerifiedAttrs, + certifiedAttributes: agreementCertifiedAttrs, + declaredAttributes: agreementDeclaredAttrs, + suspendedByConsumer: agreement.suspendedByConsumer, + suspendedByProducer: agreement.suspendedByProducer, + suspendedByPlatform: agreement.suspendedByPlatform, + isContractPresent: agreement.contract !== undefined, + consumerDocuments: agreement.consumerDocuments, + createdAt: agreement.createdAt, + updatedAt: agreement.updatedAt, + suspendedAt: agreement.suspendedAt, + consumerNotes: agreement.consumerNotes, + rejectionReason: agreement.rejectionReason, + }; +} + +function descriptorAttributesIds( + descriptor: catalogApi.EServiceDescriptor +): string[] { + const { verified, declared, certified } = descriptor.attributes; + const allAttributes = [ + ...verified.flat(), + ...declared.flat(), + ...certified.flat(), + ]; + return allAttributes.map((attr) => attr.id); +} + +function tenantAttributesIds(tenant: tenantApi.Tenant): string[] { + const verifiedIds = tenant.attributes.map((attr) => attr.verified?.id); + const certifiedIds = tenant.attributes.map((attr) => attr.certified?.id); + const declaredIds = tenant.attributes.map((attr) => attr.declared?.id); + + return [...verifiedIds, ...certifiedIds, ...declaredIds].filter( + (x): x is string => x !== undefined + ); +} + +async function getConsumerProducerEservice( + agreement: agreementApi.Agreement, + { tenantProcessClient, catalogProcessClient }: PagoPAInteropBeClients, + { headers }: WithLogger +): Promise<{ + consumer: tenantApi.Tenant; + producer: tenantApi.Tenant; + eservice: catalogApi.EService; +}> { + const consumerTask = tenantProcessClient.tenant.getTenant({ + params: { id: agreement.consumerId }, + headers, + }); + + const producerTask = tenantProcessClient.tenant.getTenant({ + params: { id: agreement.producerId }, + headers, + }); + const eserviceTask = catalogProcessClient.getEServiceById({ + params: { eServiceId: agreement.eserviceId }, + headers, + }); + const [consumer, producer, eservice] = await Promise.all([ + consumerTask, + producerTask, + eserviceTask, + ]); + + return { + consumer, + producer, + eservice, + }; +} + +function filterAttributes( + attributes: attributeRegistryApi.Attribute[], + filterIds: string[] +): attributeRegistryApi.Attribute[] { + return attributes.filter((attr) => filterIds.includes(attr.id)); +} + +export function getCurrentDescriptor( + eservice: catalogApi.EService, + agreement: agreementApi.Agreement +): catalogApi.EServiceDescriptor { + const descriptor = eservice.descriptors.find( + (descriptor) => descriptor.id === agreement.descriptorId + ); + + if (!descriptor) { + throw agreementDescriptorNotFound(agreement.id); + } + return descriptor; +} diff --git a/packages/bff/src/services/attributeService.ts b/packages/bff/src/services/attributeService.ts index 957a735a52..1641aa974e 100644 --- a/packages/bff/src/services/attributeService.ts +++ b/packages/bff/src/services/attributeService.ts @@ -123,3 +123,16 @@ export function attributeServiceBuilder( } export type AttributeService = ReturnType; + +export async function getBulkAttributes( + ids: string[], + attributeProcess: PagoPAInteropBeClients["attributeProcessClient"], + { headers }: WithLogger +): Promise { + return getAllFromPaginated((offset, limit) => + attributeProcess.getBulkedAttributes(ids, { + queries: { offset, limit }, + headers, + }) + ); +} diff --git a/packages/bff/src/services/tenantService.ts b/packages/bff/src/services/tenantService.ts index 3a75ec47ad..d1e9299f81 100644 --- a/packages/bff/src/services/tenantService.ts +++ b/packages/bff/src/services/tenantService.ts @@ -1,4 +1,8 @@ -import { bffApi, tenantApi } from "pagopa-interop-api-clients"; +import { + attributeRegistryApi, + bffApi, + tenantApi, +} from "pagopa-interop-api-clients"; import { isDefined, WithLogger } from "pagopa-interop-commons"; import { AttributeId, TenantId } from "pagopa-interop-models"; import { @@ -376,3 +380,94 @@ export function tenantServiceBuilder( }, }; } + +export function enhanceTenantAttributes( + tenantAttributes: tenantApi.TenantAttribute[], + registryAttributes: attributeRegistryApi.Attribute[] +): bffApi.TenantAttributes { + const registryAttributesMap: Map = new Map( + registryAttributes.map((attribute) => [attribute.id, attribute]) + ); + + const declared = tenantAttributes + .map((attr) => getDeclaredTenantAttribute(attr, registryAttributesMap)) + .filter(isDefined); + + const certified = tenantAttributes + .map((attr) => getCertifiedTenantAttribute(attr, registryAttributesMap)) + .filter(isDefined); + + const verified = tenantAttributes + .map((attr) => toApiVerifiedTenantAttribute(attr, registryAttributesMap)) + .filter(isDefined); + + return { + certified, + declared, + verified, + }; +} + +export function getDeclaredTenantAttribute( + attribute: tenantApi.TenantAttribute, + registryAttributeMap: Map +): bffApi.DeclaredTenantAttribute | undefined { + if (!attribute.declared) { + return undefined; + } + const registryAttribute = registryAttributeMap.get(attribute.declared.id); + if (!registryAttribute) { + return undefined; + } + + return { + id: attribute.declared.id, + name: registryAttribute.name, + description: registryAttribute.description, + assignmentTimestamp: attribute.declared.assignmentTimestamp, + revocationTimestamp: attribute.declared.revocationTimestamp, + }; +} + +export function getCertifiedTenantAttribute( + attribute: tenantApi.TenantAttribute, + registryAttributeMap: Map +): bffApi.CertifiedTenantAttribute | undefined { + if (!attribute.certified) { + return undefined; + } + const registryAttribute = registryAttributeMap.get(attribute.certified.id); + if (!registryAttribute) { + return undefined; + } + + return { + id: attribute.certified.id, + name: registryAttribute.name, + description: registryAttribute.description, + assignmentTimestamp: attribute.certified.assignmentTimestamp, + revocationTimestamp: attribute.certified.revocationTimestamp, + }; +} + +export function toApiVerifiedTenantAttribute( + attribute: tenantApi.TenantAttribute, + registryAttributeMap: Map +): bffApi.VerifiedTenantAttribute | undefined { + if (!attribute.verified) { + return undefined; + } + const registryAttribute = registryAttributeMap.get(attribute.verified.id); + if (!registryAttribute) { + return undefined; + } + + return { + id: attribute.verified.id, + name: registryAttribute.name, + description: registryAttribute.description, + assignmentTimestamp: attribute.verified.assignmentTimestamp, + verifiedBy: attribute.verified.verifiedBy, + revokedBy: attribute.verified.revokedBy, + }; +} diff --git a/packages/bff/src/utilities/errorMappers.ts b/packages/bff/src/utilities/errorMappers.ts index 9789840e4a..2e2e158044 100644 --- a/packages/bff/src/utilities/errorMappers.ts +++ b/packages/bff/src/utilities/errorMappers.ts @@ -19,6 +19,7 @@ export const bffGetCatalogErrorMapper = (error: ApiError): number => .with( "descriptorNotFound", "eserviceRiskNotFound", + "eserviceDescriptorNotFound", () => HTTP_STATUS_NOT_FOUND ) .with("invalidEserviceRequester", () => HTTP_STATUS_FORBIDDEN) @@ -80,6 +81,24 @@ export const sessionTokenErrorMapper = (error: ApiError): number => .with("tooManyRequestsError", () => HTTP_STATUS_TOO_MANY_REQUESTS) .otherwise(() => HTTP_STATUS_INTERNAL_SERVER_ERROR); +export const getAgreementsErrorMapper = (error: ApiError): number => + match(error.code) + .with( + "agreementDescriptorNotFound", + () => HTTP_STATUS_INTERNAL_SERVER_ERROR + ) + .otherwise(() => HTTP_STATUS_INTERNAL_SERVER_ERROR); + +export const getAgreementByIdErrorMapper = ( + error: ApiError +): number => + match(error.code) + .with( + "agreementDescriptorNotFound", + () => HTTP_STATUS_INTERNAL_SERVER_ERROR + ) + .otherwise(() => HTTP_STATUS_INTERNAL_SERVER_ERROR); + export const getClientUsersErrorMapper = ( error: ApiError ): number =>