diff --git a/Clase/ofundal_dev.VCT b/Clase/ofundal_dev.VCT index d6e1f03..2486448 100644 Binary files a/Clase/ofundal_dev.VCT and b/Clase/ofundal_dev.VCT differ diff --git a/Clase/ofundal_dev.vcx b/Clase/ofundal_dev.vcx index a45103f..ceab325 100644 Binary files a/Clase/ofundal_dev.vcx and b/Clase/ofundal_dev.vcx differ diff --git a/Ferestre/frm_autopass.SCT b/Ferestre/frm_autopass.SCT new file mode 100644 index 0000000..04d4ed0 Binary files /dev/null and b/Ferestre/frm_autopass.SCT differ diff --git a/Ferestre/frm_autopass.scx b/Ferestre/frm_autopass.scx new file mode 100644 index 0000000..fd11e9f Binary files /dev/null and b/Ferestre/frm_autopass.scx differ diff --git a/Programe/oproceduri_autopass.prg b/Programe/oproceduri_autopass.prg index 0bcab46..19726ba 100644 --- a/Programe/oproceduri_autopass.prg +++ b/Programe/oproceduri_autopass.prg @@ -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 diff --git a/Programe/ovariabile_globale.prg b/Programe/ovariabile_globale.prg index 51a4412..aaea9b1 100644 --- a/Programe/ovariabile_globale.prg +++ b/Programe/ovariabile_globale.prg @@ -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 diff --git a/Programe/roaauto.prg b/Programe/roaauto.prg index fdd79e6..fb49736 100644 --- a/Programe/roaauto.prg +++ b/Programe/roaauto.prg @@ -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 ; diff --git a/Programe/test_autopass_driver.prg b/Programe/test_autopass_driver.prg new file mode 100644 index 0000000..47f4efb --- /dev/null +++ b/Programe/test_autopass_driver.prg @@ -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', 'înlocuire ambreiaj - verificare etanâs la strângere') +laPrestatii2(2) = Createobject('empty') +AddProperty(laPrestatii2(2), 'cod_op_service', 'DIAC02') +AddProperty(laPrestatii2(2), 'denumire', 'Reglaj geometrie roîi faâ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 diff --git a/docs/README.md b/docs/README.md index 6243a1e..638183c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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 diff --git a/docs/cautare_vcx_vct.md b/docs/cautare_vcx_vct.md deleted file mode 100644 index 8deb6ea..0000000 --- a/docs/cautare_vcx_vct.md +++ /dev/null @@ -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). diff --git a/docs/prd/prd-roaauto-1.0-transmitere-autopass.md b/docs/prd/prd-roaauto-1.0-transmitere-autopass.md index 5ead6d8..cc9281f 100644 --- a/docs/prd/prd-roaauto-1.0-transmitere-autopass.md +++ b/docs/prd/prd-roaauto-1.0-transmitere-autopass.md @@ -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": }` — 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 diff --git a/roaauto.pjt b/roaauto.pjt index 140f9ed..09b3646 100644 Binary files a/roaauto.pjt and b/roaauto.pjt differ diff --git a/roaauto.pjx b/roaauto.pjx index 63f2503..18d3e63 100644 Binary files a/roaauto.pjx and b/roaauto.pjx differ