Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/blue-kids-flash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"io-services-cms-webapp": minor
---

implementation of activations sync to legacy function
1 change: 1 addition & 0 deletions .devcontainer/docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,4 @@ services:
AZURE_COSMOS_EMULATOR_ENABLE_DATA_PERSISTENCE: 'true'
# Use the "https" protocol for the emulator
PROTOCOL: https
GATEWAY_PUBLIC_ENDPOINT: https://cosmosdb:8081
13 changes: 13 additions & 0 deletions apps/io-services-cms-webapp/ActivationSyncToLegacy/function.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"bindings": [
{
"name": "activation",
"type": "blobTrigger",
"direction": "in",
"path": "%ACTIVATIONS_CONTAINER_NAME%/{name}",
"connection": "INTERNAL_STORAGE_CONNECTION_STRING"
}
],
"scriptFile": "../dist/main.js",
"entryPoint": "activationsSyncToLegacyEntryPoint"
}
18 changes: 18 additions & 0 deletions apps/io-services-cms-webapp/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import {
ServicePublication,
stores,
} from "@io-services-cms/models";
import {
ACTIVATION_COLLECTION_NAME,
ActivationModel,
} from "@pagopa/io-functions-commons/dist/src/models/activation";
import {
SERVICE_COLLECTION_NAME,
ServiceModel,
Expand Down Expand Up @@ -77,6 +81,7 @@ import {
handler as onIngestionActivationChangeHandler,
parseBlob,
} from "./watchers/on-activation-ingestion-change";
import { makeHandler as makeOnActivationChangeHandler } from "./watchers/on-activations-change";
import { makeHandler as makeOnLegacyActivationChangeHandler } from "./watchers/on-legacy-activations-change";
import { handler as onLegacyServiceChangeHandler } from "./watchers/on-legacy-service-change";
import { makeHandler as makeOnSelfcareGroupChangeHandler } from "./watchers/on-selfcare-group-change";
Expand Down Expand Up @@ -179,8 +184,14 @@ const legacyServicesContainer = cosmosdbClient
.database(config.LEGACY_COSMOSDB_NAME)
.container(SERVICE_COLLECTION_NAME);

const legacyActivationContainer = cosmosdbClient
.database(config.LEGACY_COSMOSDB_NAME)
.container(ACTIVATION_COLLECTION_NAME);

const legacyServiceModel = new ServiceModel(legacyServicesContainer);

const legacyActivationModel = new ActivationModel(legacyActivationContainer);

const blobService = createBlobService(config.ASSET_STORAGE_CONNECTIONSTRING);

// eventhub producer for ServicePublication
Expand Down Expand Up @@ -486,6 +497,13 @@ export const activationsSyncFromLegacyEntryPoint: AzureFunction = (
toAzureFunctionHandler,
)(context, args);

// activation sync to legacy
export const activationsSyncToLegacyEntryPoint = pipe(
{ legacyActivationModel },
makeOnActivationChangeHandler,
toAzureFunctionHandler,
);

//Ingestion Service Publication
export const onIngestionServicePublicationChangeEntryPoint = pipe(
onIngestionServicePublicationChangeHandler(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import * as E from "fp-ts/lib/Either";
import * as TE from "fp-ts/lib/TaskEither";
import { makeHandler } from "../on-activations-change";
import { CosmosErrors } from "@pagopa/io-functions-commons/dist/src/utils/cosmosdb_model";

const mock = vi.hoisted(() => ({
legacyActivationModel: { upsert: vi.fn() },
}));

beforeEach(() => {
vi.restoreAllMocks();
});

describe("makeHandler", () => {
const deps = {
legacyActivationModel: mock.legacyActivationModel,
} as unknown as Parameters<typeof makeHandler>[0];

const validActivation = {
fiscalCode: "RSSMRA80A01H501U",
serviceId: "serviceId",
status: "ACTIVE" as const,
modifiedAt: 1617187200000,
};

it("should fail when the buffer is not valid JSON", async () => {
// given
const invalidBuffer = Buffer.from("invalid json");

// when
const result = await makeHandler(deps)({ inputs: [invalidBuffer] })();

// then
expect(E.isLeft(result)).toBeTruthy();
if (E.isLeft(result)) {
expect(result.left).toBeInstanceOf(Error);
expect(result.left.message).toContain("Unexpected token");
}
expect(mock.legacyActivationModel.upsert).not.toHaveBeenCalled();
});

it("should fail when the buffer is not a valid Activation", async () => {
// given
const invalidActivation = { ...validActivation, status: "WRONG_STATUS" };
const invalidBuffer = Buffer.from(JSON.stringify(invalidActivation));

// when
const result = await makeHandler(deps)({ inputs: [invalidBuffer] })();
// then
expect(E.isLeft(result)).toBeTruthy();
if (E.isLeft(result)) {
expect(result.left).toBeInstanceOf(Error);
expect(result.left.message).toContain(
"at [root.status.0] is not a valid",
);
}
expect(mock.legacyActivationModel.upsert).not.toHaveBeenCalled();
});

it("should fail when upsert fails", async () => {
// given
const validBuffer = Buffer.from(JSON.stringify(validActivation));
const error = new Error("upsert error");
mock.legacyActivationModel.upsert.mockReturnValueOnce(TE.left(error));

// when
const result = await makeHandler(deps)({ inputs: [validBuffer] })();
// then
expect(E.isLeft(result)).toBeTruthy();
if (E.isLeft(result)) {
expect(result.left).toBeInstanceOf(Error);
expect(result.left).toStrictEqual(error);
}
expect(mock.legacyActivationModel.upsert).toHaveBeenCalledOnce();
expect(mock.legacyActivationModel.upsert).toHaveBeenCalledWith({
fiscalCode: validActivation.fiscalCode,
kind: "INewActivation",
serviceId: validActivation.serviceId,
status: "ACTIVE",
});
});

it("should fail when upsert fails for a cosmos empty response", async () => {
// given
const validBuffer = Buffer.from(JSON.stringify(validActivation));
const error: CosmosErrors = { kind: "COSMOS_CONFLICT_RESPONSE" };
mock.legacyActivationModel.upsert.mockReturnValueOnce(TE.left(error));

// when
const result = await makeHandler(deps)({ inputs: [validBuffer] })();
// then
expect(E.isLeft(result)).toBeTruthy();
if (E.isLeft(result)) {
expect(result.left).toBeInstanceOf(Error);
expect(result.left.message).toStrictEqual(error.kind);
}
expect(mock.legacyActivationModel.upsert).toHaveBeenCalledOnce();
expect(mock.legacyActivationModel.upsert).toHaveBeenCalledWith({
fiscalCode: validActivation.fiscalCode,
kind: "INewActivation",
serviceId: validActivation.serviceId,
status: "ACTIVE",
});
});

it("should fail when upsert fails for a cosmos decoding error", async () => {
// given
const validBuffer = Buffer.from(JSON.stringify(validActivation));
const error: CosmosErrors = {
kind: "COSMOS_DECODING_ERROR",
error: [{ context: [], value: "a", message: "b" }],
};
mock.legacyActivationModel.upsert.mockReturnValueOnce(TE.left(error));

// when
const result = await makeHandler(deps)({ inputs: [validBuffer] })();
// then
expect(E.isLeft(result)).toBeTruthy();
if (E.isLeft(result)) {
expect(result.left).toBeInstanceOf(Error);
expect(result.left.message).toStrictEqual(JSON.stringify(error.error));
}
expect(mock.legacyActivationModel.upsert).toHaveBeenCalledOnce();
expect(mock.legacyActivationModel.upsert).toHaveBeenCalledWith({
fiscalCode: validActivation.fiscalCode,
kind: "INewActivation",
serviceId: validActivation.serviceId,
status: "ACTIVE",
});
});

it("should fail when upsert fails for a cosmos error", async () => {
// given
const validBuffer = Buffer.from(JSON.stringify(validActivation));
const error: CosmosErrors = {
kind: "COSMOS_ERROR_RESPONSE",
error: {
code: 500,
message: "internal server error",
name: "Error Response",
},
};
mock.legacyActivationModel.upsert.mockReturnValueOnce(TE.left(error));

// when
const result = await makeHandler(deps)({ inputs: [validBuffer] })();
// then
expect(E.isLeft(result)).toBeTruthy();
if (E.isLeft(result)) {
expect(result.left).toBeInstanceOf(Error);
expect(result.left.message).toStrictEqual(error.error.message);
}
expect(mock.legacyActivationModel.upsert).toHaveBeenCalledOnce();
expect(mock.legacyActivationModel.upsert).toHaveBeenCalledWith({
fiscalCode: validActivation.fiscalCode,
kind: "INewActivation",
serviceId: validActivation.serviceId,
status: "ACTIVE",
});
});

it("should complete successfully when upsert do not fail", async () => {
// given
const validBuffer = Buffer.from(JSON.stringify(validActivation));
mock.legacyActivationModel.upsert.mockReturnValueOnce(TE.right({}));

// when
const result = await makeHandler(deps)({ inputs: [validBuffer] })();
// then
expect(E.isRight(result)).toBeTruthy();
if (E.isRight(result)) {
expect(result.right).toBeUndefined();
}
expect(mock.legacyActivationModel.upsert).toHaveBeenCalledOnce();
expect(mock.legacyActivationModel.upsert).toHaveBeenCalledWith({
fiscalCode: validActivation.fiscalCode,
kind: "INewActivation",
serviceId: validActivation.serviceId,
status: "ACTIVE",
});
});
});
86 changes: 86 additions & 0 deletions apps/io-services-cms-webapp/src/watchers/on-activations-change.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { Activations } from "@io-services-cms/models";
import { ActivationStatusEnum } from "@pagopa/io-functions-commons/dist/generated/definitions/ActivationStatus";
import {
ActivationModel,
NewActivation,
} from "@pagopa/io-functions-commons/dist/src/models/activation";
import { readableReport } from "@pagopa/ts-commons/lib/reporters";
import * as E from "fp-ts/lib/Either";
import * as RTE from "fp-ts/lib/ReaderTaskEither";
import * as TE from "fp-ts/lib/TaskEither";
import { flow, pipe } from "fp-ts/lib/function";

interface HandlerDependencies {
readonly legacyActivationModel: ActivationModel;
}

const cmsToLegacy = (activation: Activations.Activation): NewActivation => ({
fiscalCode: activation.fiscalCode,
kind: "INewActivation",
serviceId: activation.serviceId,
status: toLegacyStatus(activation.status),
});

const toLegacyStatus = (
status: Activations.Activation["status"],
): Activations.LegacyCosmosResource["status"] => {
switch (status) {
case "ACTIVE":
return ActivationStatusEnum.ACTIVE;
case "INACTIVE":
return ActivationStatusEnum.INACTIVE;
case "PENDING":
return ActivationStatusEnum.PENDING;
default:
// Should never happen if validation is correct
return ActivationStatusEnum.INACTIVE;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (non-blocking): use typechecker to be sure you have handled all cases.

Furthermore, consider whether in this “impossible” case it is correct to return a default value (are you sure INACTIVE is the correct default value?) or to throw an error. (throw new Error("Invalid status");)

Suggested change
return ActivationStatusEnum.INACTIVE;
// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-case-declarations
const _: never = status;
return ActivationStatusEnum.INACTIVE;

}
};

export const makeHandler =
({
legacyActivationModel,
}: HandlerDependencies): RTE.ReaderTaskEither<
{ inputs: unknown[] },
Error,
void
> =>
({ inputs }) =>
pipe(
inputs[0] as Buffer,
(blob) => blob.toString("utf-8"),
E.tryCatchK(JSON.parse, E.toError),
E.chain(
flow(
Activations.Activation.decode,
E.mapLeft(flow(readableReport, (e) => new Error(e))),
),
),
TE.fromEither,
TE.chainW((activation) =>
pipe(
cmsToLegacy(activation),
(newActivation) => legacyActivationModel.upsert(newActivation),
TE.mapLeft((err) => {
if (err instanceof Error) {
return err;
} else {
switch (err.kind) {
case "COSMOS_EMPTY_RESPONSE":
case "COSMOS_CONFLICT_RESPONSE":
return new Error(err.kind);
case "COSMOS_DECODING_ERROR":
return E.toError(JSON.stringify(err.error));
case "COSMOS_ERROR_RESPONSE":
return E.toError(err.error.message);
default:
// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-case-declarations
const _: never = err;
return new Error(`should not have executed this with ${err}`);
}
}
}),
),
),
TE.map(() => void 0),
);
3 changes: 3 additions & 0 deletions infra/resources/_modules/cms_function_app/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ module "cms_fn" {
"AzureWebJobs.SelfcareGroupWatcher.Disabled" = "0"
"AzureWebJobs.IngestionActivationWatcher.Disabled" = "0"
"AzureWebJobs.ActivationsSyncFromLegacy.Disabled" = "0"
"AzureWebJobs.ActivationsSyncToLegacy.Disabled" = "1"

}
)
Expand Down Expand Up @@ -94,6 +95,7 @@ module "cms_fn" {
"AzureWebJobs.SelfcareGroupWatcher.Disabled" = "1"
"AzureWebJobs.IngestionActivationWatcher.Disabled" = "1"
"AzureWebJobs.ActivationsSyncFromLegacy.Disabled" = "1"
"AzureWebJobs.ActivationsSyncToLegacy.Disabled" = "1"
}
)

Expand Down Expand Up @@ -125,6 +127,7 @@ module "cms_fn" {
"AzureWebJobs.SelfcareGroupWatcher.Disabled",
"AzureWebJobs.IngestionActivationWatcher.Disabled",
"AzureWebJobs.ActivationsSyncFromLegacy.Disabled",
"AzureWebJobs.ActivationsSyncToLegacy.Disabled",
]

tier = local.cms.tier
Expand Down
Loading