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 0><1>Vorabinformationen1><2> zurück und wähle einen Vordruck aus. Komm dann hierher zurück, um diesen zu laden.2>",
"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 0><1>Preliminary Information1><2> and select a form. Then, return here to upload it.2>",
"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 0><1>Informazioni preliminari1><2> e seleziona un modello. Poi, torna qui per caricarlo.2>",
- "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ù0>"
+ },
+ "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 0><1>Informazioni preliminari1><2> e seleziona un modello. Poi, torna qui per caricarlo.2>",
+ "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 0><1>Uvodne informacije1><2> in izberite obrazec. Nato se vrnite in ga naložite.2>",
"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 (
+
+ );
+};
+
+// 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 (
+
+ );
+};
+
+// 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 (
-
+ );
+ })}
+
);
};
-// 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: (
+
+ ),
+ }}
/>
)}
/>