# 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 ``, mai multe `` descrescator, pana la 10 zile lucratoare (v2). - `https://curs.bnr.ro/files/xml/years/nbrfxrates2026.xml` -> HTTP 200, ~245 KB, structura IDENTICA (un ``, apoi `` de la 2026-01-05 pana azi, ordine crescatoare). Acelasi cod de extragere a unui `` 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 `*`): ``` *p: ccursor *p: clink *p: ctempfile ``` **NOU:** ``` *p: ccursor *p: clink *p: clinkan *p: clinkarhiva *p: ctempfile ``` **VECHI** (`onom_curs.vc2:26-29`, in `*`): ``` 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`): ``` ``` **NOU:** ``` ``` ## 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,[],[]) lcData = Strextract(lcFisier,[date="],["]) lcFisier= Strtran(lcFisier,[date="]+lcData+["],[]) lcFisier= Strtran(Strtran(lcFisier,[">],[" curs="]),[],[" />]) 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,[],[]) 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,[],[]) Endif lcData = Str(Year(ldCandidat),4) + "-" + Padl(Alltrim(Str(Month(ldCandidat))),2,"0") + "-" + Padl(Alltrim(Str(Day(ldCandidat))),2,"0") lcCubeCautat = Strextract(lcFisier,[],[]) 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 = [] + lcCubeCautat + [] Endif lcFisier= Strtran(Strtran(lcFisier,[">],[" curs="]),[],[" />]) 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`. ## 3. `initializeaza` (`onom_curs.vc2:152-202`) — ramura `nTip=1`, cazul "prea tarziu azi": cascada + link alternativ 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 ``, 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.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.