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