aziendasanitaria-utils 1.2.71 → 1.2.73

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.73",
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
  /**
@@ -231,10 +236,23 @@ export class Nar2 {
231
236
  password: this._password
232
237
  }, {
233
238
  headers: {
234
- 'Content-Type': 'application/json'
239
+ 'Content-Type': 'application/json',
240
+ // NAR2 valida l'header Origin sul login (controllo CSRF): SENZA,
241
+ // risponde 200 con body "error" e nessun token. Nel browser è
242
+ // automatico; da Node lo impostiamo a mano. (Diagnosi 13/06/2026.)
243
+ 'Origin': new URL(Nar2.LOGIN_URL).origin
235
244
  }
236
245
  });
237
- Nar2.#token = out.data.accessToken;
246
+ Nar2.#token = (out.data && typeof out.data === "object") ? out.data.accessToken : undefined;
247
+ if (!Nar2.#token) {
248
+ // Login andato in 200 ma SENZA token (es. body letterale "error"):
249
+ // il login username/password potrebbe non essere più attivo (NAR2 via
250
+ // SPID). Mostro l'indicazione del server e ritorno undefined: il
251
+ // chiamante può richiedere un token esterno (setTokenEsterno).
252
+ const ind = typeof out.data === "string" ? out.data : JSON.stringify(out.data ?? null);
253
+ console.log(`[getToken] Login NAR2 SENZA accessToken (HTTP ${out.status}, risposta: ${String(ind).slice(0, 200)}). ` +
254
+ `Login username/password non attivo? Imposta un token con Nar2.setTokenEsterno(<bearer SPID>).`);
255
+ }
238
256
  return Nar2.#token;
239
257
  } else {
240
258
  const data = {username: this._username, password: this._password, type: "token"};
@@ -451,14 +469,17 @@ export class Nar2 {
451
469
  * @returns {Promise<{ok:boolean, data:Array<Object>|null, error?:string}>}
452
470
  */
453
471
  async getMotiviScelta(saId, config = {}) {
454
- const {soloScelta = true, semplifica = false, retryVuoto = 3} = config;
472
+ // Dropdown non critico: l'endpoint motiviOperazioneFiltered è spesso lento/intermittente.
473
+ // Timeout breve e pochi retry → se non risponde fallisce in fretta e il chiamante usa il
474
+ // default, invece di restare appeso (prima fino a ~4×10 tentativi senza timeout).
475
+ const {soloScelta = true, semplifica = false, retryVuoto = 0, timeout = 8000, maxRetry = 1} = config;
455
476
  let data = null;
456
477
  let lastRes = null;
457
478
  for (let i = 0; i <= retryVuoto; i++) {
458
479
  // NB: endpoint con envelope {status, result} → modalità default (unwrap di result),
459
480
  // a differenza di getMotiviOperazione il cui endpoint risponde con array nudo.
460
481
  const res = await this.#getDataFromUrlIdOrParams(Nar2.MOTIVI_OPERAZIONE_FILTERED, {
461
- replaceFromUrl: {sa_id: saId}
482
+ replaceFromUrl: {sa_id: saId}, timeout, maxRetry
462
483
  });
463
484
  lastRes = res;
464
485
  if (res.ok && Array.isArray(res.data)) {
@@ -695,10 +716,11 @@ export class Nar2 {
695
716
 
696
717
  // 3) Scelta medico corrente (per revoca_scelta_precedente)
697
718
  // 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.
719
+ // Il server richiede SEMPRE il blocco revoca_scelta_precedente (errore 400/500 se
720
+ // assente), ANCHE in prima iscrizione: se non c'è un rapporto precedente revoca_id = ""
721
+ // (come il portale). Se non c'è una scelta strettamente attiva (es. dopo una revoca
722
+ // standalone) si usa il rapporto più recente in storico (pm_id più alto).
723
+ // forzaSenzaRevoca:true omette del tutto il blocco (sconsigliato: il server lo rifiuta).
702
724
  let revocaPrecedente = null;
703
725
  if (!forzaSenzaRevoca) {
704
726
  const storico = Array.isArray(fullData.storico_medici) ? fullData.storico_medici : [];
@@ -707,15 +729,19 @@ export class Nar2 {
707
729
  sceltaPrecedente = storico.slice().sort((a, b) => (parseInt(b?.pm_id, 10) || 0) - (parseInt(a?.pm_id, 10) || 0))[0];
708
730
  }
709
731
  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
- }
732
+ // Il server VUOLE SEMPRE il blocco revoca_scelta_precedente, anche in PRIMA
733
+ // ISCRIZIONE (nessun medico precedente): in quel caso revoca_id = "" (stringa
734
+ // vuota), esattamente come fa il portale. Ometterlo del tutto → 500 "Server Error"
735
+ // (verificato confrontando la POST reale del portale con la nostra).
736
+ revocaPrecedente = {
737
+ pm_dt_disable: dataRevoca,
738
+ dm_dt_ins_revoca: dataInsRevoca,
739
+ dm_motivo_revoca: motivoRevoca, // popolato sotto se null (default A04)
740
+ dm_tipoop_revoca: tipoOperazioneRevoca,
741
+ revoca_id: pmIdCorrente != null
742
+ ? (typeof pmIdCorrente === "string" ? parseInt(pmIdCorrente, 10) : pmIdCorrente)
743
+ : ""
744
+ };
719
745
  }
720
746
 
721
747
  // 4) Situazione assistenziale + motivo + tipo operazione (dal catalogo)
@@ -759,11 +785,19 @@ export class Nar2 {
759
785
  return {ok: false, dryRun, payload: null, response: null, error: "Impossibile determinare il motivo della scelta"};
760
786
  }
761
787
 
762
- // Allinea il motivo revoca se non specificato
788
+ // Motivo della revoca della scelta precedente: il portale usa A04 "Cambio medico"
789
+ // (anche in prima iscrizione, con revoca_id vuoto) — NON il motivo della nuova scelta.
790
+ // L'override esplicito (config.motivoRevoca, es. A17 in deroga) resta rispettato.
763
791
  if (revocaPrecedente && !revocaPrecedente.dm_motivo_revoca) {
764
- revocaPrecedente.dm_motivo_revoca = motivoFinale;
792
+ revocaPrecedente.dm_motivo_revoca = Nar2.MOTIVO_CAMBIO_MEDICO;
765
793
  }
766
794
 
795
+ // Pediatri: "data fine proroga" (assistenza fino a 16 anni). Il portale la imposta a
796
+ // data di nascita + 16 anni; se non fornita la deriviamo (per gli MMG resta null).
797
+ const fineProrogaPed = tipoMedico === Nar2.PEDIATRA
798
+ ? (dataFineProrogaPed ?? moment(fullData.pz_dt_nas, "YYYY-MM-DD HH:mm:ss").add(16, "years").format("YYYY-MM-DD"))
799
+ : null;
800
+
767
801
  // 5) Payload
768
802
  const payload = {
769
803
  data: {
@@ -781,7 +815,7 @@ export class Nar2 {
781
815
  dm_ambito_scelta: ambitoScelta?.toString(),
782
816
  dm_motivo_scelta: motivoFinale,
783
817
  dm_tipoop_scelta: tipoOpFinale,
784
- dm_dt_fine_proroga_ped: tipoMedico === Nar2.PEDIATRA ? dataFineProrogaPed : null,
818
+ dm_dt_fine_proroga_ped: fineProrogaPed,
785
819
  dm_motivo_pror_scad_ped: tipoMedico === Nar2.PEDIATRA ? motivoProrogaPed : null
786
820
  }
787
821
  };
@@ -2010,7 +2044,9 @@ export class Nar2 {
2010
2044
  console.log("[getDatiAssistitoNar2FromCf] Eccezione durante recupero Nar2:", e.message);
2011
2045
  }
2012
2046
  if (datiAssistito && datiAssistito.ok) break;
2013
- else console.log("[getDatiAssistitoNar2FromCf] Errore Nar2, tentativi rimanenti:" + (retry - i));
2047
+ else console.log("[getDatiAssistitoNar2FromCf] Errore Nar2, tentativi rimanenti:" + (retry - i)
2048
+ + (datiIdAssistito?.error ? ` | indicazione: ${datiIdAssistito.error}` : "")
2049
+ + (datiIdAssistito?.errorDetail?.statusCode ? ` (HTTP ${datiIdAssistito.errorDetail.statusCode})` : ""));
2014
2050
  }
2015
2051
  if (datiAssistito && datiAssistito.ok) {
2016
2052
  try {
@@ -2171,6 +2207,8 @@ export class Nar2 {
2171
2207
  getParams = null,
2172
2208
  replaceFromUrl = null,
2173
2209
  rawResponse = false, // se true, accetta risposte non incapsulate in {status, result}
2210
+ timeout = 30000, // ms: evita hang indefiniti su endpoint NAR2 lenti/intermittenti
2211
+ maxRetry = this._maxRetry,
2174
2212
  } = config;
2175
2213
  let out = {ok: false, data: null, error: null, errorDetail: null};
2176
2214
  let ok = false;
@@ -2190,7 +2228,7 @@ export class Nar2 {
2190
2228
  finalUrl = finalUrl.replace(`{${key}}`, (value === null || typeof value === "undefined") ? "null" : value.toString());
2191
2229
  }
2192
2230
 
2193
- for (let i = 0; i < this._maxRetry && !ok; i++) {
2231
+ for (let i = 0; i < maxRetry && !ok; i++) {
2194
2232
  try {
2195
2233
  await this.getToken();
2196
2234
  let response = null;
@@ -2199,6 +2237,7 @@ export class Nar2 {
2199
2237
  headers: {
2200
2238
  Authorization: `Bearer ${Nar2.#token}`,
2201
2239
  },
2240
+ timeout,
2202
2241
  });
2203
2242
  else
2204
2243
  response = await axios.get(finalUrl, {
@@ -2206,6 +2245,7 @@ export class Nar2 {
2206
2245
  Authorization: `Bearer ${Nar2.#token}`,
2207
2246
  },
2208
2247
  params: params,
2248
+ timeout,
2209
2249
  });
2210
2250
 
2211
2251
  // Caso 1: risposta raw (array nudo o oggetto senza envelope)
@@ -2839,6 +2879,174 @@ export class Nar2 {
2839
2879
  return {ok, unico, documenti: risultati};
2840
2880
  }
2841
2881
 
2882
+ // Crea lazy lo scraper del portale TS (legacy) dalla stessa ImpostazioniServiziTerzi.
2883
+ // Usato come fallback quando l'endpoint NAR2/Sogei è irraggiungibile.
2884
+ #tsScraper() {
2885
+ if (!this._ts) this._ts = new Ts(this._impostazioni);
2886
+ return this._ts;
2887
+ }
2888
+
2889
+ // MAPPER UNICO p801 → Assistito (estratto dal ramo di successo di
2890
+ // getDatiAssistitoFromCfSuSogeiNew, logica INVARIATA). Usato sia dalla risposta Sogei
2891
+ // sia dal fallback legacy, così la mappatura è identica per entrambe le fonti.
2892
+ #popolaAssistitoDaP801(assistito, data, cf, envelope) {
2893
+ const nullArray = (d) => (Array.isArray(d) && d.length === 0 ? "" : d);
2894
+ const deceduto = data?.p801descrizioneCodiceTipoAssistito?.toLowerCase().includes("deceduto") ?? false;
2895
+ const asp = nullArray(data.p801codiceRegioneResidenzaAsl) + " - " +
2896
+ nullArray(data.p801descrizioneRegioneResidenzaAsl) + " " +
2897
+ nullArray(data.p801codiceAslResidenzaAsl) + " - " +
2898
+ nullArray(data.p801descrizioneAslResidenzaAsl).trim();
2899
+
2900
+ assistito.setTs(DATI.CF, cf.toUpperCase().trim());
2901
+ assistito.setTs(DATI.CF_NORMALIZZATO, nullArray(data.p801codiceFiscale));
2902
+ assistito.setTs(DATI.COGNOME, nullArray(data.p801cognome));
2903
+ assistito.setTs(DATI.NOME, nullArray(data.p801nome));
2904
+ assistito.setTs(DATI.SESSO, nullArray(data.p801sesso));
2905
+ assistito.setTs(DATI.DATA_NASCITA, nullArray(data.p801dataNascita));
2906
+ assistito.setTs(DATI.COMUNE_NASCITA, nullArray(data.p801comuneNascita));
2907
+ assistito.setTs(DATI.COD_COMUNE_NASCITA, nullArray(data.p801codiceComuneNascita));
2908
+ assistito.setTs(DATI.COD_ISTAT_COMUNE_NASCITA, nullArray(data.p801codiceistatiComuneNascita));
2909
+ assistito.setTs(DATI.INDIRIZZO_RESIDENZA, nullArray(data.p801recapitoTessera));
2910
+ assistito.setTs(DATI.COD_COMUNE_RESIDENZA, nullArray(data.p801codiceComuneResidenza));
2911
+ assistito.setTs(DATI.COD_ISTAT_COMUNE_RESIDENZA, nullArray(data.p801codiceistatiComuneResidenza));
2912
+ assistito.setTs(DATI.ASP, asp !== " - - " ? asp : "");
2913
+ assistito.setTs(DATI.MMG_CF, nullArray(data.p801codiceFiscaleMedico));
2914
+ assistito.setTs(DATI.MMG_COGNOME, nullArray(data.p801cognomeMedico));
2915
+ assistito.setTs(DATI.MMG_NOME, nullArray(data.p801nomeMedico));
2916
+ assistito.setTs(DATI.MMG_DATA_SCELTA, nullArray(data.p801dataAssociazioneMedico));
2917
+ assistito.setTs(DATI.SSN_TIPO_ASSISTITO, nullArray(data.p801descrizioneCodiceTipoAssistito));
2918
+ assistito.setTs(DATI.SSN_INIZIO_ASSISTENZA, nullArray(data.p801dataInizioValidita));
2919
+ assistito.setTs(DATI.SSN_FINE_ASSISTENZA, data.p801dataFineValidita === "31/12/9999" ? "illimitata" : nullArray(data.p801dataFineValidita));
2920
+ assistito.setTs(DATI.SSN_MOTIVAZIONE_FINE_ASSISTENZA, data.p801dataFineValidita !== "31/12/9999" ? nullArray(data.p801motivazioneFineValidita) : null);
2921
+ assistito.setTs(DATI.SSN_NUMERO_TESSERA, nullArray(data.p801numeroTessera));
2922
+ assistito.setTs(DATI.DATA_DECESSO, deceduto ? nullArray(data.p801dataDecesso) : null);
2923
+ assistito.okTs = true;
2924
+ assistito.fullDataTs = envelope;
2925
+ }
2926
+
2927
+ // FALLBACK LEGACY: quando Sogei è irraggiungibile, ricostruisce i campi p801* dallo
2928
+ // scraping del portale TS (getDatiAnagraficiAssistito + getIndirizzoUltimaTessera) e
2929
+ // li passa al MAPPER esistente (#popolaAssistitoDaP801), restituendo la STESSA shape
2930
+ // { ok, fullData:{status,data:{p801*},_fonte:"legacy-ts"}, data:Assistito }.
2931
+ // Ritorna null se anche lo scraping è irraggiungibile (→ il chiamante torna ok:false).
2932
+ async #fallbackLegacyTsToP801(cf, assistito) {
2933
+ const cfUp = cf?.toUpperCase()?.trim();
2934
+ if (!cfUp || !this._impostazioni) return null;
2935
+ const ts = this.#tsScraper();
2936
+
2937
+ // 1) Anagrafica + dati assistenza (pagina "Visualizza Assistito")
2938
+ const anagRes = await ts.getDatiAnagraficiAssistito({codiceFiscale: cfUp});
2939
+ if (anagRes?.error) {
2940
+ const msg = (anagRes.message || "").toString();
2941
+ // "non trovato/censito" = esito VALIDO (non un guasto): ok:true + okTs:false,
2942
+ // così resta la distinzione esiste=false (verificaAssistitoTsNar2).
2943
+ if (/non\s+trovat|nessun|non\s+censit|non\s+esiste/i.test(msg)) {
2944
+ assistito.okTs = false;
2945
+ assistito.erroreTs = msg || "Assistito non presente sul Sistema TS (portale)";
2946
+ return {ok: true, fullData: {status: false, _fonte: "legacy-ts"}, data: assistito};
2947
+ }
2948
+ return null; // login/rete: scraping irraggiungibile → ok:false a monte
2949
+ }
2950
+ const a = anagRes.data || {};
2951
+ const s = (x) => (x == null ? "" : String(x).trim());
2952
+
2953
+ // 2) Recapito + numero tessera (best-effort: non blocca se assente)
2954
+ let indirizzo = "", numeroTessera = "";
2955
+ try {
2956
+ const tessRes = await ts.getIndirizzoUltimaTessera({codiceFiscale: cfUp});
2957
+ if (!tessRes?.error && tessRes.data) {
2958
+ indirizzo = s(tessRes.data.indirizzo);
2959
+ numeroTessera = s(tessRes.data.numero_tessera);
2960
+ }
2961
+ } catch { /* recapito non disponibile */ }
2962
+
2963
+ // 3) Parsing dei dati grezzi in campi p801
2964
+ // CF medico dal campo "medico" = "COGNOME NOME - CFMEDICO"
2965
+ const medicoStr = s(a.medico);
2966
+ const mCf = medicoStr.match(/\b([A-Z]{6}\d{2}[A-Z]\d{2}[A-Z]\d{3}[A-Z])\b/i);
2967
+ const cfMedico = mCf ? mCf[1].toUpperCase() : "";
2968
+ const cognomeMedico = medicoStr ? medicoStr.replace(/\s*-\s*[A-Z0-9]{16}\s*$/i, "").trim() : "";
2969
+ // codice tipo assistito da "1 - assistito in ASL di residenza" (NB: inaffidabile per neonati)
2970
+ const tipoDesc = s(a.tipo_assistito);
2971
+ const mTipo = tipoDesc.match(/^\s*([A-Z0-9]+)\s*-/i);
2972
+ const codTipo = mTipo ? mTipo[1] : "";
2973
+ // ASL/regione da "190 - Sicilia 205 - ASP MESSINA"
2974
+ const aslStr = s(a.asl) || s(a.campi?.["ASL"]);
2975
+ const fonteAtStr = s(a.campi?.["Fonte AT"]) || aslStr;
2976
+ const codAsl = estraiCodiceAsl(aslStr) || "";
2977
+ const codAslAT = estraiCodiceAsl(fonteAtStr) || "";
2978
+ const mReg = aslStr.match(/(\d{3})\s*-\s*([^0-9]+?)\s+(\d{3})\s*-\s*(.+)$/);
2979
+ const codReg = mReg ? mReg[1] : "";
2980
+ const descReg = mReg ? mReg[2].trim() : "";
2981
+ const descAsl = mReg ? mReg[4].trim() : "";
2982
+ // data fine validità: "Illimitata"/vuoto → sentinella 31/12/9999 (come Sogei)
2983
+ const fineRaw = s(a.data_fine_validita);
2984
+ const dataFineValidita = (fineRaw === "" || /illimitat/i.test(fineRaw)) ? "31/12/9999" : fineRaw;
2985
+ // comune di nascita: catastale (Belfiore) dal CF stesso (es. ...F158... → "F158")
2986
+ const belfiore = /^[A-Z0-9]{16}$/i.test(cfUp) ? cfUp.substring(11, 15).toUpperCase() : "";
2987
+
2988
+ // 4) Reverse-lookup del comune di RESIDENZA dal recapito (CAP, poi nome) → id NAR2 +
2989
+ // catastale. Best-effort: usa l'endpoint comuni NAR2, disponibile solo se NAR2 è
2990
+ // raggiungibile (serve all'inserimento nuovo assistito, che richiede comunque NAR2;
2991
+ // il cambio medico usa solo il recapito testuale, già disponibile).
2992
+ let comuneResidenzaId = "", catastaleResidenza = "";
2993
+ const mRecap = indirizzo.match(/(\d{5})\s+(.+?)\s+\(([A-Za-z]{2})\)\s*$/);
2994
+ const capRes = mRecap ? mRecap[1] : (indirizzo.match(/\b(\d{5})\b/)?.[1] ?? "");
2995
+ const nomeRes = mRecap ? mRecap[2].trim() : "";
2996
+ const prendiComune = (cm) => {
2997
+ if (cm?.ok && cm.comune?.id != null) {
2998
+ comuneResidenzaId = cm.comune.id.toString();
2999
+ catastaleResidenza = (cm.comune.catastale ?? "").toString();
3000
+ return true;
3001
+ }
3002
+ return false;
3003
+ };
3004
+ try {
3005
+ const cm = capRes ? await this.getComuneNar2({cap: capRes}) : null;
3006
+ if (!prendiComune(cm) && nomeRes) prendiComune(await this.getComuneNar2({nome: nomeRes}));
3007
+ } catch { /* lookup comune non disponibile (NAR2 irraggiungibile) */ }
3008
+
3009
+ // 5) Ricostruzione p801* (stessi nomi consumati dal mapper e dai chiamanti)
3010
+ const p801 = {
3011
+ p801codiceFiscale: s(a.codice_fiscale) || cfUp,
3012
+ p801cognome: s(a.cognome),
3013
+ p801nome: s(a.nome),
3014
+ p801sesso: s(a.sesso),
3015
+ p801dataNascita: s(a.data_nascita),
3016
+ p801comuneNascita: s(a.comune_nascita),
3017
+ p801codiceComuneNascita: belfiore,
3018
+ p801codiceistatiComuneNascita: "",
3019
+ p801codiceistatiComuneId: "",
3020
+ p801recapitoTessera: indirizzo,
3021
+ p801codiceComuneResidenza: catastaleResidenza,
3022
+ p801codiceistatiComuneResidenza: "",
3023
+ p801ComuneResidenzaId: comuneResidenzaId,
3024
+ p801codiceFiscaleMedico: cfMedico,
3025
+ p801cognomeMedico: cognomeMedico,
3026
+ p801nomeMedico: "",
3027
+ p801dataAssociazioneMedico: s(a.data_associazione_medico),
3028
+ p801descrizioneCodiceTipoAssistito: tipoDesc,
3029
+ p801codiceTipoAssistito: codTipo,
3030
+ p801dataInizioValidita: s(a.data_inizio_validita),
3031
+ p801dataFineValidita: dataFineValidita,
3032
+ p801motivazioneFineValidita: "",
3033
+ p801numeroTessera: numeroTessera,
3034
+ p801dataDecesso: "",
3035
+ p801codiceAslAssistenza: codAsl,
3036
+ p801codiceRegioneAssistenza: codReg,
3037
+ p801codiceAslResidenzaAsl: codAsl,
3038
+ p801codiceAslResidenzaAT: codAslAT,
3039
+ p801codiceRegioneResidenzaAsl: codReg,
3040
+ p801descrizioneRegioneResidenzaAsl: descReg,
3041
+ p801descrizioneAslResidenzaAsl: descAsl,
3042
+ p801listaMessaggi: [],
3043
+ };
3044
+ const envelope = {status: "true", data: p801, listaMessaggi: {}, _fonte: "legacy-ts"};
3045
+ this.#popolaAssistitoDaP801(assistito, p801, cfUp, envelope);
3046
+ assistito.erroreTs = null;
3047
+ return {ok: true, fullData: envelope, data: assistito};
3048
+ }
3049
+
2842
3050
  async getDatiAssistitoFromCfSuSogeiNew(cf, assistito = null, fallback = false) {
2843
3051
  let ok = false;
2844
3052
  let risultato = null; // valore di ritorno del ramo di successo (fix: prima non veniva mai ritornato)
@@ -2851,7 +3059,10 @@ export class Nar2 {
2851
3059
  assistito = new Assistito();
2852
3060
  }
2853
3061
 
2854
- for (let i = 0; i < this._maxRetry && !ok; i++) {
3062
+ // Sogei va spesso in 500 lato server (consistente): bastano pochi tentativi prima di
3063
+ // passare al fallback legacy TS — inutile insistere 10 volte.
3064
+ const maxTentativiSogei = 2;
3065
+ for (let i = 0; i < maxTentativiSogei && !ok; i++) {
2855
3066
  try {
2856
3067
  await this.getToken({fallback});
2857
3068
  let out = null;
@@ -2891,38 +3102,9 @@ export class Nar2 {
2891
3102
  };
2892
3103
  } else {
2893
3104
  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;
3105
+ // Mapping p801 Assistito centralizzato in #popolaAssistitoDaP801, riusato
3106
+ // IDENTICO dal fallback legacy TS (la mappatura resta UNA SOLA e invariata).
3107
+ this.#popolaAssistitoDaP801(assistito, out.data.data, cf, out.data);
2926
3108
  risultato = {
2927
3109
  ok: true,
2928
3110
  fullData: out.data,
@@ -2930,12 +3112,25 @@ export class Nar2 {
2930
3112
  };
2931
3113
  }
2932
3114
  } catch (e) {
2933
- console.log(`[getDatiAssistitoFromCfSuSogeiNew] Errore tentativo Sogei:`, e.message);
3115
+ const stHttp = e.response?.status;
3116
+ const body = e.response?.data;
3117
+ const ind = typeof body === "string" ? body.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim().slice(0, 200)
3118
+ : (body ? JSON.stringify(body).slice(0, 200) : "");
3119
+ console.log(`[getDatiAssistitoFromCfSuSogeiNew] Errore tentativo Sogei: HTTP ${stHttp ?? "?"} — ${e.message}${ind ? ` | risposta: ${ind}` : ""}`);
2934
3120
  await this.getToken({fallback});
2935
3121
  }
2936
3122
  }
2937
3123
  if (!ok) {
2938
- console.log(`[getDatiAssistitoFromCfSuSogeiNew] Sogei fallito dopo ${this._maxRetry} tentativi per CF: ${cf?.substring(0, 6)}***`);
3124
+ console.log(`[getDatiAssistitoFromCfSuSogeiNew] Sogei fallito dopo ${maxTentativiSogei} tentativi per CF: ${cf?.substring(0, 6)}*** — provo fallback legacy TS`);
3125
+ try {
3126
+ const fb = await this.#fallbackLegacyTsToP801(cf, assistito);
3127
+ if (fb) {
3128
+ console.log(`[getDatiAssistitoFromCfSuSogeiNew] Fallback legacy TS riuscito (fonte: scraping portale)`);
3129
+ return fb;
3130
+ }
3131
+ } catch (e) {
3132
+ console.log(`[getDatiAssistitoFromCfSuSogeiNew] Fallback legacy TS fallito:`, e.message);
3133
+ }
2939
3134
  return {
2940
3135
  ok: false,
2941
3136
  fullData: null,
@@ -2977,9 +3172,34 @@ export class Nar2 {
2977
3172
  let p801 = sogeiData;
2978
3173
  if (!p801) {
2979
3174
  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"};
3175
+ // Sogei irraggiungibile OPPURE dato ricostruito dal fallback legacy: la
3176
+ // classificazione dello stato passa dallo snapshot del PORTALE (Ts.getStatoAssistitoTs
3177
+ // snapshotDaPortale), che NON dipende da p801codiceTipoAssistito inaffidabile dal
3178
+ // portale (per i neonati mostra "3" → falso TRASFERITO). Così i neonati restano corretti.
3179
+ if (!sogei?.ok || sogei?.fullData?._fonte === "legacy-ts") {
3180
+ try {
3181
+ const st = await this.#tsScraper().getStatoAssistitoTs({codiceFiscale, codiceAslAsp});
3182
+ if (st?.ok) {
3183
+ const cval = (k) => { const x = st.coppie?.[k]; return x == null ? "" : String(x).trim(); };
3184
+ return {
3185
+ ok: true, cf: codiceFiscale,
3186
+ stato: st.stato, etichetta: st.etichetta, daFarRientrare: st.daFarRientrare,
3187
+ residenzaInAsp: st.residenzaInAsp, aslAssistenzaInAsp: st.aslAssistenzaInAsp,
3188
+ haMedico: st.haMedico, codiceTipoAssistito: st.codiceTipoAssistito,
3189
+ descrizioneTipoAssistito: st.descrizioneTipoAssistito,
3190
+ anagrafica: {cognome: cval("Cognome"), nome: cval("Nome"), sesso: cval("Sesso"), dataNascita: cval("Data Nascita")},
3191
+ fonte: "portale", evidenze: st.evidenze ?? [], raw: st.coppie ?? null
3192
+ };
3193
+ }
3194
+ } catch (e) {
3195
+ console.log(`[getStatoAssistitoTs] Fallback portale fallito:`, e.message);
3196
+ }
3197
+ // Portale non disponibile: se Sogei è davvero irraggiungibile → errore.
3198
+ // Se invece avevamo un dato legacy ricostruito, proseguo best-effort con esso.
3199
+ if (!sogei?.ok) {
3200
+ const esito = classificaStatoTs({fonte: "sogei", trovato: false}, {codiceAslAsp});
3201
+ return {ok: false, cf: codiceFiscale, ...esito, anagrafica: null, raw: null, error: "Sistema TS (Sogei) non raggiungibile"};
3202
+ }
2983
3203
  }
2984
3204
  if (sogei?.data?.okTs !== true) {
2985
3205
  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;