Files
rar-autopass/docs/prd/prd-3.1-creare-cont.md
Claude Agent 1c5b0cbc18 feat(account): CLI lifecycle conturi + accounts.active (PRD 3.1)
Inlocuieste crearea conturilor prin INSERT SQL manual cu un tool admin
dedicat, simetric cu tools/apikey.py. Fundatia Etapei 3 (3.2/3.3).

- app/accounts.py: create_account/set_active/list_accounts (helper pur,
  partajat CLI + viitor flux web 3.3). Normalizeaza CUI (trim+upper),
  prinde IntegrityError -> ValueError cu cauza+fix.
- accounts.active (lifecycle cont) + index unic partial ux_accounts_cui
  (unicitate la nivel de index, fara fereastra de coliziune). Migrare
  idempotenta in _migrate.
- tools/account.py: create (--name/--cui/--inactive/--with-key atomic),
  list [--pending], activate/deactivate --account N. Erori -> exit 2.
- 20 teste noi (12 helper + 8 CLI); suita 299 passed.

active e inert pana la gate-ul worker din 3.3 (documentat).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 12:38:13 +00:00

178 lines
11 KiB
Markdown

# 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 <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`.