From 78d21d5a3848901ead407422fb01b769b3a4cd81 Mon Sep 17 00:00:00 2001 From: Marius Mutu Date: Sun, 14 Jun 2026 23:10:28 +0300 Subject: [PATCH] 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) --- .gitignore | 72 ++ Document informativ RAR- Autopass.txt | 51 + Documentatie Serviciu AutoPass_Final.txt | 413 +++++++ docs/CONTEXT.md | 97 ++ docs/plans/plan-design-review.md | 271 +++++ docs/plans/plan-eng-review.md | 241 ++++ export_comenzi.prg | 83 ++ mapare_prestatii.CDX | Bin 0 -> 3072 bytes mapare_prestatii.DBF | Bin 0 -> 456 bytes mapare_prestatii.FPT | Bin 0 -> 512 bytes nfjsonread.prg | 775 +++++++++++++ prestatii_rar.CDX | Bin 0 -> 3072 bytes prestatii_rar.DBF | Bin 0 -> 5773 bytes rar-forms.prg | 1296 ++++++++++++++++++++++ rar_advanced.prg | 271 +++++ rar_automate.prg | 55 + rar_autopass.PJT | Bin 0 -> 4092 bytes rar_autopass.pjx | Bin 0 -> 2103 bytes rar_autopass.prg | 611 ++++++++++ settings.xml.example | 22 + test-comenzi.xml | 58 + 21 files changed, 4316 insertions(+) create mode 100644 .gitignore create mode 100644 Document informativ RAR- Autopass.txt create mode 100644 Documentatie Serviciu AutoPass_Final.txt create mode 100644 docs/CONTEXT.md create mode 100644 docs/plans/plan-design-review.md create mode 100644 docs/plans/plan-eng-review.md create mode 100644 export_comenzi.prg create mode 100644 mapare_prestatii.CDX create mode 100644 mapare_prestatii.DBF create mode 100644 mapare_prestatii.FPT create mode 100644 nfjsonread.prg create mode 100644 prestatii_rar.CDX create mode 100644 prestatii_rar.DBF create mode 100644 rar-forms.prg create mode 100644 rar_advanced.prg create mode 100644 rar_automate.prg create mode 100644 rar_autopass.PJT create mode 100644 rar_autopass.pjx create mode 100644 rar_autopass.prg create mode 100644 settings.xml.example create mode 100644 test-comenzi.xml 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 0000000000000000000000000000000000000000..d01cfe18fa8b781de055f6896e651cf989f652a4 GIT binary patch literal 3072 zcmZQzVSocJh6jvr&M0G8hX5l3D5^)Jdss&@5uRfPrh)%ZaG&x2e+CAAegPI1W(GC~ zB6N=`8M-0h?C%oqAHcwc@PP7uR6tAczWIQJ@^yFeEh~ z{XvdFAQ5LtAi;$s2Vy#ghWN(^yZX5>F!(3~`5-%xfs12^W4xnt2vE#L1xRopO9I)v JX#A1N1pv 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 0000000000000000000000000000000000000000..ba16655abdde3923409c0db26bd5624394b70b74 GIT binary patch literal 3072 zcmZQzVSoV zCb1J&{`t+D@%r71ySGJA{8`{r6}0OTAT=AHJymaNLrkU&8rzLBE^zX1m_3w+Y53&s$7z9K^6& z8jd-PuNj8pP7csT%T8=5niivtIgD>3j9pDoMHP(>Vhz&?9hw(GdSWInol!Ss0)(AUN z_?V7s+v4kP7R($*nP-{njDUQ?6(^MhQt;4dRL6VKu~uSKbWLgk8SdCf!zRFDl7=X% z*4Z+8MIAX2wR6;Q8o`SX+~EC^RSDRzqUR;KIgILpFe*pp*@fR6hJI=oePW7;I!MFu zAqmAmc9bM#J<1xEtA^klhQAsNPhMgjuZu#dIgG&>pUgAhI@va|GKPxZOY)eI9RzNq z>5%$KYbha|OG;%9W2mbx00Y))JcWMJJ)a?3tSO_WU|la0iosrz#fdTol_Hd_8?r{) zn!xi#+0r(Qm`_@6v0yGDZ3H{y7p{+~q_RFB5r?$1aY#H3g?=f5=ciMKV1~U_-B(c9F6sZ7{@yhK?{6tY98OX;qwH;OK9^vPhj|vl#%o- zCa)XTmG>rXW^Zm=JHa3cb=T84fdO&Xs6ofj7>N*+zPBi+s=g%k^4X)