*****************************************************************************************
* 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.
*****************************************************************************************
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 .NULL.
ENDIF

lcSfarsit = INPUTBOX('Data sfarsit perioada (facturii), format ZZ.LL.AAAA:', 'Perioada export AUTOPASS', DTOC(ldSfarsit))
IF EMPTY(lcSfarsit)
	RETURN .NULL.
ENDIF

ldInceput = CTOD(lcInceput)
ldSfarsit = CTOD(lcSfarsit)

IF EMPTY(ldInceput) OR EMPTY(ldSfarsit) OR ldInceput > ldSfarsit
	MESSAGEBOX('Perioada introdusa este invalida.', 48, 'AUTOPASS')
	RETURN .NULL.
ENDIF

RETURN {||ldInceput, ldSfarsit}
ENDFUNC && autopass_cere_perioada (returneaza .NULL. la Renunta/eroare; altfel array-code-block cu 2 date - inlocuit efectiv de US-004)


*****************************************************************************************
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
