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

369 lines
16 KiB
Markdown

# 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`.
## 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 `<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.