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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ;
|
||||
|
||||
196
Programe/test_autopass_driver.prg
Normal file
196
Programe/test_autopass_driver.prg
Normal 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
|
||||
Reference in New Issue
Block a user