Files
comun/utile/Teste/cache_anaf/mock_batch_anaf.prg
Marius Mutu 859e67e874 Cache ANAF pe ISTORIC_CODURI_FISCALE: verificare din memorie, plasa la tacere ANAF
Verificarile de partener nu mai depind exclusiv de raspunsul ANAF: se citeste
intai cache-ul din ISTORIC_CODURI_FISCALE (mutat in CONTAFIN_ORACLE), cu
provenienta afisata pe ecrane (sursa / data_sursa) si cu plasa de siguranta
cand ANAF tace. Ordinea cache/ANAF e configurabila; cache-ul expira.

- programe/validare.prg: verificare single si pe loturi peste cache, contract
  404 separat de caderea de serviciu, expirarea cache-ului, corectii la
  verdictul pe firma si la etichetarea CACHE_INDISP.
- programe/ocautare.prg: plasa de siguranta si banda de provenienta la cautarea
  de partener.
- programe/oproceduri_comune.prg: VERIFICA_RTVAI_DATA citeste si cache-ul.
- clase/overificari.vc2: propagarea provenientei in interfata.
- utile/Teste/: harness-uri de testare headless - echivalenta cache/ANAF pe 200
  de coduri reale, marginile pe inactiv si TVA la incasare, mock batch ANAF,
  baseline D394 si sondele de diagnostic ANAF.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JCzVWfqscv1xyo5uvJiWiW
2026-08-02 01:33:18 +03:00

268 lines
15 KiB
Plaintext

* mock_batch_anaf.prg - mock pentru calea BATCH de verificare ANAF (VerificareANAF.ANAF_SincronWebService_PlatitorTva,
* COMUN\programe\validare.prg:1865-2009), fara niciun apel HTTP real. Poveste S11 (docs\stories-cache-anaf.md).
*
* ALEGERE (functie vs strat HTTP): mock la nivel de METODA, prin subclasare - NU la nivelul stratului HTTP
* de dedesubt. Metoda originala foloseste Createobject('winHTTP.winHTTPrequest.5.1') / Createobject('Microsoft.XMLHTTP')
* cu ProgID literal, direct in validare.prg - nu exista nicio proprietate/metoda suprascriabila prin care sa
* injectez un obiect fals fara sa editez validare.prg, iar inlocuirea globala a inregistrarii COM (regsvr32/
* registry) ar afecta toata masina, nu doar acest test - inacceptabil ca efect secundar, deci exclusa. In schimb,
* VerificareANAF e o clasa VFP obisnuita (Define Class VerificareANAF As Custom, validare.prg:1814), deci o
* subclasa AICI (VerificareANAFMockBatch) suprascrie DOAR ANAF_SincronWebService_PlatitorTva si MOSTENESTE
* neschimbata ParseJsonANAFv8 (validare.prg:2015) - logica REALA de parsare (toate campurile crsANAF, inclusiv
* sursa/data_sursa adaugate de S3 la Create Cursor) ramane exercitata real; doar transportul HTTP e simulat.
* Grupurile de maxim 100 coduri sunt reproduse identic cu originalul (lnReqNo=100), fara INKEY(1) intre grupuri
* (asteptarea de 1 secunda din original nu are rost intr-un test headless).
*
* GAP pentru S5 (contractul 404 pe calea batch): subclasa suprascrie INTREGUL ANAF_SincronWebService_PlatitorTva,
* deci NU exercita codul real pe care S5 il va adauga/modifica in validare.prg (gate-ul "If loHTTP.Status = 200",
* validare.prg:1968). Pentru ca S5 sa-si testeze headless codul real, are nevoie de un punct de injectie in
* validare.prg pe care NU l-am facut (nu e fisierul meu) - vezi docs\diff-S11-mock-batch.md, sectiunea "Pentru S5".
* Scenariul HTTP404_CORP_COMPLET de mai jos arata AMBELE comportamente (azi: corpul e ignorat; propus: cu
* glANAFBatchIgnoraGate404 = .T., corpul se parseaza chiar si la status<>200) - fixture de referinta pentru S5.
*
* CONFIGURARE (construita INLINE in programul principal apelant, NU intr-o PROCEDURA/FUNCTION separata - vezi
* capcana l, COMUN\docs\depanare_testare_vfp.md: CREATEOBJECT/ADDPROPERTY apelate dintr-o procedura se comporta
* gresit in acest runtime headless; obiectele de configurare trebuie construite direct in main):
* goANAFBatchMock - Collection, cheie 'C'+CUI normalizat (NormalizeazaCUI din validare.prg), valoare:
* - obiect Empty cu proprietati de raspuns (vezi lista mai jos) -> intra in "found"
* - sirul 'NOTFOUND' -> intra explicit in "notFound"
* - cheie lipsa din colectie -> tratat implicit tot ca 'NOTFOUND'
* (simuleaza raspunsul ANAF pentru un CUI pe care nu-l cunoaste)
* goANAFBatchScenariuPerGrup - Collection optionala, cheie TRANSFORM(nr. grup, 1-based), valoare = scenariu -
* suprascrie gcANAFBatchScenariu DOAR pentru acel grup (test cu >100 coduri,
* grupuri cu comportament diferit)
* gcANAFBatchScenariu - scenariul implicit pentru toate grupurile fara override in colectia de mai sus:
* 'NORMAL' (implicit daca lipseste) / 'TACERE' / 'RASPUNS_NEINTELES' /
* 'HTTP404_CORP_COMPLET'
* glANAFBatchIgnoraGate404 - .F. implicit (comportamentul de AZI) - cu .T., scenariul HTTP404_CORP_COMPLET
* parseaza corpul oricum (simuleaza fixul propus pentru S5)
*
* Proprietatile unui obiect de raspuns "gasit" (toate optionale, implicit gol/.F. cand lipsesc):
* denumire, adresa, nrRegCom, data (C, 'YYYY-MM-DD')
* scpTVA (L), dataInceputScpTVA, dataSfarsitScpTVA, dataAnulImpScpTVA (C 'YYYY-MM-DD'), mesajScpTVA (C)
* inactiv (L), dataInactivare, dataReactivare, dataPublicare, dataRadiere (C 'YYYY-MM-DD')
* tvaIncasare (L), dataInceputTvaInc, dataSfarsitTvaInc, dataActualizareTvaInc, dataPublicareTvaInc (C 'YYYY-MM-DD'), tipActTvaInc (C)
* splitTva (L), dataInceputSplitTVA, dataAnulareSplitTVA (C 'YYYY-MM-DD')
* Datele sunt STRING 'YYYY-MM-DD' (acelasi format trimis de ANAF), nu Date VFP - fara ambiguitate legata de SET DATE.
* SENSIBILITATE LA DATA (adaugat 31.07.2026): daca perechea tcPropStart/tcPropEnd e prezenta pe item,
* PropBool evalueaza boolean-ul FATA de data ceruta (tdData, parametrul catre ANAF_SincronWebService_PlatitorTva),
* nu mai intoarce un boolean fix - la fel ca API-ul ANAF real (cere "data" per CUI, validare.prg:2098).
* Fara acele proprietati de data pe item, cade pe boolean-ul static de dinainte (compatibilitate cu testele
* existente care nu seteaza date). Mock-ul ramane marginit la O SINGURA perioada per camp (nu omniscient).
* Toate 4 campuri (scpTVA/tvaIncasare/splitTva/inactiv) folosesc BETWEEN inclusiv la ambele capete (adaugat
* 01.08.2026) - doc_WS_V9.txt (documentatia oficiala ANAF) nu precizeaza daca ziua dataReactivare insasi mai
* conteaza inactiva, deci mock-ul NU copiaza aici sfarsitul exclusiv folosit de cache pentru inactiv
* (validare.prg:2675) - acea margine ramane neverificata, vezi test_margine_inactiv_frontiera.prg.
*
* CONTOR DE APELURI (esential pentru S6 - codurile din cache nu trebuie sa ajunga la ANAF):
* gnANAFBatchApeluri - numeric, o unitate per GRUP efectiv trimis (indiferent de rezultat) - echivalentul
* unui loHTTP.Send() real
* gcANAFBatchCuiuriTrimise - CSV, toate CUI-urile normalizate incluse efectiv intr-un grup trimis, in ordine -
* un CUI proaspat din cache NU trebuie sa apara niciodata aici
* Ambele se initializeaza (gnANAFBatchApeluri = 0, gcANAFBatchCuiuriTrimise = '') INLINE in programul principal,
* inainte de primul apel; mock-ul doar le incrementeaza/concateneaza (fara CREATEOBJECT, sigur din metoda de clasa).
Define Class VerificareANAFMockBatch As VerificareANAF
Procedure ANAF_SincronWebService_PlatitorTva
Lparameters tcCursorListaCIF, tdData, tcAlias
Local lcAlias, lnRecords, lnReqNo, lnGrupuri, lnGrup, lnRec1, lnRec2
Local lcCuiOriginal, lcCuiNorm, lcKey, loItem, lcScenariu, lcFound, lcNotFound, lcBody
Local loJsonObject, loEx As Exception, llNumeric, lnI
lcAlias = Iif(!Empty(m.tcAlias) And Type('tcAlias') = 'C', m.tcAlias, 'crsANAF')
Use In (Select(m.lcAlias))
lnRecords = Reccount(m.tcCursorListaCIF)
If m.lnRecords = 0
Return m.lcAlias
Endif
lnReqNo = 100
lnGrupuri = Max(Ceiling(m.lnRecords / m.lnReqNo), 1)
For lnGrup = 1 To m.lnGrupuri
lnRec1 = m.lnReqNo * (m.lnGrup - 1) + 1
lnRec2 = Min(m.lnReqNo * m.lnGrup, m.lnRecords)
lcScenariu = This.ScenariuPentruGrup(m.lnGrup)
lcFound = ''
lcNotFound = ''
Select (m.tcCursorListaCIF)
Scan For Between(Recno(), m.lnRec1, m.lnRec2)
lcCuiOriginal = Alltrim(cod_fiscal)
lcCuiNorm = NormalizeazaCUI(m.lcCuiOriginal)
llNumeric = !Empty(m.lcCuiNorm) And Len(m.lcCuiNorm) <= 10
If m.llNumeric
For lnI = 1 To Len(m.lcCuiNorm)
If !Isdigit(Substr(m.lcCuiNorm, lnI, 1))
llNumeric = .F.
Exit
Endif
Endfor
Endif
If !m.llNumeric
Loop
Endif
If Type('gnANAFBatchApeluri') = 'N'
gcANAFBatchCuiuriTrimise = gcANAFBatchCuiuriTrimise + Iif(Empty(gcANAFBatchCuiuriTrimise), '', ',') + m.lcCuiNorm
Endif
lcKey = 'C' + m.lcCuiNorm
If Type('goANAFBatchMock') = 'O' And goANAFBatchMock.GetKey(m.lcKey) > 0
loItem = goANAFBatchMock.Item(m.lcKey)
Else
loItem = 'NOTFOUND'
Endif
If Vartype(m.loItem) = 'C'
lcNotFound = lcNotFound + Iif(Empty(m.lcNotFound), '', ',') + m.lcCuiNorm
Else
lcFound = lcFound + Iif(Empty(m.lcFound), '', ',') + This.JsonRaspuns(m.loItem, m.lcCuiNorm, m.tdData)
Endif
Endscan
If Type('gnANAFBatchApeluri') = 'N'
gnANAFBatchApeluri = gnANAFBatchApeluri + 1
Endif
Do Case
Case m.lcScenariu == 'TACERE' Or m.lcScenariu == 'RASPUNS_NEINTELES'
* fara raspuns folosibil - grupul nu adauga niciun rand (ca la lcJsonString = '' in original)
Case m.lcScenariu == 'HTTP404_CORP_COMPLET' And !This.lIgnoraGate404()
* replica gate-ul de AZI (validare.prg:1968, "If loHTTP.Status = 200"): corpul exista dar e ignorat
Otherwise
* NORMAL, sau HTTP404_CORP_COMPLET cu glANAFBatchIgnoraGate404 = .T. (fixture pentru fixul propus S5)
lcBody = '{"found":[' + m.lcFound + '],"notFound":[' + m.lcNotFound + ']}'
Try
loJsonObject = nfjsonread(m.lcBody)
lcAlias = This.ParseJsonANAFv8(m.loJsonObject, m.tcAlias)
Catch To loEx
* pastreaza comportamentul original: o eroare de parsare nu opreste testul, doar grupul respectiv
Endtry
Endcase
Endfor
Return m.lcAlias
Endproc && ANAF_SincronWebService_PlatitorTva
*-- scenariul unui grup: intai override-ul per grup (goANAFBatchScenariuPerGrup), apoi implicitul global
Procedure ScenariuPentruGrup
Lparameters tnGrup
Local lcCheie
lcCheie = Transform(m.tnGrup)
If Type('goANAFBatchScenariuPerGrup') = 'O' And goANAFBatchScenariuPerGrup.GetKey(m.lcCheie) > 0
Return goANAFBatchScenariuPerGrup.Item(m.lcCheie)
Endif
If Type('gcANAFBatchScenariu') = 'C' And !Empty(gcANAFBatchScenariu)
Return gcANAFBatchScenariu
Endif
Return 'NORMAL'
Endproc
Procedure lIgnoraGate404
Return (Type('glANAFBatchIgnoraGate404') = 'L' And glANAFBatchIgnoraGate404)
Endproc
*-- serializeaza un raspuns "gasit" pentru un CUI, in forma JSON asteptata de ParseJsonANAFv8 (validare.prg:2015);
*-- campurile lipsa din obiectul de configurare primesc valori implicite neutre (siruri goale / false).
*!* 31.07.2026 marius.mutu: primeste tdData, il propaga la PropBool (booleanele devin sensibile la data ceruta)
Procedure JsonRaspuns
Lparameters toItem, tcCuiNorm, tdData
Local lcJ
lcJ = '{"date_generale":{'
lcJ = lcJ + '"cui":' + Transform(Val(m.tcCuiNorm)) + ','
lcJ = lcJ + '"data":"' + This.PropStr(m.toItem, 'data', '2024-01-01') + '",'
lcJ = lcJ + '"denumire":"' + This.PropStr(m.toItem, 'denumire', 'FIRMA TEST ' + m.tcCuiNorm) + '",'
lcJ = lcJ + '"adresa":"' + This.PropStr(m.toItem, 'adresa', 'ADRESA TEST') + '",'
lcJ = lcJ + '"nrRegCom":"' + This.PropStr(m.toItem, 'nrRegCom', '') + '"'
lcJ = lcJ + '},'
lcJ = lcJ + '"inregistrare_scop_tva":{'
lcJ = lcJ + '"scpTVA":' + This.PropBool(m.toItem, 'scpTVA', m.tdData, 'dataInceputScpTVA', 'dataSfarsitScpTVA') + ','
lcJ = lcJ + '"perioade_tva":[{'
lcJ = lcJ + '"data_inceput_ScpTVA":"' + This.PropStr(m.toItem, 'dataInceputScpTVA', '') + '",'
lcJ = lcJ + '"data_sfarsit_ScpTVA":"' + This.PropStr(m.toItem, 'dataSfarsitScpTVA', '') + '",'
lcJ = lcJ + '"data_anul_imp_ScpTVA":"' + This.PropStr(m.toItem, 'dataAnulImpScpTVA', '') + '",'
lcJ = lcJ + '"mesaj_ScpTVA":"' + This.PropStr(m.toItem, 'mesajScpTVA', '') + '"'
lcJ = lcJ + '}]'
lcJ = lcJ + '},'
lcJ = lcJ + '"inregistrare_RTVAI":{'
lcJ = lcJ + '"dataInceputTvaInc":"' + This.PropStr(m.toItem, 'dataInceputTvaInc', '') + '",'
lcJ = lcJ + '"dataSfarsitTvaInc":"' + This.PropStr(m.toItem, 'dataSfarsitTvaInc', '') + '",'
lcJ = lcJ + '"dataActualizareTvaInc":"' + This.PropStr(m.toItem, 'dataActualizareTvaInc', '') + '",'
lcJ = lcJ + '"dataPublicareTvaInc":"' + This.PropStr(m.toItem, 'dataPublicareTvaInc', '') + '",'
lcJ = lcJ + '"tipActTvaInc":"' + This.PropStr(m.toItem, 'tipActTvaInc', '') + '",'
lcJ = lcJ + '"statusTvaIncasare":' + This.PropBool(m.toItem, 'tvaIncasare', m.tdData, 'dataInceputTvaInc', 'dataSfarsitTvaInc')
lcJ = lcJ + '},'
lcJ = lcJ + '"stare_inactiv":{'
lcJ = lcJ + '"dataInactivare":"' + This.PropStr(m.toItem, 'dataInactivare', '') + '",'
lcJ = lcJ + '"dataReactivare":"' + This.PropStr(m.toItem, 'dataReactivare', '') + '",'
lcJ = lcJ + '"dataPublicare":"' + This.PropStr(m.toItem, 'dataPublicare', '') + '",'
lcJ = lcJ + '"dataRadiere":"' + This.PropStr(m.toItem, 'dataRadiere', '') + '",'
lcJ = lcJ + '"statusInactivi":' + This.PropBool(m.toItem, 'inactiv', m.tdData, 'dataInactivare', 'dataReactivare')
lcJ = lcJ + '},'
lcJ = lcJ + '"inregistrare_SplitTVA":{'
lcJ = lcJ + '"dataInceputSplitTVA":"' + This.PropStr(m.toItem, 'dataInceputSplitTVA', '') + '",'
lcJ = lcJ + '"dataAnulareSplitTVA":"' + This.PropStr(m.toItem, 'dataAnulareSplitTVA', '') + '",'
lcJ = lcJ + '"statusSplitTVA":' + This.PropBool(m.toItem, 'splitTva', m.tdData, 'dataInceputSplitTVA', 'dataAnulareSplitTVA')
lcJ = lcJ + '}'
lcJ = lcJ + '}'
Return m.lcJ
Endproc
Procedure PropStr
Lparameters toItem, tcProp, tcImplicit
If Type('toItem.' + m.tcProp) <> 'U'
Return Transform(Evaluate('toItem.' + m.tcProp))
Endif
Return m.tcImplicit
Endproc
*-- sensibil la data ceruta, ca serviciul real (ANAF trimite "data" per CUI, validare.prg:2098):
*-- daca item are perechea tcPropStart/tcPropEnd (siruri 'YYYY-MM-DD'), evalueaza tdData IN acea
*-- SINGURA perioada cunoscuta (mock-ul nu e omniscient, la fel ca ANAF v9 - o singura pereche
*-- data_inceput/data_sfarsit, nu o lista, vezi docs\handoff_cache_anaf.md); fara acele
*-- proprietati pe item, cade pe boolean-ul static dinainte (compatibil cu testele existente).
*-- BETWEEN inclusiv pe toate 4 campurile (tlSfarsitExclusiv nefolosit la niciun apel): documentatia
*-- oficiala ANAF (doc_WS_V9.txt) descrie fiecare boolean doar ca "true/false ... la data cautata",
*-- fara sa precizeze daca ziua dataReactivare insasi mai conteaza inactiva - nu e o regula
*-- documentata, deci nu se copiaza aici alegerea (exclusiva) facuta de cache in validare.prg:2675;
*-- vezi test_margine_inactiv_frontiera.prg pentru cazul care ar evidentia diferenta, daca exista.
*!* 01.08.2026 marius.mutu: PropBool nu mai copiaza sfarsitul exclusiv al cache-ului pentru inactiv (vezi docs\diff-S13-teste-echivalenta.md)
Procedure PropBool
Lparameters toItem, tcProp, tdData, tcPropStart, tcPropEnd, tlSfarsitExclusiv
Local llStatic, ldStart, ldEnd, llExclusiv
llStatic = (Vartype(m.tdData) <> 'D') Or Empty(m.tcPropStart) Or (Type('toItem.' + m.tcPropStart) = 'U')
If m.llStatic
If Type('toItem.' + m.tcProp) == 'L' And Evaluate('toItem.' + m.tcProp)
Return 'true'
Endif
Return 'false'
Endif
llExclusiv = (Vartype(m.tlSfarsitExclusiv) == 'L' And m.tlSfarsitExclusiv)
ldStart = This.ParseDataIso(Evaluate('toItem.' + m.tcPropStart))
ldEnd = Iif(Empty(m.tcPropEnd) Or Type('toItem.' + m.tcPropEnd) = 'U', {}, This.ParseDataIso(Evaluate('toItem.' + m.tcPropEnd)))
If Empty(m.ldStart)
Return 'false'
Endif
If m.tdData >= m.ldStart And (Empty(m.ldEnd) Or Iif(m.llExclusiv, m.tdData < m.ldEnd, m.tdData <= m.ldEnd))
Return 'true'
Endif
Return 'false'
Endproc
*-- 'YYYY-MM-DD' (formatul ANAF, vezi antet) -> Date VFP; sir gol/lipsa/invalid -> {} (data goala)
Procedure ParseDataIso
Lparameters tcIso
If Vartype(m.tcIso) <> 'C' Or Len(Alltrim(m.tcIso)) <> 10
Return {}
Endif
Return Date(Val(Substr(m.tcIso, 1, 4)), Val(Substr(m.tcIso, 6, 2)), Val(Substr(m.tcIso, 9, 2)))
Endproc
Enddefine && VerificareANAFMockBatch