aziendasanitaria-utils 1.2.71 → 1.2.72

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "engines": {
4
4
  "node": ">=14.0.0"
5
5
  },
6
- "version": "1.2.71",
6
+ "version": "1.2.72",
7
7
  "repository": "deduzzo/aziendasanitaria-utils",
8
8
  "description": "Un utility per gestire i flussi sanitari Siciliani e non solo..",
9
9
  "main": "index.js",
@@ -3,7 +3,8 @@ import {Assistito, DATI} from "../classi/Assistito.js";
3
3
  import moment from "moment";
4
4
  import _ from "lodash";
5
5
  import CryptHelper from "../CryptHelper.js";
6
- import {STATO_TS, snapshotDaSogei, classificaStatoTs, ASP_MESSINA} from "./statoAssistitoTs.js";
6
+ import {Ts} from "./Ts.js";
7
+ import {STATO_TS, snapshotDaSogei, classificaStatoTs, ASP_MESSINA, estraiCodiceAsl} from "./statoAssistitoTs.js";
7
8
 
8
9
  export class Nar2 {
9
10
  static LOGIN_URL = "https://nar2.regione.sicilia.it/services/index.php/api/login";
@@ -193,6 +194,10 @@ export class Nar2 {
193
194
  this._cryptData = (crtyptData.hasOwnProperty("KEY") && crtyptData.hasOwnProperty("IV"))
194
195
  ? crtyptData
195
196
  : null;
197
+ // Conservato per il fallback legacy verso il portale TS (scraping) quando
198
+ // l'endpoint Sogei è irraggiungibile: il Ts viene creato lazy da #tsScraper().
199
+ this._impostazioni = impostazioniServiziTerzi;
200
+ this._ts = null;
196
201
  }
197
202
 
198
203
  /**
@@ -695,10 +700,11 @@ export class Nar2 {
695
700
 
696
701
  // 3) Scelta medico corrente (per revoca_scelta_precedente)
697
702
  // La scelta attiva si trova in storico_medici[*] con pm_fstato="A" e pm_dt_disable=null.
698
- // Per una "variazione" il server richiede SEMPRE i dati di revoca della scelta precedente
699
- // (errore 400 "Dati di revoca della scelta precedente mancanti o non validi"); se non c'è
700
- // una scelta strettamente attiva (es. dopo una revoca standalone) si usa il rapporto più
701
- // recente in storico (pm_id più alto). Per una vera prima iscrizione passare forzaSenzaRevoca:true.
703
+ // Il server richiede SEMPRE il blocco revoca_scelta_precedente (errore 400/500 se
704
+ // assente), ANCHE in prima iscrizione: se non c'è un rapporto precedente revoca_id = ""
705
+ // (come il portale). Se non c'è una scelta strettamente attiva (es. dopo una revoca
706
+ // standalone) si usa il rapporto più recente in storico (pm_id più alto).
707
+ // forzaSenzaRevoca:true omette del tutto il blocco (sconsigliato: il server lo rifiuta).
702
708
  let revocaPrecedente = null;
703
709
  if (!forzaSenzaRevoca) {
704
710
  const storico = Array.isArray(fullData.storico_medici) ? fullData.storico_medici : [];
@@ -707,15 +713,19 @@ export class Nar2 {
707
713
  sceltaPrecedente = storico.slice().sort((a, b) => (parseInt(b?.pm_id, 10) || 0) - (parseInt(a?.pm_id, 10) || 0))[0];
708
714
  }
709
715
  const pmIdCorrente = sceltaPrecedente?.pm_id ?? null;
710
- if (pmIdCorrente) {
711
- revocaPrecedente = {
712
- pm_dt_disable: dataRevoca,
713
- dm_dt_ins_revoca: dataInsRevoca,
714
- dm_motivo_revoca: motivoRevoca, // popolato sotto se null
715
- dm_tipoop_revoca: tipoOperazioneRevoca,
716
- revoca_id: typeof pmIdCorrente === "string" ? parseInt(pmIdCorrente, 10) : pmIdCorrente
717
- };
718
- }
716
+ // Il server VUOLE SEMPRE il blocco revoca_scelta_precedente, anche in PRIMA
717
+ // ISCRIZIONE (nessun medico precedente): in quel caso revoca_id = "" (stringa
718
+ // vuota), esattamente come fa il portale. Ometterlo del tutto → 500 "Server Error"
719
+ // (verificato confrontando la POST reale del portale con la nostra).
720
+ revocaPrecedente = {
721
+ pm_dt_disable: dataRevoca,
722
+ dm_dt_ins_revoca: dataInsRevoca,
723
+ dm_motivo_revoca: motivoRevoca, // popolato sotto se null (default A04)
724
+ dm_tipoop_revoca: tipoOperazioneRevoca,
725
+ revoca_id: pmIdCorrente != null
726
+ ? (typeof pmIdCorrente === "string" ? parseInt(pmIdCorrente, 10) : pmIdCorrente)
727
+ : ""
728
+ };
719
729
  }
720
730
 
721
731
  // 4) Situazione assistenziale + motivo + tipo operazione (dal catalogo)
@@ -759,11 +769,19 @@ export class Nar2 {
759
769
  return {ok: false, dryRun, payload: null, response: null, error: "Impossibile determinare il motivo della scelta"};
760
770
  }
761
771
 
762
- // Allinea il motivo revoca se non specificato
772
+ // Motivo della revoca della scelta precedente: il portale usa A04 "Cambio medico"
773
+ // (anche in prima iscrizione, con revoca_id vuoto) — NON il motivo della nuova scelta.
774
+ // L'override esplicito (config.motivoRevoca, es. A17 in deroga) resta rispettato.
763
775
  if (revocaPrecedente && !revocaPrecedente.dm_motivo_revoca) {
764
- revocaPrecedente.dm_motivo_revoca = motivoFinale;
776
+ revocaPrecedente.dm_motivo_revoca = Nar2.MOTIVO_CAMBIO_MEDICO;
765
777
  }
766
778
 
779
+ // Pediatri: "data fine proroga" (assistenza fino a 16 anni). Il portale la imposta a
780
+ // data di nascita + 16 anni; se non fornita la deriviamo (per gli MMG resta null).
781
+ const fineProrogaPed = tipoMedico === Nar2.PEDIATRA
782
+ ? (dataFineProrogaPed ?? moment(fullData.pz_dt_nas, "YYYY-MM-DD HH:mm:ss").add(16, "years").format("YYYY-MM-DD"))
783
+ : null;
784
+
767
785
  // 5) Payload
768
786
  const payload = {
769
787
  data: {
@@ -781,7 +799,7 @@ export class Nar2 {
781
799
  dm_ambito_scelta: ambitoScelta?.toString(),
782
800
  dm_motivo_scelta: motivoFinale,
783
801
  dm_tipoop_scelta: tipoOpFinale,
784
- dm_dt_fine_proroga_ped: tipoMedico === Nar2.PEDIATRA ? dataFineProrogaPed : null,
802
+ dm_dt_fine_proroga_ped: fineProrogaPed,
785
803
  dm_motivo_pror_scad_ped: tipoMedico === Nar2.PEDIATRA ? motivoProrogaPed : null
786
804
  }
787
805
  };
@@ -2839,6 +2857,174 @@ export class Nar2 {
2839
2857
  return {ok, unico, documenti: risultati};
2840
2858
  }
2841
2859
 
2860
+ // Crea lazy lo scraper del portale TS (legacy) dalla stessa ImpostazioniServiziTerzi.
2861
+ // Usato come fallback quando l'endpoint NAR2/Sogei è irraggiungibile.
2862
+ #tsScraper() {
2863
+ if (!this._ts) this._ts = new Ts(this._impostazioni);
2864
+ return this._ts;
2865
+ }
2866
+
2867
+ // MAPPER UNICO p801 → Assistito (estratto dal ramo di successo di
2868
+ // getDatiAssistitoFromCfSuSogeiNew, logica INVARIATA). Usato sia dalla risposta Sogei
2869
+ // sia dal fallback legacy, così la mappatura è identica per entrambe le fonti.
2870
+ #popolaAssistitoDaP801(assistito, data, cf, envelope) {
2871
+ const nullArray = (d) => (Array.isArray(d) && d.length === 0 ? "" : d);
2872
+ const deceduto = data?.p801descrizioneCodiceTipoAssistito?.toLowerCase().includes("deceduto") ?? false;
2873
+ const asp = nullArray(data.p801codiceRegioneResidenzaAsl) + " - " +
2874
+ nullArray(data.p801descrizioneRegioneResidenzaAsl) + " " +
2875
+ nullArray(data.p801codiceAslResidenzaAsl) + " - " +
2876
+ nullArray(data.p801descrizioneAslResidenzaAsl).trim();
2877
+
2878
+ assistito.setTs(DATI.CF, cf.toUpperCase().trim());
2879
+ assistito.setTs(DATI.CF_NORMALIZZATO, nullArray(data.p801codiceFiscale));
2880
+ assistito.setTs(DATI.COGNOME, nullArray(data.p801cognome));
2881
+ assistito.setTs(DATI.NOME, nullArray(data.p801nome));
2882
+ assistito.setTs(DATI.SESSO, nullArray(data.p801sesso));
2883
+ assistito.setTs(DATI.DATA_NASCITA, nullArray(data.p801dataNascita));
2884
+ assistito.setTs(DATI.COMUNE_NASCITA, nullArray(data.p801comuneNascita));
2885
+ assistito.setTs(DATI.COD_COMUNE_NASCITA, nullArray(data.p801codiceComuneNascita));
2886
+ assistito.setTs(DATI.COD_ISTAT_COMUNE_NASCITA, nullArray(data.p801codiceistatiComuneNascita));
2887
+ assistito.setTs(DATI.INDIRIZZO_RESIDENZA, nullArray(data.p801recapitoTessera));
2888
+ assistito.setTs(DATI.COD_COMUNE_RESIDENZA, nullArray(data.p801codiceComuneResidenza));
2889
+ assistito.setTs(DATI.COD_ISTAT_COMUNE_RESIDENZA, nullArray(data.p801codiceistatiComuneResidenza));
2890
+ assistito.setTs(DATI.ASP, asp !== " - - " ? asp : "");
2891
+ assistito.setTs(DATI.MMG_CF, nullArray(data.p801codiceFiscaleMedico));
2892
+ assistito.setTs(DATI.MMG_COGNOME, nullArray(data.p801cognomeMedico));
2893
+ assistito.setTs(DATI.MMG_NOME, nullArray(data.p801nomeMedico));
2894
+ assistito.setTs(DATI.MMG_DATA_SCELTA, nullArray(data.p801dataAssociazioneMedico));
2895
+ assistito.setTs(DATI.SSN_TIPO_ASSISTITO, nullArray(data.p801descrizioneCodiceTipoAssistito));
2896
+ assistito.setTs(DATI.SSN_INIZIO_ASSISTENZA, nullArray(data.p801dataInizioValidita));
2897
+ assistito.setTs(DATI.SSN_FINE_ASSISTENZA, data.p801dataFineValidita === "31/12/9999" ? "illimitata" : nullArray(data.p801dataFineValidita));
2898
+ assistito.setTs(DATI.SSN_MOTIVAZIONE_FINE_ASSISTENZA, data.p801dataFineValidita !== "31/12/9999" ? nullArray(data.p801motivazioneFineValidita) : null);
2899
+ assistito.setTs(DATI.SSN_NUMERO_TESSERA, nullArray(data.p801numeroTessera));
2900
+ assistito.setTs(DATI.DATA_DECESSO, deceduto ? nullArray(data.p801dataDecesso) : null);
2901
+ assistito.okTs = true;
2902
+ assistito.fullDataTs = envelope;
2903
+ }
2904
+
2905
+ // FALLBACK LEGACY: quando Sogei è irraggiungibile, ricostruisce i campi p801* dallo
2906
+ // scraping del portale TS (getDatiAnagraficiAssistito + getIndirizzoUltimaTessera) e
2907
+ // li passa al MAPPER esistente (#popolaAssistitoDaP801), restituendo la STESSA shape
2908
+ // { ok, fullData:{status,data:{p801*},_fonte:"legacy-ts"}, data:Assistito }.
2909
+ // Ritorna null se anche lo scraping è irraggiungibile (→ il chiamante torna ok:false).
2910
+ async #fallbackLegacyTsToP801(cf, assistito) {
2911
+ const cfUp = cf?.toUpperCase()?.trim();
2912
+ if (!cfUp || !this._impostazioni) return null;
2913
+ const ts = this.#tsScraper();
2914
+
2915
+ // 1) Anagrafica + dati assistenza (pagina "Visualizza Assistito")
2916
+ const anagRes = await ts.getDatiAnagraficiAssistito({codiceFiscale: cfUp});
2917
+ if (anagRes?.error) {
2918
+ const msg = (anagRes.message || "").toString();
2919
+ // "non trovato/censito" = esito VALIDO (non un guasto): ok:true + okTs:false,
2920
+ // così resta la distinzione esiste=false (verificaAssistitoTsNar2).
2921
+ if (/non\s+trovat|nessun|non\s+censit|non\s+esiste/i.test(msg)) {
2922
+ assistito.okTs = false;
2923
+ assistito.erroreTs = msg || "Assistito non presente sul Sistema TS (portale)";
2924
+ return {ok: true, fullData: {status: false, _fonte: "legacy-ts"}, data: assistito};
2925
+ }
2926
+ return null; // login/rete: scraping irraggiungibile → ok:false a monte
2927
+ }
2928
+ const a = anagRes.data || {};
2929
+ const s = (x) => (x == null ? "" : String(x).trim());
2930
+
2931
+ // 2) Recapito + numero tessera (best-effort: non blocca se assente)
2932
+ let indirizzo = "", numeroTessera = "";
2933
+ try {
2934
+ const tessRes = await ts.getIndirizzoUltimaTessera({codiceFiscale: cfUp});
2935
+ if (!tessRes?.error && tessRes.data) {
2936
+ indirizzo = s(tessRes.data.indirizzo);
2937
+ numeroTessera = s(tessRes.data.numero_tessera);
2938
+ }
2939
+ } catch { /* recapito non disponibile */ }
2940
+
2941
+ // 3) Parsing dei dati grezzi in campi p801
2942
+ // CF medico dal campo "medico" = "COGNOME NOME - CFMEDICO"
2943
+ const medicoStr = s(a.medico);
2944
+ const mCf = medicoStr.match(/\b([A-Z]{6}\d{2}[A-Z]\d{2}[A-Z]\d{3}[A-Z])\b/i);
2945
+ const cfMedico = mCf ? mCf[1].toUpperCase() : "";
2946
+ const cognomeMedico = medicoStr ? medicoStr.replace(/\s*-\s*[A-Z0-9]{16}\s*$/i, "").trim() : "";
2947
+ // codice tipo assistito da "1 - assistito in ASL di residenza" (NB: inaffidabile per neonati)
2948
+ const tipoDesc = s(a.tipo_assistito);
2949
+ const mTipo = tipoDesc.match(/^\s*([A-Z0-9]+)\s*-/i);
2950
+ const codTipo = mTipo ? mTipo[1] : "";
2951
+ // ASL/regione da "190 - Sicilia 205 - ASP MESSINA"
2952
+ const aslStr = s(a.asl) || s(a.campi?.["ASL"]);
2953
+ const fonteAtStr = s(a.campi?.["Fonte AT"]) || aslStr;
2954
+ const codAsl = estraiCodiceAsl(aslStr) || "";
2955
+ const codAslAT = estraiCodiceAsl(fonteAtStr) || "";
2956
+ const mReg = aslStr.match(/(\d{3})\s*-\s*([^0-9]+?)\s+(\d{3})\s*-\s*(.+)$/);
2957
+ const codReg = mReg ? mReg[1] : "";
2958
+ const descReg = mReg ? mReg[2].trim() : "";
2959
+ const descAsl = mReg ? mReg[4].trim() : "";
2960
+ // data fine validità: "Illimitata"/vuoto → sentinella 31/12/9999 (come Sogei)
2961
+ const fineRaw = s(a.data_fine_validita);
2962
+ const dataFineValidita = (fineRaw === "" || /illimitat/i.test(fineRaw)) ? "31/12/9999" : fineRaw;
2963
+ // comune di nascita: catastale (Belfiore) dal CF stesso (es. ...F158... → "F158")
2964
+ const belfiore = /^[A-Z0-9]{16}$/i.test(cfUp) ? cfUp.substring(11, 15).toUpperCase() : "";
2965
+
2966
+ // 4) Reverse-lookup del comune di RESIDENZA dal recapito (CAP, poi nome) → id NAR2 +
2967
+ // catastale. Best-effort: usa l'endpoint comuni NAR2, disponibile solo se NAR2 è
2968
+ // raggiungibile (serve all'inserimento nuovo assistito, che richiede comunque NAR2;
2969
+ // il cambio medico usa solo il recapito testuale, già disponibile).
2970
+ let comuneResidenzaId = "", catastaleResidenza = "";
2971
+ const mRecap = indirizzo.match(/(\d{5})\s+(.+?)\s+\(([A-Za-z]{2})\)\s*$/);
2972
+ const capRes = mRecap ? mRecap[1] : (indirizzo.match(/\b(\d{5})\b/)?.[1] ?? "");
2973
+ const nomeRes = mRecap ? mRecap[2].trim() : "";
2974
+ const prendiComune = (cm) => {
2975
+ if (cm?.ok && cm.comune?.id != null) {
2976
+ comuneResidenzaId = cm.comune.id.toString();
2977
+ catastaleResidenza = (cm.comune.catastale ?? "").toString();
2978
+ return true;
2979
+ }
2980
+ return false;
2981
+ };
2982
+ try {
2983
+ const cm = capRes ? await this.getComuneNar2({cap: capRes}) : null;
2984
+ if (!prendiComune(cm) && nomeRes) prendiComune(await this.getComuneNar2({nome: nomeRes}));
2985
+ } catch { /* lookup comune non disponibile (NAR2 irraggiungibile) */ }
2986
+
2987
+ // 5) Ricostruzione p801* (stessi nomi consumati dal mapper e dai chiamanti)
2988
+ const p801 = {
2989
+ p801codiceFiscale: s(a.codice_fiscale) || cfUp,
2990
+ p801cognome: s(a.cognome),
2991
+ p801nome: s(a.nome),
2992
+ p801sesso: s(a.sesso),
2993
+ p801dataNascita: s(a.data_nascita),
2994
+ p801comuneNascita: s(a.comune_nascita),
2995
+ p801codiceComuneNascita: belfiore,
2996
+ p801codiceistatiComuneNascita: "",
2997
+ p801codiceistatiComuneId: "",
2998
+ p801recapitoTessera: indirizzo,
2999
+ p801codiceComuneResidenza: catastaleResidenza,
3000
+ p801codiceistatiComuneResidenza: "",
3001
+ p801ComuneResidenzaId: comuneResidenzaId,
3002
+ p801codiceFiscaleMedico: cfMedico,
3003
+ p801cognomeMedico: cognomeMedico,
3004
+ p801nomeMedico: "",
3005
+ p801dataAssociazioneMedico: s(a.data_associazione_medico),
3006
+ p801descrizioneCodiceTipoAssistito: tipoDesc,
3007
+ p801codiceTipoAssistito: codTipo,
3008
+ p801dataInizioValidita: s(a.data_inizio_validita),
3009
+ p801dataFineValidita: dataFineValidita,
3010
+ p801motivazioneFineValidita: "",
3011
+ p801numeroTessera: numeroTessera,
3012
+ p801dataDecesso: "",
3013
+ p801codiceAslAssistenza: codAsl,
3014
+ p801codiceRegioneAssistenza: codReg,
3015
+ p801codiceAslResidenzaAsl: codAsl,
3016
+ p801codiceAslResidenzaAT: codAslAT,
3017
+ p801codiceRegioneResidenzaAsl: codReg,
3018
+ p801descrizioneRegioneResidenzaAsl: descReg,
3019
+ p801descrizioneAslResidenzaAsl: descAsl,
3020
+ p801listaMessaggi: [],
3021
+ };
3022
+ const envelope = {status: "true", data: p801, listaMessaggi: {}, _fonte: "legacy-ts"};
3023
+ this.#popolaAssistitoDaP801(assistito, p801, cfUp, envelope);
3024
+ assistito.erroreTs = null;
3025
+ return {ok: true, fullData: envelope, data: assistito};
3026
+ }
3027
+
2842
3028
  async getDatiAssistitoFromCfSuSogeiNew(cf, assistito = null, fallback = false) {
2843
3029
  let ok = false;
2844
3030
  let risultato = null; // valore di ritorno del ramo di successo (fix: prima non veniva mai ritornato)
@@ -2891,38 +3077,9 @@ export class Nar2 {
2891
3077
  };
2892
3078
  } else {
2893
3079
  ok = true;
2894
- const deceduto = out.data.data?.p801descrizioneCodiceTipoAssistito?.toLowerCase().includes("deceduto") ?? false;
2895
- const asp = nullArray(out.data.data.p801codiceRegioneResidenzaAsl) + " - " +
2896
- nullArray(out.data.data.p801descrizioneRegioneResidenzaAsl) + " " +
2897
- nullArray(out.data.data.p801codiceAslResidenzaAsl) + " - " +
2898
- nullArray(out.data.data.p801descrizioneAslResidenzaAsl).trim();
2899
-
2900
- // Popoliamo i dati in fromTs usando i setter
2901
- assistito.setTs(DATI.CF, cf.toUpperCase().trim());
2902
- assistito.setTs(DATI.CF_NORMALIZZATO, nullArray(out.data.data.p801codiceFiscale));
2903
- assistito.setTs(DATI.COGNOME, nullArray(out.data.data.p801cognome));
2904
- assistito.setTs(DATI.NOME, nullArray(out.data.data.p801nome));
2905
- assistito.setTs(DATI.SESSO, nullArray(out.data.data.p801sesso));
2906
- assistito.setTs(DATI.DATA_NASCITA, nullArray(out.data.data.p801dataNascita));
2907
- assistito.setTs(DATI.COMUNE_NASCITA, nullArray(out.data.data.p801comuneNascita));
2908
- assistito.setTs(DATI.COD_COMUNE_NASCITA, nullArray(out.data.data.p801codiceComuneNascita));
2909
- assistito.setTs(DATI.COD_ISTAT_COMUNE_NASCITA, nullArray(out.data.data.p801codiceistatiComuneNascita));
2910
- assistito.setTs(DATI.INDIRIZZO_RESIDENZA, nullArray(out.data.data.p801recapitoTessera));
2911
- assistito.setTs(DATI.COD_COMUNE_RESIDENZA, nullArray(out.data.data.p801codiceComuneResidenza));
2912
- assistito.setTs(DATI.COD_ISTAT_COMUNE_RESIDENZA, nullArray(out.data.data.p801codiceistatiComuneResidenza));
2913
- assistito.setTs(DATI.ASP, asp !== " - - " ? asp : "");
2914
- assistito.setTs(DATI.MMG_CF, nullArray(out.data.data.p801codiceFiscaleMedico));
2915
- assistito.setTs(DATI.MMG_COGNOME, nullArray(out.data.data.p801cognomeMedico));
2916
- assistito.setTs(DATI.MMG_NOME, nullArray(out.data.data.p801nomeMedico));
2917
- assistito.setTs(DATI.MMG_DATA_SCELTA, nullArray(out.data.data.p801dataAssociazioneMedico));
2918
- assistito.setTs(DATI.SSN_TIPO_ASSISTITO, nullArray(out.data.data.p801descrizioneCodiceTipoAssistito));
2919
- assistito.setTs(DATI.SSN_INIZIO_ASSISTENZA, nullArray(out.data.data.p801dataInizioValidita));
2920
- assistito.setTs(DATI.SSN_FINE_ASSISTENZA, out.data.data.p801dataFineValidita === "31/12/9999" ? "illimitata" : nullArray(out.data.data.p801dataFineValidita));
2921
- assistito.setTs(DATI.SSN_MOTIVAZIONE_FINE_ASSISTENZA, out.data.data.p801dataFineValidita !== "31/12/9999" ? nullArray(out.data.data.p801motivazioneFineValidita) : null);
2922
- assistito.setTs(DATI.SSN_NUMERO_TESSERA, nullArray(out.data.data.p801numeroTessera));
2923
- assistito.setTs(DATI.DATA_DECESSO, deceduto ? nullArray(out.data.data.p801dataDecesso) : null);
2924
- assistito.okTs = true;
2925
- assistito.fullDataTs = out.data;
3080
+ // Mapping p801 Assistito centralizzato in #popolaAssistitoDaP801, riusato
3081
+ // IDENTICO dal fallback legacy TS (la mappatura resta UNA SOLA e invariata).
3082
+ this.#popolaAssistitoDaP801(assistito, out.data.data, cf, out.data);
2926
3083
  risultato = {
2927
3084
  ok: true,
2928
3085
  fullData: out.data,
@@ -2935,7 +3092,16 @@ export class Nar2 {
2935
3092
  }
2936
3093
  }
2937
3094
  if (!ok) {
2938
- console.log(`[getDatiAssistitoFromCfSuSogeiNew] Sogei fallito dopo ${this._maxRetry} tentativi per CF: ${cf?.substring(0, 6)}***`);
3095
+ console.log(`[getDatiAssistitoFromCfSuSogeiNew] Sogei fallito dopo ${this._maxRetry} tentativi per CF: ${cf?.substring(0, 6)}*** — provo fallback legacy TS`);
3096
+ try {
3097
+ const fb = await this.#fallbackLegacyTsToP801(cf, assistito);
3098
+ if (fb) {
3099
+ console.log(`[getDatiAssistitoFromCfSuSogeiNew] Fallback legacy TS riuscito (fonte: scraping portale)`);
3100
+ return fb;
3101
+ }
3102
+ } catch (e) {
3103
+ console.log(`[getDatiAssistitoFromCfSuSogeiNew] Fallback legacy TS fallito:`, e.message);
3104
+ }
2939
3105
  return {
2940
3106
  ok: false,
2941
3107
  fullData: null,
@@ -2977,9 +3143,34 @@ export class Nar2 {
2977
3143
  let p801 = sogeiData;
2978
3144
  if (!p801) {
2979
3145
  const sogei = await this.getDatiAssistitoFromCfSuSogeiNew(codiceFiscale);
2980
- if (!sogei?.ok) {
2981
- const esito = classificaStatoTs({fonte: "sogei", trovato: false}, {codiceAslAsp});
2982
- return {ok: false, cf: codiceFiscale, ...esito, anagrafica: null, raw: null, error: "Sistema TS (Sogei) non raggiungibile"};
3146
+ // Sogei irraggiungibile OPPURE dato ricostruito dal fallback legacy: la
3147
+ // classificazione dello stato passa dallo snapshot del PORTALE (Ts.getStatoAssistitoTs
3148
+ // snapshotDaPortale), che NON dipende da p801codiceTipoAssistito inaffidabile dal
3149
+ // portale (per i neonati mostra "3" → falso TRASFERITO). Così i neonati restano corretti.
3150
+ if (!sogei?.ok || sogei?.fullData?._fonte === "legacy-ts") {
3151
+ try {
3152
+ const st = await this.#tsScraper().getStatoAssistitoTs({codiceFiscale, codiceAslAsp});
3153
+ if (st?.ok) {
3154
+ const cval = (k) => { const x = st.coppie?.[k]; return x == null ? "" : String(x).trim(); };
3155
+ return {
3156
+ ok: true, cf: codiceFiscale,
3157
+ stato: st.stato, etichetta: st.etichetta, daFarRientrare: st.daFarRientrare,
3158
+ residenzaInAsp: st.residenzaInAsp, aslAssistenzaInAsp: st.aslAssistenzaInAsp,
3159
+ haMedico: st.haMedico, codiceTipoAssistito: st.codiceTipoAssistito,
3160
+ descrizioneTipoAssistito: st.descrizioneTipoAssistito,
3161
+ anagrafica: {cognome: cval("Cognome"), nome: cval("Nome"), sesso: cval("Sesso"), dataNascita: cval("Data Nascita")},
3162
+ fonte: "portale", evidenze: st.evidenze ?? [], raw: st.coppie ?? null
3163
+ };
3164
+ }
3165
+ } catch (e) {
3166
+ console.log(`[getStatoAssistitoTs] Fallback portale fallito:`, e.message);
3167
+ }
3168
+ // Portale non disponibile: se Sogei è davvero irraggiungibile → errore.
3169
+ // Se invece avevamo un dato legacy ricostruito, proseguo best-effort con esso.
3170
+ if (!sogei?.ok) {
3171
+ const esito = classificaStatoTs({fonte: "sogei", trovato: false}, {codiceAslAsp});
3172
+ return {ok: false, cf: codiceFiscale, ...esito, anagrafica: null, raw: null, error: "Sistema TS (Sogei) non raggiungibile"};
3173
+ }
2983
3174
  }
2984
3175
  if (sogei?.data?.okTs !== true) {
2985
3176
  const esito = classificaStatoTs({fonte: "sogei", trovato: false}, {codiceAslAsp});
@@ -128,7 +128,7 @@ export function snapshotDaPortale({coppie = {}, menuVoci = [], trovato = true, d
128
128
  }
129
129
 
130
130
  // "190 - Sicilia 205 - ASP MESSINA" → "205" (ultimo codice numerico = ASL)
131
- function estraiCodiceAsl(s) {
131
+ export function estraiCodiceAsl(s) {
132
132
  const m = String(s || "").match(/(\d{3})\s*-\s*ASP|(\d{3})(?!.*\d{3})/i);
133
133
  if (!m) return null;
134
134
  return m[1] || m[2] || null;