***************************************************************************************** * 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(citeste_optiune('AUTOPASS_URL')) lcKey = ALLTRIM(citeste_optiune('AUTOPASS_KEY')) lcEnv = ALLTRIM(citeste_optiune('AUTOPASS_ENV')) ENDIF *-- fallback pe variabilele globale gc* (setate de optiuni_firma la pornire), pentru *-- cazul in care crsOptiuni nu mai e deschis in momentul apelului IF EMPTY(lcUrl) AND TYPE('gcAUTOPASS_URL') = 'C' lcUrl = ALLTRIM(gcAUTOPASS_URL) ENDIF IF EMPTY(lcKey) AND TYPE('gcAUTOPASS_KEY') = 'C' lcKey = ALLTRIM(gcAUTOPASS_KEY) ENDIF IF EMPTY(lcEnv) AND TYPE('gcAUTOPASS_ENV') = 'C' lcEnv = ALLTRIM(gcAUTOPASS_ENV) ENDIF *-- la prima rulare, daca lipseste URL-ul gateway-ului, scriem valoarea implicita IF EMPTY(lcUrl) lcUrl = 'https://autopass.romfast.ro' scrie_optiune('AUTOPASS_URL', lcUrl, 'URL gateway RAR AUTOPASS') ENDIF 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 ***************************************************************************************** * Testeaza conexiunea la gateway-ul AUTOPASS (GET /v1/ping) * Intoarce obiect: lSucces, cMediu, lAutentificatCuCheie, lAreCredsTest, lAreCredsProd, cEroare ***************************************************************************************** FUNCTION autopass_test_conexiune LOCAL loConfig, loHTTP As "winHTTP.winHTTPrequest.5.1", loReturn As "empty", loEx As Exception LOCAL lcServer, lnStatus, lcResponseText, loJson 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() IF !loConfig.lConfigOk 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 = ALLTRIM(loConfig.cUrlGateway) + [/v1/ping] loHTTP = Createobject('winHTTP.winHTTPrequest.5.1') loHTTP.Open('GET', lcServer, .F.) loHTTP.SetTimeouts(30000, 30000, 30000, 30000) loHTTP.setRequestHeader('X-API-Key', loConfig.cApiKey) loHTTP.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 DO CASE CASE lnStatus = 401 loReturn.cEroare = 'Cheie API invalida sau neautorizata (HTTP 401) - corecteaza cheia' + ; ' in panoul Configurare.' RETURN loReturn CASE lnStatus <> 200 loReturn.cEroare = 'Gateway AUTOPASS a raspuns cu eroare (HTTP ' + TRANSFORM(lnStatus) + ').' RETURN loReturn ENDCASE loJson = Nfjsonread(lcResponseText) IF VARTYPE(loJson) <> 'O' loReturn.cEroare = 'Raspuns invalid (JSON malformat) de la gateway-ul AUTOPASS.' RETURN loReturn ENDIF loReturn.lSucces = .T. loReturn.cMediu = IIF(TYPE('loJson.mediu') = 'C', loJson.mediu, '') loReturn.lAutentificatCuCheie = IIF(TYPE('loJson.autentificatCuCheie') = 'L', loJson.autentificatCuCheie, .F.) loReturn.lAreCredsTest = IIF(TYPE('loJson.areCredsTest') = 'L', loJson.areCredsTest, .F.) loReturn.lAreCredsProd = IIF(TYPE('loJson.areCredsProd') = 'L', loJson.areCredsProd, .F.) 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 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(gcS, 'AUTO_ISTORIC_COMENZI', 'STERS') lcFiltruSters = ' AND sters = 0' ENDIF lcSql = "SELECT id_ordl, nrord, dataoravalid, datafact, nrfact, nrinmat, series, kmint, nume, tip_comanda " + ; "FROM " + gcS + ".auto_istoric_comenzi " + ; "WHERE validat = 1 AND facturat = 1 AND id_tip IN (1,2) " + ; "AND datafact BETWEEN " + lcD1 + " AND " + lcD2 + lcFiltruSters lnSucces = goExecutor.oExecute(lcSql, lcCursorHeader) IF lnSucces < 0 IF TYPE('goLog') = 'O' goLog.Log('autopass_cursor_comenzi: ' + goExecutor.cEroare, PROGRAM()) ENDIF RETURN '' ENDIF ALTER TABLE (lcCursorHeader) ADD COLUMN lvalid L ALTER TABLE (lcCursorHeader) ADD COLUMN motiv_invalid C(100) SELECT (lcCursorHeader) SCAN lcMotivRand = '' DO CASE CASE VARTYPE(dataoravalid) = 'T' ldDataPrest = TTOD(dataoravalid) CASE VARTYPE(dataoravalid) = 'D' ldDataPrest = dataoravalid OTHERWISE ldDataPrest = {} ENDCASE llOk = autopass_valideaza_rand(series, nrinmat, ldDataPrest, kmint, @lcMotivRand) REPLACE 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 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 " + gcS + ".dev_oper o " + ; "JOIN " + gcS + ".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 " + gcS + ".dev_oper o " + ; "JOIN " + gcS + ".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 lnSucces = goExecutor.oExecute(lcSql, lcCursorDetaliu) IF lnSucces < 0 IF TYPE('goLog') = 'O' goLog.Log('autopass_cursor_operatii: ' + goExecutor.cEroare, PROGRAM()) ENDIF RETURN '' ENDIF ALTER TABLE (lcCursorDetaliu) ADD COLUMN lvalid_op L ALTER TABLE (lcCursorDetaliu) ADD COLUMN motiv_op C(100) 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 SELECT id_ordl, codop, denop, lvalid_op, motiv_op ; FROM (tcCursorDetaliu) ; GROUP BY id_ordl, codop ; 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 = ALLTRIM(UPPER(IIF(VARTYPE(tcNrinmat) = 'C', tcNrinmat, ''))) IF LEN(lcVin) # 17 lcMotiv = lcMotiv + IIF(EMPTY(lcMotiv),'','; ') + 'VIN invalid (trebuie 17 caractere)' ELSE 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 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 ***************************************************************************************** * Verifica existenta unei coloane intr-un tabel Oracle (owner/table/column) ***************************************************************************************** FUNCTION autopass_coloana_exista LPARAMETERS tcSchema, 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 ALL_TAB_COLUMNS WHERE OWNER = UPPER('" + ; tcSchema + "') AND 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, laRezultate, 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 * TODO verificare empirica: testeaza cu diacritice pe mediul RAR test inainte de prima * trimitere reala; presupunem ca nfjsoncreate.prg emite \uXXXX pentru caractere ne-ASCII. * Daca emite in schimb bytes cp1250, aici trebuie STRCONV(...,9) sau trimitere ca * byte-array cu charset=utf-8 - NU aplica STRCONV suplimentar peste rezultatul lui * nfjsoncreate fara sa verifici mai intai, ar risca dubla-conversie. lcJson = Nfjsoncreate(loBody, .F.) 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 * TODO verificare empirica: numele exact al proprietatii-container din raspunsul real * al gateway-ului (rezultate/prezentari/altul) nu e confirmat in specificatie folosita * aici; ExtrageListaRezultate() incearca variantele uzuale, ajusteaza cand se cunoaste * contractul exact. laRezultate = This.ExtrageListaRezultate(m.loParsed) If Vartype(m.laRezultate) # 'A' loReturn.cDetaliu = 'formatul raspunsului nu contine lista de rezultate asteptata' This.LogEroare(m.tcAction, -1, loHttpRez.nStatusHttp, loHttpRez.cRaspuns) Return loReturn Endif loColectie = Createobject('Collection') For i = 1 To Alen(m.laRezultate, 1) loElem = laRezultate(m.i) lnIndexRel = This.GetProprietate(m.loElem, 'index', m.i - 1) loRezItem = Createobject('empty') AddProperty(loRezItem, 'index', m.lnIndexRel) AddProperty(loRezItem, 'id_ordl', ; Iif(Between(m.lnIndexRel + 1, 1, m.lnCount), laIndexMap(m.lnIndexRel + 1), 0)) AddProperty(loRezItem, 'submission_id', This.GetProprietate(m.loElem, 'submission_id', '')) AddProperty(loRezItem, 'status', This.GetProprietate(m.loElem, 'status', '')) AddProperty(loRezItem, 'motiv', This.GetProprietate(m.loElem, 'motiv', '')) AddProperty(loRezItem, 'id_prezentare', This.GetProprietate(m.loElem, 'id_prezentare', '')) If m.tcAction == 'Valideaza' AddProperty(loRezItem, 'status_estimat', This.GetProprietate(m.loElem, 'status_estimat', '')) AddProperty(loRezItem, 'coduri_nemapate', This.GetProprietate(m.loElem, 'coduri_nemapate', .Null.)) Endif loColectie.Add(loRezItem) Endfor loReturn.lSucces = .T. loReturn.oRezultate = loColectie Return loReturn Endproc *------------------------------------------------------------------- Procedure StatusPrezentare *------------------------------------------------------------------- Lparameters tcSubmissionId Local loHttpRez, loReturn, loParsed 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, 'vehicul', '') 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.motiv = This.GetProprietate(m.loParsed, 'motiv', '') loReturn.id_prezentare = This.GetProprietate(m.loParsed, 'id_prezentare', '') loReturn.vehicul = This.GetProprietate(m.loParsed, 'vehicul', '') loReturn.submission_id = This.GetProprietate(m.loParsed, 'submission_id', m.tcSubmissionId) Return loReturn 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) loHTTP.setRequestHeader('Content-Type', 'application/json') 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 *------------------------------------------------------------------- Lparameters toParsed Local Array laNume(4) Local i laNume(1) = 'rezultate' laNume(2) = 'prezentari' laNume(3) = 'items' laNume(4) = 'data' For i = 1 To Alen(laNume, 1) If Type('toParsed.' + laNume(m.i)) = 'A' Return toParsed.&laNume(m.i) Endif Endfor If Type('toParsed.array') = 'A' Return toParsed.array Endif Return .Null. 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 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' Return Evl(toObiect.&tcProprietate, m.tuDefault) 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 (GET /v1/prezentari), * FARA nicio evidenta locala. Cursorul temporar crs_autopass_situatie se reconstruieste * la fiecare apel; la eroare HTTP/parse ramane gol si functia intoarce .F. (eroarea se * logheaza prin AutopassClient.LogEroare, fara sa logheze cheia API). ***************************************************************************************** FUNCTION autopass_situatie LPARAMETERS tdD1, tdD2 LOCAL lcCursor, oClient, lcD1, lcD2, lcPath, loHttpRez, loParsed, laLista, i, loElem 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) lcD1 = STR(YEAR(tdD1),4) + '-' + PADL(MONTH(tdD1),2,'0') + '-' + PADL(DAY(tdD1),2,'0') lcD2 = STR(YEAR(tdD2),4) + '-' + PADL(MONTH(tdD2),2,'0') + '-' + PADL(DAY(tdD2),2,'0') lcPath = '/v1/prezentari?data_de=' + lcD1 + '&data_pana=' + lcD2 oClient = CREATEOBJECT('AutopassClient') loHttpRez = oClient.Get(lcPath, 'Situatie') IF !loHttpRez.lSucces oClient.LogEroare('Situatie', -1, loHttpRez.nStatusHttp, loHttpRez.cRaspuns) RETURN .F. ENDIF loParsed = Nfjsonread(loHttpRez.cRaspuns) IF VARTYPE(loParsed) <> 'O' oClient.LogEroare('Situatie', -1, loHttpRez.nStatusHttp, loHttpRez.cRaspuns) RETURN .F. ENDIF laLista = oClient.ExtrageListaRezultate(loParsed) IF VARTYPE(laLista) <> 'A' oClient.LogEroare('Situatie', -1, loHttpRez.nStatusHttp, loHttpRez.cRaspuns) RETURN .F. ENDIF SELECT (lcCursor) FOR i = 1 TO ALEN(laLista, 1) loElem = laLista(i) INSERT INTO (lcCursor) (submission_id, vin, nr_inmatriculare, data_prestatie, status, motiv, id_prezentare, obs) VALUES ( ; oClient.GetProprietate(loElem, 'submission_id', ''), ; oClient.GetProprietate(loElem, 'vin', ''), ; oClient.GetProprietate(loElem, 'nr_inmatriculare', ''), ; oClient.GetProprietate(loElem, 'data_prestatie', ''), ; oClient.GetProprietate(loElem, 'status', ''), ; oClient.GetProprietate(loElem, 'motiv', ''), ; oClient.GetProprietate(loElem, 'id_prezentare', ''), ; oClient.GetProprietate(loElem, '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 LOCAL laFields[1], lnFieldCount, i, llAreStatus, llAreMotiv, llAreIdPrez IF EMPTY(tcCursorGrid) OR !USED(tcCursorGrid) RETURN .F. ENDIF lnFieldCount = AFIELDS(laFields, tcCursorGrid) llAreStatus = .F. llAreMotiv = .F. llAreIdPrez = .F. FOR i = 1 TO lnFieldCount DO CASE CASE UPPER(laFields[i,1]) == 'STATUS_AUTOPASS' llAreStatus = .T. CASE UPPER(laFields[i,1]) == 'MOTIV_AUTOPASS' llAreMotiv = .T. CASE UPPER(laFields[i,1]) == 'ID_PREZENTARE_AUTOPASS' llAreIdPrez = .T. ENDCASE ENDFOR IF !llAreStatus ALTER TABLE (tcCursorGrid) ADD COLUMN status_autopass C(30) ENDIF IF !llAreMotiv ALTER TABLE (tcCursorGrid) ADD COLUMN motiv_autopass C(254) ENDIF IF !llAreIdPrez ALTER TABLE (tcCursorGrid) ADD COLUMN id_prezentare_autopass C(60) ENDIF 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 (forma frm_autopass.scx - lansata din xmenu ofundal_dev.vcx). * Placeholder pana la construirea formei in VFP IDE (val 3 din PRD) - momentan doar * deschide un mesaj, ca meniul sa poata fi cablat si testat inaintea formei complete. ***************************************************************************************** FUNCTION trimitere_autopass MESSAGEBOX('Fereastra Trimitere AUTOPASS (US-004) nu este inca construita in VFP IDE.', 48, 'AUTOPASS') RETURN .F. ENDFUNC && trimitere_autopass