Skip to content

Commit

Permalink
Merge pull request #364 from pagopa/IOPAE-1092-api-institutions
Browse files Browse the repository at this point in the history
[IOPAE-1092] Add `institutions` endpoint
  • Loading branch information
adelloste authored Apr 23, 2024
2 parents b881405 + 145a081 commit 7f283d3
Show file tree
Hide file tree
Showing 13 changed files with 250 additions and 38 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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/8b8a5b0c3ebc007d9b8f1a8dc4ae9b88b63b5794/src/domains/wallet-app/api/payment-wallet/v1/_openapi.json.tpl",
"api_pagopa_ecommerce": "https://raw.githubusercontent.com/pagopa/pagopa-infra/8b8a5b0c3ebc007d9b8f1a8dc4ae9b88b63b5794/src/domains/ecommerce-app/api/ecommerce-io/v1/_openapi.json.tpl",
"api_services": "https://raw.githubusercontent.com/pagopa/io-backend/master/api_services_app_backend.yaml",
"author": "Matteo Boschi",
"license": "MIT",
"private": false,
Expand Down Expand Up @@ -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": {
Expand Down
5 changes: 5 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,11 @@ const defaultConfig: IoDevServerConfig = {
fastLogin: {
sessionTTLinMS: 60000
},
service: {
response: {
institutionsResponseCode: 200
}
},
allowRandomValues: true
}
};
Expand Down
1 change: 1 addition & 0 deletions src/features/services/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { serviceRouter } from "./routers";
74 changes: 74 additions & 0 deletions src/features/services/payloads/get-institutions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import * as A from "fp-ts/lib/Array";
import { identity, pipe } from "fp-ts/lib/function";
import _ from "lodash";
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";
import { InstitutionWithScope, getInstitutions } from "../utils/institutions";

const filterByScope = (
institution: InstitutionWithScope,
scope?: ServiceScopeEnum
) => {
if (!scope) {
return true;
}
return institution.scope === scope;
};

const filterBySearch = (institution: InstitutionWithScope, search?: string) => {
if (!search) {
return true;
}
return institution.name.toLowerCase().includes(search);
};

export const getInstitutionsResponsePayload = (
limit: number = 20,
offset: number = 0,
scope?: ServiceScopeEnum,
search?: string
): InstitutionsResource => {
const filteredInstitutions = pipe(
ServicesDB.getAllServices(),
getInstitutions,
A.reduce([] as Institution[], (accumulator, institution) => {
const isValidInstitution = pipe(
[
(institution: InstitutionWithScope) =>
filterByScope(institution, scope),
(institution: InstitutionWithScope) =>
filterBySearch(institution, search)
],
A.flap(institution),
A.every(identity)
);

if (isValidInstitution) {
return [
...accumulator,
{
id: institution.id,
name: institution.name,
fiscal_code: institution.fiscal_code
}
];
}

return accumulator;
})
);

const totalElements = filteredInstitutions.length;
const startIndex = offset;
const endIndex = offset + limit;
const istitutionList = _.slice(filteredInstitutions, startIndex, endIndex);

return {
institutions: istitutionList,
limit,
offset,
count: totalElements
};
};
3 changes: 3 additions & 0 deletions src/features/services/routers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import "./institutions";

export { serviceRouter } from "./router";
54 changes: 54 additions & 0 deletions src/features/services/routers/institutions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { sequenceT } from "fp-ts/lib/Apply";
import * as O from "fp-ts/lib/Option";
import { pipe } from "fp-ts/lib/function";
import { ServiceScope } from "../../../../generated/definitions/backend/ServiceScope";
import { ioDevServerConfig } from "../../../config";
import { addHandler } from "../../../payloads/response";
import { getInstitutionsResponsePayload } from "../payloads/get-institutions";
import { addApiV2Prefix, serviceRouter } from "./router";

const serviceConfig = ioDevServerConfig.features.service;

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
);

// Find institutions
addHandler(serviceRouter, "get", addApiV2Prefix("/institutions"), (req, res) =>
pipe(
serviceConfig.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)
)
)
);
7 changes: 7 additions & 0 deletions src/features/services/routers/router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Router } from "express";

export const serviceRouter = Router();

export const SERVICES_PREFIX = "/api/v2";

export const addApiV2Prefix = (path: string) => `${SERVICES_PREFIX}${path}`;
12 changes: 12 additions & 0 deletions src/features/services/types/configuration.ts
Original file line number Diff line number Diff line change
@@ -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<typeof ServiceConfiguration>;
24 changes: 24 additions & 0 deletions src/features/services/utils/institutions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import * as A from "fp-ts/lib/Array";
import { pipe } from "fp-ts/lib/function";
import { ServicePublic } from "../../../../generated/definitions/backend/ServicePublic";
import { ServiceScopeEnum } from "../../../../generated/definitions/backend/ServiceScope";
import { Institution } from "../../../../generated/definitions/services/Institution";

export type InstitutionWithScope = Institution & { scope?: ServiceScopeEnum };

export const getInstitutions = (
services: ServicePublic[]
): InstitutionWithScope[] =>
pipe(
services,
A.uniq({
equals: (x, y) =>
x.organization_fiscal_code === y.organization_fiscal_code
}),
A.map(service => ({
id: service.organization_fiscal_code,
name: service.organization_name,
fiscal_code: service.organization_fiscal_code,
scope: service.service_metadata?.scope
}))
);
7 changes: 7 additions & 0 deletions src/persistence/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,12 @@ const deleteServices = () => {
servicePreferences.clear();
};

const getAllServices = () => [
...localServices,
...nationalServices,
...specialServices
];

const getLocalServices = () => localServices.map(ls => ({ ...ls }));

const getPreference = (
Expand Down Expand Up @@ -158,6 +164,7 @@ const updatePreference = (
export default {
createServices,
deleteServices,
getAllServices,
getLocalServices,
getPreference,
getService,
Expand Down
91 changes: 55 additions & 36 deletions src/routers/service.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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;
Expand Down
4 changes: 3 additions & 1 deletion src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -80,7 +81,8 @@ app.use(fastLoginMiddleware);
idpayRouter,
lollipopRouter,
fastLoginRouter,
newWalletRouter
newWalletRouter,
newServiceRouter
].forEach(r => app.use(r));

export default app;
4 changes: 3 additions & 1 deletion src/types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -209,7 +210,8 @@ export const IoDevServerConfig = t.interface({
t.partial({
assertionRefValidityMS: t.number
})
])
]),
service: ServiceConfiguration
}),
t.partial({
wallet: WalletConfiguration
Expand Down

0 comments on commit 7f283d3

Please sign in to comment.