From 3b56ea4d063d568341c7c8c90b1e89183b97c71e Mon Sep 17 00:00:00 2001 From: Daniele Manni Date: Mon, 22 Nov 2021 10:32:41 +0100 Subject: [PATCH 01/12] [#IC-49] Porting SpecialServices to new ProcessMessage flow --- CreateMessage/handler.ts | 1 + CreateService/__tests__/handler.test.ts | 3 + CreateService/handler.ts | 3 + EmailNotification/__tests__/handler.test.ts | 2 + ProcessMessage/__tests__/handler.test.ts | 186 ++++++++++++++---- ProcessMessage/handler.ts | 48 +++++ ProcessMessage/index.ts | 9 + UpdateService/__tests__/handler.test.ts | 3 + UpdateService/handler.ts | 25 ++- WebhookNotification/__tests__/handler.test.ts | 2 + __mocks__/mocks.ts | 16 +- openapi/index.yaml | 52 ++++- openapi/index.yaml.template | 10 +- package.json | 4 +- yarn.lock | 29 ++- 15 files changed, 329 insertions(+), 64 deletions(-) diff --git a/CreateMessage/handler.ts b/CreateMessage/handler.ts index a26a6e04..94f60993 100644 --- a/CreateMessage/handler.ts +++ b/CreateMessage/handler.ts @@ -416,6 +416,7 @@ export function CreateMessageHandler( organizationFiscalCode: service.organizationFiscalCode, organizationName: service.organizationName, requireSecureChannels: service.requireSecureChannels, + serviceCategory: service.serviceMetadata.category, serviceName: service.serviceName, serviceUserEmail } diff --git a/CreateService/__tests__/handler.test.ts b/CreateService/__tests__/handler.test.ts index fdb03e86..25ab2ab6 100644 --- a/CreateService/__tests__/handler.test.ts +++ b/CreateService/__tests__/handler.test.ts @@ -28,6 +28,7 @@ import { ServicePayload } from "../../generated/definitions/ServicePayload"; import { CreateServiceHandler } from "../handler"; import * as E from "fp-ts/lib/Either"; +import { StandardServiceCategoryEnum } from "@pagopa/io-functions-commons/dist/generated/definitions/StandardServiceCategory"; const mockContext = { // eslint-disable no-console @@ -46,6 +47,8 @@ const aTokenName = "TOKEN_NAME" as NonEmptyString; const someServicesMetadata: ServiceMetadata = { scope: ServiceScopeEnum.NATIONAL, + category: StandardServiceCategoryEnum.STANDARD, + customSpecialFlow: undefined, tokenName: aTokenName }; diff --git a/CreateService/handler.ts b/CreateService/handler.ts index 4c766a6f..9d03aa5c 100644 --- a/CreateService/handler.ts +++ b/CreateService/handler.ts @@ -59,6 +59,7 @@ import { ServiceWithSubscriptionKeys } from "../generated/definitions/ServiceWit import { withApiRequestWrapper } from "../utils/api"; import { getLogger, ILogger } from "../utils/logging"; import { ErrorResponses, IResponseErrorUnauthorized } from "../utils/responses"; +import { StandardServiceCategoryEnum } from "../generated/api-admin/StandardServiceCategory"; type ResponseTypes = | IResponseSuccessJson @@ -137,6 +138,8 @@ const createServiceTask = ( service_id: subscriptionId, service_metadata: { ...servicePayload.service_metadata, + // Only Admins can create SPECIAL Services + category: StandardServiceCategoryEnum.STANDARD, token_name: adb2cTokenName } } diff --git a/EmailNotification/__tests__/handler.test.ts b/EmailNotification/__tests__/handler.test.ts index 058c3a5f..579b79d0 100644 --- a/EmailNotification/__tests__/handler.test.ts +++ b/EmailNotification/__tests__/handler.test.ts @@ -33,6 +33,7 @@ import { NotificationModel, RetrievedNotification } from "@pagopa/io-functions-commons/dist/src/models/notification"; +import { StandardServiceCategoryEnum } from "../../generated/api-admin/StandardServiceCategory"; beforeEach(() => jest.clearAllMocks()); @@ -104,6 +105,7 @@ const aSenderMetadata: CreatedMessageEventSenderMetadata = { organizationFiscalCode: anOrganizationFiscalCode, organizationName: "AgID" as NonEmptyString, requireSecureChannels: false, + serviceCategory: StandardServiceCategoryEnum.STANDARD, serviceName: "Test" as NonEmptyString, serviceUserEmail: "email@example.com" as EmailAddress }; diff --git a/ProcessMessage/__tests__/handler.test.ts b/ProcessMessage/__tests__/handler.test.ts index 718eba8e..d76a6211 100644 --- a/ProcessMessage/__tests__/handler.test.ts +++ b/ProcessMessage/__tests__/handler.test.ts @@ -7,7 +7,10 @@ import { RetrievedProfile } from "@pagopa/io-functions-commons/dist/src/models/profile"; import { ServicesPreferencesModel } from "@pagopa/io-functions-commons/dist/src/models/service_preference"; -import { NonNegativeNumber } from "@pagopa/ts-commons/lib/numbers"; +import { + NonNegativeInteger, + NonNegativeNumber +} from "@pagopa/ts-commons/lib/numbers"; import * as E from "fp-ts/lib/Either"; import * as TE from "fp-ts/lib/TaskEither"; @@ -17,12 +20,14 @@ import { initTelemetryClient } from "../../utils/appinsights"; import { aCreatedMessageEventSenderMetadata, aDisabledServicePreference, + aFiscalCode, aMessageContent, anEnabledServicePreference, aNewMessageWithoutContent, aRetrievedMessage, aRetrievedProfile, aRetrievedServicePreference, + aServiceId, autoProfileServicePreferencesSettings, legacyProfileServicePreferencesSettings, manualProfileServicePreferencesSettings @@ -38,8 +43,16 @@ import { pipe } from "fp-ts/lib/function"; import { readableReport } from "@pagopa/ts-commons/lib/reporters"; import { ProcessedMessageEvent, - CreatedMessageEvent + CreatedMessageEvent, + CommonMessageData } from "../../utils/events/message"; +import { + ActivationModel, + RetrievedActivation +} from "@pagopa/io-functions-commons/dist/src/models/activation"; +import { StandardServiceCategoryEnum } from "@pagopa/io-functions-commons/dist/generated/definitions/StandardServiceCategory"; +import { ActivationStatusEnum } from "@pagopa/io-functions-commons/dist/generated/definitions/ActivationStatus"; +import { SpecialServiceCategoryEnum } from "@pagopa/io-functions-commons/dist/generated/definitions/SpecialServiceCategory"; const createContext = (): Context => (({ @@ -82,6 +95,11 @@ const lMessageStatusModel = ({ upsert: (...args) => TE.of({} /* anything */) } as unknown) as MessageStatusModel; +const activationFindLastVersionMock = jest.fn(); +const lActivation = ({ + findLastVersionByModelId: activationFindLastVersionMock +} as unknown) as ActivationModel; + const lastUpdateTimestamp = Math.floor(new Date().getTime() / 1000); const aFutureOptOutEmailSwitchDate = new Date(lastUpdateTimestamp + 10); @@ -106,6 +124,7 @@ const aCreatedMessageEvent: CreatedMessageEvent = { messageId: aNewMessageWithoutContent.id, serviceVersion: 1 as NonNegativeNumber }; + const aMessageContentWithPaymentData = { ...aMessageContent, payment_data: aPaymentData @@ -158,15 +177,40 @@ const withBlockedEmail = (profile: RetrievedProfile, services = []) => ({ ) }); -const aCommonMessageData = { +const aCommonMessageData: CommonMessageData = { content: aMessageContent, message: aNewMessageWithoutContent, senderMetadata: aCreatedMessageEventSenderMetadata }; +const aSpecialMessageData: CommonMessageData = { + ...aCommonMessageData, + senderMetadata: { + ...aCommonMessageData.senderMetadata, + serviceCategory: SpecialServiceCategoryEnum.SPECIAL + } +}; const mockRetrieveProcessingMessageData = jest.fn(() => TE.of(O.some(aCommonMessageData)) ); +const aDisabledActivation: RetrievedActivation = { + _etag: "a", + _rid: "a", + _self: "self", + _ts: 0, + fiscalCode: aFiscalCode, + serviceId: aServiceId, + kind: "IRetrievedActivation", + status: ActivationStatusEnum.INACTIVE, + version: 0 as NonNegativeInteger, + id: "fake-id" as NonEmptyString +}; + +const anActiveActivation: RetrievedActivation = { + ...aDisabledActivation, + status: ActivationStatusEnum.ACTIVE +}; + beforeEach(() => { // we usually prefer clearAllMocks, but tests scenarios are somehow entangled // we should refactor them to have them independent, however for now we keep the workaround @@ -178,18 +222,22 @@ beforeEach(() => { describe("getprocessMessageHandler", () => { it.each` - scenario | profileResult | storageResult | upsertResult | preferenceResult | messageEvent | expectedBIOC | optOutEmailSwitchDate | optInEmailEnabled | overrideProfileResult - ${"a retrieved profile maintaining its original isEmailEnabled property"} | ${aRetrievedProfileWithAValidTimestamp} | ${aBlobResult} | ${aRetrievedMessage} | ${O.some(aRetrievedServicePreference)} | ${aCreatedMessageEvent} | ${[]} | ${aPastOptOutEmailSwitchDate} | ${false} | ${"O.none"} - ${"retrieved profile with isEmailEnabled to false"} | ${{ ...aRetrievedProfile, isEmailEnabled: false }} | ${aBlobResult} | ${aRetrievedMessage} | ${O.some(aRetrievedServicePreference)} | ${aCreatedMessageEvent} | ${[]} | ${aPastOptOutEmailSwitchDate} | ${false} | ${"O.none"} - ${"empty blockedInboxOrChannels if message sender service does not exists in user service preference (AUTO SETTINGS)"} | ${withBlacklist(aRetrievedProfileWithAutoPreferences, [aNewMessageWithoutContent.senderServiceId])} | ${aBlobResult} | ${aRetrievedMessage} | ${O.none} | ${aCreatedMessageEvent} | ${[]} | ${aPastOptOutEmailSwitchDate} | ${false} | ${"O.none"} - ${"empty blockedInboxOrChannels if message sender service exists and is enabled in user service preference (AUTO SETTINGS)"} | ${aRetrievedProfileWithAutoPreferences} | ${aBlobResult} | ${aRetrievedMessage} | ${O.some(anEnabledServicePreference)} | ${aCreatedMessageEvent} | ${[]} | ${aPastOptOutEmailSwitchDate} | ${false} | ${"O.none"} - ${"a blocked EMAIL if sender service exists and has EMAIL disabled in user service preference (AUTO SETTINGS)"} | ${withBlacklist(aRetrievedProfileWithAutoPreferences, [aNewMessageWithoutContent.senderServiceId])} | ${aBlobResult} | ${aRetrievedMessage} | ${O.some({ ...anEnabledServicePreference, isEmailEnabled: false })} | ${aCreatedMessageEvent} | ${[BlockedInboxOrChannelEnum.EMAIL]} | ${aPastOptOutEmailSwitchDate} | ${false} | ${"O.none"} - ${"empty blockedInboxOrChannels if message sender service exists and is enabled in user service preference (MANUAL SETTINGS)"} | ${aRetrievedProfileWithManualPreferences} | ${aBlobResult} | ${aRetrievedMessage} | ${O.some(anEnabledServicePreference)} | ${aCreatedMessageEvent} | ${[]} | ${aPastOptOutEmailSwitchDate} | ${false} | ${"O.none"} - ${"blocked EMAIL if message sender service exists and has EMAIL disabled in user service preference (MANUAL SETTINGS)"} | ${aRetrievedProfileWithAutoPreferences} | ${aBlobResult} | ${aRetrievedMessage} | ${O.some({ ...anEnabledServicePreference, isEmailEnabled: false })} | ${aCreatedMessageEvent} | ${[BlockedInboxOrChannelEnum.EMAIL]} | ${aPastOptOutEmailSwitchDate} | ${false} | ${"O.none"} - ${"blocked EMAIL for a service in blockedInboxOrChannels with email disabled (LEGACY SETTINGS)"} | ${withBlockedEmail(aRetrievedProfileWithLegacyPreferences, [aNewMessageWithoutContent.senderServiceId])} | ${aBlobResult} | ${aRetrievedMessage} | ${"not-called"} | ${aCreatedMessageEvent} | ${[BlockedInboxOrChannelEnum.EMAIL]} | ${aPastOptOutEmailSwitchDate} | ${false} | ${"O.none"} - ${"empty blockedInboxOrChannels if the service is not in user's blockedInboxOrChannels (LEGACY SETTINGS)"} | ${withBlacklist(aRetrievedProfileWithLegacyPreferences, ["another-service"])} | ${aBlobResult} | ${aRetrievedMessage} | ${"not-called"} | ${aCreatedMessageEvent} | ${[]} | ${aPastOptOutEmailSwitchDate} | ${false} | ${"O.none"} - ${"isEmailEnabled overridden to false if profile's timestamp is before optOutEmailSwitchDate"} | ${aRetrievedProfileWithAValidTimestamp} | ${aBlobResult} | ${aRetrievedMessage} | ${"not-called"} | ${aCreatedMessageEvent} | ${[]} | ${aFutureOptOutEmailSwitchDate} | ${true} | ${{ ...aRetrievedProfileWithAValidTimestamp, isEmailEnabled: false }} - ${"isEmailEnabled not overridden if profile's timestamp is after optOutEmailSwitchDate"} | ${aRetrievedProfileWithAValidTimestamp} | ${aBlobResult} | ${aRetrievedMessage} | ${"not-called"} | ${aCreatedMessageEvent} | ${[]} | ${aPastOptOutEmailSwitchDate} | ${true} | ${"O.none"} + scenario | profileResult | storageResult | upsertResult | preferenceResult | activationResult | messageEvent | messageData | expectedBIOC | optOutEmailSwitchDate | optInEmailEnabled | overrideProfileResult + ${"a retrieved profile mantaining its original isEmailEnabled property"} | ${aRetrievedProfileWithAValidTimestamp} | ${aBlobResult} | ${aRetrievedMessage} | ${O.some(aRetrievedServicePreference)} | ${"not-called"} | ${aCreatedMessageEvent} | ${aCommonMessageData} | ${[]} | ${aPastOptOutEmailSwitchDate} | ${false} | ${"O.none"} + ${"retrieved profile with isEmailEnabled to false"} | ${{ ...aRetrievedProfile, isEmailEnabled: false }} | ${aBlobResult} | ${aRetrievedMessage} | ${O.some(aRetrievedServicePreference)} | ${"not-called"} | ${aCreatedMessageEvent} | ${aCommonMessageData} | ${[]} | ${aPastOptOutEmailSwitchDate} | ${false} | ${"O.none"} + ${"empty blockedInboxOrChannels if message sender service does not exists in user service preference (AUTO SETTINGS)"} | ${withBlacklist(aRetrievedProfileWithAutoPreferences, [aNewMessageWithoutContent.senderServiceId])} | ${aBlobResult} | ${aRetrievedMessage} | ${O.none} | ${"not-called"} | ${aCreatedMessageEvent} | ${aCommonMessageData} | ${[]} | ${aPastOptOutEmailSwitchDate} | ${false} | ${"O.none"} + ${"empty blockedInboxOrChannels if message sender service exists and is enabled in user service preference (AUTO SETTINGS)"} | ${aRetrievedProfileWithAutoPreferences} | ${aBlobResult} | ${aRetrievedMessage} | ${O.some(anEnabledServicePreference)} | ${"not-called"} | ${aCreatedMessageEvent} | ${aCommonMessageData} | ${[]} | ${aPastOptOutEmailSwitchDate} | ${false} | ${"O.none"} + ${"a blocked EMAIL if sender service exists and has EMAIL disabled in user service preference (AUTO SETTINGS)"} | ${withBlacklist(aRetrievedProfileWithAutoPreferences, [aNewMessageWithoutContent.senderServiceId])} | ${aBlobResult} | ${aRetrievedMessage} | ${O.some({ ...anEnabledServicePreference, isEmailEnabled: false })} | ${"not-called"} | ${aCreatedMessageEvent} | ${aCommonMessageData} | ${[BlockedInboxOrChannelEnum.EMAIL]} | ${aPastOptOutEmailSwitchDate} | ${false} | ${"O.none"} + ${"empty blockedInboxOrChannels if message sender service exists and is enabled in user service preference (MANUAL SETTINGS)"} | ${aRetrievedProfileWithManualPreferences} | ${aBlobResult} | ${aRetrievedMessage} | ${O.some(anEnabledServicePreference)} | ${"not-called"} | ${aCreatedMessageEvent} | ${aCommonMessageData} | ${[]} | ${aPastOptOutEmailSwitchDate} | ${false} | ${"O.none"} + ${"blocked EMAIL if message sender service exists and has EMAIL disabled in user service preference (MANUAL SETTINGS)"} | ${aRetrievedProfileWithAutoPreferences} | ${aBlobResult} | ${aRetrievedMessage} | ${O.some({ ...anEnabledServicePreference, isEmailEnabled: false })} | ${"not-called"} | ${aCreatedMessageEvent} | ${aCommonMessageData} | ${[BlockedInboxOrChannelEnum.EMAIL]} | ${aPastOptOutEmailSwitchDate} | ${false} | ${"O.none"} + ${"blocked EMAIL for a service in blockedInboxOrChannels with email disabled (LEGACY SETTINGS)"} | ${withBlockedEmail(aRetrievedProfileWithLegacyPreferences, [aNewMessageWithoutContent.senderServiceId])} | ${aBlobResult} | ${aRetrievedMessage} | ${"not-called"} | ${"not-called"} | ${aCreatedMessageEvent} | ${aCommonMessageData} | ${[BlockedInboxOrChannelEnum.EMAIL]} | ${aPastOptOutEmailSwitchDate} | ${false} | ${"O.none"} + ${"empty blockedInboxOrChannels if the service is not in user's blockedInboxOrChannels (LEGACY SETTINGS)"} | ${withBlacklist(aRetrievedProfileWithLegacyPreferences, ["another-service"])} | ${aBlobResult} | ${aRetrievedMessage} | ${"not-called"} | ${"not-called"} | ${aCreatedMessageEvent} | ${aCommonMessageData} | ${[]} | ${aPastOptOutEmailSwitchDate} | ${false} | ${"O.none"} + ${"isEmailEnabled overridden to false if profile's timestamp is before optOutEmailSwitchDate"} | ${aRetrievedProfileWithAValidTimestamp} | ${aBlobResult} | ${aRetrievedMessage} | ${"not-called"} | ${"not-called"} | ${aCreatedMessageEvent} | ${aCommonMessageData} | ${[]} | ${aFutureOptOutEmailSwitchDate} | ${true} | ${{ ...aRetrievedProfileWithAValidTimestamp, isEmailEnabled: false }} + ${"isEmailEnabled not overridden if profile's timestamp is after optOutEmailSwitchDate"} | ${aRetrievedProfileWithAValidTimestamp} | ${aBlobResult} | ${aRetrievedMessage} | ${"not-called"} | ${"not-called"} | ${aCreatedMessageEvent} | ${aCommonMessageData} | ${[]} | ${aPastOptOutEmailSwitchDate} | ${true} | ${"O.none"} + ${"empty blockedInboxOrChannels if message sender special service exists and is enabled in user service preference (AUTO SETTINGS)"} | ${aRetrievedProfileWithAutoPreferences} | ${aBlobResult} | ${aRetrievedMessage} | ${O.some(anEnabledServicePreference)} | ${O.some(anActiveActivation)} | ${aCreatedMessageEvent} | ${aSpecialMessageData} | ${[]} | ${aPastOptOutEmailSwitchDate} | ${false} | ${"O.none"} + ${"a blocked EMAIL if sender special service exists and has EMAIL disabled in user service preference (AUTO SETTINGS)"} | ${withBlacklist(aRetrievedProfileWithAutoPreferences, [aNewMessageWithoutContent.senderServiceId])} | ${aBlobResult} | ${aRetrievedMessage} | ${O.some({ ...anEnabledServicePreference, isEmailEnabled: false })} | ${O.some(anActiveActivation)} | ${aCreatedMessageEvent} | ${aSpecialMessageData} | ${[BlockedInboxOrChannelEnum.EMAIL]} | ${aPastOptOutEmailSwitchDate} | ${false} | ${"O.none"} + ${"empty blockedInboxOrChannels if message sender special service exists and is enabled in user service preference (MANUAL SETTINGS)"} | ${aRetrievedProfileWithManualPreferences} | ${aBlobResult} | ${aRetrievedMessage} | ${O.some(anEnabledServicePreference)} | ${O.some(anActiveActivation)} | ${aCreatedMessageEvent} | ${aSpecialMessageData} | ${[]} | ${aPastOptOutEmailSwitchDate} | ${false} | ${"O.none"} + ${"blocked EMAIL if message sender special service exists and has EMAIL disabled in user service preference (MANUAL SETTINGS)"} | ${aRetrievedProfileWithAutoPreferences} | ${aBlobResult} | ${aRetrievedMessage} | ${O.some({ ...anEnabledServicePreference, isEmailEnabled: false })} | ${O.some(anActiveActivation)} | ${aCreatedMessageEvent} | ${aSpecialMessageData} | ${[BlockedInboxOrChannelEnum.EMAIL]} | ${aPastOptOutEmailSwitchDate} | ${false} | ${"O.none"} `( "should succeed with $scenario", async ({ @@ -197,14 +245,17 @@ describe("getprocessMessageHandler", () => { storageResult, upsertResult, preferenceResult, + activationResult, messageEvent, + messageData, expectedBIOC, optOutEmailSwitchDate, optInEmailEnabled, overrideProfileResult, // mock implementation must be set only if we expect the function to be called, otherwise it will interfere with other tests // we use "not-called" to determine such - skipPreferenceMock = preferenceResult === "not-called" + skipPreferenceMock = preferenceResult === "not-called", + skipActivationMock = activationResult === "not-called" }) => { findLastVersionByModelIdMock.mockImplementationOnce(() => TE.of(O.some(profileResult)) @@ -219,8 +270,16 @@ describe("getprocessMessageHandler", () => { findServicePreferenceMock.mockImplementationOnce(() => TE.of(preferenceResult) ); + !skipActivationMock && + activationFindLastVersionMock.mockImplementationOnce(() => + TE.of(activationResult) + ); + mockRetrieveProcessingMessageData.mockImplementationOnce(() => + TE.of(O.some(messageData)) + ); const processMessageHandler = getProcessMessageHandler({ + lActivation, lProfileModel, lMessageModel, lBlobService: {} as any, @@ -255,14 +314,17 @@ describe("getprocessMessageHandler", () => { // success means message has been stored and status has been updated expect(upsertMessageMock).toHaveBeenCalledTimes(1); expect(storeContentAsBlobMock).toHaveBeenCalledTimes(1); + expect(activationFindLastVersionMock).toBeCalledTimes( + skipActivationMock ? 0 : 1 + ); } ); it.each` - scenario | preferenceResult | messageEvent | messageData | optOutEmailSwitchDate | optInEmailEnabled | expectedMessagePaymentData | profileResult | storageResult | upsertResult - ${"with original payment message with payee"} | ${O.some(aRetrievedServicePreference)} | ${aCreatedMessageEvent} | ${{ ...aCommonMessageData, content: aMessageContentWithPaymentDataWithPayee }} | ${aPastOptOutEmailSwitchDate} | ${false} | ${aPaymentDataWithPayee} | ${aRetrievedProfileWithAValidTimestamp} | ${aBlobResult} | ${aRetrievedMessage} - ${"with overridden payee if no payee is provided"} | ${O.some(aRetrievedServicePreference)} | ${aCreatedMessageEvent} | ${{ ...aCommonMessageData, content: aMessageContentWithPaymentData }} | ${aPastOptOutEmailSwitchDate} | ${false} | ${{ ...aPaymentData, payee: { fiscal_code: aCreatedMessageEventSenderMetadata.organizationFiscalCode } }} | ${aRetrievedProfileWithAValidTimestamp} | ${aBlobResult} | ${aRetrievedMessage} - ${"with a no payment message"} | ${O.none} | ${aCreatedMessageEvent} | ${aCommonMessageData} | ${aPastOptOutEmailSwitchDate} | ${false} | ${undefined} | ${aRetrievedProfileWithAValidTimestamp} | ${aBlobResult} | ${aRetrievedMessage} + scenario | preferenceResult | activationResult | messageEvent | messageData | optOutEmailSwitchDate | optInEmailEnabled | expectedMessagePaymentData | profileResult | storageResult | upsertResult + ${"with original payment message with payee"} | ${O.some(aRetrievedServicePreference)} | ${"not-called"} | ${aCreatedMessageEvent} | ${{ ...aCommonMessageData, content: aMessageContentWithPaymentDataWithPayee }} | ${aPastOptOutEmailSwitchDate} | ${false} | ${aPaymentDataWithPayee} | ${aRetrievedProfileWithAValidTimestamp} | ${aBlobResult} | ${aRetrievedMessage} + ${"with overridden payee if no payee is provided"} | ${O.some(aRetrievedServicePreference)} | ${"not-called"} | ${aCreatedMessageEvent} | ${{ ...aCommonMessageData, content: aMessageContentWithPaymentData }} | ${aPastOptOutEmailSwitchDate} | ${false} | ${{ ...aPaymentData, payee: { fiscal_code: aCreatedMessageEventSenderMetadata.organizationFiscalCode } }} | ${aRetrievedProfileWithAValidTimestamp} | ${aBlobResult} | ${aRetrievedMessage} + ${"with a no payment message"} | ${O.none} | ${"not-called"} | ${aCreatedMessageEvent} | ${aCommonMessageData} | ${aPastOptOutEmailSwitchDate} | ${false} | ${undefined} | ${aRetrievedProfileWithAValidTimestamp} | ${aBlobResult} | ${aRetrievedMessage} `( "should succeed with $scenario", async ({ @@ -270,6 +332,7 @@ describe("getprocessMessageHandler", () => { storageResult, upsertResult, preferenceResult, + activationResult, messageData, messageEvent, optOutEmailSwitchDate, @@ -277,7 +340,8 @@ describe("getprocessMessageHandler", () => { expectedMessagePaymentData, // mock implementation must be set only if we expect the function to be called, otherwise it will interfere with other tests // we use "not-called" to determine such - skipPreferenceMock = preferenceResult === "not-called" + skipPreferenceMock = preferenceResult === "not-called", + skipActivationMock = activationResult === "not-called" }) => { findLastVersionByModelIdMock.mockImplementationOnce(() => TE.of(O.some(profileResult)) @@ -292,12 +356,17 @@ describe("getprocessMessageHandler", () => { findServicePreferenceMock.mockImplementationOnce(() => TE.of(preferenceResult) ); + !skipActivationMock && + activationFindLastVersionMock.mockImplementationOnce(() => + TE.of(activationResult) + ); mockRetrieveProcessingMessageData.mockImplementationOnce(() => TE.of(O.some(messageData)) ); const processMessageHandler = getProcessMessageHandler({ + lActivation, lProfileModel, lMessageModel, lBlobService: {} as any, @@ -333,26 +402,35 @@ describe("getprocessMessageHandler", () => { ); it.each` - scenario | failureReason | profileResult | preferenceResult | messageEvent - ${"no profile was found"} | ${"PROFILE_NOT_FOUND"} | ${O.none} | ${"not-called"} | ${aCreatedMessageEvent} - ${"inbox is not enabled"} | ${"MASTER_INBOX_DISABLED"} | ${O.some({ ...aRetrievedProfile, isInboxEnabled: false })} | ${"not-called"} | ${aCreatedMessageEvent} - ${"message sender is blocked"} | ${"SENDER_BLOCKED"} | ${O.some(withBlacklist(aRetrievedProfile, [aNewMessageWithoutContent.senderServiceId]))} | ${"not-called"} | ${aCreatedMessageEvent} - ${"message sender service exists and is not enabled in user service preference (AUTO SETTINGS)"} | ${"SENDER_BLOCKED"} | ${O.some(aRetrievedProfileWithAutoPreferences)} | ${O.some(aDisabledServicePreference)} | ${aCreatedMessageEvent} - ${"message sender service exists and has INBOX disabled in user service preference (AUTO SETTINGS)"} | ${"SENDER_BLOCKED"} | ${O.some(aRetrievedProfileWithAutoPreferences)} | ${O.some({ anEnabledServicePreference, isInboxEnabled: false })} | ${aCreatedMessageEvent} - ${"message sender service does not exists in user service preference (MANUAL SETTINGS)"} | ${"SENDER_BLOCKED"} | ${O.some(aRetrievedProfileWithManualPreferences)} | ${O.none} | ${aCreatedMessageEvent} - ${"message sender service exists and is not enabled in user service preference (MANUAL SETTINGS)"} | ${"SENDER_BLOCKED"} | ${O.some(aRetrievedProfileWithManualPreferences)} | ${O.some(aDisabledServicePreference)} | ${aCreatedMessageEvent} - ${"message sender service exists and has INBOX disabled in user service preference (MANUAL SETTINGS)"} | ${"SENDER_BLOCKED"} | ${O.some(aRetrievedProfileWithManualPreferences)} | ${O.some({ anEnabledServicePreference, isInboxEnabled: false })} | ${aCreatedMessageEvent} - ${"service in blockedInboxOrChannels with blocked INBOX (LEGACY SETTINGS)"} | ${"SENDER_BLOCKED"} | ${O.some(withBlacklist(aRetrievedProfileWithLegacyPreferences, [aNewMessageWithoutContent.senderServiceId]))} | ${"not-called"} | ${aCreatedMessageEvent} + scenario | failureReason | profileResult | preferenceResult | activationResult | messageEvent | messageData + ${"no profile was found"} | ${"PROFILE_NOT_FOUND"} | ${O.none} | ${"not-called"} | ${"not-called"} | ${aCreatedMessageEvent} | ${aCommonMessageData} + ${"inbox is not enabled"} | ${"MASTER_INBOX_DISABLED"} | ${O.some({ ...aRetrievedProfile, isInboxEnabled: false })} | ${"not-called"} | ${"not-called"} | ${aCreatedMessageEvent} | ${aCommonMessageData} + ${"message sender is blocked"} | ${"SENDER_BLOCKED"} | ${O.some(withBlacklist(aRetrievedProfile, [aNewMessageWithoutContent.senderServiceId]))} | ${"not-called"} | ${"not-called"} | ${aCreatedMessageEvent} | ${aCommonMessageData} + ${"message sender service exists and is not enabled in user service preference (AUTO SETTINGS)"} | ${"SENDER_BLOCKED"} | ${O.some(aRetrievedProfileWithAutoPreferences)} | ${O.some(aDisabledServicePreference)} | ${"not-called"} | ${aCreatedMessageEvent} | ${aCommonMessageData} + ${"message sender service exists and has INBOX disabled in user service preference (AUTO SETTINGS)"} | ${"SENDER_BLOCKED"} | ${O.some(aRetrievedProfileWithAutoPreferences)} | ${O.some({ anEnabledServicePreference, isInboxEnabled: false })} | ${"not-called"} | ${aCreatedMessageEvent} | ${aCommonMessageData} + ${"message sender service does not exists in user service preference (MANUAL SETTINGS)"} | ${"SENDER_BLOCKED"} | ${O.some(aRetrievedProfileWithManualPreferences)} | ${O.none} | ${"not-called"} | ${aCreatedMessageEvent} | ${aCommonMessageData} + ${"message sender service exists and is not enabled in user service preference (MANUAL SETTINGS)"} | ${"SENDER_BLOCKED"} | ${O.some(aRetrievedProfileWithManualPreferences)} | ${O.some(aDisabledServicePreference)} | ${"not-called"} | ${aCreatedMessageEvent} | ${aCommonMessageData} + ${"message sender service exists and has INBOX disabled in user service preference (MANUAL SETTINGS)"} | ${"SENDER_BLOCKED"} | ${O.some(aRetrievedProfileWithManualPreferences)} | ${O.some({ anEnabledServicePreference, isInboxEnabled: false })} | ${"not-called"} | ${aCreatedMessageEvent} | ${aCommonMessageData} + ${"service in blockedInboxOrChannels with blocked INBOX (LEGACY SETTINGS)"} | ${"SENDER_BLOCKED"} | ${O.some(withBlacklist(aRetrievedProfileWithLegacyPreferences, [aNewMessageWithoutContent.senderServiceId]))} | ${"not-called"} | ${"not-called"} | ${aCreatedMessageEvent} | ${aCommonMessageData} + ${"message sender special service does not exists in user service preference and Activation is INACTIVE"} | ${"SENDER_BLOCKED"} | ${O.some(aRetrievedProfileWithManualPreferences)} | ${O.none} | ${O.some(aDisabledActivation)} | ${aCreatedMessageEvent} | ${aSpecialMessageData} + ${"message sender special service does not exists in user service preference and Activation not exists"} | ${"SENDER_BLOCKED"} | ${O.some(aRetrievedProfileWithManualPreferences)} | ${O.none} | ${O.none} | ${aCreatedMessageEvent} | ${aSpecialMessageData} + ${"message sender special service does not exists in user service preference and Activation is PENDING"} | ${"SENDER_BLOCKED"} | ${O.some(aRetrievedProfileWithManualPreferences)} | ${O.none} | ${O.some({ ...aDisabledActivation, status: ActivationStatusEnum.PENDING })} | ${aCreatedMessageEvent} | ${aSpecialMessageData} + ${"message sender special service exists in user service preference and Activation is INACTIVE"} | ${"SENDER_BLOCKED"} | ${O.some(aRetrievedProfileWithManualPreferences)} | ${O.some(anEnabledServicePreference)} | ${O.some(aDisabledActivation)} | ${aCreatedMessageEvent} | ${aSpecialMessageData} + ${"message sender special service exists in user service preference and Activation not exists"} | ${"SENDER_BLOCKED"} | ${O.some(aRetrievedProfileWithManualPreferences)} | ${O.some(anEnabledServicePreference)} | ${O.none} | ${aCreatedMessageEvent} | ${aSpecialMessageData} + ${"message sender special service exists in user service preference and Activation is PENDING"} | ${"SENDER_BLOCKED"} | ${O.some(aRetrievedProfileWithManualPreferences)} | ${O.some(anEnabledServicePreference)} | ${O.some({ ...aDisabledActivation, status: ActivationStatusEnum.PENDING })} | ${aCreatedMessageEvent} | ${aSpecialMessageData} `( "should fail if $scenario", async ({ profileResult, preferenceResult, + activationResult, messageEvent, + messageData, // mock implementation must be set only if we expect the function to be called, otherwise it will interfere with other tests // we use "not-called" to determine such skipProfileMock = profileResult === "not-called", - skipPreferenceMock = preferenceResult === "not-called" + skipPreferenceMock = preferenceResult === "not-called", + skipActivationMock = activationResult === "not-called" }) => { !skipProfileMock && findLastVersionByModelIdMock.mockImplementationOnce(() => { @@ -362,7 +440,15 @@ describe("getprocessMessageHandler", () => { findServicePreferenceMock.mockImplementationOnce(() => { return TE.of(preferenceResult); }); + !skipActivationMock && + activationFindLastVersionMock.mockImplementationOnce(() => { + return TE.of(activationResult); + }); + mockRetrieveProcessingMessageData.mockImplementationOnce(() => + TE.of(O.some(messageData)) + ); const processMessageHandler = getProcessMessageHandler({ + lActivation, lProfileModel, lMessageModel, lBlobService: {} as any, @@ -389,17 +475,22 @@ describe("getprocessMessageHandler", () => { expect(findServicePreferenceMock).toBeCalledTimes( skipPreferenceMock ? 0 : 1 ); + expect(activationFindLastVersionMock).toBeCalledTimes( + skipActivationMock ? 0 : 1 + ); } ); it.each` - scenario | profileResult | storageResult | upsertResult | preferenceResult | messageEvent - ${"input cannot be decoded"} | ${"not-called"} | ${"not-called"} | ${"not-called"} | ${"not-called"} | ${{}} - ${"there is an error while fetching profile"} | ${TE.left("Profile fetch error")} | ${"not-called"} | ${"not-called"} | ${"not-called"} | ${aCreatedMessageEvent} - ${"message store operation fails"} | ${TE.of(O.some(aRetrievedProfile))} | ${TE.left(new Error("Error while storing message content"))} | ${"not-called"} | ${"not-called"} | ${aCreatedMessageEvent} - ${"message upsert fails"} | ${TE.of(O.some(aRetrievedProfile))} | ${TE.of(O.some(aBlobResult))} | ${TE.left(new Error("Error while upserting message"))} | ${"not-called"} | ${aCreatedMessageEvent} - ${"user's service preference retrieval fails (AUTO)"} | ${TE.of(O.some(aRetrievedProfileWithAutoPreferences))} | ${"not-called"} | ${"not-called"} | ${TE.left(new Error("Error while reading preference"))} | ${aCreatedMessageEvent} - ${"user's service preference retrieval fails (MANUAL SETTINGS)"} | ${TE.of(O.some(aRetrievedProfileWithManualPreferences))} | ${"not-called"} | ${"not-called"} | ${TE.left({ kind: "COSMOS_EMPTY_RESPONSE" })} | ${aCreatedMessageEvent} + scenario | profileResult | storageResult | upsertResult | preferenceResult | activationResult | messageEvent | messageData + ${"input cannot be decoded"} | ${"not-called"} | ${"not-called"} | ${"not-called"} | ${"not-called"} | ${"not-called"} | ${{}} | ${aCommonMessageData} + ${"there is an error while fetching profile"} | ${TE.left("Profile fetch error")} | ${"not-called"} | ${"not-called"} | ${"not-called"} | ${"not-called"} | ${aCreatedMessageEvent} | ${aCommonMessageData} + ${"message store operation fails"} | ${TE.of(O.some(aRetrievedProfile))} | ${TE.left(new Error("Error while storing message content"))} | ${"not-called"} | ${"not-called"} | ${"not-called"} | ${aCreatedMessageEvent} | ${aCommonMessageData} + ${"message upsert fails"} | ${TE.of(O.some(aRetrievedProfile))} | ${TE.of(O.some(aBlobResult))} | ${TE.left(new Error("Error while upserting message"))} | ${"not-called"} | ${"not-called"} | ${aCreatedMessageEvent} | ${aCommonMessageData} + ${"user's service preference retrieval fails (AUTO)"} | ${TE.of(O.some(aRetrievedProfileWithAutoPreferences))} | ${"not-called"} | ${"not-called"} | ${TE.left(new Error("Error while reading preference"))} | ${"not-called"} | ${aCreatedMessageEvent} | ${aCommonMessageData} + ${"user's service preference retrieval fails (MANUAL SETTINGS)"} | ${TE.of(O.some(aRetrievedProfileWithManualPreferences))} | ${"not-called"} | ${"not-called"} | ${TE.left({ kind: "COSMOS_EMPTY_RESPONSE" })} | ${"not-called"} | ${aCreatedMessageEvent} | ${aCommonMessageData} + ${"user's activation retrieval for a service fails"} | ${TE.of(O.some(aRetrievedProfileWithManualPreferences))} | ${"not-called"} | ${"not-called"} | ${TE.of(O.none)} | ${TE.left(new Error("Error while reading activation"))} | ${aCreatedMessageEvent} | ${aSpecialMessageData} + ${"user's activation retrieval for a service fails"} | ${TE.of(O.some(aRetrievedProfileWithManualPreferences))} | ${"not-called"} | ${"not-called"} | ${TE.of(O.none)} | ${TE.left({ kind: "COSMOS_EMPTY_RESPONSE" })} | ${aCreatedMessageEvent} | ${aSpecialMessageData} `( "should throw an Error if $scenario", async ({ @@ -407,13 +498,16 @@ describe("getprocessMessageHandler", () => { storageResult, upsertResult, preferenceResult, + activationResult, messageEvent, + messageData, // mock implementation must be set only if we expect the function to be called, otherwise it will interfere with other tests // we use "not-called" to determine such skipProfileMock = profileResult === "not-called", skipStorageMock = storageResult === "not-called", skipUpsertMock = upsertResult === "not-called", - skipPreferenceMock = preferenceResult === "not-called" + skipPreferenceMock = preferenceResult === "not-called", + skipActivationMock = activationResult === "not-called" }) => { !skipProfileMock && findLastVersionByModelIdMock.mockImplementationOnce( @@ -427,8 +521,15 @@ describe("getprocessMessageHandler", () => { findServicePreferenceMock.mockImplementationOnce( () => preferenceResult ); - + !skipActivationMock && + activationFindLastVersionMock.mockImplementationOnce( + () => activationResult + ); + mockRetrieveProcessingMessageData.mockImplementationOnce(() => + TE.of(O.some(messageData)) + ); const processMessageHandler = getProcessMessageHandler({ + lActivation, lProfileModel, lMessageModel, lBlobService: {} as any, @@ -455,6 +556,9 @@ describe("getprocessMessageHandler", () => { expect(findServicePreferenceMock).toBeCalledTimes( skipPreferenceMock ? 0 : 1 ); + expect(activationFindLastVersionMock).toBeCalledTimes( + skipActivationMock ? 0 : 1 + ); } ); }); diff --git a/ProcessMessage/handler.ts b/ProcessMessage/handler.ts index 86057f0b..4e88e1ad 100644 --- a/ProcessMessage/handler.ts +++ b/ProcessMessage/handler.ts @@ -31,6 +31,8 @@ import { MessageStatusModel } from "@pagopa/io-functions-commons/dist/src/models/message_status"; import { MessageStatusValueEnum } from "@pagopa/io-functions-commons/dist/generated/definitions/MessageStatusValue"; +import { ActivationModel } from "@pagopa/io-functions-commons/dist/src/models/activation"; +import { ActivationStatusEnum } from "@pagopa/io-functions-commons/dist/generated/definitions/ActivationStatus"; import { initTelemetryClient } from "../utils/appinsights"; import { toHash } from "../utils/crypto"; import { PaymentData } from "../generated/definitions/PaymentData"; @@ -42,6 +44,7 @@ import { CreatedMessageEvent, ProcessedMessageEvent } from "../utils/events/message"; +import { SpecialServiceCategoryEnum } from "../generated/api-admin/SpecialServiceCategory"; // Interface that marks an unexpected value interface IUnexpectedValue { @@ -260,6 +263,7 @@ const createMessageOrThrow = async ( }; export interface IProcessMessageHandlerInput { + readonly lActivation: ActivationModel; readonly lProfileModel: ProfileModel; readonly lMessageModel: MessageModel; readonly lBlobService: BlobService; @@ -277,6 +281,7 @@ type Handler = (c: Context, i: unknown) => Promise; * Returns a function for handling ProcessMessage */ export const getProcessMessageHandler = ({ + lActivation, lProfileModel, lMessageModel, lBlobService, @@ -384,6 +389,49 @@ export const getProcessMessageHandler = ({ return result; }), + TE.chain(_ => { + if ( + createdMessageEvent.senderMetadata.serviceCategory === + SpecialServiceCategoryEnum.SPECIAL + ) { + return pipe( + lActivation.findLastVersionByModelId([ + createdMessageEvent.message.senderServiceId, + profile.fiscalCode + ]), + TE.mapLeft(activationError => { + // The query has failed, we consider this as a transient error. + context.log.error(`${logPrefix}|${activationError.kind}`); + throw Error( + "Error while retrieving user's service preference" + ); + }), + TE.map(maybeActivation => + pipe( + maybeActivation, + O.map( + activation => + activation.status === ActivationStatusEnum.ACTIVE + ), + O.getOrElse(() => false) + ) + ), + TE.chainW( + TE.fromPredicate( + hasActiveActivation => hasActiveActivation, + () => + _.includes(BlockedInboxOrChannelEnum.INBOX) + ? _ + : [..._, BlockedInboxOrChannelEnum.INBOX] + ) + ), + TE.map(() => + _.filter(el => el !== BlockedInboxOrChannelEnum.INBOX) + ) + ); + } + return TE.of(_); + }), TE.toUnion )(); diff --git a/ProcessMessage/index.ts b/ProcessMessage/index.ts index ba2bd73b..ba197d53 100644 --- a/ProcessMessage/index.ts +++ b/ProcessMessage/index.ts @@ -17,6 +17,10 @@ import { MESSAGE_STATUS_COLLECTION_NAME, MessageStatusModel } from "@pagopa/io-functions-commons/dist/src/models/message_status"; +import { + ActivationModel, + ACTIVATION_COLLECTION_NAME +} from "@pagopa/io-functions-commons/dist/src/models/activation"; import { cosmosdbInstance } from "../utils/cosmosdb"; import { getConfigOrThrow } from "../utils/config"; import { initTelemetryClient } from "../utils/appinsights"; @@ -52,6 +56,10 @@ const messageStatusModel = new MessageStatusModel( cosmosdbInstance.container(MESSAGE_STATUS_COLLECTION_NAME) ); +const activationModel = new ActivationModel( + cosmosdbInstance.container(ACTIVATION_COLLECTION_NAME) +); + const telemetryClient = initTelemetryClient( config.APPINSIGHTS_INSTRUMENTATIONKEY ); @@ -64,6 +72,7 @@ const retrieveProcessingMessageData = makeRetrieveExpandedDataFromBlob( const activityFunctionHandler: AzureFunction = getProcessMessageHandler({ isOptInEmailEnabled: config.FF_OPT_IN_EMAIL_ENABLED, + lActivation: activationModel, lBlobService: blobServiceForMessageContent, lMessageModel: messageModel, lMessageStatusModel: messageStatusModel, diff --git a/UpdateService/__tests__/handler.test.ts b/UpdateService/__tests__/handler.test.ts index 84ede81b..fcf2e279 100644 --- a/UpdateService/__tests__/handler.test.ts +++ b/UpdateService/__tests__/handler.test.ts @@ -28,6 +28,7 @@ import { Subscription } from "../../generated/api-admin/Subscription"; import { UserInfo } from "../../generated/api-admin/UserInfo"; import { ServicePayload } from "../../generated/definitions/ServicePayload"; import { UpdateServiceHandler } from "../handler"; +import { StandardServiceCategoryEnum } from "@pagopa/io-functions-commons/dist/generated/definitions/StandardServiceCategory"; const mockContext = { // eslint-disable no-console @@ -49,6 +50,8 @@ const aServiceId = "s123" as NonEmptyString; const aTokenName = "TOKEN_NAME" as NonEmptyString; const someServicesMetadata: ServiceMetadata = { scope: ServiceScopeEnum.NATIONAL, + category: StandardServiceCategoryEnum.STANDARD, + customSpecialFlow: undefined, tokenName: aTokenName }; diff --git a/UpdateService/handler.ts b/UpdateService/handler.ts index f4729c07..051e7c33 100644 --- a/UpdateService/handler.ts +++ b/UpdateService/handler.ts @@ -52,6 +52,10 @@ import { withApiRequestWrapper } from "../utils/api"; import { getLogger, ILogger } from "../utils/logging"; import { ErrorResponses, IResponseErrorUnauthorized } from "../utils/responses"; import { serviceOwnerCheckTask } from "../utils/subscription"; +import { StandardServiceCategoryEnum } from "../generated/api-admin/StandardServiceCategory"; +import { SpecialServiceMetadata } from "../generated/api-admin/SpecialServiceMetadata"; +import { SpecialServiceCategoryEnum } from "../generated/api-admin/SpecialServiceCategory"; +import { StandardServiceMetadata } from "../generated/api-admin/StandardServiceMetadata"; type ResponseTypes = | IResponseSuccessJson @@ -137,10 +141,23 @@ const updateServiceTask = ( ...retrievedService, ...servicePayload, service_id: serviceId, - service_metadata: { - ...servicePayload.service_metadata, - token_name: adb2cTokenName - } + // Only Admins can change service category and custom_special_flow name + service_metadata: SpecialServiceMetadata.is( + retrievedService.service_metadata + ) + ? ({ + ...servicePayload.service_metadata, + category: SpecialServiceCategoryEnum.SPECIAL, + custom_special_flow: + retrievedService.service_metadata.custom_special_flow, + token_name: adb2cTokenName + } as SpecialServiceMetadata) + : ({ + ...servicePayload.service_metadata, + category: StandardServiceCategoryEnum.STANDARD, + custom_special_flow: undefined, + token_name: adb2cTokenName + } as StandardServiceMetadata) }, service_id: serviceId }), diff --git a/WebhookNotification/__tests__/handler.test.ts b/WebhookNotification/__tests__/handler.test.ts index 2e8ce0a7..c8937855 100644 --- a/WebhookNotification/__tests__/handler.test.ts +++ b/WebhookNotification/__tests__/handler.test.ts @@ -36,6 +36,7 @@ import { Millisecond } from "@pagopa/ts-commons/lib/units"; import * as E from "fp-ts/lib/Either"; import * as TE from "fp-ts/lib/TaskEither"; import * as O from "fp-ts/lib/Option"; +import { StandardServiceCategoryEnum } from "../../generated/api-admin/StandardServiceCategory"; const mockAppinsights = { trackDependency: jest.fn(), @@ -75,6 +76,7 @@ const aSenderMetadata: CreatedMessageEventSenderMetadata = { organizationFiscalCode: anOrganizationFiscalCode, organizationName: "org" as NonEmptyString, requireSecureChannels: false, + serviceCategory: StandardServiceCategoryEnum.STANDARD, serviceName: "service" as NonEmptyString, serviceUserEmail: "email@exmaple.com" as EmailString }; diff --git a/__mocks__/mocks.ts b/__mocks__/mocks.ts index 9c307500..b9f61ee4 100644 --- a/__mocks__/mocks.ts +++ b/__mocks__/mocks.ts @@ -24,7 +24,6 @@ import { OrganizationFiscalCode } from "@pagopa/ts-commons/lib/strings"; - import { MessageBodyMarkdown } from "@pagopa/io-functions-commons/dist/generated/definitions/MessageBodyMarkdown"; import { MessageSubject } from "@pagopa/io-functions-commons/dist/generated/definitions/MessageSubject"; @@ -35,6 +34,7 @@ import { RetrievedMessageWithoutContent } from "@pagopa/io-functions-commons/dist/src/models/message"; import { CreatedMessageEventSenderMetadata } from "@pagopa/io-functions-commons/dist/src/models/created_message_sender_metadata"; +import { StandardServiceCategoryEnum } from "@pagopa/io-functions-commons/dist/generated/definitions/StandardServiceCategory"; import { MessageContent } from "@pagopa/io-functions-commons/dist/generated/definitions/MessageContent"; import { CIDR } from "@pagopa/io-functions-commons/dist/generated/definitions/CIDR"; @@ -66,7 +66,9 @@ export const aValidService: ValidService = { description: "Service Description" as NonEmptyString, privacyUrl: "https://example.com/privacy.html" as NonEmptyString, supportUrl: "https://example.com/support.html" as NonEmptyString, - scope: ServiceScopeEnum.NATIONAL + scope: ServiceScopeEnum.NATIONAL, + category: StandardServiceCategoryEnum.STANDARD, + customSpecialFlow: undefined } }; @@ -88,7 +90,9 @@ export const anIncompleteService: Service & { serviceName: "Service" as NonEmptyString, serviceMetadata: { description: "Service Description" as NonEmptyString, - scope: ServiceScopeEnum.NATIONAL + scope: ServiceScopeEnum.NATIONAL, + category: StandardServiceCategoryEnum.STANDARD, + customSpecialFlow: undefined }, version: 1 as NonNegativeInteger }; @@ -181,6 +185,7 @@ export const aCreatedMessageEventSenderMetadata: CreatedMessageEventSenderMetada organizationFiscalCode: "01234567890" as OrganizationFiscalCode, organizationName: "An Organization Name" as NonEmptyString, requireSecureChannels: false, + serviceCategory: StandardServiceCategoryEnum.STANDARD, serviceName: "A_SERVICE_NAME" as NonEmptyString, serviceUserEmail: "aaa@mail.com" as EmailString }; @@ -200,8 +205,7 @@ export const anEnabledServicePreference: ServicePreference = { isWebhookEnabled: true, serviceId: "01234567890" as NonEmptyString, settingsVersion: 0 as NonNegativeInteger -} - +}; export const aDisabledServicePreference: ServicePreference = { fiscalCode: aFiscalCode, @@ -210,4 +214,4 @@ export const aDisabledServicePreference: ServicePreference = { isWebhookEnabled: false, serviceId: "01234567890" as NonEmptyString, settingsVersion: 0 as NonNegativeInteger -} +}; diff --git a/openapi/index.yaml b/openapi/index.yaml index 5c4dbb8d..4d7c75bf 100644 --- a/openapi/index.yaml +++ b/openapi/index.yaml @@ -756,7 +756,7 @@ definitions: created_at: $ref: '#/definitions/Timestamp' content: - $ref: '#/definitions/MessageContent' + $ref: '#/definitions/NewMessageContent' sender_service_id: $ref: '#/definitions/ServiceId' required: @@ -1010,7 +1010,10 @@ definitions: - false description: It indicates that service is hidden service_metadata: - $ref: '#/definitions/ServiceMetadata' + description: >- + That service can't handle some ServiceMetadata fields (es. + category) + $ref: '#/definitions/CommonServiceMetadata' VisibleServicePayload: description: >- A payload used to create/update a service that appears in the service @@ -1025,10 +1028,20 @@ definitions: - true description: It indicates that service appears in the service list service_metadata: - $ref: '#/definitions/ServiceMetadata' + description: >- + That service can't handle some ServiceMetadata fields (es. + category) + $ref: '#/definitions/CommonServiceMetadata' required: - is_visible - service_metadata + ExtendedServicePayload: + allOf: + - $ref: '#/definitions/ServicePayload' + - type: object + properties: + service_metadata: + $ref: '#/definitions/ServiceMetadata' CommonServicePayload: description: Common properties for a ServicePayload type: object @@ -1065,7 +1078,7 @@ definitions: Service: description: A service tied to user's subscription. allOf: - - $ref: '#/definitions/ServicePayload' + - $ref: '#/definitions/ExtendedServicePayload' - type: object properties: id: @@ -1091,6 +1104,37 @@ definitions: - service_id - authorized_recipients ServiceMetadata: + x-one-of: true + allOf: + - $ref: '#/definitions/StandardServiceMetadata' + - $ref: '#/definitions/SpecialServiceMetadata' + - $ref: '#/definitions/CommonServiceMetadata' + StandardServiceMetadata: + allOf: + - $ref: '#/definitions/CommonServiceMetadata' + - type: object + properties: + category: + type: string + x-extensible-enum: + - STANDARD + required: + - category + SpecialServiceMetadata: + allOf: + - $ref: '#/definitions/CommonServiceMetadata' + - type: object + properties: + category: + type: string + x-extensible-enum: + - SPECIAL + custom_special_flow: + type: string + minLength: 1 + required: + - category + CommonServiceMetadata: type: object description: A set of metadata properties related to this service. properties: diff --git a/openapi/index.yaml.template b/openapi/index.yaml.template index 7e04cfbc..d1fd0580 100644 --- a/openapi/index.yaml.template +++ b/openapi/index.yaml.template @@ -665,13 +665,21 @@ definitions: HiddenServicePayload: $ref: "../node_modules/@pagopa/io-functions-commons/openapi/definitions.yaml#/HiddenServicePayload" VisibleServicePayload: - $ref: "../node_modules/@pagopa/io-functions-commons/openapi/definitions.yaml#/VisibleServicePayload" + $ref: "../node_modules/@pagopa/io-functions-commons/openapi/definitions.yaml#/VisibleServicePayload" + ExtendedServicePayload: + $ref: "../node_modules/@pagopa/io-functions-commons/openapi/definitions.yaml#/ExtendedServicePayload" CommonServicePayload: $ref: "../node_modules/@pagopa/io-functions-commons/openapi/definitions.yaml#/CommonServicePayload" Service: $ref: "../node_modules/@pagopa/io-functions-commons/openapi/definitions.yaml#/Service" ServiceMetadata: $ref: "../node_modules/@pagopa/io-functions-commons/openapi/definitions.yaml#/ServiceMetadata" + StandardServiceMetadata: + $ref: "../node_modules/@pagopa/io-functions-commons/openapi/definitions.yaml#/StandardServiceMetadata" + SpecialServiceMetadata: + $ref: "../node_modules/@pagopa/io-functions-commons/openapi/definitions.yaml#/SpecialServiceMetadata" + CommonServiceMetadata: + $ref: "../node_modules/@pagopa/io-functions-commons/openapi/definitions.yaml#/CommonServiceMetadata" ServiceScope: $ref: "../node_modules/@pagopa/io-functions-commons/openapi/definitions.yaml#/ServiceScope" ServiceId: diff --git a/package.json b/package.json index 945a2ff7..678eb9c5 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "generate": "npm-run-all generate:*", "generate:definitions": "rimraf ./generated/definitions && shx mkdir -p ./generated/definitions && gen-api-models --api-spec ./openapi/index.yaml --no-strict --out-dir ./generated/definitions", "generate:api-notifications": "rimraf ./generated/notifications && shx mkdir -p ./generated/notifications && gen-api-models --api-spec https://raw.githubusercontent.com/pagopa/io-backend/master/api_notifications.yaml --out-dir ./generated/notifications --response-decoders --request-types", - "generate:api-admin": "rimraf generated/api-admin && shx mkdir -p generated/api-admin && gen-api-models --api-spec https://raw.githubusercontent.com/pagopa/io-functions-admin/master/openapi/index.yaml --no-strict --out-dir generated/api-admin --request-types --response-decoders --client", + "generate:api-admin": "rimraf generated/api-admin && shx mkdir -p generated/api-admin && gen-api-models --api-spec https://raw.githubusercontent.com/pagopa/io-functions-admin/IC-40-special-service-model-upgrade/openapi/index.yaml --no-strict --out-dir generated/api-admin --request-types --response-decoders --client", "openapi:bundle": "bundle-api-spec -i openapi/index.yaml.template -o openapi/index.yaml -V $npm_package_version", "openapi:validate": "oval validate -p openapi/index.yaml", "dist:modules": "modclean -r -n default:safe && yarn install --production", @@ -59,7 +59,7 @@ "dependencies": { "@azure/cosmos": "^3.11.5", "@pagopa/express-azure-functions": "^2.0.0", - "@pagopa/io-functions-commons": "^21.7.1", + "@pagopa/io-functions-commons": "^22.0.1", "@pagopa/ts-commons": "^10.2.0", "applicationinsights": "^1.7.4", "azure-storage": "^2.10.4", diff --git a/yarn.lock b/yarn.lock index 33f163f8..e861cbbc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -738,15 +738,15 @@ resolved "https://registry.yarnpkg.com/@pagopa/express-azure-functions/-/express-azure-functions-2.0.0.tgz#eb52a0b997d931c1509372e2a9bea22a8ca85c17" integrity sha512-IFZqtk0e2sfkMZIxYqPORzxcKRkbIrVJesR6eMLNwzh1rA4bl2uh9ZHk1m55LNq4ZmaxREDu+1JcGlIaZQgKNQ== -"@pagopa/io-functions-commons@^21.7.1": - version "21.7.1" - resolved "https://registry.yarnpkg.com/@pagopa/io-functions-commons/-/io-functions-commons-21.7.1.tgz#a77b73913a0c18f15e93b54f8154a1b32fc2b7f7" - integrity sha512-6fKUmXrirKu/dl1WRn3/HdBctUlYJmvjKmomdL/mz3ByeOgSqUDYCep3+gGmLI8Pwbjx0/hyng8xkyx6R7/JnQ== +"@pagopa/io-functions-commons@^22.0.1": + version "22.0.1" + resolved "https://registry.yarnpkg.com/@pagopa/io-functions-commons/-/io-functions-commons-22.0.1.tgz#332243e405c29db4862780135525f968c667da47" + integrity sha512-9ZQ1N8PEZkSsceiP2SG2lTsO+sQxJG0QZpCPVjK/ulpkF0RSqiaFufTzUaSXxiNkaUMd+mYaeo92vfg2GWqxaA== dependencies: "@azure/cosmos" "^3.11.5" "@pagopa/ts-commons" "^10.0.1" applicationinsights "^1.8.10" - azure-storage "^2.10.4" + azure-storage "^2.10.5" cidr-matcher "^2.1.1" fp-ts "^2.10.5" helmet "^4.6.0" @@ -1538,6 +1538,23 @@ azure-storage@^2.10.4: xml2js "0.2.8" xmlbuilder "^9.0.7" +azure-storage@^2.10.5: + version "2.10.5" + resolved "https://registry.yarnpkg.com/azure-storage/-/azure-storage-2.10.5.tgz#2193314940954c8e90c14d0601fb146470740f70" + integrity sha512-kLCbiW1lvwwJwB/iOX7ic7xw/RIcSReF1sUFetEyFSiE+HDdv/wpSlsQx0F0khkXrPtJmBJRH0y9s/CRuRBWLQ== + dependencies: + browserify-mime "~1.2.9" + extend "^3.0.2" + json-edm-parser "0.1.2" + md5.js "1.3.4" + readable-stream "~2.0.0" + request "^2.86.0" + underscore "^1.12.1" + uuid "^3.0.0" + validator "~13.6.0" + xml2js "0.2.8" + xmlbuilder "^9.0.7" + babel-jest@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-24.9.0.tgz#3fc327cb8467b89d14d7bc70e315104a783ccd54" @@ -7828,7 +7845,7 @@ validator@^10.0.0, validator@^10.1.0: resolved "https://registry.yarnpkg.com/validator/-/validator-10.11.0.tgz#003108ea6e9a9874d31ccc9e5006856ccd76b228" integrity sha512-X/p3UZerAIsbBfN/IwahhYaBbY68EN/UQBWHtsbXGT5bfrH/p4NQzUCG1kF/rtKaNpnJ7jAu6NGTdSNtyNIXMw== -validator@^13.6.0: +validator@^13.6.0, validator@~13.6.0: version "13.6.0" resolved "https://registry.yarnpkg.com/validator/-/validator-13.6.0.tgz#1e71899c14cdc7b2068463cb24c1cc16f6ec7059" integrity sha512-gVgKbdbHgtxpRyR8K0O6oFZPhhB5tT1jeEHZR0Znr9Svg03U0+r9DXWMrnRAB+HtCStDQKlaIZm42tVsVjqtjg== From 5605f860bf41c004ee46f26987a6848caaaed89d Mon Sep 17 00:00:00 2001 From: Daniele Manni Date: Mon, 22 Nov 2021 14:13:49 +0100 Subject: [PATCH 02/12] [#IC-49] Remove manual casting for ServiceMetadata --- UpdateService/handler.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/UpdateService/handler.ts b/UpdateService/handler.ts index 051e7c33..231148de 100644 --- a/UpdateService/handler.ts +++ b/UpdateService/handler.ts @@ -55,7 +55,6 @@ import { serviceOwnerCheckTask } from "../utils/subscription"; import { StandardServiceCategoryEnum } from "../generated/api-admin/StandardServiceCategory"; import { SpecialServiceMetadata } from "../generated/api-admin/SpecialServiceMetadata"; import { SpecialServiceCategoryEnum } from "../generated/api-admin/SpecialServiceCategory"; -import { StandardServiceMetadata } from "../generated/api-admin/StandardServiceMetadata"; type ResponseTypes = | IResponseSuccessJson @@ -145,19 +144,18 @@ const updateServiceTask = ( service_metadata: SpecialServiceMetadata.is( retrievedService.service_metadata ) - ? ({ + ? { ...servicePayload.service_metadata, category: SpecialServiceCategoryEnum.SPECIAL, custom_special_flow: retrievedService.service_metadata.custom_special_flow, token_name: adb2cTokenName - } as SpecialServiceMetadata) - : ({ + } + : { ...servicePayload.service_metadata, category: StandardServiceCategoryEnum.STANDARD, - custom_special_flow: undefined, token_name: adb2cTokenName - } as StandardServiceMetadata) + } }, service_id: serviceId }), From f44462142db799a78b988d6e0864a42f64db1bb0 Mon Sep 17 00:00:00 2001 From: Daniele Manni Date: Mon, 22 Nov 2021 15:10:58 +0100 Subject: [PATCH 03/12] [#IC-49] Extract sub-pipe for Activation check in a new function --- ProcessMessage/handler.ts | 107 +++++++++++++++++++++++++------------- 1 file changed, 70 insertions(+), 37 deletions(-) diff --git a/ProcessMessage/handler.ts b/ProcessMessage/handler.ts index 4e88e1ad..84decb88 100644 --- a/ProcessMessage/handler.ts +++ b/ProcessMessage/handler.ts @@ -186,6 +186,67 @@ const getServicePreferenceValueOrError = ( ) ); +type ActivationForSpecialServices = (params: { + readonly createdMessageEvent: CommonMessageData; + readonly fiscalCode: FiscalCode; + readonly context: Context; + readonly logPrefix: string; + readonly blockedInboxOrChannel: ReadonlyArray; +}) => TaskEither< + ReadonlyArray, + ReadonlyArray +>; + +/** + * Override the INBOX value of BlockedInboxOrChannel with the Activation value + * related to a Special Service and a User. + * + * @param lActivation + * @returns + */ +const getActivationForSpecialServices = ( + lActivation: ActivationModel +): ActivationForSpecialServices => ({ + createdMessageEvent, + fiscalCode, + context, + logPrefix, + blockedInboxOrChannel +}): TaskEither< + ReadonlyArray, + ReadonlyArray +> => + pipe( + lActivation.findLastVersionByModelId([ + createdMessageEvent.message.senderServiceId, + fiscalCode + ]), + TE.mapLeft(activationError => { + // The query has failed, we consider this as a transient error. + context.log.error(`${logPrefix}|${activationError.kind}`); + throw Error("Error while retrieving user's service Activation"); + }), + TE.map(maybeActivation => + pipe( + maybeActivation, + O.map(activation => activation.status === ActivationStatusEnum.ACTIVE), + O.getOrElse(() => false) + ) + ), + TE.chainW( + TE.fromPredicate( + hasActiveActivation => hasActiveActivation, + () => + blockedInboxOrChannel.includes(BlockedInboxOrChannelEnum.INBOX) + ? blockedInboxOrChannel + : [...blockedInboxOrChannel, BlockedInboxOrChannelEnum.INBOX] + ) + ), + TE.map(() => + blockedInboxOrChannel.filter(el => el !== BlockedInboxOrChannelEnum.INBOX) + ) + ); + /** * Creates the message and makes it visible or throw an error * @@ -389,48 +450,20 @@ export const getProcessMessageHandler = ({ return result; }), - TE.chain(_ => { + TE.chain(blockedInboxOrChannel => { if ( createdMessageEvent.senderMetadata.serviceCategory === SpecialServiceCategoryEnum.SPECIAL ) { - return pipe( - lActivation.findLastVersionByModelId([ - createdMessageEvent.message.senderServiceId, - profile.fiscalCode - ]), - TE.mapLeft(activationError => { - // The query has failed, we consider this as a transient error. - context.log.error(`${logPrefix}|${activationError.kind}`); - throw Error( - "Error while retrieving user's service preference" - ); - }), - TE.map(maybeActivation => - pipe( - maybeActivation, - O.map( - activation => - activation.status === ActivationStatusEnum.ACTIVE - ), - O.getOrElse(() => false) - ) - ), - TE.chainW( - TE.fromPredicate( - hasActiveActivation => hasActiveActivation, - () => - _.includes(BlockedInboxOrChannelEnum.INBOX) - ? _ - : [..._, BlockedInboxOrChannelEnum.INBOX] - ) - ), - TE.map(() => - _.filter(el => el !== BlockedInboxOrChannelEnum.INBOX) - ) - ); + return getActivationForSpecialServices(lActivation)({ + blockedInboxOrChannel, + context, + createdMessageEvent, + fiscalCode: newMessageWithoutContent.fiscalCode, + logPrefix + }); } - return TE.of(_); + return TE.of(blockedInboxOrChannel); }), TE.toUnion )(); From 15c1873fedb0077380090e085587c37f47a35ab3 Mon Sep 17 00:00:00 2001 From: Daniele Manni Date: Mon, 22 Nov 2021 15:14:11 +0100 Subject: [PATCH 04/12] [#IC-49] Add comment inside blockedInboxOrChannels pipe on Special service discrimination --- ProcessMessage/handler.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ProcessMessage/handler.ts b/ProcessMessage/handler.ts index 84decb88..d90c1cd5 100644 --- a/ProcessMessage/handler.ts +++ b/ProcessMessage/handler.ts @@ -455,6 +455,8 @@ export const getProcessMessageHandler = ({ createdMessageEvent.senderMetadata.serviceCategory === SpecialServiceCategoryEnum.SPECIAL ) { + // If the service is SPECIAL we need to retrieve the Activation information of that service + // and override the INBOX value inside the original blockInboxOrChannel return getActivationForSpecialServices(lActivation)({ blockedInboxOrChannel, context, @@ -463,6 +465,7 @@ export const getProcessMessageHandler = ({ logPrefix }); } + // If the service is STANDARD we use the original value for blockedInboxOrChannel return TE.of(blockedInboxOrChannel); }), TE.toUnion From 2ca3a1a36a6305561bf5bd628afad30da13ce96e Mon Sep 17 00:00:00 2001 From: Daniele Manni Date: Mon, 22 Nov 2021 15:33:58 +0100 Subject: [PATCH 05/12] [#IC-49] Fix comments --- ProcessMessage/handler.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/ProcessMessage/handler.ts b/ProcessMessage/handler.ts index d90c1cd5..c86ebadd 100644 --- a/ProcessMessage/handler.ts +++ b/ProcessMessage/handler.ts @@ -198,8 +198,10 @@ type ActivationForSpecialServices = (params: { >; /** - * Override the INBOX value of BlockedInboxOrChannel with the Activation value - * related to a Special Service and a User. + * Returns a new blockedInboxOrChannel value for a SPECIAL service related to a User. + * When a service has category SPECIAL the INBOX value depends from the activation status + * for the couple user/service. + * A missing Activation es equal an INACTIVE one. * * @param lActivation * @returns @@ -455,8 +457,9 @@ export const getProcessMessageHandler = ({ createdMessageEvent.senderMetadata.serviceCategory === SpecialServiceCategoryEnum.SPECIAL ) { - // If the service is SPECIAL we need to retrieve the Activation information of that service - // and override the INBOX value inside the original blockInboxOrChannel + // If the service has category equals to SPECIAL, the INBOX value in blockedInboxOrChannel depends from the Activation + // value. If the SPECIAL service is ACTIVE (exists an activation for couple user/service with ACTIVE status) + // we remove INBOX from blockedInboxOrChannel, otherwise we add the INBOX value. return getActivationForSpecialServices(lActivation)({ blockedInboxOrChannel, context, @@ -465,7 +468,8 @@ export const getProcessMessageHandler = ({ logPrefix }); } - // If the service is STANDARD we use the original value for blockedInboxOrChannel + // If the service is STANDARD we use the original value of blockedInboxOrChannel + // calculated from services preferences and user profile. return TE.of(blockedInboxOrChannel); }), TE.toUnion From 65430ec766738a725a0c9d9dda1a02ddb15f0959 Mon Sep 17 00:00:00 2001 From: Daniele Manni Date: Mon, 22 Nov 2021 15:36:35 +0100 Subject: [PATCH 06/12] typo --- ProcessMessage/handler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ProcessMessage/handler.ts b/ProcessMessage/handler.ts index c86ebadd..ed3db6f6 100644 --- a/ProcessMessage/handler.ts +++ b/ProcessMessage/handler.ts @@ -201,7 +201,7 @@ type ActivationForSpecialServices = (params: { * Returns a new blockedInboxOrChannel value for a SPECIAL service related to a User. * When a service has category SPECIAL the INBOX value depends from the activation status * for the couple user/service. - * A missing Activation es equal an INACTIVE one. + * A missing Activation is equal to an INACTIVE one. * * @param lActivation * @returns From f5b41c13fa75854e4ab35fb22afefd44960448ae Mon Sep 17 00:00:00 2001 From: Daniele Manni Date: Mon, 22 Nov 2021 19:37:50 +0100 Subject: [PATCH 07/12] Update ProcessMessage/handler.ts Co-authored-by: Danilo Spinelli --- ProcessMessage/handler.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/ProcessMessage/handler.ts b/ProcessMessage/handler.ts index ed3db6f6..66df46c9 100644 --- a/ProcessMessage/handler.ts +++ b/ProcessMessage/handler.ts @@ -198,10 +198,16 @@ type ActivationForSpecialServices = (params: { >; /** - * Returns a new blockedInboxOrChannel value for a SPECIAL service related to a User. - * When a service has category SPECIAL the INBOX value depends from the activation status - * for the couple user/service. - * A missing Activation is equal to an INACTIVE one. + * Returns the updated value of `blockedInboxOrChannel` for a Service which is marked as `SPECIAL`. + * + * In case the service category field value equals `SPECIAL`, we update the value of the `INBOX` field + * in the list of user's blocked inboxes (one for each blocked service) according to the service activation status. + * + * We try to retrieve the activation status related to the tuple (user, service) from the database. + * In case the activation status is missing, its value is assumed to be `INACTIVE`. + * In case the activation status is found to be `ACTIVE` then we remove the INBOX entry + * from the list of blocked inboxes. + * * * @param lActivation * @returns From c4b459752e3ea07441385ddce4dde7f7473f38aa Mon Sep 17 00:00:00 2001 From: Daniele Manni Date: Mon, 22 Nov 2021 19:38:23 +0100 Subject: [PATCH 08/12] Update ProcessMessage/handler.ts Co-authored-by: Danilo Spinelli --- ProcessMessage/handler.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/ProcessMessage/handler.ts b/ProcessMessage/handler.ts index 66df46c9..d98faec3 100644 --- a/ProcessMessage/handler.ts +++ b/ProcessMessage/handler.ts @@ -463,9 +463,6 @@ export const getProcessMessageHandler = ({ createdMessageEvent.senderMetadata.serviceCategory === SpecialServiceCategoryEnum.SPECIAL ) { - // If the service has category equals to SPECIAL, the INBOX value in blockedInboxOrChannel depends from the Activation - // value. If the SPECIAL service is ACTIVE (exists an activation for couple user/service with ACTIVE status) - // we remove INBOX from blockedInboxOrChannel, otherwise we add the INBOX value. return getActivationForSpecialServices(lActivation)({ blockedInboxOrChannel, context, From 0b1169ee8c8ffdcbde71c81e53a5688e389497de Mon Sep 17 00:00:00 2001 From: Daniele Manni Date: Mon, 22 Nov 2021 20:05:55 +0100 Subject: [PATCH 09/12] [#IC-49] Rename method, rationalize parametes, add comment --- ProcessMessage/handler.ts | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/ProcessMessage/handler.ts b/ProcessMessage/handler.ts index d98faec3..52feeaf7 100644 --- a/ProcessMessage/handler.ts +++ b/ProcessMessage/handler.ts @@ -186,8 +186,8 @@ const getServicePreferenceValueOrError = ( ) ); -type ActivationForSpecialServices = (params: { - readonly createdMessageEvent: CommonMessageData; +type BlockedInboxesForSpecialService = (params: { + readonly senderServiceId: NonEmptyString; readonly fiscalCode: FiscalCode; readonly context: Context; readonly logPrefix: string; @@ -208,14 +208,18 @@ type ActivationForSpecialServices = (params: { * In case the activation status is found to be `ACTIVE` then we remove the INBOX entry * from the list of blocked inboxes. * + * Both Left and Right are valid BlockedInboxOrChannelEnum values. + * The right side contains the blocked inboxes when exists an `ACTIVE` Activation. + * The left side contains the blocked inboxes when the Activation is missing or has status NOT `ACTIVE` + * * * @param lActivation * @returns */ -const getActivationForSpecialServices = ( +const getBlockedInboxesForSpecialService = ( lActivation: ActivationModel -): ActivationForSpecialServices => ({ - createdMessageEvent, +): BlockedInboxesForSpecialService => ({ + senderServiceId, fiscalCode, context, logPrefix, @@ -225,10 +229,7 @@ const getActivationForSpecialServices = ( ReadonlyArray > => pipe( - lActivation.findLastVersionByModelId([ - createdMessageEvent.message.senderServiceId, - fiscalCode - ]), + lActivation.findLastVersionByModelId([senderServiceId, fiscalCode]), TE.mapLeft(activationError => { // The query has failed, we consider this as a transient error. context.log.error(`${logPrefix}|${activationError.kind}`); @@ -463,12 +464,12 @@ export const getProcessMessageHandler = ({ createdMessageEvent.senderMetadata.serviceCategory === SpecialServiceCategoryEnum.SPECIAL ) { - return getActivationForSpecialServices(lActivation)({ + return getBlockedInboxesForSpecialService(lActivation)({ blockedInboxOrChannel, context, - createdMessageEvent, fiscalCode: newMessageWithoutContent.fiscalCode, - logPrefix + logPrefix, + senderServiceId: createdMessageEvent.message.senderServiceId }); } // If the service is STANDARD we use the original value of blockedInboxOrChannel From 988385e5a9d8e747fac476e0b9264c4b5fdb55bd Mon Sep 17 00:00:00 2001 From: Daniele Manni Date: Tue, 30 Nov 2021 14:47:46 +0100 Subject: [PATCH 10/12] [#IC-49] Update admin client specs to master branch --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 678eb9c5..dfd77f05 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "generate": "npm-run-all generate:*", "generate:definitions": "rimraf ./generated/definitions && shx mkdir -p ./generated/definitions && gen-api-models --api-spec ./openapi/index.yaml --no-strict --out-dir ./generated/definitions", "generate:api-notifications": "rimraf ./generated/notifications && shx mkdir -p ./generated/notifications && gen-api-models --api-spec https://raw.githubusercontent.com/pagopa/io-backend/master/api_notifications.yaml --out-dir ./generated/notifications --response-decoders --request-types", - "generate:api-admin": "rimraf generated/api-admin && shx mkdir -p generated/api-admin && gen-api-models --api-spec https://raw.githubusercontent.com/pagopa/io-functions-admin/IC-40-special-service-model-upgrade/openapi/index.yaml --no-strict --out-dir generated/api-admin --request-types --response-decoders --client", + "generate:api-admin": "rimraf generated/api-admin && shx mkdir -p generated/api-admin && gen-api-models --api-spec https://raw.githubusercontent.com/pagopa/io-functions-admin/master/openapi/index.yaml --no-strict --out-dir generated/api-admin --request-types --response-decoders --client", "openapi:bundle": "bundle-api-spec -i openapi/index.yaml.template -o openapi/index.yaml -V $npm_package_version", "openapi:validate": "oval validate -p openapi/index.yaml", "dist:modules": "modclean -r -n default:safe && yarn install --production", From 06a1b5c5cb19d34a8c9004e00a8b60a7134ba019 Mon Sep 17 00:00:00 2001 From: Daniele Manni Date: Tue, 30 Nov 2021 15:23:47 +0100 Subject: [PATCH 11/12] [#IC-49] Replace TaskEither with Task for getBlockedInboxesForSpecialService --- ProcessMessage/handler.ts | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/ProcessMessage/handler.ts b/ProcessMessage/handler.ts index 52feeaf7..0b728daa 100644 --- a/ProcessMessage/handler.ts +++ b/ProcessMessage/handler.ts @@ -33,6 +33,7 @@ import { import { MessageStatusValueEnum } from "@pagopa/io-functions-commons/dist/generated/definitions/MessageStatusValue"; import { ActivationModel } from "@pagopa/io-functions-commons/dist/src/models/activation"; import { ActivationStatusEnum } from "@pagopa/io-functions-commons/dist/generated/definitions/ActivationStatus"; +import * as T from "fp-ts/lib/Task"; import { initTelemetryClient } from "../utils/appinsights"; import { toHash } from "../utils/crypto"; import { PaymentData } from "../generated/definitions/PaymentData"; @@ -192,10 +193,7 @@ type BlockedInboxesForSpecialService = (params: { readonly context: Context; readonly logPrefix: string; readonly blockedInboxOrChannel: ReadonlyArray; -}) => TaskEither< - ReadonlyArray, - ReadonlyArray ->; +}) => T.Task>; /** * Returns the updated value of `blockedInboxOrChannel` for a Service which is marked as `SPECIAL`. @@ -208,11 +206,6 @@ type BlockedInboxesForSpecialService = (params: { * In case the activation status is found to be `ACTIVE` then we remove the INBOX entry * from the list of blocked inboxes. * - * Both Left and Right are valid BlockedInboxOrChannelEnum values. - * The right side contains the blocked inboxes when exists an `ACTIVE` Activation. - * The left side contains the blocked inboxes when the Activation is missing or has status NOT `ACTIVE` - * - * * @param lActivation * @returns */ @@ -224,10 +217,7 @@ const getBlockedInboxesForSpecialService = ( context, logPrefix, blockedInboxOrChannel -}): TaskEither< - ReadonlyArray, - ReadonlyArray -> => +}): T.Task> => pipe( lActivation.findLastVersionByModelId([senderServiceId, fiscalCode]), TE.mapLeft(activationError => { @@ -253,7 +243,11 @@ const getBlockedInboxesForSpecialService = ( ), TE.map(() => blockedInboxOrChannel.filter(el => el !== BlockedInboxOrChannelEnum.INBOX) - ) + ), + // Both Left and Right are valid BlockedInboxOrChannelEnum values. + // The right side contains the blocked inboxes when exists an `ACTIVE` Activation. + // The left side contains the blocked inboxes when the Activation is missing or has status NOT `ACTIVE` + TE.toUnion ); /** @@ -459,7 +453,8 @@ export const getProcessMessageHandler = ({ return result; }), - TE.chain(blockedInboxOrChannel => { + TE.toUnion, + T.chain(blockedInboxOrChannel => { if ( createdMessageEvent.senderMetadata.serviceCategory === SpecialServiceCategoryEnum.SPECIAL @@ -474,9 +469,8 @@ export const getProcessMessageHandler = ({ } // If the service is STANDARD we use the original value of blockedInboxOrChannel // calculated from services preferences and user profile. - return TE.of(blockedInboxOrChannel); - }), - TE.toUnion + return T.of(blockedInboxOrChannel); + }) )(); // check whether the user has blocked inbox storage for messages from this sender From f2482cb01a2c6ae667ece02ba81a2615fe33129c Mon Sep 17 00:00:00 2001 From: Daniele Manni Date: Wed, 1 Dec 2021 11:51:53 +0100 Subject: [PATCH 12/12] [#IC-49] Extend comment to explain how admins can edit SpecialServices --- UpdateService/handler.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/UpdateService/handler.ts b/UpdateService/handler.ts index 231148de..f9f5e349 100644 --- a/UpdateService/handler.ts +++ b/UpdateService/handler.ts @@ -141,6 +141,7 @@ const updateServiceTask = ( ...servicePayload, service_id: serviceId, // Only Admins can change service category and custom_special_flow name + // calling directly the `io-functions-admin` functions. service_metadata: SpecialServiceMetadata.is( retrievedService.service_metadata )