TEN-WP-0002 T01-T03: service skeleton, domain model, storage layer

Python 3.12 + FastAPI, pyproject.toml + Makefile mirroring
qonto-assistant's exactly. src/tenant_engine/{domain,store,app,main}.py:

- domain.py: Tenant, CapabilityRole (PLTF/IAM/VEN/CUS), RoleGrant,
  PlanAssignment, create_role_grant() enforcing ADR-0014's invariants.
  Refinement made while implementing: platform_default grants are valid
  for trial-grouped tenants OR the reserved tenant:platform/tenant:coulomb
  tenants (their baseline roles were never purchased either) -- the task
  spec only named the trial case.
- store.py: TenantStore Protocol + InMemoryTenantStore, every mutation
  emits a DomainEvent per the boundary contract's Audit Correlation
  Contract.
- app.py/main.py: FastAPI factory + /health endpoint, verified live on
  127.0.0.1:8090.

29 tests passing, including non-exclusive role coexistence (CUS+VEN
simultaneously) and append-only revoke semantics.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
tegwick 2026-07-23 22:01:23 +02:00
parent 7eb21c05b8
commit 0770ce82d9
11 changed files with 774 additions and 3 deletions

View file

@ -34,7 +34,7 @@ enforcement.
```task
id: TEN-WP-0002-T01
status: todo
status: done
priority: high
state_hub_task_id: "d22b0ce8-3f25-4c8d-b54f-ba44c65159c1"
```
@ -47,11 +47,19 @@ routers), from day one — not refactored in later.
Done when: `make test` runs an empty/smoke suite; `make run` starts a bare
FastAPI app with a `/health` endpoint.
**Done 2026-07-23:** `pyproject.toml` + `Makefile` land, mirroring
`qonto-assistant`'s exactly (`install-dev`/`test`/`lint`/`run`).
`src/tenant_engine/{domain,store,app,main}.py` scaffolded per the planned
layering (`domain.py` pure, `store.py` the persistence seam, `app.py` the
FastAPI factory). Verified live: `python -m tenant_engine.main` starts on
`127.0.0.1:8090`, `GET /health` returns
`{"status": "ok", "service": "tenant-engine", "version": "0.1.0"}`.
## Task: Domain model — tenant, grouping, capability role, plan grant
```task
id: TEN-WP-0002-T02
status: todo
status: done
priority: high
state_hub_task_id: "cfceea30-f383-4781-a7e4-226653aafca9"
```
@ -84,11 +92,23 @@ Validation rules encoded as domain invariants, not just API-layer checks:
Done when: unit tests cover valid/invalid grouping values, the trial/
no-plan-required exception, and grant/revoke as append-only operations.
**Done 2026-07-23:** `domain.py` implements `Tenant`, `CapabilityRole`,
`RoleGrant`, `PlanAssignment`, and `create_role_grant()` exactly as
specified. One refinement made while implementing, not pre-specified in the
task text: `platform_default` is valid for `trial`-grouped tenants **or**
the reserved, ungrouped `tenant:platform`/`tenant:coulomb` tenants (their
baseline roles were never purchased either) — the task only mentioned the
`trial` case. Also enforced `platform_default` grants must not carry a
`plan_id` (implied by "no `plan_id`" in the task text, made an explicit
invariant). `tests/test_domain.py` covers all listed cases plus the
reserved-tenant extension and double-revoke rejection. `pytest`
29 passed (whole-repo total, includes T03's tests below).
## Task: Storage layer
```task
id: TEN-WP-0002-T03
status: todo
status: done
priority: high
state_hub_task_id: "ade9374a-ed26-45da-a424-a0d21effe520"
```
@ -103,6 +123,14 @@ Contract — the event bus itself can be a simple in-process list for now.
Done when: unit tests cover create/read/grant/revoke/assign-plan through the
store interface, plus event emission for each mutation.
**Done 2026-07-23:** `store.py`'s `TenantStore` Protocol +
`InMemoryTenantStore` implement create/get/grant/revoke/active_roles/
assign_plan, each emitting a `DomainEvent`. `tests/test_store.py` covers
the full lifecycle, non-exclusive role coexistence (`CUS` + `VEN`
simultaneously, proving ADR-0014's non-exclusivity isn't just a comment),
revoke-keeps-record-but-clears-active-set, and asserts the exact event
sequence for a full mutation chain. `python -m compileall src tests` clean.
## Task: Cache-read API (for key-cape)
```task