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:
72
.gitignore
vendored
Normal file
72
.gitignore
vendored
Normal 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
|
||||||
51
Document informativ RAR- Autopass.txt
Normal file
51
Document informativ RAR- Autopass.txt
Normal 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.
|
||||||
|
|
||||||
413
Documentatie Serviciu AutoPass_Final.txt
Normal file
413
Documentatie Serviciu AutoPass_Final.txt
Normal 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
97
docs/CONTEXT.md
Normal 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.
|
||||||
271
docs/plans/plan-design-review.md
Normal file
271
docs/plans/plan-design-review.md
Normal 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.000–2.000 lei; manipulare odometru până la 5.000 lei / penal.
|
||||||
|
|
||||||
|
Implică: **toți** clienții ROA + **mii de service-uri non-ROA** au aceeași obligație → există piață dincolo
|
||||||
|
de ROA pentru un canal de import (xlsx/csv) ulterior.
|
||||||
|
|
||||||
|
## Status Quo
|
||||||
|
|
||||||
|
Integrare VFP funcțională dar nedistribuită: `RarAutoPass` (`rar_autopass.prg`) vorbește direct cu RAR prin
|
||||||
|
`MSXML2.ServerXMLHTTP`; mapare în DBF (`mapare_prestatii`), nomenclator în `prestatii_rar`, jurnal în
|
||||||
|
`rar_log`; UI desktop cu tab-uri. Credențiale RAR în `settings.xml` pe fiecare stație (text clar).
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
|
||||||
|
- Stack: **Python / FastAPI** (ales). Hosting: **hibrid** — instanță centrală always-on operată de ROA +
|
||||||
|
Proxmox LXC pentru dev/staging. `romfast.ro`/hosting.com (doar PHP) nu găzduiește core-ul.
|
||||||
|
- Open-source pe **github.com/romfast** (licență recomandată **AGPL-3.0**).
|
||||||
|
- ROAAUTO rămâne client subțire (refolosim pattern-ul `MSXML2.ServerXMLHTTP`).
|
||||||
|
|
||||||
|
## Premises (confirmate)
|
||||||
|
|
||||||
|
1. Migrarea în web se justifică prin nevoia ISV: deploy central, fără redistribuire de exe-uri. ✅
|
||||||
|
2. Cererea e reală și legală (L.142/2023) — clienți ROA + service-uri non-ROA. ✅
|
||||||
|
3. Maparea operație→`codPrestatie` e în **core** (API-ul cere `prestatii` în `postPrezentare`). ✅ (corectat de utilizator pe baza spec-ului)
|
||||||
|
4. ROAAUTO = client subțire; mapare + retry + jurnal pe server. ✅
|
||||||
|
5. **Topologie: gateway central, pass-through credențiale, ZERO stocare de parole.** ✅
|
||||||
|
|
||||||
|
## Contract API RAR AUTOPASS (din spec oficial v0.0.1, baza `/rar-autopass`)
|
||||||
|
|
||||||
|
- Auth: `POST /public/login` {email, password} → `GetUtilizatoriDTO{ token, idUser, ... }`.
|
||||||
|
Token JWT atașat ca `Authorization: Bearer {token}` la toate apelurile securizate.
|
||||||
|
- Nomenclator: `GET /nomenclator/getNomenclatorPrestatii` → listă `{codPrestatie, numePrestatie}`;
|
||||||
|
`GET /nomenclator/getPrestatieByCodPrestatie/{cod}`.
|
||||||
|
- Prezentări: `POST /prezentari/postPrezentare` (payload `Prezentari`); `GET /prezentari/getAllPrezentari`;
|
||||||
|
`GET /prezentari/getPrezentare/{id}`; `PATCH /prezentari/markPrezentareAnulataById/{id}`;
|
||||||
|
`PATCH /prezentari/patchPrezentare/{id}`. Răspuns: `CustomResponseForMapping{ data, message, statusCode }`.
|
||||||
|
- Payload `Prezentari`: `vin`, `nrInmatriculare`, `dataPrestatie(date)`, `odometruInitial`, `odometruFinal`,
|
||||||
|
`obs`, `b64Image`, `sistemReparat`, `tipPrestatie`, `status`∈{SALVATA,FINALIZATA,ANULATA,UNDEFINED},
|
||||||
|
`prestatii: [{codPrestatie, idPrezentare}]`.
|
||||||
|
- Cont/roluri (per agent economic): ADMIN creează CLIENT/ADMIN_CLIENT. **Nu replicăm** asta în gateway.
|
||||||
|
|
||||||
|
## Recommended Approach — B: gateway central + coadă + token JWT scurt
|
||||||
|
|
||||||
|
### Flux
|
||||||
|
|
||||||
|
```
|
||||||
|
ROAAUTO (VFP) ──POST /v1/prezentari {comanda + RAR creds + idempotency_key}──▶ Gateway FastAPI
|
||||||
|
(citește creds din Oracle clientului) │
|
||||||
|
├─ rezolvă maparea op→codPrestatie
|
||||||
|
├─ INSERT submission (PII TRANZITORIU, status=queued)
|
||||||
|
◀── răspuns imediat: {submissionId, status:queued|needs_mapping} ───────────┘ (dedup pe idempotency_key)
|
||||||
|
Worker (daemon/task fundal, poll SQLite) ── login RAR → JWT ──▶ postPrezentare ──▶ RAR
|
||||||
|
│ retry cu backoff în fereastra JWT
|
||||||
|
└─ la succes: PURJEAZĂ PII, reține doar hash+status+idPrezentare
|
||||||
|
Browser ──▶ Dashboard ── monitorizare CITITĂ LIVE din RAR (getAllPrezentari), nu din PII local ── + mapări, nomenclator
|
||||||
|
```
|
||||||
|
|
||||||
|
### Gestiunea credențialelor (cheia deciziei #5)
|
||||||
|
|
||||||
|
- ROAAUTO trimite `email`+`password` RAR la fiecare apel (din Oracle-ul clientului, peste **HTTPS**).
|
||||||
|
Creds-urile trăiesc **doar în itemul de coadă** (în memorie/rândul de lucru), folosite de worker pentru `login`,
|
||||||
|
apoi **șterse**. Parola **nu se persistă** și se **scrubează** din loguri ȘI din capturile de excepție/APM.
|
||||||
|
- Worker-ul face `login` (nu API-ul) → POST-ul răspunde imediat fără latența RAR. Worker: login → JWT → postPrezentare,
|
||||||
|
retry cu backoff **în fereastra JWT-ului**.
|
||||||
|
- **Onestitate despre robustețe:** coada NU aduce reziliență la indisponibilitate RAR de durată — JWT-ul e scurt și
|
||||||
|
nu ținem parola ca să reluăm peste expirare. Ce aduce coada: răspuns asincron rapid pentru ROAAUTO + jurnal central +
|
||||||
|
retry pe erori tranzitorii scurte. **Durabilitatea reală pe pene lungi stă în ROAAUTO**, prin job-ul periodic de
|
||||||
|
**re-push** al submission-urilor rămase `error/pending` (retrimite cu creds proaspete). Coada acoperă minutele,
|
||||||
|
ROAAUTO acoperă orele. (Dacă măsurarea TTL-ului arată JWT lung, reevaluezi — vezi „The Assignment".)
|
||||||
|
|
||||||
|
### Idempotență (critic — record legal, fără dubluri)
|
||||||
|
|
||||||
|
`postPrezentare` NU e idempotent, iar avem două bucle de retry (worker + re-push ROAAUTO) → risc de **prezentări
|
||||||
|
duplicate la RAR** pentru un record urmărit legal. Soluție:
|
||||||
|
- ROAAUTO trimite un `idempotency_key` = hash(cont + VIN + dataPrestatie + set(codPrestatie)).
|
||||||
|
- Gateway: `UNIQUE(idempotency_key)` pe `submissions`. Re-trimiterea aceleiași chei NU creează submission nou.
|
||||||
|
- Worker, înainte de a retrimite: dacă submission-ul are deja `idPrezentare` (răspuns RAR) → marchează `sent`, nu reapelează.
|
||||||
|
|
||||||
|
### Mașina de stări a unui submission
|
||||||
|
|
||||||
|
`queued → sending → { sent | needs_mapping | error }`
|
||||||
|
- `needs_mapping`: operație fără `codPrestatie` mapat → **se ține gateway-side, NU se trimite incomplet** (API-ul cere
|
||||||
|
`prestatii`); după ce mapezi, trece în `queued`. (≠ VFP-ul de azi care o arunca silențios.)
|
||||||
|
- `error`: eligibil pentru re-push din ROAAUTO (`GET /v1/prezentari?status=error`).
|
||||||
|
- `sent`: are `idPrezentare` de la RAR; terminal.
|
||||||
|
|
||||||
|
### Privacy-first / stateless (mentenanță & răspundere minime)
|
||||||
|
|
||||||
|
Decizie: gateway-ul e **pur tranzit + interfață cu RAR**, NU depozit de date.
|
||||||
|
- PII-ul prezentării (VIN, km, date) trăiește în SQLite **doar tranzitoriu** cât e în coadă; **la `sent` se purjează**,
|
||||||
|
rămân doar `idempotency_key` (hash ireversibil) + status + `idPrezentare`. Nu se poate reconstrui VIN-ul din hash.
|
||||||
|
- **Monitorizarea se citește LIVE din RAR** (`getAllPrezentari`/`getPrezentare`) — RAR e sursa de adevăr, nu un jurnal local.
|
||||||
|
- Durabilitatea pe pene lungi stă **la margine** (Oracle ROAAUTO / fișierul încărcat), nu la tine → re-push din client.
|
||||||
|
- **Fără agregare de date.** Datele service-urilor NU se folosesc pentru alte produse. (Eventual, în viitor, doar
|
||||||
|
produs separat cu **opt-in explicit + anonimizare**, lawyered — niciodată default.) Privacy = argument de adopție, nu doar conformitate.
|
||||||
|
|
||||||
|
### Componente (un repo, `docker compose up`)
|
||||||
|
|
||||||
|
1. **API (`app/api/v1`)** — FastAPI:
|
||||||
|
- `POST /v1/prezentari` (una/mai multe) → validare Pydantic, enqueue, răspuns cu `submissionId`.
|
||||||
|
- `GET /v1/prezentari?status=&data=` și `/{id}` — monitorizare programatică pentru ROAAUTO + re-push.
|
||||||
|
- `GET /v1/nomenclator`, `POST /v1/nomenclator/refresh`.
|
||||||
|
- `GET/PUT /v1/mapari` — CRUD mapare per cont.
|
||||||
|
- `PATCH /v1/prezentari/{id}/anulare`, `/corectie` — proxy peste markPrezentareAnulataById / patchPrezentare.
|
||||||
|
- Auth gateway: **API key per cont ROA** (separată de credențialele RAR ale clientului); cu emitere/rotire/revocare.
|
||||||
|
- *(Amânat, NU în v1: `POST /v1/import` xlsx/csv — strat 2 / piață non-ROA.)*
|
||||||
|
2. **Client RAR (`app/rar_client.py`)** — portare din `rar_autopass.prg`: login+JWT, getNomenclatorPrestatii,
|
||||||
|
postPrezentare, getAllPrezentari, getPrezentare, markPrezentareAnulataById, patchPrezentare. `httpx` + retry/backoff.
|
||||||
|
3. **Worker (daemon / task de fundal) + coadă pe SQLite (`app/worker`)** — proces pornit non-stop (sau task
|
||||||
|
`asyncio` în aplicația FastAPI), sub Docker `restart: always`. Buclă: ia rândurile `status='queued'`,
|
||||||
|
revendică atomic (`BEGIN IMMEDIATE; UPDATE … SET status='sending' WHERE id=? AND status='queued'`),
|
||||||
|
login RAR, trimite, retry cu backoff, scrie status + `idPrezentare`. Reacție **instant**, fără întârziere.
|
||||||
|
*Fără Redis, fără arq, fără Postgres.* ROAAUTO oferă durabilitatea pe pene lungi (re-push).
|
||||||
|
Atenție `b64Image`: poate fi mare → stocat ca BLOB sau path pe disc, nu în RAM.
|
||||||
|
4. **Dashboard (`app/web`)** — **Jinja2 + HTMX** (server-rendered, zero build): Monitorizare **citită live din RAR**
|
||||||
|
(`getAllPrezentari`) + starea cozii curente (din `submissions`), Editor mapări, Browser nomenclator. API-first.
|
||||||
|
5. **SQLite** (mod WAL) — înlocuiește DBF, un singur fișier `.db`:
|
||||||
|
- `accounts`, `api_keys` (conturi ROA + chei gateway).
|
||||||
|
- `operations_mapping` (cod_op_service → codPrestatie, `auto_send`=trimite automat dacă e mapat) ← `mapare_prestatii`.
|
||||||
|
- `nomenclator_rar` (cache {codPrestatie, numePrestatie}) ← `prestatii_rar`.
|
||||||
|
- `submissions` (coadă + dedup): `idempotency_key` UNIQUE, status, statusCode RAR, eroare, `idPrezentare`, retry, timestamps.
|
||||||
|
Câmpurile PII (vin, km, dataPrestatie, prestatii) sunt **tranzitorii** — populate cât e `queued/sending`,
|
||||||
|
**purjate la `sent`**. (≠ `rar_log` care era jurnal permanent.)
|
||||||
|
- **Notă: niciun câmp pentru parole RAR; niciun PII reținut după trimitere.** (`import_jobs` — doar la xlsx/csv, amânat.)
|
||||||
|
|
||||||
|
### Client ROAAUTO (VFP) — refactor minim
|
||||||
|
|
||||||
|
- `settings.xml` păstrează doar **URL gateway + API key** (nu mai ține mapări/nomenclator).
|
||||||
|
- Credențialele RAR ale clientului se citesc din **Oracle (ROA)** și se trimit în payload la gateway.
|
||||||
|
- `export_comenzi.prg` rămâne (citește `comenzi_service`/`operatii`), dar construiește JSON și face
|
||||||
|
`POST /v1/prezentari` în loc de XML + apel direct RAR.
|
||||||
|
- Dispar din VFP: `Login`, `UpdateNomenclator`, `GetCodRarPentruOperatie`, maparea, `rar_log` → trec în web.
|
||||||
|
- Se adaugă un job periodic „re-push pending" (timer existent din `rar-forms.prg` se poate reutiliza).
|
||||||
|
|
||||||
|
## Approaches Considered
|
||||||
|
|
||||||
|
- **A — sincron, fără coadă** (S, risc mic): ships rapid, dar fără retry autonom. Bun ca prim pas, respins ca țintă.
|
||||||
|
- **B — coadă + token JWT scurt** (M, recomandat ✅): robust la indisponibilitate RAR, nu pierzi prestații obligatorii legal.
|
||||||
|
- **C — outbox în Oracle** (M/L): cel mai decuplat, dar cere acces gateway→Oracle client (VPN/rețea). Reținut ca opțiune pentru clienți non-ROA / viitor.
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
1. Durata reală a JWT-ului RAR (decide fereastra de retry autonom). De măsurat pe endpoint-ul de test.
|
||||||
|
2. `sistemReparat` / `tipPrestatie` — valori acceptate (enum nedocumentat în spec). De clarificat cu RAR.
|
||||||
|
3. Modelul de cont RAR per client: un singur user RAR per agent economic sau mai mulți (afectează cum mapezi `idUser`).
|
||||||
|
4. Monetizare/direcție (nedecisă): vezi mai jos — de reluat după ce A→B merge la primul client.
|
||||||
|
|
||||||
|
## Arhitectura în mare (modelul mental)
|
||||||
|
|
||||||
|
Azi VFP face 3 treburi pe FIECARE PC client: (a) ia comanda, (b) mapează + se loghează la RAR, (c) ține jurnalul →
|
||||||
|
de-aia trebuie redistribuit la fiecare corecție. Migrarea = muți (b)+(c) pe un server central al tău:
|
||||||
|
- **ROAAUTO (VFP, la client) = expeditorul** — citește comanda + creds RAR din Oracle și le trimite la gateway. Atât.
|
||||||
|
- **Gateway (Python, central) = creierul** — primește, mapează, login RAR, trimite, retry, jurnal. Aici faci corecțiile o dată, pentru toți.
|
||||||
|
- **Dashboard web = panoul de control** — vezi ce s-a trimis/eșuat, editezi maparea.
|
||||||
|
|
||||||
|
## Opțiuni de deploy (unde rulează gateway-ul)
|
||||||
|
|
||||||
|
| Opțiune | Cum | Cost | Când |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **LXC Proxmox + Cloudflare Tunnel** | container la birou, expus public HTTPS fără IP static / porturi deschise | 0 € | **Start + teste** (risc: netul/curentul biroului) |
|
||||||
|
| **VPS mic always-on** (ex. Hetzner) | același container, mașină care nu cade | ~5 €/lună | **Clienți reali / producție** (recomandat) |
|
||||||
|
| romfast.ro / hosting.com | Python via cPanel/Passenger (WSGI) | inclus | ⚠️ FastAPI e ASGI + worker-ul e daemon → shared hosting nepotrivit (shim fragil, fără procese persistente). Doar landing |
|
||||||
|
|
||||||
|
Recomandare: start pe **LXC + Cloudflare Tunnel** (cost 0), mutare pe **VPS** la clienți. Mutarea = copiezi containerul + fișierul `.db`.
|
||||||
|
romfast.ro rămâne doar pentru landing/prezentare, nu țintă de producție (ASGI + worker daemon nu merg pe shared hosting).
|
||||||
|
|
||||||
|
- **Cod:** open-source pe github.com/romfast, **AGPL-3.0**. Deploy = **un container** cu FastAPI (uvicorn) + worker-ul ca task de fundal/al doilea proces, sub Docker `restart: always` + un volum SQLite.
|
||||||
|
- **Dev/staging:** LXC Proxmox. **Migrare date:** `tools/import_dbf.py` (`mapare_prestatii.DBF` + `prestatii_rar.DBF` cu `dbfread`).
|
||||||
|
|
||||||
|
## Teza de produs & direcție SaaS
|
||||||
|
|
||||||
|
**Teza:** *cel mai ușor mod de a băga operațiile de service în AUTOPASS* — din ROAAUTO (API), din alte aplicații,
|
||||||
|
sau din fișiere — în loc de tastarea manuală din interfața oficială. Câștigi prin **efort minim cerut service-ului**,
|
||||||
|
nu prin features. (Funnel-ul „read public gratuit" — RESPINS: nimeni nu vrea să *citească* nomenclatorul; valoarea
|
||||||
|
e 100% în **trimitere**.)
|
||||||
|
|
||||||
|
**Lecția GTM de la demoanaf.ro:** a devenit viral nu prin model plătit, ci oferind o variantă **reimaginată, simplă,
|
||||||
|
plăcută** a unui serviciu oficial greoi/instabil; s-a răspândit în grupurile de Facebook. Echivalentul tău: o cale
|
||||||
|
**dramatic mai simplă decât tastarea manuală AUTOPASS**, construită rapid cu AI dar pe arhitectură solidă.
|
||||||
|
|
||||||
|
**Wedge validat:** automatizezi tastarea manuală AUTOPASS, **începând cu clientul care a cerut-o** (cale ROAAUTO/API).
|
||||||
|
|
||||||
|
**Trepte (același motor — mapare + coadă + trimitere + monitorizare — fără rescriere):**
|
||||||
|
- **Treapta 1 (acum):** core-ul pentru clientul care a cerut + clienții ROA, prin ROAAUTO. Îți rezolvi nevoia.
|
||||||
|
- **Treapta 2 (non-ROA, web upload):** import xlsx/csv cu **mapare reținută** (vezi mai jos) + dashboard. Login web,
|
||||||
|
fără instalare. **Primul venit** — freemium **pe volum** (gratis sub N prezentări/lună pt. service mici, plată peste;
|
||||||
|
metrica de preț = prezentări/lună = fix unitatea obligatorie legal).
|
||||||
|
- **Treapta 3 (diferențiere):** integrări mai adânci + sugestii AI de mapare (eventual conector MCP).
|
||||||
|
|
||||||
|
**Moat:** (1) mapările reținute per service cresc costul de plecare; (2) lățimea integrării (mergi indiferent ce
|
||||||
|
software are service-ul); (3) fiabilitatea de conformitate (retry, monitorizare din RAR — nu pierzi o declarație legală);
|
||||||
|
(4) **privacy** (nu reținem datele lor) — el însuși argument de adopție. Saltul fără rescriere: API key + mapare per cont,
|
||||||
|
**zero parole stocate, zero PII reținut**.
|
||||||
|
|
||||||
|
### Adopție în masă & praguri (calibrat pe cifre reale)
|
||||||
|
|
||||||
|
Declanșatorul trecerii de la tastarea manuală în RAR la upload = **timp salvat / efort de trecere**:
|
||||||
|
~2-4 min/prezentare manual × volum lunar. Clienți reali cunoscuți: **60-80** și **80-100 prezentări/lună** →
|
||||||
|
**3-6 ore/lună** de tastare = durerea care îi convertește.
|
||||||
|
- Prima folosire trebuie trivială (upload Excel existent → mapare reținută → trimite, sub 5 min).
|
||||||
|
- Gratis la volumul lor = fără decizie de achiziție, doar încearcă.
|
||||||
|
- **Freemium pe volum:** gratis **~30-40 prezentări/lună** (service mici = bază virală, cost ~0 la tine);
|
||||||
|
plată peste prag (banda 1 ≈ 50-150/lună prinde fix clienții actuali — primii bani de la cine a cerut serviciul).
|
||||||
|
Metrica de preț = prezentări/lună = unitatea obligatorie legal.
|
||||||
|
- Ironie de reținut: service-urile mici au cea mai mică durere (greu de convins, dar virale); volumele mari au cea
|
||||||
|
mai mare durere (cei mai dispuși să plătească). Gratis = achiziție; venit = volume mari.
|
||||||
|
|
||||||
|
## Import xlsx/csv — UX (stratul SaaS, treapta 2)
|
||||||
|
|
||||||
|
Două straturi de mapare, **ambele reținute per cont** (cheia produsului — „map once, reuse forever"):
|
||||||
|
1. **Mapare coloane** (schema fișierului → câmpuri canonice): ex. „«Serie șasiu»→VIN, «Index km»→odometruFinal".
|
||||||
|
Reluată automat dacă headerele se repetă.
|
||||||
|
2. **Mapare operații** (etichetele/codurile service-ului → `codPrestatie` AUTOPASS), cu **sugestie fuzzy** pe denumire.
|
||||||
|
|
||||||
|
Flux: upload → recunoaște coloanele (reia maparea) → propune maparea operațiilor (reținută + sugestii) →
|
||||||
|
**preview** (ce se trimite, rânduri nemapate flag-uite) → „Trimite la RAR" → monitorizare. A 2-a oară: upload → preview → trimite.
|
||||||
|
|
||||||
|
**Spectru de integrare (același backend):** API (POST prezentări, ca ROAAUTO) → drop fișier programat (folder/SFTP/email-to-import) → upload manual în browser (zero instalare). Cine poate, integrează API; cine nu, dă fișier.
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
- O prezentare reală trimisă din ROAAUTO prin gateway apare `FINALIZATA` la RAR (test), vizibilă în dashboard.
|
||||||
|
- Paritate cu VFP: același `codPrestatie` rezultat din mapare pe aceleași comenzi.
|
||||||
|
- Reziliență: RAR indisponibil → submission `queued/error` cu retry, ROAAUTO nu se blochează; re-push recuperează.
|
||||||
|
- Securitate: niciun credențial RAR în client/`settings.xml` și niciun câmp de parolă în SQLite.
|
||||||
|
- Privacy: după `sent`, în SQLite nu rămâne PII de vehicul (doar hash+status+idPrezentare); monitorizarea vine din RAR.
|
||||||
|
|
||||||
|
## The Assignment (următorul pas concret)
|
||||||
|
|
||||||
|
Pe endpoint-ul de **test RAR**, măsoară **durata de viață a JWT-ului** întors de `/public/login` (fă un login,
|
||||||
|
apoi `postPrezentare` la intervale crescătoare până la 401). Numărul ăsta dimensionează fereastra de retry
|
||||||
|
autonom din worker și decide dacă ai nevoie de job-ul de re-push în ROAAUTO. E o oră de muncă și deblochează
|
||||||
|
toată decizia de robustețe din B.
|
||||||
|
|
||||||
|
## What I noticed about how you think
|
||||||
|
|
||||||
|
- Ai corectat insight-ul meu „wedge = doar VIN+km" cu „API-ul cere și operațiile de manopelă" — și aveai
|
||||||
|
dreptate, ai citit contractul, nu l-ai presupus. Asta e exact instinctul care face diferența: sursa, nu pitch-ul.
|
||||||
|
- Ai pus singur problema de custodie a parolelor („nu vreau să salvez parole") înainte să ți-o ridic eu —
|
||||||
|
gândești în termeni de răspundere, nu doar de „merge/nu merge".
|
||||||
|
- Vezi proiectul ca ISV, nu ca utilizator final: „nu vreau să redistribui la fiecare corecție" e fix
|
||||||
|
raționamentul care justifică web-ul. Mulți ar fi rescris fără să poată articula de ce.
|
||||||
|
```
|
||||||
241
docs/plans/plan-eng-review.md
Normal file
241
docs/plans/plan-eng-review.md
Normal 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
83
export_comenzi.prg
Normal 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
BIN
mapare_prestatii.CDX
Normal file
Binary file not shown.
BIN
mapare_prestatii.DBF
Normal file
BIN
mapare_prestatii.DBF
Normal file
Binary file not shown.
BIN
mapare_prestatii.FPT
Normal file
BIN
mapare_prestatii.FPT
Normal file
Binary file not shown.
775
nfjsonread.prg
Normal file
775
nfjsonread.prg
Normal 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
BIN
prestatii_rar.CDX
Normal file
Binary file not shown.
BIN
prestatii_rar.DBF
Normal file
BIN
prestatii_rar.DBF
Normal file
Binary file not shown.
1296
rar-forms.prg
Normal file
1296
rar-forms.prg
Normal file
File diff suppressed because it is too large
Load Diff
271
rar_advanced.prg
Normal file
271
rar_advanced.prg
Normal 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
55
rar_automate.prg
Normal 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
BIN
rar_autopass.PJT
Normal file
Binary file not shown.
BIN
rar_autopass.pjx
Normal file
BIN
rar_autopass.pjx
Normal file
Binary file not shown.
611
rar_autopass.prg
Normal file
611
rar_autopass.prg
Normal 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
22
settings.xml.example
Normal 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
58
test-comenzi.xml
Normal 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>
|
||||||
Reference in New Issue
Block a user