Files
roafacturare/docs/patch_curs_bnr_arhiva.md
2026-09-17 22:44:24 +03:00

16 KiB

Patch propus (v3) — arhiva 10 zile + fisier anual BNR, link alternativ cursbnr.ro

Tinta: COMUN\clase\onom_curs.vc2, clasa actualizare_curs_bnr. Investigatie sursa: docs\raport_curs_bnr_politici_preturi.md. Diff unificat aplicabil: docs\patch_curs_bnr_arhiva.diff. Inlocuieste integral v1 si v2 (nu e delta) — aplicat pe fisierul original, neschimbat intre timp.

Scop restrans de Marius, valabil din v2: se lucreaza DOAR pe ramura nTip = 1, cazul de dupa ora 13. nTip = 2 si nTip = 3 raman neatinse.

Doua verificari live, facute de Marius, folosite ca atare (nereverificate aici):

  • https://curs.bnr.ro/nbrfxrates10days.xml -> HTTP 200, un <OrigCurrency>, mai multe <Cube date="YYYY-MM-DD"> descrescator, pana la 10 zile lucratoare (v2).
  • https://curs.bnr.ro/files/xml/years/nbrfxrates2026.xml -> HTTP 200, ~245 KB, structura IDENTICA (un <OrigCurrency>, apoi <Cube date="..."> de la 2026-01-05 pana azi, ordine crescatoare). Acelasi cod de extragere a unui <Cube> merge neschimbat.

1. Proprietati noi: cLinkArhiva si cLinkAn

Bloc afectat: definitia clasei (lista *p:, PropValue, _memberdata), onom_curs.vc2:16-42.

VECHI (onom_curs.vc2:16-18, in *<DefinedPropArrayMethod>):

		*p: ccursor
		*p: clink
		*p: ctempfile

NOU:

		*p: ccursor
		*p: clink
		*p: clinkan
		*p: clinkarhiva
		*p: ctempfile

VECHI (onom_curs.vc2:26-29, in *<PropValue>):

		ccursor = ccrsbnr
		clink = https://curs.bnr.ro/nbrfxrates.xml
		ctempfile = C:\temp\temp.xml

NOU:

		ccursor = ccrsbnr
		clink = https://curs.bnr.ro/nbrfxrates.xml
		clinkan = https://curs.bnr.ro/files/xml/years/nbrfxrates%.xml
		clinkarhiva = https://curs.bnr.ro/nbrfxrates10days.xml
		ctempfile = C:\temp\temp.xml

cLinkAn tine sablonul cu % in locul anului; se compune la rulare cu Strtran(This.cLinkAn,"%",Alltrim(Str(lnAn))).

VECHI (onom_curs.vc2:37-40, in _memberdata):

			<memberdata name="ctempfile" display="cTempFile"/>
			<memberdata name="clink" display="cLink"/>
			<memberdata name="ccursor" display="cCursor"/>

NOU:

			<memberdata name="ctempfile" display="cTempFile"/>
			<memberdata name="clink" display="cLink"/>
			<memberdata name="clinkan" display="cLinkAn"/>
			<memberdata name="clinkarhiva" display="cLinkArhiva"/>
			<memberdata name="ccursor" display="cCursor"/>

2. citeste_curs_bnr (onom_curs.vc2:100-136) — parametru tnSursa (0/1/2) + cautare inapoi + reincercare an anterior

Parametrul tlArhiva (logic, din v2) devine tnSursa (numeric): 0 = fisierul zilei (cLink, comportamentul actual, apelul existent This.citeste_curs_bnr() fara argumente ramane identic — parametrul netransmis e .F. logic, convertit explicit la 0 cu Vartype), 1 = arhiva 10 zile (cLinkArhiva), 2 = fisierul anual (cLinkAn, cu anul Year(This.dData - 1)). Cod de retur neschimbat fata de v2: 0 = succes, 1 = eroare HTTP/serviciu indisponibil, 2 = nicio zi din fereastra cautata nu exista in raspuns.

Cautarea inapoi zi cu zi (max 10 incercari) din v2 e neschimbata pentru tnSursa=1 si e refolosita identic pentru tnSursa=2. Cazul special pentru tnSursa=2: daca in timpul cautarii data candidata trece in anul precedent fata de anul fisierului deja descarcat (caz real: o factura din primele zile ale lui ianuarie are nevoie de publicarea din 31 decembrie anul trecut), se face o singura reincercare — un al doilea request HTTP pe fisierul anual anterior — si cautarea continua in noul raspuns. Nu se reincearca a doua oara peste alt an (llRetrasAnAnterior blocheaza a doua trecere).

VECHI (onom_curs.vc2:100-136, integral):

	PROCEDURE citeste_curs_bnr
		Local loHTTP As 'winHTTP.winHTTPrequest.5.1'
		Local lcData, lcFisier, lcServer
		
		If Empty(This.cCursor)
			This.cCursor = [ccrsbnr]
		Endif
		creeaza_backup_cursoare(This.cCursor)		&&	modificare	03.06.2013
		If Used(This.cCursor)
			Use In (This.cCursor)
		Endif
		
		lcServer = This.cLink
		loHTTP = Createobject('winHTTP.winHTTPrequest.5.1')
		loHTTP.Open('GET', lcServer, .F.)
		loHTTP.setRequestHeader("Content-Type", "application/xml;")
		poLog.Log(m.lcServer)
		loHTTP.Send()
		
		If loHTTP.Status = 200
			lcFisier = loHTTP.Responsebody
		
			lcFisier = Strextract(lcFisier,[</OrigCurrency>],[</Body>])
			lcData = Strextract(lcFisier,[date="],["])
			lcFisier= Strtran(lcFisier,[date="]+lcData+["],[])
			lcFisier= Strtran(Strtran(lcFisier,[">],[" curs="]),[</Rate>],[" />])
			Xmltocursor(lcFisier,This.cCursor)
			This.dData = Ttod(Ctot(lcData+[T000000]))+1
			*!*	modificare	03.06.2013
			sterge_backup_cursoare(This.cCursor)
		Else
			repune_backup_cursoare(This.cCursor)
		Endif
		*!*	modificare	03.06.2013	^
		Release loUpdate,loIp,loUrl,lcData,lnSize,lcFisier,lcData
		 
	ENDPROC

NOU (v4, cu reparatia Try/Catch de la sectiunea 7):

	PROCEDURE citeste_curs_bnr
		Lparameters tnSursa
		Local loHTTP As 'winHTTP.winHTTPrequest.5.1'
		Local lcData, lcFisier, lcServer, lcCubeCautat, ldCandidat, lnIncercari, lnAnCautat, llRetrasAnAnterior, lnStatus
		
		If Vartype(tnSursa) <> "N"
			tnSursa = 0
		Endif
		
		If Empty(This.cCursor)
			This.cCursor = [ccrsbnr]
		Endif
		creeaza_backup_cursoare(This.cCursor)		&&	modificare	03.06.2013
		If Used(This.cCursor)
			Use In (This.cCursor)
		Endif
		
		Do Case
			Case tnSursa = 1
				lcServer = This.cLinkArhiva
			Case tnSursa = 2
				lnAnCautat = Year(This.dData - 1)
				lcServer = Strtran(This.cLinkAn,"%",Alltrim(Str(lnAnCautat)))
			Otherwise
				lcServer = This.cLink
		Endcase
		loHTTP = Createobject('winHTTP.winHTTPrequest.5.1')
		loHTTP.Open('GET', lcServer, .F.)
		loHTTP.SetTimeouts(5000,5000,5000,15000)
		loHTTP.setRequestHeader("Content-Type", "application/xml;")
		poLog.Log(m.lcServer)
		Try
			loHTTP.Send()
			lnStatus = loHTTP.Status
		Catch
			lnStatus = 0
		Endtry
		
		If lnStatus = 200
			lcFisier = loHTTP.Responsebody
		
			lcFisier = Strextract(lcFisier,[</OrigCurrency>],[</Body>])
			If tnSursa = 0
				lcData = Strextract(lcFisier,[date="],["])
				lcFisier= Strtran(lcFisier,[date="]+lcData+["],[])
			Else
				lcCubeCautat = []
				ldCandidat = This.dData - 1
				lnIncercari = 0
				llRetrasAnAnterior = .F.
				Do While Empty(lcCubeCautat) And lnIncercari < 10
					If tnSursa = 2 And Year(ldCandidat) < lnAnCautat And !llRetrasAnAnterior
						llRetrasAnAnterior = .T.
						lnAnCautat = lnAnCautat - 1
						lcServer = Strtran(This.cLinkAn,"%",Alltrim(Str(lnAnCautat)))
						loHTTP = Createobject('winHTTP.winHTTPrequest.5.1')
						loHTTP.Open('GET', lcServer, .F.)
						loHTTP.SetTimeouts(5000,5000,5000,15000)
						loHTTP.setRequestHeader("Content-Type", "application/xml;")
						poLog.Log(m.lcServer)
						Try
							loHTTP.Send()
							lnStatus = loHTTP.Status
						Catch
							lnStatus = 0
						Endtry
						If lnStatus <> 200
							repune_backup_cursoare(This.cCursor)
							Release loUpdate,loIp,loUrl,lcData,lnSize,lcFisier,lcData
							Return 1
						Endif
						lcFisier = Strextract(loHTTP.Responsebody,[</OrigCurrency>],[</Body>])
					Endif
					lcData = Str(Year(ldCandidat),4) + "-" + Padl(Alltrim(Str(Month(ldCandidat))),2,"0") + "-" + Padl(Alltrim(Str(Day(ldCandidat))),2,"0")
					lcCubeCautat = Strextract(lcFisier,[<Cube date="]+lcData+[">],[</Cube>])
					ldCandidat = ldCandidat - 1
					lnIncercari = lnIncercari + 1
				Enddo
				If Empty(lcCubeCautat)
					repune_backup_cursoare(This.cCursor)
					Release loUpdate,loIp,loUrl,lcData,lnSize,lcFisier,lcData
					Return 2
				Endif
				lcFisier = [<Cube>] + lcCubeCautat + [</Cube>]
			Endif
			lcFisier= Strtran(Strtran(lcFisier,[">],[" curs="]),[</Rate>],[" />])
			Xmltocursor(lcFisier,This.cCursor)
			If tnSursa = 0
				This.dData = Ttod(Ctot(lcData+[T000000]))+1
			Endif
			*!*	modificare	03.06.2013
			sterge_backup_cursoare(This.cCursor)
		Else
			repune_backup_cursoare(This.cCursor)
			Release loUpdate,loIp,loUrl,lcData,lnSize,lcFisier,lcData
			Return 1
		Endif
		*!*	modificare	03.06.2013	^
		Release loUpdate,loIp,loUrl,lcData,lnSize,lcFisier,lcData
		 
		Return 0
	ENDPROC

Explicatie tehnica pe scurt (nu intra in cod, doar aici):

  • tnSursa=0 (apel normal, nemodificat functional) merge exact pe ramura veche, pastreaza This.dData = publishingDate+1.
  • tnSursa=1 — identic v2: cauta cel mai recent Cube cu data <= This.dData - 1, mergand inapoi zi cu zi, max 10 incercari.
  • tnSursa=2 — acelasi mers inapoi zi cu zi, dar pe fisierul anual al anului Year(This.dData-1); daca data candidata trece in anul precedent fara sa fi gasit inca un Cube, face un singur request suplimentar pe fisierul anului precedent si continua cautarea acolo. Daca acel request esueaza (Status<>200), intoarce direct 1 (indisponibilitate), nu 2.
  • This.dData NU se rescrie pentru tnSursa=1 sau 2 — ramane ziua ceruta, exact ce asteapta scrie_curs_bnr.

Se atinge STRICT ramura de la onom_curs.vc2:170-173. nTip=2 si nTip=3 raman neatinse.

VECHI (onom_curs.vc2:170-173):

						Case This.dData < Ttod(ltDataOra) Or This.dData = Ttod(ltDataOra) And Hour(ltDataOra) >= 13
							If amessagebox("Nu poate fi preluat automat cursul BNR. Doriti sa accesati pagina BNR?",4+32,"Confirmare") = 6
								goUrl("http://www.bnr.ro")		&&	din wwutils.prg
							Endif

NOU:

						Case This.dData < Ttod(ltDataOra) Or This.dData = Ttod(ltDataOra) And Hour(ltDataOra) >= 13
							lnStareBnr = This.citeste_curs_bnr(1)
							If lnStareBnr = 2
								lnStareBnr = This.citeste_curs_bnr(2)
							Endif
							Do Case
								Case lnStareBnr = 0
									&&	curs preluat din arhiva sau fisierul anual BNR
								Case lnStareBnr = 2
									amessagebox("Nu exista cursul BNR pentru data specificata!",48,"Atentie")
								Otherwise
									If amessagebox("Nu poate fi preluat automat cursul BNR. Doriti sa accesati pagina cursbnr.ro?",4+32,"Confirmare") = 6
										goUrl("https://www.cursbnr.ro/")
									Endif
							Endcase

Cascada, exact cum a cerut Marius:

  1. incearca arhiva 10 zile (citeste_curs_bnr(1));
  2. daca intoarce 2 (data lipsa din raspuns), incearca fisierul anual (citeste_curs_bnr(2));
  3. daca si acela intoarce 2 -> "Nu exista cursul BNR pentru data specificata!";
  4. daca oricare din cele doua intoarce 1 (eroare HTTP/timeout) -> mesajul de indisponibilitate, cu oferta de a deschide https://www.cursbnr.ro/ (nu mai http://www.bnr.ro, doar in aceasta ramura — celelalte doua aparitii ale goUrl("http://www.bnr.ro") din fisier, ramura de dinainte de ora 13 si Otherwise-ul cu nTip necunoscut, raman neschimbate).

lnStareBnr nu e declarata explicit — la fel ca lcSql, lnSucces, ltDataOra deja folosite in aceeasi metoda fara declarare explicita (scop implicit PRIVATE al VFP); pastrat pentru consistenta stilistica.

Raspuns despre nTip

Neschimbat fata de v2: se acopera doar nTip=1; nTip=2 si nTip=3 raman neatinse, prin decizie explicita a lui Marius.

4. Ce acopera

Scenariu Rezultat
Azi, dupa ora 13, Oracle nu are cursul zilei arhiva 10 zile gaseste ziua anterioara (lnStareBnr=0), fara mesaj
Weekend (luni, dupa ora 13, vinerea nu era salvata local) arhiva gaseste Cube-ul de vineri prin mersul inapoi zi cu zi
Sarbatoare legala (o zi lucratoare inconjurata de nelucratoare) arhiva merge inapoi pana gaseste ultima zi publicata, fara lista de sarbatori in cod
Data mai veche de 10 zile lucratoare, dar in anul curent sau anul trecut arhiva 10 zile esueaza cu 2, se incearca fisierul anual (cLinkAn), care acopera restul anului
Factura pe inceput de ianuarie (ex. 2 ianuarie), cursul cerut e din 31 decembrie anul trecut fisierul anual porneste pe anul curerii (Year(This.dData-1)), cautarea trece granita anului, se face o singura reincercare pe fisierul anului precedent
Data mai veche decat acopera si arhiva, si fisierul anual (inclusiv anul precedent, dupa reincercare) "Nu exista cursul BNR pentru data specificata!"
BNR picat / timeout la oricare din cele doua incercari (arhiva sau an) mesajul de indisponibilitate, cu oferta de a deschide https://www.cursbnr.ro/

5. Riscuri deschise

  1. Al doilea request HTTP inline in bucla, doar pentru tnSursa=2 la trecerea de an — cost suplimentar minor (o cerere HTTP in plus, o singura data, doar cand data ceruta cade in primele zile ale unui an); restul cautarii ramane pe sirul deja descarcat in memorie.
  2. Confirmat prin fetch real: structura nbrfxrates2026.xml e identica per-Cube cu celelalte doua fisiere BNR, doar cu mai multe elemente <Cube date="...">, in ordine crescatoare — extractia printr-un singur Cube inainte de Xmltocursor ramane corecta si aici.
  3. repune_backup_cursoare pe cursor deja inchis — risc preexistent, nu introdus acum (aceeasi observatie ca in v1/v2): citeste_curs_bnr inchide This.cCursor inainte de orice apel HTTP; repune_backup_cursoare (oproceduri_comune.prg:6358-6375) face Select pe un alias deja inchis in acel moment — comportament mostenit, nu nou, si in afara scope-ului acestui patch.
  4. URL-uri BNR pe host curs.bnr.ro, confirmate live acum (nbrfxrates10days.xml, nbrfxrates2026.xml); se pot schimba fara preaviz — de reverificat manual daca reapare mesajul de indisponibilitate des dupa aplicare.
  5. Fisierul anual creste an de an (~245 KB la mijlocul lui 2026) — cautarea foloseste tot Strextract pe sirul deja incarcat in memorie, fara reincarcare per incercare; cost acceptabil pentru o operatie declansata manual din UI, nu intr-o bucla de facturare in masa.

6. Cum se probeaza (fara sa ruleze acum)

Nu am rulat nimic din ce urmeaza — se lasa pentru proba manuala dupa aprobare si aplicare:

  1. Aplica diff-ul (docs\patch_curs_bnr_arhiva.diff) pe COMUN\clase\onom_curs.vc2 prin fluxul text->binar existent (skill roa-vfp-text-edit, txt2vcx.ps1 cu -ProjectRoot D:\ROA\ROAFACTURARE).
  2. Caz "azi dupa ora 13": instantiaza actualizare_curs_bnr cu nTip=1, seteaza data sistemului sau ltDataOra simulat dupa ora 13, cere citeste_curs(Ttod(get_ora())) pentru o zi pentru care Oracle nu are inca rand — verifica in poLog ca al doilea link apelat e https://curs.bnr.ro/nbrfxrates10days.xml.
  3. Caz "mai vechi de 10 zile, in acelasi an": alege o data acoperita doar de fisierul anual — verifica in poLog ca dupa arhiva urmeaza apelul catre https://curs.bnr.ro/files/xml/years/nbrfxrates<an>.xml.
  4. Caz trecere de an: cere cursul pentru 2 ianuarie (anul curent), cand 31 decembrie anul trecut nu e salvat local — verifica cele doua request-uri succesive catre fisierul anual curent si cel anterior, si ca This.dData ramane 2 ianuarie (nu 31 decembrie).
  5. Verifica in This.cCursor dupa apel: Reccount()>0, campurile Currency/Curs/multiplier populate identic cu un fetch normal.
  6. Verifica in Oracle, dupa scrie_curs_bnr, ca randul scris in CURS are data = ziua ceruta.
  7. Caz negativ, mai vechi decat orice arhiva: seteaza This.dData cu cativa ani in urma — asteapta mesajul "Nu exista cursul BNR pentru data specificata!".
  8. Caz eroare retea: schimba temporar cLinkArhiva (sau cLinkAn) la un URL invalid — asteapta mesajul de indisponibilitate si confirma linkul nou https://www.cursbnr.ro/ in loc de http://www.bnr.ro, doar pe aceasta ramura.

7. Reparatie (v4) — loHTTP.Send() neprotejat (host nerezolvabil)

Defect gasit de testul headless (Cazul 6, docs\test_curs_bnr_headless.md): loHTTP.Send() nu era in Try/Catch. La host complet nerezolvabil (DNS esuat, eroare OLE 1429), executia sarea peste blocul If loHTTP.Status = 200 ... Else ... Return 1 ... Endif, intra in bucla de cautare Cube cu lcFisier nedefinit si iesea gresit pe Return 2 ("data lipsa" in loc de "BNR picat").

Reparatie, pe ambele aparitii ale loHTTP.Send() din metoda (fisierul zilei/arhiva/an, si reincercarea pe anul precedent):

		Try
			loHTTP.Send()
			lnStatus = loHTTP.Status
		Catch
			lnStatus = 0
		Endtry

Status se citeste o singura data, in interiorul lui Try — citit dupa, a doua exceptie OLE ("The data necessary...") ar fi picat imediat. lnStatus inlocuieste toate referintele la loHTTP.Status din metoda si e adaugat la lista Local existenta. Catch gol, fara logare noua; pe al doilea Send() comportamentul la esec ramane neschimbat (repune_backup_cursoare + Return 1). Cod complet, integrat in blocul "NOU" de la sectiunea 2.