* 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