diff --git a/scripts/generate-api-models.sh b/scripts/generate-api-models.sh index aa04c38f..1e897839 100755 --- a/scripts/generate-api-models.sh +++ b/scripts/generate-api-models.sh @@ -17,7 +17,7 @@ declare -a noParams=( "./generated/definitions/pagopa/walletv2 https://raw.githubusercontent.com/pagopa/io-services-metadata/$IO_SERVICES_METADATA_VERSION/bonus/specs/bpd/pm/walletv2.json" "./generated/definitions/pagopa/walletv3 https://raw.githubusercontent.com/pagopa/pagopa-infra/refs/tags/v1.745.1/src/domains/pay-wallet-app/api/io-payment-wallet/v1/_openapi.json.tpl" "./generated/definitions/pagopa/ecommerce https://raw.githubusercontent.com/pagopa/pagopa-infra/v1.724.0/src/domains/ecommerce-app/api/ecommerce-io/v2/_openapi.json.tpl" - "./generated/definitions/pagopa/transactions https://raw.githubusercontent.com/pagopa/pagopa-biz-events-service/refs/tags/0.1.87/openapi/openapi_lap_jwt.json" + "./generated/definitions/pagopa/transactions https://raw.githubusercontent.com/pagopa/pagopa-biz-events-service/refs/tags/0.3.5/openapi/openapi_lap_jwt.json" "./generated/definitions/pagopa/platform https://raw.githubusercontent.com/pagopa/pagopa-infra/v1.64.0/src/domains/shared-app/api/session-wallet/v1/_openapi.json.tpl" "./generated/definitions/pagopa https://raw.githubusercontent.com/pagopa/io-app/master/assets/paymentManager/spec.json" "./generated/definitions/cgn https://raw.githubusercontent.com/pagopa/io-backend/$IO_BACKEND_VERSION/api_cgn.yaml" @@ -28,7 +28,7 @@ declare -a noParams=( declare -a noStrict=( "./generated/definitions/fci https://raw.githubusercontent.com/pagopa/io-backend/$IO_BACKEND_VERSION/api_io_sign.yaml" - "./generated/definitions/idpay https://raw.githubusercontent.com/pagopa/cstar-securehub-infra-api-spec/refs/tags/v2.47.6/src/idpay/apim/api/idpay_appio_full/openapi.appio.full.yml" + "./generated/definitions/idpay https://raw.githubusercontent.com/pagopa/cstar-securehub-infra-api-spec/refs/tags/v3.10.2/src/idpay/apim/api/idpay_appio_full/openapi.appio.full.yml" "./generated/definitions/services https://raw.githubusercontent.com/pagopa/io-backend/$IO_BACKEND_VERSION/api_services_app_backend.yaml" ) diff --git a/src/config.ts b/src/config.ts index 21b73eb3..ddc818fe 100644 --- a/src/config.ts +++ b/src/config.ts @@ -237,6 +237,16 @@ const defaultConfig: IoDevServerConfig = { numberOfTransactions: 12, hideReceiptResponseCode: 400 }, + receipts: { + pdfNotFoundResponse: undefined + // { + // title: "Attachment not found", + // status: 400, + // detail: + // "Attachment of 321 for bizEvent with id 123 is still generating", + // code: "AT_404_002" + // } + }, bonus: { cgn: { isCgnEligible: true, diff --git a/src/features/payments/persistence/notices.ts b/src/features/payments/persistence/notices.ts index 9b3327f5..20f83191 100644 --- a/src/features/payments/persistence/notices.ts +++ b/src/features/payments/persistence/notices.ts @@ -1,21 +1,37 @@ import { fakerIT as faker } from "@faker-js/faker"; import { pipe } from "fp-ts/lib/function"; import * as O from "fp-ts/lib/Option"; -import { NoticeListItem } from "../../../../generated/definitions/pagopa/transactions/NoticeListItem"; -import { NoticeDetailResponse } from "../../../../generated/definitions/pagopa/transactions/NoticeDetailResponse"; import { TransactionInfo } from "../../../../generated/definitions/pagopa/ecommerce/TransactionInfo"; +import { NoticeDetailResponse } from "../../../../generated/definitions/pagopa/transactions/NoticeDetailResponse"; +import { NoticeListItem } from "../../../../generated/definitions/pagopa/transactions/NoticeListItem"; import { generateRandomInfoNotice } from "../utils/transactions"; -import { ioDevServerConfig } from "../../../config"; const mockedTaxCodes = ["1199250158", "13756881002", "262700362", "31500945"]; +const receiptSubjects = [ + "Tassa sui rifiuti", + "Imposta comunale immobiliare", + "Bollo auto", + "Canone RAI", + "Imposta ipotecaria", + "Diritti di segreteria", + "Sanzione amministrativa", + "Contributi sociali", + "Tassa di concessione governativa", + "Imposta di registro", + "Contributo provinciale", + "Tassa provinciale per i rifiuti", + "Diritti di istruttoria", + "Sanzione traffico", + "Multa amministrativa" +]; + type EventId = NoticeListItem["eventId"]; -const userNotices = new Map(); +const userNotices: NoticeListItem[] = []; const notices = new Map(); -const getUserNotices = () => - Array.from(userNotices.size > 0 ? userNotices.values() : []); +const getUserNotices = () => [...userNotices]; const getNoticeDetails = (eventId: EventId) => pipe( @@ -25,11 +41,18 @@ const getNoticeDetails = (eventId: EventId) => ); const addUserNotice = (transaction: NoticeListItem) => { - userNotices.set(transaction.eventId, transaction); + // eslint-disable-next-line functional/immutable-data + userNotices.push(transaction); }; const removeUserNotice = (eventId: EventId) => { - userNotices.delete(eventId); + // Remove all notices with the given eventId + // eslint-disable-next-line functional/immutable-data + userNotices.splice( + 0, + userNotices.length, + ...userNotices.filter(n => n.eventId !== eventId) + ); removeNoticeDetails(eventId); }; @@ -44,16 +67,16 @@ const removeNoticeDetails = (eventId: EventId) => { const generateUserNotice = ( eventId: EventId, idx: number, - additionalTransactionInfo: Partial = {} + additionalTransactionInfo: Partial = {}, + notice?: NoticeListItem ) => { - const payeeTaxCode = - mockedTaxCodes[ - faker.number.int({ min: 0, max: mockedTaxCodes.length - 1 }) - ]; - const randomNotice: NoticeListItem = { + const noticeToAdd = notice || { eventId, payeeName: faker.company.name(), - payeeTaxCode, + payeeTaxCode: + mockedTaxCodes[ + faker.number.int({ min: 0, max: mockedTaxCodes.length - 1 }) + ], isDebtor: false, isPayer: true, amount: additionalTransactionInfo.payments?.[0]?.amount.toString() || "", @@ -66,11 +89,14 @@ const generateUserNotice = ( const cartList = Array.from( { length: faker.number.int({ min: 1, max: 2 }) }, () => ({ - subject: faker.lorem.sentence(faker.number.int({ min: 2, max: 4 })), + subject: + receiptSubjects[ + faker.number.int({ min: 0, max: receiptSubjects.length - 1 }) + ], amount: faker.finance.amount({ min: 1, max: 1000 }), payee: { - name: randomNotice.payeeName, - taxCode: randomNotice.payeeTaxCode + name: noticeToAdd.payeeName, + taxCode: noticeToAdd.payeeTaxCode }, debtor: { name: faker.person.fullName(), @@ -82,33 +108,111 @@ const generateUserNotice = ( .toString() }) ); - const updatedNotice = { - ...randomNotice, - isCart: cartList.length > 1, - amount: cartList - .reduce((acc, item) => acc + Number(item.amount), 0) - .toString() - }; + + const updatedNotice = notice + ? noticeToAdd + : { + ...noticeToAdd, + isCart: cartList.length > 1, + amount: cartList + .reduce((acc, item) => acc + Number(item.amount), 0) + .toString() + }; + addUserNotice(updatedNotice); - const randomNoticeDetails: NoticeDetailResponse = { + const noticeDetails: NoticeDetailResponse = { infoNotice: generateRandomInfoNotice(cartList), carts: cartList }; - addNoticeDetails(eventId, randomNoticeDetails); + addNoticeDetails(eventId, noticeDetails); return updatedNotice; }; -const generateUserNoticeData = () => { - for (const i of Array( - ioDevServerConfig.features.payments.numberOfTransactions - ).keys()) { +const addMockedCartNotices = () => { + const cartConfig = [ + { + count: 5, + payeeName: "Ministero delle infrastrutture e dei trasporti", + payeeTaxCode: "97532760580" + }, + { + count: 2, + payeeName: "EC_TE", + payeeTaxCode: "77777777777" + } + ]; + + cartConfig.forEach(config => { + const cartId = faker.string.uuid(); + const cartPrefix = `test-ricevute-carrello-${cartId}-0_CART_`; + const cartItems = []; + + // eslint-disable-next-line functional/no-let + for (let i = 0; i < config.count; i += 1) { + const amount = faker.finance.amount({ min: 10, max: 150 }); + const notice: NoticeListItem = { + eventId: cartPrefix, + payeeName: config.payeeName, + payeeTaxCode: config.payeeTaxCode, + amount, + noticeDate: new Date( + new Date().setDate(new Date().getDate() - i) + ).toISOString(), + isCart: true, + isPayer: true, + isDebtor: false + }; + + // Build cart items for the detail response + // eslint-disable-next-line functional/immutable-data + cartItems.push({ + subject: + receiptSubjects[ + faker.number.int({ min: 0, max: receiptSubjects.length - 1 }) + ], + amount, + payee: { + name: config.payeeName, + taxCode: config.payeeTaxCode + }, + debtor: { + name: faker.person.fullName(), + taxCode: faker.string.alphanumeric(16).toUpperCase() + }, + refNumberType: "IBAN", + refNumberValue: faker.number + .int({ min: 100000000000, max: 999999999999 }) + .toString() + }); + + addUserNotice(notice); + } + + // Add notice details once for the entire cart with all items + const noticeDetails: NoticeDetailResponse = { + infoNotice: generateRandomInfoNotice(cartItems), + carts: cartItems + }; + addNoticeDetails(cartPrefix, noticeDetails); + }); + + // Add some regular (non-cart) notices + // eslint-disable-next-line functional/no-let + for (let i = 0; i < 5; i += 1) { generateUserNotice(faker.string.uuid(), i); } }; +const shuffleNotices = () => { + // eslint-disable-next-line functional/immutable-data + userNotices.sort(() => faker.number.int({ min: -1, max: 1 })); +}; + // At server startup -generateUserNoticeData(); +addMockedCartNotices(); +// Comment out to keep notices in order addedMockedCartNotices(); +shuffleNotices(); export default { addUserNotice, diff --git a/src/features/payments/routers/notices.ts b/src/features/payments/routers/notices.ts index f2839e4c..cc7c2c8c 100644 --- a/src/features/payments/routers/notices.ts +++ b/src/features/payments/routers/notices.ts @@ -1,15 +1,15 @@ import * as O from "fp-ts/lib/Option"; import { pipe } from "fp-ts/lib/function"; -import { sendFileFromRootPath } from "../../../utils/file"; -import NoticesDB from "../persistence/notices"; - import { NoticeListWrapResponse } from "../../../../generated/definitions/pagopa/transactions/NoticeListWrapResponse"; import { ioDevServerConfig } from "../../../config"; +import { sendFileFromRootPath } from "../../../utils/file"; +import NoticesDB from "../persistence/notices"; import { addNoticesHandler } from "./router"; const CONTINUATION_TOKEN_HEADER = "x-continuation-token"; const DEFAULT_SIZE = 10; const { hideReceiptResponseCode } = ioDevServerConfig.features.payments; +const { pdfNotFoundResponse } = ioDevServerConfig.features.receipts; addNoticesHandler("get", "/paids", (req, res) => { const size = req.query.size ? Number(req.query.size) : DEFAULT_SIZE; @@ -71,11 +71,23 @@ addNoticesHandler("get", "/paids/:eventId/pdf", (req, res) => { O.fold( () => res.sendStatus(400), eventId => { + if (pdfNotFoundResponse) { + return res + .status(pdfNotFoundResponse.status) + .json(pdfNotFoundResponse); + } const transaction = NoticesDB.getNoticeDetails(eventId); return pipe( transaction, O.fold( - () => res.sendStatus(404), + () => + res.status(404).json({ + title: "Attachment not found", + status: 404, + detail: + "Attachment of 321 for bizEvent with id 123 is still generating", + code: "AT_404_002" + }), _ => { sendFileFromRootPath( "assets/payments/receipts/loremIpsum.pdf", @@ -97,7 +109,9 @@ addNoticesHandler("post", "/paids/:eventId/disable", (req, res) => { O.fold( () => res.sendStatus(400), eventId => { - NoticesDB.removeUserNotice(eventId); + if (hideReceiptResponseCode === 200) { + NoticesDB.removeUserNotice(eventId); + } return res.sendStatus(hideReceiptResponseCode); } ) diff --git a/src/types/config.ts b/src/types/config.ts index c90644cb..c3221a2c 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -192,6 +192,18 @@ export const IoDevServerConfig = t.interface({ // the response code when hiding a receipt hideReceiptResponseCode: HttpResponseCode }), + receipts: t.interface({ + // if defined, the PDF endpoint returns this 404 response instead of the file + pdfNotFoundResponse: t.union([ + t.interface({ + title: t.string, + status: t.number, + detail: t.string, + code: t.string + }), + t.undefined + ]) + }), bonus: t.interface({ // defines the special configuration for cgn eligibility cgn: t.intersection([