# PRD 3.1 — Creare cont nou **Stare**: verify-pass > Proces complet: `docs/ROADMAP.md` §5. Contract RAR (sursa de adevar): `docs/api-rar-contract.md`. > Starea trece: `draft → aprobat → in-executie → verify-pass → inchis` (actualizata de lead). ## 1. Obiectiv Inlocuieste crearea conturilor prin `INSERT` SQL manual cu un tool admin dedicat (`tools/account.py`), simetric cu `tools/apikey.py`. Este **fundatia Etapei 3**: 3.2 (filtrare GET pe cont) si 3.3 (self-onboarding web) au nevoie de o cale curata si testata de a materializa conturi. Pastram filozofia din `app/auth.py`: lifecycle-ul de admin traieste in CLI pe masina gateway, **fara suprafata HTTP de admin**. ## 2. Non-Goals (anti scope-creep) - **Fara endpoint HTTP** de creare cont in 3.1. (Nota: la poarta de aprobare utilizatorul a cerut o **interfata web de admin** pentru activarea conturilor — vezi 3.3; aceasta inverseaza stanta initiala "fara suprafata HTTP de admin". Admin web traieste in 3.3, nu aici. CLI-ul 3.1 ramane pentru bootstrap/suport.) - **Fara autentificare de utilizator** (email/parola, sesiuni) — apartine 3.3. Aici contul are doar `name` + `cui`, ca azi. - **Fara setare creds RAR** in acest tool — exista deja `POST /v1/conturi/rar-creds` (T1). - **Fara coloane de user/parola** — identitatea de login (email/parola) vine in 3.3. Aici adaugam doar `accounts.active` (lifecycle de cont), de care depinde gate-ul „cont in asteptare" din 3.3. - **Fara stergere cont** — momentan nu e nevoie; conturile sunt putine si durabile. (Dezactivarea exista, via `active`.) ## 3. Stories atomice ### US-001: Coloana `accounts.active` + helper-e cont in `app/accounts.py` **Ca** dezvoltator **vreau** functii pure de creare/listare/(de)activare cont **pentru ca** atat CLI-ul (3.1) cat si fluxul web (3.3) sa materializeze si sa activeze conturi prin aceeasi cale testata. - **Depinde de**: — - **Fisiere**: `app/schema.sql` (`accounts.active`), `app/db.py` (migrare `_migrate`), `app/accounts.py` (nou), `tests/test_accounts.py` (nou) (~4 fisiere) - **Test intai (RED)**: `tests/test_accounts.py` — `test_create_account_returneaza_id`, `test_create_account_activ_implicit`, `test_create_account_name_gol_ridica_eroare`, `test_create_account_cui_duplicat_respins`, `test_set_active_comuta`, `test_list_accounts_ordonat` - **Acceptance criteria**: - [ ] `accounts.active INTEGER NOT NULL DEFAULT 1` adaugata in `schema.sql` + migrata idempotent in `_migrate` (conturi existente raman active; default id=1 activ). - [ ] `create_account(conn, name, cui=None, active=True) -> int` insereaza si intoarce `id`-ul nou. - [ ] `name` gol/whitespace → `ValueError` (nu insereaza). - [ ] `cui` ne-nul duplicat → `ValueError` (un CUI = un cont — decizie confirmata §5). `cui=None` se accepta multiplu (conturi fara CUI, ex. default). - [ ] `set_active(conn, account_id, active: bool)` comuta starea; cont inexistent → `ValueError`. - [ ] `list_accounts(conn) -> list[dict]` intoarce `id, name, cui, active, created_at`, ordonat dupa `id`, **fara** `rar_creds_enc`. - **Verificare E2E**: n/a (helper pur) — acoperit de teste unitare. ### US-002: CLI `tools/account.py` (create/list/activate/deactivate) **Ca** admin gateway **vreau** `python -m tools.account create|list|activate|deactivate` **pentru ca** sa onboardez si sa **activez** un client fara SQL manual, optional emitand prima cheie API intr-un pas. - **Depinde de**: US-001 - **Fisiere**: `tools/account.py` (nou), `tests/test_tools_account.py` (nou) (~2 fisiere) - **Test intai (RED)**: `tests/test_tools_account.py` — `test_create_afiseaza_id`, `test_create_with_key_emite_cheie`, `test_create_cui_duplicat_exit_2`, `test_activate_comuta_starea`, `test_list_afiseaza_activ` - **Acceptance criteria**: - [ ] `create --name "Service X" [--cui RO123] [--inactive]` creeaza contul (implicit activ) si tipareste `id`-ul; `--inactive` creeaza cont in asteptare. - [ ] `--with-key` emite si o cheie API (`app.auth.create_api_key`), afisata **o singura data** (reuseaza pattern-ul din `tools/apikey.py`). - [ ] `activate --id N` / `deactivate --id N` comuta `active` (mesaj de confirmare). - [ ] `name` gol, `cui` duplicat sau `id` inexistent → mesaj la stderr + exit code 2. - [ ] `list` tipareste tabelul `id | name | cui | activ | created_at`. - [ ] `init_db()` apelat la start (asigura schema + migrare `active`), ca in `tools/apikey.py`. - **Verificare E2E**: `python -m tools.account create --name "Test SRL" --cui RO99 --inactive --with-key` → cont inactiv + cheie; `python -m tools.account activate --id ` → `list` arata `activ=da`. ## 4. Riscuri - **Unicitate CUI** — daca un client are mai multe puncte de lucru sub acelasi CUI, constrangerea blocheaza al doilea cont. Mitigare: o aplicam la nivel de helper (nu schema UNIQUE) ca sa fie usor de relaxat; decizia finala in §5. - **Coliziune cu cont default (id=1)** — `INSERT OR IGNORE ... id=1 'default'` exista in schema. `create_account` foloseste AUTOINCREMENT, deci nu atinge id=1. Test: primul cont creat are id≥2. ## 5. Intrebari deschise (REZOLVATE la poarta de aprobare) - **CUI unic** — REZOLVAT: unic cand e prezent (un CUI = un cont), `NULL` permis multiplu. - **`--with-key`** — REZOLVAT: optional (flag), emiterea cheii e o decizie constienta. - **Coloana `active`** — adaugata aici (lifecycle de cont) pentru ca 3.3 sa creeze conturi „in asteptare" (`active=0`) si gate-ul de trimitere sa le opreasca pana la activarea de catre admin. ## 6. Valuri de executie (graful de dependente) ``` Val 1: [US-001] ← schema active + helper, fisiere (cvasi-)noi → fara dependente Val 2: [US-002] ← deblocat de US-001 (CLI peste helper) ``` --- ## Addendum review (autoplan, `[subagent-only]` — Codex indisponibil: usage limit) > Doua voci Claude independente (Eng + Produs/DX). Schimbarile de mai jos sunt **obligatorii la > executie** (auto-decise prin principiile autoplan: completitudine, explicit, DRY). Convergenta > mare intre voci pe primele doua. **A1 [HIGH, ambele voci] — Unicitatea CUI prin index partial, NU check in helper.** Check-ul `SELECT cui → ValueError → INSERT` are o fereastra de coliziune (doi `create_account` concurenti, relevant cand 3.3 reuseaza helperul din web). Fix: in `schema.sql` + `_migrate` `CREATE UNIQUE INDEX IF NOT EXISTS ux_accounts_cui ON accounts(cui) WHERE cui IS NOT NULL` (SQLite trateaza NULL ca distincte nativ → `cui=NULL` multiplu merge gratis). Helperul **normalizeaza** (`trim` + `upper`) si prinde `IntegrityError → ValueError` pentru mesaj prietenos. Inlocuieste AC "cui duplicat" + adauga `test_create_cui_null_multiplu_permis`, `test_create_cui_normalizat`. **A2 [HIGH, ambele voci] — `accounts.active` este INERT pana la 3.3.** Nimic nu citeste `active` azi (`resolve_account_id` verifica doar `api_keys.active`; worker-ul nu se uita la cont). Pana la 3.3/US-008 (gate worker), `deactivate` NU opreste trimiterile. Documenteaza-l explicit ca lifecycle flag consumat de 3.3; nu lasa asteptarea falsa ca dezactiveaza trimiterile. (Decizia keep-in-3.1 vs move-to-3.3 = taste, vezi poarta.) **A3 [MEDIUM, Produs] — Flag consistent cu `tools/apikey.py`:** `activate`/`deactivate` folosesc `--account N` (nu `--id N`), aliniat cu sora. `create` pastreaza `--name`/`--cui`. **A4 [MEDIUM, ambele] — Mesaje de eroare cu cauza+fix.** CUI duplicat → numeste contul existent: `eroare: CUI RO123 e deja folosit de contul 4 (foloseste 'activate --account 4' sau alt CUI)`. Specifica textul ca tinta de test. **A5 [LOW→MEDIUM] — `--with-key` atomic:** `create_account` + `create_api_key` in aceeasi tranzactie (`BEGIN IMMEDIATE` → ambele → COMMIT; pe esec ROLLBACK). DB ruleaza autocommit (`isolation_level=None`) — tranzactia trebuie explicita. Pe esec emitere cheie: mesaj clar ca contul A fost creat. **A6 [MEDIUM, Produs — pentru 3.3] — adauga comenzi CLI ceruta de 3.3:** - `list --pending` (filtreaza `active=0`) — adminul descopera conturile de activat (3.3 nu are notificare; vezi addendum 3.3). - `set-password --account N` — scapare de reset parola pentru 3.3 (Non-Goal SMTP), altfel "reset prin admin" e o promisiune fara implementare. (Implementarea hash-ului = `app/users.py` din 3.3; comanda poate astepta US din 3.3, dar planific-o aici ca sa nu se piarda.) **A7 [MEDIUM] — Teste RED lipsa de adaugat:** `cui=None` multiplu (A1), `list` fara `rar_creds_enc`, `set_active` idempotent (set activ pe activ nu arunca), `create_account(active=False)` la nivel de helper, primul cont creat are `id>=2` (nu atinge default id=1). **A8 [LOW] — Obiectiv corectat:** 3.1 e fundatie pentru **3.3** (care reuseaza `create_account` + `active`); pentru 3.2 e doar suport de VERIFY (al doilea cont). 3.2 NU materializeaza conturi. **Deferat (P3, in afara scope-ului imediat — noteaza in ROADMAP/TODO):** `rename`/`set-cui` (corectie typo fara SQL manual), `--if-not-exists` (provisioning idempotent). Regret probabil, dar nu blocheaza livrabila. ## Raport VERIFY > Executat 2026-06-17. Suita completa: **299 passed** (20 teste noi: 12 helper + 8 CLI). **US-001 — PASS** - [x] `accounts.active INTEGER NOT NULL DEFAULT 1` in `schema.sql` + migrat idempotent in `_migrate` (conturi existente raman active; default id=1 activ). Dovada: `test_foundation` + migrare ALTER. - [x] `create_account(conn, name, cui=None, active=True) -> int` insereaza + intoarce id. `test_create_account_returneaza_id`. - [x] `name` gol/whitespace → `ValueError`, nu insereaza. `test_create_account_name_gol_ridica_eroare`. - [x] `cui` duplicat → `ValueError` (cauza+fix, numeste contul); `cui=None` multiplu OK. `test_create_account_cui_duplicat_respins`, `test_create_cui_null_multiplu_permis`. - [x] `set_active` comuta; inexistent → `ValueError`; idempotent. `test_set_active_*`. - [x] `list_accounts` → `id,name,cui,active,created_at`, ordonat, FARA `rar_creds_enc`. `test_list_accounts_ordonat_fara_creds`. **US-002 — PASS** (E2E PRD reprodus: `create --inactive --with-key` → id=2 + cheie; `activate` → list `activ=da`) - [x] `create --name [--cui] [--inactive]` creeaza (implicit activ), tipareste id. `test_create_afiseaza_id`. - [x] `--with-key` emite cheie afisata o data, atomic (rollback pe esec). `test_create_with_key_emite_cheie`, `test_with_key_atomic_pe_cui_duplicat`. - [x] `activate`/`deactivate --account N` comuta. `test_activate_comuta_starea`. - [x] erori → stderr + exit 2. `test_create_cui_duplicat_exit_2`, `test_activate_inexistent_exit_2`. - [x] `list` tipareste tabelul; `list --pending` filtreaza. `test_list_afiseaza_activ`, `test_list_pending_filtreaza`. - [x] `init_db()` la start. **Addendum** — A1 (index unic partial `ux_accounts_cui` + normalizare trim/upper, `test_create_cui_normalizat`), A3 (`--account` pe activate/deactivate), A4 (mesaj cauza+fix), A5 (`--with-key` atomic), A6 (`list --pending`; `set-password` deferat la 3.3), A7 (teste RED suplimentare) — toate aplicate. A2 (`active` inert pana la 3.3) documentat in `app/accounts.py` + `tools/account.py`.