diff --git a/GetCgnStatus/handler.ts b/GetCgnStatus/handler.ts index fbf2cdc..f160c26 100644 --- a/GetCgnStatus/handler.ts +++ b/GetCgnStatus/handler.ts @@ -4,7 +4,6 @@ import { Context } from "@azure/functions"; import { fromOption } from "fp-ts/lib/Either"; import { identity } from "fp-ts/lib/function"; import { fromEither } from "fp-ts/lib/TaskEither"; -import { fromLeft } from "fp-ts/lib/TaskEither"; import { ContextMiddleware } from "io-functions-commons/dist/src/utils/middlewares/context_middleware"; import { RequiredParamMiddleware } from "io-functions-commons/dist/src/utils/middlewares/required_param"; import { @@ -21,7 +20,7 @@ import { } from "italia-ts-commons/lib/responses"; import { FiscalCode } from "italia-ts-commons/lib/strings"; import { Card } from "../generated/definitions/Card"; -import { UserCgn, UserCgnModel } from "../models/user_cgn"; +import { UserCgnModel } from "../models/user_cgn"; type ResponseTypes = | IResponseSuccessJson @@ -39,17 +38,15 @@ export function GetCgnStatusHandler( return async (_, fiscalCode) => { return userCgnModel .findLastVersionByModelId([fiscalCode]) - .mapLeft(() => + .mapLeft(() => ResponseErrorInternal("Error trying to retrieve user's CGN status") ) - .foldTaskEither( - fromLeft, - maybeUserCgn => - fromEither( - fromOption( - ResponseErrorNotFound("Not Found", "User's CGN status not found") - )(maybeUserCgn) - ) + .chain(maybeUserCgn => + fromEither( + fromOption( + ResponseErrorNotFound("Not Found", "User's CGN status not found") + )(maybeUserCgn) + ) ) .fold(identity, userCgn => ResponseSuccessJson(userCgn.card) diff --git a/GetEycaStatus/__tests__/handler.test.ts b/GetEycaStatus/__tests__/handler.test.ts new file mode 100644 index 0000000..0d036ca --- /dev/null +++ b/GetEycaStatus/__tests__/handler.test.ts @@ -0,0 +1,101 @@ +/* tslint:disable: no-any */ + +import * as date_fns from "date-fns"; +import { some } from "fp-ts/lib/Option"; +import { none } from "fp-ts/lib/Option"; +import { fromLeft, taskEither } from "fp-ts/lib/TaskEither"; +import { FiscalCode } from "italia-ts-commons/lib/strings"; +import { NonEmptyString } from "italia-ts-commons/lib/strings"; +import { cgnActivatedDates, now } from "../../__mocks__/mock"; +import { StatusEnum as ActivatedStatusEnum } from "../../generated/definitions/CardActivated"; +import { + CardPending, + StatusEnum as PendingStatusEnum +} from "../../generated/definitions/CardPending"; +import { StatusEnum as RevokedStatusEnum } from "../../generated/definitions/CardRevoked"; +import { EycaCardActivated } from "../../generated/definitions/EycaCardActivated"; +import { EycaCardRevoked } from "../../generated/definitions/EycaCardRevoked"; +import { UserEycaCard, UserEycaCardModel } from "../../models/user_eyca_card"; +import { GetEycaStatusHandler } from "../handler"; + +const aFiscalCode = "RODFDS82S10H501T" as FiscalCode; +const aUserEycaCardNumber = "AN_ID" as NonEmptyString; + +const aPendingEycaCard: CardPending = { + status: PendingStatusEnum.PENDING +}; + +const aUserEycaCard: UserEycaCard = { + card: aPendingEycaCard, + fiscalCode: aFiscalCode +}; + +const aRevokedEycaCard: EycaCardRevoked = { + ...cgnActivatedDates, + card_number: aUserEycaCardNumber, + revocation_date: now, + revocation_reason: "A motivation" as NonEmptyString, + status: RevokedStatusEnum.REVOKED +}; + +const findLastVersionByModelIdMock = jest + .fn() + .mockImplementation(() => taskEither.of(some(aUserEycaCard))); +const userEycaCardModelMock = { + findLastVersionByModelId: findLastVersionByModelIdMock +}; + +const anActivatedEycaCard: EycaCardActivated = { + activation_date: now, + card_number: aUserEycaCardNumber, + expiration_date: date_fns.addDays(now, 10), + status: ActivatedStatusEnum.ACTIVATED +}; + +const successImpl = async (userEycaCard: UserEycaCard) => { + const handler = GetEycaStatusHandler(userEycaCardModelMock as any); + const response = await handler({} as any, aFiscalCode); + expect(response.kind).toBe("IResponseSuccessJson"); + if (response.kind === "IResponseSuccessJson") { + expect(response.value).toEqual({ + ...userEycaCard.card + }); + } +}; +describe("GetEycaCardStatusHandler", () => { + it("should return an internal error when a query error occurs", async () => { + findLastVersionByModelIdMock.mockImplementationOnce(() => + fromLeft(new Error("Query Error")) + ); + const handler = GetEycaStatusHandler(userEycaCardModelMock as any); + const response = await handler({} as any, aFiscalCode); + expect(response.kind).toBe("IResponseErrorInternal"); + }); + + it("should return not found if no userEycaCard is found", async () => { + findLastVersionByModelIdMock.mockImplementationOnce(() => + taskEither.of(none) + ); + const handler = GetEycaStatusHandler(userEycaCardModelMock as any); + const response = await handler({} as any, aFiscalCode); + expect(response.kind).toBe("IResponseErrorNotFound"); + }); + + it("should return success if a pending userEycaCard is found", async () => { + await successImpl(aUserEycaCard); + }); + + it("should return success if a revoked userEycaCard is found", async () => { + findLastVersionByModelIdMock.mockImplementationOnce(() => + taskEither.of(some({ ...aUserEycaCard, card: aRevokedEycaCard })) + ); + await successImpl({ ...aUserEycaCard, card: aRevokedEycaCard }); + }); + + it("should return success if an activated userEycaCard is found", async () => { + findLastVersionByModelIdMock.mockImplementationOnce(() => + taskEither.of(some({ ...aUserEycaCard, card: anActivatedEycaCard })) + ); + await successImpl({ ...aUserEycaCard, card: anActivatedEycaCard }); + }); +}); diff --git a/GetEycaStatus/function.json b/GetEycaStatus/function.json new file mode 100644 index 0000000..375e6b9 --- /dev/null +++ b/GetEycaStatus/function.json @@ -0,0 +1,20 @@ +{ + "bindings": [ + { + "authLevel": "function", + "type": "httpTrigger", + "direction": "in", + "name": "req", + "route": "api/v1/cgn/eyca/status/{fiscalcode}", + "methods": [ + "get" + ] + }, + { + "type": "http", + "direction": "out", + "name": "res" + } + ], + "scriptFile": "../dist/GetEycaStatus/index.js" +} diff --git a/GetEycaStatus/handler.ts b/GetEycaStatus/handler.ts new file mode 100644 index 0000000..6fac3c1 --- /dev/null +++ b/GetEycaStatus/handler.ts @@ -0,0 +1,75 @@ +import * as express from "express"; + +import { Context } from "@azure/functions"; +import { fromOption } from "fp-ts/lib/Either"; +import { identity } from "fp-ts/lib/function"; +import { fromEither } from "fp-ts/lib/TaskEither"; +import { ContextMiddleware } from "io-functions-commons/dist/src/utils/middlewares/context_middleware"; +import { RequiredParamMiddleware } from "io-functions-commons/dist/src/utils/middlewares/required_param"; +import { + withRequestMiddlewares, + wrapRequestHandler +} from "io-functions-commons/dist/src/utils/request_middleware"; +import { + IResponseErrorInternal, + IResponseErrorNotFound, + IResponseSuccessJson, + ResponseErrorInternal, + ResponseErrorNotFound, + ResponseSuccessJson +} from "italia-ts-commons/lib/responses"; +import { FiscalCode } from "italia-ts-commons/lib/strings"; + +import { EycaCard } from "../generated/definitions/EycaCard"; +import { UserEycaCardModel } from "../models/user_eyca_card"; + +type ResponseTypes = + | IResponseSuccessJson + | IResponseErrorNotFound + | IResponseErrorInternal; + +type IGetEycaStatusHandler = ( + context: Context, + fiscalCode: FiscalCode +) => Promise; + +export function GetEycaStatusHandler( + userEycaCardModel: UserEycaCardModel +): IGetEycaStatusHandler { + return async (_, fiscalCode) => { + return userEycaCardModel + .findLastVersionByModelId([fiscalCode]) + .mapLeft(() => + ResponseErrorInternal( + "Error trying to retrieve user's EYCA Card status" + ) + ) + .chain(maybeUserEycaCard => + fromEither( + fromOption( + ResponseErrorNotFound( + "Not Found", + "User's EYCA Card status not found" + ) + )(maybeUserEycaCard) + ) + ) + .fold(identity, userEycaCard => + ResponseSuccessJson(userEycaCard.card) + ) + .run(); + }; +} + +export function GetEycaStatus( + userEycaCardModel: UserEycaCardModel +): express.RequestHandler { + const handler = GetEycaStatusHandler(userEycaCardModel); + + const middlewaresWrap = withRequestMiddlewares( + ContextMiddleware(), + RequiredParamMiddleware("fiscalcode", FiscalCode) + ); + + return wrapRequestHandler(middlewaresWrap(handler)); +} diff --git a/GetEycaStatus/index.ts b/GetEycaStatus/index.ts new file mode 100644 index 0000000..45500ed --- /dev/null +++ b/GetEycaStatus/index.ts @@ -0,0 +1,56 @@ +import * as express from "express"; +import * as winston from "winston"; + +import { Context } from "@azure/functions"; +import { secureExpressApp } from "io-functions-commons/dist/src/utils/express"; +import { AzureContextTransport } from "io-functions-commons/dist/src/utils/logging"; +import { setAppContext } from "io-functions-commons/dist/src/utils/middlewares/context_middleware"; +import createAzureFunctionHandler from "io-functions-express/dist/src/createAzureFunctionsHandler"; + +import { + USER_EYCA_CARD_COLLECTION_NAME, + UserEycaCardModel +} from "../models/user_eyca_card"; +import { getConfigOrThrow } from "../utils/config"; +import { cosmosdbClient } from "../utils/cosmosdb"; +import { GetEycaStatus } from "./handler"; + +// +// CosmosDB initialization +// + +const config = getConfigOrThrow(); + +const userEycaCardsContainer = cosmosdbClient + .database(config.COSMOSDB_CGN_DATABASE_NAME) + .container(USER_EYCA_CARD_COLLECTION_NAME); + +const userEycaCardModel = new UserEycaCardModel(userEycaCardsContainer); + +// tslint:disable-next-line: no-let +let logger: Context["log"] | undefined; +const contextTransport = new AzureContextTransport(() => logger, { + level: "debug" +}); +winston.add(contextTransport); + +// Setup Express +const app = express(); +secureExpressApp(app); + +// Add express route +app.get( + "/api/v1/cgn/eyca/status/:fiscalcode", + GetEycaStatus(userEycaCardModel) +); + +const azureFunctionHandler = createAzureFunctionHandler(app); + +// Binds the express app to an Azure Function handler +function httpStart(context: Context): void { + logger = context.log; + setAppContext(app, context); + azureFunctionHandler(context); +} + +export default httpStart; diff --git a/openapi/index.yaml b/openapi/index.yaml index 907bd86..2a3a38d 100755 --- a/openapi/index.yaml +++ b/openapi/index.yaml @@ -84,6 +84,33 @@ paths: description: Service unavailable. schema: $ref: "#/definitions/ProblemJson" + + "/cgn/eyca/status/{fiscalcode}": + get: + summary: Get EYCA details Status + operationId: getEycaStatus + description: | + Get the EYCA status details by the provided fiscal code. + In case of success the response could be one of: + - CardPending + - EycaCardActivated + - EycaCardRevoked + - EycaCardExpired + parameters: + - $ref: "#/parameters/FiscalCode" + responses: + "200": + description: EYCA details. + schema: + $ref: "#/definitions/EycaCard" + "401": + description: Wrong or missing function key. + "404": + description: No EYCA found. + "500": + description: Service unavailable. + schema: + $ref: "#/definitions/ProblemJson" "/cgn/{fiscalcode}/activation": post: