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,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 {
|
|
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
|
-
|
|
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
|
-
//
|
|
699
|
-
//
|
|
700
|
-
// una scelta strettamente attiva (es. dopo una revoca
|
|
701
|
-
// recente in storico (pm_id più alto).
|
|
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
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
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
|
-
//
|
|
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 =
|
|
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:
|
|
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 <
|
|
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
|
-
|
|
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
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
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
|
-
|
|
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 ${
|
|
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
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
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;
|