Files
roaauto/Programe/oproceduri_autopass.prg
2026-08-07 14:20:58 +03:00

1489 lines
56 KiB
Plaintext

*****************************************************************************************
* oproceduri_autopass.prg
* Transmitere operatii de service (comenzi inchise + facturate) la RAR prin gateway-ul
* autopass.romfast.ro. Vezi PRD: docs\prd\prd-roaauto-1.0-transmitere-autopass.md
*
* Continut:
* US-001 autopass_get_config / autopass_salveaza_config / autopass_test_conexiune
* US-002 autopass_cursor_comenzi / autopass_cursor_operatii / autopass_dedup_operatii /
* autopass_valideaza_rand / autopass_alfanumeric / autopass_coloana_exista
* US-003 DEFINE CLASS AutopassClient / autopass_obs
* US-005 autopass_situatie / autopass_coreleaza_situatie
* US-006 autopass_export_csv / export_operatii_csv / autopass_cere_perioada
*
* Configurare (US-001): fara tabele noi - AUTOPASS_URL/AUTOPASS_KEY/AUTOPASS_ENV se tin
* in tabelul standard optiuni, citite/scrise prin citeste_optiune()/scrie_optiune() din
* COMUN\programe\oinit_optiuni.prg. Situatia transmiterilor (US-005) nu se persista local -
* se citeste din gateway la fiecare refresh, intr-un cursor temporar.
*
* Fisierul trebuie adaugat ca membru in roaauto.pjx din VFP IDE (Project > Add File) -
* nu se poate face prin editare de text pe binarul .pjx.
*****************************************************************************************
*****************************************************************************************
* US-001: Configurare conexiune AUTOPASS (URL gateway, cheie API, mediu RAR)
*****************************************************************************************
FUNCTION autopass_get_config
LOCAL loConfig, lcUrl, lcKey, lcEnv
loConfig = Createobject('empty')
AddProperty(loConfig, 'cUrlGateway', '')
AddProperty(loConfig, 'cApiKey', '')
AddProperty(loConfig, 'cRarEnv', '')
AddProperty(loConfig, 'lConfigOk', .F.)
lcUrl = ''
lcKey = ''
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(NVL(citeste_optiune('AUTOPASS_URL'),''))
lcKey = ALLTRIM(NVL(citeste_optiune('AUTOPASS_KEY'),''))
lcEnv = ALLTRIM(NVL(citeste_optiune('AUTOPASS_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
loConfig.cRarEnv = lcEnv
* fara URL sau cheie API configurate (panoul Configurare din fereastra de transmitere),
* transmiterea nu poate functiona
loConfig.lConfigOk = !EMPTY(loConfig.cUrlGateway) AND !EMPTY(loConfig.cApiKey)
RETURN loConfig
ENDFUNC && autopass_get_config
*****************************************************************************************
* Salveaza configurarea AUTOPASS din panoul Configurare al ferestrei de transmitere
* (US-004) - scrie cele 3 optiuni prin scrie_optiune (randurile se creeaza la prima
* scriere, nu e nevoie de script de instalare)
*****************************************************************************************
FUNCTION autopass_salveaza_config
LPARAMETERS tcUrl, tcKey, tcEnv
LOCAL lnSucces
lnSucces = scrie_optiune('AUTOPASS_URL', ALLTRIM(NVL(tcUrl,'')), 'URL gateway RAR AUTOPASS')
IF lnSucces >= 0
lnSucces = scrie_optiune('AUTOPASS_KEY', ALLTRIM(NVL(tcKey,'')), 'Cheie API gateway AUTOPASS')
ENDIF
IF lnSucces >= 0
lnSucces = scrie_optiune('AUTOPASS_ENV', LOWER(ALLTRIM(NVL(tcEnv,''))), 'Mediu RAR AUTOPASS (test/prod/gol=implicit cont)')
ENDIF
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, lcUrl, lcKey
loReturn = Createobject('empty')
AddProperty(loReturn, 'lSucces', .F.)
AddProperty(loReturn, 'cMediu', '')
AddProperty(loReturn, 'lAutentificatCuCheie', .F.)
AddProperty(loReturn, 'lAreCredsTest', .F.)
AddProperty(loReturn, 'lAreCredsProd', .F.)
AddProperty(loReturn, 'cEroare', '')
loConfig = autopass_get_config()
*-- 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
ENDIF
lnStatus = 0
lcResponseText = ''
TRY
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', lcKey)
loHTTP.Send()
lnStatus = loHTTP.Status
lcResponseText = loHTTP.ResponseText
CATCH TO loEx
* de regula lipsa retea / timeout / gateway inaccesibil - nu se reincearca automat aici,
* reincercarea (daca e cazul) ramane manuala, initiata de utilizator
loReturn.cEroare = 'Nu s-a putut contacta gateway-ul AUTOPASS: ' + loEx.Message
RETURN loReturn
ENDTRY
IF lnStatus <> 200
loReturn.cEroare = autopass_mesaj_eroare_http(lnStatus)
RETURN loReturn
ENDIF
loJson = Nfjsonread(lcResponseText)
IF VARTYPE(loJson) <> 'O'
loReturn.cEroare = 'Raspuns invalid (JSON malformat) de la gateway-ul AUTOPASS.'
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.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
*****************************************************************************************
* US-002: extragere comenzi + operatii pe perioada
*****************************************************************************************
FUNCTION autopass_cursor_comenzi
LPARAMETERS tdDataInceput, tdDataSfarsit
LOCAL lcCursorHeader, lcD1, lcD2, lcSql, lcFiltruSters, lnSucces
LOCAL llOk, lcMotivRand, ldDataPrest, lcCursorRaw
lcCursorHeader = 'crs_autopass_comenzi'
IF USED(lcCursorHeader)
USE IN (lcCursorHeader)
ENDIF
*-- literalii de data construiti explicit (independent de SET DATE)
lcD1 = "TO_DATE('" + STR(YEAR(tdDataInceput),4) + "-" + PADL(MONTH(tdDataInceput),2,'0') + "-" + PADL(DAY(tdDataInceput),2,'0') + "','YYYY-MM-DD')"
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('AUTO_ISTORIC_COMENZI', 'STERS')
lcFiltruSters = ' AND sters = 0'
ENDIF
lcSql = "SELECT id_ordl, nrord, dataoravalid, datafact, nrfact, nrinmat, series, kmint, nume, tip_comanda " + ;
"FROM auto_istoric_comenzi " + ;
"WHERE validat = 1 AND facturat = 1 AND id_tip IN (1,2) " + ;
"AND datafact BETWEEN " + lcD1 + " AND " + lcD2 + lcFiltruSters
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())
ENDIF
RETURN ''
ENDIF
*-- 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
lcMotivRand = ''
DO CASE
CASE VARTYPE(dataoravalid) = 'T'
ldDataPrest = TTOD(dataoravalid)
CASE VARTYPE(dataoravalid) = 'D'
ldDataPrest = dataoravalid
OTHERWISE
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 nrinmat WITH autopass_normalizeaza_nrinmat(nrinmat), ;
lvalid WITH llOk, motiv_invalid WITH lcMotivRand
ENDSCAN
RETURN lcCursorHeader
ENDFUNC && autopass_cursor_comenzi
*****************************************************************************************
FUNCTION autopass_cursor_operatii
LPARAMETERS tcCursorHeader
LOCAL lcCursorDetaliu, lcCursorCount, lcSql, lcChunkSql, lcIdList, lnSucces
LOCAL lnTotal, lnChunkSize, lnStart, lnEnd, i, lcCursorRaw
LOCAL ARRAY laId[1]
LOCAL lnCnt, lcMotivExist
IF EMPTY(tcCursorHeader) OR !USED(tcCursorHeader)
RETURN ''
ENDIF
lcCursorDetaliu = 'crs_autopass_operatii'
IF USED(lcCursorDetaliu)
USE IN (lcCursorDetaliu)
ENDIF
SELECT (tcCursorHeader)
lnTotal = RECCOUNT()
IF lnTotal = 0
lcSql = "SELECT o.id_ordl, n.codop, n.denop " + ;
"FROM dev_oper o " + ;
"JOIN dev_nom_norme n ON o.id_norme = n.id_norme " + ;
"WHERE 1 = 2"
ELSE
DIMENSION laId[lnTotal]
i = 0
SELECT (tcCursorHeader)
GO TOP
SCAN
i = i + 1
laId[i] = id_ordl
ENDSCAN
lnChunkSize = 500
lcSql = ''
lnStart = 1
DO WHILE lnStart <= lnTotal
lnEnd = MIN(lnStart + lnChunkSize - 1, lnTotal)
lcIdList = ''
FOR i = lnStart TO lnEnd
lcIdList = lcIdList + IIF(EMPTY(lcIdList),'',',') + ALLTRIM(STR(laId[i]))
ENDFOR
lcChunkSql = "SELECT o.id_ordl, n.codop, n.denop " + ;
"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
lnStart = lnEnd + 1
ENDDO
ENDIF
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())
ENDIF
RETURN ''
ENDIF
*-- 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
IF EMPTY(ALLTRIM(IIF(VARTYPE(codop) = 'C', codop, '')))
REPLACE lvalid_op WITH .F., motiv_op WITH 'operatie fara cod'
ELSE
REPLACE lvalid_op WITH .T., motiv_op WITH ''
ENDIF
ENDSCAN
*-- dedup operatii identice (acelasi id_ordl+codop) inainte de transmitere
lcCursorDetaliu = autopass_dedup_operatii(lcCursorDetaliu)
*-- numaram operatiile valide (cu codop) ramase per comanda
lcCursorCount = 'crs_autopass_opcount'
IF USED(lcCursorCount)
USE IN (lcCursorCount)
ENDIF
SELECT id_ordl, COUNT(*) AS ncnt ;
FROM (lcCursorDetaliu) ;
WHERE lvalid_op ;
GROUP BY id_ordl ;
INTO CURSOR (lcCursorCount) READWRITE
INDEX ON id_ordl TAG id_ordl
SET ORDER TO id_ordl IN (lcCursorCount)
*-- marcam in header comenzile ramase fara nicio operatie de manopera valida
SELECT (tcCursorHeader)
SCAN
lnCnt = 0
IF SEEK(id_ordl, lcCursorCount)
lnCnt = &lcCursorCount..ncnt
ENDIF
IF lnCnt = 0
lcMotivExist = ALLTRIM(motiv_invalid)
IF !('fara operatii de manopera' $ lcMotivExist)
REPLACE motiv_invalid WITH lcMotivExist + IIF(EMPTY(lcMotivExist),'','; ') + 'fara operatii de manopera', ;
lvalid WITH .F.
ENDIF
ENDIF
ENDSCAN
USE IN (lcCursorCount)
RETURN lcCursorDetaliu
ENDFUNC && autopass_cursor_operatii
*****************************************************************************************
FUNCTION autopass_dedup_operatii
LPARAMETERS tcCursorDetaliu
LOCAL lcTemp, lcPath
IF EMPTY(tcCursorDetaliu) OR !USED(tcCursorDetaliu)
RETURN tcCursorDetaliu
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, denop, lvalid_op, motiv_op ;
INTO CURSOR (lcTemp) READWRITE
lcPath = DBF(lcTemp)
USE IN (tcCursorDetaliu)
USE (lcPath) AGAIN ALIAS (tcCursorDetaliu) IN 0
SELECT (tcCursorDetaliu)
INDEX ON id_ordl TAG id_ordl
USE IN (lcTemp)
RETURN tcCursorDetaliu
ENDFUNC && autopass_dedup_operatii
*****************************************************************************************
* Validare LOCALA per rand comanda (reutilizabila si de US-004 grid, US-006 CSV)
* Returneaza .T./.F.; motivul (daca invalid) e scris prin referinta in tcMotiv (@)
*****************************************************************************************
FUNCTION autopass_valideaza_rand
LPARAMETERS tcVin, tcNrinmat, tdDataPrestatie, tnKm, tcMotiv
LOCAL lcVin, lcNrinmat, lcMotiv, ldData, llOk
lcMotiv = ''
lcVin = ALLTRIM(UPPER(IIF(VARTYPE(tcVin) = 'C', tcVin, '')))
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
ENDIF
IF EMPTY(lcNrinmat) OR LEN(lcNrinmat) > 10 OR !autopass_alfanumeric(lcNrinmat)
lcMotiv = lcMotiv + IIF(EMPTY(lcMotiv),'','; ') + 'Numar de inmatriculare invalid'
ENDIF
DO CASE
CASE VARTYPE(tdDataPrestatie) = 'T'
ldData = TTOD(tdDataPrestatie)
CASE VARTYPE(tdDataPrestatie) = 'D'
ldData = tdDataPrestatie
OTHERWISE
ldData = {}
ENDCASE
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
llOk = EMPTY(lcMotiv)
tcMotiv = lcMotiv
RETURN llOk
ENDFUNC && autopass_valideaza_rand
*****************************************************************************************
FUNCTION autopass_alfanumeric
LPARAMETERS tcStr
LOCAL i, lcCh, llOk
llOk = !EMPTY(tcStr)
FOR i = 1 TO LEN(tcStr)
lcCh = UPPER(SUBSTR(tcStr, i, 1))
IF !(lcCh $ 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789')
llOk = .F.
EXIT
ENDIF
ENDFOR
RETURN llOk
ENDFUNC && autopass_alfanumeric
*****************************************************************************************
* 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 tcTabel, tcColoana
LOCAL lcCursor, llExista, lnSucces
lcCursor = 'crs_ap_colcheck'
IF USED(lcCursor)
USE IN (lcCursor)
ENDIF
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)
llExista = (&lcCursor..ncnt > 0)
USE IN (lcCursor)
ENDIF
RETURN llExista
ENDFUNC && autopass_coloana_exista
*****************************************************************************************
* US-003: Client HTTP + serializare JSON (POST /v1/prezentari)
*****************************************************************************************
DEFINE CLASS AutopassClient AS Custom
cUrlGateway = ''
cApiKey = ''
cRarEnv = ''
cLogFile = 'LOG.txt'
nMaxLot = 50
*-------------------------------------------------------------------
Procedure Init
*-------------------------------------------------------------------
Local loConfig
loConfig = autopass_get_config()
This.cUrlGateway = Alltrim(loConfig.cUrlGateway)
This.cApiKey = Alltrim(loConfig.cApiKey)
This.cRarEnv = Alltrim(loConfig.cRarEnv)
Endproc
*-------------------------------------------------------------------
Procedure Ping
*-------------------------------------------------------------------
* Testarea conexiunii este implementata in autopass_test_conexiune() (US-001);
* nu o duplicam aici. Metoda exista doar ca AutopassClient sa poata delega la ea
* din contextul clasei, daca apelantul are deja o instanta creata.
Return autopass_test_conexiune()
Endproc
*-------------------------------------------------------------------
Procedure TrimiteLot
*-------------------------------------------------------------------
Lparameters taComenzi
Return This.EnviaLot(m.taComenzi, '/v1/prezentari', 'TrimiteLot')
Endproc
*-------------------------------------------------------------------
Procedure Valideaza
*-------------------------------------------------------------------
Lparameters taComenzi
Return This.EnviaLot(m.taComenzi, '/v1/prezentari/valideaza', 'Valideaza')
Endproc
*-------------------------------------------------------------------
Procedure EnviaLot
*-------------------------------------------------------------------
* Motor comun pentru TrimiteLot (enqueue) si Valideaza (dry-run): construieste
* payload-ul, tine maparea index_in_lot -> id_ordl, apeleaza gateway-ul si
* normalizeaza raspunsul. FARA retry automat - idempotenta e responsabilitatea
* serverului, retrimiterea o initiaza manual utilizatorul (US-004).
Lparameters taComenzi, tcPath, tcAction
Local lnCount, i, lcJson, loBody, loComanda, loPrezentare, loHttpRez
Local loReturn, loParsed, lcNumeLista, lnNr, loColectie, loElem, loRezItem, lnIndexRel
Local Array laPrezentari(1), laIndexMap(1)
lnCount = This.GetNumarElemente(m.taComenzi)
If m.lnCount = 0
Error 'AutopassClient.' + m.tcAction + ': lista de comenzi este goala'
Endif
If m.lnCount > This.nMaxLot
Error 'AutopassClient.' + m.tcAction + ': maxim ' + Transform(This.nMaxLot) + ;
' comenzi per apel (primit ' + Transform(m.lnCount) + ;
'); impartirea pe loturi de ' + Transform(This.nMaxLot) + ;
' este responsabilitatea apelantului (US-004), nu a clientului'
Endif
Dimension laPrezentari(m.lnCount)
Dimension laIndexMap(m.lnCount)
For i = 1 To m.lnCount
loComanda = This.GetElement(m.taComenzi, m.i)
laIndexMap(m.i) = loComanda.id_ordl
loPrezentare = Createobject('empty')
AddProperty(loPrezentare, 'vin', Alltrim(loComanda.vin))
AddProperty(loPrezentare, 'nr_inmatriculare', Alltrim(loComanda.nrinmat))
AddProperty(loPrezentare, 'data_prestatie', Alltrim(loComanda.data_prestatie))
AddProperty(loPrezentare, 'odometru_final', Alltrim(Transform(loComanda.odometru_final)))
This.AsignaListaProprietate(loPrezentare, 'prestatii', loComanda.prestatii)
AddProperty(loPrezentare, 'obs', Alltrim(loComanda.obs))
laPrezentari(m.i) = loPrezentare
Endfor
loBody = Createobject('empty')
This.AsignaListaProprietate(loBody, 'prezentari', laPrezentari)
If !Empty(This.cRarEnv)
AddProperty(loBody, 'rar_env', This.cRarEnv)
Endif
* 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.)
AddProperty(loReturn, 'nStatusHttp', 0)
AddProperty(loReturn, 'cDetaliu', '')
AddProperty(loReturn, 'oRezultate', .Null.)
loHttpRez = This.Post(m.tcPath, m.lcJson, m.tcAction)
loReturn.nStatusHttp = loHttpRez.nStatusHttp
If !loHttpRez.lSucces
loReturn.cDetaliu = loHttpRez.cDetaliu
This.LogEroare(m.tcAction, -1, loHttpRez.nStatusHttp, loHttpRez.cRaspuns)
Return loReturn
Endif
loParsed = Nfjsonread(loHttpRez.cRaspuns)
If Vartype(m.loParsed) # 'O'
loReturn.cDetaliu = 'raspuns JSON invalid de la gateway'
This.LogEroare(m.tcAction, -1, loHttpRez.nStatusHttp, loHttpRez.cRaspuns)
Return loReturn
Endif
* 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 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 m.lnNr
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))
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, 'rar_env', This.GetProprietate(m.loElem, 'rar_env', ''))
This.CopiazaListaProprietate(loRezItem, 'erori', m.loElem, 'erori')
This.CopiazaListaProprietate(loRezItem, 'nemapate', m.loElem, 'nemapate')
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)
Endfor
loReturn.lSucces = .T.
loReturn.oRezultate = loColectie
Return loReturn
Endproc
*-------------------------------------------------------------------
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, loRarError
loReturn = Createobject('empty')
AddProperty(loReturn, 'lSucces', .F.)
AddProperty(loReturn, 'nStatusHttp', 0)
AddProperty(loReturn, 'cDetaliu', '')
AddProperty(loReturn, 'submission_id', m.tcSubmissionId)
AddProperty(loReturn, 'status', '')
AddProperty(loReturn, 'motiv', '')
AddProperty(loReturn, 'id_prezentare', '')
AddProperty(loReturn, 'rar_env', '')
AddProperty(loReturn, 'held', .F.)
loHttpRez = This.Get('/v1/prezentari/' + Alltrim(m.tcSubmissionId), 'StatusPrezentare')
loReturn.nStatusHttp = loHttpRez.nStatusHttp
If !loHttpRez.lSucces
loReturn.cDetaliu = loHttpRez.cDetaliu
This.LogEroare('StatusPrezentare', -1, loHttpRez.nStatusHttp, loHttpRez.cRaspuns)
Return loReturn
Endif
loParsed = Nfjsonread(loHttpRez.cRaspuns)
If Vartype(m.loParsed) # 'O'
loReturn.cDetaliu = 'raspuns JSON invalid de la gateway'
This.LogEroare('StatusPrezentare', -1, loHttpRez.nStatusHttp, loHttpRez.cRaspuns)
Return loReturn
Endif
loReturn.lSucces = .T.
loReturn.status = This.GetProprietate(m.loParsed, 'status', '')
loReturn.id_prezentare = This.GetProprietate(m.loParsed, 'id_prezentare', '')
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
*-------------------------------------------------------------------
* Transport HTTP POST pur - FARA retry automat. In caz de eroare (HTTP != 200
* sau exceptie de retea) nu arunca, intoarce obiect cu lSucces=.F.
Lparameters tcPath, tcJsonBody, tcAction
Local loHTTP As 'winHTTP.winHTTPrequest.5.1'
Local loReturn As "empty", loEx As Exception
Local lcUrl
loReturn = Createobject('empty')
AddProperty(loReturn, 'lSucces', .F.)
AddProperty(loReturn, 'nStatusHttp', 0)
AddProperty(loReturn, 'cRaspuns', '')
AddProperty(loReturn, 'cDetaliu', '')
lcUrl = Alltrim(This.cUrlGateway) + m.tcPath
Try
loHTTP = Createobject('winHTTP.winHTTPrequest.5.1')
loHTTP.Open('POST', m.lcUrl, .F.)
loHTTP.SetTimeouts(30000, 30000, 30000, 30000)
* 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)
loReturn.nStatusHttp = loHTTP.Status
loReturn.cRaspuns = loHTTP.ResponseText
loReturn.lSucces = (loHTTP.Status = 200)
If !loReturn.lSucces
loReturn.cDetaliu = This.ExtrageDetaliuEroare(loReturn.cRaspuns)
Endif
Catch To loEx
loReturn.lSucces = .F.
loReturn.nStatusHttp = 0
loReturn.cDetaliu = 'Eroare comunicare HTTP (' + m.tcAction + '): ' + loEx.Message
Endtry
Return loReturn
Endproc
*-------------------------------------------------------------------
Procedure Get
*-------------------------------------------------------------------
* Transport HTTP GET pur - vezi Post() pentru contractul de raspuns.
Lparameters tcPath, tcAction
Local loHTTP As 'winHTTP.winHTTPrequest.5.1'
Local loReturn As "empty", loEx As Exception
Local lcUrl
loReturn = Createobject('empty')
AddProperty(loReturn, 'lSucces', .F.)
AddProperty(loReturn, 'nStatusHttp', 0)
AddProperty(loReturn, 'cRaspuns', '')
AddProperty(loReturn, 'cDetaliu', '')
lcUrl = Alltrim(This.cUrlGateway) + m.tcPath
Try
loHTTP = Createobject('winHTTP.winHTTPrequest.5.1')
loHTTP.Open('GET', m.lcUrl, .F.)
loHTTP.SetTimeouts(30000, 30000, 30000, 30000)
loHTTP.setRequestHeader('Content-Type', 'application/json')
loHTTP.setRequestHeader('X-API-Key', This.cApiKey)
loHTTP.Send()
loReturn.nStatusHttp = loHTTP.Status
loReturn.cRaspuns = loHTTP.ResponseText
loReturn.lSucces = (loHTTP.Status = 200)
If !loReturn.lSucces
loReturn.cDetaliu = This.ExtrageDetaliuEroare(loReturn.cRaspuns)
Endif
Catch To loEx
loReturn.lSucces = .F.
loReturn.nStatusHttp = 0
loReturn.cDetaliu = 'Eroare comunicare HTTP (' + m.tcAction + '): ' + loEx.Message
Endtry
Return loReturn
Endproc
*-------------------------------------------------------------------
Procedure ExtrageDetaliuEroare
*-------------------------------------------------------------------
Lparameters tcRaspuns
Local loParsed
loParsed = .Null.
Try
loParsed = Nfjsonread(m.tcRaspuns)
Catch
loParsed = .Null.
Endtry
If Vartype(m.loParsed) = 'O' And Type('loParsed.detail') # 'U'
Return Transform(loParsed.detail)
Endif
Return Left(Alltrim(m.tcRaspuns), 500)
Endproc
*-------------------------------------------------------------------
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(6)
Local i, lcNume
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)
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
Return ''
Endproc
*-------------------------------------------------------------------
Procedure AsignaListaProprietate
*-------------------------------------------------------------------
* Adauga o proprietate array/collection unui obiect "empty", respectand
* conventia nfjsoncreate.prg pentru proprietati de tip array (AddProperty
* cu dimensiune + Acopy).
Lparameters toObj, tcPropName, txValue
Local lnCount
If Vartype(m.txValue) = 'A'
lnCount = Max(Alen(m.txValue, 1), 1)
AddProperty(m.toObj, m.tcPropName + '(' + Transform(m.lnCount) + ')')
Acopy(m.txValue, toObj.&tcPropName)
Else
AddProperty(m.toObj, m.tcPropName, m.txValue)
Endif
Endproc
*-------------------------------------------------------------------
Procedure CopiazaListaProprietate
*-------------------------------------------------------------------
* Copiaza pe un obiect-empty destinatie o proprietate a unui obiect-sursa parsat
* de nfjson care poate fi ARRAY (erori[]/nemapate[] din raspunsul Valideaza),
* PASTRAND tot array-ul. GetProprietate NU merge aici: pe un array intoarce doar
* element(1) (o functie VFP nu poate intoarce un array). Array gol (conventie nfjson
* '_EMPTY_ARRAY_FLAG_') sau proprietate lipsa/scalara -> proprietate .Null. pe
* destinatie (consumatorul verifica TYPE(...) = 'A' inainte de a itera). Inversul
* practic al AsignaListaProprietate.
Lparameters toDest, tcPropDest, toSursa, tcPropSursa
Local lnCount
If Vartype(m.toSursa) # 'O' Or Type('toSursa.' + m.tcPropSursa + '(1)') = 'U'
*-- proprietate lipsa sau scalara (nu se poate indexa cu (1)) -> nimic de copiat
AddProperty(m.toDest, m.tcPropDest, .Null.)
Return
Endif
If Type('toSursa.' + m.tcPropSursa + '(1)') = 'C' ;
And toSursa.&tcPropSursa(1) == '_EMPTY_ARRAY_FLAG_'
*-- array gol semnalat de nfjson
AddProperty(m.toDest, m.tcPropDest, .Null.)
Return
Endif
lnCount = Alen(m.toSursa.&tcPropSursa, 1)
AddProperty(m.toDest, m.tcPropDest + '(' + Transform(m.lnCount) + ')')
Acopy(m.toSursa.&tcPropSursa, toDest.&tcPropDest)
Endproc
*-------------------------------------------------------------------
Procedure GetNumarElemente
*-------------------------------------------------------------------
Lparameters taElemente
If Vartype(m.taElemente) = 'A'
Return Alen(m.taElemente, 1)
Else
Return m.taElemente.Count
Endif
Endproc
*-------------------------------------------------------------------
Procedure GetElement
*-------------------------------------------------------------------
Lparameters taElemente, tnIndex
If Vartype(m.taElemente) = 'A'
Return m.taElemente(m.tnIndex)
Else
Return m.taElemente.Item(m.tnIndex)
Endif
Endproc
*-------------------------------------------------------------------
Procedure GetProprietate
*-------------------------------------------------------------------
Lparameters toObiect, tcProprietate, tuDefault
If Vartype(m.toObiect) = 'O' And Type('toObiect.' + m.tcProprietate) # 'U'
*-- 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
Endproc
*-------------------------------------------------------------------
Procedure LogEroare
*-------------------------------------------------------------------
* Scrie context de diagnostic in LOG.txt. NU logheaza NICIODATA cheia API
* (This.cApiKey) sau headerele complete de request.
Lparameters tcAction, tnIndexLot, tnStatusHttp, tcRaspuns
Local lcMesaj
lcMesaj = Ttoc(Datetime()) + ' AUTOPASS [' + m.tcAction + ']' + ;
' index_lot=' + Transform(m.tnIndexLot) + ;
' http_status=' + Transform(m.tnStatusHttp) + ;
' raspuns=' + Left(Alltrim(m.tcRaspuns), 500) + Chr(13) + Chr(10)
Strtofile(m.lcMesaj, This.cLogFile, 1)
Endproc
ENDDEFINE && AutopassClient
*****************************************************************************************
* Sirul "obs" folosit ca cheie de corelare comanda <-> prezentare in gateway. Sursa unica -
* aceeasi functie e folosita la construirea payload-ului POST, la corelarea situatiei
* (US-005) si la exportul CSV (US-006), ca sa nu se construiasca ad-hoc in mai multe locuri.
*****************************************************************************************
FUNCTION autopass_obs
LPARAMETERS tnNrord, tcNrfact
LOCAL lcNrord, lcNrfact
lcNrord = ALLTRIM(TRANSFORM(tnNrord))
lcNrfact = ALLTRIM(TRANSFORM(tcNrfact))
RETURN 'Comanda ' + lcNrord + ' / Factura ' + lcNrfact
ENDFUNC && autopass_obs
*****************************************************************************************
* 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, 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)
USE IN (lcCursor)
ENDIF
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);
*-- 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')
*-- 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
*-- 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 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 ( ;
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', ''), ;
lcMotiv, ;
TRANSFORM(oClient.GetProprietate(loElem, 'id_prezentare', '')), ;
IIF(VARTYPE(loPrezentare) = 'O', ALLTRIM(oClient.GetProprietate(loPrezentare, 'obs', '')), ''))
ENDFOR
RETURN .T.
ENDFUNC && autopass_situatie
*****************************************************************************************
* Coreleaza fiecare rand din cursorul de comenzi (tcCursorGrid - format autopass_cursor_
* comenzi, cu nrord/nrfact/series/nrinmat/dataoravalid) cu crs_autopass_situatie
* (populat de autopass_situatie): primar pe obs, fallback pe vin+data_prestatie. Adauga
* pe cursorul grid coloanele status_autopass/motiv_autopass/id_prezentare_autopass;
* comanda negasita = status gol ("netransmis").
*****************************************************************************************
FUNCTION autopass_coreleaza_situatie
LPARAMETERS tcCursorGrid
LOCAL lcObsCurent, lcDataPrest, lcVin, llGasit
IF EMPTY(tcCursorGrid) OR !USED(tcCursorGrid)
RETURN .F.
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"
SELECT (tcCursorGrid)
REPLACE ALL status_autopass WITH '', motiv_autopass WITH '', id_prezentare_autopass WITH ''
RETURN .F.
ENDIF
SELECT (tcCursorGrid)
SCAN
lcObsCurent = autopass_obs(nrord, nrfact)
DO CASE
CASE VARTYPE(dataoravalid) = 'T'
lcDataPrest = STR(YEAR(TTOD(dataoravalid)),4) + '-' + PADL(MONTH(TTOD(dataoravalid)),2,'0') + '-' + PADL(DAY(TTOD(dataoravalid)),2,'0')
CASE VARTYPE(dataoravalid) = 'D'
lcDataPrest = STR(YEAR(dataoravalid),4) + '-' + PADL(MONTH(dataoravalid),2,'0') + '-' + PADL(DAY(dataoravalid),2,'0')
OTHERWISE
lcDataPrest = ''
ENDCASE
lcVin = ALLTRIM(UPPER(series))
llGasit = .F.
SELECT crs_autopass_situatie
LOCATE FOR ALLTRIM(obs) == ALLTRIM(m.lcObsCurent)
IF FOUND()
llGasit = .T.
ELSE
IF !EMPTY(m.lcVin) AND !EMPTY(m.lcDataPrest)
LOCATE FOR ALLTRIM(UPPER(vin)) == m.lcVin AND ALLTRIM(data_prestatie) == m.lcDataPrest
llGasit = FOUND()
ENDIF
ENDIF
SELECT (tcCursorGrid)
IF llGasit
REPLACE status_autopass WITH crs_autopass_situatie.status, ;
motiv_autopass WITH crs_autopass_situatie.motiv, ;
id_prezentare_autopass WITH crs_autopass_situatie.id_prezentare
ELSE
REPLACE status_autopass WITH '', motiv_autopass WITH '', id_prezentare_autopass WITH ''
ENDIF
ENDSCAN
RETURN .T.
ENDFUNC && autopass_coreleaza_situatie
*****************************************************************************************
* US-006: Export CSV operatii facturate (fallback oficial, functioneaza fara config API)
*****************************************************************************************
FUNCTION autopass_export_csv
LPARAMETERS tcCursorHeader, tcCursorDetaliu, tcFisierDestinatie
LOCAL lcContinut, lcDataPrestatie, lcOperatie, lcObservatii
LOCAL lnIdOrdl, lcVin, lcNrInmat, lnKmInt
IF EMPTY(tcCursorHeader) OR !USED(tcCursorHeader) OR EMPTY(tcCursorDetaliu) OR !USED(tcCursorDetaliu)
RETURN .F.
ENDIF
lcContinut = "VIN;Numar inmatriculare;Data prestatie;Odometru initial;Odometru final;Operatie;Observatii" + CHR(13) + CHR(10)
SELECT (tcCursorHeader)
SCAN
lnIdOrdl = id_ordl
lcVin = ALLTRIM(series)
lcNrInmat = ALLTRIM(nrinmat)
lnKmInt = kmint
IF !EMPTY(dataoravalid)
lcDataPrestatie = STR(YEAR(dataoravalid),4) + '-' + PADL(MONTH(dataoravalid),2,'0') + '-' + PADL(DAY(dataoravalid),2,'0')
ELSE
lcDataPrestatie = ''
ENDIF
lcObservatii = autopass_obs(nrord, nrfact)
SELECT (tcCursorDetaliu)
SCAN FOR id_ordl = lnIdOrdl
lcOperatie = ALLTRIM(codop)
IF EMPTY(lcOperatie)
lcOperatie = ALLTRIM(denop)
ENDIF
lcContinut = lcContinut + ;
lcVin + ';' + ;
lcNrInmat + ';' + ;
lcDataPrestatie + ';' + ;
';' + ;
TRANSFORM(lnKmInt) + ';' + ;
lcOperatie + ';' + ;
lcObservatii + CHR(13) + CHR(10)
ENDSCAN
SELECT (tcCursorHeader)
ENDSCAN
RETURN STRTOFILE(lcContinut, tcFisierDestinatie)
ENDFUNC && autopass_export_csv
*****************************************************************************************
* Dialog minimal de perioada (data facturii), implicit luna curenta. US-004 va inlocui
* acest apel cu propriile campuri din forma daca export_operatii_csv se declanseaza
* din interiorul ferestrei "Trimitere AUTOPASS"; aici e varianta standalone din xmenu.
*****************************************************************************************
* Se apeleaza cu parametrii prin referinta: autopass_cere_perioada(@ldStart, @ldEnd)
FUNCTION autopass_cere_perioada
LPARAMETERS tdDataInceput, tdDataSfarsit
LOCAL ldInceput, ldSfarsit, lcInceput, lcSfarsit
ldInceput = IIF(VARTYPE(tdDataInceput) = 'D' AND !EMPTY(tdDataInceput), tdDataInceput, GOMONTH(DATE(), -1*(DAY(DATE())-1)) )
ldSfarsit = IIF(VARTYPE(tdDataSfarsit) = 'D' AND !EMPTY(tdDataSfarsit), tdDataSfarsit, DATE())
lcInceput = INPUTBOX('Data inceput perioada (facturii), format ZZ.LL.AAAA:', 'Perioada export AUTOPASS', DTOC(ldInceput))
IF EMPTY(lcInceput)
RETURN .F.
ENDIF
lcSfarsit = INPUTBOX('Data sfarsit perioada (facturii), format ZZ.LL.AAAA:', 'Perioada export AUTOPASS', DTOC(ldSfarsit))
IF EMPTY(lcSfarsit)
RETURN .F.
ENDIF
ldInceput = CTOD(lcInceput)
ldSfarsit = CTOD(lcSfarsit)
IF EMPTY(ldInceput) OR EMPTY(ldSfarsit) OR ldInceput > ldSfarsit
MESSAGEBOX('Perioada introdusa este invalida.', 48, 'AUTOPASS')
RETURN .F.
ENDIF
tdDataInceput = ldInceput
tdDataSfarsit = ldSfarsit
RETURN .T.
ENDFUNC && autopass_cere_perioada (returneaza .F. la Renunta/eroare; la .T. datele sunt puse in parametrii transmisi prin referinta)
*****************************************************************************************
FUNCTION export_operatii_csv
LOCAL ldDataInceput, ldDataSfarsit, lcCursorHeader, lcCursorDetaliu, lcFisier
ldDataInceput = GOMONTH(DATE(), -1*(DAY(DATE())-1))
ldDataSfarsit = DATE()
lcInceput = INPUTBOX('Data inceput perioada (facturii), format ZZ.LL.AAAA:', 'Export CSV AUTOPASS', DTOC(ldDataInceput))
IF EMPTY(lcInceput)
RETURN .F.
ENDIF
lcSfarsit = INPUTBOX('Data sfarsit perioada (facturii), format ZZ.LL.AAAA:', 'Export CSV AUTOPASS', DTOC(ldDataSfarsit))
IF EMPTY(lcSfarsit)
RETURN .F.
ENDIF
ldDataInceput = CTOD(lcInceput)
ldDataSfarsit = CTOD(lcSfarsit)
IF EMPTY(ldDataInceput) OR EMPTY(ldDataSfarsit) OR ldDataInceput > ldDataSfarsit
MESSAGEBOX('Perioada introdusa este invalida.', 48, 'AUTOPASS')
RETURN .F.
ENDIF
lcCursorHeader = autopass_cursor_comenzi(ldDataInceput, ldDataSfarsit)
IF EMPTY(lcCursorHeader)
MESSAGEBOX('Nu s-au putut extrage comenzile pentru perioada aleasa.', 48, 'AUTOPASS')
RETURN .F.
ENDIF
lcCursorDetaliu = autopass_cursor_operatii(lcCursorHeader)
lcFisier = PUTFILE('Export CSV AUTOPASS', 'export_autopass_' + DTOS(DATE()) + '.csv', 'csv')
IF EMPTY(lcFisier)
RETURN .F.
ENDIF
IF autopass_export_csv(lcCursorHeader, lcCursorDetaliu, lcFisier)
MESSAGEBOX('Export finalizat: ' + lcFisier, 64, 'AUTOPASS')
ELSE
MESSAGEBOX('Exportul a esuat - vezi LOG.txt.', 48, 'AUTOPASS')
ENDIF
IF USED(lcCursorHeader)
USE IN (lcCursorHeader)
ENDIF
IF !EMPTY(lcCursorDetaliu) AND USED(lcCursorDetaliu)
USE IN (lcCursorDetaliu)
ENDIF
RETURN .T.
ENDFUNC && export_operatii_csv
*****************************************************************************************
* 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
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