Skip to content

Commit 7a544ee

Browse files
feat: added the isFiscalCodeWhitelisted() endpoint to check if the fiscal code is whitelisted
Refs: #1209 SIW-2188
1 parent 40ea3b5 commit 7a544ee

File tree

7 files changed

+231
-1
lines changed

7 files changed

+231
-1
lines changed

.changeset/quick-radios-brush.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@pagopa/io-backend": minor
3+
---
4+
5+
Added GET /whitelisted-fiscal-code/{fiscalCode} IO Wallet endpoint to check if the fiscal code is whitelisted

api_io_wallet.yaml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,22 @@ paths:
152152
$ref: "#/components/responses/Unexpected"
153153
"503":
154154
$ref: "#/components/responses/ServiceUnavailable"
155+
156+
/whitelisted-fiscal-code:
157+
get:
158+
operationId: isFiscalCodeWhitelisted
159+
summary: Used to check if the fiscal code is in the whitelist
160+
responses:
161+
"200":
162+
description: Returned when no issues were found in either case (fiscal code whitelisted, or not)
163+
content:
164+
application/json:
165+
schema:
166+
$ref: "#/components/schemas/WhitelistedFiscalCodeData"
167+
"500":
168+
$ref: "#/components/responses/Unexpected"
169+
"503":
170+
$ref: "#/components/responses/ServiceUnavailable"
155171

156172
components:
157173
securitySchemes:
@@ -287,6 +303,26 @@ components:
287303
- id
288304
- is_revoked
289305

306+
WhitelistedFiscalCodeData:
307+
type: object
308+
required:
309+
- whitelisted
310+
- fiscalCode
311+
properties:
312+
whitelisted:
313+
type: boolean
314+
description: |-
315+
Boolean value that specifies whether the tax code is whitelisted or not.
316+
whitelistedAt:
317+
type: string
318+
description: |-
319+
Date, expressed in ISO 8601 format, when the fiscal code was added to the whitelist.
320+
Only present if the fiscal code is whitelisted.
321+
fiscalCode:
322+
type: string
323+
description: |-
324+
The fiscal code, the same one passed in input.
325+
290326
RevocationReason:
291327
type: string
292328
enum:

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
"generate:proxy:api-session": "rimraf generated/session && gen-api-models --api-spec api_session.yaml --out-dir generated/session",
6363
"generate:lollipop-definitions": "rimraf generated/lollipop && gen-api-models --api-spec openapi/lollipop_definitions.yaml --out-dir generated/lollipop",
6464
"generate:lollipop-first-sign": "rimraf generated/lollipop-first-consumer && gen-api-models --api-spec openapi/consumed/lollipop_first_consumer.yaml --out-dir generated/lollipop-first-consumer --request-types --response-decoders --client",
65-
"generate:api:io-wallet": "rimraf generated/io-wallet-api && gen-api-models --api-spec https://raw.githubusercontent.com/pagopa/io-wallet/io-wallet-user-func@3.0.4/apps/io-wallet-user-func/openapi.yaml --no-strict --out-dir generated/io-wallet-api --request-types --response-decoders --client",
65+
"generate:api:io-wallet": "rimraf generated/io-wallet-api && gen-api-models --api-spec https://raw.githubusercontent.com/pagopa/io-wallet/io-wallet-user-func@3.2.2/apps/io-wallet-user-func/openapi.yaml --no-strict --out-dir generated/io-wallet-api --request-types --response-decoders --client",
6666
"generate:proxy:io-wallet-models": "rimraf generated/io-wallet && gen-api-models --api-spec api_io_wallet.yaml --out-dir generated/io-wallet",
6767
"dist:modules": "modclean -r -n default:safe && yarn install --production",
6868
"predeploy": "npm-run-all generate build dist:modules",

src/controllers/ioWalletController.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { NonceDetailView } from "../../generated/io-wallet/NonceDetailView";
3030
import { SetWalletInstanceStatusBody } from "../../generated/io-wallet/SetWalletInstanceStatusBody";
3131
import { WalletAttestationView } from "../../generated/io-wallet/WalletAttestationView";
3232
import { WalletInstanceData } from "../../generated/io-wallet/WalletInstanceData";
33+
import { WhitelistedFiscalCodeData } from "../../generated/io-wallet/WhitelistedFiscalCodeData";
3334
import { FF_IO_WALLET_TRIAL_ENABLED } from "../config";
3435
import IoWalletService from "../services/ioWalletService";
3536
import { withUserFromRequest } from "../types/user";
@@ -233,6 +234,21 @@ export default class IoWalletController {
233234
)
234235
);
235236

237+
/**
238+
* Check if a fiscal code is whitelisted or not.
239+
*/
240+
public readonly isFiscalCodeWhitelisted = (
241+
req: express.Request
242+
): Promise<
243+
| IResponseSuccessJson<WhitelistedFiscalCodeData>
244+
| IResponseErrorInternal
245+
| IResponseErrorServiceUnavailable
246+
| IResponseErrorValidation
247+
> =>
248+
withUserFromRequest(req, async (user) =>
249+
this.ioWalletService.isFiscalCodeWhitelisted(user.fiscal_code)
250+
);
251+
236252
private readonly ensureFiscalCodeIsAllowed = (fiscalCode: FiscalCode) =>
237253
FF_IO_WALLET_TRIAL_ENABLED
238254
? pipe(

src/routes/ioWalletRoutes.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,13 @@ export const registerIoWalletAPIRoutes = (
7171
ioWalletController
7272
)
7373
);
74+
75+
app.get(
76+
`${basePath}/whitelisted-fiscal-code`,
77+
bearerSessionTokenAuth,
78+
toExpressHandler(
79+
ioWalletController.isFiscalCodeWhitelisted,
80+
ioWalletController
81+
)
82+
);
7483
};

src/services/__tests__/ioWalletService.test.ts

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ const mockGetWalletInstanceStatus = jest.fn();
1616
const mockGetCurrentWalletInstanceStatus = jest.fn();
1717
const mockSetWalletInstanceStatus = jest.fn();
1818
const mockDeleteWalletInstances = jest.fn();
19+
const mockIsFiscalCodeWhitelisted = jest.fn();
20+
const mockCreateWalletAttestationV2 = jest.fn();
1921

2022
mockGetNonce.mockImplementation(() =>
2123
t.success({
@@ -67,16 +69,30 @@ mockSetWalletInstanceStatus.mockImplementation(() =>
6769
})
6870
);
6971

72+
mockIsFiscalCodeWhitelisted.mockImplementation(() =>
73+
t.success({
74+
status: 200,
75+
})
76+
);
77+
78+
mockCreateWalletAttestationV2.mockImplementation(() =>
79+
t.success({
80+
status: 201,
81+
})
82+
);
83+
7084
const api = {
7185
getEntityConfiguration: mockGetEntityConfiguration,
7286
getNonce: mockGetNonce,
7387
createWalletInstance: mockCreateWalletInstance,
7488
createWalletAttestation: mockCreateWalletAttestation,
89+
createWalletAttestationV2: mockCreateWalletAttestationV2,
7590
healthCheck: mockHealthCheck,
7691
getWalletInstanceStatus: mockGetWalletInstanceStatus,
7792
setWalletInstanceStatus: mockSetWalletInstanceStatus,
7893
deleteWalletInstances: mockDeleteWalletInstances,
7994
getCurrentWalletInstanceStatus: mockGetCurrentWalletInstanceStatus,
95+
isFiscalCodeWhitelisted: mockIsFiscalCodeWhitelisted,
8096
};
8197

8298
const mockCreateSubscription = jest.fn();
@@ -772,6 +788,118 @@ describe("IoWalletService#getCurrentWalletInstanceStatus", () => {
772788
});
773789
});
774790

791+
describe("IoWalletService#isFiscalCodeWhitelisted", () => {
792+
beforeEach(() => {
793+
jest.clearAllMocks();
794+
});
795+
796+
it("should make the correct api call", async () => {
797+
const service = new IoWalletService(api, trialSystemApi);
798+
799+
await service.isFiscalCodeWhitelisted(aFiscalCode);
800+
801+
expect(mockIsFiscalCodeWhitelisted).toHaveBeenCalledWith({
802+
fiscalCode: aFiscalCode,
803+
});
804+
});
805+
806+
it("should handle a success response", async () => {
807+
const service = new IoWalletService(api, trialSystemApi);
808+
809+
const res = await service.isFiscalCodeWhitelisted(aFiscalCode);
810+
811+
expect(res).toMatchObject({
812+
kind: "IResponseSuccessJson",
813+
});
814+
});
815+
816+
it("should handle an internal error when the API client returns 500", async () => {
817+
const aGenericProblem = {};
818+
mockIsFiscalCodeWhitelisted.mockImplementationOnce(() =>
819+
t.success({ status: 500, value: aGenericProblem })
820+
);
821+
822+
const service = new IoWalletService(api, trialSystemApi);
823+
824+
const res = await service.isFiscalCodeWhitelisted(aFiscalCode);
825+
826+
expect(res).toMatchObject({
827+
kind: "IResponseErrorInternal",
828+
});
829+
});
830+
831+
it("should handle an internal error when the API client returns 400", async () => {
832+
const aGenericProblem = {};
833+
mockIsFiscalCodeWhitelisted.mockImplementationOnce(() =>
834+
t.success({ status: 400, value: aGenericProblem })
835+
);
836+
837+
const service = new IoWalletService(api, trialSystemApi);
838+
839+
const res = await service.isFiscalCodeWhitelisted(aFiscalCode);
840+
841+
expect(res).toMatchObject({
842+
kind: "IResponseErrorInternal",
843+
});
844+
});
845+
846+
it("should handle an internal error when the API client returns 422", async () => {
847+
mockIsFiscalCodeWhitelisted.mockImplementationOnce(() =>
848+
t.success({ status: 422 })
849+
);
850+
851+
const service = new IoWalletService(api, trialSystemApi);
852+
853+
const res = await service.isFiscalCodeWhitelisted(aFiscalCode);
854+
855+
expect(res).toMatchObject({
856+
kind: "IResponseErrorInternal",
857+
});
858+
});
859+
860+
it("should handle a service unavailable error when the API client returns 503", async () => {
861+
mockIsFiscalCodeWhitelisted.mockImplementationOnce(() =>
862+
t.success({ status: 503 })
863+
);
864+
865+
const service = new IoWalletService(api, trialSystemApi);
866+
867+
const res = await service.isFiscalCodeWhitelisted(aFiscalCode);
868+
869+
expect(res).toMatchObject({
870+
kind: "IResponseErrorServiceUnavailable",
871+
});
872+
});
873+
874+
it("should handle an internal error when the API client returns a code not specified in spec", async () => {
875+
const aGenericProblem = {};
876+
mockIsFiscalCodeWhitelisted.mockImplementationOnce(() =>
877+
t.success({ status: 599, value: aGenericProblem })
878+
);
879+
880+
const service = new IoWalletService(api, trialSystemApi);
881+
882+
const res = await service.isFiscalCodeWhitelisted(aFiscalCode);
883+
884+
expect(res).toMatchObject({
885+
kind: "IResponseErrorInternal",
886+
});
887+
});
888+
889+
it("should return an error if the api call throws an error", async () => {
890+
mockIsFiscalCodeWhitelisted.mockImplementationOnce(() => {
891+
throw new Error();
892+
});
893+
const service = new IoWalletService(api, trialSystemApi);
894+
895+
const res = await service.isFiscalCodeWhitelisted(aFiscalCode);
896+
897+
expect(res).toMatchObject({
898+
kind: "IResponseErrorInternal",
899+
});
900+
});
901+
});
902+
775903
describe("IoWalletService#getSubscription", () => {
776904
beforeEach(() => {
777905
jest.clearAllMocks();

src/services/ioWalletService.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import * as O from "fp-ts/Option";
2323
import { pipe } from "fp-ts/lib/function";
2424
import { Grant_typeEnum } from "generated/io-wallet-api/CreateWalletAttestationBody";
2525
import { NonceDetailView } from "generated/io-wallet-api/NonceDetailView";
26+
import { WhitelistedFiscalCodeData } from "generated/io-wallet-api/WhitelistedFiscalCodeData";
2627

2728
import { SetWalletInstanceStatusWithFiscalCodeData } from "../../generated/io-wallet-api/SetWalletInstanceStatusWithFiscalCodeData";
2829
import { WalletAttestationView } from "../../generated/io-wallet-api/WalletAttestationView";
@@ -366,4 +367,39 @@ export default class IoWalletService {
366367
}
367368
});
368369
});
370+
371+
/**
372+
* Check if the fiscal code is whitelisted or not.
373+
*/
374+
public readonly isFiscalCodeWhitelisted = (
375+
fiscalCode: FiscalCode
376+
): Promise<
377+
| IResponseErrorInternal
378+
| IResponseSuccessJson<WhitelistedFiscalCodeData>
379+
| IResponseErrorServiceUnavailable
380+
> =>
381+
withCatchAsInternalError(async () => {
382+
const validated = await this.ioWalletApiClient.isFiscalCodeWhitelisted({
383+
fiscalCode
384+
});
385+
return withValidatedOrInternalError(validated, (response) => {
386+
switch (response.status) {
387+
case 200:
388+
return ResponseSuccessJson(response.value);
389+
case 400:
390+
case 422:
391+
case 500:
392+
return ResponseErrorInternal(
393+
`Internal server error | ${response.value}`
394+
);
395+
case 503:
396+
return ResponseErrorServiceTemporarilyUnavailable(
397+
serviceUnavailableDetail,
398+
"10"
399+
);
400+
default:
401+
return ResponseErrorStatusNotDefinedInSpec(response);
402+
}
403+
});
404+
});
369405
}

0 commit comments

Comments
 (0)