diff --git a/packages/pn-commons/src/components/FileUpload.tsx b/packages/pn-commons/src/components/FileUpload.tsx index 0f222e6af7..5e49614d2e 100644 --- a/packages/pn-commons/src/components/FileUpload.tsx +++ b/packages/pn-commons/src/components/FileUpload.tsx @@ -32,6 +32,7 @@ type Props = { calcSha256?: boolean; fileUploaded?: { file: { data?: File; sha256?: { hashBase64: string; hashHex: string } } }; fileSizeLimit?: number; + showHashCode?: boolean; }; enum UploadStatus { @@ -165,6 +166,7 @@ const FileUpload = ({ calcSha256 = false, fileUploaded, fileSizeLimit = 209715200, + showHashCode = true, }: Props) => { const [fileData, dispatch] = useReducer(reducer, { status: UploadStatus.TO_UPLOAD, @@ -369,7 +371,7 @@ const FileUpload = ({ )} - {fileData.sha256 && ( + {fileData.sha256 && showHashCode && ( {getLocalizedOrDefaultLabel('common', 'upload-file.hash-code', 'Codice hash')} diff --git a/packages/pn-commons/src/components/PnAutocomplete.tsx b/packages/pn-commons/src/components/PnAutocomplete.tsx index f2ffe77040..5b46cfd436 100644 --- a/packages/pn-commons/src/components/PnAutocomplete.tsx +++ b/packages/pn-commons/src/components/PnAutocomplete.tsx @@ -10,8 +10,8 @@ const PnAutocomplete = < ) => ( {children}} /> ); diff --git a/packages/pn-commons/src/test-utils.tsx b/packages/pn-commons/src/test-utils.tsx index 43c20b4c4e..98dcd333f3 100644 --- a/packages/pn-commons/src/test-utils.tsx +++ b/packages/pn-commons/src/test-utils.tsx @@ -17,6 +17,7 @@ import { waitFor, within, } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { appStateSlice } from './redux/slices/appStateSlice'; import { formatDate } from './utility'; @@ -164,15 +165,16 @@ async function testAutocomplete( closeOnSelect?: boolean ) { const autocomplete = within(container as HTMLElement).getByTestId(elementName); + const autocompleteInput = autocomplete.querySelector('input'); + expect(autocompleteInput).toBeInTheDocument(); if (mustBeOpened) { - const button = autocomplete.querySelector('button[class*="MuiAutocomplete-popupIndicator"]'); - fireEvent.click(button!); + await userEvent.click(autocompleteInput!); } - const dropdown = (await waitFor(() => - document.querySelector('[role="presentation"][class*="MuiAutocomplete-popper"') - )) as HTMLElement; + const dropdown = await waitFor(() => + document.querySelector('[role="presentation"][class*="MuiAutocomplete-popper"]') + ); expect(dropdown).toBeInTheDocument(); - const dropdownOptionsList = within(dropdown).getByRole('listbox'); + const dropdownOptionsList = within(dropdown!).getByRole('listbox'); expect(dropdownOptionsList).toBeInTheDocument(); const dropdownOptionsListItems = within(dropdownOptionsList).getAllByRole('option'); expect(dropdownOptionsListItems).toHaveLength(options.length); diff --git a/packages/pn-commons/src/utility/string.utility.ts b/packages/pn-commons/src/utility/string.utility.ts index 36c9adc566..994749b607 100644 --- a/packages/pn-commons/src/utility/string.utility.ts +++ b/packages/pn-commons/src/utility/string.utility.ts @@ -68,6 +68,7 @@ export const dataRegex = { // Cfr. the comment in src/utility/user.utility.ts // ------------------------------------ // Carlos Lombardi, 2023.01.24 + currency: /^\d+(?:[.,]\d+)*$/, }; /** diff --git a/packages/pn-data-viz/src/setupTests.ts b/packages/pn-data-viz/src/setupTests.ts index 5cb7e91ab7..05257d1b6d 100644 --- a/packages/pn-data-viz/src/setupTests.ts +++ b/packages/pn-data-viz/src/setupTests.ts @@ -23,7 +23,6 @@ beforeAll(() => { SELFCARE_URL_FE_LOGIN: 'https://test.selfcare.pagopa.it/auth/login', SELFCARE_BASE_URL: 'https://test.selfcare.pagopa.it', SELFCARE_SEND_PROD_ID: 'prod-pn-test', - IS_PAYMENT_ENABLED: false, MIXPANEL_TOKEN: 'DUMMY', IS_MANUAL_SEND_ENABLED: true, }); diff --git a/packages/pn-pa-webapp/public/conf/config-dev.json b/packages/pn-pa-webapp/public/conf/config-dev.json index 563adc8151..23e17b4b4c 100644 --- a/packages/pn-pa-webapp/public/conf/config-dev.json +++ b/packages/pn-pa-webapp/public/conf/config-dev.json @@ -17,5 +17,7 @@ "API_B2B_LINK": "https://developer.pagopa.it/send/api#/", "IS_STATISTICS_ENABLED": true, "TAXONOMY_SEND_URL": "https://docs.pagopa.it/f.a.q.-per-integratori/tassonomia-send", - "DOWNTIME_EXAMPLE_LINK": "https://www.dev.notifichedigitali.it/static/documents/LegalFactMalfunction.pdf" + "DOWNTIME_EXAMPLE_LINK": "https://www.dev.notifichedigitali.it/static/documents/LegalFactMalfunction.pdf", + "PAYMENT_INFO_LINK": "https://developer.pagopa.it/send/guides/knowledge-base/readme/pagamenti-e-spese-di-notifica/pagamenti-pagopa", + "DEVELOPER_API_DOCUMENTATION_LINK": "https://developer.pagopa.it/send/api" } diff --git a/packages/pn-pa-webapp/public/locales/de/notifiche.json b/packages/pn-pa-webapp/public/locales/de/notifiche.json index 3f64a1e4d3..54d7fa911e 100644 --- a/packages/pn-pa-webapp/public/locales/de/notifiche.json +++ b/packages/pn-pa-webapp/public/locales/de/notifiche.json @@ -412,8 +412,7 @@ "simple-registered-letter": "Einschreiben mit Rückschein", "payment-method": "Zahlungsmodell", "pagopa-notice": "Bescheid pagoPA", - "pagopa-notice-f24-flatrate": "Bescheid pagoPA und Vordruck F24 pauschal", - "pagopa-notice-f24": "Bescheid pagoPA und Vordruck F24", + "f24": "Vordruck F24", "nothing": "Keine" }, "recipient": { @@ -451,7 +450,6 @@ "remove-recipient": "Empfänger entfernen" }, "attachments": { - "title": "Anhänge", "max-attachments": "Du kannst bis zu 11 Anhänge hochladen, einschließlich des zuzustellenden Bescheids.", "attach-for-recipients": "Anhänge", "act-attachment": "Urkunde anhängen", @@ -466,11 +464,9 @@ "payment-methods": { "title": "Zahlungsmodelle", "pagopa-notice": "Bescheid pagoPA", - "pagopa-notice-f24-flatrate": "F24 pauschal", - "pagopa-notice-f24": "F24", + "f24": "F24", "payment-models": "Zahlungsvordrucke für", "attach-pagopa-notice": "Bescheid pagoPA anhängen", - "attach-f24-flatrate": "Muster F24 pauschal anhängen", "attach-f24": "Modell F24 anhängen", "nothing": "<0>Wenn diese Zustellung eine Zahlung vorsieht, kehre zu <1>Vorabinformationen<2> zurück und wähle einen Vordruck aus. Komm dann hierher zurück, um diesen zu laden.", "back-to-attachments": "Zurück zu Anhänge" diff --git a/packages/pn-pa-webapp/public/locales/en/notifiche.json b/packages/pn-pa-webapp/public/locales/en/notifiche.json index bd8e07cf8e..4b69aab37b 100644 --- a/packages/pn-pa-webapp/public/locales/en/notifiche.json +++ b/packages/pn-pa-webapp/public/locales/en/notifiche.json @@ -412,8 +412,7 @@ "simple-registered-letter": "Registered letter with acknowledgment of receipt", "payment-method": "Payment form", "pagopa-notice": "PagoPA notice", - "pagopa-notice-f24-flatrate": "pagoPA Notice and Flat-rate Form F24", - "pagopa-notice-f24": "pagoPA Notice and Form F24", + "f24": "Form F24", "nothing": "None" }, "recipient": { @@ -451,7 +450,6 @@ "remove-recipient": "Remove recipient" }, "attachments": { - "title": "Attachments", "max-attachments": "You can upload up to 11 attachments, including the document to be notified.", "attach-for-recipients": "Attachments", "act-attachment": "Attach the deed", @@ -466,11 +464,9 @@ "payment-methods": { "title": "Payment forms", "pagopa-notice": "PagoPA notice", - "pagopa-notice-f24-flatrate": "F24 flat rate", - "pagopa-notice-f24": "F24", + "f24": "F24", "payment-models": "Payment forms for", "attach-pagopa-notice": "Attach pagoPA Notice", - "attach-f24-flatrate": "Attach Flat-rate Form F24", "attach-f24": "Attach Form F24", "nothing": "<0>If this notification involves a payment, go back to <1>Preliminary Information<2> and select a form. Then, return here to upload it.", "back-to-attachments": "Back to Attachments" diff --git a/packages/pn-pa-webapp/public/locales/fr/notifiche.json b/packages/pn-pa-webapp/public/locales/fr/notifiche.json index 7ea331f4f4..ddedbad664 100644 --- a/packages/pn-pa-webapp/public/locales/fr/notifiche.json +++ b/packages/pn-pa-webapp/public/locales/fr/notifiche.json @@ -412,8 +412,7 @@ "simple-registered-letter": "Lettre recommandée avec A/R", "payment-method": "Modèle de paiement", "pagopa-notice": "Avis de pagoPA", - "pagopa-notice-f24-flatrate": "Avis de pagoPA et modèle F24 forfaitaire", - "pagopa-notice-f24": "Avis de pagoPA et modèle F24", + "f24": "Modèle F24", "nothing": "Aucun" }, "recipient": { @@ -451,7 +450,6 @@ "remove-recipient": "Supprimez un destinataire" }, "attachments": { - "title": "Annexes", "max-attachments": "Vous pouvez télécharger jusqu’à 11 pièces jointes, y compris le document à notifier.", "attach-for-recipients": "Annexes", "act-attachment": "Joindre l’acte", @@ -466,11 +464,9 @@ "payment-methods": { "title": "Modèles de paiement", "pagopa-notice": "Avis de pagoPA", - "pagopa-notice-f24-flatrate": "F24 forfaitaire", - "pagopa-notice-f24": "F24", + "f24": "F24", "payment-models": "Modèles de paiement pour", "attach-pagopa-notice": "Joindre un avis de pagoPA", - "attach-f24-flatrate": "Joindre Modèle F24 forfaitaire", "attach-f24": "Joindre Modèle F24", "nothing": "<0>Si cette notification prévoit un paiement, revenez à <0><1>Informations préliminaires<1><2> et sélectionnez un modèle. Ensuite, revenez ici pour le charger.<2>", "back-to-attachments": "Retour à Pièces jointes" diff --git a/packages/pn-pa-webapp/public/locales/it/common.json b/packages/pn-pa-webapp/public/locales/it/common.json index 97b998c58d..c92e5232b7 100644 --- a/packages/pn-pa-webapp/public/locales/it/common.json +++ b/packages/pn-pa-webapp/public/locales/it/common.json @@ -10,7 +10,8 @@ "send": "Invia", "go-to-home": "Torna alla home page", "go-to-login": "Accedi", - "close": "Chiudi" + "close": "Chiudi", + "delete": "Elimina" }, "menu": { "notifications": "Notifiche", diff --git a/packages/pn-pa-webapp/public/locales/it/notifiche.json b/packages/pn-pa-webapp/public/locales/it/notifiche.json index d1c55e9d8b..a01e88ee3d 100644 --- a/packages/pn-pa-webapp/public/locales/it/notifiche.json +++ b/packages/pn-pa-webapp/public/locales/it/notifiche.json @@ -391,7 +391,8 @@ }, "new-notification": { "title": "Invia una nuova notifica", - "subtitle": "Per inviare una notifica, inserisci i dati richiesti e aggiungi i modelli di pagamento. Se devi fare un invio massivo, puoi usare le", + "subtitle": "Per inviare una notifica, inserisci i dati richiesti e aggiungi eventualmente una Posizione debitoria. Se devi fare un invio massivo,", + "how-it-works": "scopri come funziona", "breadcrumb-root": "Notifiche", "breadcrumb-leaf": "Nuova notifica", "prompt": { @@ -424,8 +425,7 @@ "simple-registered-letter": "Raccomandata A/R", "payment-method": "Modello di pagamento", "pagopa-notice": "Avviso pagoPA", - "pagopa-notice-f24-flatrate": "Avviso pagoPA e Modello F24 forfettario", - "pagopa-notice-f24": "Avviso pagoPA e Modello F24", + "f24": "Modello F24", "nothing": "Nessuno", "notification-language-title": "Lingua di invio", "notification-language-subtitle": "La notifica, i documenti e le attestazioni verranno inviati secondo le preferenze di lingua selezionate.", @@ -440,7 +440,6 @@ "title": "Destinatari", "fiscal-code-error": "Il valore inserito non è corretto", "identical-fiscal-codes-error": "C'è già un destinatario con questo Codice Fiscale. Inseriscine uno diverso.", - "identical-notice-codes-error": "C'è già un destinatario con questo Codice Avviso. Inseriscine uno diverso.", "notice-code-error": "Inserisci un codice di 18 caratteri numerici", "pec-error": "Indirizzo PEC non valido", "forbidden-characters-denomination-error": "Hai inserito caratteri non validi", @@ -473,7 +472,7 @@ "remove-recipient": "Rimuovi destinatario" }, "attachments": { - "title": "Allegati", + "title": "Documentazione", "max-attachments": "Questi sono i documenti che saranno notificati tramite SEND a tutti i destinatari inseriti. Puoi allegare fino a un massimo di {{maxNumber}} documenti.", "attach-for-recipients": "Documenti allegati", "act-attachment": "Allega documento", @@ -483,21 +482,70 @@ "add-doc": "Aggiungi un documento", "add-another-doc": "Aggiungi un altro documento", "back-to-recipient": "Torna a Destinatario", + "back-to-debt-position": "Torna a Posizione debitoria", + "back-to-debt-position-detail": "Torna a Dettaglio posizione debitoria", "remove-document": "Rimuovi documento", "banner-additional-languages": "Hai scelto di inviare la notifica in più lingue. Ricorda di allegare i documenti in entrambe le lingue selezionate.", "file-upload-helper": "Il documento deve essere in formato {{format}} e non deve superare le dimensioni di {{size}}" }, - "payment-methods": { - "title": "Modelli di pagamento", - "pagopa-notice": "Avviso pagoPA", - "pagopa-notice-f24-flatrate": "F24 forfettario", - "pagopa-notice-f24": "F24", - "payment-models": "Modelli di pagamento per", - "attach-pagopa-notice": "Allega Avviso pagoPA", - "attach-f24-flatrate": "Allega Modello F24 forfettario", - "attach-f24": "Allega Modello F24", - "nothing": "<0>Se questa notifica prevede un pagamento, torna a <1>Informazioni preliminari<2> e seleziona un modello. Poi, torna qui per caricarlo.", - "back-to-attachments": "Torna a Allegati" + "debt-position": { + "title": "Posizione debitoria", + "debt-position": "Posizione debitoria", + "debt-position-of": "Posizione debitoria di {{fullName}}", + "which-type-of-payments": "Quale tipo di pagamento prevedi?", + "back-to-recipient": "Torna a Destinatari", + "radios": { + "pago-pa": "Avviso pagoPA", + "f24": "Modello F24", + "pago-pa-f24": "Avviso pagoPA + Modello F24", + "nothing": "Nessun pagamento" + } + }, + "debt-position-detail": { + "title": "Dettaglio posizione debitoria", + "debt-position-of": "Posizione debitoria di {{fullName}}", + "notification-fee": { + "title": "Costo di notifica", + "description": "Scegli il tipo di costo di notifica: puoi scegliere fra quello già incluso nell’atto oppure inserire un costo di notifica personalizzato a carico del destinatario.", + "disclaimer": "L’IVA verrà applicata solo alle notifiche recapitate in modalità cartacea.", + "pa-fee": "Costo di notifica", + "vat": "IVA" + }, + "pagopa-int-mode": { + "title": "Tecnologia del pagamento", + "description": "Nella modalità sincrona la posizione debitoria è presso il sistema dell'Ente Creditore (EC), mentre nella modalità asincrona è caricata sul sistema Gestione Posizioni Debitorie (GPD) di pagoPA. <0>Scopri di più" + }, + "alert": "Se non hai informazioni certe sull’integrazione pagoPA verifica con il tuo provider tecnologico: in caso di scelta non corretta l’addebito della notifica potrebbe essere errato.", + "back-to-debt-position": "Torna a Posizione debitoria", + "radios": { + "flat-rate": "Incluso nell’atto (forfettario)", + "delivery-mode": "A carico del destinatario (puntuale)", + "sync": "Sincrona", + "async": "Asincrona" + }, + "identical-notice-codes-error": "C'è già un destinatario con questo Codice Avviso. Inseriscine uno diverso.", + "at-least-one-applycost": "Applica i costi di notifica almeno ad un pagamento", + "payment-methods": { + "title": "Modelli di pagamento", + "pagopa-notice": "Avviso pagoPA", + "payment-models": "Posizione debitoria di", + "nothing": "<0>Se questa notifica prevede un pagamento, torna a <1>Informazioni preliminari<2> e seleziona un modello. Poi, torna qui per caricarlo.", + "back-to-attachments": "Torna a Allegati", + "apply-cost-installment": "Attenzione: se è prevista la possibilità di un pagamento rateale dovrai applicare il costo di notifica solo ad una rata.", + "pagopa": { + "attach-pagopa-notice": "Specifiche Avviso pagoPA", + "notice-code": "Codice avviso", + "creditor-taxid": "Codice fiscale ente creditore", + "apply-cost": "Applica costo di notifica", + "add-new-pagopa-notice": "Aggiungi codice di avviso pagoPA" + }, + "f24": { + "attach-f24": "Specifiche Modello F24", + "document-name": "Titolo documento", + "apply-cost": "Applica costo di notifica", + "add-new-f24": "Aggiungi un altro Modello F24" + } + } }, "sync-feedback": { "title": "La notifica è stata creata", diff --git a/packages/pn-pa-webapp/public/locales/sl/notifiche.json b/packages/pn-pa-webapp/public/locales/sl/notifiche.json index 700896a116..3864611cc4 100644 --- a/packages/pn-pa-webapp/public/locales/sl/notifiche.json +++ b/packages/pn-pa-webapp/public/locales/sl/notifiche.json @@ -412,8 +412,7 @@ "simple-registered-letter": "Priporočeno pismo s povratnico", "payment-method": "Plačilni obrazec", "pagopa-notice": "Opozorilo PagoPA", - "pagopa-notice-f24-flatrate": "Obvestilo pagoPA in pavšalni obrazec F24", - "pagopa-notice-f24": "Obvestilo PagoPA in obrazec F24", + "f24": "Obrazec F24", "nothing": "Noben" }, "recipient": { @@ -451,7 +450,6 @@ "remove-recipient": "Odstrani prejemnika" }, "attachments": { - "title": "Priloge", "max-attachments": "Naložite lahko do 11 prilog, vključno z dokumentom, ki ga je treba obvestiti.", "attach-for-recipients": "Priloge", "act-attachment": "Priloži listino", @@ -466,11 +464,9 @@ "payment-methods": { "title": "Plačilni obrazci", "pagopa-notice": "Opozorilo PagoPA", - "pagopa-notice-f24-flatrate": "Pavšalni F24", - "pagopa-notice-f24": "F24", + "f24": "F24", "payment-models": "Plačilni obrazci za", "attach-pagopa-notice": "Priložite obvestilo pagoPA", - "attach-f24-flatrate": "Priložite pavšalni obrazec F24", "attach-f24": "Priložite obrazec F24", "nothing": "<0>Če to obvestilo vključuje plačilo, se vrnite na <1>Uvodne informacije<2> in izberite obrazec. Nato se vrnite in ga naložite.", "back-to-attachments": "Nazaj na priloge" diff --git a/packages/pn-pa-webapp/src/__mocks__/NewNotification.mock.ts b/packages/pn-pa-webapp/src/__mocks__/NewNotification.mock.ts index e042440289..db4d9e2712 100644 --- a/packages/pn-pa-webapp/src/__mocks__/NewNotification.mock.ts +++ b/packages/pn-pa-webapp/src/__mocks__/NewNotification.mock.ts @@ -1,19 +1,24 @@ -import { - DigitalDomicileType, - PhysicalCommunicationType, - RecipientType, -} from '@pagopa-pn/pn-commons'; +import { PhysicalCommunicationType, RecipientType } from '@pagopa-pn/pn-commons'; +import { + BffNewNotificationRequest, + F24Payment, + NotificationDigitalAddressTypeEnum, + NotificationDocument, + NotificationRecipientV23, + PagoPaPayment, +} from '../generated-client/notifications'; import { NewNotification, - NewNotificationDTO, + NewNotificationDigitalAddressType, NewNotificationDocument, + NewNotificationF24Payment, + NewNotificationPagoPaPayment, NewNotificationRecipient, NotificationFeePolicy, PaymentModel, } from '../models/NewNotification'; import { UserGroup } from '../models/user'; -import { newNotificationMapper } from '../utility/notification.utility'; import { userResponse } from './Auth.mock'; export const newNotificationGroups: Array = [ @@ -35,7 +40,77 @@ export const newNotificationGroups: Array = [ }, ]; -const newNotificationRecipients: Array = [ +const newNotificationPagoPa: NewNotificationPagoPaPayment = { + id: 'mocked-pagopa-id', + idx: 0, + contentType: 'application/pdf', + creditorTaxId: 'mocked-creditor-taxid', + noticeCode: 'mocked-noticecode', + applyCost: true, + file: { + data: new File([''], 'mocked-name', { type: 'application/pdf' }), + sha256: { + hashBase64: 'mocked-pa-sha256', + hashHex: '', + }, + }, + ref: { + key: '', + versionToken: '', + }, +}; + +const newNotificationPagoPaForBff: PagoPaPayment = { + creditorTaxId: 'mocked-creditor-taxid', + noticeCode: 'mocked-noticecode', + applyCost: true, + attachment: { + contentType: 'application/pdf', + digests: { + sha256: 'mocked-pa-sha256', + }, + ref: { + key: '', + versionToken: '', + }, + }, +}; + +const newNotificationF24: NewNotificationF24Payment = { + id: 'mocked-f24-id', + idx: 0, + name: 'mocked-name', + contentType: 'application/json', + applyCost: false, + file: { + data: new File([''], 'mocked-name', { type: 'application/json' }), + sha256: { + hashBase64: 'mocked-f24-sha256', + hashHex: '', + }, + }, + ref: { + key: '', + versionToken: '', + }, +}; + +const newNotificationF24ForBff: F24Payment = { + title: 'mocked-name', + applyCost: false, + metadataAttachment: { + contentType: 'application/json', + digests: { + sha256: 'mocked-f24-sha256', + }, + ref: { + key: '', + versionToken: '', + }, + }, +}; + +export const newNotificationRecipients: Array = [ { id: 'recipient.0', idx: 0, @@ -43,9 +118,7 @@ const newNotificationRecipients: Array = [ firstName: 'Mario', lastName: 'Rossi', recipientType: RecipientType.PF, - creditorTaxId: '12345678910', - noticeCode: '123456789123456788', - type: DigitalDomicileType.PEC, + type: NewNotificationDigitalAddressType.PEC, digitalDomicile: 'mario.rossi@pec.it', address: 'via del corso', addressDetails: '', @@ -55,6 +128,12 @@ const newNotificationRecipients: Array = [ municipalityDetails: '', province: 'Roma', foreignState: 'Italia', + payments: [ + { + pagoPa: { ...newNotificationPagoPa }, + }, + ], + debtPosition: PaymentModel.PAGO_PA, }, { id: 'recipient.1', @@ -63,9 +142,7 @@ const newNotificationRecipients: Array = [ firstName: 'Sara Gallo srl', lastName: '', recipientType: RecipientType.PG, - creditorTaxId: '12345678910', - noticeCode: '123456789123456789', - type: DigitalDomicileType.PEC, + type: NewNotificationDigitalAddressType.PEC, digitalDomicile: '', address: 'via delle cicale', addressDetails: '', @@ -75,6 +152,59 @@ const newNotificationRecipients: Array = [ municipalityDetails: '', province: 'Roma', foreignState: 'Italia', + payments: [ + { + pagoPa: { ...newNotificationPagoPa }, + }, + { + f24: { ...newNotificationF24 }, + }, + ], + debtPosition: PaymentModel.PAGO_PA_F24, + }, +]; + +const newNotificationRecipientsForBff: Array = [ + { + taxId: 'MRARSS90P08H501Q', + denomination: 'Mario Rossi', + recipientType: RecipientType.PF, + digitalDomicile: { + type: NotificationDigitalAddressTypeEnum.Pec, + address: 'mario.rossi@pec.it', + }, + physicalAddress: { + address: 'via del corso 49', + zip: '00122', + municipality: 'Roma', + province: 'Roma', + foreignState: 'Italia', + }, + payments: [ + { + pagoPa: newNotificationPagoPaForBff, + }, + ], + }, + { + taxId: '12345678901', + denomination: 'Sara Gallo srl', + recipientType: RecipientType.PG, + physicalAddress: { + address: 'via delle cicale 21', + zip: '00035', + municipality: 'Anzio', + province: 'Roma', + foreignState: 'Italia', + }, + payments: [ + { + pagoPa: { ...newNotificationPagoPaForBff }, + }, + { + f24: { ...newNotificationF24ForBff }, + }, + ], }, ]; @@ -119,39 +249,38 @@ const newNotificationDocuments: Array = [ }, ]; -const newNotificationPagoPa: NewNotificationDocument = { - id: 'mocked-pagopa-id', - idx: 0, - name: 'mocked-name', - contentType: 'application/pdf', - file: { - data: new File([''], 'mocked-name', { type: 'application/pdf' }), - sha256: { - hashBase64: 'mocked-pa-sha256', - hashHex: '', +const newNotificationDocumentsForBff: Array = [ + { + title: 'mocked-name-0', + contentType: 'application/pdf', + digests: { + sha256: 'mocked-sha256-0', + }, + ref: { + key: 'mocked-key-0', + versionToken: 'mocked-versionToken-0', }, }, - ref: { - key: '', - versionToken: '', + { + title: 'mocked-name-1', + contentType: 'application/pdf', + digests: { + sha256: 'mocked-sha256-1', + }, + ref: { + key: 'mocked-key-1', + versionToken: 'mocked-versionToken-1', + }, }, -}; +]; -const newNotificationF24Standard: NewNotificationDocument = { - id: 'mocked-f24standard-id', - idx: 0, - name: 'mocked-name', - contentType: 'application/pdf', - file: { - data: new File([''], 'mocked-name', { type: 'application/pdf' }), - sha256: { - hashBase64: 'mocked-f24standard-sha256', - hashHex: '', - }, +export const payments = { + [newNotificationRecipients[0].taxId]: { + pagoPa: { ...newNotificationPagoPa }, }, - ref: { - key: '', - versionToken: '', + [newNotificationRecipients[1].taxId]: { + pagoPa: { ...newNotificationPagoPa }, + f24: { ...newNotificationF24 }, }, }; @@ -161,17 +290,7 @@ export const newNotification: NewNotification = { subject: 'Multone esagerato', recipients: newNotificationRecipients, documents: newNotificationDocuments, - payment: { - [newNotificationRecipients[0].taxId]: { - pagoPaForm: { ...newNotificationPagoPa }, - }, - [newNotificationRecipients[1].taxId]: { - pagoPaForm: { ...newNotificationPagoPa }, - f24standard: { ...newNotificationF24Standard }, - }, - }, physicalCommunicationType: PhysicalCommunicationType.REGISTERED_LETTER_890, - paymentMode: PaymentModel.PAGO_PA_NOTICE_F24, group: newNotificationGroups[2].id, taxonomyCode: '010801N', notificationFeePolicy: NotificationFeePolicy.FLAT_RATE, @@ -188,13 +307,24 @@ export const newNotificationEmpty: NewNotification = { subject: '', recipients: [], documents: [], - payment: {}, - physicalCommunicationType: '' as PhysicalCommunicationType, - paymentMode: '' as PaymentModel, + physicalCommunicationType: PhysicalCommunicationType.REGISTERED_LETTER_890, group: '', taxonomyCode: '', + senderTaxId: '', notificationFeePolicy: '' as NotificationFeePolicy, senderDenomination: userResponse.organization.name, }; -export const newNotificationDTO: NewNotificationDTO = newNotificationMapper(newNotification); +export const newNotificationForBff: BffNewNotificationRequest = { + abstract: '', + paProtocolNumber: '12345678910', + subject: 'Multone esagerato', + recipients: newNotificationRecipientsForBff, + documents: newNotificationDocumentsForBff, + physicalCommunicationType: PhysicalCommunicationType.REGISTERED_LETTER_890, + group: newNotificationGroups[2].id, + taxonomyCode: '010801N', + notificationFeePolicy: NotificationFeePolicy.FLAT_RATE, + senderDenomination: userResponse.organization.name, + senderTaxId: userResponse.organization.fiscal_code, +}; diff --git a/packages/pn-pa-webapp/src/api/notifications/Notifications.api.ts b/packages/pn-pa-webapp/src/api/notifications/Notifications.api.ts index aedc17a997..23f19740a0 100644 --- a/packages/pn-pa-webapp/src/api/notifications/Notifications.api.ts +++ b/packages/pn-pa-webapp/src/api/notifications/Notifications.api.ts @@ -14,12 +14,13 @@ export const NotificationsApi = { sha256: string, secret: string, file: Uint8Array, - httpMethod: string + httpMethod: string, + contentType = 'application/pdf' ): Promise => { const method = httpMethod.toLowerCase() as 'get' | 'post' | 'put'; return externalClient[method](url, file, { headers: { - 'Content-Type': 'application/pdf', + 'Content-Type': contentType, 'x-amz-meta-secret': secret, 'x-amz-checksum-sha256': sha256, }, diff --git a/packages/pn-pa-webapp/src/components/NewNotification/Attachments.tsx b/packages/pn-pa-webapp/src/components/NewNotification/Attachments.tsx index 7512a9341e..5f5b372180 100644 --- a/packages/pn-pa-webapp/src/components/NewNotification/Attachments.tsx +++ b/packages/pn-pa-webapp/src/components/NewNotification/Attachments.tsx @@ -20,7 +20,7 @@ import { ButtonNaked } from '@pagopa/mui-italia'; import { NewNotificationDocument } from '../../models/NewNotification'; import { useAppDispatch } from '../../redux/hooks'; import { uploadNotificationDocument } from '../../redux/newNotification/actions'; -import { setAttachments } from '../../redux/newNotification/reducers'; +import { setAttachments, setIsCompleted } from '../../redux/newNotification/reducers'; import { getConfiguration } from '../../services/configuration.service'; import { requiredStringFieldValidation } from '../../utility/validation.utility'; import NewNotificationCard from './NewNotificationCard'; @@ -131,11 +131,12 @@ const AttachmentBox: React.FC = ({ type Props = { onConfirm: () => void; - onPreviousStep?: () => void; + onPreviousStep?: (step?: number) => void; attachmentsData?: Array; forwardedRef: ForwardedRef; isCompleted: boolean; hasAdditionalLang?: boolean; + hasDebtPosition?: boolean; }; const emptyFileData = { @@ -162,6 +163,7 @@ const Attachments: React.FC = ({ forwardedRef, isCompleted, hasAdditionalLang, + hasDebtPosition, }) => { const dispatch = useAppDispatch(); const { t } = useTranslation(['notifiche'], { @@ -232,6 +234,7 @@ const Attachments: React.FC = ({ .then((docs) => { // update formik void formik.setFieldValue('documents', docs, false); + dispatch(setIsCompleted()); onConfirm(); }) .catch(() => undefined); @@ -308,10 +311,18 @@ const Attachments: React.FC = ({ const handlePreviousStep = () => { if (onPreviousStep) { storeAttachments(formik.values.documents); - onPreviousStep(); + return hasDebtPosition ? onPreviousStep() : onPreviousStep(2); } }; + const getPreviousStepLabel = () => { + if (!IS_PAYMENT_ENABLED) { + return t('back-to-recipient'); + } + + return hasDebtPosition ? t('back-to-debt-position-detail') : t('back-to-debt-position'); + }; + useImperativeHandle(forwardedRef, () => ({ confirm() { storeAttachments(formik.values.documents); @@ -323,8 +334,8 @@ const Attachments: React.FC = ({ handlePreviousStep()} > diff --git a/packages/pn-pa-webapp/src/components/NewNotification/DebtPosition.tsx b/packages/pn-pa-webapp/src/components/NewNotification/DebtPosition.tsx new file mode 100644 index 0000000000..bd3f1d95b4 --- /dev/null +++ b/packages/pn-pa-webapp/src/components/NewNotification/DebtPosition.tsx @@ -0,0 +1,167 @@ +import { useFormik } from 'formik'; +import React, { ChangeEvent, ForwardedRef, forwardRef, useImperativeHandle } from 'react'; +import { useTranslation } from 'react-i18next'; +import * as yup from 'yup'; + +import { + Box, + FormControl, + FormControlLabel, + FormLabel, + Paper, + Radio, + RadioGroup, + Typography, +} from '@mui/material'; + +import { NewNotificationRecipient, PaymentModel } from '../../models/NewNotification'; +import { useAppDispatch } from '../../redux/hooks'; +import { setDebtPosition } from '../../redux/newNotification/reducers'; +import NewNotificationCard from './NewNotificationCard'; +import { FormBoxTitle } from './NewNotificationFormElelements'; + +type Props = { + recipients: Array; + onConfirm: () => void; + onPreviousStep: () => void; + goToLastStep: () => void; + forwardedRef: ForwardedRef; +}; + +const DebtPosition: React.FC = ({ + recipients, + onConfirm, + onPreviousStep, + goToLastStep, + forwardedRef, +}) => { + const dispatch = useAppDispatch(); + const { t } = useTranslation(['notifiche'], { + keyPrefix: 'new-notification.steps.debt-position', + }); + const { t: tc } = useTranslation(['common']); + + const initialValues = () => ({ + recipients: recipients.map((recipient) => ({ + ...recipient, + debtPosition: recipient.debtPosition ?? undefined, + })), + }); + + const validationSchema = yup.object().shape({ + recipients: yup.array().of( + yup.object().shape({ + debtPosition: yup.string().required(tc('required-field')), + }) + ), + }); + + const formik = useFormik({ + initialValues: initialValues(), + validateOnMount: true, + validationSchema, + enableReinitialize: true, + onSubmit: (values) => { + dispatch(setDebtPosition(values)); + if (values.recipients.every((recipient) => recipient.debtPosition === PaymentModel.NOTHING)) { + goToLastStep(); + return; + } + onConfirm(); + }, + }); + + const handleChange = async (event: ChangeEvent, index: number) => { + const { value } = event.target; + await formik.setFieldValue(`recipients.${index}.debtPosition`, value); + }; + + const handlePreviousStep = () => { + if (onPreviousStep) { + dispatch(setDebtPosition(formik.values)); + onPreviousStep(); + } + }; + + useImperativeHandle(forwardedRef, () => ({ + confirm() { + dispatch(setDebtPosition(formik.values)); + }, + })); + + return ( +
+ + {recipients.map((recipient, index) => ( + + + {recipients.length === 1 + ? t('debt-position') + : t('debt-position-of', { + fullName: `${recipient.firstName} ${recipient.lastName}`, + })} + + + + + + + handleChange(e, index)} + sx={{ mt: 2 }} + > + } + label={t('radios.pago-pa')} + componentsProps={{ typography: { fontSize: '16px' } }} + data-testid="paymentModel" + /> + } + label={t('radios.f24')} + componentsProps={{ typography: { fontSize: '16px' } }} + data-testid="paymentModel" + /> + } + label={t('radios.pago-pa-f24')} + componentsProps={{ typography: { fontSize: '16px' } }} + data-testid="paymentModel" + /> + } + label={t('radios.nothing')} + componentsProps={{ typography: { fontSize: '16px' } }} + data-testid="paymentModel" + /> + + + + + ))} + +
+ ); +}; + +// This is a workaorund to prevent cognitive complexity warning +export default forwardRef((props: Omit, ref) => ( + +)); diff --git a/packages/pn-pa-webapp/src/components/NewNotification/DebtPositionDetail.tsx b/packages/pn-pa-webapp/src/components/NewNotification/DebtPositionDetail.tsx new file mode 100644 index 0000000000..4eb3b85ef3 --- /dev/null +++ b/packages/pn-pa-webapp/src/components/NewNotification/DebtPositionDetail.tsx @@ -0,0 +1,548 @@ +import { useFormik } from 'formik'; +import _, { mapValues } from 'lodash'; +import { ChangeEvent, ForwardedRef, forwardRef, useImperativeHandle, useMemo } from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import * as yup from 'yup'; + +import EuroIcon from '@mui/icons-material/Euro'; +import { + Alert, + FormControlLabel, + InputAdornment, + Link, + MenuItem, + Paper, + Radio, + RadioGroup, + Stack, + TextField, + Typography, +} from '@mui/material'; +import { CustomDropdown, dataRegex } from '@pagopa-pn/pn-commons'; + +import { + NewNotification, + NewNotificationF24Payment, + NewNotificationPagoPaPayment, + NewNotificationPayment, + NewNotificationRecipient, + NotificationFeePolicy, + PagoPaIntegrationMode, + PaymentModel, + VAT, +} from '../../models/NewNotification'; +import { useAppDispatch, useAppSelector } from '../../redux/hooks'; +import { uploadNotificationPaymentDocument } from '../../redux/newNotification/actions'; +import { setDebtPositionDetail } from '../../redux/newNotification/reducers'; +import { RootState } from '../../redux/store'; +import { getConfiguration } from '../../services/configuration.service'; +import { + checkApplyCost, + f24ValidationSchema, + identicalIUV, + pagoPaValidationSchema, +} from '../../utility/validation.utility'; +import NewNotificationCard from './NewNotificationCard'; +import { FormBox, FormBoxSubtitle, FormBoxTitle } from './NewNotificationFormElelements'; +import PaymentMethods from './PaymentMethods'; + +type Props = { + notification: NewNotification; + onConfirm: () => void; + onPreviousStep: () => void; + forwardedRef: ForwardedRef; +}; + +const emptyFileData = { + data: undefined, + sha256: { hashBase64: '', hashHex: '' }, +}; + +const DebtPositionDetail: React.FC = ({ + notification, + onConfirm, + onPreviousStep, + forwardedRef, +}) => { + const { t } = useTranslation(['notifiche'], { + keyPrefix: 'new-notification.steps.debt-position-detail', + }); + const { t: tc } = useTranslation(['common']); + const organization = useAppSelector((state: RootState) => state.userState.user.organization); + + const hasPagoPa = notification.recipients.some( + (recipient) => + recipient.debtPosition === PaymentModel.PAGO_PA || + recipient.debtPosition === PaymentModel.PAGO_PA_F24 + ); + + const { PAYMENT_INFO_LINK } = getConfiguration(); + const dispatch = useAppDispatch(); + + const newPagopaPayment = (id: string, idx: number): NewNotificationPagoPaPayment => ({ + id, + idx, + contentType: 'application/pdf', + file: emptyFileData, + creditorTaxId: organization.fiscal_code, + noticeCode: '', + applyCost: false, + ref: { + key: '', + versionToken: '', + }, + }); + + const newF24Payment = (id: string, idx: number): NewNotificationF24Payment => ({ + id, + idx, + contentType: 'application/json', + file: emptyFileData, + name: '', + applyCost: false, + ref: { + key: '', + versionToken: '', + }, + }); + + const formatPayments = (): Array => { + const recipients = _.cloneDeep(notification.recipients); + return recipients.map((recipient) => { + const recipientData = formik.values.recipients[recipient.taxId]; + const payments = [ + ...recipientData.pagoPa + // .filter((payment) => payment?.file?.data) + .map((payment) => ({ pagoPa: payment })), + ...recipientData.f24 + .filter((payment) => payment?.file?.data) + .map((payment) => ({ f24: payment })), + ]; + + // eslint-disable-next-line functional/immutable-data + recipient.payments = payments; + + return recipient; + }); + }; + + const initialValues = useMemo( + // eslint-disable-next-line sonarjs/cognitive-complexity + () => ({ + notificationFeePolicy: notification.notificationFeePolicy, + paFee: notification.paFee || undefined, + vat: notification.vat || undefined, + pagoPaIntMode: notification.pagoPaIntMode ?? PagoPaIntegrationMode.NONE, + recipients: notification.recipients.reduce( + ( + acc: { + [taxId: string]: { + pagoPa: Array; + f24: Array; + }; + }, + recipient + ) => { + const recipientPayments = !_.isNil(recipient.payments) ? recipient.payments : []; + const debtPosition = recipient.debtPosition; + + const hasPagoPa = recipientPayments.some((p) => p.pagoPa); + const hasF24 = recipientPayments.some((p) => p.f24); + + // eslint-disable-next-line prefer-const, functional/no-let + let payments: Array = [...recipientPayments]; + + /* eslint-disable functional/immutable-data */ + if ( + (debtPosition === PaymentModel.PAGO_PA || debtPosition === PaymentModel.PAGO_PA_F24) && + !hasPagoPa + ) { + const lastPaymentIdx = payments[payments.length - 1]?.pagoPa?.idx ?? -1; + const newPaymentIdx = lastPaymentIdx + 1; + + payments.push({ + pagoPa: newPagopaPayment(`${recipient.taxId}-${newPaymentIdx}-pagoPa`, newPaymentIdx), + }); + } + if ( + (debtPosition === PaymentModel.F24 || debtPosition === PaymentModel.PAGO_PA_F24) && + !hasF24 + ) { + const lastPaymentIdx = payments[payments.length - 1]?.f24?.idx ?? -1; + const newPaymentIdx = lastPaymentIdx + 1; + payments.push({ + f24: newF24Payment(`${recipient.taxId}-${newPaymentIdx}-f24`, newPaymentIdx), + }); + } + /* eslint-enable functional/immutable-data */ + + const pagoPaPayments = payments + .filter((p) => p.pagoPa) + .map((p) => p.pagoPa as NewNotificationPagoPaPayment); + const f24Payments = payments + .filter((p) => p.f24) + .map((p) => p.f24 as NewNotificationF24Payment); + + return { + ...acc, + [recipient.taxId]: { + pagoPa: pagoPaPayments, + f24: f24Payments, + }, + }; + }, + {} + ), + }), + [] + ); + + const validationSchema = yup.object().shape({ + notificationFeePolicy: yup + .string() + .oneOf(Object.values(NotificationFeePolicy)) + .required(tc('required-field')), + paFee: yup + .mixed() + .optional() + .when('notificationFeePolicy', { + is: NotificationFeePolicy.DELIVERY_MODE, + then: yup + .mixed() + .required(tc('required-field')) + .test('is-currency', `${t('notification-fee.pa-fee')} ${tc('invalid')}`, (value) => + dataRegex.currency.test(String(value)) + ), + }), + vat: yup + .number() + .optional() + .when('notificationFeePolicy', { + is: NotificationFeePolicy.DELIVERY_MODE, + then: yup.number().oneOf(VAT).required(tc('required-field')), + }), + pagoPaIntMode: yup + .string() + .oneOf(Object.values(PagoPaIntegrationMode)) + .test('checkRecipientDebtPosition', tc('required-field'), (value) => { + const hasPagoPaDebtPosition = notification.recipients.some( + (r) => + r.debtPosition === PaymentModel.PAGO_PA || r.debtPosition === PaymentModel.PAGO_PA_F24 + ); + + return !(hasPagoPaDebtPosition && value === PagoPaIntegrationMode.NONE); + }), + recipients: yup.lazy((obj) => + yup + .object( + mapValues(obj, (_, taxId) => + yup.object({ + pagoPa: yup.array().of( + yup.object().when([], { + is: () => { + const debtPosition = notification.recipients.find( + (r) => r.taxId === taxId + )?.debtPosition; + return ( + debtPosition === PaymentModel.PAGO_PA || + debtPosition === PaymentModel.PAGO_PA_F24 + ); + }, + then: () => pagoPaValidationSchema(t, tc), + }) + ), + f24: yup.array().of( + yup.object().when([], { + is: () => { + const debtPosition = notification.recipients.find( + (r) => r.taxId === taxId + )?.debtPosition; + return ( + debtPosition === PaymentModel.F24 || debtPosition === PaymentModel.PAGO_PA_F24 + ); + }, + then: () => f24ValidationSchema(tc), + }) + ), + }) + ) + ) + .test('identicalIUV', t('identical-notice-codes-error'), function (values) { + const errors = identicalIUV(values as any); + + if (errors.length === 0) { + return true; + } + + return new yup.ValidationError( + errors.map( + (e) => new yup.ValidationError(e.messageKey ? t(e.messageKey) : '', e.value, e.id) + ) + ); + }) + .test('apply-cost-validation', t('at-least-one-applycost'), function (values) { + if (this.parent.notificationFeePolicy !== NotificationFeePolicy.DELIVERY_MODE) { + return true; + } + + const validationErrors = checkApplyCost(values as any); + + if (validationErrors.length === 0) { + return true; + } + + return new yup.ValidationError( + validationErrors.map( + (e) => new yup.ValidationError(e.messageKey ? t(e.messageKey) : '', e.value, e.id) + ) + ); + }) + ), + }); + + const updateRefAfterUpload = async (paymentPayload: Array) => { + for (const recipient of paymentPayload) { + const taxId = recipient.taxId; + if (recipient.payments) { + for (const [index, payment] of recipient.payments.entries()) { + if (payment.pagoPa) { + await formik.setFieldValue( + `recipients[${taxId}].pagoPa[${index}].ref`, + payment.pagoPa.ref, + false + ); + } + if (payment.f24) { + await formik.setFieldValue( + `recipients[${taxId}].f24[${index}].ref`, + payment.f24.ref, + false + ); + } + } + } + } + }; + + const formik = useFormik({ + initialValues, + validateOnMount: true, + validationSchema, + enableReinitialize: true, + onSubmit: async () => { + const paymentData = await dispatch(uploadNotificationPaymentDocument(formatPayments())); + const paymentPayload = paymentData.payload as Array; + if (paymentPayload) { + await updateRefAfterUpload(paymentPayload); + } + saveDebtPositionDetail(paymentPayload); + onConfirm(); + }, + }); + + const saveDebtPositionDetail = (recipients: Array) => { + dispatch( + setDebtPositionDetail({ + recipients, + vat: formik.values.vat, + paFee: formik.values.paFee, + notificationFeePolicy: formik.values.notificationFeePolicy, + pagoPaIntMode: formik.values.pagoPaIntMode, + }) + ); + }; + + const isDeliveryMode = + formik.values.notificationFeePolicy === NotificationFeePolicy.DELIVERY_MODE; + + const handleChange = async (e: React.ChangeEvent) => { + const { name, value } = e.target; + + if (name === 'notificationFeePolicy' && value === NotificationFeePolicy.FLAT_RATE) { + await formik.setFieldValue('paFee', undefined); + await formik.setFieldTouched('paFee', false); + + await formik.setFieldValue('vat', undefined); + await formik.setFieldTouched('vat', false); + const updatedRecipients = Object.fromEntries( + Object.entries(formik.values.recipients).map(([taxId, payments]) => [ + taxId, + { + pagoPa: payments.pagoPa.map((payment) => ({ + ...payment, + applyCost: false, + })), + f24: payments.f24.map((payment) => ({ + ...payment, + applyCost: false, + })), + }, + ]) + ); + await formik.setFieldValue('recipients', updatedRecipients); + } + await formik.setFieldValue(name, value); + }; + + const handleChangeTouched = async (e: ChangeEvent) => { + formik.handleChange(e); + await formik.setFieldTouched(e.target.id, true, false); + }; + + const handlePreviousStep = () => { + saveDebtPositionDetail(formatPayments()); + onPreviousStep(); + }; + + useImperativeHandle(forwardedRef, () => ({ + confirm() { + saveDebtPositionDetail(formatPayments()); + }, + })); + + return ( +
+ + + + {t('title')} + + + + + {/* TODO: CHECK IF ARIA-LIVE IS ENOUGH */} + + handleChange(e)} + > + } + label={t('radios.flat-rate')} + componentsProps={{ typography: { fontSize: '16px' } }} + /> + } + label={t('radios.delivery-mode')} + componentsProps={{ typography: { fontSize: '16px' } }} + /> + + {isDeliveryMode && ( + + + + + ), + }} + sx={{ flexBasis: '75%', margin: '0rem 0.8rem' }} + /> + + {VAT.map((option) => ( + + {option}% + + ))} + + + )} + + {isDeliveryMode && ( + {t('notification-fee.disclaimer')} + )} + + + {hasPagoPa && ( + + + + , + ]} + /> + + + {t('alert', { ns: 'notifiche' })} + + + } + label={t('radios.sync')} + componentsProps={{ typography: { fontSize: '16px' } }} + /> + } + label={t('radios.async')} + componentsProps={{ typography: { fontSize: '16px' } }} + /> + + + )} + + + +
+ ); +}; + +// This is a workaorund to prevent cognitive complexity warning +export default forwardRef((props: Omit, ref) => ( + +)); diff --git a/packages/pn-pa-webapp/src/components/NewNotification/F24PaymentBox.tsx b/packages/pn-pa-webapp/src/components/NewNotification/F24PaymentBox.tsx new file mode 100644 index 0000000000..cdabeb6a43 --- /dev/null +++ b/packages/pn-pa-webapp/src/components/NewNotification/F24PaymentBox.tsx @@ -0,0 +1,136 @@ +import { FieldMetaProps } from 'formik'; +import { Fragment } from 'react'; +import { useTranslation } from 'react-i18next'; + +import DeleteIcon from '@mui/icons-material/Delete'; +import { Alert, FormControlLabel, FormHelperText, Stack, Switch, TextField } from '@mui/material'; +import { FileUpload, useIsMobile } from '@pagopa-pn/pn-commons'; +import { ButtonNaked } from '@pagopa/mui-italia'; + +import { NewNotificationF24Payment, NotificationFeePolicy } from '../../models/NewNotification'; + +type PaymentBoxProps = { + id: string; + onFileUploaded: ( + id: string, + file?: File, + sha256?: { hashBase64: string; hashHex: string } + ) => void; + onRemoveFile: (id: string) => void; + f24Payment: NewNotificationF24Payment; + notificationFeePolicy: NotificationFeePolicy; + handleChange: (event: React.ChangeEvent) => void; + showDeleteButton: boolean; + onDeletePayment: () => void; + fieldMeta: (name: string) => FieldMetaProps; +}; + +const F24PaymentBox: React.FC = ({ + id, + onFileUploaded, + onRemoveFile, + f24Payment, + notificationFeePolicy, + handleChange, + showDeleteButton, + onDeletePayment, + fieldMeta, +}) => { + const { t } = useTranslation(['notifiche', 'common']); + const isMobile = useIsMobile('md'); + + const { name, applyCost, file } = f24Payment; + + const getError = (fieldId: string, shouldBeTouched = true) => { + if (!shouldBeTouched) { + return fieldMeta(`${id}.${fieldId}`).error; + } + + if (fieldMeta(`${id}.${fieldId}`).touched) { + return fieldMeta(`${id}.${fieldId}`).error; + } + + return null; + }; + + return ( + + onFileUploaded(id, file, sha256)} + onRemoveFile={() => onRemoveFile(id)} + sx={{ marginTop: '10px' }} + calcSha256 + fileUploaded={{ file }} + showHashCode={false} + /> + + + + {(notificationFeePolicy === NotificationFeePolicy.DELIVERY_MODE || showDeleteButton) && ( + + {notificationFeePolicy === NotificationFeePolicy.DELIVERY_MODE && ( + + handleChange(e)} + /> + } + label={t( + 'new-notification.steps.debt-position-detail.payment-methods.f24.apply-cost' + )} + componentsProps={{ typography: { fontSize: '16px' } }} + /> + {getError('applyCost', false) && ( + {getError('applyCost', false)} + )} + + )} + + {showDeleteButton && ( + } + onClick={onDeletePayment} + sx={{ + justifyContent: { xs: 'flex-start', md: 'flex-end' }, + ml: { xs: 'none', md: 'auto' }, + }} + > + {t('button.delete', { ns: 'common' })} + + )} + + )} + + {showDeleteButton && ( + + {t('new-notification.steps.debt-position-detail.payment-methods.apply-cost-installment')} + + )} + + ); +}; + +export default F24PaymentBox; diff --git a/packages/pn-pa-webapp/src/components/NewNotification/NewNotificationFormElelements.tsx b/packages/pn-pa-webapp/src/components/NewNotification/NewNotificationFormElelements.tsx index 9b2691080e..206aeff53c 100644 --- a/packages/pn-pa-webapp/src/components/NewNotification/NewNotificationFormElelements.tsx +++ b/packages/pn-pa-webapp/src/components/NewNotification/NewNotificationFormElelements.tsx @@ -18,8 +18,8 @@ export const FormBox = ({ testid, children }: { testid?: string; children: React
); -export const FormBoxTitle = ({ text }: { text: string }) => ( - +export const FormBoxTitle = ({ text, id }: { text: string; id?: string }) => ( + {text} ); diff --git a/packages/pn-pa-webapp/src/components/NewNotification/PagoPaPaymentBox.tsx b/packages/pn-pa-webapp/src/components/NewNotification/PagoPaPaymentBox.tsx new file mode 100644 index 0000000000..1cd5737daf --- /dev/null +++ b/packages/pn-pa-webapp/src/components/NewNotification/PagoPaPaymentBox.tsx @@ -0,0 +1,153 @@ +import { FieldMetaProps } from 'formik'; +import { Fragment } from 'react'; +import { useTranslation } from 'react-i18next'; + +import DeleteIcon from '@mui/icons-material/Delete'; +import { Alert, FormControlLabel, FormHelperText, Stack, Switch, TextField } from '@mui/material'; +import { FileUpload, useIsMobile } from '@pagopa-pn/pn-commons'; +import { ButtonNaked } from '@pagopa/mui-italia'; + +import { NewNotificationPagoPaPayment, NotificationFeePolicy } from '../../models/NewNotification'; + +type PaymentBoxProps = { + id: string; + onFileUploaded: ( + id: string, + file?: File, + sha256?: { hashBase64: string; hashHex: string } + ) => void; + onRemoveFile: (id: string) => void; + pagoPaPayment: NewNotificationPagoPaPayment; + notificationFeePolicy: NotificationFeePolicy; + handleChange: (event: React.ChangeEvent) => void; + showDeleteButton: boolean; + onDeletePayment: () => void; + fieldMeta: (name: string) => FieldMetaProps; +}; + +const PagoPaPaymentBox: React.FC = ({ + id, + onFileUploaded, + onRemoveFile, + pagoPaPayment, + notificationFeePolicy, + handleChange, + showDeleteButton, + onDeletePayment, + fieldMeta, +}) => { + const { t } = useTranslation(['notifiche', 'common']); + const isMobile = useIsMobile('md'); + + const { noticeCode, creditorTaxId, applyCost, file } = pagoPaPayment; + + const getError = (fieldId: string, shouldBeTouched = true) => { + if (!shouldBeTouched) { + return fieldMeta(`${id}.${fieldId}`).error; + } + + if (fieldMeta(`${id}.${fieldId}`).touched) { + return fieldMeta(`${id}.${fieldId}`).error; + } + + return null; + }; + + return ( + + onFileUploaded(id, file, sha256)} + onRemoveFile={() => onRemoveFile(id)} + calcSha256 + fileUploaded={{ file }} + showHashCode={false} + /> + + + + + + {(notificationFeePolicy === NotificationFeePolicy.DELIVERY_MODE || showDeleteButton) && ( + + {notificationFeePolicy === NotificationFeePolicy.DELIVERY_MODE && ( + + handleChange(e)} + /> + } + label={t( + 'new-notification.steps.debt-position-detail.payment-methods.pagopa.apply-cost' + )} + componentsProps={{ typography: { fontSize: '16px' } }} + /> + {getError('applyCost', false) && ( + {getError('applyCost', false)} + )} + + )} + + {showDeleteButton && ( + } + onClick={onDeletePayment} + sx={{ + justifyContent: { xs: 'flex-start', md: 'flex-end' }, + ml: { xs: 'none', md: 'auto' }, + }} + > + {t('button.delete', { ns: 'common' })} + + )} + + )} + + {showDeleteButton && notificationFeePolicy === NotificationFeePolicy.DELIVERY_MODE && ( + + {t('new-notification.steps.debt-position-detail.payment-methods.apply-cost-installment')} + + )} + + ); +}; + +export default PagoPaPaymentBox; diff --git a/packages/pn-pa-webapp/src/components/NewNotification/PaymentMethods.tsx b/packages/pn-pa-webapp/src/components/NewNotification/PaymentMethods.tsx index 26618b34e1..821efcb159 100644 --- a/packages/pn-pa-webapp/src/components/NewNotification/PaymentMethods.tsx +++ b/packages/pn-pa-webapp/src/components/NewNotification/PaymentMethods.tsx @@ -1,70 +1,26 @@ import { useFormik } from 'formik'; -import _ from 'lodash'; -import { ForwardedRef, Fragment, forwardRef, useImperativeHandle, useMemo } from 'react'; -import { Trans, useTranslation } from 'react-i18next'; +import { Fragment } from 'react'; +import { useTranslation } from 'react-i18next'; -import { Link, Paper, Typography } from '@mui/material'; -import { FileUpload, SectionHeading, useIsMobile } from '@pagopa-pn/pn-commons'; +import AddIcon from '@mui/icons-material/Add'; +import { Divider, Paper, Stack, Typography } from '@mui/material'; +import { ButtonNaked } from '@pagopa/mui-italia'; import { NewNotification, - NewNotificationDocument, + NewNotificationF24Payment, + NewNotificationPagoPaPayment, + PaymentMethodsFormValues, PaymentModel, - PaymentObject, } from '../../models/NewNotification'; -import { useAppDispatch } from '../../redux/hooks'; -import { uploadNotificationPaymentDocument } from '../../redux/newNotification/actions'; -import { setIsCompleted, setPaymentDocuments } from '../../redux/newNotification/reducers'; -import NewNotificationCard from './NewNotificationCard'; - -type PaymentBoxProps = { - id: string; - title: string; - onFileUploaded: ( - id: string, - file?: File, - sha256?: { hashBase64: string; hashHex: string } - ) => void; - onRemoveFile: (id: string) => void; - fileUploaded?: NewNotificationDocument; -}; - -const PaymentBox: React.FC = ({ - id, - title, - onFileUploaded, - onRemoveFile, - fileUploaded, -}) => { - const { t } = useTranslation(['notifiche']); - const isMobile = useIsMobile('md'); - - return ( - - - {title} - - onFileUploaded(id, file, sha256)} - onRemoveFile={() => onRemoveFile(id)} - sx={{ marginTop: '10px' }} - calcSha256 - fileUploaded={fileUploaded} - /> - - ); -}; +import F24PaymentBox from './F24PaymentBox'; +import PagoPaPaymentBox from './PagoPaPaymentBox'; type Props = { notification: NewNotification; - onConfirm: () => void; - onPreviousStep?: (step?: number) => void; - isCompleted: boolean; - forwardedRef: ForwardedRef; + formik: ReturnType>; + newPagopaPayment: (id: string, idx: number) => NewNotificationPagoPaPayment; + newF24Payment: (id: string, idx: number) => NewNotificationF24Payment; }; const emptyFileData = { @@ -72,229 +28,29 @@ const emptyFileData = { sha256: { hashBase64: '', hashHex: '' }, }; -const newPaymentDocument = (id: string, name: string): NewNotificationDocument => ({ - id, - idx: 0, - name, - contentType: 'application/pdf', - file: emptyFileData, - ref: { - key: '', - versionToken: '', - }, -}); - -/** - * @deprecated - * Last step of the notification creation, where the user configures the payments - * @returns - */ const PaymentMethods: React.FC = ({ notification, - onConfirm, - isCompleted, - onPreviousStep, - forwardedRef, + formik, + newPagopaPayment, + newF24Payment, }) => { - const dispatch = useAppDispatch(); const { t } = useTranslation(['notifiche'], { - keyPrefix: 'new-notification.steps.payment-methods', - }); - const { t: tc } = useTranslation(['common']); - - const paymentDocumentsExists = !_.isNil(notification.payment) && !_.isEmpty(notification.payment); - const initialValues = useMemo( - () => - notification.recipients.reduce((obj: { [key: string]: PaymentObject }, r) => { - const recipientPayment = paymentDocumentsExists - ? (notification.payment as { [key: string]: PaymentObject })[r.taxId] - : undefined; - const pagoPaForm = recipientPayment?.pagoPaForm; - const f24flatRate = recipientPayment?.f24flatRate; - const f24standard = recipientPayment?.f24standard; - // eslint-disable-next-line functional/immutable-data - obj[r.taxId] = { - pagoPaForm: pagoPaForm - ? pagoPaForm - : newPaymentDocument(`${r.taxId}-pagoPaDoc`, t('pagopa-notice')), - }; - if (notification.paymentMode === PaymentModel.PAGO_PA_NOTICE_F24_FLATRATE) { - // eslint-disable-next-line functional/immutable-data - obj[r.taxId].f24flatRate = f24flatRate - ? f24flatRate - : newPaymentDocument(`${r.taxId}-f24flatRateDoc`, t('pagopa-notice-f24-flatrate')); - } - if (notification.paymentMode === PaymentModel.PAGO_PA_NOTICE_F24) { - // eslint-disable-next-line functional/immutable-data - obj[r.taxId].f24standard = f24standard - ? f24standard - : newPaymentDocument(`${r.taxId}-f24standardDoc`, t('pagopa-notice-f24')); - } - return obj; - }, {}), - [] - ); - - const formatPaymentDocuments = () => - notification.recipients.reduce((obj: { [key: string]: PaymentObject }, r) => { - const formikPagoPaForm = formik.values[r.taxId].pagoPaForm; - const formikF24flatRate = formik.values[r.taxId].f24flatRate; - const formikF24standard = formik.values[r.taxId].f24standard; - // I avoid including empty file object into the result - // hence I check for any file object that it actually points to a file - // (this is the condition XXX.file.data) - // and then I don't add the payment info for a recipient if it doesn't include any actual file pointer - // (this is the Object.keys(paymentsForThisRecipient).length > 0 condition below) - // --------------------------------------------- - // Carlos Lombardi, 2023.01.10 - const paymentsForThisRecipient: any = {}; - if (formikPagoPaForm.file.data) { - // eslint-disable-next-line functional/immutable-data - paymentsForThisRecipient.pagoPaForm = { - ...newPaymentDocument(`${r.taxId}-pagoPaDoc`, t('pagopa-notice')), - file: { - data: formikPagoPaForm.file.data, - sha256: { - hashBase64: formikPagoPaForm.file.sha256.hashBase64, - hashHex: formikPagoPaForm.file.sha256.hashHex, - }, - }, - ref: { - key: formikPagoPaForm.ref.key, - versionToken: formikPagoPaForm.ref.versionToken, - }, - }; - } - if (formikF24flatRate?.file.data) { - // eslint-disable-next-line functional/immutable-data - paymentsForThisRecipient.f24flatRate = { - ...newPaymentDocument(`${r.taxId}-f24flatRateDoc`, t('pagopa-notice-f24-flatrate')), - file: { - data: formikF24flatRate.file.data, - sha256: { - hashBase64: formikF24flatRate.file.sha256.hashBase64, - hashHex: formikF24flatRate.file.sha256.hashHex, - }, - }, - ref: { - key: formikF24flatRate.ref.key, - versionToken: formikF24flatRate.ref.versionToken, - }, - }; - } - if (formikF24standard?.file.data) { - // eslint-disable-next-line functional/immutable-data - paymentsForThisRecipient.f24standard = { - ...newPaymentDocument(`${r.taxId}-f24standardDoc`, t('pagopa-notice-f24')), - file: { - data: formikF24standard.file.data, - sha256: { - hashBase64: formikF24standard.file.sha256.hashBase64, - hashHex: formikF24standard.file.sha256.hashHex, - }, - }, - ref: { - key: formikF24standard.ref.key, - versionToken: formikF24standard.ref.versionToken, - }, - }; - } - if (Object.keys(paymentsForThisRecipient).length > 0) { - // eslint-disable-next-line functional/immutable-data - obj[r.taxId] = paymentsForThisRecipient; - } - return obj; - }, {}); - - const handlePreviousStep = () => { - if (onPreviousStep) { - dispatch(setPaymentDocuments({ paymentDocuments: formatPaymentDocuments() })); - onPreviousStep(); - } - }; - - const updateRefAfterUpload = async (paymentPayload: { [key: string]: PaymentObject }) => { - // set ref - for (const [taxId, payment] of Object.entries(paymentPayload)) { - if (payment.pagoPaForm) { - await formik.setFieldValue(`${taxId}.pagoPaForm.ref`, payment.pagoPaForm.ref, false); - } - if (payment.f24standard) { - await formik.setFieldValue(`${taxId}.f24standard.ref`, payment.f24standard.ref, false); - } - if (payment.f24flatRate) { - await formik.setFieldValue(`${taxId}.f24flatRate.ref`, payment.f24flatRate.ref, false); - } - } - }; - - const formIsEmpty = (values: any) => { - // eslint-disable-next-line functional/no-let - let isEmpty = true; - notification.recipients.forEach((recipient) => { - const currentDocument = values[recipient.taxId]; - if (currentDocument.pagoPaForm && currentDocument.pagoPaForm.file.name !== '') { - isEmpty = false; - } - if (currentDocument.f24flatRate && currentDocument.f24flatRate.file.name !== '') { - isEmpty = false; - } - if (currentDocument.f24standard && currentDocument.f24standard.file.name !== '') { - isEmpty = false; - } - }); - return isEmpty; - }; - - const formik = useFormik({ - initialValues, - validateOnMount: true, - onSubmit: async (values) => { - const emptyForm = formIsEmpty(values); - if (isCompleted) { - onConfirm(); - } else if (emptyForm || notification.paymentMode === PaymentModel.NOTHING) { - // Maybe now the form is empty, but in the previous time the user went back - // from the payments step the form wasn't empty. - // Just in case, we clean the payment info from the Redux store - dispatch(setPaymentDocuments({ paymentDocuments: {} })); - dispatch(setIsCompleted()); - } else { - // Beware! - - // Recall that the taxId is the key for the payment document info in the Redux storage. - // If the user changes the taxId of a recipient and/or deletes a recipient - // after having attached payment documents, - // the information related to the "old" taxIds is kept in the Redux store - // until the user returns to the payment document step. - // Fortunately, the formatPaymentDocuments function "sanitizes" the payment document info, - // since it includes the information related to current taxIds only. - // If the call to formatPaymentDocuments were omitted, then we would probably risk sending - // garbage to the API call. - // Please take this note into consideration in case of refactoring of this part. - // -------------------------------------- - // Carlos Lombardi, 2023.01.19 - const paymentData = await dispatch( - uploadNotificationPaymentDocument(formatPaymentDocuments()) - ); - const paymentPayload = paymentData.payload as { [key: string]: PaymentObject }; - if (paymentPayload) { - await updateRefAfterUpload(paymentPayload); - } - } - }, + keyPrefix: 'new-notification.steps.debt-position-detail.payment-methods', }); const fileUploadedHandler = async ( taxId: string, - paymentType: 'pagoPaForm' | 'f24flatRate' | 'f24standard', - id: string, + paymentType: 'pagoPa' | 'f24', + index: number, file?: File, sha256?: { hashBase64: string; hashHex: string } ) => { + const payment = formik.values.recipients[taxId][paymentType][index]; + await formik.setFieldValue( - id, + `recipients.${taxId}.${paymentType}.${index}`, { - ...formik.values[taxId][paymentType], + ...payment, file: { data: file, sha256 }, ref: { key: '', @@ -303,16 +59,14 @@ const PaymentMethods: React.FC = ({ }, false ); - await formik.setFieldTouched(`${id}.file`, true, true); + await formik.setFieldTouched(`recipients.${taxId}.${paymentType}.${index}.file`, true, true); }; - const removeFileHandler = async ( - id: string, - taxId: string, - paymentType: 'pagoPaForm' | 'f24flatRate' | 'f24standard' - ) => { - await formik.setFieldValue(id, { - ...formik.values[taxId][paymentType], + const removeFileHandler = async (taxId: string, paymentType: 'pagoPa' | 'f24', index: number) => { + const payment = formik.values.recipients[taxId][paymentType][index]; + + await formik.setFieldValue(`recipients.${taxId}.${paymentType}.${index}`, { + ...payment, file: emptyFileData, ref: { key: '', @@ -321,102 +75,154 @@ const PaymentMethods: React.FC = ({ }); }; - useImperativeHandle(forwardedRef, () => ({ - confirm() { - dispatch(setPaymentDocuments({ paymentDocuments: formatPaymentDocuments() })); - }, - })); + const handleChange = async ( + event: React.ChangeEvent, + taxId: string, + paymentType: 'pagoPa' | 'f24', + paymentIndex: number + ) => { + const value = event.target.name === 'applyCost' ? event.target.checked : event.target.value; + + await formik.setFieldValue( + `recipients.${taxId}.${paymentType}.${paymentIndex}.${event.target.name}`, + value + ); + await formik.setFieldTouched( + `recipients.${taxId}.${paymentType}.${paymentIndex}.${event.target.name}`, + true, + true + ); + }; + + const handleAddNewPagoPa = async (taxId: string) => { + const newPayment = newPagopaPayment(taxId, formik.values.recipients[taxId].pagoPa.length); + await formik.setFieldValue(`recipients.${taxId}.pagoPa`, [ + ...formik.values.recipients[taxId].pagoPa, + newPayment, + ]); + }; + + const handleAddNewF24 = async (taxId: string) => { + const newPayment = newF24Payment(taxId, formik.values.recipients[taxId].f24.length); + await formik.setFieldValue(`recipients.${taxId}.f24`, [ + ...formik.values.recipients[taxId].f24, + newPayment, + ]); + }; + + const handleRemovePagoPa = async (taxId: string, index: number) => { + const pagoPaPayments = formik.values.recipients[taxId].pagoPa.filter((_, i) => i !== index); + await formik.setFieldValue(`recipients.${taxId}.pagoPa`, pagoPaPayments); + }; + + const handleRemoveF24 = async (taxId: string, index: number) => { + const f24Payments = formik.values.recipients[taxId].f24.filter((_, i) => i !== index); + await formik.setFieldValue(`recipients.${taxId}.f24`, f24Payments); + }; return ( -
- handlePreviousStep()} - > - {notification.paymentMode !== PaymentModel.NOTHING && - notification.recipients.map((recipient) => ( - - - {t('payment-models')} {recipient.firstName} {recipient.lastName} - - - fileUploadedHandler(recipient.taxId, 'pagoPaForm', id, file, sha256) - } - onRemoveFile={(id) => removeFileHandler(id, recipient.taxId, 'pagoPaForm')} - fileUploaded={formik.values[recipient.taxId].pagoPaForm} - /> - {notification.paymentMode === PaymentModel.PAGO_PA_NOTICE_F24_FLATRATE && ( - - fileUploadedHandler(recipient.taxId, 'f24flatRate', id, file, sha256) - } - onRemoveFile={(id) => removeFileHandler(id, recipient.taxId, 'f24flatRate')} - fileUploaded={formik.values[recipient.taxId].f24flatRate} - /> - )} - {notification.paymentMode === PaymentModel.PAGO_PA_NOTICE_F24 && ( - - fileUploadedHandler(recipient.taxId, 'f24standard', id, file, sha256) - } - onRemoveFile={(id) => removeFileHandler(id, recipient.taxId, 'f24standard')} - fileUploaded={formik.values[recipient.taxId].f24standard} - /> - )} - - ))} - {notification.paymentMode === PaymentModel.NOTHING && ( - - , - + {notification.recipients.map((recipient) => { + if (recipient.debtPosition === PaymentModel.NOTHING) { + return <>; + } + return ( + + + {t('payment-models')} {recipient.firstName} {recipient.lastName} + + + {formik.values.recipients[recipient.taxId].pagoPa.length > 0 && ( + + )} + + {formik.values.recipients[recipient.taxId].f24.length > 0 && ( + } > - Informazioni preliminari - - -  e seleziona un modello. Poi, torna qui per caricarlo. - - + + {t('f24.attach-f24')} + + {formik.values.recipients[recipient.taxId].f24.map((f24Payment, index) => ( + + fileUploadedHandler(recipient.taxId, 'f24', index, file, sha256) + } + onRemoveFile={() => removeFileHandler(recipient.taxId, 'f24', index)} + f24Payment={f24Payment} + notificationFeePolicy={formik.values.notificationFeePolicy} + handleChange={(event) => handleChange(event, recipient.taxId, 'f24', index)} + showDeleteButton={index > 0} + onDeletePayment={() => handleRemoveF24(recipient.taxId, index)} + fieldMeta={(fieldName) => formik.getFieldMeta(fieldName)} + /> + ))} + + } + onClick={() => handleAddNewF24(recipient.taxId)} + sx={{ justifyContent: 'start' }} + > + {t('f24.add-new-f24')} + + + )} - )} - -
+ ); + })} + ); }; -// This is a workaorund to prevent cognitive complexity warning -export default forwardRef((props: Omit, ref) => ( - -)); +export default PaymentMethods; diff --git a/packages/pn-pa-webapp/src/components/NewNotification/PreliminaryInformations.tsx b/packages/pn-pa-webapp/src/components/NewNotification/PreliminaryInformations.tsx index 60b04f1fde..94b2f140b6 100644 --- a/packages/pn-pa-webapp/src/components/NewNotification/PreliminaryInformations.tsx +++ b/packages/pn-pa-webapp/src/components/NewNotification/PreliminaryInformations.tsx @@ -26,13 +26,12 @@ import { LangCode } from '@pagopa/mui-italia'; import { NewNotification, NewNotificationLangOther, - PaymentModel, + PreliminaryInformationsPayload, } from '../../models/NewNotification'; import { GroupStatus } from '../../models/user'; import { useAppDispatch, useAppSelector } from '../../redux/hooks'; import { NEW_NOTIFICATION_ACTIONS, getUserGroups } from '../../redux/newNotification/actions'; import { setPreliminaryInformations } from '../../redux/newNotification/reducers'; -import { PreliminaryInformationsPayload } from '../../redux/newNotification/types'; import { RootState } from '../../redux/store'; import { getConfiguration } from '../../services/configuration.service'; import { requiredStringFieldValidation } from '../../utility/validation.utility'; @@ -78,7 +77,7 @@ const PreliminaryInformations = ({ notification, onConfirm }: Props) => { const initialValues = useCallback(() => { const additionalLang = additionalLanguages?.length > 0 ? additionalLanguages[0] : undefined; - + return { paProtocolNumber: notification.paProtocolNumber || '', subject: notification.subject || '', @@ -87,7 +86,6 @@ const PreliminaryInformations = ({ notification, onConfirm }: Props) => { group: notification.group ?? '', taxonomyCode: notification.taxonomyCode || '', physicalCommunicationType: notification.physicalCommunicationType || '', - paymentMode: notification.paymentMode || (IS_PAYMENT_ENABLED ? '' : PaymentModel.NOTHING), lang: notification.lang || (additionalLang ? NewNotificationLangOther : 'it'), additionalLang: notification.additionalLang || additionalLang || '', additionalSubject: notification.additionalSubject || '', @@ -97,11 +95,10 @@ const PreliminaryInformations = ({ notification, onConfirm }: Props) => { const validationSchema = yup.object({ paProtocolNumber: requiredStringFieldValidation(tc, 256), - subject: yup.string() - .when('lang',{ + subject: yup.string().when('lang', { is: NewNotificationLangOther, then: requiredStringFieldValidation(tc, 66, 10), - otherwise: requiredStringFieldValidation(tc, 134, 10) + otherwise: requiredStringFieldValidation(tc, 134, 10), }), senderDenomination: yup .string() @@ -112,7 +109,6 @@ const PreliminaryInformations = ({ notification, onConfirm }: Props) => { .max(1024, tc('too-long-field-error', { maxLength: 1024 })) .matches(dataRegex.noSpaceAtEdges, tc('no-spaces-at-edges')), physicalCommunicationType: yup.string().required(), - paymentMode: yup.string().required(), group: hasGroups ? yup.string().required() : yup.string(), taxonomyCode: yup .string() @@ -159,9 +155,9 @@ const PreliminaryInformations = ({ notification, onConfirm }: Props) => { const handleChange = (e: ChangeEvent) => { const value = e.target.value; - if(value === 'it'){ - void formik.setValues({...formik.values, additionalLang:'', additionalSubject: ''},false); - void formik.setFieldTouched('additionalSubject',false); + if (value === 'it') { + void formik.setValues({ ...formik.values, additionalLang: '', additionalSubject: '' }, false); + void formik.setFieldTouched('additionalSubject', false); } formik.handleChange(e); }; @@ -313,47 +309,6 @@ const PreliminaryInformations = ({ notification, onConfirm }: Props) => { ))} - - {IS_PAYMENT_ENABLED && ( - - - - {`${t('payment-method')}*`} - - - - } - label={t('pagopa-notice')} - data-testid="paymentMethodRadio" - /> - } - label={t('pagopa-notice-f24-flatrate')} - data-testid="paymentMethodRadio" - /> - } - label={t('pagopa-notice-f24')} - data-testid="paymentMethodRadio" - /> - } - label={t('nothing')} - data-testid="paymentMethodRadio" - /> - - - )} diff --git a/packages/pn-pa-webapp/src/components/NewNotification/PreliminaryInformationsContent.tsx b/packages/pn-pa-webapp/src/components/NewNotification/PreliminaryInformationsContent.tsx index f6974a0da3..559910dbfe 100644 --- a/packages/pn-pa-webapp/src/components/NewNotification/PreliminaryInformationsContent.tsx +++ b/packages/pn-pa-webapp/src/components/NewNotification/PreliminaryInformationsContent.tsx @@ -5,8 +5,7 @@ import { useTranslation } from 'react-i18next'; import { TextField, Typography, useFormControl } from '@mui/material'; import { LangCode, LangLabels } from '@pagopa/mui-italia'; -import { NewNotificationLangOther } from '../../models/NewNotification'; -import { PreliminaryInformationsPayload } from '../../redux/newNotification/types'; +import { NewNotificationLangOther, PreliminaryInformationsPayload } from '../../models/NewNotification'; import { FormBox, FormBoxSubtitle, FormBoxTitle } from './NewNotificationFormElelements'; type SubjectFocusHelperTextProps = { diff --git a/packages/pn-pa-webapp/src/components/NewNotification/PreliminaryInformationsLang.tsx b/packages/pn-pa-webapp/src/components/NewNotification/PreliminaryInformationsLang.tsx index d5ff80ad5a..00e0072b5f 100644 --- a/packages/pn-pa-webapp/src/components/NewNotification/PreliminaryInformationsLang.tsx +++ b/packages/pn-pa-webapp/src/components/NewNotification/PreliminaryInformationsLang.tsx @@ -14,8 +14,7 @@ import { import { CustomDropdown } from '@pagopa-pn/pn-commons'; import { LangCode, LangLabels } from '@pagopa/mui-italia'; -import { BILINGUALISM_LANGUAGES, NewNotificationLangOther } from '../../models/NewNotification'; -import { PreliminaryInformationsPayload } from '../../redux/newNotification/types'; +import { BILINGUALISM_LANGUAGES, NewNotificationLangOther, PreliminaryInformationsPayload } from '../../models/NewNotification'; import { FormBox, FormBoxSubtitle, FormBoxTitle } from './NewNotificationFormElelements'; type Props = { diff --git a/packages/pn-pa-webapp/src/components/NewNotification/Recipient.tsx b/packages/pn-pa-webapp/src/components/NewNotification/Recipient.tsx index 5dccde2eea..0a701cd866 100644 --- a/packages/pn-pa-webapp/src/components/NewNotification/Recipient.tsx +++ b/packages/pn-pa-webapp/src/components/NewNotification/Recipient.tsx @@ -14,15 +14,17 @@ import { Stack, Typography, } from '@mui/material'; -import { DigitalDomicileType, RecipientType, dataRegex } from '@pagopa-pn/pn-commons'; +import { RecipientType, dataRegex } from '@pagopa-pn/pn-commons'; import { ButtonNaked } from '@pagopa/mui-italia'; -import { NewNotificationRecipient, PaymentModel } from '../../models/NewNotification'; +import { + NewNotificationDigitalAddressType, + NewNotificationRecipient, +} from '../../models/NewNotification'; import { useAppDispatch } from '../../redux/hooks'; import { saveRecipients } from '../../redux/newNotification/reducers'; import { denominationLengthAndCharacters, - identicalIUV, identicalTaxIds, requiredStringFieldValidation, taxIdDependingOnRecipientType, @@ -35,11 +37,9 @@ import PhysicalAddress from './PhysicalAddress'; const singleRecipient = { recipientType: RecipientType.PF, taxId: '', - creditorTaxId: '', - noticeCode: '', firstName: '', lastName: '', - type: DigitalDomicileType.PEC, + type: NewNotificationDigitalAddressType.PEC, digitalDomicile: '', address: '', houseNumber: '', @@ -56,7 +56,6 @@ type FormRecipients = { }; type Props = { - paymentMode: PaymentModel | undefined; onConfirm: () => void; onPreviousStep?: () => void; recipientsData?: Array; @@ -64,7 +63,6 @@ type Props = { }; const Recipient: React.FC = ({ - paymentMode, onConfirm, onPreviousStep, recipientsData, @@ -89,95 +87,77 @@ const Recipient: React.FC = ({ } : { recipients: [{ ...singleRecipient, idx: 0, id: 'recipient.0' }] }; - const buildRecipientValidationObject = () => { - const validationObject = { - recipientType: yup.string(), - // validazione sulla denominazione (firstName + " " + lastName per PF, firstName per PG) - // la lunghezza non può superare i 80 caratteri - firstName: requiredStringFieldValidation(tc).test({ + const buildRecipientValidationObject = () => ({ + recipientType: yup.string(), + // validazione sulla denominazione (firstName + " " + lastName per PF, firstName per PG) + // la lunghezza non può superare i 80 caratteri + firstName: requiredStringFieldValidation(tc).test({ + name: 'denominationLengthAndCharacters', + test(value?: string) { + const error = denominationLengthAndCharacters(value, this.parent.lastName); + if (error) { + return this.createError({ + message: + error.messageKey === 'too-long-field-error' + ? tc(error.messageKey, error.data) + : t(error.messageKey, error.data), + path: this.path, + }); + } + return true; + }, + }), + // la validazione di lastName è condizionale perché per persone giuridiche questo attributo + // non viene richiesto + lastName: yup.string().when('recipientType', { + is: (value: string) => value !== RecipientType.PG, + then: requiredStringFieldValidation(tc).test({ name: 'denominationLengthAndCharacters', test(value?: string) { - const error = denominationLengthAndCharacters(value, this.parent.lastName); + const error = denominationLengthAndCharacters(this.parent.firstName, value as string); if (error) { return this.createError({ - message: - error.messageKey === 'too-long-field-error' - ? tc(error.messageKey, error.data) - : t(error.messageKey, error.data), + message: ' ', path: this.path, }); } return true; }, }), - // la validazione di lastName è condizionale perché per persone giuridiche questo attributo - // non viene richiesto - lastName: yup.string().when('recipientType', { - is: (value: string) => value !== RecipientType.PG, - then: requiredStringFieldValidation(tc).test({ - name: 'denominationLengthAndCharacters', - test(value?: string) { - const error = denominationLengthAndCharacters(this.parent.firstName, value as string); - if (error) { - return this.createError({ - message: ' ', - path: this.path, - }); - } - return true; - }, - }), + }), + taxId: yup + .string() + .required(tc('required-field')) + // validazione su CF: deve accettare solo formato a 16 caratteri per PF, e sia 16 sia 11 caratteri per PG + .test('taxIdDependingOnRecipientType', t('fiscal-code-error'), function (value) { + return taxIdDependingOnRecipientType(value, this.parent.recipientType); }), - taxId: yup - .string() - .required(tc('required-field')) - // validazione su CF: deve accettare solo formato a 16 caratteri per PF, e sia 16 sia 11 caratteri per PG - .test('taxIdDependingOnRecipientType', t('fiscal-code-error'), function (value) { - return taxIdDependingOnRecipientType(value, this.parent.recipientType); - }), - digitalDomicile: yup - .string() - .max(320, tc('too-long-field-error')) - .matches(dataRegex.noSpaceAtEdges, tc('no-spaces-at-edges')) - .matches(dataRegex.email, t('pec-error')), - address: requiredStringFieldValidation(tc, 1024), - houseNumber: yup.string().required(tc('required-field')), - /* + digitalDomicile: yup + .string() + .max(320, tc('too-long-field-error')) + .matches(dataRegex.noSpaceAtEdges, tc('no-spaces-at-edges')) + .matches(dataRegex.email, t('pec-error')), + address: requiredStringFieldValidation(tc, 1024), + houseNumber: yup.string().required(tc('required-field')), + /* addressDetails: yup.string().when('showPhysicalAddress', { is: true, then: yup.string().required(tc('required-field')), }), */ - zip: yup - .string() - .required(tc('required-field')) - .max(12, tc('too-long-field-error', { maxLength: 12 })) - .matches(dataRegex.zipCode, `${t('zip')} ${tc('invalid')}`), - municipalityDetails: yup - .string() - .max(256, tc('too-long-field-error', { maxLength: 256 })) - .matches(dataRegex.noSpaceAtEdges, tc('no-spaces-at-edges')), - municipality: requiredStringFieldValidation(tc, 256), - province: requiredStringFieldValidation(tc, 256), - foreignState: requiredStringFieldValidation(tc), - }; - - if (paymentMode !== PaymentModel.NOTHING) { - return { - ...validationObject, - creditorTaxId: yup - .string() - .required(tc('required-field')) - .matches(dataRegex.pIva, t('fiscal-code-error')), - noticeCode: yup - .string() - .matches(dataRegex.noticeCode, t('notice-code-error')) - .required(tc('required-field')), - }; - } - - return validationObject; - }; + zip: yup + .string() + .required(tc('required-field')) + .max(12, tc('too-long-field-error', { maxLength: 12 })) + .matches(dataRegex.zipCode, `${t('zip')} ${tc('invalid')}`), + municipalityDetails: yup + .string() + .max(256, tc('too-long-field-error', { maxLength: 256 })) + .matches(dataRegex.noSpaceAtEdges, tc('no-spaces-at-edges')), + municipality: requiredStringFieldValidation(tc, 256), + province: requiredStringFieldValidation(tc, 256), + foreignState: requiredStringFieldValidation(tc), + }); const validationSchema = yup.object({ recipients: yup @@ -191,20 +171,6 @@ const Recipient: React.FC = ({ return new yup.ValidationError( errors.map((e) => new yup.ValidationError(t(e.messageKey), e.value, e.id)) ); - }) - .test('identicalIUV', t('identical-fiscal-codes-error'), (values) => { - const errors = identicalIUV( - values as Array | undefined, - paymentMode - ); - if (errors.length === 0) { - return true; - } - return new yup.ValidationError( - errors.map( - (e) => new yup.ValidationError(e.messageKey ? t(e.messageKey) : '', e.value, e.id) - ) - ); }), }); @@ -464,33 +430,6 @@ const Recipient: React.FC = ({ /> - - {paymentMode !== PaymentModel.NOTHING && ( - - - - - - - - - )} {values.recipients.length < 5 && values.recipients.length - 1 === index && ( diff --git a/packages/pn-pa-webapp/src/components/NewNotification/__test__/Attachments.test.tsx b/packages/pn-pa-webapp/src/components/NewNotification/__test__/Attachments.test.tsx index 181dc1ad3f..be869c5572 100644 --- a/packages/pn-pa-webapp/src/components/NewNotification/__test__/Attachments.test.tsx +++ b/packages/pn-pa-webapp/src/components/NewNotification/__test__/Attachments.test.tsx @@ -88,7 +88,7 @@ describe('Attachments Component with payment enabled', async () => { const buttonSubmit = result.getByTestId('step-submit'); const buttonPrevious = result.getByTestId('previous-step'); expect(buttonSubmit).toBeDisabled(); - expect(buttonSubmit).toHaveTextContent('button.continue'); + expect(buttonSubmit).toHaveTextContent('button.send'); expect(buttonPrevious).toBeInTheDocument(); }); diff --git a/packages/pn-pa-webapp/src/components/NewNotification/__test__/DebtPosition.test.tsx b/packages/pn-pa-webapp/src/components/NewNotification/__test__/DebtPosition.test.tsx new file mode 100644 index 0000000000..ac17c33421 --- /dev/null +++ b/packages/pn-pa-webapp/src/components/NewNotification/__test__/DebtPosition.test.tsx @@ -0,0 +1,299 @@ +import { vi } from 'vitest'; + +import { testRadio } from '@pagopa-pn/pn-commons/src/test-utils'; + +import { newNotification } from '../../../__mocks__/NewNotification.mock'; +import { fireEvent, render, testStore, waitFor } from '../../../__test__/test-utils'; +import { PaymentModel } from '../../../models/NewNotification'; +import DebtPosition from '../DebtPosition'; + +const recipientsWithoutPayment = newNotification.recipients.map( + ({ payments, debtPosition, ...recipient }) => recipient +); +const confirmHandlerMk = vi.fn(); +const goToLasStepMk = vi.fn(); +const previousStepMk = vi.fn(); + +// mock imports +vi.mock('react-i18next', () => ({ + // this mock makes sure any components using the translate hook can use it without a warning being shown + useTranslation: () => ({ + t: (str: string) => str, + i18n: { language: 'it' }, + }), +})); + +describe('DebtPosition Component', async () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + it('renders component - empty state (one recipient)', async () => { + // render component + const { getByTestId } = render( + + ); + // we wait that the component is correctly rendered + const paymentChoiceBox = await waitFor(() => getByTestId('payments-type-choice')); + expect(paymentChoiceBox).toHaveTextContent('debt-position'); + expect(paymentChoiceBox).toHaveTextContent('which-type-of-payments'); + await testRadio(paymentChoiceBox, 'paymentModel', [ + 'radios.pago-pa', + 'radios.f24', + 'radios.pago-pa-f24', + 'radios.nothing', + ]); + const buttonSubmit = getByTestId('step-submit'); + const buttonPrevious = getByTestId('previous-step'); + expect(buttonSubmit).toBeDisabled(); + expect(buttonSubmit).toHaveTextContent('button.continue'); + expect(buttonPrevious).toBeInTheDocument(); + expect(buttonPrevious).toHaveTextContent('back-to-recipient'); + // check the click on prev button + fireEvent.click(buttonPrevious); + expect(previousStepMk).toHaveBeenCalledTimes(1); + }); + + it('renders component - empty state (multi recipients)', async () => { + // render component + const { getAllByTestId, getByTestId } = render( + + ); + // we wait that the component is correctly rendered + const paymentChoiceBoxes = await waitFor(() => getAllByTestId('payments-type-choice')); + expect(paymentChoiceBoxes).toHaveLength(recipientsWithoutPayment.length); + for (const paymentChoiceBox of paymentChoiceBoxes) { + expect(paymentChoiceBox).toHaveTextContent('debt-position-of'); + expect(paymentChoiceBox).toHaveTextContent('which-type-of-payments'); + await testRadio(paymentChoiceBox, 'paymentModel', [ + 'radios.pago-pa', + 'radios.f24', + 'radios.pago-pa-f24', + 'radios.nothing', + ]); + } + const buttonSubmit = getByTestId('step-submit'); + const buttonPrevious = getByTestId('previous-step'); + expect(buttonSubmit).toBeDisabled(); + expect(buttonSubmit).toHaveTextContent('button.continue'); + expect(buttonPrevious).toBeInTheDocument(); + expect(buttonPrevious).toHaveTextContent('back-to-recipient'); + }); + + it('choose an option (two recipients)', async () => { + // render component + const { getAllByTestId, getByTestId } = render( + , + { + preloadedState: { + newNotificationState: { + notification: { + ...newNotification, + recipients: [recipientsWithoutPayment[0], recipientsWithoutPayment[1]], + }, + }, + }, + } + ); + // we wait that the component is correctly rendered + const paymentChoiceBoxes = await waitFor(() => getAllByTestId('payments-type-choice')); + expect(paymentChoiceBoxes).toHaveLength(recipientsWithoutPayment.length); + const buttonSubmit = getByTestId('step-submit'); + expect(buttonSubmit).toBeDisabled(); + // choose an option for the first recipient + await testRadio( + paymentChoiceBoxes[0], + 'paymentModel', + ['radios.pago-pa', 'radios.f24', 'radios.pago-pa-f24', 'radios.nothing'], + 1, + true + ); + // to enable the continue button we need that all the recipients have an option selected + expect(buttonSubmit).toBeDisabled(); + // choose an option for the second recipient + await testRadio( + paymentChoiceBoxes[1], + 'paymentModel', + ['radios.pago-pa', 'radios.f24', 'radios.pago-pa-f24', 'radios.nothing'], + 3, + true + ); + expect(buttonSubmit).toBeEnabled(); + fireEvent.click(buttonSubmit); + // check if redux is updated correctly + await waitFor(() => + expect(testStore.getState().newNotificationState.notification.recipients).toStrictEqual( + [recipientsWithoutPayment[0], recipientsWithoutPayment[1]].map((recipient, index) => ({ + ...recipient, + debtPosition: index === 0 ? PaymentModel.F24 : PaymentModel.NOTHING, + payments: [], + })) + ) + ); + expect(confirmHandlerMk).toHaveBeenCalledTimes(1); + }); + + it('choose an option (two recipients) - back button', async () => { + // render component + const { getAllByTestId, getByTestId } = render( + , + { + preloadedState: { + newNotificationState: { + notification: { + ...newNotification, + recipients: recipientsWithoutPayment, + }, + }, + }, + } + ); + // we wait that the component is correctly rendered + const paymentChoiceBoxes = await waitFor(() => getAllByTestId('payments-type-choice')); + expect(paymentChoiceBoxes).toHaveLength(recipientsWithoutPayment.length); + const buttonPrevious = getByTestId('previous-step'); + // choose first option for all the recipients + for (const paymentChoiceBox of paymentChoiceBoxes) { + expect(paymentChoiceBox).toHaveTextContent('debt-position-of'); + expect(paymentChoiceBox).toHaveTextContent('which-type-of-payments'); + await testRadio( + paymentChoiceBox, + 'paymentModel', + ['radios.pago-pa', 'radios.f24', 'radios.pago-pa-f24', 'radios.nothing'], + 0, + true + ); + } + fireEvent.click(buttonPrevious); + // check if redux is updated correctly + await waitFor(() => + expect(testStore.getState().newNotificationState.notification.recipients).toStrictEqual( + recipientsWithoutPayment.map((recipient) => ({ + ...recipient, + debtPosition: PaymentModel.PAGO_PA, + payments: [], + })) + ) + ); + expect(previousStepMk).toHaveBeenCalledTimes(1); + }); + + it('choose nothing option (multi recipients)', async () => { + // render component + const { getAllByTestId, getByTestId } = render( + , + { + preloadedState: { + newNotificationState: { + notification: { + ...newNotification, + recipients: recipientsWithoutPayment, + }, + }, + }, + } + ); + // we wait that the component is correctly rendered + const paymentChoiceBoxes = await waitFor(() => getAllByTestId('payments-type-choice')); + expect(paymentChoiceBoxes).toHaveLength(recipientsWithoutPayment.length); + const buttonSubmit = getByTestId('step-submit'); + expect(buttonSubmit).toBeDisabled(); + // choose nothing option for all the recipients + for (const paymentChoiceBox of paymentChoiceBoxes) { + expect(paymentChoiceBox).toHaveTextContent('debt-position-of'); + expect(paymentChoiceBox).toHaveTextContent('which-type-of-payments'); + await testRadio( + paymentChoiceBox, + 'paymentModel', + ['radios.pago-pa', 'radios.f24', 'radios.pago-pa-f24', 'radios.nothing'], + 3, + true + ); + } + expect(buttonSubmit).toBeEnabled(); + fireEvent.click(buttonSubmit); + // check if redux is updated correctly + await waitFor(() => + expect(testStore.getState().newNotificationState.notification.recipients).toStrictEqual( + recipientsWithoutPayment.map((recipient) => ({ + ...recipient, + debtPosition: PaymentModel.NOTHING, + payments: [], + })) + ) + ); + expect(goToLasStepMk).toHaveBeenCalledTimes(1); + }); + + it('initally filled (multi recipients)', async () => { + // render component + const { getAllByTestId, getByTestId } = render( + , + { + preloadedState: { + newNotificationState: { + notification: newNotification, + }, + }, + } + ); + // we wait that the component is correctly rendered + const paymentChoiceBoxes = await waitFor(() => getAllByTestId('payments-type-choice')); + expect(paymentChoiceBoxes).toHaveLength(newNotification.recipients.length); + const buttonSubmit = getByTestId('step-submit'); + expect(buttonSubmit).toBeEnabled(); + // check that radio buttons are correctly filled + const radioOptions = ['radios.pago-pa', 'radios.f24', 'radios.pago-pa-f24', 'radios.nothing']; + let recipientIdx = 0; + for (const paymentChoiceBox of paymentChoiceBoxes) { + // get radio value from debtPosition set for the recipient + const radioSelectedValue = newNotification.recipients[recipientIdx].debtPosition + ?.toLocaleLowerCase() + .replace(/_/g, '-'); + const radioSelectedIndex = radioOptions.indexOf(`radios.${radioSelectedValue!}`); + expect(paymentChoiceBox).toHaveTextContent('debt-position-of'); + expect(paymentChoiceBox).toHaveTextContent('which-type-of-payments'); + await testRadio(paymentChoiceBox, 'paymentModel', radioOptions, radioSelectedIndex); + recipientIdx++; + } + expect(buttonSubmit).toBeEnabled(); + fireEvent.click(buttonSubmit); + // check if redux is not updated + await waitFor(() => + expect(testStore.getState().newNotificationState.notification.recipients).toStrictEqual( + newNotification.recipients + ) + ); + expect(confirmHandlerMk).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/pn-pa-webapp/src/components/NewNotification/__test__/PaymentMethods.test.tsx b/packages/pn-pa-webapp/src/components/NewNotification/__test__/PaymentMethods.test.tsx index 79bb12b6d3..001f12039c 100644 --- a/packages/pn-pa-webapp/src/components/NewNotification/__test__/PaymentMethods.test.tsx +++ b/packages/pn-pa-webapp/src/components/NewNotification/__test__/PaymentMethods.test.tsx @@ -1,157 +1,149 @@ -import * as redux from 'react-redux'; -import { Mock, vi } from 'vitest'; +// import axios from 'axios'; +// import MockAdapter from 'axios-mock-adapter'; +// import { vi } from 'vitest'; -import { newNotification } from '../../../__mocks__/NewNotification.mock'; -import { - RenderResult, - act, - fireEvent, - render, - waitFor, - within, -} from '../../../__test__/test-utils'; -import { PaymentObject } from '../../../models/NewNotification'; -import * as actions from '../../../redux/newNotification/actions'; -import PaymentMethods from '../PaymentMethods'; +// import { newNotification } from '../../../__mocks__/NewNotification.mock'; +// import { RenderResult, act, fireEvent, render, within } from '../../../__test__/test-utils'; +// import PaymentMethods from '../PaymentMethods'; -const file = new File(['mocked content'], 'Mocked file', { type: 'application/pdf' }); +// // mock imports +// vi.mock('react-i18next', () => ({ +// // this mock makes sure any components using the translate hook can use it without a warning being shown +// useTranslation: () => ({ +// t: (str: string) => str, +// }), +// })); -function uploadDocument(elem: HTMLElement) { - const fileInput = within(elem).getByTestId('fileInput'); - const input = fileInput?.querySelector('input'); - fireEvent.change(input!, { target: { files: [file] } }); -} +// const file = new File(['mocked content'], 'Mocked file', { type: 'application/pdf' }); -// Tutto il blocco di test su PaymentMethods è skippato -describe.skip('PaymentMethods Component', () => { - let result: RenderResult; - let mockDispatchFn: Mock; - let mockActionFn: Mock; - const confirmHandlerMk = vi.fn(); +// function uploadDocument(elem: HTMLElement) { +// const fileInput = within(elem).getByTestId('fileInput'); +// const input = fileInput?.querySelector('input'); +// fireEvent.change(input!, { target: { files: [file] } }); +// } - beforeEach(async () => { - // mock action - mockActionFn = vi.fn(); - const actionSpy = vi.spyOn(actions, 'uploadNotificationPaymentDocument'); - actionSpy.mockImplementation(mockActionFn); - // mock dispatch - mockDispatchFn = vi.fn(() => ({ - unwrap: () => Promise.resolve(), - })); - const useDispatchSpy = vi.spyOn(redux, 'useDispatch'); - useDispatchSpy.mockReturnValue(mockDispatchFn as any); - // render component - await act(async () => { - const notification = { ...newNotification, payment: undefined }; - result = render( - - ); - }); - }); +// describe('PaymentMethods Component', () => { +// let result: RenderResult; +// const confirmHandlerMk = vi.fn(); - afterEach(() => { - vi.clearAllMocks(); - }); +// beforeEach(async () => { +// const mock = new MockAdapter(axios); +// mock.onPost('https://mocked-url.com').reply(200, { success: true }); - it('renders PaymentMethods', () => { - expect(result.container).toHaveTextContent( - `${newNotification.recipients[0].firstName} ${newNotification.recipients[0].lastName}` - ); - expect(result.container).toHaveTextContent( - `${newNotification.recipients[1].firstName} ${newNotification.recipients[1].lastName}` - ); - const paymentBoxes = result.queryAllByTestId('paymentBox'); - expect(paymentBoxes).toHaveLength(4); - paymentBoxes.forEach((paymentBox, index) => { - expect(paymentBox).toHaveTextContent( - index % 2 === 0 ? /attach-pagopa-notice*/i : /attach-f24/i - ); - const fileInput = paymentBox.parentNode?.querySelector('[data-testid="fileInput"]'); - expect(fileInput).toBeInTheDocument(); - }); - const buttonSubmit = result.getByTestId('step-submit'); - const buttonPrevious = result.getByTestId('previous-step'); - expect(buttonSubmit).toBeInTheDocument(); - expect(buttonPrevious).toBeInTheDocument(); - // Avendo cambiato posizione nella lista dei bottoni (in modo da avere sempre il bottone "continua" a dx, qui vado a prendere il primo bottone) - // vedi flexDirection row-reverse - // PN-1843 Carlotta Dimatteo 12/08/2022 - expect(buttonPrevious).toHaveTextContent(/back-to-attachments/i); - }); +// // render component +// await act(async () => { +// const notification = { ...newNotification, payment: undefined }; +// result = render( +// +// ); +// }); +// }); - it('adds first and second pagoPa documents (confirm disabled)', async () => { - // const form = result.container.querySelector('form'); - const paymentBoxes = result.queryAllByTestId('paymentBox'); - uploadDocument(paymentBoxes[0].parentElement!); - uploadDocument(paymentBoxes[2].parentElement!); - // const buttons = await waitFor(() => form?.querySelectorAll('button')); - // Avendo cambiato posizione nella lista dei bottoni (in modo da avere sempre il bottone "continua" a dx, qui vado a prendere il primo bottone) - // vedi flexDirection row-reverse - // PN-1843 Carlotta Dimatteo 12/08/2022 - }); +// afterEach(() => { +// vi.clearAllMocks(); +// }); - it('adds all payment documents and clicks on confirm', async () => { - const form = result.container.querySelector('form'); - const paymentBoxes = result.queryAllByTestId('paymentBox'); - uploadDocument(paymentBoxes[0].parentElement!); - uploadDocument(paymentBoxes[1].parentElement!); - uploadDocument(paymentBoxes[2].parentElement!); - uploadDocument(paymentBoxes[3].parentElement!); - const buttons = await waitFor(() => form?.querySelectorAll('button')); - // Avendo cambiato posizione nella lista dei bottoni (in modo da avere sempre il bottone "continua" a dx, qui vado a prendere il primo bottone) - // vedi flexDirection row-reverse - // PN-1843 Carlotta Dimatteo 12/08/2022 - expect(buttons![0]).toBeEnabled(); - fireEvent.click(buttons![0]); - await waitFor(() => { - expect(mockDispatchFn).toHaveBeenCalledTimes(1); - expect(mockActionFn).toHaveBeenCalledTimes(1); - expect(mockActionFn).toHaveBeenCalledWith( - newNotification.recipients.reduce((obj: { [key: string]: PaymentObject }, r, index) => { - obj[r.taxId] = { - pagoPaForm: { - id: index === 0 ? 'MRARSS90P08H501Q-pagoPaDoc' : 'SRAGLL00P48H501U-pagoPaDoc', - idx: 0, - name: 'pagopa-notice', - file: { - sha256: { - hashBase64: 'mocked-hasBase64', - hashHex: 'mocked-hashHex', - }, - data: file, - }, - contentType: 'application/pdf', - ref: { - key: '', - versionToken: '', - }, - }, - f24standard: { - id: - index === 0 ? 'MRARSS90P08H501Q-f24standardDoc' : 'SRAGLL00P48H501U-f24standardDoc', - idx: 0, - name: 'pagopa-notice-f24', - file: { - sha256: { - hashBase64: 'mocked-hasBase64', - hashHex: 'mocked-hashHex', - }, - data: file, - }, - contentType: 'application/pdf', - ref: { - key: '', - versionToken: '', - }, - }, - }; - return obj; - }, {}) - ); - }); - }); +// it('renders PaymentMethods', () => { +// expect(result.container).toHaveTextContent( +// `${newNotification.recipients[0].firstName} ${newNotification.recipients[0].lastName}` +// ); +// expect(result.container).toHaveTextContent( +// `${newNotification.recipients[1].firstName} ${newNotification.recipients[1].lastName}` +// ); +// const paymentBoxes = result.queryAllByTestId('paymentBox'); +// expect(paymentBoxes).toHaveLength(4); + +// const paymentForRecipient = result.queryAllByTestId('paymentForRecipient'); +// const firstPayment = paymentForRecipient[0]; +// expect(within(firstPayment).queryAllByTestId('removeDocument')).toHaveLength(1); +// expect(within(firstPayment).queryAllByTestId('fileInput')).toHaveLength(1); + +// const secondPayment = paymentForRecipient[1]; +// expect(within(secondPayment).queryAllByTestId('removeDocument')).toHaveLength(2); + +// const buttonSubmit = result.getByTestId('step-submit'); +// const buttonPrevious = result.getByTestId('previous-step'); +// expect(buttonSubmit).toBeInTheDocument(); +// expect(buttonPrevious).toBeInTheDocument(); +// expect(buttonPrevious).toHaveTextContent(/back-to-attachments/i); +// }); + +// it.skip('adds first and second pagoPa documents (confirm disabled)', async () => { +// // const form = result.container.querySelector('form'); +// const paymentBoxes = result.queryAllByTestId('paymentBox'); +// uploadDocument(paymentBoxes[0].parentElement!); +// uploadDocument(paymentBoxes[2].parentElement!); +// // const buttons = await waitFor(() => form?.querySelectorAll('button')); +// // Avendo cambiato posizione nella lista dei bottoni (in modo da avere sempre il bottone "continua" a dx, qui vado a prendere il primo bottone) +// // vedi flexDirection row-reverse +// // PN-1843 Carlotta Dimatteo 12/08/2022 +// }); + +// // it.skip('adds all payment documents and clicks on confirm', async () => { +// // const form = result.container.querySelector('form'); +// // const paymentBoxes = result.queryAllByTestId('paymentBox'); +// // uploadDocument(paymentBoxes[0].parentElement!); +// // uploadDocument(paymentBoxes[1].parentElement!); +// // uploadDocument(paymentBoxes[2].parentElement!); +// // uploadDocument(paymentBoxes[3].parentElement!); +// // const buttons = await waitFor(() => form?.querySelectorAll('button')); +// // // Avendo cambiato posizione nella lista dei bottoni (in modo da avere sempre il bottone "continua" a dx, qui vado a prendere il primo bottone) +// // // vedi flexDirection row-reverse +// // // PN-1843 Carlotta Dimatteo 12/08/2022 +// // expect(buttons![0]).toBeEnabled(); +// // fireEvent.click(buttons![0]); +// // await waitFor(() => { +// // expect(mockActionFn).toHaveBeenCalledTimes(1); +// // expect(mockActionFn).toHaveBeenCalledWith( +// // newNotification.recipients.reduce((obj: { [key: string]: PaymentObject }, r, index) => { +// // obj[r.taxId] = { +// // pagoPa: { +// // id: index === 0 ? 'MRARSS90P08H501Q-pagoPaDoc' : 'SRAGLL00P48H501U-pagoPaDoc', +// // idx: 0, +// // name: 'pagopa-notice', +// // file: { +// // sha256: { +// // hashBase64: 'mocked-hasBase64', +// // hashHex: 'mocked-hashHex', +// // }, +// // data: file, +// // }, +// // contentType: 'application/pdf', +// // ref: { +// // key: '', +// // versionToken: '', +// // }, +// // }, +// // f24: { +// // id: index === 0 ? 'MRARSS90P08H501Q-f24' : 'SRAGLL00P48H501U-f24', +// // idx: 0, +// // name: 'pagopa-notice-f24', +// // file: { +// // sha256: { +// // hashBase64: 'mocked-hasBase64', +// // hashHex: 'mocked-hashHex', +// // }, +// // data: file, +// // }, +// // contentType: 'application/json', +// // ref: { +// // key: '', +// // versionToken: '', +// // }, +// // }, +// // }; +// // return obj; +// // }, {}) +// // ); +// // }); +// // }); +// }); + +// TODO fix this +describe('Payment methods', () => { + it.skip('test', () => {}); }); diff --git a/packages/pn-pa-webapp/src/components/NewNotification/__test__/PreliminaryInformations.test.tsx b/packages/pn-pa-webapp/src/components/NewNotification/__test__/PreliminaryInformations.test.tsx index 4cb3cecee5..9c47847385 100644 --- a/packages/pn-pa-webapp/src/components/NewNotification/__test__/PreliminaryInformations.test.tsx +++ b/packages/pn-pa-webapp/src/components/NewNotification/__test__/PreliminaryInformations.test.tsx @@ -31,7 +31,7 @@ import { within, } from '../../../__test__/test-utils'; import { apiClient } from '../../../api/apiClients'; -import { NotificationFeePolicy, PaymentModel } from '../../../models/NewNotification'; +import { NotificationFeePolicy } from '../../../models/NewNotification'; import { NEW_NOTIFICATION_ACTIONS } from '../../../redux/newNotification/actions'; import PreliminaryInformations from '../PreliminaryInformations'; @@ -49,7 +49,6 @@ vi.mock('../../../services/configuration.service', async () => { const populateForm = async ( form: HTMLFormElement, - hasPayment: boolean, organizationName: string = userResponse.organization.name ) => { await testInput(form, 'paProtocolNumber', newNotification.paProtocolNumber); @@ -69,19 +68,9 @@ const populateForm = async ( 1, true ); - - if (hasPayment) { - await testRadio( - form, - 'paymentMethodRadio', - ['pagopa-notice', 'pagopa-notice-f24-flatrate', 'pagopa-notice-f24', 'nothing'], - 1, - true - ); - } }; -describe('PreliminaryInformations component with payment enabled', async () => { +describe('PreliminaryInformations Component', async () => { let result: RenderResult; const confirmHandlerMk = vi.fn(); let mock: MockAdapter; @@ -90,10 +79,6 @@ describe('PreliminaryInformations component with payment enabled', async () => { mock = new MockAdapter(apiClient); }); - beforeEach(() => { - mockIsPaymentEnabledGetter.mockReturnValue(true); - }); - afterEach(() => { mock.reset(); vi.clearAllMocks(); @@ -133,12 +118,6 @@ describe('PreliminaryInformations component with payment enabled', async () => { testFormElements(form, 'taxonomyCode', 'taxonomy-id*'); testFormElements(form, 'senderDenomination', 'sender-name*'); testRadio(form, 'comunicationTypeRadio', ['registered-letter-890', 'simple-registered-letter']); - testRadio(form, 'paymentMethodRadio', [ - 'pagopa-notice', - 'pagopa-notice-f24-flatrate', - 'pagopa-notice-f24', - 'nothing', - ]); const button = within(form).getByTestId('step-submit'); expect(button).toBeDisabled(); }); @@ -181,7 +160,7 @@ describe('PreliminaryInformations component with payment enabled', async () => { preloadedState: { userState: { user: { - organization: { name: 'Comune di Palermo', hasGroup: true }, + organization: { name: 'Comune di Palermo', fiscal_code: '00000', hasGroup: true }, }, }, }, @@ -191,8 +170,9 @@ describe('PreliminaryInformations component with payment enabled', async () => { const form = result.getByTestId('preliminaryInformationsForm') as HTMLFormElement; const button = within(form).getByTestId('step-submit'); expect(button).toBeDisabled(); - await populateForm(form, true); + await populateForm(form); expect(button).toBeEnabled(); + fireEvent.click(button); await waitFor(() => { const state = testStore.getState(); @@ -203,16 +183,15 @@ describe('PreliminaryInformations component with payment enabled', async () => { taxonomyCode: newNotification.taxonomyCode, group: newNotificationGroups[1].id, notificationFeePolicy: NotificationFeePolicy.FLAT_RATE, - payment: {}, documents: [], recipients: [], physicalCommunicationType: PhysicalCommunicationType.AR_REGISTERED_LETTER, - paymentMode: PaymentModel.PAGO_PA_NOTICE_F24_FLATRATE, senderDenomination: newNotification.senderDenomination, lang: 'it', additionalAbstract: '', additionalLang: '', additionalSubject: '', + senderTaxId: '', }); }); expect(confirmHandlerMk).toHaveBeenCalledTimes(1); @@ -238,7 +217,7 @@ describe('PreliminaryInformations component with payment enabled', async () => { ); }); const form = result.getByTestId('preliminaryInformationsForm') as HTMLFormElement; - await populateForm(form, true); + await populateForm(form); // set invalid values // paProtocolNumber await testInput(form, 'paProtocolNumber', ''); @@ -285,169 +264,6 @@ describe('PreliminaryInformations component with payment enabled', async () => { expect(button).toBeDisabled(); }); - it('form initially filled', async () => { - mock.onGet('/bff/v1/pa/groups?status=ACTIVE').reply(200, newNotificationGroups); - await act(async () => { - result = render( - , - { - preloadedState: { - userState: { - user: { - organization: { name: 'Comune di Palermo', hasGroup: true }, - }, - }, - }, - } - ); - }); - const form = result.getByTestId('preliminaryInformationsForm') as HTMLFormElement; - testFormElements( - form, - 'paProtocolNumber', - 'protocol-number*', - newNotification.paProtocolNumber - ); - testFormElements(form, 'subject', 'subject*', newNotification.subject); - testFormElements(form, 'abstract', 'abstract', newNotification.abstract); - testFormElements(form, 'group', 'group', newNotification.group); - testFormElements(form, 'taxonomyCode', 'taxonomy-id*', newNotification.taxonomyCode); - testFormElements(form, 'senderDenomination', 'sender-name*', userResponse.organization.name); - const physicalCommunicationType = form.querySelector( - `input[name="physicalCommunicationType"][value="${newNotification.physicalCommunicationType}"]` - ); - expect(physicalCommunicationType).toBeChecked(); - const paymentMode = form.querySelector( - `input[name="paymentMode"][value="${newNotification.paymentMode}"]` - ); - expect(paymentMode).toBeChecked(); - }); - - it('errors on api call', async () => { - mock.onGet('/bff/v1/pa/groups?status=ACTIVE').reply(errorMock.status, errorMock.data); - await act(async () => { - result = render( - <> - - - - , - { - preloadedState: { - userState: { - user: { - organization: { name: 'Comune di Palermo', hasGroup: true }, - }, - }, - }, - } - ); - }); - const statusApiErrorComponent = result.queryByTestId( - `api-error-${NEW_NOTIFICATION_ACTIONS.GET_USER_GROUPS}` - ); - expect(statusApiErrorComponent).toBeInTheDocument(); - }); -}); - -describe('PreliminaryInformations Component with payment disabled', async () => { - let result: RenderResult; - const confirmHandlerMk = vi.fn(); - let mock: MockAdapter; - - beforeAll(() => { - mock = new MockAdapter(apiClient); - }); - - beforeEach(() => { - mockIsPaymentEnabledGetter.mockReturnValue(false); - }); - - afterEach(() => { - mock.reset(); - vi.clearAllMocks(); - }); - - afterAll(() => { - mock.restore(); - }); - - it('renders component', async () => { - mock.onGet('/bff/v1/pa/groups?status=ACTIVE').reply(200, newNotificationGroups); - await act(async () => { - result = render( - , - { - preloadedState: { - userState: { - user: { - organization: { name: 'Comune di Palermo', hasGroup: true }, - }, - }, - }, - } - ); - }); - expect(result.container).toHaveTextContent(/title/i); - const form = result.getByTestId('preliminaryInformationsForm') as HTMLFormElement; - const paymentMethodRadio = within(form).queryAllByTestId('paymentMethodRadio'); - expect(paymentMethodRadio).toHaveLength(0); - const button = within(form).getByTestId('step-submit'); - expect(button).toBeDisabled(); - }); - - it('changes form values and clicks on confirm', async () => { - mock.onGet('/bff/v1/pa/groups?status=ACTIVE').reply(200, newNotificationGroups); - await act(async () => { - result = render( - , - { - preloadedState: { - userState: { - user: { - organization: { name: 'Comune di Palermo', hasGroup: true }, - }, - }, - }, - } - ); - }); - const form = result.getByTestId('preliminaryInformationsForm') as HTMLFormElement; - const button = within(form).getByTestId('step-submit'); - expect(button).toBeDisabled(); - await populateForm(form, false); - expect(button).toBeEnabled(); - fireEvent.click(button); - await waitFor(() => { - const state = testStore.getState(); - expect(state.newNotificationState.notification).toEqual({ - paProtocolNumber: newNotification.paProtocolNumber, - abstract: '', - subject: newNotification.subject, - taxonomyCode: newNotification.taxonomyCode, - group: newNotificationGroups[1].id, - notificationFeePolicy: NotificationFeePolicy.FLAT_RATE, - payment: {}, - documents: [], - recipients: [], - physicalCommunicationType: PhysicalCommunicationType.AR_REGISTERED_LETTER, - paymentMode: PaymentModel.NOTHING, - senderDenomination: newNotification.senderDenomination, - lang: 'it', - additionalAbstract: '', - additionalLang: '', - additionalSubject: '', - }); - }); - expect(confirmHandlerMk).toHaveBeenCalledTimes(1); - }); - it('set senderDenomination longer than 80 characters', async () => { mock.onGet('/bff/v1/pa/groups?status=ACTIVE').reply(200, newNotificationGroups); await act(async () => { @@ -539,4 +355,64 @@ describe('PreliminaryInformations Component with payment disabled', async () => ); testFormElements(form, 'additionalLang', 'select-other-language*', 'de'); }); + + it('form initially filled', async () => { + mock.onGet('/bff/v1/pa/groups?status=ACTIVE').reply(200, newNotificationGroups); + await act(async () => { + result = render( + , + { + preloadedState: { + userState: { + user: { + organization: { name: 'Comune di Palermo', hasGroup: true }, + }, + }, + }, + } + ); + }); + const form = result.getByTestId('preliminaryInformationsForm') as HTMLFormElement; + testFormElements( + form, + 'paProtocolNumber', + 'protocol-number*', + newNotification.paProtocolNumber + ); + testFormElements(form, 'subject', 'subject*', newNotification.subject); + testFormElements(form, 'abstract', 'abstract', newNotification.abstract); + testFormElements(form, 'group', 'group', newNotification.group); + testFormElements(form, 'taxonomyCode', 'taxonomy-id*', newNotification.taxonomyCode); + testFormElements(form, 'senderDenomination', 'sender-name*', userResponse.organization.name); + const physicalCommunicationType = form.querySelector( + `input[name="physicalCommunicationType"][value="${newNotification.physicalCommunicationType}"]` + ); + expect(physicalCommunicationType).toBeChecked(); + }); + + it('errors on api call', async () => { + mock.onGet('/bff/v1/pa/groups?status=ACTIVE').reply(errorMock.status, errorMock.data); + await act(async () => { + result = render( + <> + + + + , + { + preloadedState: { + userState: { + user: { + organization: { name: 'Comune di Palermo', hasGroup: true }, + }, + }, + }, + } + ); + }); + const statusApiErrorComponent = result.queryByTestId( + `api-error-${NEW_NOTIFICATION_ACTIONS.GET_USER_GROUPS}` + ); + expect(statusApiErrorComponent).toBeInTheDocument(); + }); }); diff --git a/packages/pn-pa-webapp/src/components/NewNotification/__test__/PreliminaryInformationsContent.test.tsx b/packages/pn-pa-webapp/src/components/NewNotification/__test__/PreliminaryInformationsContent.test.tsx index 8cbf9baedd..7594ddc4c7 100644 --- a/packages/pn-pa-webapp/src/components/NewNotification/__test__/PreliminaryInformationsContent.test.tsx +++ b/packages/pn-pa-webapp/src/components/NewNotification/__test__/PreliminaryInformationsContent.test.tsx @@ -3,8 +3,7 @@ import { Formik } from 'formik'; import { PhysicalCommunicationType } from '@pagopa-pn/pn-commons'; import { fireEvent, getById, render, testFormElements } from '@pagopa-pn/pn-commons/src/test-utils'; -import { NewNotificationLangOther, PaymentModel } from '../../../models/NewNotification'; -import { PreliminaryInformationsPayload } from '../../../redux/newNotification/types'; +import { NewNotificationLangOther, PaymentModel, PreliminaryInformationsPayload } from '../../../models/NewNotification'; import PreliminaryInformationsContent from '../PreliminaryInformationsContent'; describe('PreliminaryInformationsContent', () => { diff --git a/packages/pn-pa-webapp/src/components/NewNotification/__test__/PreliminaryInformationsLang.test.tsx b/packages/pn-pa-webapp/src/components/NewNotification/__test__/PreliminaryInformationsLang.test.tsx index 6150d77f59..a10c3d9be6 100644 --- a/packages/pn-pa-webapp/src/components/NewNotification/__test__/PreliminaryInformationsLang.test.tsx +++ b/packages/pn-pa-webapp/src/components/NewNotification/__test__/PreliminaryInformationsLang.test.tsx @@ -13,8 +13,7 @@ import { import { LangLabels } from '@pagopa/mui-italia'; import userEvent from '@testing-library/user-event'; -import { PaymentModel } from '../../../models/NewNotification'; -import { PreliminaryInformationsPayload } from '../../../redux/newNotification/types'; +import { PaymentModel, PreliminaryInformationsPayload } from '../../../models/NewNotification'; import PreliminaryInformationsLang from '../PreliminaryInformationsLang'; describe('PreliminaryInformationsLang', () => { diff --git a/packages/pn-pa-webapp/src/components/NewNotification/__test__/Recipient.test.tsx b/packages/pn-pa-webapp/src/components/NewNotification/__test__/Recipient.test.tsx index 75526f83fe..26f129378a 100644 --- a/packages/pn-pa-webapp/src/components/NewNotification/__test__/Recipient.test.tsx +++ b/packages/pn-pa-webapp/src/components/NewNotification/__test__/Recipient.test.tsx @@ -14,13 +14,12 @@ import { waitFor, within, } from '../../../__test__/test-utils'; -import { NewNotificationRecipient, PaymentModel } from '../../../models/NewNotification'; +import { NewNotificationRecipient } from '../../../models/NewNotification'; import Recipient from '../Recipient'; const testRecipientFormRendering = async ( form: HTMLElement, recipientIndex: number, - hasPayment: boolean, recipient?: NewNotificationRecipient ) => { await testRadio( @@ -52,20 +51,7 @@ const testRecipientFormRendering = async ( : 'recipient-citizen-tax-id*', recipient ? recipient.taxId : undefined ); - if (hasPayment) { - testFormElements( - form, - `recipients[${recipientIndex}].creditorTaxId`, - 'creditor-fiscal-code*', - recipient ? recipient.creditorTaxId : undefined - ); - testFormElements( - form, - `recipients[${recipientIndex}].noticeCode`, - 'notice-code*', - recipient ? recipient.noticeCode : undefined - ); - } + // check that recipientType is initially selected const recipientType = form.querySelector( `input[name="recipients[${recipientIndex}].recipientType"][value="${ @@ -113,7 +99,6 @@ const testRecipientFormRendering = async ( const populateForm = async ( form: HTMLFormElement, recipientIndex: number, - hasPayment: boolean, recipient: NewNotificationRecipient ) => { // if pg select the right radio button @@ -131,10 +116,7 @@ const populateForm = async ( await testInput(form, `recipients[${recipientIndex}].lastName`, recipient.lastName); } await testInput(form, `recipients[${recipientIndex}].taxId`, recipient.taxId); - if (hasPayment) { - await testInput(form, `recipients[${recipientIndex}].creditorTaxId`, recipient.creditorTaxId); - await testInput(form, `recipients[${recipientIndex}].noticeCode`, recipient.noticeCode); - } + // show physical address form await testInput(form, `recipients[${recipientIndex}].address`, recipient.address); await testInput(form, `recipients[${recipientIndex}].houseNumber`, recipient.houseNumber); @@ -168,6 +150,10 @@ const testStringFieldValidation = async ( return error!; }; +const recipientsWithoutPayments = newNotification.recipients.map( + ({ payments, debtPosition, ...recipient }) => recipient +); + describe('Recipient Component with payment enabled', async () => { const confirmHandlerMk = vi.fn(); let result: RenderResult; @@ -179,13 +165,11 @@ describe('Recipient Component with payment enabled', async () => { it('renders component', async () => { // render component await act(async () => { - result = render( - - ); + result = render(); }); expect(result.container).toHaveTextContent(/title/i); const form = result.getByTestId('recipientForm'); - await testRecipientFormRendering(form, 0, true); + await testRecipientFormRendering(form, 0); const addButton = within(form).getByTestId('add-recipient'); expect(addButton).toBeInTheDocument(); const button = within(form).getByTestId('step-submit'); @@ -197,13 +181,11 @@ describe('Recipient Component with payment enabled', async () => { it('changes form values and clicks on confirm - two recipients', async () => { // render component await act(async () => { - result = render( - - ); + result = render(); }); const form = result.getByTestId('recipientForm') as HTMLFormElement; // fill the first recipient - await populateForm(form, 0, true, newNotification.recipients[0]); + await populateForm(form, 0, newNotification.recipients[0]); const submitButton = within(form).getByTestId('step-submit'); expect(submitButton).toBeEnabled(); // add new recipient @@ -212,15 +194,15 @@ describe('Recipient Component with payment enabled', async () => { await waitFor(() => { expect(submitButton).toBeDisabled(); }); - await testRecipientFormRendering(form, 1, true); + await testRecipientFormRendering(form, 1); // fill the second recipient - await populateForm(form, 1, true, newNotification.recipients[1]); + await populateForm(form, 1, newNotification.recipients[1]); expect(submitButton).toBeEnabled(); fireEvent.click(submitButton); await waitFor(() => { const state = testStore.getState(); expect(state.newNotificationState.notification.recipients).toStrictEqual( - newNotification.recipients + recipientsWithoutPayments ); }); expect(confirmHandlerMk).toHaveBeenCalledTimes(1); @@ -229,19 +211,17 @@ describe('Recipient Component with payment enabled', async () => { it('fills form with invalid values - two recipients', async () => { // render component await act(async () => { - result = render( - - ); + result = render(); }); const form = result.getByTestId('recipientForm') as HTMLFormElement; // fill the first recipient - await populateForm(form, 0, true, newNotification.recipients[0]); + await populateForm(form, 0, newNotification.recipients[0]); const submitButton = within(form).getByTestId('step-submit'); // add new recipient const addButton = within(form).getByTestId('add-recipient'); fireEvent.click(addButton); // fill the second recipient - await populateForm(form, 1, true, newNotification.recipients[1]); + await populateForm(form, 1, newNotification.recipients[1]); expect(submitButton).toBeEnabled(); // set invalid values // compared to PF case, only firstName and taxId validations change @@ -268,21 +248,6 @@ describe('Recipient Component with payment enabled', async () => { await testInput(form, 'recipients[0].taxId', newNotification.recipients[0].taxId, true); await testInput(form, 'recipients[1].taxId', newNotification.recipients[0].taxId, true); expect(taxIdError).toHaveTextContent('identical-fiscal-codes-error'); - // identical creditorTaxId and noticeCode - await testInput( - form, - 'recipients[0].creditorTaxId', - newNotification.recipients[0].creditorTaxId - ); - await testInput( - form, - 'recipients[1].creditorTaxId', - newNotification.recipients[0].creditorTaxId - ); - await testInput(form, 'recipients[0].noticeCode', newNotification.recipients[0].noticeCode); - await testInput(form, 'recipients[1].noticeCode', newNotification.recipients[0].noticeCode); - const noticeCodeError = form.querySelector('[id="recipients[1].noticeCode-helper-text"]'); - expect(noticeCodeError).toHaveTextContent('identical-notice-codes-error'); // remove second recipient and check that the form returns valid const deleteIcon = result.queryAllByTestId('DeleteRecipientIcon'); fireEvent.click(deleteIcon[1]); @@ -293,16 +258,12 @@ describe('Recipient Component with payment enabled', async () => { // render component await act(async () => { result = render( - + ); }); const form = result.getByTestId('recipientForm') as HTMLFormElement; - await testRecipientFormRendering(form, 0, true, newNotification.recipients[0]); - await testRecipientFormRendering(form, 1, true, newNotification.recipients[1]); + await testRecipientFormRendering(form, 0, newNotification.recipients[0]); + await testRecipientFormRendering(form, 1, newNotification.recipients[1]); const submitButton = within(form).getByTestId('step-submit'); expect(submitButton).toBeEnabled(); }, 10000); @@ -310,13 +271,11 @@ describe('Recipient Component with payment enabled', async () => { it('changes form values and clicks on confirm - one recipient', async () => { // render component await act(async () => { - result = render( - - ); + result = render(); }); const form = result.getByTestId('recipientForm') as HTMLFormElement; // fill the first recipient - await populateForm(form, 0, true, newNotification.recipients[0]); + await populateForm(form, 0, newNotification.recipients[0]); const submitButton = within(form).getByTestId('step-submit'); expect(submitButton).toBeEnabled(); // add new recipient @@ -340,7 +299,7 @@ describe('Recipient Component with payment enabled', async () => { await waitFor(() => { const state = testStore.getState(); expect(state.newNotificationState.notification.recipients).toStrictEqual([ - newNotification.recipients[0], + recipientsWithoutPayments[0], ]); }); expect(confirmHandlerMk).toHaveBeenCalledTimes(1); @@ -351,22 +310,18 @@ describe('Recipient Component with payment enabled', async () => { // render component await act(async () => { result = render( - + ); }); const form = result.getByTestId('recipientForm') as HTMLFormElement; // fill the first recipient - await populateForm(form, 0, true, newNotification.recipients[0]); + await populateForm(form, 0, newNotification.recipients[0]); const backButton = within(form).getByTestId('previous-step'); fireEvent.click(backButton); await waitFor(() => { const state = testStore.getState(); expect(state.newNotificationState.notification.recipients).toStrictEqual([ - newNotification.recipients[0], + recipientsWithoutPayments[0], ]); }); expect(previousHandlerMk).toHaveBeenCalledTimes(1); @@ -375,14 +330,12 @@ describe('Recipient Component with payment enabled', async () => { it('fills form with invalid values - one recipient', async () => { // render component await act(async () => { - result = render( - - ); + result = render(); }); const form = result.getByTestId('recipientForm') as HTMLFormElement; const submitButton = within(form).getByTestId('step-submit'); expect(submitButton).toBeDisabled(); - await populateForm(form, 0, true, newNotification.recipients[0]); + await populateForm(form, 0, newNotification.recipients[0]); expect(submitButton).toBeEnabled(); // set invalid values // firstName @@ -437,19 +390,6 @@ describe('Recipient Component with payment enabled', async () => { await testStringFieldValidation(form, 0, 'province', 257); // foreignState await testStringFieldValidation(form, 0, 'foreignState'); - // creditorTaxId - await testInput(form, 'recipients[0].creditorTaxId', '', true); - const creditorTaxIdError = form.querySelector('[id="recipients[0].creditorTaxId-helper-text"]'); - expect(creditorTaxIdError).toHaveTextContent('required-field'); - await testInput(form, 'recipients[0].creditorTaxId', 'wrong-fiscal-code'); - expect(creditorTaxIdError).toHaveTextContent('fiscal-code-error'); - // noticeCode - await testInput(form, 'recipients[0].noticeCode', '', true); - const noticeCodeError = form.querySelector('[id="recipients[0].noticeCode-helper-text"]'); - expect(noticeCodeError).toHaveTextContent('required-field'); - await testInput(form, 'recipients[0].noticeCode', 'wrong-notice-code'); - expect(noticeCodeError).toHaveTextContent('notice-code-error'); - expect(submitButton).toBeDisabled(); }, 10000); }); @@ -464,31 +404,27 @@ describe('Recipient Component without payment enabled', async () => { it('renders component', async () => { // render component await act(async () => { - result = render( - - ); + result = render(); }); const form = result.getByTestId('recipientForm'); - await testRecipientFormRendering(form, 0, false); + await testRecipientFormRendering(form, 0); }); it('changes form values and clicks on confirm - one recipient', async () => { // render component await act(async () => { - result = render( - - ); + result = render(); }); const form = result.getByTestId('recipientForm') as HTMLFormElement; // fill the first recipient - await populateForm(form, 0, false, newNotification.recipients[0]); + await populateForm(form, 0, newNotification.recipients[0]); const submitButton = within(form).getByTestId('step-submit'); expect(submitButton).toBeEnabled(); fireEvent.click(submitButton); await waitFor(() => { const state = testStore.getState(); expect(state.newNotificationState.notification.recipients).toStrictEqual([ - { ...newNotification.recipients[0], creditorTaxId: '', noticeCode: '' }, + recipientsWithoutPayments[0], ]); }); expect(confirmHandlerMk).toHaveBeenCalledTimes(1); diff --git a/packages/pn-pa-webapp/src/models/NewNotification.ts b/packages/pn-pa-webapp/src/models/NewNotification.ts index 09cf454c3e..4537803052 100644 --- a/packages/pn-pa-webapp/src/models/NewNotification.ts +++ b/packages/pn-pa-webapp/src/models/NewNotification.ts @@ -1,15 +1,11 @@ -import { - DigitalDomicileType, - NotificationDetailDocument, - NotificationDetailRecipient, - PhysicalCommunicationType, - RecipientType, -} from '@pagopa-pn/pn-commons'; +import { PhysicalCommunicationType, RecipientType } from '@pagopa-pn/pn-commons'; + +import { NotificationAttachmentBodyRef } from '../generated-client/notifications'; export enum PaymentModel { - PAGO_PA_NOTICE = 'PAGO_PA_NOTICE', - PAGO_PA_NOTICE_F24_FLATRATE = 'PAGO_PA_NOTICE_F24_FLATRATE', - PAGO_PA_NOTICE_F24 = 'PAGO_PA_NOTICE_F24', + PAGO_PA = 'PAGO_PA', + F24 = 'F24', + PAGO_PA_F24 = 'PAGO_PA_F24', NOTHING = 'NOTHING', } @@ -18,25 +14,57 @@ export enum NotificationFeePolicy { DELIVERY_MODE = 'DELIVERY_MODE', } -interface BaseNewNotification { - notificationFeePolicy: NotificationFeePolicy; - idempotenceToken?: string; - paProtocolNumber: string; - subject: string; - abstract?: string; - cancelledIun?: string; - physicalCommunicationType: PhysicalCommunicationType; - senderDenomination: string; - senderTaxId?: string; - group?: string; - taxonomyCode: string; +// NotificationDigital Domicile Type +export enum NewNotificationDigitalAddressType { + PEC = 'PEC', } -// New Notification DTO -export interface NewNotificationDTO extends BaseNewNotification { - recipients: Array; - documents: Array; - additionalLanguages?: Array; +export enum PagoPaIntegrationMode { + NONE = 'NONE', + SYNC = 'SYNC', + ASYNC = 'ASYNC', +} + +export interface NewNotificationDocumentFile { + data?: File; + sha256: { + hashBase64: string; + hashHex: string; + }; +} + +export interface NewNotificationDocumentRef { + key: string; + versionToken: string; +} + +export interface NewNotificationDocument { + id: string; + idx: number; + contentType: string; + name: string; + file: NewNotificationDocumentFile; + ref: NewNotificationDocumentRef; +} + +export interface NewNotificationPagoPaPayment { + id: string; + idx: number; + contentType: string; + creditorTaxId: string; + noticeCode: string; + applyCost: boolean; + file: NewNotificationDocumentFile; + ref: NewNotificationDocumentRef; +} + +export interface NewNotificationF24Payment extends NewNotificationDocument { + applyCost: boolean; +} + +export interface NewNotificationPayment { + pagoPa?: NewNotificationPagoPaPayment; + f24?: NewNotificationF24Payment; } // New Notification @@ -45,11 +73,9 @@ export interface NewNotificationRecipient { idx: number; recipientType: RecipientType; taxId: string; - creditorTaxId: string; - noticeCode: string; firstName: string; lastName: string; - type: DigitalDomicileType; + type: NewNotificationDigitalAddressType; digitalDomicile: string; address: string; houseNumber: string; @@ -59,31 +85,27 @@ export interface NewNotificationRecipient { municipalityDetails?: string; province: string; foreignState: string; + payments?: Array; + debtPosition?: PaymentModel; } -export interface NewNotificationDocument { - id: string; - idx: number; - name: string; - contentType: string; - file: { - data?: File; - sha256: { - hashBase64: string; - hashHex: string; - }; - }; - ref: { - key: string; - versionToken: string; - }; -} - -export interface NewNotification extends BaseNewNotification, NewNotificationBilingualism { - paymentMode?: PaymentModel; +export interface NewNotification extends NewNotificationBilingualism { + idempotenceToken?: string; + paProtocolNumber: string; + subject: string; + abstract?: string; + cancelledIun?: string; + physicalCommunicationType: PhysicalCommunicationType; + senderDenomination: string; + senderTaxId: string; + group?: string; + taxonomyCode: string; recipients: Array; documents: Array; - payment?: { [key: string]: PaymentObject }; + paFee?: number; + vat?: number; + notificationFeePolicy: NotificationFeePolicy; + pagoPaIntMode?: PagoPaIntegrationMode; } export interface NewNotificationBilingualism { @@ -94,16 +116,56 @@ export interface NewNotificationBilingualism { } export interface PaymentObject { - pagoPaForm: NewNotificationDocument; - f24flatRate?: NewNotificationDocument; - f24standard?: NewNotificationDocument; + pagoPa: NewNotificationDocument; + f24?: NewNotificationDocument; } -export interface NewNotificationResponse { - notificationRequestId: string; +export interface PreliminaryInformationsPayload extends NewNotificationBilingualism { paProtocolNumber: string; - idempotenceToken: string; + subject: string; + abstract?: string; + physicalCommunicationType: PhysicalCommunicationType; + group?: string; + paymentMode: PaymentModel; + taxonomyCode: string; + senderDenomination?: string; +} + +export interface UploadDocumentParams { + id: string; + contentType: string; + file: Uint8Array | undefined; + sha256: string; +} + +export interface UploadPaymentResponse { + [key: string]: { + pagoPaForm: UploadDocumentsResponse; + f24flatRate?: UploadDocumentsResponse; + f24standard?: UploadDocumentsResponse; + }; } +export interface UploadDocumentsResponse { + [id: string]: NotificationAttachmentBodyRef; +} + +export type RecipientPaymentsFormValues = { + [taxId: string]: { + pagoPa: Array; + f24: Array; + }; +}; + +export type PaymentMethodsFormValues = { + notificationFeePolicy: NotificationFeePolicy; + paFee: number | undefined; + vat: number | undefined; + pagoPaIntMode: PagoPaIntegrationMode; + recipients: RecipientPaymentsFormValues; +}; + export const BILINGUALISM_LANGUAGES = ['de', 'sl', 'fr']; -export const NewNotificationLangOther = 'other'; \ No newline at end of file +export const NewNotificationLangOther = 'other'; + +export const VAT = [4, 5, 10, 22]; diff --git a/packages/pn-pa-webapp/src/pages/NewNotification.page.tsx b/packages/pn-pa-webapp/src/pages/NewNotification.page.tsx index 9d47e0e3ee..ba0f293abc 100644 --- a/packages/pn-pa-webapp/src/pages/NewNotification.page.tsx +++ b/packages/pn-pa-webapp/src/pages/NewNotification.page.tsx @@ -1,15 +1,17 @@ import { useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Link } from 'react-router-dom'; -import { Alert, Box, Grid, Step, StepLabel, Stepper, Typography } from '@mui/material'; +import { Alert, Box, Grid, Link, Step, StepLabel, Stepper, Typography } from '@mui/material'; import { PnBreadcrumb, Prompt, TitleBox, useIsMobile } from '@pagopa-pn/pn-commons'; import Attachments from '../components/NewNotification/Attachments'; +import DebtPosition from '../components/NewNotification/DebtPosition'; +import DebtPositionDetail from '../components/NewNotification/DebtPositionDetail'; +// import PaymentMethods from '../components/NewNotification/PaymentMethods'; import PreliminaryInformations from '../components/NewNotification/PreliminaryInformations'; import Recipient from '../components/NewNotification/Recipient'; import SyncFeedback from '../components/NewNotification/SyncFeedback'; -import { NewNotificationLangOther } from '../models/NewNotification'; +import { NewNotificationLangOther, PaymentModel } from '../models/NewNotification'; import * as routes from '../navigation/routes.const'; import { useAppDispatch, useAppSelector } from '../redux/hooks'; import { createNewNotification } from '../redux/newNotification/actions'; @@ -19,10 +21,14 @@ import { getConfiguration } from '../services/configuration.service'; const SubTitle = () => { const { t } = useTranslation(['common', 'notifiche']); + const { DEVELOPER_API_DOCUMENTATION_LINK } = getConfiguration(); return ( <> - {t('new-notification.subtitle', { ns: 'notifiche' })} {/* PN-2028 */} - {t('menu.api-key')} + {t('new-notification.subtitle', { ns: 'notifiche' })} {/* PN-14000 */} + + {t('new-notification.how-it-works', { ns: 'notifiche' })} + + . ); }; @@ -38,18 +44,33 @@ const NewNotification = () => { const { IS_PAYMENT_ENABLED } = useMemo(() => getConfiguration(), []); const dispatch = useAppDispatch(); const { t } = useTranslation(['common', 'notifiche']); - const steps = [ - t('new-notification.steps.preliminary-informations.title', { ns: 'notifiche' }), - t('new-notification.steps.recipient.title', { ns: 'notifiche' }), - t('new-notification.steps.attachments.title', { ns: 'notifiche' }), - ]; - const childRef = useRef<{ confirm: () => void }>(); + const steps = useMemo(() => { + const baseSteps = [ + t('new-notification.steps.preliminary-informations.title', { ns: 'notifiche' }), + t('new-notification.steps.recipient.title', { ns: 'notifiche' }), + ]; + + if (IS_PAYMENT_ENABLED) { + // eslint-disable-next-line functional/immutable-data + baseSteps.push( + t('new-notification.steps.debt-position.title', { ns: 'notifiche' }), + t('new-notification.steps.debt-position-detail.title', { ns: 'notifiche' }) + ); + } - if (IS_PAYMENT_ENABLED) { // eslint-disable-next-line functional/immutable-data - steps.push(t('new-notification.steps.payment-methods.title', { ns: 'notifiche' })); - } + baseSteps.push(t('new-notification.steps.attachments.title', { ns: 'notifiche' })); + return baseSteps; + }, []); + + const hasDebtPosition = + IS_PAYMENT_ENABLED && + notification.recipients.some( + (recipient) => recipient.debtPosition && recipient.debtPosition !== PaymentModel.NOTHING + ); + + const childRef = useRef<{ confirm: () => void }>(); const goToNextStep = () => { setActiveStep((previousStep) => previousStep + 1); @@ -84,6 +105,12 @@ const NewNotification = () => { } }; + const isPaymentMethodStepDisabled = (index: number) => + IS_PAYMENT_ENABLED && index === 3 && !hasDebtPosition; + + const onStepClick = (index: number) => + index < activeStep && !isPaymentMethodStepDisabled(index) ? goToPreviousStep(index) : undefined; + useEffect(() => { createNotification(); }, [isCompleted]); @@ -143,9 +170,10 @@ const NewNotification = () => { (index < activeStep ? goToPreviousStep(index) : undefined)} + onClick={() => onStepClick(index)} sx={{ cursor: index < activeStep ? 'pointer' : 'auto' }} data-testid={`step-${index}`} + disabled={isPaymentMethodStepDisabled(index)} > {label} @@ -160,33 +188,40 @@ const NewNotification = () => { onConfirm={goToNextStep} onPreviousStep={goToPreviousStep} recipientsData={notification.recipients} - paymentMode={notification.paymentMode} ref={childRef} /> )} - {activeStep === 2 && ( + {activeStep === 2 && IS_PAYMENT_ENABLED && ( + setActiveStep(steps.length - 1)} + ref={childRef} + /> + )} + {activeStep === 3 && IS_PAYMENT_ENABLED && ( + + )} + {((IS_PAYMENT_ENABLED && activeStep === 4) || + (!IS_PAYMENT_ENABLED && activeStep === 2)) && ( )} - {/* - activeStep === 3 && ( - - ) - */} diff --git a/packages/pn-pa-webapp/src/pages/__test__/NewNotification.page.test.tsx b/packages/pn-pa-webapp/src/pages/__test__/NewNotification.page.test.tsx index ba9bc47e65..2d5efbbb52 100644 --- a/packages/pn-pa-webapp/src/pages/__test__/NewNotification.page.test.tsx +++ b/packages/pn-pa-webapp/src/pages/__test__/NewNotification.page.test.tsx @@ -6,6 +6,7 @@ import { vi } from 'vitest'; import { AppMessage, AppResponseMessage, + Configuration, ResponseEventDispatcher, errorFactoryManager, } from '@pagopa-pn/pn-commons'; @@ -15,6 +16,7 @@ import { newNotification, newNotificationGroups } from '../../__mocks__/NewNotif import { RenderResult, act, fireEvent, render, waitFor, within } from '../../__test__/test-utils'; import { apiClient } from '../../api/apiClients'; import * as routes from '../../navigation/routes.const'; +import { PaConfiguration } from '../../services/configuration.service'; import { PAAppErrorFactory } from '../../utility/AppError/PAAppErrorFactory'; import { newNotificationMapper } from '../../utility/notification.utility'; import NewNotification from '../NewNotification.page'; @@ -24,12 +26,13 @@ vi.mock('../../services/configuration.service', async () => { return { ...(await vi.importActual('../../services/configuration.service')), getConfiguration: () => ({ + ...Configuration.get(), IS_PAYMENT_ENABLED: mockIsPaymentEnabledGetter(), }), }; }); -describe('NewNotification Page without payment', async () => { +describe('NewNotification Page without payment enabled in configuration', async () => { let result: RenderResult; let mock: MockAdapter; @@ -145,22 +148,12 @@ describe('NewNotification Page without payment', async () => { expect(mockedPageBefore).not.toBeInTheDocument(); // simulate clicking the link - const links = result.getAllByRole('link'); - expect(links[1]).toHaveTextContent(/menu.api-key/i); - expect(links[1]).toHaveAttribute('href', routes.API_KEYS); - - fireEvent.click(links[1]); - // prompt must be shown - const promptDialog = await waitFor(() => result.getByTestId('promptDialog')); - expect(promptDialog).toBeInTheDocument(); - const confirmExitBtn = within(promptDialog).getByTestId('confirmExitBtn'); - fireEvent.click(confirmExitBtn); - - // after clicking link - mocked api keys page present - await waitFor(() => { - const mockedPageAfter = result.queryByTestId('mocked-api-keys-page'); - expect(mockedPageAfter).toBeInTheDocument(); - }); + const link = result.getByTestId('api-how-it-works'); + expect(link).toHaveTextContent(/new-notification.how-it-works/i); + expect(link).toHaveAttribute( + 'href', + Configuration.get().DEVELOPER_API_DOCUMENTATION_LINK + ); }); it('clicks on stepper and navigate', async () => { @@ -215,6 +208,7 @@ describe('NewNotification Page without payment', async () => { it('create new notification', async () => { const mappedNotification = newNotificationMapper(newNotification); + const mockResponse = { notificationRequestId: 'mocked-notificationRequestId', paProtocolNumber: 'mocked-paProtocolNumber', @@ -252,7 +246,9 @@ describe('NewNotification Page without payment', async () => { buttonSubmit = result.getByTestId('step-submit'); const attachmentsForm = result.getByTestId('attachmentsForm'); expect(attachmentsForm).toBeInTheDocument(); + // FINAL + expect(buttonSubmit).toBeEnabled(); fireEvent.click(buttonSubmit); await waitFor(() => { expect(mock.history.post).toHaveLength(1); @@ -336,4 +332,112 @@ describe('NewNotification Page without payment', async () => { }); // TODO: to be enriched when payment is enabled again -describe.skip('NewNotification Page with payment', () => {}); +describe.skip('NewNotification Page with payment enabled in configuration', () => { + let result: RenderResult; + let mock: MockAdapter; + + beforeAll(() => { + mock = new MockAdapter(apiClient); + }); + + beforeEach(() => { + mockIsPaymentEnabledGetter.mockReturnValue(true); + mock.onGet('/bff/v1/pa/groups?status=ACTIVE').reply(200, newNotificationGroups); + }); + + afterEach(() => { + mock.reset(); + vi.clearAllMocks(); + }); + + afterAll(() => { + mock.restore(); + }); + + it('renders page', async () => { + // render component + await act(async () => { + result = render(, { + preloadedState: { + userState: { user: userResponse }, + }, + }); + }); + expect(result.getByTestId('titleBox')).toHaveTextContent('new-notification.title'); + const stepper = result.getByTestId('stepper'); + expect(stepper).toBeInTheDocument(); + const preliminaryInformation = result.getByTestId('preliminaryInformationsForm'); + expect(preliminaryInformation).toBeInTheDocument(); + const recipientForm = result.queryByTestId('recipientForm'); + expect(recipientForm).not.toBeInTheDocument(); + const paymentMethodForm = result.queryByTestId('paymentMethodForm'); + expect(paymentMethodForm).not.toBeInTheDocument(); + const attachmentsForm = result.queryByTestId('attachmentsForm'); + expect(attachmentsForm).not.toBeInTheDocument(); + const finalStep = result.queryByTestId('finalStep'); + expect(finalStep).not.toBeInTheDocument(); + const alert = result.queryByTestId('alert'); + expect(alert).toBeInTheDocument(); + }); + + it('create new notification', async () => { + const mappedNotification = newNotificationMapper(newNotification); + + const mockResponse = { + notificationRequestId: 'mocked-notificationRequestId', + paProtocolNumber: 'mocked-paProtocolNumber', + idempotenceToken: 'mocked-idempotenceToken', + }; + mock.onPost('/bff/v1/notifications/sent', mappedNotification).reply(200, mockResponse); + // render component + // because all the step are already deeply tested, we can set the new notification already populated + await act(async () => { + result = render(, { + preloadedState: { + newNotificationState: { notification: newNotification, groups: [] }, + userState: { user: userResponse }, + }, + }); + }); + // STEP 1 + let buttonSubmit = await waitFor(() => result.getByTestId('step-submit')); + expect(buttonSubmit).toBeEnabled(); + const preliminaryInformation = result.getByTestId('preliminaryInformationsForm'); + expect(preliminaryInformation).toBeInTheDocument(); + fireEvent.click(buttonSubmit); + + // STEP 2 + await waitFor(() => { + expect(preliminaryInformation).not.toBeInTheDocument(); + }); + buttonSubmit = result.getByTestId('step-submit'); + const recipientForm = result.getByTestId('recipientForm'); + expect(recipientForm).toBeInTheDocument(); + fireEvent.click(buttonSubmit); + + // STEP 3 + await waitFor(() => { + expect(recipientForm).not.toBeInTheDocument(); + }); + buttonSubmit = result.getByTestId('step-submit'); + const paymentMethodForm = result.getByTestId('paymentMethodForm'); + expect(paymentMethodForm).toBeInTheDocument(); + + // STEP 4 + await waitFor(() => { + expect(paymentMethodForm).not.toBeInTheDocument(); + }); + buttonSubmit = result.getByTestId('step-submit'); + const attachmentsForm = result.getByTestId('attachmentsForm'); + expect(attachmentsForm).toBeInTheDocument(); + + // FINAL + expect(buttonSubmit).toBeEnabled(); + fireEvent.click(buttonSubmit); + await waitFor(() => { + expect(mock.history.post).toHaveLength(1); + }); + const finalStep = result.getByTestId('finalStep'); + expect(finalStep).toBeInTheDocument(); + }); +}); diff --git a/packages/pn-pa-webapp/src/redux/newNotification/__test__/reducers.test.ts b/packages/pn-pa-webapp/src/redux/newNotification/__test__/reducers.test.ts index ac451093c0..ac123ba5b2 100644 --- a/packages/pn-pa-webapp/src/redux/newNotification/__test__/reducers.test.ts +++ b/packages/pn-pa-webapp/src/redux/newNotification/__test__/reducers.test.ts @@ -3,9 +3,14 @@ import MockAdapter from 'axios-mock-adapter'; import { PhysicalCommunicationType } from '@pagopa-pn/pn-commons'; import { mockAuthentication } from '../../../__mocks__/Auth.mock'; -import { newNotification } from '../../../__mocks__/NewNotification.mock'; +import { + newNotification, + newNotificationRecipients, + payments, +} from '../../../__mocks__/NewNotification.mock'; import { apiClient, externalClient } from '../../../api/apiClients'; -import { PaymentModel, PaymentObject } from '../../../models/NewNotification'; +import { NotificationFeePolicy } from '../../../generated-client/notifications'; +import { PaymentModel, PreliminaryInformationsPayload } from '../../../models/NewNotification'; import { GroupStatus } from '../../../models/user'; import { newNotificationMapper } from '../../../utility/notification.utility'; import { store } from '../../store'; @@ -20,27 +25,26 @@ import { saveRecipients, setAttachments, setCancelledIun, + setDebtPosition, + setDebtPositionDetail, setIsCompleted, - setPaymentDocuments, setPreliminaryInformations, setSenderInfos, } from '../reducers'; -import { PreliminaryInformationsPayload } from '../types'; const initialState = { loading: false, notification: { + notificationFeePolicy: NotificationFeePolicy.FlatRate, paProtocolNumber: '', subject: '', recipients: [], documents: [], - payment: {}, - physicalCommunicationType: '', - paymentMode: '', + physicalCommunicationType: PhysicalCommunicationType.REGISTERED_LETTER_890, group: '', taxonomyCode: '', - notificationFeePolicy: '', senderDenomination: '', + senderTaxId: '', }, groups: [], isCompleted: false, @@ -105,7 +109,7 @@ describe('New notification redux state tests', () => { physicalCommunicationType: PhysicalCommunicationType.REGISTERED_LETTER_890, group: '', taxonomyCode: '010801N', - paymentMode: PaymentModel.PAGO_PA_NOTICE_F24, + paymentMode: PaymentModel.PAGO_PA, }; const action = store.dispatch(setPreliminaryInformations(preliminaryInformations)); expect(action.type).toBe('newNotificationSlice/setPreliminaryInformations'); @@ -172,35 +176,79 @@ describe('New notification redux state tests', () => { extMock.restore(); }); + it('should be able to set new debt position', () => { + const recipients = newNotification.recipients.map((recipient) => ({ + ...recipient, + debtPosition: PaymentModel.PAGO_PA_F24, + })); + + const action = store.dispatch(setDebtPosition({ recipients })); + expect(action.type).toBe('newNotificationSlice/setDebtPosition'); + expect(action.payload).toEqual({ recipients }); + expect(store.getState().newNotificationState.notification.recipients).toEqual(recipients); + }); + + it('should clear payments when set debt position to NOTHING', () => { + store.dispatch( + setDebtPosition({ + recipients: newNotificationRecipients.map((recipient) => ({ + ...recipient, + debtPosition: PaymentModel.PAGO_PA_F24, + })), + }) + ); + + const updatedRecipients = newNotification.recipients.map((recipient) => ({ + ...recipient, + debtPosition: PaymentModel.NOTHING, + })); + + const action = store.dispatch(setDebtPosition({ recipients: updatedRecipients })); + expect(action.type).toBe('newNotificationSlice/setDebtPosition'); + expect(action.payload).toEqual({ recipients: updatedRecipients }); + expect(store.getState().newNotificationState.notification.recipients).toEqual( + updatedRecipients.map((recipient) => ({ + ...recipient, + payments: [], + })) + ); + }); + it('Should be able to save payment documents', () => { const action = store.dispatch( - setPaymentDocuments({ paymentDocuments: newNotification.payment! }) + setDebtPositionDetail({ + recipients: newNotification.recipients, + paFee: newNotification.paFee, + vat: newNotification.vat, + notificationFeePolicy: newNotification.notificationFeePolicy, + pagoPaIntMode: newNotification.pagoPaIntMode, + }) ); - expect(action.type).toBe('newNotificationSlice/setPaymentDocuments'); - expect(action.payload).toEqual({ paymentDocuments: newNotification.payment! }); + expect(action.type).toBe('newNotificationSlice/setDebtPositionDetail'); + expect(action.payload).toEqual({ + recipients: newNotification.recipients, + paFee: newNotification.paFee, + vat: newNotification.vat, + notificationFeePolicy: newNotification.notificationFeePolicy, + pagoPaIntMode: newNotification.pagoPaIntMode, + }); }); it('Should be able to upload payment document', async () => { mock .onPost( '/bff/v1/notifications/sent/documents/preload', - Object.values(newNotification.payment!).reduce((arr, elem) => { - if (elem.pagoPaForm) { - arr.push({ - contentType: elem.pagoPaForm.contentType, - sha256: elem.pagoPaForm.file.sha256.hashBase64, - }); - } - if (elem.f24flatRate) { + Object.values(payments).reduce((arr, elem) => { + if (elem.pagoPa) { arr.push({ - contentType: elem.f24flatRate.contentType, - sha256: elem.f24flatRate.file.sha256.hashBase64, + contentType: elem.pagoPa.contentType, + sha256: elem.pagoPa.file?.sha256.hashBase64, }); } - if (elem.f24standard) { + if (elem.f24) { arr.push({ - contentType: elem.f24standard.contentType, - sha256: elem.f24standard.file.sha256.hashBase64, + contentType: elem.f24.contentType, + sha256: elem.f24.file.sha256.hashBase64, }); } return arr; @@ -227,59 +275,49 @@ describe('New notification redux state tests', () => { }, ]); const extMock = new MockAdapter(externalClient); - for (const payment of Object.values(newNotification.payment!)) { - if (payment.pagoPaForm) { - extMock.onPost(`https://mocked-url.com`).reply(200, payment.pagoPaForm.file.data, { + for (const payment of Object.values(payments)) { + if (payment.pagoPa) { + extMock.onPost(`https://mocked-url.com`).reply(200, payment.pagoPa.file?.data, { 'x-amz-version-id': 'mocked-versionToken', }); } - if (payment.f24flatRate) { - extMock.onPost(`https://mocked-url.com`).reply(200, payment.f24flatRate.file.data, { - 'x-amz-version-id': 'mocked-versionToken', - }); - } - if (payment.f24standard) { - extMock.onPost(`https://mocked-url.com`).reply(200, payment.f24standard.file.data, { + if (payment.f24) { + extMock.onPost(`https://mocked-url.com`).reply(200, payment.f24.file.data, { 'x-amz-version-id': 'mocked-versionToken', }); } } const action = await store.dispatch( - uploadNotificationPaymentDocument(newNotification.payment!) + uploadNotificationPaymentDocument(newNotificationRecipients) ); expect(action.type).toBe('uploadNotificationPaymentDocument/fulfilled'); - const response: { [key: string]: PaymentObject } = {}; - for (const [key, value] of Object.entries(newNotification.payment!)) { - response[key] = {} as PaymentObject; - if (value.pagoPaForm) { - response[key].pagoPaForm = { - ...value.pagoPaForm, - ref: { - key: 'mocked-preload-key', - versionToken: 'mocked-versionToken', - }, - }; - } - if (value.f24flatRate) { - response[key].f24flatRate = { - ...value.f24flatRate, - ref: { - key: 'mocked-preload-key', - versionToken: 'mocked-versionToken', - }, - }; - } - if (value.f24standard) { - response[key].f24standard = { - ...value.f24standard, - ref: { - key: 'mocked-preload-key', - versionToken: 'mocked-versionToken', - }, - }; - } - } - expect(action.payload).toEqual(response); + + const expectedResponse = newNotificationRecipients.map((recipient) => ({ + ...recipient, + payments: recipient.payments?.map((payment) => ({ + ...payment, + pagoPa: payment.pagoPa + ? { + ...payment.pagoPa, + ref: { + key: 'mocked-preload-key', + versionToken: 'mocked-versionToken', + }, + } + : undefined, + f24: payment.f24 + ? { + ...payment.f24, + ref: { + key: 'mocked-preload-key', + versionToken: 'mocked-versionToken', + }, + } + : undefined, + })), + })); + + expect(action.payload).toEqual(expectedResponse); extMock.restore(); }); @@ -296,6 +334,7 @@ describe('New notification redux state tests', () => { idempotenceToken: 'mocked-idempotenceToken', }; const mappedNotification = newNotificationMapper(newNotification); + mock.onPost('/bff/v1/notifications/sent', mappedNotification).reply(200, mockResponse); const action = await store.dispatch(createNewNotification(newNotification)); expect(action.type).toBe('createNewNotification/fulfilled'); diff --git a/packages/pn-pa-webapp/src/redux/newNotification/actions.ts b/packages/pn-pa-webapp/src/redux/newNotification/actions.ts index 623f15fce4..89ba8fad3d 100644 --- a/packages/pn-pa-webapp/src/redux/newNotification/actions.ts +++ b/packages/pn-pa-webapp/src/redux/newNotification/actions.ts @@ -7,18 +7,20 @@ import { apiClient } from '../../api/apiClients'; import { NotificationsApi } from '../../api/notifications/Notifications.api'; import { InfoPaApiFactory } from '../../generated-client/info-pa'; import { - BffNewNotificationRequest, + BffNewNotificationResponse, NotificationSentApiFactory, } from '../../generated-client/notifications'; import { NewNotification, NewNotificationDocument, - NewNotificationResponse, - PaymentObject, + NewNotificationF24Payment, + NewNotificationPagoPaPayment, + NewNotificationRecipient, + UploadDocumentParams, + UploadDocumentsResponse, } from '../../models/NewNotification'; import { GroupStatus, UserGroup } from '../../models/user'; -import { newNotificationMapper } from '../../utility/notification.utility'; -import { UploadDocumentParams, UploadDocumentsResponse } from './types'; +import { hasPagoPaDocument, newNotificationMapper } from '../../utility/notification.utility'; export enum NEW_NOTIFICATION_ACTIONS { GET_USER_GROUPS = 'getUserGroups', @@ -43,12 +45,11 @@ export const getUserGroups = createAsyncThunk( ); const createPayloadToUpload = async ( - item: NewNotificationDocument + item: NewNotificationDocument | Required | NewNotificationF24Payment ): Promise => { const unit8Array = await calcUnit8Array(item.file.data); return { id: item.id, - key: item.name, contentType: item.contentType, file: unit8Array, sha256: item.file.sha256.hashBase64, @@ -76,7 +77,8 @@ const uploadNotificationDocumentCbk = async ( items[index].sha256, presigneUrl.secret, items[index].file as Uint8Array, - presigneUrl.httpMethod + presigneUrl.httpMethod, + items[index].contentType ) ); } @@ -132,56 +134,70 @@ export const uploadNotificationDocument = createAsyncThunk< } ); -const getPaymentDocumentsToUpload = (items: { - [key: string]: PaymentObject; -}): Array> => { +const getPaymentDocumentsToUpload = ( + recipients: Array +): Array> => { const documentsArr: Array> = []; - for (const item of Object.values(items)) { - /* eslint-disable functional/immutable-data */ - if (item.pagoPaForm && !item.pagoPaForm.ref.key && !item.pagoPaForm.ref.versionToken) { - documentsArr.push(createPayloadToUpload(item.pagoPaForm)); + /* eslint-disable functional/immutable-data */ + for (const recipient of recipients) { + if (!recipient.payments) { + continue; } - if (item.f24flatRate && !item.f24flatRate.ref.key && !item.f24flatRate.ref.versionToken) { - documentsArr.push(createPayloadToUpload(item.f24flatRate)); - } - if (item.f24standard && !item.f24standard.ref.key && !item.f24standard.ref.versionToken) { - documentsArr.push(createPayloadToUpload(item.f24standard)); + + for (const payment of recipient.payments) { + if ( + payment.pagoPa && + hasPagoPaDocument(payment.pagoPa) && + !payment.pagoPa.ref.key && + !payment.pagoPa.ref.versionToken + ) { + documentsArr.push(createPayloadToUpload(payment.pagoPa)); + } + + if (payment.f24 && !payment.f24.ref.key && !payment.f24.ref.versionToken) { + documentsArr.push(createPayloadToUpload(payment.f24)); + } } - /* eslint-enable functional/immutable-data */ } + /* eslint-enable functional/immutable-data */ + return documentsArr; }; export const uploadNotificationPaymentDocument = createAsyncThunk< - { [key: string]: PaymentObject }, - { [key: string]: PaymentObject } + Array, + Array >( NEW_NOTIFICATION_ACTIONS.UPLOAD_PAYMENT_DOCUMENT, - async (items: { [key: string]: PaymentObject }, { rejectWithValue }) => { + async (recipients: Array, { rejectWithValue }) => { try { // before upload, filter out documents already uploaded - const documentsToUpload = await Promise.all(getPaymentDocumentsToUpload(items)); + const documentsToUpload = await Promise.all(getPaymentDocumentsToUpload(recipients)); if (documentsToUpload.length === 0) { - return items; + return recipients; } const documentsUploaded = await uploadNotificationDocumentCbk(documentsToUpload); - const updatedItems = _.cloneDeep(items); - for (const item of Object.values(updatedItems)) { - /* eslint-disable functional/immutable-data */ - if (item.pagoPaForm && documentsUploaded[item.pagoPaForm.id]) { - item.pagoPaForm.ref.key = documentsUploaded[item.pagoPaForm.id].key; - item.pagoPaForm.ref.versionToken = documentsUploaded[item.pagoPaForm.id].versionToken; - } - if (item.f24flatRate && documentsUploaded[item.f24flatRate.id]) { - item.f24flatRate.ref.key = documentsUploaded[item.f24flatRate.id].key; - item.f24flatRate.ref.versionToken = documentsUploaded[item.f24flatRate.id].versionToken; + const updatedItems = _.cloneDeep(recipients); + + for (const updatedItem of updatedItems) { + if (!updatedItem.payments) { + continue; } - if (item.f24standard && documentsUploaded[item.f24standard.id]) { - item.f24standard.ref.key = documentsUploaded[item.f24standard.id].key; - item.f24standard.ref.versionToken = documentsUploaded[item.f24standard.id].versionToken; + + for (const payment of updatedItem.payments) { + /* eslint-disable functional/immutable-data */ + if (payment.pagoPa?.ref && documentsUploaded[payment.pagoPa.id]) { + payment.pagoPa.ref.key = documentsUploaded[payment.pagoPa.id].key; + payment.pagoPa.ref.versionToken = documentsUploaded[payment.pagoPa.id].versionToken; + } + if (payment.f24 && documentsUploaded[payment.f24.id]) { + payment.f24.ref.key = documentsUploaded[payment.f24.id].key; + payment.f24.ref.versionToken = documentsUploaded[payment.f24.id].versionToken; + } + /* eslint-enable functional/immutable-data */ } - /* eslint-enable functional/immutable-data */ } + return updatedItems; } catch (e) { return rejectWithValue(parseError(e)); @@ -189,7 +205,7 @@ export const uploadNotificationPaymentDocument = createAsyncThunk< } ); -export const createNewNotification = createAsyncThunk( +export const createNewNotification = createAsyncThunk( NEW_NOTIFICATION_ACTIONS.CREATE_NOTIFICATION, async (notification: NewNotification, { rejectWithValue }) => { try { @@ -199,10 +215,8 @@ export const createNewNotification = createAsyncThunk; + isCompleted: boolean; +}; + +const initialState: NewNotificationInitialState = { loading: false, notification: { + notificationFeePolicy: NotificationFeePolicy.FLAT_RATE, paProtocolNumber: '', subject: '', recipients: [], documents: [], - payment: {}, - physicalCommunicationType: '' as PhysicalCommunicationType, + physicalCommunicationType: PhysicalCommunicationType.REGISTERED_LETTER_890, group: '', taxonomyCode: '', - paymentMode: '' as PaymentModel, - notificationFeePolicy: '' as NotificationFeePolicy, senderDenomination: '', - } as NewNotification, + senderTaxId: '', + }, groups: [] as Array, isCompleted: false, }; @@ -54,19 +59,9 @@ const newNotificationSlice = createSlice({ state.notification.senderTaxId = action.payload.senderTaxId; }, setPreliminaryInformations: (state, action: PayloadAction) => { - // TODO: capire la logica di set della fee policy sia corretta state.notification = { ...state.notification, ...action.payload, - // PN-1835 - // in questa fase la notificationFeePolicy viene assegnata di default a FLAT_RATE - // Carlotta Dimatteo 10/08/2022 - notificationFeePolicy: NotificationFeePolicy.FLAT_RATE, - // reset payment data if payment mode has changed - payment: - state.notification.paymentMode !== action.payload.paymentMode - ? {} - : state.notification.payment, }; }, saveRecipients: ( @@ -84,13 +79,58 @@ const newNotificationSlice = createSlice({ ) => { state.notification.documents = action.payload.documents; }, - setPaymentDocuments: ( + setDebtPosition: ( + state, + action: PayloadAction<{ + recipients: Array; + }> + ) => { + const { recipients } = action.payload; + + recipients.forEach(({ taxId, debtPosition: newDebtPosition }) => { + const currentRecipientIdx = state.notification.recipients.findIndex( + (r) => r.taxId === taxId + ); + + // Skip if recipient not found + if (currentRecipientIdx === -1 || !newDebtPosition) { + return; + } + + const currentRecipient = state.notification.recipients[currentRecipientIdx]; + const oldDebtPosition = currentRecipient.debtPosition; + + // Update payments + const updatedPayments = filterPaymentsByDebtPositionChange( + currentRecipient.payments || [], + newDebtPosition, + oldDebtPosition + ); + + state.notification.recipients[currentRecipientIdx] = { + ...currentRecipient, + debtPosition: newDebtPosition, + payments: updatedPayments, + }; + }); + }, + setDebtPositionDetail: ( state, - action: PayloadAction<{ paymentDocuments: { [key: string]: PaymentObject } }> + action: PayloadAction<{ + recipients: Array; + vat?: number; + paFee?: number; + notificationFeePolicy: NotificationFeePolicy; + pagoPaIntMode?: PagoPaIntegrationMode; + }> ) => { state.notification = { ...state.notification, - payment: action.payload.paymentDocuments, + recipients: action.payload.recipients, + vat: action.payload.vat, + paFee: Number(action.payload.paFee), + notificationFeePolicy: action.payload.notificationFeePolicy, + pagoPaIntMode: action.payload.pagoPaIntMode, }; }, setIsCompleted: (state) => { @@ -104,11 +144,9 @@ const newNotificationSlice = createSlice({ }); builder.addCase(uploadNotificationDocument.fulfilled, (state, action) => { state.notification.documents = action.payload; - state.isCompleted = !getConfiguration().IS_PAYMENT_ENABLED; }); builder.addCase(uploadNotificationPaymentDocument.fulfilled, (state, action) => { - state.notification.payment = action.payload; - state.isCompleted = true; + state.notification.recipients = action.payload; }); builder.addCase(createNewNotification.rejected, (state) => { state.isCompleted = false; @@ -122,9 +160,10 @@ export const { setPreliminaryInformations, saveRecipients, setAttachments, - setPaymentDocuments, + setDebtPositionDetail, resetState, setIsCompleted, + setDebtPosition, } = newNotificationSlice.actions; export default newNotificationSlice; diff --git a/packages/pn-pa-webapp/src/redux/newNotification/types.ts b/packages/pn-pa-webapp/src/redux/newNotification/types.ts deleted file mode 100644 index e5742b9295..0000000000 --- a/packages/pn-pa-webapp/src/redux/newNotification/types.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { PhysicalCommunicationType } from '@pagopa-pn/pn-commons'; - -import { NewNotificationBilingualism, PaymentModel } from '../../models/NewNotification'; - -export interface PreliminaryInformationsPayload extends NewNotificationBilingualism { - paProtocolNumber: string; - subject: string; - abstract?: string; - physicalCommunicationType: PhysicalCommunicationType; - group?: string; - paymentMode: PaymentModel; - taxonomyCode: string; - senderDenomination?: string; -} - -export interface UploadDocumentParams { - id: string; - key: string; - contentType: string; - file: Uint8Array | undefined; - sha256: string; -} - -export interface UploadPaymentResponse { - [key: string]: { - pagoPaForm: UploadDocumentsResponse; - f24flatRate?: UploadDocumentsResponse; - f24standard?: UploadDocumentsResponse; - }; -} - -export interface UploadDocumentsResponse { - [id: string]: { - key: string; - versionToken: string; - }; -} diff --git a/packages/pn-pa-webapp/src/services/configuration.service.ts b/packages/pn-pa-webapp/src/services/configuration.service.ts index 1907687278..a20eac50b2 100644 --- a/packages/pn-pa-webapp/src/services/configuration.service.ts +++ b/packages/pn-pa-webapp/src/services/configuration.service.ts @@ -1,4 +1,4 @@ -import { Configuration, dataRegex, IS_DEVELOP } from '@pagopa-pn/pn-commons'; +import { Configuration, IS_DEVELOP, dataRegex } from '@pagopa-pn/pn-commons'; import { Validator } from '@pagopa-pn/pn-validator'; export interface PaConfiguration { @@ -21,6 +21,8 @@ export interface PaConfiguration { IS_STATISTICS_ENABLED: boolean; TAXONOMY_SEND_URL: string; DOWNTIME_EXAMPLE_LINK: string; + PAYMENT_INFO_LINK: string; + DEVELOPER_API_DOCUMENTATION_LINK: string; } class PaConfigurationValidator extends Validator { @@ -45,6 +47,11 @@ class PaConfigurationValidator extends Validator { this.ruleFor('IS_STATISTICS_ENABLED').isBoolean(); this.ruleFor('TAXONOMY_SEND_URL').isString().isRequired(); this.ruleFor('DOWNTIME_EXAMPLE_LINK').isString().isRequired().matches(dataRegex.htmlPageUrl); + this.ruleFor('PAYMENT_INFO_LINK').isString().isRequired().matches(dataRegex.htmlPageUrl); + this.ruleFor('DEVELOPER_API_DOCUMENTATION_LINK') + .isString() + .isRequired() + .matches(dataRegex.htmlPageUrl); } } diff --git a/packages/pn-pa-webapp/src/setupTests.tsx b/packages/pn-pa-webapp/src/setupTests.tsx index 4bea7a10f3..9adfe52601 100644 --- a/packages/pn-pa-webapp/src/setupTests.tsx +++ b/packages/pn-pa-webapp/src/setupTests.tsx @@ -34,6 +34,8 @@ beforeAll(() => { TAXONOMY_SEND_URL: 'https://test.taxonomy.pagopa.it', DOWNTIME_EXAMPLE_LINK: 'https://test.downtime.pagopa.it', LANDING_SITE_URL: 'https://test.landing.pagopa.it', + PAYMENT_INFO_LINK: 'https://test.payment.pagopa.it', + DEVELOPER_API_DOCUMENTATION_LINK: 'https://test.api.pagopa.it', }); initStore(false); initAxiosClients(); diff --git a/packages/pn-pa-webapp/src/utility/__test__/notification.utility.test.ts b/packages/pn-pa-webapp/src/utility/__test__/notification.utility.test.ts index bad65118b3..81a0f8d66f 100644 --- a/packages/pn-pa-webapp/src/utility/__test__/notification.utility.test.ts +++ b/packages/pn-pa-webapp/src/utility/__test__/notification.utility.test.ts @@ -1,6 +1,15 @@ -import { newNotification, newNotificationDTO } from '../../__mocks__/NewNotification.mock'; -import { NewNotificationDTO } from '../../models/NewNotification'; -import { getDuplicateValuesByKeys, newNotificationMapper } from '../notification.utility'; +import { + newNotification, + newNotificationForBff, + newNotificationRecipients, +} from '../../__mocks__/NewNotification.mock'; +import { BffNewNotificationRequest } from '../../generated-client/notifications'; +import { NewNotificationPayment, PaymentModel } from '../../models/NewNotification'; +import { + filterPaymentsByDebtPositionChange, + getDuplicateValuesByKeys, + newNotificationMapper, +} from '../notification.utility'; const mockArray = [ { key1: 'value1', key2: 'value2', key3: 'value3' }, @@ -14,31 +23,7 @@ const mockArray = [ describe('Test notification utility', () => { it('Map notification from presentation layer to api layer', () => { const result = newNotificationMapper(newNotification); - expect(result).toEqual(newNotificationDTO); - }); - - it('Checks that if physical address has empty required fields, its value is set to undefined', () => { - const request = { - ...newNotification, - recipients: newNotification.recipients.map((recipient, index) => { - if (index === 0) { - recipient.address = ''; - recipient.houseNumber = ''; - } - return recipient; - }), - }; - const response = { - ...newNotificationDTO, - recipients: newNotificationDTO.recipients.map((recipient, index) => { - if (index === 0) { - recipient.physicalAddress = undefined; - } - return recipient; - }), - }; - const result = newNotificationMapper(request); - expect(result).toEqual(response); + expect(result).toEqual(newNotificationForBff); }); it('Checks that getDuplicateValuesByKeys returns duplicate values', () => { @@ -47,6 +32,7 @@ describe('Test notification utility', () => { }); it('Checks that notificationMapper returns correct bilingualism dto', () => { + // fe version after mapper const result = newNotificationMapper({ ...newNotification, lang: 'other', @@ -54,12 +40,104 @@ describe('Test notification utility', () => { additionalAbstract: 'abstract for de', additionalSubject: 'subject for de', }); - const response: NewNotificationDTO = { - ...newNotificationDTO, + + const response: BffNewNotificationRequest = { + ...newNotificationForBff, subject: 'Multone esagerato|subject for de', abstract: 'abstract for de', additionalLanguages: ['DE'], }; expect(result).toEqual(response); }); + + describe('Test filter payments by debt position change', () => { + const recipientPayments = newNotificationRecipients[1].payments ?? []; + + it('should return the same payments if the debt position does not change', () => { + const result = filterPaymentsByDebtPositionChange( + recipientPayments, + PaymentModel.PAGO_PA, + PaymentModel.PAGO_PA + ); + expect(result).toEqual(recipientPayments); + }); + + it('should return all payments when previous debt position is undefined', () => { + const result = filterPaymentsByDebtPositionChange( + recipientPayments, + PaymentModel.PAGO_PA, + undefined + ); + expect(result).toEqual(recipientPayments); + }); + + it('should return empty array when set debt position to NOTHING', () => { + const result = filterPaymentsByDebtPositionChange( + recipientPayments, + PaymentModel.NOTHING, + PaymentModel.PAGO_PA + ); + expect(result).toEqual([]); + }); + + it('should keep only PAGOPA payments when debt position change from PAGO_PA_F24 to PAGOPA', () => { + const pagopaPayments = recipientPayments.reduce((acc, item) => { + acc.push({ pagoPa: item.pagoPa }); + return acc; + }, [] as Array); + + const result = filterPaymentsByDebtPositionChange( + recipientPayments, + PaymentModel.PAGO_PA, + PaymentModel.PAGO_PA_F24 + ); + expect(result).toEqual(pagopaPayments); + }); + + it('should keep only F24 payments when debt position change from PAGO_PA_F24 to F24', () => { + const f24Payments = recipientPayments.reduce((acc, item) => { + acc.push({ f24: item.f24 }); + return acc; + }, [] as Array); + + const result = filterPaymentsByDebtPositionChange( + recipientPayments, + PaymentModel.F24, + PaymentModel.PAGO_PA_F24 + ); + expect(result).toEqual(f24Payments); + }); + + it('should clear all payments when debt position change from from PAGOPA to F24', () => { + const result = filterPaymentsByDebtPositionChange( + recipientPayments, + PaymentModel.F24, + PaymentModel.PAGO_PA + ); + expect(result).toEqual([]); + }); + + it('should clear all payments when debt position change from F24 to PAGOPA', () => { + const result = filterPaymentsByDebtPositionChange( + recipientPayments, + PaymentModel.PAGO_PA, + PaymentModel.F24 + ); + expect(result).toEqual([]); + }); + + it('should return all payments when debt position change from NONE', () => { + const result = filterPaymentsByDebtPositionChange( + recipientPayments, + PaymentModel.PAGO_PA, + PaymentModel.NOTHING + ); + expect(result).toEqual(recipientPayments); + }); + + it('should handle empty payments array', () => { + const result = filterPaymentsByDebtPositionChange([], PaymentModel.PAGO_PA, PaymentModel.F24); + expect(result).toEqual([]); + }); + }); }); diff --git a/packages/pn-pa-webapp/src/utility/__test__/validation.utility.test.ts b/packages/pn-pa-webapp/src/utility/__test__/validation.utility.test.ts index 41a5186e06..24a5961045 100644 --- a/packages/pn-pa-webapp/src/utility/__test__/validation.utility.test.ts +++ b/packages/pn-pa-webapp/src/utility/__test__/validation.utility.test.ts @@ -1,10 +1,9 @@ import { RecipientType } from '@pagopa-pn/pn-commons'; import { randomString } from '../../__test__/test-utils'; -import { NewNotificationRecipient, PaymentModel } from '../../models/NewNotification'; +import { NewNotificationRecipient } from '../../models/NewNotification'; import { denominationLengthAndCharacters, - identicalIUV, identicalTaxIds, taxIdDependingOnRecipientType, } from '../validation.utility'; @@ -84,13 +83,14 @@ describe('test custom validation for recipients', () => { ]); }); + /* it('identicalIUV (no errors)', () => { const result = identicalIUV( [ { creditorTaxId: 'creditorTaxId1', noticeCode: 'noticeCode1' }, { creditorTaxId: 'creditorTaxId2', noticeCode: 'noticeCode2' }, ] as Array, - PaymentModel.PAGO_PA_NOTICE + PaymentModel.PAGO_PA ); expect(result).toHaveLength(0); }); @@ -102,7 +102,7 @@ describe('test custom validation for recipients', () => { { creditorTaxId: 'creditorTaxId2', noticeCode: 'noticeCode2' }, { creditorTaxId: 'creditorTaxId1', noticeCode: 'noticeCode1' }, ] as Array, - PaymentModel.PAGO_PA_NOTICE + PaymentModel.PAGO_PA ); expect(result).toHaveLength(4); expect(result).toStrictEqual([ @@ -128,4 +128,5 @@ describe('test custom validation for recipients', () => { }, ]); }); + */ }); diff --git a/packages/pn-pa-webapp/src/utility/notification.utility.ts b/packages/pn-pa-webapp/src/utility/notification.utility.ts index 1edfc24421..01786c7e9d 100644 --- a/packages/pn-pa-webapp/src/utility/notification.utility.ts +++ b/packages/pn-pa-webapp/src/utility/notification.utility.ts @@ -1,66 +1,52 @@ /* eslint-disable functional/no-let */ import _ from 'lodash'; -import { - NotificationDetailDocument, - NotificationDetailRecipient, - PhysicalAddress, - RecipientType, -} from '@pagopa-pn/pn-commons'; +import { NotificationDetailDocument, PhysicalAddress, RecipientType } from '@pagopa-pn/pn-commons'; +import { + BffNewNotificationRequest, + NotificationDocument, + NotificationPaymentItem, + NotificationRecipientV23, +} from '../generated-client/notifications'; import { NewNotification, - NewNotificationDTO, NewNotificationDocument, + NewNotificationDocumentFile, + NewNotificationDocumentRef, NewNotificationLangOther, + NewNotificationPagoPaPayment, + NewNotificationPayment, NewNotificationRecipient, PaymentModel, - PaymentObject, } from '../models/NewNotification'; const checkPhysicalAddress = (recipient: NewNotificationRecipient) => { - if ( - recipient.address && - recipient.houseNumber && - recipient.zip && - recipient.municipality && - recipient.province && - recipient.foreignState - ) { - const address = { - address: `${recipient.address} ${recipient.houseNumber}`, - addressDetails: recipient.addressDetails, - zip: recipient.zip, - municipality: recipient.municipality, - municipalityDetails: recipient.municipalityDetails, - province: recipient.province, - foreignState: recipient.foreignState, - }; + const address = { + address: `${recipient.address} ${recipient.houseNumber}`, + addressDetails: recipient.addressDetails, + zip: recipient.zip, + municipality: recipient.municipality, + municipalityDetails: recipient.municipalityDetails, + province: recipient.province, + foreignState: recipient.foreignState, + }; - // clean the object from undefined keys - (Object.keys(address) as Array>).forEach((key) => { - if (!address[key]) { - // eslint-disable-next-line functional/immutable-data - delete address[key]; - } - }); - return address; - } - return undefined; + // clean the object from undefined keys + (Object.keys(address) as Array>).forEach((key) => { + if (!address[key]) { + // eslint-disable-next-line functional/immutable-data + delete address[key]; + } + }); + return address; }; const newNotificationRecipientsMapper = ( - recipients: Array, - paymentMethod?: PaymentModel -): Array => + recipients: Array +): Array => recipients.map((recipient) => { - const digitalDomicile = recipient.digitalDomicile - ? { - type: recipient.type, - address: recipient.digitalDomicile, - } - : undefined; - const parsedRecipient: NotificationDetailRecipient = { + const parsedRecipient: NotificationRecipientV23 = { denomination: recipient.recipientType === RecipientType.PG ? recipient.firstName @@ -69,86 +55,93 @@ const newNotificationRecipientsMapper = ( taxId: recipient.taxId, physicalAddress: checkPhysicalAddress(recipient), }; - if (digitalDomicile) { + if (recipient.digitalDomicile) { // eslint-disable-next-line functional/immutable-data - parsedRecipient.digitalDomicile = digitalDomicile; + parsedRecipient.digitalDomicile = { + type: recipient.type, + address: recipient.digitalDomicile, + }; } - if (paymentMethod !== PaymentModel.NOTHING) { + if (recipient.payments) { // eslint-disable-next-line functional/immutable-data - // parsedRecipient.payment = { - // creditorTaxId: recipient.creditorTaxId, - // noticeCode: recipient.noticeCode, - // }; + parsedRecipient.payments = newNotificationPaymentDocumentsMapper(recipient.payments); } return parsedRecipient; }); -const newNotificationDocumentMapper = ( - document: NewNotificationDocument -): NotificationDetailDocument => ({ +const newNotificationDocumentMapper = (document: { + file: NewNotificationDocumentFile; + ref: NewNotificationDocumentRef; + contentType: string; +}): NotificationDetailDocument => ({ digests: { sha256: document.file.sha256.hashBase64, }, contentType: document.contentType, ref: document.ref, - title: document.name, }); const newNotificationAttachmentsMapper = ( documents: Array -): Array => - documents.map((document) => newNotificationDocumentMapper(document)); +): Array => + documents.map((document) => ({ + ...newNotificationDocumentMapper({ + file: document.file, + ref: document.ref, + contentType: document.contentType, + }), + title: document.name, + })); + +export const hasPagoPaDocument = ( + document: NewNotificationPagoPaPayment +): document is Required => !!document.file.data && !!document.ref; const newNotificationPaymentDocumentsMapper = ( - recipients: Array, - paymentDocuments: { [key: string]: PaymentObject } -): Array => - recipients.map((r) => { - const documents: { - pagoPaForm?: NotificationDetailDocument; - f24flatRate?: NotificationDetailDocument; - f24standard?: NotificationDetailDocument; - } = {}; + recipientPayments: Array +): Array => + recipientPayments.map((payment) => { + const mappedPayment: NotificationPaymentItem = {}; + /* eslint-disable functional/immutable-data */ - if ( - paymentDocuments[r.taxId].pagoPaForm && - paymentDocuments[r.taxId].pagoPaForm.file.sha256.hashBase64 !== '' - ) { - documents.pagoPaForm = newNotificationDocumentMapper(paymentDocuments[r.taxId].pagoPaForm); - } - if ( - paymentDocuments[r.taxId].f24flatRate && - paymentDocuments[r.taxId].f24flatRate?.file.sha256.hashBase64 !== '' - ) { - documents.f24flatRate = newNotificationDocumentMapper( - paymentDocuments[r.taxId].f24flatRate as NewNotificationDocument - ); + if (payment.pagoPa) { + mappedPayment.pagoPa = { + creditorTaxId: payment.pagoPa.creditorTaxId, + noticeCode: payment.pagoPa.noticeCode, + applyCost: payment.pagoPa.applyCost, + }; + + if ( + payment.pagoPa.file && + payment.pagoPa.ref && + payment.pagoPa.file?.sha256.hashBase64 !== '' + ) { + mappedPayment.pagoPa.attachment = newNotificationDocumentMapper({ + file: payment.pagoPa.file, + ref: payment.pagoPa.ref, + contentType: payment.pagoPa.contentType, + }); + } } - if ( - paymentDocuments[r.taxId].f24standard && - paymentDocuments[r.taxId].f24standard?.file.sha256.hashBase64 !== '' - ) { - documents.f24standard = newNotificationDocumentMapper( - paymentDocuments[r.taxId].f24standard as NewNotificationDocument - ); + + if (payment.f24 && payment.f24.file.sha256.hashBase64 !== '') { + mappedPayment.f24 = { + title: payment.f24.name, + applyCost: payment.f24.applyCost, + metadataAttachment: newNotificationDocumentMapper({ + file: payment.f24.file, + ref: payment.f24.ref, + contentType: payment.f24.contentType, + }), + }; } - // Con l'introduzione dei multi pagamenti (pn-7336), è necessario apportare delle modifiche anche in fase di creazione - // Andrea Cimini - 16/08/2023 - /* - r.payment = { - ...documents, - creditorTaxId: r.payment ? r.payment.creditorTaxId : '', - noticeCode: r.payment?.noticeCode, - }; - */ /* eslint-enable functional/immutable-data */ - return r; + + return mappedPayment; }); -export function newNotificationMapper(newNotification: NewNotification): NewNotificationDTO { +export function newNotificationMapper(newNotification: NewNotification): BffNewNotificationRequest { const clonedNotification = _.omit(_.cloneDeep(newNotification), [ - 'paymentMode', - 'payment', 'additionalAbstract', 'additionalLang', 'additionalSubject', @@ -174,7 +167,7 @@ export function newNotificationMapper(newNotification: NewNotification): NewNoti : undefined; /* eslint-disable functional/immutable-data */ - const newNotificationParsed: NewNotificationDTO = { + const newNotificationParsed: BffNewNotificationRequest = { ...clonedNotification, recipients: [], documents: [], @@ -185,20 +178,9 @@ export function newNotificationMapper(newNotification: NewNotification): NewNoti } // format recipients - newNotificationParsed.recipients = newNotificationRecipientsMapper( - newNotification.recipients, - newNotification.paymentMode - ); + newNotificationParsed.recipients = newNotificationRecipientsMapper(newNotification.recipients); // format attachments newNotificationParsed.documents = newNotificationAttachmentsMapper(newNotification.documents); - // format payments - if (newNotification.payment && Object.keys(newNotification.payment).length > 0) { - newNotificationParsed.recipients = newNotificationPaymentDocumentsMapper( - newNotificationParsed.recipients, - newNotification.payment - ); - } - /* eslint-enable functional/immutable-data */ return newNotificationParsed; } @@ -232,3 +214,75 @@ const concatAdditionalContent = (content?: string, additionalContent?: string): } return content || additionalContent || ''; }; + +const shouldClearPayments = (newMethod: PaymentModel, previousMethod?: PaymentModel): boolean => { + if (!previousMethod || previousMethod === PaymentModel.NOTHING) { + return false; + } + + if (newMethod === PaymentModel.NOTHING) { + return true; + } + + const transitionMap: Record> = { + PAGO_PA: { + PAGO_PA: false, + F24: true, + PAGO_PA_F24: false, + NOTHING: true, + }, + F24: { + PAGO_PA: true, + F24: false, + PAGO_PA_F24: false, + NOTHING: true, + }, + PAGO_PA_F24: { + PAGO_PA: true, // Remove f24 payments + F24: true, // Remove pagopa payments + PAGO_PA_F24: false, + NOTHING: true, + }, + NOTHING: { + PAGO_PA: false, + F24: false, + PAGO_PA_F24: false, + NOTHING: false, + }, + }; + + return transitionMap[previousMethod]?.[newMethod] ?? false; +}; + +export const filterPaymentsByDebtPositionChange = ( + payments: Array, + newDebtPosition: PaymentModel, + previousDebtPosition?: PaymentModel +): Array => { + if (!shouldClearPayments(newDebtPosition, previousDebtPosition)) { + return payments; + } + + if (newDebtPosition === PaymentModel.NOTHING) { + return []; + } + + if (previousDebtPosition === PaymentModel.PAGO_PA_F24) { + if (newDebtPosition === PaymentModel.PAGO_PA) { + return payments.reduce((acc, item) => { + // eslint-disable-next-line functional/immutable-data + acc.push({ pagoPa: item.pagoPa }); + return acc; + }, [] as Array); + } + if (newDebtPosition === PaymentModel.F24) { + return payments.reduce((acc, item) => { + // eslint-disable-next-line functional/immutable-data + acc.push({ f24: item.f24 }); + return acc; + }, [] as Array); + } + } + + return []; +}; diff --git a/packages/pn-pa-webapp/src/utility/validation.utility.ts b/packages/pn-pa-webapp/src/utility/validation.utility.ts index 419a68af97..1e662d0671 100644 --- a/packages/pn-pa-webapp/src/utility/validation.utility.ts +++ b/packages/pn-pa-webapp/src/utility/validation.utility.ts @@ -1,8 +1,14 @@ +import { TFunction } from 'react-i18next'; import * as yup from 'yup'; import { RecipientType, dataRegex } from '@pagopa-pn/pn-commons'; -import { NewNotificationRecipient, PaymentModel } from '../models/NewNotification'; +import { + NewNotificationF24Payment, + NewNotificationPagoPaPayment, + NewNotificationRecipient, + RecipientPaymentsFormValues, +} from '../models/NewNotification'; import { getDuplicateValuesByKeys } from './notification.utility'; export function requiredStringFieldValidation( @@ -74,36 +80,153 @@ export function identicalTaxIds( return errors; } +export const pagoPaValidationSchema = (t: TFunction, tc: TFunction) => + yup.object().shape({ + noticeCode: yup + .string() + .required(tc('required-field')) + .matches(dataRegex.noticeCode, `${t('payment-methods.pagopa.notice-code')} ${tc('invalid')}`), + creditorTaxId: yup + .string() + .required(tc('required-field')) + .matches(dataRegex.pIva, `${t('payment-methods.pagopa.creditor-taxid')} ${tc('invalid')}`), + applyCost: yup.boolean(), + file: yup + .object({ + data: yup + .mixed() + .test('fileType', '', (input) => input === undefined || input instanceof File) + .optional(), + sha256: yup.object({ + hashBase64: yup.string(), + hashHex: yup.string(), + }), + }) + .optional(), + }); + +export const f24ValidationSchema = (tc: TFunction) => + yup.object().shape({ + name: requiredStringFieldValidation(tc, 512), + applyCost: yup.boolean(), + file: yup + .object() + .shape({ + data: yup + .mixed() + .test((input) => input instanceof File) + .required(), + sha256: yup + .object({ + hashBase64: yup.string().required(), + hashHex: yup.string().required(), + }) + .required(), + }) + .required(), + }); + export function identicalIUV( - values: Array | undefined, - paymentMode: PaymentModel | undefined -): Array<{ messageKey: string; value: NewNotificationRecipient; id: string }> { - const errors: Array<{ messageKey: string; value: NewNotificationRecipient; id: string }> = []; - if (values && paymentMode !== PaymentModel.NOTHING) { - const duplicateIUVs = getDuplicateValuesByKeys(values, ['creditorTaxId', 'noticeCode']); - if (duplicateIUVs.length > 0) { - values.forEach((value: NewNotificationRecipient, i: number) => { - if ( - value.creditorTaxId && - value.noticeCode && - duplicateIUVs.includes(value.creditorTaxId + value.noticeCode) - ) { - // eslint-disable-next-line functional/immutable-data - errors.push( - { - messageKey: 'identical-notice-codes-error', - value, - id: `recipients[${i}].noticeCode`, - }, - { - messageKey: '', - value, - id: `recipients[${i}].creditorTaxId`, - } - ); - } - }); - } + values: RecipientPaymentsFormValues | undefined +): Array<{ messageKey: string; value: NewNotificationPagoPaPayment; id: string }> { + const errors: Array<{ messageKey: string; value: NewNotificationPagoPaPayment; id: string }> = []; + + if (!values) { + return errors; + } + + const allPagoPaPayments: Array = []; + + Object.entries(values).forEach(([taxIdKey, payments]) => { + payments.pagoPa.forEach((payment) => { + // eslint-disable-next-line functional/immutable-data + allPagoPaPayments.push({ ...payment, taxIdKey }); + }); + }); + + const duplicateIUVs = getDuplicateValuesByKeys(allPagoPaPayments, [ + 'creditorTaxId', + 'noticeCode', + ]); + + if (duplicateIUVs.length > 0) { + allPagoPaPayments.forEach((payment) => { + if ( + payment.creditorTaxId && + payment.noticeCode && + duplicateIUVs.includes(payment.creditorTaxId + payment.noticeCode) + ) { + // eslint-disable-next-line functional/immutable-data + errors.push( + { + messageKey: 'identical-notice-codes-error', + value: payment, + id: `recipients[${payment.taxIdKey}].pagoPa[${payment.idx}].noticeCode`, + }, + { + messageKey: '', + value: payment, + id: `recipients[${payment.taxIdKey}].pagoPa[${payment.idx}].creditorTaxId`, + } + ); + } + }); } + return errors; } + +const checkPaymentsApplyCost = ( + recipientId: string, + payments: Array | Array, + paymentType: 'pagoPa' | 'f24', + errors: Array<{ + messageKey: string; + value: Array | Array; + id: string; + }> +) => { + if (!payments || payments.length === 0) { + return; + } + + const hasApplyCost = payments.some((item) => item.applyCost); + + if (!hasApplyCost) { + payments.forEach((payment, idx) => { + if (!payment.applyCost) { + // eslint-disable-next-line functional/immutable-data + errors.push({ + messageKey: 'at-least-one-applycost', + value: payments, + id: `recipients[${recipientId}].${paymentType}[${idx}].applyCost`, + }); + } + }); + } +}; + +export const checkApplyCost = ( + values: RecipientPaymentsFormValues | undefined +): Array<{ + messageKey: string; + value: Array | Array; + id: string; +}> => { + const errors: Array<{ + messageKey: string; + value: Array | Array; + id: string; + }> = []; + + if (!values) { + return errors; + } + + Object.entries(values).forEach(([recipientId, recipient]) => { + checkPaymentsApplyCost(recipientId, recipient.pagoPa, 'pagoPa', errors); + checkPaymentsApplyCost(recipientId, recipient.f24, 'f24', errors); + }); + + return errors; +}; diff --git a/packages/pn-personafisica-webapp/public/locales/it/notifiche.json b/packages/pn-personafisica-webapp/public/locales/it/notifiche.json index 0f8f8bf465..0b884d3ec8 100644 --- a/packages/pn-personafisica-webapp/public/locales/it/notifiche.json +++ b/packages/pn-personafisica-webapp/public/locales/it/notifiche.json @@ -16,7 +16,8 @@ "data_a": "Al", "data_a-input-aria-label": "Inserisci la data finale della ricerca", "errors": { - "iun": "Inserisci un codice IUN valido" + "iun": "Inserisci un codice IUN valido", + "data_a": "La data di inizio deve precedere la data di fine" } }, "sort": { diff --git a/packages/pn-personafisica-webapp/src/components/Notifications/FilterNotifications.tsx b/packages/pn-personafisica-webapp/src/components/Notifications/FilterNotifications.tsx index 2f2449a8b6..4783259480 100644 --- a/packages/pn-personafisica-webapp/src/components/Notifications/FilterNotifications.tsx +++ b/packages/pn-personafisica-webapp/src/components/Notifications/FilterNotifications.tsx @@ -146,7 +146,6 @@ const FilterNotifications = forwardRef(({ showFilters, currentDelegator }: Props if (!showFilters) { return <>; } - const isInitialSearch = _.isEqual(formik.values, initialEmptyValues); return isMobile ? ( diff --git a/packages/pn-personafisica-webapp/src/components/Notifications/FilterNotificationsFormBody.tsx b/packages/pn-personafisica-webapp/src/components/Notifications/FilterNotificationsFormBody.tsx index 8843985a30..dfdfc8a93a 100644 --- a/packages/pn-personafisica-webapp/src/components/Notifications/FilterNotificationsFormBody.tsx +++ b/packages/pn-personafisica-webapp/src/components/Notifications/FilterNotificationsFormBody.tsx @@ -2,7 +2,7 @@ import { FormikErrors, FormikTouched, FormikValues } from 'formik'; import { ChangeEvent, Fragment } from 'react'; import { useTranslation } from 'react-i18next'; -import { Grid, TextField } from '@mui/material'; +import { FormHelperText, Grid, TextField } from '@mui/material'; import { CustomDatePicker, DATE_FORMAT, @@ -90,8 +90,11 @@ const FilterNotificationsFormBody = ({ error={formikInstance.touched.iunMatch && Boolean(formikInstance.errors.iunMatch)} helperText={ formikInstance.touched.iunMatch && - formikInstance.errors.iunMatch && - String(formikInstance.errors.iunMatch) + formikInstance.errors.iunMatch && ( + + {String(formikInstance.errors.iunMatch)} + + ) } fullWidth sx={{ marginBottom: isMobile ? '20px' : '0' }} @@ -126,6 +129,12 @@ const FilterNotificationsFormBody = ({ type: 'text', 'data-testid': 'input(start date)', }, + helperText: ( + + {!!formikInstance.errors.startDate && + t('filters.errors.data_a', { ns: 'notifiche' })} + + ), }, }} disableFuture={true} @@ -160,6 +169,12 @@ const FilterNotificationsFormBody = ({ type: 'text', 'data-testid': 'input(end date)', }, + helperText: ( + + {!!formikInstance.errors.endDate && + t('filters.errors.data_a', { ns: 'notifiche' })} + + ), }, }} disableFuture={true} diff --git a/packages/pn-personafisica-webapp/src/pages/Notifiche.page.tsx b/packages/pn-personafisica-webapp/src/pages/Notifiche.page.tsx index 3fabe407e3..cb2e44c311 100644 --- a/packages/pn-personafisica-webapp/src/pages/Notifiche.page.tsx +++ b/packages/pn-personafisica-webapp/src/pages/Notifiche.page.tsx @@ -113,7 +113,7 @@ const Notifiche = () => { return ( - {!mandateId && } + {!mandateId && } = ({ ? t('required-field', { ns: 'common' }) : '' } + InputProps={{ + ...params.InputProps, + endAdornment: ( + + ), + }} /> )} value={groupForm.value} diff --git a/packages/pn-personagiuridica-webapp/src/components/Deleghe/DelegationsOfTheCompany.tsx b/packages/pn-personagiuridica-webapp/src/components/Deleghe/DelegationsOfTheCompany.tsx index cee33c0863..665cd19091 100644 --- a/packages/pn-personagiuridica-webapp/src/components/Deleghe/DelegationsOfTheCompany.tsx +++ b/packages/pn-personagiuridica-webapp/src/components/Deleghe/DelegationsOfTheCompany.tsx @@ -196,6 +196,7 @@ const DelegationsOfTheCompany = () => { ) => (
  • { sx={{ marginBottom: isMobile ? '20px' : '0' }} /> - + + {/* c''e ancora il bottone anche se non é raggiungibile o cliccabile */} group.status === GroupStatus.ACTIVE)} disableCloseOnSelect + forcePopupIcon={false} multiple noOptionsText={t('deleghe.table.no-group-found')} getOptionLabel={getOptionLabel} isOptionEqualToValue={(option, value) => option.id === value.id} - popupIcon={} sx={{ [`& .MuiAutocomplete-popupIndicator`]: { transform: 'none', + pointerEvents: 'none', }, marginBottom: isMobile ? '20px' : '0', }} @@ -402,6 +405,10 @@ const DelegationsOfTheCompany = () => { label={t('deleghe.table.group')} placeholder={t('deleghe.table.group')} name="groups" + InputProps={{ + ...params.InputProps, + endAdornment: , + }} /> )} value={formik.values.groups} diff --git a/packages/pn-personagiuridica-webapp/src/components/Notifications/FilterNotificationsFormActions.tsx b/packages/pn-personagiuridica-webapp/src/components/Notifications/FilterNotificationsFormActions.tsx index b025f37013..d8037265df 100644 --- a/packages/pn-personagiuridica-webapp/src/components/Notifications/FilterNotificationsFormActions.tsx +++ b/packages/pn-personagiuridica-webapp/src/components/Notifications/FilterNotificationsFormActions.tsx @@ -1,7 +1,7 @@ import { Fragment } from 'react'; import { useTranslation } from 'react-i18next'; import { Button, Grid } from '@mui/material'; -import { CustomMobileDialogAction } from '@pagopa-pn/pn-commons'; +import { CustomMobileDialogAction } from '@pagopa-pn/pn-commons'; type Props = { filtersApplied: boolean; diff --git a/packages/pn-personagiuridica-webapp/src/components/Notifications/FilterNotificationsFormBody.tsx b/packages/pn-personagiuridica-webapp/src/components/Notifications/FilterNotificationsFormBody.tsx index 8843985a30..dfdfc8a93a 100644 --- a/packages/pn-personagiuridica-webapp/src/components/Notifications/FilterNotificationsFormBody.tsx +++ b/packages/pn-personagiuridica-webapp/src/components/Notifications/FilterNotificationsFormBody.tsx @@ -2,7 +2,7 @@ import { FormikErrors, FormikTouched, FormikValues } from 'formik'; import { ChangeEvent, Fragment } from 'react'; import { useTranslation } from 'react-i18next'; -import { Grid, TextField } from '@mui/material'; +import { FormHelperText, Grid, TextField } from '@mui/material'; import { CustomDatePicker, DATE_FORMAT, @@ -90,8 +90,11 @@ const FilterNotificationsFormBody = ({ error={formikInstance.touched.iunMatch && Boolean(formikInstance.errors.iunMatch)} helperText={ formikInstance.touched.iunMatch && - formikInstance.errors.iunMatch && - String(formikInstance.errors.iunMatch) + formikInstance.errors.iunMatch && ( + + {String(formikInstance.errors.iunMatch)} + + ) } fullWidth sx={{ marginBottom: isMobile ? '20px' : '0' }} @@ -126,6 +129,12 @@ const FilterNotificationsFormBody = ({ type: 'text', 'data-testid': 'input(start date)', }, + helperText: ( + + {!!formikInstance.errors.startDate && + t('filters.errors.data_a', { ns: 'notifiche' })} + + ), }, }} disableFuture={true} @@ -160,6 +169,12 @@ const FilterNotificationsFormBody = ({ type: 'text', 'data-testid': 'input(end date)', }, + helperText: ( + + {!!formikInstance.errors.endDate && + t('filters.errors.data_a', { ns: 'notifiche' })} + + ), }, }} disableFuture={true} diff --git a/packages/pn-personagiuridica-webapp/src/pages/NuovaDelega.page.tsx b/packages/pn-personagiuridica-webapp/src/pages/NuovaDelega.page.tsx index f1cdc0250a..5362022c17 100644 --- a/packages/pn-personagiuridica-webapp/src/pages/NuovaDelega.page.tsx +++ b/packages/pn-personagiuridica-webapp/src/pages/NuovaDelega.page.tsx @@ -3,7 +3,7 @@ import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import * as yup from 'yup'; - +import SearchIcon from '@mui/icons-material/Search'; import PeopleIcon from '@mui/icons-material/People'; import { Box, @@ -413,6 +413,12 @@ const NuovaDelega = () => { label={entitySearchLabel(senderInputValue)} error={Boolean(getError(touched.enti, errors.enti))} helperText={getError(touched.enti, errors.enti)} + InputProps={{ + ...params.InputProps, + endAdornment: ( + + ), + }} /> )} />