Verification Log
This scaffold was not just written — it was run. Every endpoint below was exercised against a real PostgreSQL 16 + PostGIS 3 instance and a live uvicorn process, not unit-tested in isolation. Five real bugs were found and fixed in the process; all are documented here so the fix history isn't lost.
Environment
- PostgreSQL 16 + PostGIS 3.4, local instance
- Python 3.12, dependencies from
api/requirements.txtin a clean venv - App run directly with
uvicorn app.main:app(equivalent to the Dockerfile's gunicorn setup, minus multi-worker concurrency)
Core platform verification
| Check | Result |
|---|---|
| All Python files byte-compile | Pass |
| FastAPI app imports, all routes register | Pass (after adding email-validator) |
| Table creation against live DB | Pass |
| Seed script inserts territory polygons, property polygons, premise points | Pass |
GET /healthz, GET /readyz |
Pass |
GET /assets/{id}/geometry — polygon round-trips through PostGIS with exact coordinates preserved |
Pass |
?format=esri — same polygon as Esri JSON rings |
Pass |
POST /geo/query — mixed-geometry results (a Polygon and a Point in one FeatureCollection) |
Pass |
GET /geo/bbox — viewport query |
Pass |
POST /geo/query/radius — geodesic proximity search |
Pass (after fix) |
GET /utilities/lookup — point-in-polygon match |
Pass (after fix) |
POST / PUT / DELETE /assets |
Pass |
| Plans, rate history, usage, forecast, enrollments, exchange transactions | Pass |
POST /auth/token — JWT issuance |
Pass |
| Unauthenticated request | Pass (401) |
GET /openapi.json — valid spec |
Pass |
Reseller multi-tenancy verification
A second pass after the tenancy work, against the same live setup:
| Check | Result |
|---|---|
| One customer attached to 2 resellers across 3 states, each attachment cost-keyed | Pass |
| Cost key defaults to the reseller UUID; explicit override honored | Pass |
| Reseller-scoped JWT sees only its own properties | Pass |
| Reseller 1 reading Reseller 2's property directly | Pass (403) |
| Shared customer: each reseller sees only its own attachment rows | Pass |
Spatial /geo/bbox respects tenant scope across a 3-state box |
Pass |
/geo/customers/search returns customers + properties + cost keys in an area |
Pass |
/geo/utilities/status returns per-utility status and affected property counts |
Pass |
Attaching in a state outside the reseller's operating_states |
Pass (422) |
Duplicate (customer, reseller, state) attachment |
Pass (409) |
/cost-keys rollup maps each key to reseller, customers, properties, states |
Pass |
Partial unique index rejects a second primary via direct SQL UPDATE |
Pass (unique violation) |
| Service-layer promote still succeeds through that index (demote → flush → promote) | Pass (exactly 1 primary after) |
Canonical schema.sql applies clean from scratch, partial index present |
Pass |
GeoJSON and Esri features carry reseller_id / customer_id / cost_key / state |
Pass (after fix) |
Bugs found and fixed during verification
1. Missing email-validator dependency
Pydantic's EmailStr (used on EnrollmentCreate.customer_email) requires
the optional email-validator package, which was not pinned. The app
failed to import at all.
Fix: requirements.txt now pins pydantic[email].
2. Raw WKT strings passed to PostGIS without a geometry cast
GET /utilities/lookup and POST /geo/query/radius built a point as an
f-string (f"SRID=4326;POINT({lon} {lat})") and passed it into
ST_Contains(...) / ST_DWithin(...). PostgreSQL has no
ST_Contains(geometry, character varying) overload, so both endpoints
returned 500.
Fix: both now build a shapely.geometry.Point and convert with
from_shape(point, srid=4326) — the same pattern already used for asset
writes — producing a properly-typed WKBElement bound as real geometry.
3. geoalchemy2.functions.ST_Geography doesn't exist as a callable
The radius query's geography cast raised AttributeError — GeoAlchemy2
doesn't pre-register that name as a function proxy in this version.
Fix: switched to SQLAlchemy's generic cast(Asset.geom, Geography),
which compiles to the ::geography cast PostGIS expects without depending
on a specific ST_* proxy.
4. Cost key silently mis-attributed across states
A property created in a state the customer had no attachment for was accepted and stamped with an unrelated state's cost key — a property in Kansas inherited the Oklahoma key. That is exactly the mis-billing the cost key exists to prevent, and it failed silently rather than loudly.
Fix: cost_key_for() no longer falls back to an arbitrary attachment.
A state-specific attachment wins, then a stateless catch-all; if neither
exists the request fails with 422 naming the covered states, so the
caller creates the right attachment instead of getting wrong data.
5. GeoJSON/Esri output omitted tenancy attribution
asset_to_feature() and asset_to_esri_feature() were never updated when
the tenancy columns were added, so every map feature came back without
reseller_id, customer_id, cost_key, or state. An ArcGIS layer
therefore could not style or filter by reseller, and a property could not
be attributed to a cost key without a second round trip — which defeats the
point of the attribution work. Caught while wiring the front-end to real
exported output rather than to hand-written fixtures.
Fix: both serializers now emit the four tenancy fields alongside the existing properties.
What was not covered
- Load testing / concurrency behavior under the multi-replica nginx setup (the nginx config was verified syntactically; the full docker-compose stack was not spun up — no Docker daemon was available in the verification environment, only a bare Python/Postgres setup).
- Alembic migration path (schema currently relies on
create_alllocally andschema.sqlelsewhere). - Per-route authorization scopes —
require_scope()exists but no route enforces a specific scope beyond "some valid credential" plus tenant filtering. - Backfill for pre-tenancy rows: the tenancy columns are nullable so the
change is non-breaking, but existing rows have
NULLreseller/customer/ cost_key and are visible only to platform-level callers. Real data needs a backfill before those columns could be madeNOT NULL. - Automated test suite — this was manual verification via
curl. Before real usage, port these checks intopytest+httpx.AsyncClientso they run on every change instead of once.