diff --git a/package.json b/package.json index e3daf1bc..dc45a6e7 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "api_fast_login": "https://raw.githubusercontent.com/pagopa/io-backend/v13.32.1-RELEASE/openapi/generated/api_fast_login.yaml", "api_pagopa_walletv3": "https://raw.githubusercontent.com/pagopa/pagopa-infra/bec0f57606a2002706bd5f5286fad3e508d2f50e/src/domains/wallet-app/api/payment-wallet/v1/_openapi.json.tpl", "api_pagopa_ecommerce": "https://raw.githubusercontent.com/pagopa/pagopa-infra/2adee4c0a8de8570e74710379f9cccb04c6925b6/src/domains/ecommerce-app/api/ecommerce-io/v1/_openapi.json.tpl", + "api_services": "https://raw.githubusercontent.com/pagopa/io-services-cms/master/apps/app-backend/openapi.yaml", "author": "Matteo Boschi", "license": "MIT", "private": false, @@ -55,6 +56,7 @@ "generate:idpay-definitions": "rimraf generated/definitions/idpay && mkdir -p generated/definitions/idpay && gen-api-models --api-spec $npm_package_api_idpay --out-dir ./generated/definitions/idpay --no-strict", "generate:fast-login-definitions": "rimraf generated/definitions/fast_login && mkdir -p generated/definitions/fast_login && gen-api-models --api-spec $npm_package_api_fast_login --out-dir ./generated/definitions/fast_login --no-strict --request-types --response-decoders", "generate:pagopa": "npm-run-all generate:pagopa-walletv2-definitions generate:pagopa-privative-configuration-definitions generate:pagopa-cobadge-configuration-definitions generate:pagopa-walletv3-definitions generate:pagopa-ecommerce-definitions", + "generate:services-definitions": "rimraf generated/definitions/services && mkdir -p generated/definitions/services && gen-api-models --api-spec $npm_package_api_services --out-dir ./generated/definitions/services --no-strict", "generate": "npm-run-all generate:*" }, "jest": { diff --git a/src/config.ts b/src/config.ts index 3d5e4325..0484a2dd 100644 --- a/src/config.ts +++ b/src/config.ts @@ -189,6 +189,11 @@ const defaultConfig: IoDevServerConfig = { fastLogin: { sessionTTLinMS: 60000 }, + service: { + response: { + institutionsResponseCode: 200 + } + }, allowRandomValues: true } }; diff --git a/src/features/services/index.ts b/src/features/services/index.ts new file mode 100644 index 00000000..4a3d1385 --- /dev/null +++ b/src/features/services/index.ts @@ -0,0 +1 @@ +export { serviceRouter } from "./routers"; diff --git a/src/features/services/payloads/get-institutions.ts b/src/features/services/payloads/get-institutions.ts new file mode 100644 index 00000000..72b8d3e2 --- /dev/null +++ b/src/features/services/payloads/get-institutions.ts @@ -0,0 +1,73 @@ +import * as A from "fp-ts/lib/Array"; +import { identity, pipe } from "fp-ts/lib/function"; +import _ from "lodash"; +import { ServicePublic } from "../../../../generated/definitions/backend/ServicePublic"; +import { ServiceScopeEnum } from "../../../../generated/definitions/backend/ServiceScope"; +import { Institution } from "../../../../generated/definitions/services/Institution"; +import { InstitutionsResource } from "../../../../generated/definitions/services/InstitutionsResource"; +import ServicesDB from "../../../persistence/services"; + +const filterByScope = (service: ServicePublic, scope?: ServiceScopeEnum) => { + if (!scope) { + return true; + } + return service.service_metadata?.scope === scope; +}; + +const filterBySearch = (service: ServicePublic, search?: string) => { + if (!search) { + return true; + } + return service.service_name.toLowerCase().includes(search); +}; + +// services?offset=0&limit=20 +// services?offset=20&limit=20 +// services?offset=40&limit=20 +export const getInstitutionsResponsePayload = ( + limit: number = 20, + offset: number = 0, + scope?: ServiceScopeEnum, + search?: string +): InstitutionsResource => { + const filteredInstitutions = pipe( + ServicesDB.getAllServices(), + A.reduce([] as Institution[], (accumulator, service) => { + const isValidService = pipe( + [ + (service: ServicePublic) => filterByScope(service, scope), + (service: ServicePublic) => filterBySearch(service, search) + ], + A.flap(service), + A.every(identity) + ); + + if (isValidService) { + return [ + ...accumulator, + { + id: service.service_id, + name: service.service_name, + fiscal_code: service.organization_fiscal_code + } + ]; + } + + return accumulator; + }) + ); + + const totalElements = filteredInstitutions.length; + const startIndex = offset; + const endIndex = offset + limit; + const istitutionList = _.slice(filteredInstitutions, startIndex, endIndex); + + const response: InstitutionsResource = { + institutions: istitutionList, + limit, + offset, + count: totalElements + }; + + return response; +}; diff --git a/src/features/services/routers/index.ts b/src/features/services/routers/index.ts new file mode 100644 index 00000000..9e98f1bc --- /dev/null +++ b/src/features/services/routers/index.ts @@ -0,0 +1,3 @@ +import "./institutions"; + +export { serviceRouter } from "./router"; diff --git a/src/features/services/routers/institutions.ts b/src/features/services/routers/institutions.ts new file mode 100644 index 00000000..4866aa99 --- /dev/null +++ b/src/features/services/routers/institutions.ts @@ -0,0 +1,52 @@ +import { sequenceT } from "fp-ts/lib/Apply"; +import * as O from "fp-ts/lib/Option"; +import { pipe } from "fp-ts/lib/function"; +import { addHandler } from "../../../payloads/response"; +import { addApiV1Prefix } from "../../../utils/strings"; +import { ServiceScope } from "../../../../generated/definitions/backend/ServiceScope"; +import { getInstitutionsResponsePayload } from "../payloads/get-institutions"; +import { ioDevServerConfig } from "../../../config"; +import { serviceRouter } from "./router"; + +type Query = string | qs.ParsedQs | string[] | qs.ParsedQs[] | undefined; + +const extractQuery = (query: Query) => + pipe( + query, + O.fromNullable, + O.map(s => parseInt(s as string, 10)), + O.toUndefined + ); + +addHandler(serviceRouter, "get", addApiV1Prefix("/institutions"), (req, res) => + pipe( + ioDevServerConfig.features.service.response.institutionsResponseCode, + O.fromPredicate(statusCode => statusCode !== 200), + O.fold( + () => + pipe( + sequenceT(O.Monad)( + O.of(pipe(req.query.limit, extractQuery)), + O.of(pipe(req.query.offset, extractQuery)), + O.of( + pipe( + req.query.scope, + ServiceScope.decode, + O.fromEither, + O.toUndefined + ) + ), + O.of( + pipe(req.query.search as string, O.fromNullable, O.toUndefined) + ) + ), + O.map(args => getInstitutionsResponsePayload(...args)), + O.fold( + () => res.status(404), + instituitions => res.status(200).json(instituitions) + ) + ), + statusCode => res.sendStatus(statusCode) + ) + ) +); diff --git a/src/features/services/routers/router.ts b/src/features/services/routers/router.ts new file mode 100644 index 00000000..43d1ab04 --- /dev/null +++ b/src/features/services/routers/router.ts @@ -0,0 +1,3 @@ +import { Router } from "express"; + +export const serviceRouter = Router(); diff --git a/src/features/services/types/configuration.ts b/src/features/services/types/configuration.ts new file mode 100644 index 00000000..af7eba9c --- /dev/null +++ b/src/features/services/types/configuration.ts @@ -0,0 +1,12 @@ +import * as t from "io-ts"; +import { HttpResponseCode } from "../../../types/httpResponseCode"; + +export const ServiceConfiguration = t.interface({ + // configure some API response error code + response: t.interface({ + // 200 success with payload + institutionsResponseCode: HttpResponseCode + }) +}); + +export type ServiceConfiguration = t.TypeOf; diff --git a/src/persistence/services.ts b/src/persistence/services.ts index dfe2b4c8..26562c1e 100644 --- a/src/persistence/services.ts +++ b/src/persistence/services.ts @@ -90,6 +90,12 @@ const deleteServices = () => { servicePreferences.clear(); }; +const getAllServices = () => [ + ...localServices, + ...nationalServices, + ...specialServices +]; + const getLocalServices = () => localServices.map(ls => ({ ...ls })); const getPreference = ( @@ -158,6 +164,7 @@ const updatePreference = ( export default { createServices, deleteServices, + getAllServices, getLocalServices, getPreference, getService, diff --git a/src/routers/service.ts b/src/routers/service.ts index 62f62b31..254d6bd7 100644 --- a/src/routers/service.ts +++ b/src/routers/service.ts @@ -1,5 +1,7 @@ import { Router } from "express"; +import { pipe } from "fp-ts/lib/function"; import * as E from "fp-ts/lib/Either"; +import * as O from "fp-ts/lib/Option"; import { ServiceId } from "../../generated/definitions/backend/ServiceId"; import { ServicePreference } from "../../generated/definitions/backend/ServicePreference"; import { UpsertServicePreference } from "../../generated/definitions/backend/UpsertServicePreference"; @@ -35,38 +37,60 @@ addHandler( serviceRouter, "get", addApiV1Prefix("/services/:service_id"), - (req, res) => { - if (configResponse.getServiceResponseCode !== 200) { - res.sendStatus(configResponse.getServiceResponseCode); - return; - } - const serviceId = req.params.service_id as ServiceId; - const service = ServicesDB.getService(serviceId); - if (service === undefined) { - res.sendStatus(404); - return; - } - res.json(service); - } + (req, res) => + pipe( + configResponse.getServiceResponseCode, + O.fromPredicate(statusCode => statusCode !== 200), + O.fold( + () => + pipe( + req.params.service_id, + O.fromNullable, + O.chain(serviceId => + pipe( + serviceId as ServiceId, + ServicesDB.getService, + O.fromNullable + ) + ), + O.fold( + () => res.sendStatus(404), + service => res.status(200).json(service) + ) + ), + statusCode => res.sendStatus(statusCode) + ) + ) ); addHandler( serviceRouter, "get", addApiV1Prefix("/services/:service_id/preferences"), - (req, res) => { - if (configResponse.getServicesPreference !== 200) { - res.sendStatus(configResponse.getServicesPreference); - return; - } - const serviceId = req.params.service_id as ServiceId; - const servicePreference = ServicesDB.getPreference(serviceId); - if (servicePreference === undefined) { - res.sendStatus(404); - return; - } - res.json(servicePreference); - } + (req, res) => + pipe( + configResponse.getServicesPreference, + O.fromPredicate(statusCode => statusCode !== 200), + O.fold( + () => + pipe( + req.params.service_id, + O.fromNullable, + O.chain(serviceId => + pipe( + serviceId as ServiceId, + ServicesDB.getPreference, + O.fromNullable + ) + ), + O.fold( + () => res.sendStatus(404), + servicePreference => res.status(200).json(servicePreference) + ) + ), + statusCode => res.sendStatus(statusCode) + ) + ) ); addHandler( @@ -98,17 +122,12 @@ addHandler( res.sendStatus(409); return; } - const increasedSettingsVersion = - ((servicePreference.settings_version as number) + - 1) as ServicePreference["settings_version"]; - const updatedServicePreference = { + + const persistedServicePreference = ServicesDB.updatePreference(serviceId, { ...updatedPreference, - settings_version: increasedSettingsVersion - } as ServicePreference; - const persistedServicePreference = ServicesDB.updatePreference( - serviceId, - updatedServicePreference - ); + settings_version: servicePreference.settings_version + 1 + } as ServicePreference); + if (!persistedServicePreference) { res.sendStatus(500); return; diff --git a/src/server.ts b/src/server.ts index 130bb81b..8bffa288 100644 --- a/src/server.ts +++ b/src/server.ts @@ -33,6 +33,7 @@ import { satispayRouter } from "./routers/walletsV2/methods/satispay"; import { payPalRouter } from "./routers/walletsV3/methods/paypal"; import { delayer } from "./utils/delay_middleware"; import { walletRouter as newWalletRouter } from "./features/wallet"; +import { serviceRouter as newServiceRouter } from "./features/services"; import { dashboardHomeRouter } from "./routers/configHomeDashboard/configHomeDashboard"; // create express server @@ -80,7 +81,8 @@ app.use(fastLoginMiddleware); idpayRouter, lollipopRouter, fastLoginRouter, - newWalletRouter + newWalletRouter, + newServiceRouter ].forEach(r => app.use(r)); export default app; diff --git a/src/types/config.ts b/src/types/config.ts index c5250658..472b217a 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -15,6 +15,7 @@ import { PushNotificationsContentType } from "../../generated/definitions/backen import { ReminderStatus } from "../../generated/definitions/backend/ReminderStatus"; import { MessagesConfig } from "../features/messages/types/messagesConfig"; import { WalletConfiguration } from "../features/wallet/types/configuration"; +import { ServiceConfiguration } from "../features/services/types/configuration"; import { AllowRandomValue } from "./allowRandomValue"; import { HttpResponseCode } from "./httpResponseCode"; @@ -209,7 +210,8 @@ export const IoDevServerConfig = t.interface({ t.partial({ assertionRefValidityMS: t.number }) - ]) + ]), + service: ServiceConfiguration }), t.partial({ wallet: WalletConfiguration