commit 78d21d5a3848901ead407422fb01b769b3a4cd81 Author: Marius Mutu Date: Sun Jun 14 23:10:28 2026 +0300 Initial commit: baza VFP ROAAUTO + planuri migrare Web API Arhiva clasei RarAutoPass (VFP) care declara prestatiile la RAR AUTOPASS, ca baza pentru rescrierea ca gateway central Python/FastAPI. Include: - sursa VFP (.prg) + datele necesare migrarii (mapare_prestatii, prestatii_rar) - spec-ul oficial RAR (txt) - docs/plans/: plan-design-review + plan-eng-review - docs/CONTEXT.md: handoff pentru continuarea in alta sesiune - .gitignore specific Visual FoxPro (ignora artefacte compilate + credentiale) settings.xml (cu parola de test in clar) EXCLUS; vezi settings.xml.example. Co-Authored-By: Claude Opus 4.8 (1M context) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ffc24eb --- /dev/null +++ b/.gitignore @@ -0,0 +1,72 @@ +# ============================================================ +# .gitignore — proiect Visual FoxPro (ROAAUTO / RAR AutoPass) +# Acest repo este ARHIVA bazei VFP + planurile pentru +# rescrierea ca Web API (Python/FastAPI). Vezi docs/. +# Păstrăm SURSA (.prg) și DATELE necesare migrării +# (mapare_prestatii.*, prestatii_rar.*). Ignorăm artefactele +# compilate/temporare și orice conține credențiale. +# ============================================================ + +# --- Credențiale / secrete (NU se comit niciodată) --- +settings.xml +*.pem +*.key +.env +.env.* + +# --- VFP: programe compilate (se regenerează din .prg) --- +*.fxp +*.FXP +*.app +*.APP +*.exe +*.EXE +*.dll +*.DLL + +# --- VFP: erori de compilare / loguri --- +*.err +*.ERR +*.log +*.LOG + +# --- VFP: fișiere temporare / backup --- +*.bak +*.BAK +*.tmp +*.TMP +~*.* +*.~* + +# --- VFP: fișier de resurse al utilizatorului (per-stație) --- +foxuser.dbf +foxuser.fpt +foxuser.DBF +foxuser.FPT + +# --- VFP: jurnal de rulare (NU se migrează — vezi planul) --- +rar_log.dbf +rar_log.fpt +rar_log.DBF +rar_log.FPT +rar_log.cdx +rar_log.CDX + +# --- Vechi control de versiune (Subversion) --- +.svn/ + +# --- IDE / OS --- +.vscode/ +.idea/ +Thumbs.db +desktop.ini +.DS_Store + +# --- Viitor: stratul Web API (Python) --- +__pycache__/ +*.py[cod] +.venv/ +venv/ +*.db +*.db-wal +*.db-shm diff --git a/Document informativ RAR- Autopass.txt b/Document informativ RAR- Autopass.txt new file mode 100644 index 0000000..860b54a --- /dev/null +++ b/Document informativ RAR- Autopass.txt @@ -0,0 +1,51 @@ +Document informativ +RAR - Autopass + + In urma OMTI 210-2024 pentru punerea in aplicare a Legii nr 142/2023, sistemul RAR-Autopass are urmatoarele functionalitati: + Inrolarea Agentilor Economici: + Inrolarea in sistemul Autopass se face in prim pas prin intermediul Organismului de Certificare Sisteme de Management (OCS) care va genera Agentului Economic o CHEIE UNICA si instructiuni de inrolare in RAR - Autopass, prin e-mail, fie la certificarea acestuia, fie prin intermediul adresei e-mail declarata la certificare (pentru cei deja inregistrati de catre OCS). + Odata ce a fost emisa CHEIA UNICA, Agentul Economic se va putea inrola in sistemul Autopass cu rol ADMIN CLIENT, fiindu-i necesare urmatoarele: +- Cheia unica +- Adresa e-mail +- Codul de atelier +- Nume +- Prenume +- Setarea unei parole + Ulterior, va fi necesara confirmarea crearii contului, actiune ce se va putea face prin intermediul unui link unic valid 24h de la solicitarea inrolarii, link ce va fi trimis pe adresa de e-mail inscrisa in formularul de inrolare. + In cazul in care linkul primit pe e-mail este accesat la mai mult de 24h de cand acesta a fost creat, un nou link va fi emis, cu o noua valabilitate de 24h. + Odata creat, acest utilizator ADMIN CLIENT, la randul sau isi poate crea si administra multiple conturi cu rol CLIENT pentru proprii angajati, fiind necesare urmatoarele: +- Nume +- Prenume +- Adresa e-mail +Aceste conturi CLIENT vor trebui confirmate printr-un link valid 24h primit pe e-mail, proces +similar crearii contului de ADMIN CLIENT. + Odata create aceste conturi, Agentul Economic (rol ADMIN CLIENT) precum si utilizatorii sai (rol +CLIENT) vor putea : +- introduce date in sistemul RAR Autopass +- vizualiza datele deja introduse +- genera clientilor o fisa PDF care sa afiseze datele introduse in sistemul Autopass +Datele necesare inserarii unei Prezentari : + O Prezentare reprezinta intrarea unui autovehicul in cadrul unei unitati service/ atelier pentru efectuarea unei interventii. + Date necesare: +- Serie VIN +o Se va introduce in doua campuri distincte, fara posibilitatea de copier/lipire +- Numar inmatriculare +- Data +- Observatii +o Acest camp nu este obligatoriu +- Prestatii ( o singura selectie sau selectii multiple in functie de interventii) +o Lista de prestatii este furnizata ca nomenclator si este construita be baza informatiilor specificate in OMTI 210 -2024. Tipul de prestatie care implica inlocuirea sau reparatia odometrului va determina o functionare diferita a programului. +- Nr. km. parcursi (Valoarea citita a indicatiei odometrului) +o In cazul in care in lista de prestatii este selectata una din optiunile INLOCUIRE ODOMETRU sau REPARATIE ODOMETRU, va fi necesara introducerea valorilor indicatiei odometrului inainte si dupa inlocuire/reparatie. +- Poza odometru +Nota: Pentru diminuarea riscurilor de introducere gresita a valorii indicatiei odometrului, aplicatia prevede un pas suplimentar la momentul transmiterii datelor, pas in care se va cere confirmarea valorilor introduse, fara posibilitatea de copier/lipire. +Preluarea datelor acopera urmatoarele domenii: +a. Reparatori +b. Reparatori la Inlocuire/Reparatie odometru +c. Reparatori dupa avarii grave. Sistemul reparat este codificat in lista de prestatii. +Pentru domeniile SITP si AITLV informatiile se for extrage direct din baza de date centralizata a RAR. +Toate campurile vor fi obligatorii, exceptie facand campul de Observatii. + + Aceste informatii pot fi depuse si prin intermediul unui serviciu web expus de catre RAR, documentatia acestui serviciu fiind atasata acestui document. + Totodata, dupa stabilirea formei finale a aplicatiei, va fi pus la dispozitie si un manual de utilizare. + diff --git a/Documentatie Serviciu AutoPass_Final.txt b/Documentatie Serviciu AutoPass_Final.txt new file mode 100644 index 0000000..9a8522b --- /dev/null +++ b/Documentatie Serviciu AutoPass_Final.txt @@ -0,0 +1,413 @@ + + + + + + + + + + + + + + SERVICIU WEB PENTRU SCHIMB DE DATE + R.A.R.-R.A. Diverse institutii + + + + + + + + + + + + + + + + + + +Proprietar : Registrul Auto Romn R.A. Data: 08.04.2024 Versiunea: 0.0.1 Departament: D.T.I.C. ?ef Departament: Manuel Farca? ?ef Dezvoltare: Cristian Mardare Analist Programator: Razvan Chirculescu + + + + +Document control + +Versiunea Data Observa?ii 0.0.1 08.04.2024 Start Proiect + + + + + + +Con?inut: + + +1. Introducere Pag. 4 +2. Mesaje definite si metode de acces Pag. 4 +3. Modele de date Pag.10 + + + +1. Introducere + + General + +n acest document sunt definite metodele expuse in vederea transmiterii de catre agentii economici autorizati a informatiilor necesare emiterii raportului RAR-Autopass conform Legii 142/2023 si OM 210/2024 + + Scop + + Colecterea de informatiilor despre interventiile asupra anumitor sisteme ale vehiculelor precum si informatiile privind indicatia odometrului, cu ocazia prezentarii vehiculelor in unitati service autorizate RAR + +2. Mesaje definite si Metode de acces + +2.1. Endpoint: account-controller + +a. Metoda addClient - Adaugare utilizator +- Adresa access: /rar-autopass/account/addClient +- Tip apel: POST +- Tip access: Securizat +- Descriere: +Agentul economic (rol ADMIN) poate crea utilizatori cu rol CLIENT, vor primi pe email un link de confirmare a adresei de e-mail si o parola initiala, iar contul va deveni activ doar dupa ce acestia vor accesa linkul. +Linkul de activare al contului este valid 24 de ore. +Daca nu se confirma in acest interval, atunci contul va trebui sa fie creat din nou. +- Payload: PostUtilizatoriDTO +- Response: ApiResponseMessage + +b. Metoda changePass - Schimba parola unui utilizator +- Adresa access: /rar-autopass/account/changePass +- Tip apel: POST +- Tip access: Securizat +- Descriere: Se transmite un obiect compus din UserID, parola veche, parola noua, reconfirmare parola noua. +- Payload: RequestChangePassword +- Raspuns: String + + +c. Metoda getAllUsers - Lista de utilizatori in functie de rolul userului logat +- Adresa access: /rar-autopass/account/getAllUsers +- Tip apel: GET +- Tip access: Securizat +- Descriere: + Raspunde cu o lista de obiecte de tip USER in functie de rolul utilizatorului logat. + Daca utilizatorul este ADMIN atunci lista va contine utilizatorii creati de client. + +- Payload/Parametru: +- Raspuns: CustomResponseForMapping + + + +d. Metoda getRoles - Interogare lista roluri +- Adresa access: /rar-autopass/account/getRoles +- Tip apel: GET +- Tip access: Securizat +- Descriere: Intoarce un obiect de roluri de utilizator cu ID-urile corespunzatoare. +- Payload/Parametru: +- Raspuns: GetUtilizatoriDTO + +e. Metoda getUserById/{id} - Detalii utilizator +- Adresa access: /rar-autopass/account/getUserById/{id} +- Tip apel: GET +- Tip access: Securizat +- Descriere: Cere un parametru ID si raspunde cu un obiect de tip user. +- Payload/Parametru: id +- Raspuns: GetUtilizatoriDTO + +f. Metoda inactivateUser/{id} - Dezactiveaza utilizator +- Adresa access: /rar-autopass/account/inactivateUser/{id} +- Tip apel: PATCH +- Tip access: Securizat +- Descriere: +Endpoint de tip PATCH, care primeste un parametru ID si seteaza utilizatorul ca inactiv. +Raspunde cu un obiect de tip {key:"raspuns", value:"descriere raspuns"}. +Daca utilizatorul a fost inactivat cu success raspunsul va fi "USER_INACTIVE" ("Utilizatorul a fost dezactivat!"), iar cand nu, atunci utilizatorul nu exista: +"USER_NOT_PRESENT" ("Utilizator inexistent!"). +- Payload/Parametru: id +- Raspuns: ApiResponseMessage + +g. Metoda patchUser/{userId} - Modificare utilizator +- Adresa access: /rar-autopass/account/patchUser/{userId} +- Tip apel: PATCH +- Tip access: Securizat +- Descriere: +Endpoint de tip PATCH pentru modificarea campurilor unui utilizator. Primeste un obiect de tip PostUtilizatorDTO si un parametru ID, +si raspunde cu un obiect de tip {key:"raspuns", value:"descriere raspuns"}. +Daca userul a fost modificat cu succes +raspunsul va fi "USER_UPDATED" ("Utilizatorul a fost actualizat!"), +iar cand nu, atunci utilizatorul nu exista: +"USER_NOT_PRESENT" ("Utilizator inexistent!"). +- Payload/Parametru: user +- Raspuns: ApiResponseMessage + +h. Metoda reactivateUser/{id} - Reactiveaza utilizator +- Adresa access: /rar-autopass/account/reactivateUser/{id} +- Tip apel: PATCH +- Tip access: Securizat +- Descriere: +Endpoint de tip PATCH, care primeste un parametru ID si seteaza userul ca activ. +Raspunde cu un obiect de tip {key:"raspuns", value:"descriere raspuns"}. +Daca userul a fost reactivat cu succes raspunsul va fi "USER_REACTIVATED" ("Utilizatorul a fost reactivat!"), +iar cand nu, atunci utilizatorul nu exista: +"USER_NOT_PRESENT" ("Utilizator inexistent!"). +- Payload/Parametru: id +- Raspuns: ApiResponseMessage + +i. Metoda resetPassToDefault - Reseteaza parola unui utilizator +- Adresa access: /rar-autopass/account/resetPassToDefault/{id} - Reseteaza parola unui utilizator +- Tip apel: POST +- Tip access: Securizat +- Descriere: Endpoint de tip POST, care primeste ca parametru ID si trimite utilizatorului o noua parola pe emailul asociat. +- Payload/Parametru: id +- Raspuns: GetUtilizatoriDTO +- +j. Metoda makeAdminClient/{id} +- Adresa access: /rar-autopass/account/makeAdminClient/{id} +- Tip apel: POST +- Tip access: Securizat +- Descriere: +Endpoint de tip POST, care permite userului cu rol ADMIN sa aloce rol ADMIN_CLIENT conturilor cu rol de CLIENT deja existente. Primeste un parametru ID si schimba rolul userului din CLIENT in ADMIN_CLIENT. +Raspunde cu un obiect de tip {key:"raspuns", value:"descriere raspuns"}. +Daca rolul este deja ADMIN_CLIENT raspunsul va fi USER_ALREADY_ADMIN(Userul are deja rol admin) +Daca rolul a fost schimbat cu succes raspunsul va fi "USER_ROLE_UPDATED" ("Rolul a fost modificat in ADMIN_CLIENT!"), +iar cand nu, atunci utilizatorul nu exista: +"USER_UPDATE_FAILED" ("Eroare la actualizarea userului!"). +- Payload/Parametru: id +- Raspuns: ApiResponseMessage + + + + +2.2. Endpoint: nomenclator-controller + +a. Metoda getNomenclatorPrestatii Afiseaza lista prestatiilor +- Adresa access: /rar-autopass/nomenclator/getNomenclatorPrestatii +- Tip apel: GET +- Tip access: Securizat +- Descriere: Afiseaza lista prestatiilor. +- Payload/Parametri: --- +- Response: CustomResponseForMapping + +b. Metoda getPrestatieByCodPrestatie Afiseaza prestatie dupa cod +- Adresa access: +/rar-autopass/nomenclator/getPrestatieByCodPrestatie/{cod} +- Tip apel: GET +- Tip access: Securizat +- Descriere: Afiseaza prestatie dupa cod. +- Payload/Parametri: cod +- Response: NomenclatorPrestatii + + + + + +2.3. Endpoint: prezentari-controller + +a. Metoda getAllPrezentari Afiseaza toate prezentarile active ale utilizatorului logat +- Adresa access: /rar-autopass/prezentari/getAllPrezentari +- Tip apel: GET +- Tip access: Securizat +- Descriere: Afiseaza toate prezentarile active ale utilizatorulului logat. +- Payload/Parametri: --- +- Response: CustomResponseForMapping + +b. Metoda getPrezentare/{id} Afiseaza prezentare dupa ID +- Adresa access: /rar-autopass/prezentari/getPrezentare/{id} +- Tip apel: GET +- Tip access: Securizat +- Descriere: Afiseaza prezentare dupa ID. +- Payload/Parametri: id +- Response: Prezentari + +c. Metoda markPrezentareAnulataById/{id} + Marcheaza o prezentare ca ANULATA +- Adresa access: /rar-autopass/prezentari/getPrezentare/{id} +- Tip apel: PATCH +- Tip access: Securizat +- Descriere: Marcheaza o prezentare ca ANULATA. +- Payload/Parametri: id +- Raspuns: ApiResponseMessage + +d. Metoda patchPrezentare/{id} + Modifica fielduri pentru o prezentare deja existenta +- Adresa access: /rar-autopass/prezentari/patchPrezentare/{id} +- Tip apel: PATCH +- Tip access: Securizat +- Descriere: Marcheaza o prezentare ca ANULATA. +- Payload/Parametri: id +- Raspuns: PostPrezentareUpdateDTO + +e. Metoda postPrezentare Salveaza o noua prezentare +- Adresa access: /rar-autopass/prezentari/patchPrezentare/{id} +- Tip apel: POST +- Tip access: Securizat +- Descriere: Salveaza o noua prezentare. +- Payload: Prezentari +- Raspuns: CustomResponseForMapping + + + + + + + + +2.4. Endpoint: public-controller + +a. Metoda checkIfEmailExists Verificare async daca email este deja existent +- Adresa access: /rar-autopass/public/checkIfEmailExists/{email} +- Tip apel: GET +- Tip access: Public +- Descriere: Verificare async daca email este deja existent. +- Payload/Parametri: email +- Raspuns: ApiResponseMessage + +b. Metoda confirmationEmailAccount Confirmare adresa de email +- Adresa access: /rar-autopass/public/checkIfEmailExists/{email} +- Tip apel: GET +- Tip access: Public +- Descriere: Confirmare adresa de email. +- Payload/Parametri: uuid +- Raspuns: ApiResponse + +c. Metoda confirmationPassReset + Resetare parola cu token, parola noua si confirmare parola noua +- Adresa access: /rar-autopass/public/confirmationPassReset +- Tip apel: POST +- Tip access: Public +- Descriere: Resetare parola cu token, parola noua si confirmare parola noua. +- Payload/Parametri: requestForgotPassword +- Raspuns: ApiResponse + + +d. Metoda forgotPasswordLink/{email} Parola uitata +- Adresa access: rar-autopass/public/forgotPasswordLink/{email} +- Tip apel: POST +- Tip access: Public +- Descriere: +Endpoint pentru setarea unei noi parole in cazul in care parola actuala a fost uitata. Primeste parametru EMAIL si raspunde cu un link pe email pentru resetare. +- Payload/Parametri: email +- Raspuns: ApiResponseMessage + +e. Metoda login Endpoint pentru login +- Adresa access: /rar-autopass/public/login +- Tip apel: POST +- Tip access: Public +- Descriere: +Primeste 2 parametri, email si password, genereaza token utilizatorului si autentifica login. +- Payload/Parametri: email, password +- Raspuns: GetUtilizatoriDTO + +Nota: Pentru toate metodele cu tip acces securizat se va folosi metoda de autentificare de tip JWT Token. +Tokenul de autentificare se obtine prin apel la metoda LOGIN din controller-ul public. Token-ul primit in raspuns trebuie atasat fiecarui request in HEADER AUTHORIZATION,in forma Bearer: {token} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +3. Models/Modele + +(1) ApiResponseMessage{ + < * >: string } + +ApiResponseMessage + + +(2) ApiResponse{ + description string details string status stringEnum: +[INVALID_EMAIL, INVALID_TOKEN, NOT_OK, OK] } + + +(3) CreateAgentEconomicDTO{ + codAtelier string confirmPassword string email string key string nume string password string prenume string } + + + + +(4) CustomResponseForMapping{ + data { + } message string statusCode integer($int32) } + + +(5) GetUtilizatoriDTO{ + activ integer($int64) authorities [GrantedAuthority{ +authority string }] email string idUser integer($int64) nume string prenume string token string } + + + +(6) GrantedAuthority{ + authority string } + + +(7) NomenclatorPrestatii{ + codPrestatie string numePrestatie string } + + +(8) PostPrezentareUpdateDTO{ + b64Image string nrInmatriculare string obs string odometruFinal integer($int64) odometruInitial integer($int64) prestatii [PrestatiiPrezentare{ +codPrestatie string idPrezentare integer($int64) }] sistemReparat string status stringEnum: +[ANULATA, FINALIZATA, SALVATA, UNDEFINED] vin string } + + +(9) PostUtilizatoriDTO{ + email string nume string prenume string } + + +(10) PrestatiiPrezentare{ + codPrestatie string idPrezentare integer($int64) } + + +(11) Prezentari{ + b64Image string dataPrestatie string($date) id integer($int64) idAgent integer($int64) idUser integer($int64) nrInmatriculare string obs string odometruFinal integer($int64) odometruInitial integer($int64) prestatii [PrestatiiPrezentare{ +codPrestatie string idPrezentare integer($int64) }] sistemReparat string status stringEnum: +[ANULATA, FINALIZATA, SALVATA, UNDEFINED] tipPrestatie stringEnum: + vin string } + + +(12) RequestChangePassword{ + confirmNewPass string newPass string oldPass string userId integer($int64) } + + +(13) RequestForgotPassword{ + confirmNewPass string newPass string token string } + + +(14) Roluri{ + idRol integer($int64) numeRol string } + + Registrul Auto Romn R.A. Direc?ia General? Venituri Buget Local Sector 2 + +Protocol de colaborare + + Specificare Mesaje JSON Versiunea 0.0.1 + + +08.04.2024 Registrul Auto Roman R.A. Pag. 1 + diff --git a/docs/CONTEXT.md b/docs/CONTEXT.md new file mode 100644 index 0000000..a26811c --- /dev/null +++ b/docs/CONTEXT.md @@ -0,0 +1,97 @@ +# Context proiect — Gateway RAR AutoPass (migrare ROAAUTO din VFP în Web API) + +> Fișier de continuitate între sesiuni. Citește-l înainte de a relua lucrul. +> Ultima actualizare: 2026-06-14. + +## Ce este acest repo + +Arhiva **bazei Visual FoxPro** existente (clasa `RarAutoPass`, ROAAUTO) care declară +prestațiile de service la **RAR AUTOPASS** (Legea 142/2023, OM 210/2024), **plus** +planurile pentru rescrierea ca **Web API central (Python / FastAPI)**. + +Codul VFP de aici este **punctul de plecare / sursa de adevăr de contract** pentru +versiunea web. Nu se mai dezvoltă; se portează. + +## Stare actuală (iunie 2026) + +- Integrarea VFP **funcționează** și e **testată pe endpoint-ul de test RAR**, dar + **nu e pusă la clienți** încă. +- Comunică direct cu RAR prin `MSXML2.ServerXMLHTTP` din `rar_autopass.prg` / `rar-forms.prg`. +- Maparea operație→`codPrestatie` în `mapare_prestatii.DBF`; nomenclator în `prestatii_rar.DBF`; + jurnal în `rar_log.DBF`; credențiale (în clar) în `settings.xml`. +- ⚠️ `settings.xml` conținea o **parolă de test reală** (`marius.mutu@romfast.ro`). + E **exclus din git** (`.gitignore`) și înlocuit cu `settings.xml.example`. + **De rotit parola** — a fost expusă în istoricul SVN vechi. + +## Fișiere-cheie (VFP) și ce reutilizăm + +| Fișier | Rol | Se portează în | +|---|---|---| +| `rar_autopass.prg` | clasa `RarAutoPass`: login+JWT, nomenclator, postPrezentare, cancel | `app/rar_client.py` | +| `rar-forms.prg` | UI + timer auto-process (`OnAutoProcessTimer`) | logica → worker; timer → re-push ROAAUTO | +| `export_comenzi.prg` | citește comenzi/operații, construiește payload | client subțire: `POST /v1/prezentari` | +| `rar_advanced.prg` | export Excel (oglindă pentru treapta 2) | referință import xlsx/csv | +| `mapare_prestatii.DBF` | cod_op_service → codPrestatie | `operations_mapping` (via `tools/import_dbf.py`) | +| `prestatii_rar.DBF` | nomenclator {codPrestatie, numePrestatie} | `nomenclator_rar` (via `tools/import_dbf.py`) | +| `Documentatie Serviciu AutoPass_Final.txt`, `Document informativ RAR- Autopass.txt` | spec oficial RAR | contract API | + +## Planurile (în `docs/plans/`) + +1. **`plan-design-review.md`** — designul produsului/arhitecturii (output `/office-hours`). + Problemă ISV, topologie gateway central pass-through credențiale, zero stocare parole, + privacy-first, teză SaaS pe trepte. +2. **`plan-eng-review.md`** — planul de implementare (review CEO, SELECTIVE EXPANSION peste design). + Decizii blocate, constatări din spec, mașina de stări submission, securitate, verificare E2E. + +> Continuă cu **plan-eng-review** și **plan-design-review** — acestea sunt cele două +> documente de reluat în următoarea sesiune. + +## Arhitectura țintă (rezumat) + +``` +ROAAUTO (VFP, client subțire) ──HTTPS──▶ Gateway FastAPI (central, 1 container) + trimite comanda + creds RAR API: validare → mapare op→cod → enqueue (PII criptat) + ◀── {submissionId, status} ─────────────┘ + WORKER (proces separat): claim atomic → login RAR → postPrezentare → retry + Dashboard (Jinja2+HTMX): monitorizare live din RAR + stare coadă + editor mapări + ROAAUTO (timer) ──▶ GET /v1/prezentari?status=error → re-push (durabilitate pene lungi) +``` + +Stack: Python/FastAPI + SQLite (WAL) + httpx. Deploy: LXC Proxmox + Cloudflare Tunnel (start) → VPS (~5€/lună). +Open-source pe github.com/romfast, AGPL-3.0 (⚠️ decide CLA din ziua 1 dacă vrei dual-license). + +## ⚠️ Următorul pas BLOCANT — „The Assignment" (spike, ~1h, ÎNAINTE de cod) + +Pe endpoint-ul de **test RAR**, măsoară: +1. **Durata de viață a JWT-ului** (`/public/login` → `postPrezentare` la intervale crescătoare până la 401) + → dimensionează fereastra de retry autonom din worker. +2. **Dacă `postPrezentare` trece fără `b64Image`** (poză odometru) și fără `odometruInitial` + → decide dacă poza e obligatorie în prod și dacă ROAAUTO trebuie s-o atașeze. +3. **Valorile acceptate pentru `tipPrestatie` / `sistemReparat`** (enum nedocumentat). + +Rezultatul decide robustețea cozii și scopul real al ROAAUTO. Nu porni worker-ul înainte. + +## De făcut după spike (din plan-eng-review, secțiunea Verificare) + +1. `tools/import_dbf.py --dry-run` pe `mapare_prestatii.DBF` + `prestatii_rar.DBF` (raport întâi, apoi import). +2. Schelet repo: `app/api/v1`, `app/rar_client.py`, `app/worker`, `app/web`, SQLite (WAL), `docker compose up`, `/healthz` verde. +3. `POST /v1/prezentari` cu o comandă reală (test) → worker trimite → `FINALIZATA` la RAR + în dashboard. +4. Test idempotency (re-trimitere identică → același `submissionId`, fără dublu la RAR). +5. `needs_mapping` / `needs_data` (nu se trimite incomplet); `error` + re-push. +6. Verifică: SQLite fără câmp parolă; după `sent` PII criptat + `purge_after`; loguri fără parole. +7. Teste: unit (mapare, hash idempotency, validare odometru), integration (claim atomic, retry), E2E test RAR. + +## Decizii deja blocate (nu le re-deschide fără motiv) + +- Idempotency = **hash de conținut pe server**, UNIQUE (RAR n-are câmp nr. comandă, acceptă duplicate). +- **Reținere temporară 90 zile** a payload-ului **criptat**, apoi purjare (defensibilitate vs privacy). +- Odometru repair: **strict + stare `needs_data`** (nu trimite incomplet). +- Cherry-picks în v1: alertă submission-uri blocate, `/healthz`+`/metrics`, sugestie fuzzy mapare, export audit CSV. +- URL-urile RAR: **sursa de adevăr = VFP testat**, NU spec-ul (are typo-uri de copy/paste). + +## Open questions rămase + +1. Sursa pozei odometrului în fluxul ROAAUTO (dacă spike confirmă `b64Image` obligatoriu). +2. `tipPrestatie` — valori acceptate (de probat la spike). +3. Un singur user RAR per agent economic sau mai mulți (afectează `idUser` / filtrare monitorizare). +4. Monetizare/direcție SaaS — de reluat după ce prima prezentare reală merge la primul client. diff --git a/docs/plans/plan-design-review.md b/docs/plans/plan-design-review.md new file mode 100644 index 0000000..fa71a58 --- /dev/null +++ b/docs/plans/plan-design-review.md @@ -0,0 +1,271 @@ +# Design: Gateway RAR AUTOPASS (migrare ROAAUTO din VFP în web) + +Generat de /office-hours pe 2026-06-14 +Mod: Startup (proiect intern / intrapreneurship ISV) +Status: DRAFT + +## Problem Statement + +ROA (ERP) are nevoie să declare la **RAR AUTOPASS** prestațiile de service ale clienților săi +(service-uri auto care rulează **ROAAUTO**, Visual FoxPro + Oracle), conform **Legii 142/2023** și +**OM 210/2024**. Integrarea există azi în VFP (clasa `RarAutoPass`), dar **nu e încă pusă la clienți** — +doar testată pe endpoint-ul de test RAR. + +Problema reală a lui Mihai NU e a unui service, ci a unui **ISV**: nu vrea să redistribuie un `.exe` VFP +la fiecare client la fiecare corecție. Vrea **logica pe un server central, depanabilă central**, cu ROAAUTO +ca simplu client subțire care trimite comenzile. + +## Demand Evidence (validat: client real + lege) + +**Cel mai tare semnal:** un **client real a cerut automatizarea** introducerii prezentărilor în AUTOPASS — de +aici a pornit tot proiectul. Primul plătitor a cerut-o, nu e ipoteză. **Status quo înlocuit:** interfața web +oficială AUTOPASS, unde service-urile introduc **manual, prezentare cu prezentare**, operația principală — tedios. + +Obligație legală reală, nu ipotetică — **Legea 142/2023** (registrul electronic al istoricului vehiculelor) +obligă operatorii economici autorizați să transmită la RAR: +- la fiecare prestație: **VIN + indicația odometrului**; +- repararea/înlocuirea odometrului; +- operațiunile principale de reparație la **direcție, frânare, structura caroseriei/șasiului** și alte + sisteme de siguranță. +- Amenzi: informații eronate de la service 1.000–2.000 lei; manipulare odometru până la 5.000 lei / penal. + +Implică: **toți** clienții ROA + **mii de service-uri non-ROA** au aceeași obligație → există piață dincolo +de ROA pentru un canal de import (xlsx/csv) ulterior. + +## Status Quo + +Integrare VFP funcțională dar nedistribuită: `RarAutoPass` (`rar_autopass.prg`) vorbește direct cu RAR prin +`MSXML2.ServerXMLHTTP`; mapare în DBF (`mapare_prestatii`), nomenclator în `prestatii_rar`, jurnal în +`rar_log`; UI desktop cu tab-uri. Credențiale RAR în `settings.xml` pe fiecare stație (text clar). + +## Constraints + +- Stack: **Python / FastAPI** (ales). Hosting: **hibrid** — instanță centrală always-on operată de ROA + + Proxmox LXC pentru dev/staging. `romfast.ro`/hosting.com (doar PHP) nu găzduiește core-ul. +- Open-source pe **github.com/romfast** (licență recomandată **AGPL-3.0**). +- ROAAUTO rămâne client subțire (refolosim pattern-ul `MSXML2.ServerXMLHTTP`). + +## Premises (confirmate) + +1. Migrarea în web se justifică prin nevoia ISV: deploy central, fără redistribuire de exe-uri. ✅ +2. Cererea e reală și legală (L.142/2023) — clienți ROA + service-uri non-ROA. ✅ +3. Maparea operație→`codPrestatie` e în **core** (API-ul cere `prestatii` în `postPrezentare`). ✅ (corectat de utilizator pe baza spec-ului) +4. ROAAUTO = client subțire; mapare + retry + jurnal pe server. ✅ +5. **Topologie: gateway central, pass-through credențiale, ZERO stocare de parole.** ✅ + +## Contract API RAR AUTOPASS (din spec oficial v0.0.1, baza `/rar-autopass`) + +- Auth: `POST /public/login` {email, password} → `GetUtilizatoriDTO{ token, idUser, ... }`. + Token JWT atașat ca `Authorization: Bearer {token}` la toate apelurile securizate. +- Nomenclator: `GET /nomenclator/getNomenclatorPrestatii` → listă `{codPrestatie, numePrestatie}`; + `GET /nomenclator/getPrestatieByCodPrestatie/{cod}`. +- Prezentări: `POST /prezentari/postPrezentare` (payload `Prezentari`); `GET /prezentari/getAllPrezentari`; + `GET /prezentari/getPrezentare/{id}`; `PATCH /prezentari/markPrezentareAnulataById/{id}`; + `PATCH /prezentari/patchPrezentare/{id}`. Răspuns: `CustomResponseForMapping{ data, message, statusCode }`. +- Payload `Prezentari`: `vin`, `nrInmatriculare`, `dataPrestatie(date)`, `odometruInitial`, `odometruFinal`, + `obs`, `b64Image`, `sistemReparat`, `tipPrestatie`, `status`∈{SALVATA,FINALIZATA,ANULATA,UNDEFINED}, + `prestatii: [{codPrestatie, idPrezentare}]`. +- Cont/roluri (per agent economic): ADMIN creează CLIENT/ADMIN_CLIENT. **Nu replicăm** asta în gateway. + +## Recommended Approach — B: gateway central + coadă + token JWT scurt + +### Flux + +``` +ROAAUTO (VFP) ──POST /v1/prezentari {comanda + RAR creds + idempotency_key}──▶ Gateway FastAPI + (citește creds din Oracle clientului) │ + ├─ rezolvă maparea op→codPrestatie + ├─ INSERT submission (PII TRANZITORIU, status=queued) + ◀── răspuns imediat: {submissionId, status:queued|needs_mapping} ───────────┘ (dedup pe idempotency_key) + Worker (daemon/task fundal, poll SQLite) ── login RAR → JWT ──▶ postPrezentare ──▶ RAR + │ retry cu backoff în fereastra JWT + └─ la succes: PURJEAZĂ PII, reține doar hash+status+idPrezentare +Browser ──▶ Dashboard ── monitorizare CITITĂ LIVE din RAR (getAllPrezentari), nu din PII local ── + mapări, nomenclator +``` + +### Gestiunea credențialelor (cheia deciziei #5) + +- ROAAUTO trimite `email`+`password` RAR la fiecare apel (din Oracle-ul clientului, peste **HTTPS**). + Creds-urile trăiesc **doar în itemul de coadă** (în memorie/rândul de lucru), folosite de worker pentru `login`, + apoi **șterse**. Parola **nu se persistă** și se **scrubează** din loguri ȘI din capturile de excepție/APM. +- Worker-ul face `login` (nu API-ul) → POST-ul răspunde imediat fără latența RAR. Worker: login → JWT → postPrezentare, + retry cu backoff **în fereastra JWT-ului**. +- **Onestitate despre robustețe:** coada NU aduce reziliență la indisponibilitate RAR de durată — JWT-ul e scurt și + nu ținem parola ca să reluăm peste expirare. Ce aduce coada: răspuns asincron rapid pentru ROAAUTO + jurnal central + + retry pe erori tranzitorii scurte. **Durabilitatea reală pe pene lungi stă în ROAAUTO**, prin job-ul periodic de + **re-push** al submission-urilor rămase `error/pending` (retrimite cu creds proaspete). Coada acoperă minutele, + ROAAUTO acoperă orele. (Dacă măsurarea TTL-ului arată JWT lung, reevaluezi — vezi „The Assignment".) + +### Idempotență (critic — record legal, fără dubluri) + +`postPrezentare` NU e idempotent, iar avem două bucle de retry (worker + re-push ROAAUTO) → risc de **prezentări +duplicate la RAR** pentru un record urmărit legal. Soluție: +- ROAAUTO trimite un `idempotency_key` = hash(cont + VIN + dataPrestatie + set(codPrestatie)). +- Gateway: `UNIQUE(idempotency_key)` pe `submissions`. Re-trimiterea aceleiași chei NU creează submission nou. +- Worker, înainte de a retrimite: dacă submission-ul are deja `idPrezentare` (răspuns RAR) → marchează `sent`, nu reapelează. + +### Mașina de stări a unui submission + +`queued → sending → { sent | needs_mapping | error }` +- `needs_mapping`: operație fără `codPrestatie` mapat → **se ține gateway-side, NU se trimite incomplet** (API-ul cere + `prestatii`); după ce mapezi, trece în `queued`. (≠ VFP-ul de azi care o arunca silențios.) +- `error`: eligibil pentru re-push din ROAAUTO (`GET /v1/prezentari?status=error`). +- `sent`: are `idPrezentare` de la RAR; terminal. + +### Privacy-first / stateless (mentenanță & răspundere minime) + +Decizie: gateway-ul e **pur tranzit + interfață cu RAR**, NU depozit de date. +- PII-ul prezentării (VIN, km, date) trăiește în SQLite **doar tranzitoriu** cât e în coadă; **la `sent` se purjează**, + rămân doar `idempotency_key` (hash ireversibil) + status + `idPrezentare`. Nu se poate reconstrui VIN-ul din hash. +- **Monitorizarea se citește LIVE din RAR** (`getAllPrezentari`/`getPrezentare`) — RAR e sursa de adevăr, nu un jurnal local. +- Durabilitatea pe pene lungi stă **la margine** (Oracle ROAAUTO / fișierul încărcat), nu la tine → re-push din client. +- **Fără agregare de date.** Datele service-urilor NU se folosesc pentru alte produse. (Eventual, în viitor, doar + produs separat cu **opt-in explicit + anonimizare**, lawyered — niciodată default.) Privacy = argument de adopție, nu doar conformitate. + +### Componente (un repo, `docker compose up`) + +1. **API (`app/api/v1`)** — FastAPI: + - `POST /v1/prezentari` (una/mai multe) → validare Pydantic, enqueue, răspuns cu `submissionId`. + - `GET /v1/prezentari?status=&data=` și `/{id}` — monitorizare programatică pentru ROAAUTO + re-push. + - `GET /v1/nomenclator`, `POST /v1/nomenclator/refresh`. + - `GET/PUT /v1/mapari` — CRUD mapare per cont. + - `PATCH /v1/prezentari/{id}/anulare`, `/corectie` — proxy peste markPrezentareAnulataById / patchPrezentare. + - Auth gateway: **API key per cont ROA** (separată de credențialele RAR ale clientului); cu emitere/rotire/revocare. + - *(Amânat, NU în v1: `POST /v1/import` xlsx/csv — strat 2 / piață non-ROA.)* +2. **Client RAR (`app/rar_client.py`)** — portare din `rar_autopass.prg`: login+JWT, getNomenclatorPrestatii, + postPrezentare, getAllPrezentari, getPrezentare, markPrezentareAnulataById, patchPrezentare. `httpx` + retry/backoff. +3. **Worker (daemon / task de fundal) + coadă pe SQLite (`app/worker`)** — proces pornit non-stop (sau task + `asyncio` în aplicația FastAPI), sub Docker `restart: always`. Buclă: ia rândurile `status='queued'`, + revendică atomic (`BEGIN IMMEDIATE; UPDATE … SET status='sending' WHERE id=? AND status='queued'`), + login RAR, trimite, retry cu backoff, scrie status + `idPrezentare`. Reacție **instant**, fără întârziere. + *Fără Redis, fără arq, fără Postgres.* ROAAUTO oferă durabilitatea pe pene lungi (re-push). + Atenție `b64Image`: poate fi mare → stocat ca BLOB sau path pe disc, nu în RAM. +4. **Dashboard (`app/web`)** — **Jinja2 + HTMX** (server-rendered, zero build): Monitorizare **citită live din RAR** + (`getAllPrezentari`) + starea cozii curente (din `submissions`), Editor mapări, Browser nomenclator. API-first. +5. **SQLite** (mod WAL) — înlocuiește DBF, un singur fișier `.db`: + - `accounts`, `api_keys` (conturi ROA + chei gateway). + - `operations_mapping` (cod_op_service → codPrestatie, `auto_send`=trimite automat dacă e mapat) ← `mapare_prestatii`. + - `nomenclator_rar` (cache {codPrestatie, numePrestatie}) ← `prestatii_rar`. + - `submissions` (coadă + dedup): `idempotency_key` UNIQUE, status, statusCode RAR, eroare, `idPrezentare`, retry, timestamps. + Câmpurile PII (vin, km, dataPrestatie, prestatii) sunt **tranzitorii** — populate cât e `queued/sending`, + **purjate la `sent`**. (≠ `rar_log` care era jurnal permanent.) + - **Notă: niciun câmp pentru parole RAR; niciun PII reținut după trimitere.** (`import_jobs` — doar la xlsx/csv, amânat.) + +### Client ROAAUTO (VFP) — refactor minim + +- `settings.xml` păstrează doar **URL gateway + API key** (nu mai ține mapări/nomenclator). +- Credențialele RAR ale clientului se citesc din **Oracle (ROA)** și se trimit în payload la gateway. +- `export_comenzi.prg` rămâne (citește `comenzi_service`/`operatii`), dar construiește JSON și face + `POST /v1/prezentari` în loc de XML + apel direct RAR. +- Dispar din VFP: `Login`, `UpdateNomenclator`, `GetCodRarPentruOperatie`, maparea, `rar_log` → trec în web. +- Se adaugă un job periodic „re-push pending" (timer existent din `rar-forms.prg` se poate reutiliza). + +## Approaches Considered + +- **A — sincron, fără coadă** (S, risc mic): ships rapid, dar fără retry autonom. Bun ca prim pas, respins ca țintă. +- **B — coadă + token JWT scurt** (M, recomandat ✅): robust la indisponibilitate RAR, nu pierzi prestații obligatorii legal. +- **C — outbox în Oracle** (M/L): cel mai decuplat, dar cere acces gateway→Oracle client (VPN/rețea). Reținut ca opțiune pentru clienți non-ROA / viitor. + +## Open Questions + +1. Durata reală a JWT-ului RAR (decide fereastra de retry autonom). De măsurat pe endpoint-ul de test. +2. `sistemReparat` / `tipPrestatie` — valori acceptate (enum nedocumentat în spec). De clarificat cu RAR. +3. Modelul de cont RAR per client: un singur user RAR per agent economic sau mai mulți (afectează cum mapezi `idUser`). +4. Monetizare/direcție (nedecisă): vezi mai jos — de reluat după ce A→B merge la primul client. + +## Arhitectura în mare (modelul mental) + +Azi VFP face 3 treburi pe FIECARE PC client: (a) ia comanda, (b) mapează + se loghează la RAR, (c) ține jurnalul → +de-aia trebuie redistribuit la fiecare corecție. Migrarea = muți (b)+(c) pe un server central al tău: +- **ROAAUTO (VFP, la client) = expeditorul** — citește comanda + creds RAR din Oracle și le trimite la gateway. Atât. +- **Gateway (Python, central) = creierul** — primește, mapează, login RAR, trimite, retry, jurnal. Aici faci corecțiile o dată, pentru toți. +- **Dashboard web = panoul de control** — vezi ce s-a trimis/eșuat, editezi maparea. + +## Opțiuni de deploy (unde rulează gateway-ul) + +| Opțiune | Cum | Cost | Când | +|---|---|---|---| +| **LXC Proxmox + Cloudflare Tunnel** | container la birou, expus public HTTPS fără IP static / porturi deschise | 0 € | **Start + teste** (risc: netul/curentul biroului) | +| **VPS mic always-on** (ex. Hetzner) | același container, mașină care nu cade | ~5 €/lună | **Clienți reali / producție** (recomandat) | +| romfast.ro / hosting.com | Python via cPanel/Passenger (WSGI) | inclus | ⚠️ FastAPI e ASGI + worker-ul e daemon → shared hosting nepotrivit (shim fragil, fără procese persistente). Doar landing | + +Recomandare: start pe **LXC + Cloudflare Tunnel** (cost 0), mutare pe **VPS** la clienți. Mutarea = copiezi containerul + fișierul `.db`. +romfast.ro rămâne doar pentru landing/prezentare, nu țintă de producție (ASGI + worker daemon nu merg pe shared hosting). + +- **Cod:** open-source pe github.com/romfast, **AGPL-3.0**. Deploy = **un container** cu FastAPI (uvicorn) + worker-ul ca task de fundal/al doilea proces, sub Docker `restart: always` + un volum SQLite. +- **Dev/staging:** LXC Proxmox. **Migrare date:** `tools/import_dbf.py` (`mapare_prestatii.DBF` + `prestatii_rar.DBF` cu `dbfread`). + +## Teza de produs & direcție SaaS + +**Teza:** *cel mai ușor mod de a băga operațiile de service în AUTOPASS* — din ROAAUTO (API), din alte aplicații, +sau din fișiere — în loc de tastarea manuală din interfața oficială. Câștigi prin **efort minim cerut service-ului**, +nu prin features. (Funnel-ul „read public gratuit" — RESPINS: nimeni nu vrea să *citească* nomenclatorul; valoarea +e 100% în **trimitere**.) + +**Lecția GTM de la demoanaf.ro:** a devenit viral nu prin model plătit, ci oferind o variantă **reimaginată, simplă, +plăcută** a unui serviciu oficial greoi/instabil; s-a răspândit în grupurile de Facebook. Echivalentul tău: o cale +**dramatic mai simplă decât tastarea manuală AUTOPASS**, construită rapid cu AI dar pe arhitectură solidă. + +**Wedge validat:** automatizezi tastarea manuală AUTOPASS, **începând cu clientul care a cerut-o** (cale ROAAUTO/API). + +**Trepte (același motor — mapare + coadă + trimitere + monitorizare — fără rescriere):** +- **Treapta 1 (acum):** core-ul pentru clientul care a cerut + clienții ROA, prin ROAAUTO. Îți rezolvi nevoia. +- **Treapta 2 (non-ROA, web upload):** import xlsx/csv cu **mapare reținută** (vezi mai jos) + dashboard. Login web, + fără instalare. **Primul venit** — freemium **pe volum** (gratis sub N prezentări/lună pt. service mici, plată peste; + metrica de preț = prezentări/lună = fix unitatea obligatorie legal). +- **Treapta 3 (diferențiere):** integrări mai adânci + sugestii AI de mapare (eventual conector MCP). + +**Moat:** (1) mapările reținute per service cresc costul de plecare; (2) lățimea integrării (mergi indiferent ce +software are service-ul); (3) fiabilitatea de conformitate (retry, monitorizare din RAR — nu pierzi o declarație legală); +(4) **privacy** (nu reținem datele lor) — el însuși argument de adopție. Saltul fără rescriere: API key + mapare per cont, +**zero parole stocate, zero PII reținut**. + +### Adopție în masă & praguri (calibrat pe cifre reale) + +Declanșatorul trecerii de la tastarea manuală în RAR la upload = **timp salvat / efort de trecere**: +~2-4 min/prezentare manual × volum lunar. Clienți reali cunoscuți: **60-80** și **80-100 prezentări/lună** → +**3-6 ore/lună** de tastare = durerea care îi convertește. +- Prima folosire trebuie trivială (upload Excel existent → mapare reținută → trimite, sub 5 min). +- Gratis la volumul lor = fără decizie de achiziție, doar încearcă. +- **Freemium pe volum:** gratis **~30-40 prezentări/lună** (service mici = bază virală, cost ~0 la tine); + plată peste prag (banda 1 ≈ 50-150/lună prinde fix clienții actuali — primii bani de la cine a cerut serviciul). + Metrica de preț = prezentări/lună = unitatea obligatorie legal. +- Ironie de reținut: service-urile mici au cea mai mică durere (greu de convins, dar virale); volumele mari au cea + mai mare durere (cei mai dispuși să plătească). Gratis = achiziție; venit = volume mari. + +## Import xlsx/csv — UX (stratul SaaS, treapta 2) + +Două straturi de mapare, **ambele reținute per cont** (cheia produsului — „map once, reuse forever"): +1. **Mapare coloane** (schema fișierului → câmpuri canonice): ex. „«Serie șasiu»→VIN, «Index km»→odometruFinal". + Reluată automat dacă headerele se repetă. +2. **Mapare operații** (etichetele/codurile service-ului → `codPrestatie` AUTOPASS), cu **sugestie fuzzy** pe denumire. + +Flux: upload → recunoaște coloanele (reia maparea) → propune maparea operațiilor (reținută + sugestii) → +**preview** (ce se trimite, rânduri nemapate flag-uite) → „Trimite la RAR" → monitorizare. A 2-a oară: upload → preview → trimite. + +**Spectru de integrare (același backend):** API (POST prezentări, ca ROAAUTO) → drop fișier programat (folder/SFTP/email-to-import) → upload manual în browser (zero instalare). Cine poate, integrează API; cine nu, dă fișier. + +## Success Criteria + +- O prezentare reală trimisă din ROAAUTO prin gateway apare `FINALIZATA` la RAR (test), vizibilă în dashboard. +- Paritate cu VFP: același `codPrestatie` rezultat din mapare pe aceleași comenzi. +- Reziliență: RAR indisponibil → submission `queued/error` cu retry, ROAAUTO nu se blochează; re-push recuperează. +- Securitate: niciun credențial RAR în client/`settings.xml` și niciun câmp de parolă în SQLite. +- Privacy: după `sent`, în SQLite nu rămâne PII de vehicul (doar hash+status+idPrezentare); monitorizarea vine din RAR. + +## The Assignment (următorul pas concret) + +Pe endpoint-ul de **test RAR**, măsoară **durata de viață a JWT-ului** întors de `/public/login` (fă un login, +apoi `postPrezentare` la intervale crescătoare până la 401). Numărul ăsta dimensionează fereastra de retry +autonom din worker și decide dacă ai nevoie de job-ul de re-push în ROAAUTO. E o oră de muncă și deblochează +toată decizia de robustețe din B. + +## What I noticed about how you think + +- Ai corectat insight-ul meu „wedge = doar VIN+km" cu „API-ul cere și operațiile de manopelă" — și aveai + dreptate, ai citit contractul, nu l-ai presupus. Asta e exact instinctul care face diferența: sursa, nu pitch-ul. +- Ai pus singur problema de custodie a parolelor („nu vreau să salvez parole") înainte să ți-o ridic eu — + gândești în termeni de răspundere, nu doar de „merge/nu merge". +- Vezi proiectul ca ISV, nu ca utilizator final: „nu vreau să redistribui la fiecare corecție" e fix + raționamentul care justifică web-ul. Mulți ar fi rescris fără să poată articula de ce. +``` diff --git a/docs/plans/plan-eng-review.md b/docs/plans/plan-eng-review.md new file mode 100644 index 0000000..1d9043e --- /dev/null +++ b/docs/plans/plan-eng-review.md @@ -0,0 +1,241 @@ +# Plan implementare: Gateway RAR AUTOPASS (migrare ROAAUTO din VFP în web) + +Sursă: review CEO (SELECTIVE EXPANSION) peste design-ul `vreau-sa-migrez-acest-precious-mochi.md`. +Grounded pe codul VFP existent + spec-ul oficial RAR (`Documentatie Serviciu AutoPass_Final.txt`, +`Document informativ RAR- Autopass.txt`). + +## Context + +ROAAUTO (Visual FoxPro + Oracle, la fiecare service client) declară azi prestațiile la RAR AUTOPASS +direct din clasa `RarAutoPass` (`rar_autopass.prg`), prin `MSXML2.ServerXMLHTTP`. Obligație legală +(L.142/2023, OM 210/2024). Integrarea e testată doar pe endpoint-ul de test RAR, nepusă la clienți. + +Problema reală e de **ISV**: nu vrei să redistribui un `.exe` VFP la fiecare corecție. Muți logica +(mapare + login RAR + jurnal + retry) pe un **gateway central depanabil o dată pentru toți**, iar ROAAUTO +rămâne client subțire. Un client real a cerut automatizarea — primul plătitor, nu ipoteză. + +Rezultat țintă (treapta 1): o prezentare reală trimisă din ROAAUTO prin gateway apare la RAR, vizibilă +în dashboard, cu retry pe erori tranzitorii și fără a stoca parole. + +## Decizii blocate în review + +| # | Decizie | Alegere | +|---|---|---| +| Mod | Postura review | **SELECTIVE EXPANSION** — bulletproof treapta 1 + 4 cherry-picks acceptate | +| Idempotency | Anti-dublură | **Hash de conținut pe server**, UNIQUE; "nu se acceptă 2 prezentări identice". `nr_comanda` NU e cerut (RAR n-are câmpul; SaaS n-are comenzi) | +| Defensibilitate | Dovadă vs privacy | **Reținere temporară 90 zile** a payload-ului **criptat**, apoi purjare | +| Poză odometru | b64Image obligatoriu? | **Se validează întâi la „The Assignment"** pe endpoint test; nu construim orbește | +| Odometru repair | Validare | **Strict + stare `needs_data`** (nu trimite incomplet) | +| Cherry-picks | Adăugate în v1 | Alertă submission-uri blocate; `/healthz`+`/metrics`; sugestie fuzzy mapare; export audit CSV | +| Import DBF | Migrare date | `import_dbf.py` cu **dry-run + raport** înainte de scriere | + +## Constatări din spec-ul oficial (corecturi față de design) + +1. **`Prezentari` n-are câmp de număr comandă** (spec model #11, l.387-391). RAR acceptă duplicate (fără + constrângere unică). → cheia de idempotență e doar pentru retry-urile *tale*, hash pe conținut. +2. **Toate câmpurile sunt obligatorii except `obs`** (doc informativ l.47). Asta include **`b64Image` + (poză odometru, l.40)** — VFP-ul trimite azi gol. Posibil gap de conformitate în producție. De validat. +3. **Înlocuire/reparație odometru** (l.37, l.39): cer **ambele** valori `odometruInitial` + `odometruFinal`. + VFP trimite azi `odometruInitial: null`. Anti-fraudă (penal până la 5.000 lei). +4. **`sistemReparat` e „codificat în lista de prestații"** (l.45) → probabil derivabil din codurile + `codPrestatie` prin mapare, nu input liber separat. Reduce Open Question #2. +5. **URL-uri: spec-ul are typo-uri de copy/paste** (postPrezentare listat ca `/patchPrezentare/{id}` l.244; + markAnulata ca `/getPrezentare/{id}` l.227). **Sursa de adevăr = URL-urile testate din VFP** + (`/prezentari/postPrezentare`, `/prezentari/markPrezentareAnulataById/{id}`). +6. **Monitorizare:** spec-ul are `getAllPrezentari` (prezentări active); VFP folosește + `getAllPrezentariFinalizate` (nedocumentat). De ales deliberat — vezi „Monitorizare" mai jos. + +## ⚠️ Prerequisite blocant — „The Assignment" (spike, ~1h, ÎNAINTE de orice cod) + +Pe endpoint-ul de **test RAR**, măsoară pe `/public/login` + `postPrezentare`: +- **Durata de viață a JWT-ului** (login, apoi postPrezentare la intervale crescătoare până la 401) + → dimensionează fereastra de retry autonom din worker. +- **Dacă `postPrezentare` trece FĂRĂ `b64Image`** și fără `odometruInitial` → decide dacă poza e + obligatorie în prod (constatare #2) și dacă ROAAUTO trebuie să atașeze poza. +- **Valorile acceptate pentru `tipPrestatie` / `sistemReparat`** (enum nedocumentat) — probează câteva. + +Rezultatul acestui spike decide robustețea cozii și scopul real al ROAAUTO. Nu pornește implementarea +worker-ului înainte de el. + +## Arhitectură + +``` +ROAAUTO (VFP, la client) GATEWAY FastAPI (central, 1 container) + citește comanda + creds RAR din Oracle POST /v1/prezentari {comanda + RAR creds + idempotency implicit} + ──HTTPS──────────────────────────────────────▶ API + ├─ valid Pydantic (vin, odometru, prestatii) + ├─ rezolvă op→codPrestatie (operations_mapping) + ├─ derivă sistemReparat din coduri + ├─ calc idempotency_key = hash(conținut canonic) + ├─ INSERT submission (PII criptat tranzitoriu, queued) + ◀── {submissionId, status: queued|needs_mapping|needs_data} ──┘ (UNIQUE → dedup, întoarce id existent) + + WORKER (proces separat, restart:always, poll SQLite WAL) + claim atomic: BEGIN IMMEDIATE; UPDATE…SET sending WHERE id=? AND status='queued' + login RAR → JWT → postPrezentare → retry/backoff ÎN fereastra JWT + succes: scrie idPrezentare; PURJEAZĂ creds; PII criptat rămâne max 90 zile + + Browser ─▶ Dashboard (Jinja2+HTMX): monitorizare + coadă curentă + editor mapări + nomenclator + audit CSV + ROAAUTO (timer re-push) ─▶ GET /v1/prezentari?status=error → retrimite cu creds proaspete (durabilitate pene lungi) +``` + +### Data flow + shadow paths (postPrezentare) + +``` +INPUT ──▶ VALIDARE ──▶ MAPARE op→cod ──▶ ENQUEUE ──▶ WORKER login+send ──▶ RAR + │ │ │ │ │ + nil/empty vin invalid cod lipsă → idempotency JWT expirat → error (re-push ROAAUTO) + vin? odometru<0 needs_mapping key dublu → RAR 4xx → needs_data/error (logat, NU silent) + creds? repair fără sistemReparat întoarce id RAR 5xx/timeout → retry backoff + init → needs_data nederivabil existent b64Image lipsă (dacă obligatoriu) → needs_data +``` + +### Mașina de stări submission + +``` +queued → sending → { sent | needs_mapping | needs_data | error } + needs_mapping : operație fără codPrestatie mapat → ținut gateway-side, NU trimis incomplet + needs_data : repair odometru fără init/final SAU poză lipsă (dacă obligatorie) → ținut, NU trimis + error : eligibil re-push din ROAAUTO (GET ?status=error) + sent : are idPrezentare RAR; creds purjate; PII criptat max 90 zile; terminal +``` + +## Componente (un repo, `docker compose up`) + +1. **API `app/api/v1`** (FastAPI): + - `POST /v1/prezentari` (una/mai multe) → Pydantic, mapare, enqueue, răspuns `submissionId`. + - `GET /v1/prezentari?status=&data=` și `/{id}` — monitorizare programatică + re-push ROAAUTO. + - `GET /v1/nomenclator`, `POST /v1/nomenclator/refresh`. + - `GET/PUT /v1/mapari` — CRUD mapare per cont, cu **sugestie fuzzy** pe denumire (cherry-pick). + - `PATCH /v1/prezentari/{id}/anulare`, `/corectie` — proxy markPrezentareAnulataById / patchPrezentare. + - `GET /v1/audit/export?from=&to=` — **CSV** cu ce s-a trimis (cherry-pick, leagă reținerea 90 zile). + - Auth gateway: **API key per cont ROA** (separată de creds RAR), cu emitere/rotire/revocare. + - **Redactare credențiale (CORE, nu opțional):** middleware care garantează că body-ul pe + `/v1/prezentari` NU se loghează niciodată și `password` se scrubează din excepții/APM. + - *(Amânat: `POST /v1/import` xlsx/csv — treapta 2.)* +2. **Client RAR `app/rar_client.py`** — portare din `rar_autopass.prg` + `rar-forms.prg`: + login+JWT, getNomenclatorPrestatii, postPrezentare, getAllPrezentari, getPrezentare, + markPrezentareAnulataById, patchPrezentare. `httpx` + retry/backoff. **URL-uri din VFP testat**, nu din spec. +3. **Worker `app/worker`** — proces separat sub Docker `restart: always` (NU asyncio în uvicorn dacă scalezi + workeri — un singur scriitor pe coadă ca să nu dublezi un record legal). Buclă claim atomic → login → send → + retry → scrie status+idPrezentare. `b64Image` mare → BLOB/path pe disc, nu RAM. +4. **Dashboard `app/web`** — Jinja2 + HTMX (server-rendered, zero build): monitorizare, stare coadă, + editor mapări (cu fuzzy), browser nomenclator, **banner alertă submission-uri blocate** (cherry-pick). +5. **SQLite (WAL)** — un fișier `.db`: + - `accounts`, `api_keys`. + - `operations_mapping` (cod_op_service → codPrestatie, `auto_send`) ← `mapare_prestatii.DBF`. + - `nomenclator_rar` (cache {codPrestatie, numePrestatie}) ← `prestatii_rar.DBF`. + - `submissions`: `idempotency_key` UNIQUE, status, statusCode RAR, eroare, `idPrezentare`, retry, timestamps. + PII (`vin`, `odometru`, `dataPrestatie`, `prestatii`, `b64Image`) **criptat** + `purge_after` (sent+90z). + - **Niciun câmp pentru parole RAR.** + +## Securitate & raza de explozie (constatare review #5) + +Gateway-ul vede parolele RAR ale tuturor clienților în memorie/tranzit. Zero-storage reduce riscul la rest, +dar o greșeală de logging scurge parole live, iar AGPL = codul e public. Controale **hard**: +- Middleware de redactare (mai sus) — body-uri cu parole niciodată în loguri/APM/excepții. +- HTTPS obligatoriu; recomandare TLS pinning din ROAAUTO către gateway. +- Parola folosită doar pentru `login` în worker, apoi ștearsă din itemul de coadă. + +### Error & rescue map (extras) + +| Codepath | Ce poate eșua | Excepție | Rescued? | Acțiune | User vede | +|---|---|---|---|---|---| +| rar_client.login | timeout/5xx | httpx.TimeoutException | Y | retry backoff în fereastra JWT | submission `error`, re-push | +| rar_client.login | 401 creds greșite | AuthError | Y | NU retry; marchează error+motiv | „credențiale RAR invalide" | +| rar_client.postPrezentare | 4xx validare RAR | RarValidationError | Y | needs_data + payload RAR logat | rând flag-uit în dashboard | +| rar_client.postPrezentare | JSON malformat/empty | JSONDecodeError | Y | error + raw response logat (scrub) | submission `error` | +| API enqueue | idempotency dublu | IntegrityError(UNIQUE) | Y | întoarce submissionId existent | „deja înregistrat" | +| worker claim | două procese | (prevenit) BEGIN IMMEDIATE | Y | un singur scriitor | n/a | +| mapare | cod lipsă | (control de flux) | Y | needs_mapping, NU trimite | „necesită mapare" | + +**Regulă:** fără `except Exception` generic. Fiecare rescue: retry / degradare cu mesaj / re-raise cu context. + +### Failure modes registry (gap-uri critice de evitat) + +| Codepath | Failure | Rescued | Test | User vede | Logged | +|---|---|---|---|---|---| +| postPrezentare repair fără odometru init | record fraud-sensibil incomplet | Y (needs_data) | DA | flag dashboard | DA | +| dublu-send din 2 bucle retry | duplicat la RAR | Y (idempotency UNIQUE) | DA | nimic (transparent) | DA | +| poză lipsă dacă obligatorie | RAR respinge | Y (needs_data după spike) | DA | flag dashboard | DA | +| submission blocat tăcut | declarație legală pierdută | Y (alertă cherry-pick) | DA | banner + webhook | DA | + +## Client ROAAUTO (VFP) — refactor minim + +- `settings.xml` păstrează doar **URL gateway + API key** (rotește parola de test expusă acum în SVN!). +- Creds RAR ale clientului se citesc din **Oracle** și se trimit în payload la gateway peste HTTPS. +- `export_comenzi.prg` rămâne, dar construiește JSON și face `POST /v1/prezentari` (nu XML + apel RAR direct). +- Dispar din VFP: `Login`, `UpdateNomenclator`, `GetCodRarPentruOperatie`, maparea, `rar_log` → în web. +- Job periodic „re-push pending" — reutilizează timer-ul existent (`OnAutoProcessTimer`/`nTimerHandle` din `rar-forms.prg`). +- Dacă spike-ul confirmă poza obligatorie: ROAAUTO atașează poza odometrului (de clarificat sursa). + +## Migrare date + +`tools/import_dbf.py` (cu `dbfread`) — **dry-run + raport întâi**: rânduri valide, mapări orfane, +coduri necunoscute în nomenclator. Confirmi, apoi scrie în SQLite. Surse: `mapare_prestatii.DBF`, +`prestatii_rar.DBF`. (`rar_log.DBF` NU se migrează — jurnalul nou e `submissions` + live din RAR.) + +## Observabilitate (cherry-picks) + +- `/healthz` — worker viu + ultimul login RAR reușit + adâncime coadă. +- `/metrics` — submissions pe status, latență send, retry count, backlog needs_mapping/needs_data. +- Alertă submission-uri blocate — banner dashboard + webhook/email peste prag (plasă de siguranță legală). + +## Monitorizare — sursa de adevăr + +Citit **live din RAR** + stare coadă locală. Atenție: `getAllPrezentariFinalizate` (VFP) întoarce doar +FINALIZATA; `getAllPrezentari` (spec) întoarce active. Alege `getAllPrezentari` dacă vrei și draft/anulate. +Cache scurt (ex. 30-60s) + UX „RAR indisponibil, arăt ultima stare a cozii" (nu lega dashboard-ul de uptime RAR). + +## Deploy + +- Start: **LXC Proxmox + Cloudflare Tunnel** (0 €, teste). Producție: **VPS mic always-on** (~5 €/lună). +- Un container: uvicorn (API) + worker (proces 2), `restart: always`, volum SQLite. Mutare = copiezi container + `.db`. +- Open-source pe github.com/romfast, **AGPL-3.0**. ⚠️ Vezi „Decizie one-way door" mai jos. +- romfast.ro/hosting.com = doar landing (ASGI + worker daemon nu merg pe shared hosting). + +## Verificare (end-to-end) + +1. Spike „The Assignment" rulat, JWT TTL + cerințe b64Image/odometru documentate. +2. `import_dbf.py --dry-run` produce raport corect pe DBF-urile reale; apoi import confirmat. +3. `docker compose up`; `/healthz` verde. +4. `POST /v1/prezentari` cu o comandă reală din ROAAUTO (test) → `submissionId`, worker trimite, + apare `FINALIZATA` la RAR (test) și în dashboard. +5. Re-trimite aceeași comandă identică → întoarce același `submissionId` (idempotency), NU dublă la RAR. +6. Trimite operație nemapată → `needs_mapping`; repair odometru fără init → `needs_data`; nu se trimit incomplet. +7. Oprește RAR (sau forțează 5xx) → submission `error`, ROAAUTO re-push recuperează; nimic blocat tăcut. +8. Verifică: SQLite n-are câmp parolă; după `sent`, PII e criptat și are `purge_after`; logurile n-au parole. +9. Teste automate: unit (mapare, idempotency hash, validare odometru), integration (worker claim atomic, + retry/backoff), E2E pe endpoint test RAR. + +## Decizie one-way door de semnalat (CEO) + +**Licență AGPL fără CLA** poate bloca un viitor strat SaaS comercial: odată ce intră PR-uri externe sub AGPL, +relicențierea cere acordul tuturor contributorilor. Dacă vrei să păstrezi opțiunea de dual-license (open core + +hosted comercial — exact teza ta de la treapta 2), adoptă **CLA / copyright assignment din ziua 1**. AGPL în sine +e bună pentru moat (forțează competitorii care-l găzduiesc să-și deschidă modificările). Decizia: deliberat, acum. + +## NOT in scope (amânat, cu motiv) + +- `POST /v1/import` xlsx/csv + UX mapare coloane — treapta 2 (piață non-ROA). Motor identic, fără rescriere. +- Modelul de conturi RAR (addClient/roluri) — nu-l replicăm; rămâne la RAR. +- Outbox în Oracle (Approach C) — pentru clienți non-ROA / viitor, cere acces gateway→Oracle. +- Agregare/produse din datele service-urilor — niciodată default; doar opt-in + anonimizare + lawyered. +- Redis/arq/Postgres — SQLite WAL + un worker acoperă volumul (60-100 prezentări/lună/client). + +## Open questions rămase + +1. Sursa pozei odometrului în fluxul ROAAUTO (dacă spike-ul confirmă b64Image obligatoriu). +2. `tipPrestatie` — valori acceptate (de probat la spike). +3. Un singur user RAR per agent economic sau mai mulți (afectează `idUser`/filtrare monitorizare). +4. Monetizare/direcție SaaS — de reluat după ce prima prezentare reală merge la primul client. + +## What already exists (reuse) + +| Sub-problemă | Reuse din VFP | +|---|---| +| Contract RAR (login/JWT, nomenclator, postPrezentare, cancel) | `rar_autopass.prg`, `rar-forms.prg:655,720` → `rar_client.py` | +| Mapare op→codPrestatie + `auto_send` | `GetCodRarPentruOperatie` → `operations_mapping` | +| Timer re-push | `OnAutoProcessTimer`/`nTimerHandle` (`rar-forms.prg`) | +| Export (oglindă treapta 2) | `btnExportExcel.Click` (`rar_advanced.prg`) | +| Migrare DBF | `import_dbf.py` citește direct cele 3 `.DBF` | diff --git a/export_comenzi.prg b/export_comenzi.prg new file mode 100644 index 0000000..1acb2d9 --- /dev/null +++ b/export_comenzi.prg @@ -0,0 +1,83 @@ +* export_comenzi.prg +PROCEDURE ExportComenziXML +PARAMETERS tcFileName, tdData + +LOCAL loXML AS MSXML2.DOMDocument.6.0 +LOCAL lcXML, llSuccess +llSuccess = .F. + +Try + loXML = CREATEOBJECT("MSXML2.DOMDocument.6.0") + loXML.async = .F. + + * Creare structura XML + TEXT TO lcXML NOSHOW + + + + ENDTEXT + + loXML.loadXML(lcXML) + loRoot = loXML.documentElement + + * Selectam comenzile pentru export + SELECT comenzi_service + SET FILTER TO data_comanda = tdData AND status = "FINALIZAT" + SCAN + * Adaugam nodul pentru comanda + loComanda = loXML.createElement("comanda") + + * Adaugam detaliile comenzii + THIS.AddXMLNode(loXML, loComanda, "nr_comanda", nr_comanda) + THIS.AddXMLNode(loXML, loComanda, "data", TTOC(data_comanda, 1)) + THIS.AddXMLNode(loXML, loComanda, "vin", vin) + THIS.AddXMLNode(loXML, loComanda, "nr_inmatriculare", nr_auto) + THIS.AddXMLNode(loXML, loComanda, "km_final", TRANSFORM(km)) + THIS.AddXMLNode(loXML, loComanda, "km_initial", "0") + THIS.AddXMLNode(loXML, loComanda, "observatii", observatii) + + * Adaugam operatiile + loOperatii = loXML.createElement("operatii") + + SELECT operatii + SCAN FOR id_comanda = comenzi_service.id + loOperatie = loXML.createElement("operatie") + THIS.AddXMLNode(loXML, loOperatie, "cod_operatie", cod_operatie) + THIS.AddXMLNode(loXML, loOperatie, "denumire", denumire) + loOperatii.appendChild(loOperatie) + ENDSCAN + + loComanda.appendChild(loOperatii) + loRoot.appendChild(loComanda) + ENDSCAN + + * Salvam XML-ul + loXML.save(tcFileName) + llSuccess = .T. + +Catch To loEx + MESSAGEBOX("Eroare export XML: " + loEx.Message, 16, "Eroare") + llSuccess = .F. +Endtry + +Return llSuccess + +* Helper pentru adaugare noduri +PROCEDURE AddXMLNode +PARAMETERS loXML, loParent, tcName, tcValue +LOCAL loNode, loText, llSuccess +llSuccess = .F. + +Try + loNode = loXML.createElement(tcName) + loText = loXML.createTextNode(tcValue) + loNode.appendChild(loText) + loParent.appendChild(loNode) + llSuccess = .T. + +Catch To loEx + MESSAGEBOX("Eroare adaugare nod XML: " + loEx.Message, 16, "Eroare") + llSuccess = .F. +Endtry + +Return llSuccess \ No newline at end of file diff --git a/mapare_prestatii.CDX b/mapare_prestatii.CDX new file mode 100644 index 0000000..d01cfe1 Binary files /dev/null and b/mapare_prestatii.CDX differ diff --git a/mapare_prestatii.DBF b/mapare_prestatii.DBF new file mode 100644 index 0000000..70f291d Binary files /dev/null and b/mapare_prestatii.DBF differ diff --git a/mapare_prestatii.FPT b/mapare_prestatii.FPT new file mode 100644 index 0000000..601fa8d Binary files /dev/null and b/mapare_prestatii.FPT differ diff --git a/nfjsonread.prg b/nfjsonread.prg new file mode 100644 index 0000000..09a0573 --- /dev/null +++ b/nfjsonread.prg @@ -0,0 +1,775 @@ +*------------------------------------------------------------------- +* Created by Marco Plaza vfp2nofox@gmail.com / @vfp2Nofox +* ver 2.000 - 26/03/2016 +* ver 2.090 - 22/07/2016 : +* improved error management +* nfjsonread will return .null. for invalid json +*------------------------------------------------------------------- +Lparameters cjsonstr,isFileName,reviveCollection + +#Define crlf Chr(13)+Chr(10) + +Private All + +stackLevels=Astackinfo(aerrs) + +If m.stackLevels > 1 + calledFrom = 'called From '+aerrs(m.stackLevels-1,4)+' line '+Transform(aerrs(m.stackLevels-1,5)) +Else + calledFrom = '' +Endif + +oJson = nfJsonCreate2(cjsonstr,isFileName,reviveCollection) + +Return Iif(Vartype(m.oJson)='O',m.oJson,.Null.) + + +*------------------------------------------------------------------------- +Function nfJsonCreate2(cjsonstr,isFileName,reviveCollection) +*------------------------------------------------------------------------- +* validate parameters: + +Do Case +Case ; + Vartype(m.cjsonstr) # 'C' Or; + Vartype(m.reviveCollection) # 'L' Or ; + Vartype(m.isFileName) # 'L' + + jERROR('invalid parameter type') + +Case m.isFileName And !File(m.cjsonstr) + + jERROR('File "'+Rtrim(Left(m.cjsonstr,255))+'" does not exist') + + +Endcase + +* process json: + +If m.isFileName + cjsonstr = Filetostr(m.cjsonstr) +Endif + + +cJson = Rtrim(Chrtran(m.cjsonstr,Chr(13)+Chr(9)+Chr(10),'')) +pChar = Left(Ltrim(m.cJson),1) + + +nl = Alines(aj,m.cJson,20,'{','}','"',',',':','[',']') + +For xx = 1 To Alen(aj) + If Left(Ltrim(aj(m.xx)),1) $ '{}",:[]' Or Left(Ltrim(m.aj(m.xx)),4) $ 'true/false/null' + aj(m.xx) = Ltrim(aj(m.xx)) + Endif +Endfor + + +Try + + x = 1 + cError = '' + oStack = Createobject('stack') + + oJson = Createobject('empty') + + Do Case + Case aj(1)='{' + x = 1 + oStack.pushObject() + procstring(m.oJson) + + Case aj(1) = '[' + x = 0 + procstring(m.oJson,.T.) + + Otherwise + Error 'Invalid Json: expecting [{ received '+m.pChar + + Endcase + + + If m.reviveCollection + oJson = reviveCollection(m.oJson) + Endif + + +Catch To oerr + + strp = '' + + For Y = 1 To m.x + strp = m.strp+aj(m.y) + Endfor + + Do Case + Case oerr.ErrorNo = 1098 + + cError = ' Invalid Json: '+ m.oerr.Message+crlf+' Parsing: '+Right(m.strp,80) + +*+' program line: '+Transform(oerr.Lineno)+' array item '+Transform(m.x) + + Case oerr.ErrorNo = 2034 + + cError = ' INVALID DATE: '+crlf+' Parsing: '+Right(m.strp,80) + + + Otherwise + + cError = 'program error # '+Transform(m.oerr.ErrorNo)+crlf+m.oerr.Message+' at: '+Transform(oerr.Lineno)+crlf+' Parsing ('+Transform(m.x)+') ' + + Endcase + +Endtry + +If !Empty(m.cError) + jERROR(m.cError) +Endif + +Return m.oJson + + + +*------------------------------------------------ +Procedure jERROR( cMessage ) +*------------------------------------------------ +Error 'nfJson ('+m.calledFrom+'):'+crlf+m.cMessage +Return To nfJsonRead + + + +*-------------------------------------------------------------------------------- +Procedure procstring(obj,eValue) +*-------------------------------------------------------------------------------- +#Define cvalid 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890_' +#Define creem '_______________________________________________________________' + +Private rowpos,colpos,bidim,ncols,arrayName,expecting,arrayLevel,vari +Private expectingPropertyName,expectingValue,objectOpen + +expectingPropertyName = !m.eValue +expectingValue = m.eValue +expecting = Iif(expectingPropertyName,'"}','') +objectOpen = .T. +bidim = .F. +colpos = 0 +rowpos = 0 +arrayLevel = 0 +arrayName = '' +vari = '' +ncols = 0 + +Do While m.objectOpen + + x = m.x+1 + + Do Case + + Case m.x > m.nl + + m.x = m.nl + + If oStack.Count > 0 + Error 'expecting '+m.expecting + Endif + + Return + + Case aj(m.x) = '}' And '}' $ m.expecting + closeObject() + + Case aj(x) = ']' And ']' $ m.expecting + closeArray() + + Case m.expecting = ':' + If aj(m.x) = ':' + expecting = '' + Loop + Else + Error 'expecting : received '+aj(m.x) + Endif + + Case ',' $ m.expecting + + Do Case + Case aj(x) = ',' + expecting = Iif( '[' $ m.expecting , '[' , '' ) + Case Not aj(m.x) $ m.expecting + Error 'expecting '+m.expecting+' received '+aj(m.x) + Otherwise + expecting = Strtran(m.expecting,',','') + Endcase + + + Case m.expectingPropertyName + + If aj(m.x) = '"' + propertyName(m.obj) + Else + Error 'expecting "'+m.expecting+' received '+aj(m.x) + Endif + + + Case m.expectingValue + + If m.expecting == '[' And m.aj(m.x) # '[' + Error 'expecting [ received '+aj(m.x) + Else + procValue(m.obj) + Endif + + + Endcase + + +Enddo + + +*---------------------------------------------------------- +Function anuevoel(obj,arrayName,valasig,bidim,colpos,rowpos) +*---------------------------------------------------------- + + +If m.bidim + + colpos = m.colpos+1 + + If colpos > m.ncols + ncols = m.colpos + Endif + + Dimension obj.&arrayName(m.rowpos,m.ncols) + + obj.&arrayName(m.rowpos,m.colpos) = m.valasig + + If Vartype(m.valasig) = 'O' + procstring(obj.&arrayName(m.rowpos,m.colpos)) + Endif + +Else + + rowpos = m.rowpos+1 + Dimension obj.&arrayName(m.rowpos) + + obj.&arrayName(m.rowpos) = m.valasig + + If Vartype(m.valasig) = 'O' + procstring(obj.&arrayName(m.rowpos)) + Endif + +Endif + + +*----------------------------------------- +Function unescunicode( Value ) +*----------------------------------------- + + +noc=1 + +Do While .T. + + posunicode = At('\u',m.value,m.noc) + + If m.posunicode = 0 + Return + Endif + + If Substr(m.value,m.posunicode-1,1) = '\' And Substr(m.value,m.posunicode-2,1) # '\' + noc=m.noc+1 + Loop + Endif + + nunic = Evaluate('0x'+ Substr(m.value,m.posunicode+2,4) ) + + If Between(m.nunic,0,255) + unicodec = Chr(m.nunic) + Else + unicodec = '&#'+Transform(m.nunic)+';' + Endif + + Value = Stuff(m.value,m.posunicode,6,m.unicodec) + + +Enddo + +*----------------------------------- +Function unescapecontrolc( Value ) +*----------------------------------- + +If At('\', m.value) = 0 + Return +Endif + +* unescape special characters: + +Private aa,elem,unesc + + +Declare aa(1) +=Alines(m.aa,m.value,18,'\\','\b','\f','\n','\r','\t','\"','\/') + +unesc ='' + +#Define sustb 'bnrt/"' +#Define sustr Chr(127)+Chr(10)+Chr(13)+Chr(9)+Chr(47)+Chr(34) + +For Each elem In m.aa + + If ! m.elem == '\\' And Right(m.elem,2) = '\' + elem = Left(m.elem,Len(m.elem)-2)+Chrtran(Right(m.elem,1),sustb,sustr) + Endif + + unesc = m.unesc+m.elem + +Endfor + +Value = m.unesc + +*-------------------------------------------- +Procedure propertyName(obj) +*-------------------------------------------- + +vari='' + +Do While ( Right(m.vari,1) # '"' Or ( Right(m.vari,2) = '\"' And Right(m.vari,3) # '\\"' ) ) And Alen(aj) > m.x + x=m.x+1 + vari = m.vari+aj(m.x) +Enddo + +If Right(m.vari,1) # '"' + Error ' expecting " received '+ Right(Rtrim(m.vari),1) +Endif + +vari = Left(m.vari,Len(m.vari)-1) +vari = Iif(Isalpha(m.vari),'','_')+m.vari +vari = Chrtran( vari, Chrtran( vari, cvalid,'' ) , creem ) + +If vari = 'tabindex' + vari = '_tabindex' +Endif + + +expecting = ':' +expectingValue = .T. +expectingPropertyName = .F. + + +*------------------------------------------------------------- +Procedure procValue(obj) +*------------------------------------------------------------- + +Do Case +Case aj(m.x) = '{' + + oStack.pushObject() + + If m.arrayLevel = 0 + + AddProperty(obj,m.vari,Createobject('empty')) + + procstring(obj.&vari) + expectingPropertyName = .T. + expecting = ',}' + expectingValue = .F. + + Else + + anuevoel(m.obj,m.arrayName,Createobject('empty'),m.bidim,@colpos,@rowpos) + expectingPropertyName = .F. + expecting = ',]' + expectingValue = .T. + + Endif + + +Case aj(x) = '[' + + oStack.pushArray() + + Do Case + + Case m.arrayLevel = 0 + + arrayName = Evl(m.vari,'array') + rowpos = 0 + colpos = 0 + bidim = .F. + +#DEFINE EMPTYARRAYFLAG '_EMPTY_ARRAY_FLAG_' + + Try + AddProperty(obj,(m.arrayName+'(1)'),EMPTYARRAYFLAG) + Catch + m.arrayName = m.arrayName+'_vfpSafe_' + AddProperty(obj,(m.arrayName+'(1)'),EMPTYARRAYFLAG) + Endtry + + + Case m.arrayLevel = 1 And !m.bidim + + rowpos = 1 + colpos = 0 + ncols = 1 + + Dime obj.&arrayName(1,2) + bidim = .T. + + Endcase + + arrayLevel = m.arrayLevel+1 + + vari='' + + expecting = Iif(!m.bidim,'[]{',']') + expectingValue = .T. + expectingPropertyName = .F. + +Otherwise + + isstring = aj(m.x)='"' + x = m.x + Iif(m.isstring,1,0) + + Value = '' + + Do While .T. + + Value = m.value+m.aj(m.x) + + If m.isstring + If Right(m.value,1) = '"' And ( Right(m.value,2) # '\"' Or Right(m.value,3) = '\\' ) + Exit + Endif + Else + If Right(m.value,1) $ '}],' And ( Left(Right(m.value,2),1) # '\' Or Left(Right(Value,3),2) = '\\') + Exit + Endif + Endif + + If m.x < Alen(aj) + x = m.x+1 + Else + Exit + Endif + + Enddo + + closeChar = Right(m.value,1) + + Value = Rtrim(m.value,1,m.closeChar) + + If Empty(Value) And Not ( m.isstring And m.closeChar = '"' ) + Error 'Expecting value received '+m.closeChar + Endif + + Do Case + + Case m.isstring + If m.closeChar # '"' + Error 'expecting " received '+m.closeChar + Endif + + Case oStack.isObject() And Not m.closeChar $ ',}' + Error 'expecting ,} received '+m.closeChar + + Case oStack.isArray() And Not m.closeChar $ ',]' + Error 'expecting ,] received '+m.closeChar + + Endcase + + + + If m.isstring + +* don't change this lines sequence!: + unescunicode(@Value) && 1 + unescapecontrolc(@Value) && 2 + Value = Strtran(m.value,'\\','\') && 3 + +** check for Json Date: + If isJsonDt( m.value ) + Value = jsonDateToDT( m.value ) + Endif + + Else + + Value = Alltrim(m.value) + + Do Case + Case m.value == 'null' + Value = .Null. + Case m.value == 'true' Or m.value == 'false' + Value = Value='true' + Case Empty(Chrtran(m.value,'-1234567890.E','')) And Occurs('.',m.value) <= 1 And Occurs('-',m.value) <= 1 And Occurs('E',m.value)<=1 + If Not 'E' $ m.value + Value = Cast( m.value As N( Len(m.value) , Iif(At('.',m.value)>0,Len(m.value)-At( '.',m.value) ,0) )) + Endif + Otherwise + Error 'expecting "|number|null|true|false| received '+aj(m.x) + Endcase + + + Endif + + + If m.arrayLevel = 0 + + + AddProperty(obj,m.vari,m.value) + + expecting = '}' + expectingValue = .F. + expectingPropertyName = .T. + + Else + + anuevoel(obj,m.arrayName,m.value,m.bidim,@colpos,@rowpos) + expecting = ']' + expectingValue = .T. + expectingPropertyName = .F. + + Endif + + expecting = Iif(m.isstring,',','')+m.expecting + + + Do Case + Case m.closeChar = ']' + closeArray() + Case m.closeChar = '}' + closeObject() + Endcase + +Endcase + + +*------------------------------ +Function closeArray() +*------------------------------ + +If oStack.Pop() # 'A' + Error 'unexpected ] ' +Endif + +If m.arrayLevel = 0 + Error 'unexpected ] ' +Endif + +arrayLevel = m.arrayLevel-1 + +If m.arrayLevel = 0 + + arrayName = '' + rowpos = 0 + colpos = 0 + + expecting = Iif(oStack.isObject(),',}','') + expectingPropertyName = .T. + expectingValue = .F. + +Else + + If m.bidim + rowpos = m.rowpos+1 + colpos = 0 + expecting = ',][' + Else + expecting = ',]' + Endif + + expectingValue = .T. + expectingPropertyName = .F. + +Endif + + + +*------------------------------------- +Procedure closeObject +*------------------------------------- + +If oStack.Pop() # 'O' + Error 'unexpected }' +Endif + +If m.arrayLevel = 0 + expecting = ',}' + expectingValue = .F. + expectingPropertyName = .T. + objectOpen = .F. +Else + expecting = ',]' + expectingValue = .T. + expectingPropertyName = .F. +Endif + + +*---------------------------------------------- +Function reviveCollection( o ) +*---------------------------------------------- + +Private All + +oConv = Createobject('empty') + +nProp = Amembers(elem,m.o,0,'U') + +For x = 1 To m.nProp + + estaVar = m.elem(x) + + esArray = .F. + esColeccion = Type('m.o.'+m.estaVar) = 'O' And Right( m.estaVar , 14 ) $ '_KV_COLLECTION,_KL_COLLECTION' And Type( 'm.o.'+m.estaVar+'.collectionitems',1) = 'A' + + Do Case + Case m.esColeccion + + estaProp = Createobject('collection') + + tv = m.o.&estaVar + + m.keyValColl = Right( m.estaVar , 14 ) = '_KV_COLLECTION' + + For T = 1 To Alen(m.tv.collectionItems) + + If m.keyValColl + esteval = m.tv.collectionItems(m.T).Value + Else + esteval = m.tv.collectionItems(m.T) + ENDIF + + IF VARTYPE(m.esteval) = 'C' AND m.esteval = emptyarrayflag + loop + ENDIF + + If Vartype(m.esteval) = 'O' Or Type('esteVal',1) = 'A' + esteval = reviveCollection(m.esteval) + Endif + + If m.keyValColl + estaProp.Add(esteval,m.tv.collectionItems(m.T).Key) + Else + estaProp.Add(m.esteval) + Endif + + Endfor + + Case Type('m.o.'+m.estaVar,1) = 'A' + + esArray = .T. + + For T = 1 To Alen(m.o.&estaVar) + + Dimension &estaVar(m.T) + + If Type('m.o.&estaVar(m.T)') = 'O' + &estaVar(m.T) = reviveCollection(m.o.&estaVar(m.T)) + Else + &estaVar(m.T) = m.o.&estaVar(m.T) + Endif + + Endfor + + Case Type('m.o.'+estaVar) = 'O' + estaProp = reviveCollection(m.o.&estaVar) + + Otherwise + estaProp = m.o.&estaVar + + Endcase + + + estaVar = Strtran( m.estaVar,'_KV_COLLECTION', '' ) + estaVar = Strtran( m.estaVar, '_KL_COLLECTION', '' ) + + Do Case + Case m.esColeccion + AddProperty(m.oConv,m.estaVar,m.estaProp) + Case m.esArray + AddProperty(m.oConv,m.estaVar+'(1)') + Acopy(&estaVar,m.oConv.&estaVar) + Otherwise + AddProperty(m.oConv,m.estaVar,m.estaProp) + Endcase + +Endfor + +Try + retCollection = m.oConv.Collection.BaseClass = 'Collection' +Catch + retCollection = .F. +Endtry + +If m.retCollection + Return m.oConv.Collection +Else + Return m.oConv +Endif + + +*---------------------------------- +Function isJsonDt( cstr ) +*---------------------------------- +Return Iif( Len(m.cstr) = 19 ; + AND Len(Chrtran(m.cstr,'01234567890:T-','')) = 0 ; + and Substr(m.cstr,5,1) = '-' ; + and Substr(m.cstr,8,1) = '-' ; + and Substr(m.cstr,11,1) = 'T' ; + and Substr(m.cstr,14,1) = ':' ; + and Substr(m.cstr,17,1) = ':' ; + and Occurs('T',m.cstr) = 1 ; + and Occurs('-',m.cstr) = 2 ; + and Occurs(':',m.cstr) = 2 ,.T.,.F. ) + + +*----------------------------------- +Procedure jsonDateToDT( cJsonDate ) +*----------------------------------- +Return Eval("{^"+m.cJsonDate+"}") + + + +****************************************** +Define Class Stack As Collection +****************************************** + +*--------------------------- + Function pushObject() +*--------------------------- + This.Add('O') + +*--------------------------- + Function pushArray() +*--------------------------- + This.Add('A') + +*-------------------------------------- + Function isObject() +*-------------------------------------- + If This.Count > 0 + Return This.Item( This.Count ) = 'O' + Else + Return .F. + Endif + + +*-------------------------------------- + Function isArray() +*-------------------------------------- + If This.Count > 0 + Return This.Item( This.Count ) = 'A' + Else + Return .F. + Endif + +*---------------------------- + Function Pop() +*---------------------------- + cret = This.Item( This.Count ) + This.Remove( This.Count ) + Return m.cret + +****************************************** +Enddefine +****************************************** + + diff --git a/prestatii_rar.CDX b/prestatii_rar.CDX new file mode 100644 index 0000000..ba16655 Binary files /dev/null and b/prestatii_rar.CDX differ diff --git a/prestatii_rar.DBF b/prestatii_rar.DBF new file mode 100644 index 0000000..c5f05bb Binary files /dev/null and b/prestatii_rar.DBF differ diff --git a/rar-forms.prg b/rar-forms.prg new file mode 100644 index 0000000..e42b970 --- /dev/null +++ b/rar-forms.prg @@ -0,0 +1,1296 @@ +* rar_forms.prg +#Define C_FORM_WIDTH 800 +#Define C_FORM_HEIGHT 600 +#Define C_BTN_WIDTH 120 +#Define C_BTN_HEIGHT 30 +#Define C_API_TIMEOUT 30 + +Close Databases All +Set Century On +Set Exact On +Set Ansi On +Set Deleted On +Set Date Dmy +Set Hours To 24 +Set Notify Off +Set Safety Off + +lcPath = Justpath(Sys(16,0)) +Set Default To (m.lcPath) + +Set Procedure To rar_autopass Additive +Set Procedure To nfjsonread Additive + +Do Main + +* Form principal pentru managementul RAR AutoPass +Define Class RarAutoPassForm As Form + Caption = "RAR AutoPass Manager" + Width = C_FORM_WIDTH + Height = C_FORM_HEIGHT + AutoCenter = .T. + MaxButton = .F. + WindowType = 1 + BackColor = Rgb(240,240,240) + + * Membri privati + Hidden oRar + Hidden nTimerHandle + Hidden cLastXmlPath + + ErrorMsg = "" + + * Adaugam PageFrame-ul ?i paginile sale + Add Object PageFrame1 As PageFrame With ; + Left = 10, ; + Top = 10, ; + Width = C_FORM_WIDTH - 20, ; + Height = C_FORM_HEIGHT - 80, ; + PageCount = 4, ; + Visible = .T. + + Function Init + Local loRar, llSuccess, llTestMode + llSuccess = .F. + llTestMode = .T. + + Try + * Adaugam controalele pentru fiecare pagina + This.SetupPage1() && Procesare + This.SetupPage2() && Mapari + This.SetupPage3() && Monitorizare + This.SetupPage4() && Prezentari + + * Initializare RAR AutoPass + This.oRar = Createobject("RarAutoPass") + If !This.oRar.SetCredentials(m.llTestMode) + Messagebox("Eroare setare credentiale: " + This.oRar.ErrorMsg, 16, "Eroare") + llSuccess = .F. + Return llSuccess + Endif + + * Deschidem tabelele necesare + Use mapare_prestatii In 0 Shared + Use rar_log In 0 Shared + Use prestatii_rar In 0 Shared + + Create Cursor xml_preview ; + (nr_comanda C(20), ; + data D, ; + vin C(20), ; + nr_inm C(10), ; + km_final N(10), ; + km_init N(10), ; + observatii M) + + This.SetupGridMapari() + This.SetupGridLog() + This.SetupLogFilter() + + This.nTimerHandle = 0 + This.cLastXmlPath = "" + + llSuccess = .T. + + Catch To loError + Messagebox("Eroare la initializarea formularului: " + ; + loError.Message, 16, "Eroare") + llSuccess = .F. + Endtry + + Return llSuccess + Endfunc + + * Setup pentru Page1 (Procesare Comenzi) + Function SetupPage1 + Local llSuccess + llSuccess = .F. + + Try + With This.PageFrame1.Page1 + .Caption = "Procesare Comenzi" + + .AddObject("lblXmlPath", "Label") + With .lblXmlPath + .Caption = "Fisier XML:" + .Left = 20 + .Top = 20 + .Visible = .T. + Endwith + + .AddObject("txtXmlPath", "TextBox") + With .txtXmlPath + .Left = 100 + .Top = 20 + .Width = 500 + .Visible = .T. + Endwith + + .AddObject("btnBrowse", "CommandButton") + With .btnBrowse + .Caption = "..." + .Left = 610 + .Top = 20 + .Width = 30 + .Height = C_BTN_HEIGHT + .Visible = .T. + Endwith + + * Ata?am handler-ul pentru evenimentul Click + Bindevent(.btnBrowse, "Click", This, "OnBrowseClick") + + .AddObject("btnProcess", "CommandButton") + With .btnProcess + .Caption = "Proceseaza" + .Left = 20 + .Top = 60 + .Width = C_BTN_WIDTH + .Height = C_BTN_HEIGHT + .Visible = .T. + Endwith + + * Ata?am handler-ul pentru evenimentul Click + Bindevent(.btnProcess, "Click", This, "OnProcessClick") + + .AddObject("chkAutoProcess", "CheckBox") + With .chkAutoProcess + .Caption = "Procesare automata" + .Left = 160 + .Top = 65 + .AutoSize = .T. + .Visible = .T. + Endwith + + * Ata?am handler pentru AutoProcess + Bindevent(.chkAutoProcess, "InteractiveChange", This, "OnAutoProcessChange") + + .AddObject("txtInterval", "TextBox") + With .txtInterval + .Left = 300 + .Top = 65 + .Width = 50 + .Value = "5" + .Visible = .T. + Endwith + + .AddObject("lblMinute", "Label") + With .lblMinute + .Caption = "minute" + .Left = 360 + .Top = 65 + .Visible = .T. + Endwith + + .AddObject("edtStatus", "EditBox") + With .edtStatus + .Left = 20 + .Top = 100 + .Width = 700 + .Height = 200 + .ReadOnly = .T. + .ScrollBars = 2 + .Visible = .T. + Endwith + + .AddObject("grdPreview", "Grid") + With .grdPreview + .Left = 20 + .Top = 320 + .Width = 700 + .Height = 200 + .ReadOnly = .T. + .Visible = .F. + + .ColumnCount = 7 + + .Column1.Header1.Caption = "Nr. Comanda" + .Column1.Width = 100 + + .Column2.Header1.Caption = "Data" + .Column2.Width = 80 + + .Column3.Header1.Caption = "VIN" + .Column3.Width = 120 + + .Column4.Header1.Caption = "Nr. Inmatriculare" + .Column4.Width = 100 + + .Column5.Header1.Caption = "KM Final" + .Column5.Width = 80 + + .Column6.Header1.Caption = "KM Initial" + .Column6.Width = 80 + + .Column7.Header1.Caption = "Observatii" + .Column7.Width = 140 + Endwith + + .AddObject("lblPreview", "Label") + With .lblPreview + .Caption = "Preview comenzi din XML:" + .Left = 20 + .Top = 300 + .Visible = .F. + Endwith + Endwith + + llSuccess = .T. + + Catch To loError + Messagebox("Eroare la setup Page1: " + loError.Message, 16, "Eroare") + llSuccess = .F. + Endtry + + Return llSuccess + Endfunc + + * Continuare din partea anterioara... + + * Setup pentru Page2 (Mapare Operatii) + Function SetupPage2 + Local llSuccess + llSuccess = .F. + + Try + With This.PageFrame1.Page2 + .Caption = "Mapare Operatii" + + .AddObject("Grid1", "Grid") + With .Grid1 + .Left = 20 + .Top = 20 + .Width = 700 + .Height = 300 + .AllowAddNew = .T. + .DeleteMark = .F. + .Visible = .T. + Endwith + + .AddObject("btnSaveMapari", "CommandButton") + With .btnSaveMapari + .Caption = "Salveaza" + .Left = 20 + .Top = 330 + .Width = C_BTN_WIDTH + .Height = C_BTN_HEIGHT + .Visible = .T. + Endwith + + .AddObject("btnRefreshNomenclator", "CommandButton") + With .btnRefreshNomenclator + .Caption = "Actualizare Nomenclator" + .Left = 150 + .Top = 330 + .Width = 150 + .Height = C_BTN_HEIGHT + .Visible = .T. + Endwith + + Bindevent(.btnSaveMapari, "Click", This, "OnSaveMappingsClick") + Bindevent(.btnRefreshNomenclator, "Click", This, "OnRefreshNomenclatorClick") + Endwith + + llSuccess = .T. + + Catch To loError + Messagebox("Eroare la setup Page2: " + loError.Message, 16, "Eroare") + llSuccess = .F. + Endtry + + Return llSuccess + Endfunc + + * Setup pentru Page3 (Monitorizare) + Function SetupPage3 + Local llSuccess + llSuccess = .F. + + Try + With This.PageFrame1.Page3 + .Caption = "Monitorizare" + + .AddObject("Grid2", "Grid") + With .Grid2 + .Left = 20 + .Top = 50 + .Width = 700 + .Height = 300 + .Visible = .T. + Endwith + + .AddObject("cboLogFilter", "ComboBox") + With .cboLogFilter + .Left = 20 + .Top = 20 + .Width = 150 + .Style = 2 + .Visible = .T. + + .AddItem("Toate") + .AddItem("Succes") + .AddItem("Erori") + .ListIndex = 1 + Endwith + + .AddObject("btnRefreshLog", "CommandButton") + With .btnRefreshLog + .Caption = "Refresh" + .Left = 180 + .Top = 20 + .Width = C_BTN_WIDTH + .Height = C_BTN_HEIGHT + .Visible = .T. + Endwith + + .AddObject("btnAdvSearch", "CommandButton") + With .btnAdvSearch + .Caption = "Cautare avansata" + .Left = 320 + .Top = 20 + .Width = C_BTN_WIDTH + .Height = C_BTN_HEIGHT + .Visible = .T. + Endwith + + .AddObject("btnExportExcel", "CommandButton") + With .btnExportExcel + .Caption = "Export Excel" + .Left = 450 + .Top = 20 + .Width = C_BTN_WIDTH + .Height = C_BTN_HEIGHT + .Visible = .T. + Endwith + + * Ata?am event handlers + Bindevent(.cboLogFilter, "InteractiveChange", This, "OnLogFilterChange") + Bindevent(.btnRefreshLog, "Click", This, "OnRefreshLogClick") + Bindevent(.btnAdvSearch, "Click", This, "OnAdvSearchClick") + Bindevent(.btnExportExcel, "Click", This, "OnExportExcelClick") + Endwith + + llSuccess = .T. + + Catch To loError + Messagebox("Eroare la setup Page3: " + loError.Message, 16, "Eroare") + llSuccess = .F. + Endtry + + Return llSuccess + Endfunc + + * Setup pentru Page4 (Prezentari Finalizate) + Function SetupPage4 + Local llSuccess + llSuccess = .F. + + Try + With This.PageFrame1.Page4 + .Caption = "Prezentari Finalizate" + + * Grid pentru prezentari + .AddObject("grdPrezentari", "Grid") + With .grdPrezentari + .Left = 20 + .Top = 50 + .Width = 700 + .Height = 300 + .ReadOnly = .T. + .DeleteMark = .F. + .Visible = .T. + + .ColumnCount = 7 + .Column1.Header1.Caption = "ID" + .Column1.Width = 60 + .Column2.Header1.Caption = "Data Presta?ie" + .Column2.Width = 100 + .Column3.Header1.Caption = "VIN" + .Column3.Width = 120 + .Column4.Header1.Caption = "Nr. nmatriculare" + .Column4.Width = 100 + .Column5.Header1.Caption = "Km Final" + .Column5.Width = 80 + .Column6.Header1.Caption = "Status" + .Column6.Width = 80 + .Column7.Header1.Caption = "Observa?ii" + .Column7.Width = 160 + Endwith + + * Buton refresh prezentari + .AddObject("btnRefreshPrez", "CommandButton") + With .btnRefreshPrez + .Caption = "Actualizeaza" + .Left = 20 + .Top = 20 + .Width = C_BTN_WIDTH + .Height = C_BTN_HEIGHT + .Visible = .T. + Endwith + + * Buton anulare prezentare + .AddObject("btnAnulare", "CommandButton") + With .btnAnulare + .Caption = "Anuleaza Prezentare" + .Left = 150 + .Top = 20 + .Width = C_BTN_WIDTH + .Height = C_BTN_HEIGHT + .Visible = .T. + Endwith + + * Ata?am event handlers + Bindevent(.btnRefreshPrez, "Click", This, "OnRefreshPrezentariClick") + Bindevent(.btnAnulare, "Click", This, "OnAnularePrezentareClick") + Endwith + + * Cream cursor pentru prezentari + If Used("prezentari_fin") + Use In prezentari_fin + Endif + + Create Cursor prezentari_fin ; + (Id I, ; + data_prestatie D, ; + vin C(20), ; + nr_inmatriculare C(10), ; + odometru_final N(10), ; + status C(20), ; + observatii M) + + * Setam sursa pentru grid + This.PageFrame1.Page4.grdPrezentari.RecordSource = "prezentari_fin" + With This.PageFrame1.Page4.grdPrezentari + .Column1.ControlSource = "prezentari_fin.id" + .Column2.ControlSource = "prezentari_fin.data_prestatie" + .Column3.ControlSource = "prezentari_fin.vin" + .Column4.ControlSource = "prezentari_fin.nr_inmatriculare" + .Column5.ControlSource = "prezentari_fin.odometru_final" + .Column6.ControlSource = "prezentari_fin.status" + .Column7.ControlSource = "prezentari_fin.observatii" + Endwith + + llSuccess = .T. + + Catch To loError + Messagebox("Eroare la setup Page4: " + loError.Message, 16, "Eroare") + llSuccess = .F. + Endtry + + Return llSuccess + Endfunc + + * Event Handlers pentru Page1 + Function OnBrowseClick + Local lcFile + lcFile = Getfile("XML", "Alege fisier XML", "Deschide", 0) + If !Empty(lcFile) + This.PageFrame1.Page1.txtXmlPath.Value = lcFile + This.AddStatus("Fisier selectat: " + lcFile) + This.ParseXMLToPreview(lcFile) + Endif + Endfunc + + Function OnProcessClick + Local lcFile + lcFile = This.PageFrame1.Page1.txtXmlPath.Value + If Empty(lcFile) + Messagebox("Selecta?i un fisier XML!", 16, "Aten?ie") + Return + Endif + + This.ProcessXML(lcFile) + Endfunc + + Function OnAutoProcessChange + Local loControl + loControl = This.PageFrame1.Page1.chkAutoProcess + + If loControl.Value + This.StartAutoProcessing() + Else + This.StopAutoProcessing() + Endif + Endfunc + + * Event Handlers pentru Page3 (Monitorizare) + Function OnLogFilterChange + Lparameters toControl + Local lcFilter + + Do Case + Case toControl.ListIndex = 1 && Toate + lcFilter = "" + Case toControl.ListIndex = 2 && Succes + lcFilter = "status = 'SUCCESS'" + Case toControl.ListIndex = 3 && Erori + lcFilter = "status = 'ERROR'" + Endcase + + This.ApplyLogFilter(lcFilter) + Endfunc + + Function OnRefreshLogClick + Select rar_log + Use In rar_log + Use rar_log In 0 Shared + Go Top + This.PageFrame1.Page3.Grid2.Refresh() + This.AddStatus("Log actualizat.") + Endfunc + + Function OnAdvSearchClick + Do Form search_log + This.PageFrame1.Page3.Grid2.Refresh() + Endfunc + + * Event Handlers pentru Page4 (Prezentari) + Function OnRefreshPrezentariClick + This.RefreshPrezentari() + Endfunc + + Function OnAnularePrezentareClick + This.AnularePrezentare(prezentari_fin.id) + Endfunc + + * Func?ii utilitare + Function AddStatus + Lparameters tcMessage + With This.PageFrame1.Page1.edtStatus + .Value = .Value + Ttoc(Datetime()) + ": " + tcMessage + Chr(13) + Chr(10) + .SelStart = Len(.Value) + Endwith + Endfunc + + Function ApplyLogFilter + Parameter tcFilter + Select rar_log + If !Empty(tcFilter) + Set Filter To &tcFilter + Else + Set Filter To + Endif + Go Top + This.PageFrame1.Page3.Grid2.Refresh() + Endfunc + + * Func?ii pentru procesare XML + Function ParseXMLToPreview + Parameters tcXMLFile + Local loXML As MSXML2.DOMDocument.6.0 + Local loNodes As MSXML2.IXMLDOMNodeList + Local loNode As MSXML2.IXMLDOMNode + Local llSuccess, lcData + llSuccess = .F. + + Try + loXML = Createobject("MSXML2.DOMDocument.6.0") + loXML.Async = .F. + + If !loXML.Load(tcXMLFile) + This.ErrorMsg = "Eroare incarcare XML: " + loXML.parseError.reason + This.AddStatus(This.ErrorMsg) + llSuccess = .F. + Return llSuccess + Endif + + * Obtinem toate nodurile de comenzi + loNodes = loXML.selectNodes("/comenzi/comanda") + + If Used("xml_preview") + Select xml_preview + Delete All + Endif + + For Each loNode In loNodes + lcData = loNode.selectSingleNode("data").Text + + ldData = This.Data2Date(m.lcData) + Insert Into xml_preview ; + (nr_comanda, ; + data, ; + vin, ; + nr_inm, ; + km_final, ; + km_init, ; + observatii) ; + VALUES ; + (loNode.selectSingleNode("nr_comanda").Text, ; + M.ldData, ; + loNode.selectSingleNode("vin").Text, ; + loNode.selectSingleNode("nr_inmatriculare").Text, ; + VAL(loNode.selectSingleNode("km_final").Text), ; + VAL(loNode.selectSingleNode("km_initial").Text), ; + NVL(loNode.selectSingleNode("observatii").Text, "")) + Endfor + + Go Top + This.PageFrame1.Page1.grdPreview.Refresh() + This.PageFrame1.Page1.grdPreview.Visible = .T. + This.PageFrame1.Page1.lblPreview.Visible = .T. + + This.AddStatus("XML citit cu succes: " + Transform(Reccount()) + " comenzi") + llSuccess = .T. + + Catch To loError + This.ErrorMsg = "Eroare parsare XML: " + loError.Message + This.AddStatus(This.ErrorMsg) + llSuccess = .F. + Endtry + + Return llSuccess + Endfunc + + * Func?ii pentru prezentari + Function RefreshPrezentari + Local llSuccess + llSuccess = .F. + + This.AddStatus("Actualizare prezentari...") + Try + If !This.oRar.Login() + This.AddStatus("Eroare login: " + This.oRar.ErrorMsg) + Return .F. + Endif + + This.oRar.loXMLHTTP.Open("GET", This.oRar.lcUrl + "/prezentari/getAllPrezentariFinalizate", .F.) + This.oRar.loXMLHTTP.setRequestHeader("Authorization", "Bearer " + This.oRar.lcToken) + This.oRar.loXMLHTTP.Send() + + If This.oRar.loXMLHTTP.Status != 200 + This.ErrorMsg = "Eroare obtinere prezentari: " + This.oRar.loXMLHTTP.responseText + This.AddStatus(This.ErrorMsg) + Return .F. + Endif + + loJson = nfjsonread(This.oRar.loXMLHTTP.responseText) + + If Type('loJson.data.content') == 'O' + Select prezentari_fin + Delete All + + For Each loItem In loJson.Data.content + ldData = This.Data2Date(loItem.dataPrestatie) + + Insert Into prezentari_fin ; + (Id, ; + data_prestatie, ; + vin, ; + nr_inmatriculare, ; + odometru_final, ; + status, ; + observatii) ; + VALUES ; + (loItem.Id, ; + M.ldData, ; + loItem.vin, ; + loItem.nrInmatriculare, ; + loItem.odometruFinal, ; + loItem.Status, ; + loItem.obs) + Endfor + + Go Top + This.PageFrame1.Page4.grdPrezentari.Refresh() + This.AddStatus("Prezentari actualizate cu succes.") + llSuccess = .T. + Endif + + Catch To loError + This.ErrorMsg = "Eroare actualizare prezentari: " + loError.Message + This.AddStatus(This.ErrorMsg) + llSuccess = .F. + Endtry + + Return llSuccess + Endfunc + + * Anulare prezentare + Function AnularePrezentare + Parameters tnIdPrezentare + Local llSuccess + llSuccess = .F. + + If !This.oRar.Login() + Return .F. + Endif + + Try + * Facem request de tip PATCH pentru anulare + This.oRar.loXMLHTTP.Open("PATCH", ; + THIS.oRar.lcUrl + "/prezentari/markPrezentareAnulataById/" + ALLTRIM(Transform(tnIdPrezentare)), ; + .F.) + This.oRar.loXMLHTTP.setRequestHeader("Authorization", "Bearer " + This.oRar.lcToken) + This.oRar.loXMLHTTP.Send() + + This.oRar.LastResponse = This.oRar.loXMLHTTP.responseText + + * Parsam raspunsul JSON pentru a verifica statusul + loResponse = nfjsonread(This.oRar.LastResponse) + + Do Case + Case This.oRar.loXMLHTTP.Status != 200 + This.ErrorMsg = "Eroare la anularea prezentarii: " + This.oRar.LastResponse + This.AddStatus(This.ErrorMsg) + llSuccess = .F. + + + Case Type('loResponse.STATUS_ANULATA') = 'C' + * Anulare reusita + This.AddStatus('Anulare reusita ID: ' + ALLTRIM(TRANSFORM(tnIdPrezentare))) + + llSuccess = .T. + This.oRar.LogPrezentare(Ttoc(Date()), ; + "", ; && nr_comanda + "", ; && vin + "", ; && nr_inm + 0, ; && km_final + 0, ; && km_initial + "", ; && prestatii + "ANULATA", ; && status + loResponse.STATUS_ANULATA) && mesaj succes + This.AddStatus('Prezentare anulata') + + Case Type('loResponse.EROARE_STATUS_ANULARE') = 'C' + This.ErrorMsg = loResponse.EROARE_STATUS_ANULARE + This.oRar.LogPrezentare(Ttoc(Date()), ; + "", ; + "", ; + "", ; + 0, ; + 0, ; + "", ; + "ERROR", ; + THIS.ErrorMsg) + + This.AddStatus(This.ErrorMsg) + llSuccess = .F. + + Otherwise + This.ErrorMsg = "Raspuns neasteptat: " + This.oRar.LastResponse + This.AddStatus(This.ErrorMsg) + llSuccess = .F. + + Endcase + + Catch To loEx + This.ErrorMsg = "Eroare anulare prezentare: " + loEx.Message + This.AddStatus(This.ErrorMsg) + llSuccess = .F. + Endtry + + * Actualizam statusul in log daca exista inregistrarea + If Used("rar_log") + Select rar_log + Locate For Id = tnIdPrezentare + If Found() + Replace Status With Iif(llSuccess, "ANULATA", "ERROR"), ; + error_msg With Iif(!llSuccess, This.ErrorMsg, ""), ; + data_trim With Datetime() + Endif + Endif + + Return llSuccess + Endfunc + + * Func?ii pentru procesare automata + Function StartAutoProcessing + Local lnInterval + + If !Empty(This.nTimerHandle) + Return + Endif + + lnInterval = Val(This.PageFrame1.Page1.txtInterval.Value) + If lnInterval <= 0 + Messagebox("Interval invalid!", 16, "Eroare") + This.PageFrame1.Page1.chkAutoProcess.Value = .F. + Return + Endif + + * Convertim intervalul din minute n milisecunde + lnInterval = lnInterval * 60 * 1000 + + This.nTimerHandle = _Screen.AddProperty("Timer" + Sys(2015), ; + NEWOBJECT("Timer")) + + With _Screen.&("Timer" + Sys(2015)) + .Interval = lnInterval + Bindevent(.Timer, This, "OnAutoProcessTimer") + .Enabled = .T. + Endwith + + This.AddStatus("Procesare automata pornita. Interval: " + ; + TRANSFORM(Val(This.PageFrame1.Page1.txtInterval.Value)) + " minute") + Endfunc + + Function StopAutoProcessing + If !Empty(This.nTimerHandle) + _Screen.RemoveObject("Timer" + Sys(2015)) + This.nTimerHandle = 0 + This.AddStatus("Procesare automata oprita.") + Endif + Endfunc + + Function OnAutoProcessTimer + Local lcFile + lcFile = This.PageFrame1.Page1.txtXmlPath.Value + + If !Empty(lcFile) And File(lcFile) + This.ProcessXML(lcFile) + Else + This.AddStatus("EROARE: Fi?ier XML invalid sau lipsa!") + This.StopAutoProcessing() + This.PageFrame1.Page1.chkAutoProcess.Value = .F. + Endif + Endfunc + + + * Event Handlers pentru Page2 (Mapari) + Function OnSaveMappingsClick + Local llSuccess + llSuccess = .F. + + Try + Select mapare_prestatii + Tableupdate(.T.) + Messagebox("Mapari salvate cu succes!", 64, "Succes") + llSuccess = .T. + This.AddStatus("Mapari actualizate cu succes.") + + Catch To loError + Messagebox("Eroare salvare mapari: " + loError.Message, 16, "Eroare") + This.AddStatus("EROARE salvare mapari: " + loError.Message) + llSuccess = .F. + Endtry + + Return llSuccess + Endfunc + + Function OnRefreshNomenclatorClick + Local llSuccess + llSuccess = .F. + + Wait Window "Actualizare nomenclator n curs..." Nowait + + Try + If !This.oRar.UpdateNomenclator(.T.) + Messagebox("Eroare actualizare nomenclator: " + This.oRar.ErrorMsg, 16, "Eroare") + This.AddStatus("EROARE actualizare nomenclator: " + This.oRar.ErrorMsg) + llSuccess = .F. + Else + Messagebox("Nomenclator actualizat cu succes!", 64, "Succes") + This.AddStatus("Nomenclator actualizat cu succes.") + llSuccess = .T. + + * Remprospatam grid-ul cu mapari + Select mapare_prestatii + Go Top + This.PageFrame1.Page2.Grid1.Refresh() + Endif + + Catch To loError + Messagebox("Eroare actualizare nomenclator: " + loError.Message, 16, "Eroare") + This.AddStatus("EROARE actualizare nomenclator: " + loError.Message) + llSuccess = .F. + Finally + Wait Clear + Endtry + + Return llSuccess + Endfunc + + * Functii pentru procesare XML + Function ProcessXML + Parameters tcXMLFile + Local llSuccess + llSuccess = .F. + + This.AddStatus("Procesare " + tcXMLFile + "...") + + Try + If !This.oRar.ProcessXMLComenzi(tcXMLFile) + This.AddStatus("EROARE: " + This.oRar.ErrorMsg) + Messagebox(This.oRar.ErrorMsg, 16, "Eroare") + llSuccess = .F. + Else + This.AddStatus("Procesare finalizata cu succes!") + Messagebox("Procesare finalizata cu succes!", 64, "Succes") + llSuccess = .T. + Endif + + Catch To loError + This.AddStatus("EROARE: " + loError.Message) + Messagebox(loError.Message, 16, "Eroare") + llSuccess = .F. + Endtry + + Return llSuccess + Endfunc + + * Handler pentru activare/dezactivare procesare automata + Function OnAutoProcessChange + Local loControl + loControl = This.PageFrame1.Page1.chkAutoProcess + + If loControl.Value + This.StartAutoProcessing() + Else + This.StopAutoProcessing() + Endif + Endfunc + + * Handler pentru timer procesare automata + Function OnAutoProcessTimer + Local lcFile + lcFile = This.PageFrame1.Page1.txtXmlPath.Value + + If !Empty(lcFile) And File(lcFile) + This.ProcessXML(lcFile) + Else + This.AddStatus("EROARE: Fi?ier XML invalid sau lipsa!") + This.StopAutoProcessing() + This.PageFrame1.Page1.chkAutoProcess.Value = .F. + Endif + Endfunc + + + * Export Excel pentru log + Function OnExportExcelClick + Local lcFile, loExcel, loWorkbook, loWorksheet + Local lnRow, lnCol + + lcFile = Putfile("Excel files|*.xlsx", "RAR_Log_Export.xlsx", "", 0) + If Empty(lcFile) + Return + Endif + + This.AddStatus("Export Excel n " + lcFile + "...") + + Try + loExcel = Createobject("Excel.Application") + loExcel.Visible = .F. + + loWorkbook = loExcel.Workbooks.Add() + loWorksheet = loWorkbook.Sheets(1) + + * Setam headerele + With loWorksheet + .Cells(1,1).Value = "Data prezentare" + .Cells(1,2).Value = "Nr. comanda" + .Cells(1,3).Value = "VIN" + .Cells(1,4).Value = "Nr. nmatriculare" + .Cells(1,5).Value = "Km final" + .Cells(1,6).Value = "Km initial" + .Cells(1,7).Value = "Status" + .Cells(1,8).Value = "Eroare" + .Cells(1,9).Value = "Data trimitere" + + * Formatare header + .Range(.Cells(1,1), .Cells(1,9)).Font.Bold = .T. + .Range(.Cells(1,1), .Cells(1,9)).Interior.Color = Rgb(200,200,200) + Endwith + + * Populam datele + Select rar_log + lnRow = 2 + Scan + loWorksheet.Cells(lnRow, 1).Value = Dtoc(data_prez) + loWorksheet.Cells(lnRow, 2).Value = nr_comanda + loWorksheet.Cells(lnRow, 3).Value = vin + loWorksheet.Cells(lnRow, 4).Value = nr_inm + loWorksheet.Cells(lnRow, 5).Value = Transform(km_final) + loWorksheet.Cells(lnRow, 6).Value = Transform(km_init) + loWorksheet.Cells(lnRow, 7).Value = Status + loWorksheet.Cells(lnRow, 8).Value = error_msg + loWorksheet.Cells(lnRow, 9).Value = Ttoc(data_trim) + + * Coloram randurile cu erori in rosu deschis + If Status = "ERROR" + loWorksheet.Range(loWorksheet.Cells(lnRow,1), ; + loWorksheet.Cells(lnRow,9)).Interior.Color = Rgb(255,200,200) + Endif + + lnRow = lnRow + 1 + Endscan + + * Auto-fit coloane + loWorksheet.Range("A:I").Columns.AutoFit() + + * Salvam si inchidem + loWorkbook.SaveAs(lcFile) + loWorkbook.Close() + loExcel.Quit() + + This.AddStatus("Export Excel finalizat cu succes") + + * Deschidem fisierul + Run /N "explorer.exe" &lcFile + + Catch To loError + This.AddStatus("EROARE la export Excel: " + loError.Message) + Messagebox("Eroare la export Excel: " + loError.Message, 16, "Eroare") + + Try + loWorkbook.Close() + loExcel.Quit() + Catch + Endtry + Endtry + Endfunc + + * Refresh pentru grid-ul de log + Function OnRefreshLogClick + Select rar_log + Use In rar_log + Use rar_log In 0 Shared + Go Top + This.PageFrame1.Page3.Grid2.Refresh() + This.AddStatus("Log actualizat.") + Endfunc + + * Cautare avansata n log + Function OnAdvSearchClick + Local loSearch + loSearch = Createobject("SearchLogForm") + loSearch.Show(1) + This.PageFrame1.Page3.Grid2.Refresh() + Endfunc + + * Handler pentru filtrul de log + Function OnLogFilterChange + Lparameters toControl + Local lcFilter + + Do Case + Case toControl.ListIndex = 1 && Toate + lcFilter = "" + Case toControl.ListIndex = 2 && Succes + lcFilter = "status = 'SUCCESS'" + Case toControl.ListIndex = 3 && Erori + lcFilter = "status = 'ERROR'" + Endcase + + This.ApplyLogFilter(lcFilter) + Endfunc + + * Aplicare filtru pentru log + Function ApplyLogFilter + Parameter tcFilter + Select rar_log + If !Empty(tcFilter) + Set Filter To &tcFilter + Else + Set Filter To + Endif + Go Top + This.PageFrame1.Page3.Grid2.Refresh() + Endfunc + + * Helper pentru procesare automata + Function StartAutoProcessing + Local lnInterval + + If !Empty(This.nTimerHandle) + Return + Endif + + lnInterval = Val(This.PageFrame1.Page1.txtInterval.Value) + If lnInterval <= 0 + Messagebox("Interval invalid!", 16, "Eroare") + This.PageFrame1.Page1.chkAutoProcess.Value = .F. + Return + Endif + + * Convertim intervalul din minute n milisecunde + lnInterval = lnInterval * 60 * 1000 + + This.nTimerHandle = _Screen.AddProperty("Timer" + Sys(2015), ; + NEWOBJECT("Timer")) + + With _Screen.&("Timer" + Sys(2015)) + .Interval = lnInterval + Bindevent(.Timer, This, "OnAutoProcessTimer") + .Enabled = .T. + Endwith + + This.AddStatus("Procesare automata pornita. Interval: " + ; + TRANSFORM(Val(This.PageFrame1.Page1.txtInterval.Value)) + " minute") + Endfunc + + Function StopAutoProcessing + If !Empty(This.nTimerHandle) + _Screen.RemoveObject("Timer" + Sys(2015)) + This.nTimerHandle = 0 + This.AddStatus("Procesare automata oprita.") + Endif + Endfunc + + + Function SetupLogFilter + Local llSuccess + llSuccess = .F. + + Try + With This.PageFrame1.Page3.cboLogFilter + .Clear() + .AddItem("Toate") + .AddItem("Succes") + .AddItem("Erori") + .ListIndex = 1 + Endwith + + * Aplicam filtrul ini?ial (fara filtru) + This.ApplyLogFilter("") + + llSuccess = .T. + + Catch To loError + Messagebox("Eroare la setup filtru log: " + loError.Message, 16, "Eroare") + llSuccess = .F. + Endtry + + Return llSuccess + Endfunc + + * Gestionare grid-uri + Function SetupGridMapari + Local llSuccess + llSuccess = .F. + + Try + With This.PageFrame1.Page2.Grid1 + .RecordSource = "mapare_prestatii" + .ColumnCount = 4 + + .Column1.Header1.Caption = "Cod Operatie" + .Column1.ControlSource = "mapare_prestatii.cod_op" + .Column1.Width = 150 + + .Column2.Header1.Caption = "Descriere" + .Column2.ControlSource = "mapare_prestatii.descr_op" + .Column2.Width = 250 + + .Column3.Header1.Caption = "Cod RAR" + .Column3.ControlSource = "mapare_prestatii.cod_rar" + .Column3.Width = 100 + + .Column4.Header1.Caption = "Auto Send" + .Column4.ControlSource = "mapare_prestatii.auto_send" + .Column4.Width = 80 + + .SetAll("ReadOnly", .F., "Column") + .Refresh() + Endwith + llSuccess = .T. + + Catch To loError + Messagebox("Eroare la setup grid mapari: " + loError.Message, 16, "Eroare") + llSuccess = .F. + Endtry + + Return llSuccess + Endfunc + + Function SetupGridLog + Local llSuccess + llSuccess = .F. + + Try + With This.PageFrame1.Page3.Grid2 + .RecordSource = "rar_log" + .ColumnCount = 6 + + .Column1.Header1.Caption = "Data" + .Column1.ControlSource = "rar_log.data_prez" + .Column1.Width = 100 + + .Column2.Header1.Caption = "Comanda" + .Column2.ControlSource = "rar_log.nr_comanda" + .Column2.Width = 100 + + .Column3.Header1.Caption = "VIN" + .Column3.ControlSource = "rar_log.vin" + .Column3.Width = 150 + + .Column4.Header1.Caption = "Status" + .Column4.ControlSource = "rar_log.status" + .Column4.Width = 80 + + .Column5.Header1.Caption = "Eroare" + .Column5.ControlSource = "rar_log.error_msg" + .Column5.Width = 200 + + .Column6.Header1.Caption = "Data Trimitere" + .Column6.ControlSource = "rar_log.data_trim" + .Column6.Width = 150 + + .SetAll("ReadOnly", .T., "Column") + .Refresh() + Endwith + llSuccess = .T. + + Catch To loError + Messagebox("Eroare la setup grid log: " + loError.Message, 16, "Eroare") + llSuccess = .F. + Endtry + + Return llSuccess + Endfunc + + * Convertim din YYYY-MM-DD n data VFP + Function Data2Date + Lparameters lcData + Local lnAn, lnLuna, lnZi, ldData + lnAn = Val(Getwordnum(lcData,1,'-')) + lnLuna = Val(Getwordnum(lcData,2,'-')) + lnZi = Val(Getwordnum(lcData,3,'-')) + ldData = Date(lnAn,lnLuna,lnZi) + Return m.ldData + Endfunc + + Function Destroy + This.StopAutoProcessing() + + * nchidem toate tabelele + If Used("mapare_prestatii") + Use In mapare_prestatii + Endif + + If Used("rar_log") + Use In rar_log + Endif + + If Used("prestatii_rar") + Use In prestatii_rar + Endif + + If Used("prezentari_fin") + Use In prezentari_fin + Endif + + If Used("xml_preview") + Use In xml_preview + Endif + + DoDefault() + Endfunc +Enddefine + +Enddefine + +Function Main + Local loForm, llSuccess + llSuccess = .F. + + Try + loForm = Createobject("RarAutoPassForm") + loForm.Show(1) + llSuccess = .T. + + Catch To loError + Messagebox("Eroare la lansare aplicatie: " + loError.Message, 16, "Eroare") + llSuccess = .F. + Endtry + + Return llSuccess +Endfunc diff --git a/rar_advanced.prg b/rar_advanced.prg new file mode 100644 index 0000000..55782bd --- /dev/null +++ b/rar_advanced.prg @@ -0,0 +1,271 @@ +* Adaugam controale noi in clasa RarAutoPassForm pentru tab-ul Log + +* Form pentru cautare avansata +DEFINE CLASS SearchLogForm AS Form + Caption = "Cautare avansata in log" + Width = 600 + Height = 400 + AutoCenter = .T. + MaxButton = .F. + BorderStyle = 2 + + ADD OBJECT lblDateFrom AS Label WITH ; + Caption = "De la data:", ; + Left = 20, ; + Top = 20 + + ADD OBJECT txtDateFrom AS TextBox WITH ; + Left = 100, ; + Top = 20, ; + Width = 100, ; + Value = DATE() - 30 + + ADD OBJECT lblDateTo AS Label WITH ; + Caption = "Pana la:", ; + Left = 220, ; + Top = 20 + + ADD OBJECT txtDateTo AS TextBox WITH ; + Left = 280, ; + Top = 20, ; + Width = 100, ; + Value = DATE() + + ADD OBJECT lblVin AS Label WITH ; + Caption = "VIN:", ; + Left = 20, ; + Top = 60 + + ADD OBJECT txtVin AS TextBox WITH ; + Left = 100, ; + Top = 60, ; + Width = 150 + + ADD OBJECT lblComanda AS Label WITH ; + Caption = "Nr. Comanda:", ; + Left = 270, ; + Top = 60 + + ADD OBJECT txtComanda AS TextBox WITH ; + Left = 350, ; + Top = 60, ; + Width = 100 + + ADD OBJECT lblStatus AS Label WITH ; + Caption = "Status:", ; + Left = 20, ; + Top = 100 + + ADD OBJECT cboStatus AS ComboBox WITH ; + Left = 100, ; + Top = 100, ; + Width = 150, ; + Style = 2 + + ADD OBJECT chkHasError AS Checkbox WITH ; + Caption = "Doar cu erori", ; + Left = 270, ; + Top = 100 + + ADD OBJECT btnSearch AS CommandButton WITH ; + Caption = "Caută", ; + Left = 20, ; + Top = 140, ; + Width = 100, ; + Height = 30 + + ADD OBJECT btnCancel AS CommandButton WITH ; + Caption = "Anulează", ; + Left = 130, ; + Top = 140, ; + Width = 100, ; + Height = 30 + + * Constructor + FUNCTION Init + THIS.cboStatus.AddItem("Toate") + THIS.cboStatus.AddItem("SUCCESS") + THIS.cboStatus.AddItem("ERROR") + THIS.cboStatus.ListIndex = 1 + RETURN .T. + + * Handler pentru butonul Cauta + FUNCTION btnSearch.Click + LOCAL lcFilter + lcFilter = THIS.BuildFilter() + THISFORM.Parent.ApplyLogFilter(lcFilter) + THISFORM.Release() + + * Handler pentru butonul Anuleaza + FUNCTION btnCancel.Click + THISFORM.Release() + + * Construieste filtrul SQL + FUNCTION BuildFilter + LOCAL lcFilter, lcAnd + lcFilter = "" + lcAnd = "" + + * Filtru data + IF !EMPTY(THIS.txtDateFrom.Value) + lcFilter = lcFilter + "data_prezentare >= CTOD('" + ; + DTOC(THIS.txtDateFrom.Value) + "')" + lcAnd = " AND " + ENDIF + + IF !EMPTY(THIS.txtDateTo.Value) + lcFilter = lcFilter + lcAnd + "data_prezentare <= CTOD('" + ; + DTOC(THIS.txtDateTo.Value) + "')" + lcAnd = " AND " + ENDIF + + * Filtru VIN + IF !EMPTY(THIS.txtVin.Value) + lcFilter = lcFilter + lcAnd + "UPPER(vin) LIKE '" + ; + UPPER(ALLTRIM(THIS.txtVin.Value)) + "%'" + lcAnd = " AND " + ENDIF + + * Filtru comanda + IF !EMPTY(THIS.txtComanda.Value) + lcFilter = lcFilter + lcAnd + "UPPER(nr_comanda) LIKE '" + ; + UPPER(ALLTRIM(THIS.txtComanda.Value)) + "%'" + lcAnd = " AND " + ENDIF + + * Filtru status + IF THIS.cboStatus.ListIndex > 1 + lcFilter = lcFilter + lcAnd + "status = '" + ; + THIS.cboStatus.Value + "'" + lcAnd = " AND " + ENDIF + + * Filtru erori + IF THIS.chkHasError.Value + lcFilter = lcFilter + lcAnd + "!EMPTY(error_msg)" + ENDIF + + RETURN lcFilter +ENDDEFINE + +* Adaugam metodele noi in RarAutoPassForm +* In functia Init, adaugam butoanele noi: +ADD OBJECT btnAdvSearch AS CommandButton OF tabLog WITH ; + Caption = "Căutare avansată", ; + Left = 320, ; + Top = 20, ; + Width = 120, ; + Height = C_BTN_HEIGHT + +ADD OBJECT btnExportExcel AS CommandButton OF tabLog WITH ; + Caption = "Export Excel", ; + Left = 450, ; + Top = 20, ; + Width = 120, ; + Height = C_BTN_HEIGHT + +* Handler pentru cautare avansata +FUNCTION btnAdvSearch.Click + LOCAL loSearch + loSearch = CREATEOBJECT("SearchLogForm") + loSearch.Show(1) +ENDFUNC + +* Aplicare filtru de cautare +FUNCTION ApplyLogFilter +PARAMETERS tcFilter + SELECT rar_log + IF !EMPTY(tcFilter) + SET FILTER TO &tcFilter + ELSE + SET FILTER TO + ENDIF + GO TOP + THIS.grdLog.Refresh() +ENDFUNC + +* Handler pentru export Excel +FUNCTION btnExportExcel.Click + LOCAL lcFile, loExcel, loWorkbook, loWorksheet + LOCAL lnRow, lnCol, lcValue + + * Alegem locatia fisierului + lcFile = PUTFILE("Excel files|*.xlsx", "RAR_Log_Export.xlsx", "", 0) + IF EMPTY(lcFile) + RETURN + ENDIF + + THIS.AddStatus("Export Excel în " + lcFile + "...") + + TRY + * Cream obiectul Excel + loExcel = CREATEOBJECT("Excel.Application") + loExcel.Visible = .F. + + * Adaugam un workbook nou + loWorkbook = loExcel.Workbooks.Add() + loWorksheet = loWorkbook.Sheets(1) + + * Setam headerele + WITH loWorksheet + .Cells(1,1).Value = "Data prezentare" + .Cells(1,2).Value = "Nr. comandă" + .Cells(1,3).Value = "VIN" + .Cells(1,4).Value = "Nr. înmatriculare" + .Cells(1,5).Value = "Km final" + .Cells(1,6).Value = "Km initial" + .Cells(1,7).Value = "Status" + .Cells(1,8).Value = "Eroare" + .Cells(1,9).Value = "Data trimitere" + + * Formatare header + .Range(.Cells(1,1), .Cells(1,9)).Font.Bold = .T. + .Range(.Cells(1,1), .Cells(1,9)).Interior.Color = RGB(200,200,200) + ENDWITH + + * Populam datele + SELECT rar_log + lnRow = 2 + SCAN + loWorksheet.Cells(lnRow, 1).Value = data_prezentare + loWorksheet.Cells(lnRow, 2).Value = nr_comanda + loWorksheet.Cells(lnRow, 3).Value = vin + loWorksheet.Cells(lnRow, 4).Value = nr_inm + loWorksheet.Cells(lnRow, 5).Value = km_final + loWorksheet.Cells(lnRow, 6).Value = km_initial + loWorksheet.Cells(lnRow, 7).Value = status + loWorksheet.Cells(lnRow, 8).Value = error_msg + loWorksheet.Cells(lnRow, 9).Value = data_trimitere + + * Coloram randurile cu erori in rosu deschis + IF status = "ERROR" + loWorksheet.Range(loWorksheet.Cells(lnRow,1), ; + loWorksheet.Cells(lnRow,9)).Interior.Color = RGB(255,200,200) + ENDIF + + lnRow = lnRow + 1 + ENDSCAN + + * Auto-fit coloane + loWorksheet.Range("A:I").Columns.AutoFit() + + * Salvam si inchidem + loWorkbook.SaveAs(lcFile) + loWorkbook.Close() + loExcel.Quit() + + THIS.AddStatus("Export Excel finalizat cu succes") + + * Deschidem fisierul + RUN /N "explorer.exe" &lcFile + + CATCH TO loError + THIS.AddStatus("EROARE la export Excel: " + loError.Message) + + TRY + loWorkbook.Close() + loExcel.Quit() + CATCH + ENDTRY + ENDTRY +ENDFUNC diff --git a/rar_automate.prg b/rar_automate.prg new file mode 100644 index 0000000..41003b7 --- /dev/null +++ b/rar_automate.prg @@ -0,0 +1,55 @@ +#DEFINE XML_PATH "C:\RAR\comenzi.xml" +#DEFINE LOG_PATH "C:\RAR\auto_log.txt" + +PROCEDURE AutomateProcesare + LOCAL lcDate, llSuccess, llTestMode + llSuccess = .F. + llTestMode = .F. && Productie + + lcDate = DTOC(DATE()) + + Try + * Export comenzi in XML + DO export_comenzi WITH XML_PATH, DATE() + + * Procesare prin RAR AutoPass + loRar = CREATEOBJECT("RarAutoPass") + IF !loRar.SetCredentials(m.llTestMode) && .F. pentru productie + THIS.WriteLog("EROARE: " + loRar.ErrorMsg) + llSuccess = .F. + RETURN llSuccess + ENDIF + + IF loRar.ProcessXMLComenzi(XML_PATH) + THIS.WriteLog("Procesare reu?ita pentru " + lcDate) + llSuccess = .T. + ELSE + THIS.WriteLog("EROARE: " + loRar.ErrorMsg) + llSuccess = .F. + ENDIF + + Catch To loError + THIS.WriteLog("EROARE: " + loError.Message) + llSuccess = .F. + Endtry + + Return llSuccess + +PROCEDURE WriteLog + PARAMETERS tcMessage + LOCAL llSuccess + llSuccess = .F. + + Try + STRTOFILE(; + TTOC(DATETIME()) + ": " + tcMessage + CHR(13) + CHR(10),; + LOG_PATH,; + 1) + llSuccess = .T. + + Catch To loError + ? "Eroare scriere log: " + loError.Message + llSuccess = .F. + Endtry + + Return llSuccess \ No newline at end of file diff --git a/rar_autopass.PJT b/rar_autopass.PJT new file mode 100644 index 0000000..259586a Binary files /dev/null and b/rar_autopass.PJT differ diff --git a/rar_autopass.pjx b/rar_autopass.pjx new file mode 100644 index 0000000..434f297 Binary files /dev/null and b/rar_autopass.pjx differ diff --git a/rar_autopass.prg b/rar_autopass.prg new file mode 100644 index 0000000..104e32e --- /dev/null +++ b/rar_autopass.prg @@ -0,0 +1,611 @@ +SET STEP ON +Test1() +Test2() +Procedure Test1 + Local loRar As "RarAutoPass" + Local lcXMLComenzi, llTestMode + + * Creare instanta + loRar = Createobject("RarAutoPass") + + * Setare credentiale - pentru test + llTestMode = .T. + loRar.SetCredentials(m.llTestMode) + + * Actualizare nomenclator + loRar.UpdateNomenclator() + + TEXT TO lcXMLComenzi NOSHOW + + + + COM001 + 2024-02-04 + VF1234567890 + B01AAA + 150000 + 0 + Test prezentare + + + SCHIMB_VITEZOMETRU + Schimbare vitezometru + + + REPARATIE_BORD + Reparatie instrumente bord + + + + + + + + ENDTEXT + + * Procesare fisier XML cu comenzi + If !loRar.ProcessXMLComenzi("comenzi.xml") + ? "Eroare: " + loRar.ErrorMsg + Endif + +Endproc && Test1 + +*********************************** +Procedure Test2 + Local loRar As "RarAutoPass" + Local llTestMode + + * Creare instanta si setare mediu + loRar = Createobject("RarAutoPass") + + * Pentru test + llTestMode = .T. + loRar.SetCredentials(m.llTestMode) + + * SAU pentru productie + * loRar.SetCredentials("email@service.ro", "parola", .F.) + + * Actualizare nomenclator (fortat) + If !loRar.UpdateNomenclator(.T.) + ? "Eroare actualizare nomenclator: " + loRar.ErrorMsg + Return + Endif + + * Adaugare mapari operatii + Use mapare_prestatii + Append Blank + Replace cod_op_service With "SCHIMB_VITEZOMETRU", ; + descr_op_service With "Schimbare vitezometru/instrumente bord", ; + cod_prestatie_rar With "OE-1", ; + auto_send With .T., ; + data_actualizare With Date() + + * Procesare comenzi din XML + If !loRar.ProcessXMLComenzi("comenzi.xml") + ? "Eroare procesare comenzi: " + loRar.ErrorMsg + * Verificam log-ul pentru detalii + Select rar_log + Browse + Endif + + * Sau trimitere prezentare individuala + If !loRar.SendPrezentare(; + "2024-02-04", ; && Data prestatie + "B01AAA", ; && Nr inmatriculare + "VF1234567890", ; && VIN + 150000, ; && Km final + 0, ; && Km initial + "Test", ; && Observatii + "", ; && Imagine base64 + '[{"codPrestatie":"OE-1"}]', ; && Prestatii JSON + "COM001") && Nr comanda pentru logging + ? "Eroare trimitere prezentare: " + loRar.ErrorMsg + Endif + +Endproc && Test2 + +* Clasa pentru integrarea cu RAR AutoPass +#DEFINE C_API_TIMEOUT 30 +#DEFINE C_TEST_URL "https://apps.rarom.ro/test-rar-autopass" +#DEFINE C_PROD_URL "https://apps.rarom.ro/rar-autopass" + +* Clasa pentru integrarea cu RAR AutoPass +DEFINE CLASS RarAutoPass AS Custom + + * Proprietati private + lcUrl = "" + lcEmail = "" + lcPassword = "" + lcToken = "" + lcError = "" + llTestMode = .T. + loXMLHTTP = null + + * Proprietati publice + ErrorMsg = "" + LastResponse = "" + + * Constructor + FUNCTION Init + THIS.lcUrl = "" + THIS.lcEmail = "" + THIS.lcPassword = "" + THIS.lcToken = "" + THIS.lcError = "" + THIS.llTestMode = .T. + THIS.loXMLHTTP = CREATEOBJECT("MSXML2.ServerXMLHTTP.6.0") + THIS.SetHttpTimeout() + + * Cream tabelele necesare daca nu exista + THIS.InitializeStructure() + RETURN .T. + ENDFUNC + + * Setare timeout pentru requesturi HTTP + FUNCTION SetHttpTimeout + LOCAL llSuccess + llSuccess = .F. + + TRY + THIS.loXMLHTTP.setTimeouts(C_API_TIMEOUT*1000, ; && ResolveTimeout + C_API_TIMEOUT*1000, ; && ConnectTimeout + C_API_TIMEOUT*1000, ; && SendTimeout + C_API_TIMEOUT*1000) && ReceiveTimeout + llSuccess = .T. + CATCH TO loError + THIS.ErrorMsg = "Eroare setare timeout: " + loError.Message + llSuccess = .F. + ENDTRY + + RETURN llSuccess + ENDFUNC + + * Setare credentiale + FUNCTION SetCredentials + PARAMETERS tlTestMode + LOCAL llSuccess + llSuccess = .F. + + TRY + * Setam modul de lucru (test/productie) + THIS.llTestMode = tlTestMode + + * Setam URL-ul corespunzator + THIS.lcUrl = IIF(THIS.llTestMode, C_TEST_URL, C_PROD_URL) + + * Citim credentialele din fisierul de setari + llSuccess = THIS.ReadSettings(tlTestMode) + + CATCH TO loEx + THIS.ErrorMsg = "Eroare setare credentiale: " + loEx.Message + llSuccess = .F. + ENDTRY + + RETURN llSuccess + ENDFUNC + + * Initializare structura tabele + FUNCTION InitializeStructure + LOCAL llSuccess + llSuccess = .T. + + TRY + IF !FILE("prestatii_rar.dbf") + CREATE TABLE prestatii_rar FREE (; + cod_prest C(10), ; && cod_prestatie + nume_prest C(250), ; && nume_prestatie + data_act D) && data_actualizare + + INDEX ON cod_prest TAG cod_prest + USE IN SELECT('prestatii_rar') + ENDIF + + IF !FILE("mapare_prestatii.dbf") + CREATE TABLE mapare_prestatii FREE (; + cod_op C(20), ; && cod_op_service + descr_op M, ; && descr_op_service + cod_rar C(10), ; && cod_prestatie_rar + auto_send L, ; && auto_send + data_act D) && data_actualizare + INDEX ON cod_op TAG cod_op + USE IN SELECT('mapare_prestatii') + ENDIF + + IF !FILE("rar_log.dbf") + CREATE TABLE rar_log FREE (; + id I AUTOINC, ; + data_prez D, ; && data_prezentare + nr_comanda C(20), ; + vin C(20), ; + nr_inm C(10), ; + km_final N(10), ; + km_init N(10), ; && km_initial + prestatii M, ; + status C(20), ; + error_msg M, ; + data_trim T) && data_trimitere + USE IN SELECT('rar_log') + ENDIF + + CATCH TO loEx + THIS.ErrorMsg = "Eroare initializare structura: " + loEx.Message + llSuccess = .F. + ENDTRY + + RETURN llSuccess + ENDFUNC + + * Login si obtinere token + FUNCTION Login + LOCAL lcJsonLogin, llSuccess + llSuccess = .F. + + lcJsonLogin = '{"email":"' + THIS.lcEmail + '","password":"' + THIS.lcPassword + '"}' + + TRY + THIS.loXMLHTTP.Open("POST", THIS.lcUrl + "/public/login", .F.) + THIS.loXMLHTTP.setRequestHeader("Content-Type", "application/json") + THIS.loXMLHTTP.Send(lcJsonLogin) + + IF THIS.loXMLHTTP.Status != 200 + THIS.ErrorMsg = "Eroare login: " + THIS.loXMLHTTP.responseText + llSuccess = .F. + RETURN llSuccess + ENDIF + + * Folosim nfjsonread pentru a parsa raspunsul + loResponse = nfjsonread(THIS.loXMLHTTP.responseText) + + IF TYPE('loResponse') == 'O' AND !ISNULL(loResponse) + THIS.lcToken = loResponse.token + IF EMPTY(THIS.lcToken) + THIS.ErrorMsg = "Nu s-a putut obtine token-ul" + llSuccess = .F. + ELSE + llSuccess = .T. + ENDIF + ELSE + THIS.ErrorMsg = "Eroare parsare raspuns JSON" + llSuccess = .F. + ENDIF + + CATCH TO loEx + THIS.ErrorMsg = "Eroare login: " + loEx.Message + llSuccess = .F. + ENDTRY + + RETURN llSuccess + ENDFUNC +* Actualizare nomenclator presta?ii + FUNCTION UpdateNomenclator + LPARAMETERS tlForceRefresh + LOCAL llSuccess + llSuccess = .F. + + IF !tlForceRefresh AND FILE("prestatii_rar.dbf") + llSuccess = .T. + RETURN llSuccess + ENDIF + + IF !THIS.Login() + RETURN .F. + ENDIF + + TRY + THIS.loXMLHTTP.Open("GET", THIS.lcUrl + "/nomenclator/getNomenclatorPrestatii", .F.) + THIS.loXMLHTTP.setRequestHeader("Authorization", "Bearer " + THIS.lcToken) + THIS.loXMLHTTP.Send() + + IF THIS.loXMLHTTP.Status != 200 + THIS.ErrorMsg = "Eroare obtinere nomenclator: " + THIS.loXMLHTTP.responseText + llSuccess = .F. + RETURN llSuccess + ENDIF + + * Folosim nfjsonread pentru a parsa raspunsul + loJson = nfjsonread(THIS.loXMLHTTP.responseText) + + IF TYPE('loJson') == 'O' AND !ISNULL(loJson) AND TYPE('loJson.data') == 'O' + * Deschidem tabelul prestatii_rar + IF !USED("prestatii_rar") + USE prestatii_rar IN 0 + ENDIF + + SELECT prestatii_rar + + * Procesam fiecare prestatie + FOR EACH loItem IN loJson.data + * Cautam prestatia dupa cod + LOCATE FOR cod_prest = loItem.codPrestatie + + IF FOUND() + * Update daca exista ?i s-a modificat numele + IF nume_prest != loItem.numePrestatie + REPLACE ; + nume_prest WITH loItem.numePrestatie, ; + data_act WITH DATE() + ENDIF + ELSE + * Insert daca nu exista + INSERT INTO prestatii_rar (; + cod_prest, ; + nume_prest, ; + data_act) ; + VALUES (; + loItem.codPrestatie, ; + loItem.numePrestatie, ; + DATE()) + ENDIF + ENDFOR + + llSuccess = .T. + ELSE + THIS.ErrorMsg = "Eroare parsare raspuns JSON pentru nomenclator" + llSuccess = .F. + ENDIF + + CATCH TO loEx + THIS.ErrorMsg = "Eroare actualizare nomenclator: " + loEx.Message + llSuccess = .F. + ENDTRY + + RETURN llSuccess + ENDFUNC + + * Trimitere prezentare individuala + FUNCTION SendPrezentare + LPARAMETERS tcDataPrest, tcNrInm, tcVin, tnKmFinal, tnKmInitial, ; + tcObs, tcImagineB64, tcPrestatii, tcNrComanda + LOCAL llSuccess, lcJson + llSuccess = .F. + + IF !THIS.Login() + RETURN .F. + ENDIF + + * Construim JSON-ul pentru prezentare + TEXT TO lcJson NOSHOW TEXTMERGE +{ + "dataPrestatie": "<>", + "nrInmatriculare": "<>", + "vin": "<>", + "odometruFinal": <>, + "odometruInitial": <>, + "obs": "<>", + "b64Image": "<>", + "status": "FINALIZATA", + "prestatii": <> +} + ENDTEXT + + TRY + THIS.loXMLHTTP.Open("POST", THIS.lcUrl + "/prezentari/postPrezentare", .F.) + THIS.loXMLHTTP.setRequestHeader("Content-Type", "application/json") + THIS.loXMLHTTP.setRequestHeader("Authorization", "Bearer " + THIS.lcToken) + THIS.loXMLHTTP.Send(lcJson) + + THIS.LastResponse = THIS.loXMLHTTP.responseText + + IF THIS.loXMLHTTP.Status != 200 + THIS.ErrorMsg = "Eroare trimitere prezentare: " + THIS.LastResponse + THIS.LogPrezentare(tcDataPrest, tcNrComanda, tcVin, tcNrInm, ; + tnKmFinal, tnKmInitial, tcPrestatii, "ERROR", THIS.ErrorMsg) + llSuccess = .F. + ELSE + THIS.LogPrezentare(tcDataPrest, tcNrComanda, tcVin, tcNrInm, ; + tnKmFinal, tnKmInitial, tcPrestatii, "SUCCESS") + llSuccess = .T. + ENDIF + + CATCH TO loEx + THIS.ErrorMsg = "Eroare trimitere prezentare: " + loEx.Message + THIS.LogPrezentare(tcDataPrest, tcNrComanda, tcVin, tcNrInm, ; + tnKmFinal, tnKmInitial, tcPrestatii, "ERROR", THIS.ErrorMsg) + llSuccess = .F. + ENDTRY + + RETURN llSuccess + ENDFUNC + + * Logging prezentari + FUNCTION LogPrezentare + LPARAMETERS tcDataPrest, tcNrComanda, tcVin, tcNrInm, tnKmFinal, ; + tnKmInitial, tcPrestatii, tcStatus, tcErrorMsg + LOCAL llSuccess + llSuccess = .F. + + TRY + IF !USED("rar_log") + USE rar_log IN 0 + ENDIF + + INSERT INTO rar_log (; + data_prez, ; + nr_comanda, ; + vin, ; + nr_inm, ; + km_final, ; + km_init, ; + prestatii, ; + status, ; + error_msg, ; + data_trim) ; + VALUES (; + CTOD(tcDataPrest), ; + tcNrComanda, ; + tcVin, ; + tcNrInm, ; + tnKmFinal, ; + tnKmInitial, ; + tcPrestatii, ; + tcStatus, ; + tcErrorMsg, ; + DATETIME()) + llSuccess = .T. + + CATCH TO loEx + THIS.ErrorMsg = "Eroare logging: " + loEx.Message + llSuccess = .F. + ENDTRY + + RETURN llSuccess + ENDFUNC + +* Procesare comenzi din XML + FUNCTION ProcessXMLComenzi + LPARAMETERS tcXMLFile + LOCAL loXML AS MSXML2.DOMDocument.6.0 + LOCAL loNodes AS MSXML2.IXMLDOMNodeList + LOCAL loNode AS MSXML2.IXMLDOMNode + LOCAL lcPrestatii, llSuccess + llSuccess = .F. + + TRY + loXML = CREATEOBJECT("MSXML2.DOMDocument.6.0") + loXML.async = .F. + + IF !loXML.load(tcXMLFile) + THIS.ErrorMsg = "Eroare incarcare XML: " + loXML.parseError.reason + llSuccess = .F. + RETURN llSuccess + ENDIF + + * Obtinem toate nodurile de comenzi + loNodes = loXML.selectNodes("/comenzi/comanda") + + FOR EACH loNode IN loNodes + * Extragem datele din XML + lcNrComanda = loNode.selectSingleNode("nr_comanda").text + lcDataPrest = loNode.selectSingleNode("data").text + lcVin = loNode.selectSingleNode("vin").text + lcNrInm = loNode.selectSingleNode("nr_inmatriculare").text + lnKmFinal = VAL(loNode.selectSingleNode("km_final").text) + lnKmInitial = VAL(loNode.selectSingleNode("km_initial").text) + lcObs = NVL(loNode.selectSingleNode("observatii").text, "") + + * Generam JSON cu prestatii din operatiile comenzii + lcPrestatii = THIS.GetPrestatiiFromXMLNode(loNode.selectNodes("operatii/operatie")) + + * Trimitem prezentarea + IF !THIS.SendPrezentare(lcDataPrest, lcNrInm, lcVin, ; + lnKmFinal, lnKmInitial, lcObs, ; + "", lcPrestatii, lcNrComanda) + * Continuam cu urmatoarea comanda chiar daca una esueaza + * dar am logat deja eroarea in SendPrezentare + LOOP + ENDIF + ENDFOR + llSuccess = .T. + + CATCH TO loEx + THIS.ErrorMsg = "Eroare procesare XML: " + loEx.Message + llSuccess = .F. + ENDTRY + + RETURN llSuccess + ENDFUNC + + * Generare JSON prestatii din noduri XML + FUNCTION GetPrestatiiFromXMLNode + LPARAMETERS loOperatiiNodes + LOCAL lcPrestatiiJson, lcCodRar + LOCAL loNode AS MSXML2.IXMLDOMNode + + TEXT TO lcPrestatiiJson NOSHOW +[ + ENDTEXT + + lnCount = 0 + FOR EACH loNode IN loOperatiiNodes + lcCodOperatie = loNode.selectSingleNode("cod_operatie").text + lcCodRar = THIS.GetCodRarPentruOperatie(lcCodOperatie) + + IF !EMPTY(lcCodRar) + lcPrestatiiJson = lcPrestatiiJson + ; + IIF(lnCount > 0, ",", "") + ; + '{"codPrestatie":"' + lcCodRar + '","idPrezentare":null}' + lnCount = lnCount + 1 + ENDIF + ENDFOR + + lcPrestatiiJson = lcPrestatiiJson + "]" + RETURN lcPrestatiiJson + ENDFUNC + + * Obtinere cod RAR pentru operatie service + FUNCTION GetCodRarPentruOperatie + LPARAMETERS tcCodOperatie + LOCAL lcCodRar + + IF !USED("mapare_prestatii") + USE mapare_prestatii IN 0 + ENDIF + + SELECT mapare_prestatii + LOCATE FOR cod_op = tcCodOperatie + IF FOUND() AND auto_send + lcCodRar = cod_rar + ELSE + lcCodRar = "" + ENDIF + + RETURN lcCodRar + ENDFUNC + + * Functie pentru citirea credentialelor din settings.xml + FUNCTION ReadSettings + PARAMETERS tlTestMode + LOCAL loXML AS MSXML2.DOMDocument.6.0 + LOCAL lcSettingsPath, lcEnvironment, loCredentials + LOCAL lcEmail, lcPassword, llSuccess + llSuccess = .F. + + TRY + * Cream obiectul XML + loXML = CREATEOBJECT("MSXML2.DOMDocument.6.0") + loXML.async = .F. + + * Determinam calea catre settings.xml + lcSettingsPath = ADDBS(JUSTPATH(SYS(16,0))) + "settings.xml" + + * Verificam daca exista fisierul + IF !FILE(lcSettingsPath) + THIS.ErrorMsg = "Fisierul settings.xml nu exista in: " + lcSettingsPath + llSuccess = .F. + RETURN llSuccess + ENDIF + + * Incarcam XML-ul + IF !loXML.load(lcSettingsPath) + THIS.ErrorMsg = "Eroare incarcare settings.xml: " + loXML.parseError.reason + llSuccess = .F. + RETURN llSuccess + ENDIF + + * Determinam mediul (test/productie) + lcEnvironment = IIF(tlTestMode, "test", "production") + + * Selectam credentialele pentru mediul dorit + loCredentials = loXML.selectSingleNode("/settings/" + lcEnvironment + "/credentials") + + IF !ISNULL(loCredentials) + lcEmail = loCredentials.selectSingleNode("email").text + lcPassword = loCredentials.selectSingleNode("password").text + + * Setam credentialele + THIS.lcEmail = lcEmail + THIS.lcPassword = lcPassword + llSuccess = .T. + ELSE + THIS.ErrorMsg = "Nu s-au gasit credentiale pentru mediul: " + lcEnvironment + llSuccess = .F. + ENDIF + + CATCH TO loEx + THIS.ErrorMsg = "Eroare citire setari: " + loEx.Message + llSuccess = .F. + ENDTRY + + RETURN llSuccess + ENDFUNC +ENDDEFINE \ No newline at end of file diff --git a/settings.xml.example b/settings.xml.example new file mode 100644 index 0000000..43e4bbc --- /dev/null +++ b/settings.xml.example @@ -0,0 +1,22 @@ + + + + + + EMAIL_TEST_RAR + PAROLA_TEST_RAR + + + + + EMAIL_PROD_RAR + PAROLA_PROD_RAR + + + diff --git a/test-comenzi.xml b/test-comenzi.xml new file mode 100644 index 0000000..fe494a8 --- /dev/null +++ b/test-comenzi.xml @@ -0,0 +1,58 @@ + + + + TEST001 + 2024-02-05 + WBA1234567890 + B101TEST + 125000 + 0 + Test prezentare service + + + OE-1 + Schimbare instrumente bord + + + OE-2 + Reparatie instrumente bord + + + + + + TEST002 + 2024-02-05 + WDC9876543210 + B202TEST + 89750 + 89500 + Reparatie kilometraj electronic + + + OE-1 + Schimbare instrumente bord + + + + + + TEST003 + 2024-02-05 + VSSZZZ1234567 + B303TEST + 45000 + 0 + Service general + + + OE-2 + Reparatie instrumente bord + + + OE-1 + Schimbare instrumente bord + + + + \ No newline at end of file