feat(autopass): baseline transmitere operatii la RAR AUTOPASS (PRD ROAAUTO-1.0)

Baseline al muncii autopass acumulate (necomise pana acum) pe branch dedicat,
ca sa existe git diff pentru commit-uri incrementale ulterioare.

- oproceduri_autopass.prg: client HTTP + proceduri situatie/transmite/valideaza;
  fix parsare JSON array (detectie ALEN, iterare directa); GetProprietate fara EVL;
  fix 'held' numeric -> normalizare la logic inainte de IIF (AUTOPASS_SITUATIE).
- frm_autopass.scx: formular US-002/US-005; caption succes "statusuri actualizate".
- roaauto.prg: NFJSON in SET PATH + SET PROCEDURE proceduri autopass/nfjson.
- ovariabile_globale.prg: scoate SET STEP ON ramas commis din greseala.
- ofundal_dev.vcx: buton de deschidere a formularului autopass.
- test_autopass_driver.prg: driver de test.
- docs: reorg README (insight-uri transversale mutate in COMUN/docs/).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-13 10:49:46 +03:00
parent 211b7ea8a0
commit 5cac7069a6
13 changed files with 601 additions and 273 deletions

Binary file not shown.

Binary file not shown.

BIN
Ferestre/frm_autopass.SCT Normal file

Binary file not shown.

BIN
Ferestre/frm_autopass.scx Normal file

Binary file not shown.

View File

@@ -39,28 +39,21 @@ lcEnv = ''
*-- citire prin mecanismul standard optiuni (crsOptiuni, incarcat la pornirea aplicatiei
*-- de actualizeaza_optiuni in COMUN\programe\oinit_optiuni.prg)
IF USED('crsOptiuni')
lcUrl = ALLTRIM(citeste_optiune('AUTOPASS_URL'))
lcKey = ALLTRIM(citeste_optiune('AUTOPASS_KEY'))
lcEnv = ALLTRIM(citeste_optiune('AUTOPASS_ENV'))
lcUrl = ALLTRIM(NVL(citeste_optiune('AUTOPASS_URL'),''))
lcKey = ALLTRIM(NVL(citeste_optiune('AUTOPASS_KEY'),''))
lcEnv = ALLTRIM(NVL(citeste_optiune('AUTOPASS_ENV'),''))
ENDIF
*-- fallback pe variabilele globale gc* (setate de optiuni_firma la pornire), pentru
*-- cazul in care crsOptiuni nu mai e deschis in momentul apelului
IF EMPTY(lcUrl) AND TYPE('gcAUTOPASS_URL') = 'C'
lcUrl = ALLTRIM(gcAUTOPASS_URL)
ENDIF
IF EMPTY(lcKey) AND TYPE('gcAUTOPASS_KEY') = 'C'
lcKey = ALLTRIM(gcAUTOPASS_KEY)
ENDIF
IF EMPTY(lcEnv) AND TYPE('gcAUTOPASS_ENV') = 'C'
lcEnv = ALLTRIM(gcAUTOPASS_ENV)
ENDIF
*-- la prima rulare, daca lipseste URL-ul gateway-ului, scriem valoarea implicita
IF EMPTY(lcUrl)
lcUrl = 'https://autopass.romfast.ro'
scrie_optiune('AUTOPASS_URL', lcUrl, 'URL gateway RAR AUTOPASS')
ENDIF
IF EMPTY(lcKey)
scrie_optiune('AUTOPASS_KEY', lcUrl, 'Cheie API gateway AUTOPASS')
ENDIF
loConfig.cUrlGateway = lcUrl
loConfig.cApiKey = lcKey
@@ -95,13 +88,33 @@ RETURN lnSucces >= 0
ENDFUNC && autopass_salveaza_config
*****************************************************************************************
* Mesaj de eroare uman pt un status HTTP de la gateway-ul AUTOPASS - folosit oriunde in UI
* se afiseaza rezultatul unui apel esuat (test conexiune, situatie), ca sa nu mai trimitem
* utilizatorul la "vezi LOG.txt" cand avem deja un motiv clar de aratat direct pe forma.
*****************************************************************************************
FUNCTION autopass_mesaj_eroare_http
LPARAMETERS tnStatus
DO CASE
CASE tnStatus = 401
RETURN 'Cheie API invalida sau neautorizata (HTTP 401) - corecteaza cheia in panoul Configurare.'
CASE tnStatus = 0
RETURN 'Gateway AUTOPASS indisponibil (fara raspuns) - verifica URL-ul si conexiunea la retea.'
OTHERWISE
RETURN 'Gateway AUTOPASS a raspuns cu eroare (HTTP ' + TRANSFORM(tnStatus) + ').'
ENDCASE
ENDFUNC && autopass_mesaj_eroare_http
*****************************************************************************************
* Testeaza conexiunea la gateway-ul AUTOPASS (GET /v1/ping)
* Intoarce obiect: lSucces, cMediu, lAutentificatCuCheie, lAreCredsTest, lAreCredsProd, cEroare
*****************************************************************************************
FUNCTION autopass_test_conexiune
LPARAMETERS tcUrl, tcKey
LOCAL loConfig, loHTTP As "winHTTP.winHTTPrequest.5.1", loReturn As "empty", loEx As Exception
LOCAL lcServer, lnStatus, lcResponseText, loJson
LOCAL lcServer, lnStatus, lcResponseText, loJson, lcUrl, lcKey
loReturn = Createobject('empty')
AddProperty(loReturn, 'lSucces', .F.)
@@ -112,7 +125,12 @@ AddProperty(loReturn, 'lAreCredsProd', .F.)
AddProperty(loReturn, 'cEroare', '')
loConfig = autopass_get_config()
IF !loConfig.lConfigOk
*-- URL/cheie efective: daca apelantul le trimite explicit (panoul Configurare, ca sa poata
*-- testa o cheie inainte de a o salva), au prioritate; altfel se cade pe configul salvat.
lcUrl = IIF(VARTYPE(tcUrl) = 'C' AND !EMPTY(ALLTRIM(tcUrl)), ALLTRIM(tcUrl), ALLTRIM(loConfig.cUrlGateway))
lcKey = IIF(VARTYPE(tcKey) = 'C' AND !EMPTY(ALLTRIM(tcKey)), ALLTRIM(tcKey), ALLTRIM(loConfig.cApiKey))
IF EMPTY(lcUrl) OR EMPTY(lcKey)
loReturn.cEroare = 'Configurare AUTOPASS incompleta: completeaza cheia API' + ;
' (si URL-ul, daca e nevoie) in panoul Configurare.'
RETURN loReturn
@@ -122,12 +140,12 @@ lnStatus = 0
lcResponseText = ''
TRY
lcServer = ALLTRIM(loConfig.cUrlGateway) + [/v1/ping]
lcServer = lcUrl + [/v1/ping]
loHTTP = Createobject('winHTTP.winHTTPrequest.5.1')
loHTTP.Open('GET', lcServer, .F.)
loHTTP.SetTimeouts(30000, 30000, 30000, 30000)
loHTTP.setRequestHeader('X-API-Key', loConfig.cApiKey)
loHTTP.setRequestHeader('X-API-Key', lcKey)
loHTTP.Send()
lnStatus = loHTTP.Status
@@ -139,15 +157,10 @@ CATCH TO loEx
RETURN loReturn
ENDTRY
DO CASE
CASE lnStatus = 401
loReturn.cEroare = 'Cheie API invalida sau neautorizata (HTTP 401) - corecteaza cheia' + ;
' in panoul Configurare.'
IF lnStatus <> 200
loReturn.cEroare = autopass_mesaj_eroare_http(lnStatus)
RETURN loReturn
CASE lnStatus <> 200
loReturn.cEroare = 'Gateway AUTOPASS a raspuns cu eroare (HTTP ' + TRANSFORM(lnStatus) + ').'
RETURN loReturn
ENDCASE
ENDIF
loJson = Nfjsonread(lcResponseText)
IF VARTYPE(loJson) <> 'O'
@@ -155,11 +168,16 @@ IF VARTYPE(loJson) <> 'O'
RETURN loReturn
ENDIF
*-- nume de proprietati confirmate din contract (rar-autopass app/api/v1/integrare_router.py:74-82,
*-- GET /v1/ping): raspunsul e snake_case (mediu, autentificat_cu_cheie, are_creds_test,
*-- are_creds_prod, are_creds_rar) - NU camelCase. nfjsonread.prg (procedura propertyName)
*-- pastreaza numele de proprietate JSON asa cum vine (litere/cifre/underscore), fara nicio
*-- conversie de convenite, deci underscore-urile din JSON raman underscore-uri in obiectul VFP.
loReturn.lSucces = .T.
loReturn.cMediu = IIF(TYPE('loJson.mediu') = 'C', loJson.mediu, '')
loReturn.lAutentificatCuCheie = IIF(TYPE('loJson.autentificatCuCheie') = 'L', loJson.autentificatCuCheie, .F.)
loReturn.lAreCredsTest = IIF(TYPE('loJson.areCredsTest') = 'L', loJson.areCredsTest, .F.)
loReturn.lAreCredsProd = IIF(TYPE('loJson.areCredsProd') = 'L', loJson.areCredsProd, .F.)
loReturn.lAutentificatCuCheie = IIF(TYPE('loJson.autentificat_cu_cheie') = 'L', loJson.autentificat_cu_cheie, .F.)
loReturn.lAreCredsTest = IIF(TYPE('loJson.are_creds_test') = 'L', loJson.are_creds_test, .F.)
loReturn.lAreCredsProd = IIF(TYPE('loJson.are_creds_prod') = 'L', loJson.are_creds_prod, .F.)
RETURN loReturn
ENDFUNC && autopass_test_conexiune
@@ -171,7 +189,7 @@ ENDFUNC && autopass_test_conexiune
FUNCTION autopass_cursor_comenzi
LPARAMETERS tdDataInceput, tdDataSfarsit
LOCAL lcCursorHeader, lcD1, lcD2, lcSql, lcFiltruSters, lnSucces
LOCAL llOk, lcMotivRand, ldDataPrest
LOCAL llOk, lcMotivRand, ldDataPrest, lcCursorRaw
lcCursorHeader = 'crs_autopass_comenzi'
IF USED(lcCursorHeader)
@@ -183,16 +201,21 @@ lcD1 = "TO_DATE('" + STR(YEAR(tdDataInceput),4) + "-" + PADL(MONTH(tdDataInceput
lcD2 = "TO_DATE('" + STR(YEAR(tdDataSfarsit),4) + "-" + PADL(MONTH(tdDataSfarsit),2,'0') + "-" + PADL(DAY(tdDataSfarsit),2,'0') + "','YYYY-MM-DD')"
lcFiltruSters = ''
IF autopass_coloana_exista(gcS, 'AUTO_ISTORIC_COMENZI', 'STERS')
IF autopass_coloana_exista('AUTO_ISTORIC_COMENZI', 'STERS')
lcFiltruSters = ' AND sters = 0'
ENDIF
lcSql = "SELECT id_ordl, nrord, dataoravalid, datafact, nrfact, nrinmat, series, kmint, nume, tip_comanda " + ;
"FROM " + gcS + ".auto_istoric_comenzi " + ;
"FROM auto_istoric_comenzi " + ;
"WHERE validat = 1 AND facturat = 1 AND id_tip IN (1,2) " + ;
"AND datafact BETWEEN " + lcD1 + " AND " + lcD2 + lcFiltruSters
lnSucces = goExecutor.oExecute(lcSql, lcCursorHeader)
lcCursorRaw = lcCursorHeader + '_raw'
IF USED(lcCursorRaw)
USE IN (lcCursorRaw)
ENDIF
lnSucces = goExecutor.oExecute(lcSql, lcCursorRaw)
IF lnSucces < 0
IF TYPE('goLog') = 'O'
goLog.Log('autopass_cursor_comenzi: ' + goExecutor.cEroare, PROGRAM())
@@ -200,8 +223,20 @@ IF lnSucces < 0
RETURN ''
ENDIF
ALTER TABLE (lcCursorHeader) ADD COLUMN lvalid L
ALTER TABLE (lcCursorHeader) ADD COLUMN motiv_invalid C(100)
*-- rematerializam ca cursor local simplu, sub numele final, CU coloanele suplimentare deja
*-- incluse in SELECT (nu ALTER TABLE dupa) - cursorul intors de goExecutor.oExecute() poate
*-- ramane legat de conexiune ("remote table"/cursor cu operatii de structura restrictionate),
*-- caz in care ALTER TABLE separat pica ("Function is not supported on remote tables",
*-- apoi "Invalid operation for the cursor" chiar si dupa o rematerializare simpla). Taina de
*-- "remote" persista pe acest cursor oricat de multe ori il rematerializam - de-aia coloanele
*-- de UI/corelare (lbifat/ncntop/cstatusro/cverdict, status_autopass/motiv_autopass/
*-- id_prezentare_autopass), adaugate altfel prin ALTER TABLE mai jos in Load()/incarca_date()/
*-- autopass_coreleaza_situatie(), trebuie incluse direct AICI, la creare.
SELECT *, .F. AS lvalid, SPACE(100) AS motiv_invalid, ;
.F. AS lbifat, 0 AS ncntop, SPACE(40) AS cstatusro, SPACE(200) AS cverdict, ;
SPACE(30) AS status_autopass, SPACE(254) AS motiv_autopass, SPACE(60) AS id_prezentare_autopass ;
FROM (lcCursorRaw) INTO CURSOR (lcCursorHeader) READWRITE
USE IN (lcCursorRaw)
SELECT (lcCursorHeader)
SCAN
@@ -215,8 +250,12 @@ SCAN
ldDataPrest = {}
ENDCASE
*-- nr. inmatriculare normalizat aici (sters spatii/cratime/puncte, cf. gateway), ca
*-- valoarea propagata mai departe in cursor (CSV, payload catre AutopassClient) sa fie
*-- deja normalizata, nu doar cea validata local
llOk = autopass_valideaza_rand(series, nrinmat, ldDataPrest, kmint, @lcMotivRand)
REPLACE lvalid WITH llOk, motiv_invalid WITH lcMotivRand
REPLACE nrinmat WITH autopass_normalizeaza_nrinmat(nrinmat), ;
lvalid WITH llOk, motiv_invalid WITH lcMotivRand
ENDSCAN
RETURN lcCursorHeader
@@ -227,7 +266,7 @@ ENDFUNC && autopass_cursor_comenzi
FUNCTION autopass_cursor_operatii
LPARAMETERS tcCursorHeader
LOCAL lcCursorDetaliu, lcCursorCount, lcSql, lcChunkSql, lcIdList, lnSucces
LOCAL lnTotal, lnChunkSize, lnStart, lnEnd, i
LOCAL lnTotal, lnChunkSize, lnStart, lnEnd, i, lcCursorRaw
LOCAL ARRAY laId[1]
LOCAL lnCnt, lcMotivExist
@@ -245,8 +284,8 @@ lnTotal = RECCOUNT()
IF lnTotal = 0
lcSql = "SELECT o.id_ordl, n.codop, n.denop " + ;
"FROM " + gcS + ".dev_oper o " + ;
"JOIN " + gcS + ".dev_nom_norme n ON o.id_norme = n.id_norme " + ;
"FROM dev_oper o " + ;
"JOIN dev_nom_norme n ON o.id_norme = n.id_norme " + ;
"WHERE 1 = 2"
ELSE
DIMENSION laId[lnTotal]
@@ -269,8 +308,8 @@ ELSE
ENDFOR
lcChunkSql = "SELECT o.id_ordl, n.codop, n.denop " + ;
"FROM " + gcS + ".dev_oper o " + ;
"JOIN " + gcS + ".dev_nom_norme n ON o.id_norme = n.id_norme " + ;
"FROM dev_oper o " + ;
"JOIN dev_nom_norme n ON o.id_norme = n.id_norme " + ;
"WHERE o.sters = 0 AND o.id_ordl IN (" + lcIdList + ")"
lcSql = lcSql + IIF(EMPTY(lcSql),'',' UNION ALL ') + lcChunkSql
@@ -278,7 +317,12 @@ ELSE
ENDDO
ENDIF
lnSucces = goExecutor.oExecute(lcSql, lcCursorDetaliu)
lcCursorRaw = lcCursorDetaliu + '_raw'
IF USED(lcCursorRaw)
USE IN (lcCursorRaw)
ENDIF
lnSucces = goExecutor.oExecute(lcSql, lcCursorRaw)
IF lnSucces < 0
IF TYPE('goLog') = 'O'
goLog.Log('autopass_cursor_operatii: ' + goExecutor.cEroare, PROGRAM())
@@ -286,8 +330,11 @@ IF lnSucces < 0
RETURN ''
ENDIF
ALTER TABLE (lcCursorDetaliu) ADD COLUMN lvalid_op L
ALTER TABLE (lcCursorDetaliu) ADD COLUMN motiv_op C(100)
*-- vezi autopass_cursor_comenzi() - rematerializam ca cursor local simplu, sub numele final,
*-- CU coloanele suplimentare deja incluse in SELECT (nu ALTER TABLE dupa)
SELECT *, .F. AS lvalid_op, SPACE(100) AS motiv_op ;
FROM (lcCursorRaw) INTO CURSOR (lcCursorDetaliu) READWRITE
USE IN (lcCursorRaw)
SELECT (lcCursorDetaliu)
SCAN
@@ -351,9 +398,13 @@ ENDIF
lcTemp = 'crs_ap_dedup_' + SYS(2015)
*-- pastram o singura instanta per id_ordl+codop
*-- VFP e strict ANSI-92 pe GROUP BY: orice camp din SELECT trebuie sa fie fie in GROUP BY,
*-- fie agregat. denop/lvalid_op/motiv_op sunt deja deterministe per codop (calculate mai sus,
*-- la liniile 322-326), deci adaugarea lor in GROUP BY nu schimba rezultatul, doar satisface
*-- sintaxa (fara aceasta, eroare runtime "GROUP BY clause is missing or invalid").
SELECT id_ordl, codop, denop, lvalid_op, motiv_op ;
FROM (tcCursorDetaliu) ;
GROUP BY id_ordl, codop ;
GROUP BY id_ordl, codop, denop, lvalid_op, motiv_op ;
INTO CURSOR (lcTemp) READWRITE
lcPath = DBF(lcTemp)
@@ -380,11 +431,17 @@ LOCAL lcVin, lcNrinmat, lcMotiv, ldData, llOk
lcMotiv = ''
lcVin = ALLTRIM(UPPER(IIF(VARTYPE(tcVin) = 'C', tcVin, '')))
lcNrinmat = ALLTRIM(UPPER(IIF(VARTYPE(tcNrinmat) = 'C', tcNrinmat, '')))
lcNrinmat = autopass_normalizeaza_nrinmat(tcNrinmat)
*-- aliniat la regula reala din gateway (rar-autopass app/validation.py, VIN_RE =
*-- ^[A-HJ-NPR-Z0-9]{17}$): 17 caractere, DOAR A-Z/0-9 (fara spatii/cratime/caractere
*-- speciale) si fara literele interzise O/I/Q
IF LEN(lcVin) # 17
lcMotiv = lcMotiv + IIF(EMPTY(lcMotiv),'','; ') + 'VIN invalid (trebuie 17 caractere)'
ELSE
IF !autopass_alfanumeric(lcVin)
lcMotiv = lcMotiv + IIF(EMPTY(lcMotiv),'','; ') + 'VIN invalid (caractere nepermise)'
ENDIF
IF 'O' $ lcVin OR 'I' $ lcVin OR 'Q' $ lcVin
lcMotiv = lcMotiv + IIF(EMPTY(lcMotiv),'','; ') + 'VIN invalid (contine litera interzisa O/I/Q)'
ENDIF
@@ -407,6 +464,9 @@ IF EMPTY(ldData) OR ldData < {^2024-12-01} OR ldData > DATE()
lcMotiv = lcMotiv + IIF(EMPTY(lcMotiv),'','; ') + 'Data prestatiei in afara intervalului permis'
ENDIF
*-- intentionat mai strict decat gateway-ul (care accepta si 0): PRD (Failure Modes
*-- Registry) trateaza KMINT=0 explicit ca date istorice proaste, nebifabile - NU se
*-- aliniaza la contract pe acest punct
IF VARTYPE(tnKm) # 'N' OR tnKm <= 0
lcMotiv = lcMotiv + IIF(EMPTY(lcMotiv),'','; ') + 'Kilometraj invalid'
ENDIF
@@ -437,10 +497,29 @@ ENDFUNC && autopass_alfanumeric
*****************************************************************************************
* Verifica existenta unei coloane intr-un tabel Oracle (owner/table/column)
* Normalizare nr. inmatriculare - aliniata la normalizarea aplicata acum si in gateway
* (rar-autopass, inainte de validare): elimina spatii, cratime si puncte, pe langa
* strip+upper standard. Trebuie aplicata local ACEEASI normalizare inainte de orice
* validare/trimitere, ca "B 99 XYZ" / "B-99-XYZ" sa devina "B99XYZ" si clientul sa nu
* respinga local randuri pe care gateway-ul le-ar accepta.
*****************************************************************************************
FUNCTION autopass_normalizeaza_nrinmat
LPARAMETERS tcNrinmat
LOCAL lcNrinmat
lcNrinmat = ALLTRIM(UPPER(IIF(VARTYPE(tcNrinmat) = 'C', tcNrinmat, '')))
lcNrinmat = STRTRAN(STRTRAN(STRTRAN(lcNrinmat, ' ', ''), '-', ''), '.', '')
RETURN lcNrinmat
ENDFUNC && autopass_normalizeaza_nrinmat
*****************************************************************************************
* Verifica existenta unei coloane intr-un tabel Oracle (schema curenta - aplicatia e
* conectata permanent pe schema curenta, USER_TAB_COLUMNS nu mai are nevoie de OWNER)
*****************************************************************************************
FUNCTION autopass_coloana_exista
LPARAMETERS tcSchema, tcTabel, tcColoana
LPARAMETERS tcTabel, tcColoana
LOCAL lcCursor, llExista, lnSucces
lcCursor = 'crs_ap_colcheck'
@@ -448,8 +527,8 @@ IF USED(lcCursor)
USE IN (lcCursor)
ENDIF
lnSucces = goExecutor.oExecute("SELECT COUNT(*) AS ncnt FROM ALL_TAB_COLUMNS WHERE OWNER = UPPER('" + ;
tcSchema + "') AND TABLE_NAME = UPPER('" + tcTabel + "') AND COLUMN_NAME = UPPER('" + tcColoana + "')", lcCursor)
lnSucces = goExecutor.oExecute("SELECT COUNT(*) AS ncnt FROM USER_TAB_COLUMNS WHERE " + ;
"TABLE_NAME = UPPER('" + tcTabel + "') AND COLUMN_NAME = UPPER('" + tcColoana + "')", lcCursor)
llExista = .F.
IF lnSucces >= 0 AND USED(lcCursor)
@@ -524,7 +603,7 @@ DEFINE CLASS AutopassClient AS Custom
Lparameters taComenzi, tcPath, tcAction
Local lnCount, i, lcJson, loBody, loComanda, loPrezentare, loHttpRez
Local loReturn, loParsed, laRezultate, loColectie, loElem, loRezItem, lnIndexRel
Local loReturn, loParsed, lcNumeLista, lnNr, loColectie, loElem, loRezItem, lnIndexRel
Local Array laPrezentari(1), laIndexMap(1)
lnCount = This.GetNumarElemente(m.taComenzi)
@@ -567,12 +646,16 @@ DEFINE CLASS AutopassClient AS Custom
AddProperty(loBody, 'rar_env', This.cRarEnv)
Endif
* TODO verificare empirica: testeaza cu diacritice pe mediul RAR test inainte de prima
* trimitere reala; presupunem ca nfjsoncreate.prg emite \uXXXX pentru caractere ne-ASCII.
* Daca emite in schimb bytes cp1250, aici trebuie STRCONV(...,9) sau trimitere ca
* byte-array cu charset=utf-8 - NU aplica STRCONV suplimentar peste rezultatul lui
* nfjsoncreate fara sa verifici mai intai, ar risca dubla-conversie.
lcJson = Nfjsoncreate(loBody, .F.)
* Verificat static (D:\ROA\PROGRAME\nfjsoncreate.prg, functiile concatval/mustEncode/
* escapeandencode): mustEncode() escapeaza in \uXXXX DOAR caracterele de control
* (chr(0)-chr(31)) si \ / " - diacriticele (a,a,i,s,t cu diacritice romanesti) NU sunt in
* specialChars, deci nfjsoncreate le scrie ca octeti bruti in codepage-ul nativ VFP
* (NU \uXXXX, NU UTF-8). JSON cere UTF-8 (RFC 8259) -> conversia explicita e obligatorie,
* o singura data, DUPA Nfjsoncreate (nu inainte - ar corupe delimitatorii JSON, care sunt
* toti ASCII si trec neschimbati prin STRCONV ANSI->UTF-8).
* Ramane de facut: round-trip pe mediul RAR test cu operatii cu diacritice, verificat in
* dashboard (nu se poate rula VFP din acest mediu de analiza statica).
lcJson = Strconv(Nfjsoncreate(loBody, .F.), 9)
loReturn = Createobject('empty')
AddProperty(loReturn, 'lSucces', .F.)
@@ -598,37 +681,47 @@ DEFINE CLASS AutopassClient AS Custom
Return loReturn
Endif
* TODO verificare empirica: numele exact al proprietatii-container din raspunsul real
* al gateway-ului (rezultate/prezentari/altul) nu e confirmat in specificatie folosita
* aici; ExtrageListaRezultate() incearca variantele uzuale, ajusteaza cand se cunoaste
* contractul exact.
laRezultate = This.ExtrageListaRezultate(m.loParsed)
* Confirmat din contract (rar-autopass app/models.py PrezentariResponse/ValidareResponse
* + app/api/v1/router.py:179,429): proprietatea-container e "results" pe AMBELE
* POST /v1/prezentari si POST /v1/prezentari/valideaza. ExtrageListaRezultate() cauta
* "results" intai; celelalte variante raman doar fallback defensiv.
lcNumeLista = This.ExtrageListaRezultate(m.loParsed)
If Vartype(m.laRezultate) # 'A'
If Empty(m.lcNumeLista)
loReturn.cDetaliu = 'formatul raspunsului nu contine lista de rezultate asteptata'
This.LogEroare(m.tcAction, -1, loHttpRez.nStatusHttp, loHttpRez.cRaspuns)
Return loReturn
Endif
lnNr = Alen(m.loParsed.&lcNumeLista, 1)
loColectie = Createobject('Collection')
For i = 1 To Alen(m.laRezultate, 1)
For i = 1 To m.lnNr
loElem = laRezultate(m.i)
loElem = m.loParsed.&lcNumeLista(m.i)
lnIndexRel = This.GetProprietate(m.loElem, 'index', m.i - 1)
loRezItem = Createobject('empty')
AddProperty(loRezItem, 'index', m.lnIndexRel)
AddProperty(loRezItem, 'id_ordl', ;
Iif(Between(m.lnIndexRel + 1, 1, m.lnCount), laIndexMap(m.lnIndexRel + 1), 0))
AddProperty(loRezItem, 'submission_id', This.GetProprietate(m.loElem, 'submission_id', ''))
AddProperty(loRezItem, 'status', This.GetProprietate(m.loElem, 'status', ''))
AddProperty(loRezItem, 'motiv', This.GetProprietate(m.loElem, 'motiv', ''))
AddProperty(loRezItem, 'id_prezentare', This.GetProprietate(m.loElem, 'id_prezentare', ''))
If m.tcAction == 'Valideaza'
* POST /v1/prezentari/valideaza intoarce ValidareResult (app/models.py) - forma DIFERITA
* de SubmissionResultMinim, fara submission_id/status/motiv/id_prezentare:
* index, valid, status_estimat, rar_env, erori[], nemapate[], prestatii_rezolvate[]
AddProperty(loRezItem, 'valid', This.GetProprietate(m.loElem, 'valid', .F.))
AddProperty(loRezItem, 'status_estimat', This.GetProprietate(m.loElem, 'status_estimat', ''))
AddProperty(loRezItem, 'coduri_nemapate', This.GetProprietate(m.loElem, 'coduri_nemapate', .Null.))
AddProperty(loRezItem, 'rar_env', This.GetProprietate(m.loElem, 'rar_env', ''))
AddProperty(loRezItem, 'erori', This.GetProprietate(m.loElem, 'erori', .Null.))
AddProperty(loRezItem, 'nemapate', This.GetProprietate(m.loElem, 'nemapate', .Null.))
Else
* POST /v1/prezentari (raspuns implicit 'minim', SubmissionResultMinim din app/models.py):
* index, submission_id, vehicul, status, motiv, id_prezentare
AddProperty(loRezItem, 'submission_id', This.GetProprietate(m.loElem, 'submission_id', ''))
AddProperty(loRezItem, 'status', This.GetProprietate(m.loElem, 'status', ''))
AddProperty(loRezItem, 'motiv', This.GetProprietate(m.loElem, 'motiv', ''))
AddProperty(loRezItem, 'id_prezentare', This.GetProprietate(m.loElem, 'id_prezentare', ''))
Endif
loColectie.Add(loRezItem)
@@ -645,9 +738,17 @@ DEFINE CLASS AutopassClient AS Custom
*-------------------------------------------------------------------
Procedure StatusPrezentare
*-------------------------------------------------------------------
* Contract confirmat (rar-autopass app/api/v1/router.py:562 get_prezentare,
* allowlist _PREZENTARE_FIELDS linia 547): campurile expuse de GET /v1/prezentari/{id}
* sunt id, status, id_prezentare, rar_status_code, retry_count, next_attempt_at,
* created_at, updated_at, account_id, batch_id, row_index, purge_after, rar_error,
* rar_env, held. NU exista "submission_id"/"motiv"/"vehicul" ca atare in raspuns:
* id-ul e cel din URL (ecou = tcSubmissionId), iar "motiv" se deriva din rar_error
* (forma variabila - vezi ExtrageMotivRarError si docs/api-rar-contract.md "Envelope
* de eroare imbogatit").
Lparameters tcSubmissionId
Local loHttpRez, loReturn, loParsed
Local loHttpRez, loReturn, loParsed, loRarError
loReturn = Createobject('empty')
AddProperty(loReturn, 'lSucces', .F.)
@@ -657,7 +758,8 @@ DEFINE CLASS AutopassClient AS Custom
AddProperty(loReturn, 'status', '')
AddProperty(loReturn, 'motiv', '')
AddProperty(loReturn, 'id_prezentare', '')
AddProperty(loReturn, 'vehicul', '')
AddProperty(loReturn, 'rar_env', '')
AddProperty(loReturn, 'held', .F.)
loHttpRez = This.Get('/v1/prezentari/' + Alltrim(m.tcSubmissionId), 'StatusPrezentare')
@@ -679,15 +781,49 @@ DEFINE CLASS AutopassClient AS Custom
loReturn.lSucces = .T.
loReturn.status = This.GetProprietate(m.loParsed, 'status', '')
loReturn.motiv = This.GetProprietate(m.loParsed, 'motiv', '')
loReturn.id_prezentare = This.GetProprietate(m.loParsed, 'id_prezentare', '')
loReturn.vehicul = This.GetProprietate(m.loParsed, 'vehicul', '')
loReturn.submission_id = This.GetProprietate(m.loParsed, 'submission_id', m.tcSubmissionId)
loReturn.rar_env = This.GetProprietate(m.loParsed, 'rar_env', '')
loReturn.held = This.GetProprietate(m.loParsed, 'held', .F.)
loRarError = This.GetProprietate(m.loParsed, 'rar_error', .Null.)
loReturn.motiv = This.ExtrageMotivRarError(m.loRarError)
Return loReturn
Endproc
*-------------------------------------------------------------------
Procedure ExtrageMotivRarError
*-------------------------------------------------------------------
* rar_error NU are forma fixa (docs/api-rar-contract.md "Envelope de eroare
* imbogatit"): poate fi NULL (fara eroare), array de obiecte {field,message,cod,
* problema,cauza,fix} (needs_data / eroare RAR 400 - un obiect per camp invalid),
* sau un obiect unic cu "unmapped"/"cauza" (needs_mapping / RAR_CREDS_INVALIDE /
* RAR_EROARE_SERVER). Extrage un rezumat uman pe o linie din campul cel mai util
* disponibil (cauza intai, apoi problema/message ca fallback).
Lparameters toRarError
Local lcTxt
If Vartype(m.toRarError) = 'A' And Alen(m.toRarError, 1) > 0
lcTxt = This.GetProprietate(m.toRarError(1), 'cauza', '')
If Empty(m.lcTxt)
lcTxt = This.GetProprietate(m.toRarError(1), 'message', '')
Endif
Return m.lcTxt
Endif
If Vartype(m.toRarError) = 'O'
lcTxt = This.GetProprietate(m.toRarError, 'cauza', '')
If Empty(m.lcTxt)
lcTxt = This.GetProprietate(m.toRarError, 'problema', '')
Endif
Return m.lcTxt
Endif
Return ''
Endproc
*-------------------------------------------------------------------
Procedure Post
*-------------------------------------------------------------------
@@ -711,7 +847,9 @@ DEFINE CLASS AutopassClient AS Custom
loHTTP = Createobject('winHTTP.winHTTPrequest.5.1')
loHTTP.Open('POST', m.lcUrl, .F.)
loHTTP.SetTimeouts(30000, 30000, 30000, 30000)
loHTTP.setRequestHeader('Content-Type', 'application/json')
* charset explicit - corpul e convertit la UTF-8 in EnviaLot (Strconv(..., 9)) inainte
* sa ajunga aici; declaratia trebuie sa reflecte octetii trimisi efectiv.
loHTTP.setRequestHeader('Content-Type', 'application/json; charset=utf-8')
loHTTP.setRequestHeader('X-API-Key', This.cApiKey)
loHTTP.Send(m.tcJsonBody)
@@ -803,27 +941,42 @@ DEFINE CLASS AutopassClient AS Custom
*-------------------------------------------------------------------
Procedure ExtrageListaRezultate
*-------------------------------------------------------------------
* Container confirmat din contract (rar-autopass app/models.py PrezentariResponse
* si ValidareResponse): "results" pe AMBELE POST /v1/prezentari si
* POST /v1/prezentari/valideaza. Celelalte nume raman doar fallback defensiv
* (nefolosit in contractul actual, pastrat pentru robustete la schimbari viitoare).
*
* Intoarce NUMELE proprietatii-container (string), NU array-ul insusi: o functie VFP nu
* poate intoarce un array (ar intoarce doar primul element). Apelantul itereaza direct
* cu toParsed.&nume(i). '' daca nu exista lista sau e goala.
Lparameters toParsed
Local Array laNume(4)
Local i
Local Array laNume(6)
Local i, lcNume
laNume(1) = 'rezultate'
laNume(2) = 'prezentari'
laNume(3) = 'items'
laNume(4) = 'data'
laNume(1) = 'results'
laNume(2) = 'rezultate'
laNume(3) = 'prezentari'
laNume(4) = 'items'
laNume(5) = 'data'
laNume(6) = 'array'
For i = 1 To Alen(laNume, 1)
If Type('toParsed.' + laNume(m.i)) = 'A'
Return toParsed.&laNume(m.i)
lcNume = laNume(m.i)
*-- TYPE('toParsed.'+nume) NU e de incredere - pe un array de OBIECTE, VFP intoarce
*-- tipul primului element ('O'), nu 'A'. Detectam prin accesul la elementul (1).
If Type('toParsed.' + m.lcNume + '(1)') = 'U'
Loop
Endif
*-- array gol marcat de nfjson cu flag pe elementul (1) -> se sare (cade pe '')
If Type('toParsed.' + m.lcNume + '(1)') = 'C' ;
And toParsed.&lcNume(1) == '_EMPTY_ARRAY_FLAG_'
Loop
Endif
Return m.lcNume
Endfor
If Type('toParsed.array') = 'A'
Return toParsed.array
Endif
Return .Null.
Return ''
Endproc
@@ -879,7 +1032,10 @@ DEFINE CLASS AutopassClient AS Custom
Lparameters toObiect, tcProprietate, tuDefault
If Vartype(m.toObiect) = 'O' And Type('toObiect.' + m.tcProprietate) # 'U'
Return Evl(toObiect.&tcProprietate, m.tuDefault)
*-- proprietatea exista -> intoarce valoarea ei reala. NU folosim EVL: cere ambele
*-- argumente de ACELASI tip (ex. held=0 numeric vs default .F. logic -> "argument
*-- invalid") si ar inlocui gresit valori valide considerate "empty" (index=0) cu default.
Return toObiect.&tcProprietate
Endif
Return m.tuDefault
@@ -923,14 +1079,40 @@ ENDFUNC && autopass_obs
*****************************************************************************************
* US-005: Situatia prezentarilor - citita direct din gateway (GET /v1/prezentari),
* FARA nicio evidenta locala. Cursorul temporar crs_autopass_situatie se reconstruieste
* la fiecare apel; la eroare HTTP/parse ramane gol si functia intoarce .F. (eroarea se
* logheaza prin AutopassClient.LogEroare, fara sa logheze cheia API).
* US-005: Situatia prezentarilor - citita direct din gateway, FARA nicio evidenta locala.
* Cursorul temporar crs_autopass_situatie se reconstruieste la fiecare apel; la eroare
* HTTP/parse ramane gol si functia intoarce .F. (eroarea se logheaza prin
* AutopassClient.LogEroare, fara sa logheze cheia API).
*
* *** GAP DE CONTRACT - BLOCANT PRD US-005 (risc documentat in PRD sect. 5 si Val 0) ***
* La data scrierii, GET /v1/prezentari (rar-autopass app/api/v1/router.py:506-540, functia
* list_prezentari) NU avea filtru de perioada: accepta doar `status` (un singur status,
* optional) si `limit` (implicit 100), rezultatele fiind sortate descrescator dupa id (nu
* dupa data_prestatie). Gap-ul se rezolva ACUM in gateway pe branch-ul
* feature/list-prezentari-filtru-perioada (parametri noi `data_de`/`data_pana` pe
* data_prestatie, format YYYY-MM-DD, restul structurii raspunsului neschimbat).
* Clientul de mai jos trimite deja `data_de`/`data_pana` (belt-and-suspenders, compatibil
* cu AMBELE versiuni de gateway):
* - gateway VECHI (fara filtru): parametrii necunoscuti sunt ignorati de FastAPI -
* filtrarea client-side de mai jos ramane singura activa;
* limita lnLimita ramane posibil trunchianta - vezi avertismentul din log mai jos.
* - gateway NOU (cu filtru): serverul filtreaza deja pe perioada - limita lnLimita nu
* mai trunchiaza perioada ceruta, filtrarea client-side devine redundanta dar inofensiva
* (re-verifica acelasi interval, deja garantat de server).
* US-004 trebuie sa trateze situatia ca posibil incompleta pana se confirma ca gateway-ul
* din productie ruleaza versiunea cu filtru.
*
* Envelope confirmat din contract (app/api/v1/router.py:540): {"submissions": [...]},
* fiecare element cu "id" (NU "submission_id"), "status", "id_prezentare", "rar_env",
* "held" si un sub-obiect "prezentare" (payload_view.prezentare_din_payload, linia 149)
* cu vehicul_nr/vin/data_prestatie/obs. NU exista camp "motiv" in listare (doar in
* GET pe un singur id, via rar_error - vezi StatusPrezentare) - aici se aproximeaza
* doar din "held".
*****************************************************************************************
FUNCTION autopass_situatie
LPARAMETERS tdD1, tdD2
LOCAL lcCursor, oClient, lcD1, lcD2, lcPath, loHttpRez, loParsed, laLista, i, loElem
LPARAMETERS tdD1, tdD2, tcEroare
LOCAL lcCursor, oClient, lcD1, lcD2, lcPath, loHttpRez, loParsed, lnNr, i, loElem
LOCAL loPrezentare, lcDataPrest, lnLimita, lcMotiv, llHeld, luHeld
lcCursor = 'crs_autopass_situatie'
IF USED(lcCursor)
@@ -941,44 +1123,94 @@ CREATE CURSOR (lcCursor) ;
(submission_id C(60), vin C(20), nr_inmatriculare C(15), data_prestatie C(10), ;
status C(30), motiv C(254), id_prezentare C(60), obs C(120))
*-- date construite explicit YYYY-MM-DD, niciodata DTOC/DTOS (depind de SET DATE)
*-- date construite explicit YYYY-MM-DD, niciodata DTOC/DTOS (depind de SET DATE);
*-- format direct comparabil ca string cu data_prestatie pentru filtrul client-side
lcD1 = STR(YEAR(tdD1),4) + '-' + PADL(MONTH(tdD1),2,'0') + '-' + PADL(DAY(tdD1),2,'0')
lcD2 = STR(YEAR(tdD2),4) + '-' + PADL(MONTH(tdD2),2,'0') + '-' + PADL(DAY(tdD2),2,'0')
lcPath = '/v1/prezentari?data_de=' + lcD1 + '&data_pana=' + lcD2
*-- belt-and-suspenders (vezi comentariul-avertisment de mai sus): trimitem data_de/data_pana
*-- pentru gateway-ul nou cu filtru server-side, dar cerem si cel mai mare lot rezonabil,
*-- fara parametru status (toate starile), si filtram local - ramane corect si pe un
*-- gateway vechi care ignora data_de/data_pana
lnLimita = 1000
lcPath = '/v1/prezentari?limit=' + TRANSFORM(lnLimita) + '&data_de=' + lcD1 + '&data_pana=' + lcD2
oClient = CREATEOBJECT('AutopassClient')
loHttpRez = oClient.Get(lcPath, 'Situatie')
IF !loHttpRez.lSucces
oClient.LogEroare('Situatie', -1, loHttpRez.nStatusHttp, loHttpRez.cRaspuns)
tcEroare = autopass_mesaj_eroare_http(loHttpRez.nStatusHttp)
RETURN .F.
ENDIF
loParsed = Nfjsonread(loHttpRez.cRaspuns)
IF VARTYPE(loParsed) <> 'O'
oClient.LogEroare('Situatie', -1, loHttpRez.nStatusHttp, loHttpRez.cRaspuns)
tcEroare = 'Raspuns invalid (JSON malformat) de la gateway-ul AUTOPASS.'
RETURN .F.
ENDIF
laLista = oClient.ExtrageListaRezultate(loParsed)
IF VARTYPE(laLista) <> 'A'
oClient.LogEroare('Situatie', -1, loHttpRez.nStatusHttp, loHttpRez.cRaspuns)
RETURN .F.
*-- Numarul de submisii. ATENTIE: TYPE('loParsed.submissions') NU e de incredere - pe un
*-- array de OBIECTE, VFP intoarce tipul primului element ('O'), nu 'A'; verificarea veche
*-- `<> 'A'` raporta fals "fara lista de submisii" pe raspunsuri valide (confirmat in
*-- debugger). Numaram direct cu ALEN. Array gol (flag nfjson '_EMPTY_ARRAY_FLAG_') sau
*-- proprietate lipsa = zero submisii: rezultat VALID (cursor gol), NU eroare.
lnNr = 0
IF TYPE('loParsed.submissions(1)') <> 'U'
IF TYPE('loParsed.submissions(1)') = 'C' AND loParsed.submissions(1) == '_EMPTY_ARRAY_FLAG_'
lnNr = 0
ELSE
lnNr = ALEN(loParsed.submissions, 1)
ENDIF
ENDIF
IF lnNr >= lnLimita AND TYPE('goLog') = 'O'
*-- posibila trunchiere - vezi avertismentul de gap din capul functiei
goLog.Log('autopass_situatie: raspunsul a atins limita de ' + TRANSFORM(lnLimita) + ;
' submisii - situatia pe perioada poate fi incompleta (gateway fara filtru de perioada)', PROGRAM())
ENDIF
SELECT (lcCursor)
FOR i = 1 TO ALEN(laLista, 1)
loElem = laLista(i)
FOR i = 1 TO lnNr
loElem = loParsed.submissions(m.i)
loPrezentare = oClient.GetProprietate(loElem, 'prezentare', .NULL.)
lcDataPrest = ''
IF VARTYPE(loPrezentare) = 'O'
lcDataPrest = ALLTRIM(oClient.GetProprietate(loPrezentare, 'data_prestatie', ''))
ENDIF
*-- filtru client-side pe perioada - compensatie temporara, vezi avertismentul de gap
IF EMPTY(lcDataPrest) OR lcDataPrest < lcD1 OR lcDataPrest > lcD2
LOOP
ENDIF
*-- 'held' poate veni ca boolean (.T./.F.), numeric (0/1) sau string din gateway.
*-- IIF cere primul argument LOGIC - un numeric (held=0) arunca "Function argument
*-- value, type, or count is invalid". Normalizam la logic inainte de IIF.
luHeld = oClient.GetProprietate(loElem, 'held', .F.)
DO CASE
CASE VARTYPE(luHeld) = 'L'
llHeld = luHeld
CASE VARTYPE(luHeld) = 'N'
llHeld = (luHeld <> 0)
CASE VARTYPE(luHeld) = 'C'
llHeld = INLIST(UPPER(ALLTRIM(luHeld)), 'TRUE', '1', 'T', 'Y')
OTHERWISE
llHeld = .F.
ENDCASE
lcMotiv = IIF(llHeld, 'Retinut (Auto OFF)', '')
INSERT INTO (lcCursor) (submission_id, vin, nr_inmatriculare, data_prestatie, status, motiv, id_prezentare, obs) VALUES ( ;
oClient.GetProprietate(loElem, 'submission_id', ''), ;
oClient.GetProprietate(loElem, 'vin', ''), ;
oClient.GetProprietate(loElem, 'nr_inmatriculare', ''), ;
oClient.GetProprietate(loElem, 'data_prestatie', ''), ;
TRANSFORM(oClient.GetProprietate(loElem, 'id', '')), ;
IIF(VARTYPE(loPrezentare) = 'O', ALLTRIM(oClient.GetProprietate(loPrezentare, 'vin', '')), ''), ;
IIF(VARTYPE(loPrezentare) = 'O', ALLTRIM(oClient.GetProprietate(loPrezentare, 'vehicul_nr', '')), ''), ;
lcDataPrest, ;
oClient.GetProprietate(loElem, 'status', ''), ;
oClient.GetProprietate(loElem, 'motiv', ''), ;
oClient.GetProprietate(loElem, 'id_prezentare', ''), ;
oClient.GetProprietate(loElem, 'obs', ''))
lcMotiv, ;
TRANSFORM(oClient.GetProprietate(loElem, 'id_prezentare', '')), ;
IIF(VARTYPE(loPrezentare) = 'O', ALLTRIM(oClient.GetProprietate(loPrezentare, 'obs', '')), ''))
ENDFOR
RETURN .T.
@@ -995,36 +1227,14 @@ ENDFUNC && autopass_situatie
FUNCTION autopass_coreleaza_situatie
LPARAMETERS tcCursorGrid
LOCAL lcObsCurent, lcDataPrest, lcVin, llGasit
LOCAL laFields[1], lnFieldCount, i, llAreStatus, llAreMotiv, llAreIdPrez
IF EMPTY(tcCursorGrid) OR !USED(tcCursorGrid)
RETURN .F.
ENDIF
lnFieldCount = AFIELDS(laFields, tcCursorGrid)
llAreStatus = .F.
llAreMotiv = .F.
llAreIdPrez = .F.
FOR i = 1 TO lnFieldCount
DO CASE
CASE UPPER(laFields[i,1]) == 'STATUS_AUTOPASS'
llAreStatus = .T.
CASE UPPER(laFields[i,1]) == 'MOTIV_AUTOPASS'
llAreMotiv = .T.
CASE UPPER(laFields[i,1]) == 'ID_PREZENTARE_AUTOPASS'
llAreIdPrez = .T.
ENDCASE
ENDFOR
IF !llAreStatus
ALTER TABLE (tcCursorGrid) ADD COLUMN status_autopass C(30)
ENDIF
IF !llAreMotiv
ALTER TABLE (tcCursorGrid) ADD COLUMN motiv_autopass C(254)
ENDIF
IF !llAreIdPrez
ALTER TABLE (tcCursorGrid) ADD COLUMN id_prezentare_autopass C(60)
ENDIF
*-- status_autopass/motiv_autopass/id_prezentare_autopass sunt incluse direct la crearea
*-- cursorului in autopass_cursor_comenzi() (vezi comentariul de-acolo) - nu mai adaugam
*-- prin ALTER TABLE aici (cursorul ramane "remote"-tainted, ALTER TABLE pica oricum).
IF !USED('crs_autopass_situatie')
* situatia gateway-ului nu e incarcata - toate randurile raman "netransmis"
@@ -1214,11 +1424,28 @@ ENDFUNC && export_operatii_csv
*****************************************************************************************
* Punct de intrare US-004 (forma frm_autopass.scx - lansata din xmenu ofundal_dev.vcx).
* Placeholder pana la construirea formei in VFP IDE (val 3 din PRD) - momentan doar
* deschide un mesaj, ca meniul sa poata fi cablat si testat inaintea formei complete.
* Punct de intrare US-004: lanseaza fereastra "Trimitere AUTOPASS" (frm_autopass.scx),
* apelata din xmenu-ul din Clase\ofundal_dev.vcx (Page2.Cw8.do_actiune). Fereastra se
* lanseaza cu numele scurt (fara cale), la fel ca restul aplicatiei - vezi
* "Do Form frm_filtru_text" in oproceduri_devize.prg/oproceduri_vizualizare.prg -
* rezolvarea cade pe SET PATH ADDITIVE spre gcAppPath+'FERESTRE' facut la pornire in
* roaauto.prg (linia ~87), nu pe o cale explicita.
* Guard pe FILE(): shell-ul frm_autopass.scx se creeaza manual in VFP IDE de Marius si
* necesita write-back-ul foxbin2prg (textul .sc2 e deja livrat de lane 3 in cache) - pana
* atunci, xmenu-ul din ofundal_dev.vcx nu trebuie sa crape daca optiunea e aleasa inaintea
* acestor pasi manuali.
*****************************************************************************************
FUNCTION trimitere_autopass
MESSAGEBOX('Fereastra Trimitere AUTOPASS (US-004) nu este inca construita in VFP IDE.', 48, 'AUTOPASS')
RETURN .F.
LOCAL lcCaleForma
lcCaleForma = gcAppPath + 'FERESTRE\frm_autopass.scx'
IF !FILE(lcCaleForma)
MESSAGEBOX('Forma frm_autopass nu exista inca - creeaza shell-ul in VFP IDE si ruleaza write-back-ul foxbin2prg.', 48, 'AUTOPASS')
RETURN .F.
ENDIF
DO FORM frm_autopass
RETURN .T.
ENDFUNC && trimitere_autopass

View File

@@ -64,7 +64,7 @@ ELSE
USE IN gest_permis
ENDIF
ENDIF
SET STEP ON
* initializare variabile publice gnInchidereValidare1/2/3/4/5/6/7
=cctipuri_deviz()
SELECT crstipuri_deviz

View File

@@ -101,6 +101,7 @@ lcPath = gcAppPath + 'Date;' + ;
gcAppPath + 'COMUN\UTILE\HPDF;' + ;
gcAppPath + 'COMUN\UTILE\HPDF\REPORTOUTPUT;' + ;
gcAppPath + 'COMUN\UTILE\WEB;' + ;
gcAppPath + 'COMUN\UTILE\NFJSON;' + ;
Addbs(Substr(gcAppPath,1,Rat([\],gcAppPath,2)))+[COMUNROA\]
SET PATH TO &lcPath ADDITIVE
@@ -197,6 +198,12 @@ SET PROCEDURE TO xdate.prg Additive
SET PROCEDURE TO validare.prg Additive
SET PROCEDURE TO regex.prg Additive
*!* modificare AUTOPASS (PRD ROAAUTO-1.0) - transmitere operatii la RAR prin gateway
Set Procedure To oproceduri_autopass Additive
Set Procedure To nfjsoncreate.prg Additive
Set Procedure To nfjsonread.prg Additive
*!* modificare AUTOPASS ^
SET PROCEDURE TO excelxml.prg ADDITIVE
Declare Integer GetPrivateProfileString In Kernel32 ;

View File

@@ -0,0 +1,196 @@
*****************************************************************************************
* test_autopass_driver.prg
* Driver .prg fara forma pentru US-003 (client AutopassClient) - PRD ROAAUTO-1.0,
* sectiunea "Acoperire de test": "driver .prg fara forma (1 comanda hand-built) - E2E:
* acopera encoding/headers/timeouts izolat".
*
* Ruleaza autonom din Command Window:
* DO Programe\test_autopass_driver.prg
*
* Ce face:
* (a) apeleaza autopass_test_conexiune() si afiseaza rezultatul (ping)
* (b) construieste MANUAL (fara Oracle) 2 obiecte comanda in formatul asteptat de
* AutopassClient.EnviaLot - una din ele cu operatii cu diacritice in denumire,
* ca sa acopere codepath-ul de encoding
* (c) apeleaza Valideaza (dry-run) pe cele 2 comenzi
* (d) doar cu confirmare MESSAGEBOX explicita SI doar daca mediul citit din config e
* "test", apeleaza si TrimiteLot - altfel se opreste dupa dry-run
* (e) afiseaza raspunsurile per element (index, id_ordl, status, motiv, submission_id)
*
* Nu se apeleaza din meniul aplicatiei - e un instrument de diagnostic, lansat manual.
*****************************************************************************************
CLOSE ALL
CLEAR
SET PROCEDURE TO Programe\oproceduri_autopass.prg ADDITIVE
SET PROCEDURE TO COMUN\utile\nfjson\nfjsoncreate.prg ADDITIVE
SET PROCEDURE TO COMUN\utile\nfjson\nfjsonread.prg ADDITIVE
LOCAL loPing, oClient, loComenzi, loRezValidare, loRezTrimite, loConfig
LOCAL lnAlegere, i, loItem
?'============================================================================='
?'AUTOPASS - driver de test fara forma'
?'============================================================================='
*-- (a) test conexiune (GET /v1/ping) -------------------------------------------------
?
?'--- (a) autopass_test_conexiune() ---'
loPing = autopass_test_conexiune()
?' lSucces : ' + TRANSFORM(loPing.lSucces)
?' cMediu : ' + loPing.cMediu
?' lAutentificatCuCheie : ' + TRANSFORM(loPing.lAutentificatCuCheie)
?' lAreCredsTest : ' + TRANSFORM(loPing.lAreCredsTest)
?' lAreCredsProd : ' + TRANSFORM(loPing.lAreCredsProd)
?' cEroare : ' + loPing.cEroare
IF !loPing.lSucces
?
?'Ping esuat - verifica AUTOPASS_URL/AUTOPASS_KEY in tabelul optiuni inainte de a continua.'
?'Driverul continua totusi (poate testa doar constructia payload-ului si Valideaza va esua controlat).'
ENDIF
*-- (b) construim manual 2 "comenzi", fara Oracle --------------------------------------
?
?'--- (b) construire manuala a comenzilor de test ---'
loComenzi = Createobject('Collection')
*-- comanda 1: fara diacritice, caz de baza
loItem = Createobject('empty')
AddProperty(loItem, 'id_ordl', 900001)
AddProperty(loItem, 'vin', 'WVWZZZ1KZAW000001')
AddProperty(loItem, 'nrinmat', 'B99TST01')
AddProperty(loItem, 'data_prestatie', '2026-07-01')
AddProperty(loItem, 'odometru_final', '123456')
LOCAL ARRAY laPrestatii1(2)
laPrestatii1(1) = Createobject('empty')
AddProperty(laPrestatii1(1), 'cod_op_service', 'SCH001')
AddProperty(laPrestatii1(1), 'denumire', 'Schimb ulei motor')
laPrestatii1(2) = Createobject('empty')
AddProperty(laPrestatii1(2), 'cod_op_service', 'FRN010')
AddProperty(laPrestatii1(2), 'denumire', 'Verificare frane')
AddProperty(loItem, 'prestatii(2)')
Acopy(laPrestatii1, loItem.prestatii)
AddProperty(loItem, 'obs', autopass_obs(90001, 190001))
loComenzi.Add(loItem)
*-- comanda 2: cu diacritice in denumirea operatiei - acopera codepath-ul de encoding
loItem = Createobject('empty')
AddProperty(loItem, 'id_ordl', 900002)
AddProperty(loItem, 'vin', 'WVWZZZ1KZAW000002')
AddProperty(loItem, 'nrinmat', 'B99TST02')
AddProperty(loItem, 'data_prestatie', '2026-07-02')
AddProperty(loItem, 'odometru_final', '87654')
LOCAL ARRAY laPrestatii2(2)
laPrestatii2(1) = Createobject('empty')
AddProperty(laPrestatii2(1), 'cod_op_service', 'DIAC01')
* diacritice romanesti in text sursa (comentariile raman fara diacritice, dar
* denumirea trimisa in payload TREBUIE sa contina diacriticele reale, ca sa testeze
* efectiv encoding-ul):
AddProperty(laPrestatii2(1), 'denumire', '<27>nlocuire ambreiaj - verificare etan<61>s la str<74>ngere')
laPrestatii2(2) = Createobject('empty')
AddProperty(laPrestatii2(2), 'cod_op_service', 'DIAC02')
AddProperty(laPrestatii2(2), 'denumire', 'Reglaj geometrie ro<72>i fa<66>a/spate')
AddProperty(loItem, 'prestatii(2)')
Acopy(laPrestatii2, loItem.prestatii)
AddProperty(loItem, 'obs', autopass_obs(90002, 190002))
loComenzi.Add(loItem)
?' Comenzi construite: ' + TRANSFORM(loComenzi.Count)
FOR i = 1 TO loComenzi.Count
loItem = loComenzi.Item(i)
?' [' + TRANSFORM(i) + '] id_ordl=' + TRANSFORM(loItem.id_ordl) + ;
' vin=' + loItem.vin + ' obs="' + loItem.obs + '"'
ENDFOR
*-- (c) Valideaza (dry-run) -------------------------------------------------------------
?
?'--- (c) AutopassClient.Valideaza() [dry-run, fara enqueue] ---'
oClient = Createobject('AutopassClient')
loRezValidare = .NULL.
TRY
loRezValidare = oClient.Valideaza(loComenzi)
CATCH TO loEx
?' EROARE la Valideaza(): ' + loEx.Message
ENDTRY
IF VARTYPE(loRezValidare) = 'O'
?' lSucces : ' + TRANSFORM(loRezValidare.lSucces)
?' nStatusHttp : ' + TRANSFORM(loRezValidare.nStatusHttp)
?' cDetaliu : ' + loRezValidare.cDetaliu
IF loRezValidare.lSucces AND VARTYPE(loRezValidare.oRezultate) = 'O'
?' Rezultate dry-run:'
FOR EACH loItem IN loRezValidare.oRezultate
?' index=' + TRANSFORM(loItem.index) + ;
' id_ordl=' + TRANSFORM(loItem.id_ordl) + ;
' valid=' + TRANSFORM(loItem.valid) + ;
' status_estimat=' + TRANSFORM(EVL(loItem.status_estimat,'')) + ;
' nemapate=' + IIF(TYPE('loItem.nemapate') = 'A', TRANSFORM(ALEN(loItem.nemapate,1)), '0')
ENDFOR
ENDIF
ENDIF
*-- (d) TrimiteLot - DOAR cu confirmare explicita SI DOAR daca mediul e test -------------
?
?'--- (d) AutopassClient.TrimiteLot() [enqueue real - necesita confirmare] ---'
loConfig = autopass_get_config()
IF UPPER(ALLTRIM(loConfig.cRarEnv)) <> 'TEST'
?' Mediul curent din configurare (AUTOPASS_ENV) nu este "test" (este: "' + ;
loConfig.cRarEnv + '") - TrimiteLot NU se apeleaza din driver.'
?' Seteaza AUTOPASS_ENV=test in tabelul optiuni daca vrei sa testezi enqueue-ul real.'
ELSE
lnAlegere = MESSAGEBOX('Mediul este TEST. Trimiti (enqueue real) cele ' + ;
TRANSFORM(loComenzi.Count) + ' comenzi de test la gateway-ul AUTOPASS?' + CHR(13) + ;
'Aceasta e o transmitere reala pe mediul de test (nu Productie).', 4 + 32, 'AUTOPASS - confirmare TrimiteLot')
IF lnAlegere = 6 && Yes
loRezTrimite = .NULL.
TRY
loRezTrimite = oClient.TrimiteLot(loComenzi)
CATCH TO loEx
?' EROARE la TrimiteLot(): ' + loEx.Message
ENDTRY
IF VARTYPE(loRezTrimite) = 'O'
?' lSucces : ' + TRANSFORM(loRezTrimite.lSucces)
?' nStatusHttp : ' + TRANSFORM(loRezTrimite.nStatusHttp)
?' cDetaliu : ' + loRezTrimite.cDetaliu
IF loRezTrimite.lSucces AND VARTYPE(loRezTrimite.oRezultate) = 'O'
?' Rezultate TrimiteLot:'
FOR EACH loItem IN loRezTrimite.oRezultate
?' index=' + TRANSFORM(loItem.index) + ;
' id_ordl=' + TRANSFORM(loItem.id_ordl) + ;
' status=' + TRANSFORM(loItem.status) + ;
' motiv=' + TRANSFORM(loItem.motiv) + ;
' submission_id=' + TRANSFORM(loItem.submission_id) + ;
' id_prezentare=' + TRANSFORM(loItem.id_prezentare)
ENDFOR
ENDIF
ENDIF
ELSE
?' Renuntat de utilizator - TrimiteLot nu s-a apelat.'
ENDIF
ENDIF
?
?'============================================================================='
?'Driver terminat.'
?'============================================================================='
RETURN

View File

@@ -13,4 +13,18 @@ Notițele sunt organizate pe fișiere separate, câte un subiect pe fișier (ex.
## Fișiere
- [cautare_vcx_vct.md](cautare_vcx_vct.md) — cum se caută cod în binarele `.vcx`/`.scx` (conversie text + grep pe cache)
Insight-uri valabile doar pentru ROAAUTO stau aici. Insight-urile valabile pe orice proiect
ROA* (convenții, capcane de tooling) stau în `COMUN/docs/` (repo separat, partajat) — vezi mai
jos.
### Specifice ROAAUTO
(niciunul încă — primele insight-uri de proiect au fost, până acum, toate transversale și au
migrat în `COMUN/docs/`)
### Partajate (COMUN/docs/, valabile pe toate proiectele ROA*)
- [cautare_vcx_vct.md](../COMUN/docs/cautare_vcx_vct.md) — cum se caută cod în binarele `.vcx`/`.scx` (conversie text + grep pe cache)
- [conventie_encoding_cp1252.md](../COMUN/docs/conventie_encoding_cp1252.md) — capcană: editarea `.sc2`/`.vc2` cu encoding greșit corupe ireversibil diacriticele
- [conventie_ux_formulare.md](../COMUN/docs/conventie_ux_formulare.md) — cum îi place lui Marius formatat un formular nou (grupare, spațiere, culoare = semnal)
- [conventie_goexecutor_alter_table.md](../COMUN/docs/conventie_goexecutor_alter_table.md) — capcană: `ALTER TABLE` pe cursorul întors de `goExecutor.oExecute()` pică; include coloanele noi direct în SELECT

View File

@@ -1,127 +0,0 @@
# Căutare în codul din librăriile/formele VFP (`.vcx`/`.vct`, `.scx`/`.sct`, ...)
Cum se caută cod-sursă (proceduri, metode, parametri) care **nu** stă în fișiere `.prg`, ci
în interiorul fișierelor binare VFP: librării de clase (`.vcx`+`.vct`), forme (`.scx`+`.sct`),
rapoarte (`.frx`+`.frt`), meniuri (`.mnx`+`.mnt`), baze de date (`.dbc`+`.dct`).
## De ce e nevoie de asta
Un `.vcx` este de fapt o **tabelă DBF**: codul metodelor stă în câmpuri memo în `.vct`. Un
`grep` direct pe `.vct` *găsește* textul, dar îl vede ca fișier binar (octeți `\0` între linii),
fără numere de linie utile și greu de citit. Soluția: convertim binarul în reprezentarea lui
**TEXT** (`.vc2`, `.sc2`, …) — un PRG-style lizibil, cu metode sortate alfabetic — și căutăm
în text cu `grep`/`Grep`.
> **Regula de aur:** textul generat e **doar pentru citire/căutare**. Modificările de cod se fac
> tot în **IDE-ul VFP** (vezi `CLAUDE.md`). Nu edita `.vcx/.vct` pe text și nu pune textul înapoi
> în binar pentru aceste scopuri.
## Unelte disponibile (verificate)
| Unealtă | Cale | Rol | Recomandare |
|---|---|---|---|
| **FoxBin2Prg** | `D:\ROA\UTIL\foxbin2prg\FoxBin2Prg.EXE` | Binar VFP ⇄ text bidirecțional (`.vc2/.sc2/.fr2/.mn2/.dc2/.lb2`) | **De folosit.** Complet, fidel, include corpul metodelor, parametri, comentarii |
| vcx2prg | `D:\ROA\UTIL\vcx2prg\x2prg.prg` | Doar `.vcx``.prg` *compilabil* | Alternativă; mai vechi, doar clase, necesită IDE VFP deschis pentru a rula forma |
| prgtovcx | `D:\ROA\UTIL\prgtovcx\prgtovcx.PRG` | Invers: `.prg``.vcx` | **Nu** pentru căutare (e direcția opusă) |
FoxBin2Prg e prima alegere: rulează din linia de comandă fără IDE deschis (EXE compilat), acoperă
toate tipurile de container, și produce text mult mai fidel decât vcx2prg.
> Dacă `FoxBin2Prg.EXE` lipsește, se compilează o singură dată: deschide
> `D:\ROA\UTIL\foxbin2prg\foxbin2prg.pj2` în VFP 9 și *Build > Build Executable*. Lângă EXE trebuie
> să existe `filename_caps.exe` și fișierele `props*.txt` (sunt deja în folder).
## Mod recomandat: scriptul `vcx2txt.ps1`
`D:\ROA\UTIL\foxbin2prg\vcx2txt.ps1` automatizează tot: copiază binarul + memo-ul într-un
**cache** care oglindește structura proiectului (ca să nu murdărească arborele SVN), rulează
FoxBin2Prg, păstrează doar fișierul text. E **incremental** (sare peste ce e deja la zi).
Pentru ROAAUTO, scriptul trebuie parametrizat cu proiectul curent și un cache separat (nu-l
amesteca cu cel al altor proiecte ROA, ex. ROAACNPRO):
```powershell
# DOAR fișierele proiectului ROAAUTO (recomandat, rapid) -> cache text propriu
powershell -ExecutionPolicy Bypass -File D:\ROA\UTIL\foxbin2prg\vcx2txt.ps1 `
-Project D:\ROA\ROAAUTO\roaauto.pjx -ProjectRoot D:\ROA\ROAAUTO `
-CacheRoot D:\ROA\UTIL\foxbin2prg\_textcache_roaauto
# include și rapoartele (.frx) și meniurile (.mnx)
powershell -ExecutionPolicy Bypass -File D:\ROA\UTIL\foxbin2prg\vcx2txt.ps1 `
-Project D:\ROA\ROAAUTO\roaauto.pjx -ProjectRoot D:\ROA\ROAAUTO `
-CacheRoot D:\ROA\UTIL\foxbin2prg\_textcache_roaauto -Types vcx,scx,frx,mnx
# conversie punctuală a unui fișier sau folder (chiar dacă NU e în proiect)
powershell -ExecutionPolicy Bypass -File D:\ROA\UTIL\foxbin2prg\vcx2txt.ps1 -Source D:\ROA\ROAAUTO\COMUN\clase\caut.vcx
# rebuild curat (șterge cache-ul întâi) / forțează reconversia
powershell -ExecutionPolicy Bypass -File D:\ROA\UTIL\foxbin2prg\vcx2txt.ps1 `
-Project D:\ROA\ROAAUTO\roaauto.pjx -ProjectRoot D:\ROA\ROAAUTO `
-CacheRoot D:\ROA\UTIL\foxbin2prg\_textcache_roaauto -Clean
powershell -ExecutionPolicy Bypass -File D:\ROA\UTIL\foxbin2prg\vcx2txt.ps1 `
-Project D:\ROA\ROAAUTO\roaauto.pjx -ProjectRoot D:\ROA\ROAAUTO `
-CacheRoot D:\ROA\UTIL\foxbin2prg\_textcache_roaauto -Force
```
Rezultatul ajunge în `D:\ROA\UTIL\foxbin2prg\_textcache_roaauto\` cu aceeași structură de
directoare ca proiectul (ex.: `_textcache_roaauto\Clase\odevize.vc2`,
`_textcache_roaauto\comun\clase\_frm_base.vc2`). Apoi:
```bash
# cu unealta Grep (preferat) sau grep:
grep -rn "PROCEDURE do_calcul_tarife" "D:\ROA\UTIL\foxbin2prg\_textcache_roaauto"
grep -rln "factura_salvare" "D:\ROA\UTIL\foxbin2prg\_textcache_roaauto" # ce clase ating subiectul
```
Flux tipic pentru o problemă: rulează scriptul cu parametrii de mai sus o dată (populează
cache-ul), apoi caută cu `Grep` în `_textcache_roaauto`. La sesiuni următoare rescrie doar ce
s-a schimbat (incremental). Dacă ai nevoie de o librărie din `COMUN\` care nu e în proiect,
convertește-o punctual cu `-Source`.
## Alternativă manuală (fără script)
FoxBin2Prg scrie textul **lângă sursă**. Ca să nu murdărești `Clase/`, copiază perechea
binar+memo într-un folder temporar și convertește acolo:
```powershell
# .vcx -> .vc2 (cType gol = extensia decide direcția BIN2PRG)
Copy-Item D:\ROA\ROAAUTO\Clase\odevize.vcx, D:\ROA\ROAAUTO\Clase\odevize.vct D:\tmp\
& 'D:\ROA\UTIL\foxbin2prg\FoxBin2Prg.EXE' 'D:\tmp\odevize.vcx' '' '' '' '1' '0' '1'
# => D:\tmp\odevize.vc2
```
Pozițiile parametrilor folosite: `fișier, cType, cTextName, lGenText, cDontShowErrors=1,
cDebug=0, cDontShowProgress=1`. **Lasă `cType` gol** — extensia (`.vcx`) determină direcția.
(Nu da explicit `"BIN2PRG"` pentru un singur fișier: în acel mod EXE-ul așteaptă un *director*.)
Detalii complete de parametri: `D:\ROA\UTIL\foxbin2prg\docs\FoxBin2Prg_Run.md`.
## Soluție rapidă „murdară" (un singur fișier, fără conversie)
Pentru o verificare rapidă „există metoda X undeva în librărie?", `grep` merge direct pe `.vct`
(o vede ca binar, dar găsește potrivirile):
```bash
grep -c "PROCEDURE" "D:\ROA\ROAAUTO\Clase\odevize.VCT"
grep -a -o "PROCEDURE [a-zA-Z0-9_]*" "D:\ROA\ROAAUTO\Clase\odevize.VCT"
```
Bun pentru „da/nu" și nume de metode; pentru citit corpul/contextul, folosește conversia în text.
## Corespondență extensii binar → text
| Container | Binar (+memo) | Text |
|---|---|---|
| Librărie clase | `.vcx` + `.vct` | `.vc2` |
| Formă | `.scx` + `.sct` | `.sc2` |
| Raport | `.frx` + `.frt` | `.fr2` |
| Etichetă | `.lbx` + `.lbt` | `.lb2` |
| Meniu | `.mnx` + `.mnt` | `.mn2` |
| Bază de date | `.dbc` + `.dct` | `.dc2` |
## Note
- Codul aplicației stă mai ales în `Clase\*.vcx`; cea mai mare parte din `.vcx`-uri sunt în
`COMUN\` (framework partajat — vezi `CLAUDE.md`).
- `_textcache_roaauto\` e regenerabil și e în afara arborelui SVN — poate fi șters oricând.
- Conversia **nu** necesită IDE-ul VFP deschis (folosește EXE-ul compilat).

View File

@@ -127,8 +127,8 @@ operatiile lor, **pentru ca** sa vad exact ce urmeaza sa se transmita.
**pentru ca** transmiterea sa fie robusta si raspunsul parsabil.
- **Depinde de**: US-001
- **Fisiere**: `Programe\oproceduri_autopass.prg` (clasa `AutopassClient`); vendored
`nfjsoncreate.prg`/`nfjsonread.prg` in `comun_plugins\utils\` (sursa: `rar-autopass/legacy-vfp/` sau ROAPRELUARECONPRESS)
- **Fisiere**: `Programe\oproceduri_autopass.prg` (clasa `AutopassClient`); biblioteca JSON
`nfjsoncreate.prg`/`nfjsonread.prg` din `COMUN\utile\nfjson\`
- **Acceptance criteria**:
- [ ] POST via `winHTTP.winHTTPrequest.5.1` (pattern `D:\ROA\ems_trimite_facturi_spv.prg``SendeFactura`), headere `Content-Type: application/json` + `X-API-Key`, body UTF-8.
- [ ] Body: `{"prezentari": [...], "rar_env": <din config, optional>}` — prestatii cu `cod_op_service` + `denumire` (maparea ramane in gateway); `raspuns` implicit `minim`.
@@ -378,9 +378,17 @@ Decizii luate (fost intrebari 1-4):
mediul RAR se editeaza direct din fereastra de transmitere (panou Configurare pliabil
in `frm_autopass.scx`), nu dintr-o fereastra separata; salvarea trece prin
`scrie_optiune()` (`autopass_salveaza_config`).
9. **DECIS (revizie 2026-07-10, Marius)****normalizare NRINMAT aprobata**: eliminarea
spatiilor, cratimelor si punctelor din numarul de inmatriculare inainte de validare se
aplica consecvent pe cele 3 canale: gateway `rar-autopass` (validare + idempotency),
clientul VFP (`autopass_valideaza_rand` din `oproceduri_autopass.prg`) si spike-ul SQL de
calitate a datelor (`docs\spike_calitate_date_autopass_v2.sql`, versiune noua fata de
spike-ul v1 rulat initial). Motivatie: spike-ul v1 a aratat doar 13,8% NRINMAT valide din
cauza formatelor reale cu spatii/cratime (ex. "B 99 XYZ"), regula stricta anterioara
(doar strip+upper) fiind prea restrictiva fata de datele istorice reale.
Nu mai exista intrebari deschise — PRD aprobat prin poarta /autoplan (2026-07-09),
revizuit conform deciziilor 7-8.
revizuit conform deciziilor 7-9 (decizia 9 e o completare post-aprobare, 2026-07-10).
## 7. Valuri de executie
@@ -557,5 +565,8 @@ nu se arunca.
- **Revizie post-aprobare (2026-07-09, Marius)**: decizia 7 — zero tabele noi; US-001
rescris pe tabelul `optiuni`, US-005 rescris pe situatia din gateway (cursor temporar);
scripturile SQL revertate.
- **Revizie post-aprobare (2026-07-10, Marius)**: decizia 9 — normalizare NRINMAT (strip
spatii/cratime/puncte) aprobata pe cele 3 canale (gateway, client VFP, spike SQL v2);
motivata de rata scazuta de valid (13,8%) gasita in spike-ul v1.
NO UNRESOLVED DECISIONS

Binary file not shown.

Binary file not shown.