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) <noreply@anthropic.com>
This commit is contained in:
2026-06-14 23:10:28 +03:00
commit 78d21d5a38
21 changed files with 4316 additions and 0 deletions

72
.gitignore vendored Normal file
View File

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

View File

@@ -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 <20>Prezentare<72> 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.

View File

@@ -0,0 +1,413 @@
SERVICIU WEB PENTRU SCHIMB DE DATE
R.A.R.-R.A. <20> Diverse institutii
Proprietar :
Registrul Auto Rom<6F>n 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
<EFBFBD>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 <20>USER_ALREADY_ADMIN<49>(<28>Userul are deja rol admin<69>)
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 <20> 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 <20> 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 <20> 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} <20> 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} <20>
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} <20>
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 <20> 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 <20> 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 <20> 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 <20>
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} <20> 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 <20> 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 <20>Bearer: {token}<7D>
3. Models/Modele
(1) ApiResponseMessage{
< * >:
string
}
ApiResponseMessage
(2) ApiResponse{
description
string
details
string
status
stringEnum:
[<5B>INVALID_EMAIL, INVALID_TOKEN, NOT_OK, OK<4F>]
}
(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

97
docs/CONTEXT.md Normal file
View File

@@ -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.

View File

@@ -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.0002.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.
```

View File

@@ -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` |

83
export_comenzi.prg Normal file
View File

@@ -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
<?xml version="1.0" encoding="UTF-8"?>
<comenzi>
</comenzi>
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

BIN
mapare_prestatii.CDX Normal file

Binary file not shown.

BIN
mapare_prestatii.DBF Normal file

Binary file not shown.

BIN
mapare_prestatii.FPT Normal file

Binary file not shown.

775
nfjsonread.prg Normal file
View File

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

BIN
prestatii_rar.CDX Normal file

Binary file not shown.

BIN
prestatii_rar.DBF Normal file

Binary file not shown.

1296
rar-forms.prg Normal file

File diff suppressed because it is too large Load Diff

271
rar_advanced.prg Normal file
View File

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

55
rar_automate.prg Normal file
View File

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

BIN
rar_autopass.PJT Normal file

Binary file not shown.

BIN
rar_autopass.pjx Normal file

Binary file not shown.

611
rar_autopass.prg Normal file
View File

@@ -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
<?xml version="1.0" encoding="UTF-8"?>
<comenzi>
<comanda>
<nr_comanda>COM001</nr_comanda>
<data>2024-02-04</data>
<vin>VF1234567890</vin>
<nr_inmatriculare>B01AAA</nr_inmatriculare>
<km_final>150000</km_final>
<km_initial>0</km_initial>
<observatii>Test prezentare</observatii>
<operatii>
<operatie>
<cod_operatie>SCHIMB_VITEZOMETRU</cod_operatie>
<denumire>Schimbare vitezometru</denumire>
</operatie>
<operatie>
<cod_operatie>REPARATIE_BORD</cod_operatie>
<denumire>Reparatie instrumente bord</denumire>
</operatie>
</operatii>
</comanda>
<comanda>
<!-- ... alte comenzi ... -->
</comanda>
</comenzi>
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": "<<tcDataPrest>>",
"nrInmatriculare": "<<tcNrInm>>",
"vin": "<<tcVin>>",
"odometruFinal": <<tnKmFinal>>,
"odometruInitial": <<IIF(EMPTY(tnKmInitial), "null", tnKmInitial)>>,
"obs": "<<tcObs>>",
"b64Image": "<<tcImagineB64>>",
"status": "FINALIZATA",
"prestatii": <<tcPrestatii>>
}
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

22
settings.xml.example Normal file
View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
TEMPLATE. Copiază în `settings.xml` (ignorat de git) și
completează credențialele reale. NU comite settings.xml.
În arhitectura web, aceste credențiale NU se mai țin în
client — se citesc din Oracle și se trimit la gateway.
Vezi docs/plans/plan-eng-review.md (secțiunea Securitate).
-->
<settings>
<test>
<credentials>
<email>EMAIL_TEST_RAR</email>
<password>PAROLA_TEST_RAR</password>
</credentials>
</test>
<production>
<credentials>
<email>EMAIL_PROD_RAR</email>
<password>PAROLA_PROD_RAR</password>
</credentials>
</production>
</settings>

58
test-comenzi.xml Normal file
View File

@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<comenzi>
<comanda>
<nr_comanda>TEST001</nr_comanda>
<data>2024-02-05</data>
<vin>WBA1234567890</vin>
<nr_inmatriculare>B101TEST</nr_inmatriculare>
<km_final>125000</km_final>
<km_initial>0</km_initial>
<observatii>Test prezentare service</observatii>
<operatii>
<operatie>
<cod_operatie>OE-1</cod_operatie>
<denumire>Schimbare instrumente bord</denumire>
</operatie>
<operatie>
<cod_operatie>OE-2</cod_operatie>
<denumire>Reparatie instrumente bord</denumire>
</operatie>
</operatii>
</comanda>
<comanda>
<nr_comanda>TEST002</nr_comanda>
<data>2024-02-05</data>
<vin>WDC9876543210</vin>
<nr_inmatriculare>B202TEST</nr_inmatriculare>
<km_final>89750</km_final>
<km_initial>89500</km_initial>
<observatii>Reparatie kilometraj electronic</observatii>
<operatii>
<operatie>
<cod_operatie>OE-1</cod_operatie>
<denumire>Schimbare instrumente bord</denumire>
</operatie>
</operatii>
</comanda>
<comanda>
<nr_comanda>TEST003</nr_comanda>
<data>2024-02-05</data>
<vin>VSSZZZ1234567</vin>
<nr_inmatriculare>B303TEST</nr_inmatriculare>
<km_final>45000</km_final>
<km_initial>0</km_initial>
<observatii>Service general</observatii>
<operatii>
<operatie>
<cod_operatie>OE-2</cod_operatie>
<denumire>Reparatie instrumente bord</denumire>
</operatie>
<operatie>
<cod_operatie>OE-1</cod_operatie>
<denumire>Schimbare instrumente bord</denumire>
</operatie>
</operatii>
</comanda>
</comenzi>