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>
178 lines
11 KiB
Markdown
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`.
|