TransitPin — Technical Specification: OPS Assume Identity, Map Traffic/Weather, Billing¶
Status: OPEN (building live). Date: 2026-08-14. Author: Sho'Nuff (IT Pro Partner).
Three net-new feature areas. Ground-truth verified against the live deployment 2026-08-14
(app3, /opt/transitpin-api): none of this existed prior — no household/billing/payment
tables, no audit log, no trip_type field, and the operator map (fleet/dashboard.html)
had no traffic/weather. The admin dashboard (admin.html) already contained a working
TomTom traffic overlay + Open-Meteo weather, reused here as the reference implementation.
1. Architecture context¶
| Layer | Stack | Location |
|---|---|---|
| API | FastAPI + SQLite | /opt/transitpin-api/main.py, data/transitpin.db, uvicorn 127.0.0.1:8900, systemd transitpin-api.service |
| Frontend | Static HTML | /home/transitpin-dash/htdocs/my.transitpin.com/ |
| Auth | auth2 (Hexclave) | auth2-api.itpropartner.com → :8102 (whoami/register proxy) |
1.1 Shared helper extraction (common.py)¶
To let feature routers import auth/tenant/DB dependencies without editing main.py, a
common.py module is extracted (deployed + import-verified 2026-08-14). It mirrors
main.py's helpers:
DB_PATH,AUTH2_API,DEFAULT_TENANT,PLATFORM_ADMIN_ROLES = {"platform_admin","admin"}get_db()(yieldssqlite3.Row),_jwt_claims(),_slug_from_path()get_current_user(request)— validates auth2 viaAuthorization: Bearer, demo fallbackget_tenant_id(request, user)— resolution order: JWTtenant_id→ URL slug →X-Tenant-Idheader → defaultdemo
1.2 Router pattern (per feature)¶
Each feature ships as a self-contained module with:
1. router = APIRouter() — all endpoints as @router.<method>(...) using Depends(get_current_user) / Depends(get_tenant_id).
2. def ensure_schema(conn) — idempotent CREATE TABLE IF NOT EXISTS (+ ALTER TABLE guarded by PRAGMA table_info) for that feature's tables.
The integrator wires app.include_router(router, prefix="/api") and calls ensure_schema() at
startup. This keeps parallel feature teams from clobbering main.py.
2. Feature A — OPS Assume Identity (impersonation + audit)¶
Requirement. A platform_admin in ops.transitpin.com performs an action for/on behalf
of a customer "as them". Every impersonated action is logged.
2.1 Data model¶
impersonation_sessions—token TEXT PK, actor_id, actor_email, tenant_id, target_role, exp, created_ataudit_log—id TEXT PK, actor_id, actor_email, impersonated_id, impersonated_email, tenant_id, action, resource, details (JSON), created_at
2.2 Endpoints (ops_routes.py)¶
| Method | Path | Auth | Notes |
|---|---|---|---|
| POST | /api/ops/assume-identity |
platform_admin | body {tenant_id, user_email?, target_role} → returns {token, expires_at, banner}; logs assume_identity_start |
| POST | /api/ops/exit-impersonation |
token | deletes session; logs assume_identity_end |
| GET | /api/ops/audit-log |
platform_admin | ?tenant_id=&limit= → newest-first trail |
| POST | /api/ops/log-action |
token | body {action, resource, details} → writes audit row tagged with real actor + impersonated subject |
2.3 Security model¶
- Impersonation token is DB-backed (
secrets.token_urlsafe(32), exp +15 min), looked up + exp-checked by aget_impersonated_userdependency. It is not a valid auth2 token. - Assume/audit endpoints require
platform_admin(403 otherwise). - Every mutating action taken while impersonating is written to
audit_logwith both the real actor (actor_id/actor_email) and the impersonated subject.
3. Feature B — Map traffic + weather + route conditions¶
Requirement. Add TomTom live-traffic overlay, local weather, and per-route driving
conditions to the operator dashboard map (fleet/dashboard.html), and expose TomTom health
to the OPS API Health view.
3.1 Endpoints (maps_routes.py)¶
| Method | Path | Notes |
|---|---|---|
| GET | /api/health/tomtom |
server-side ping to TomTom flowSegmentData (point 32.0158,-81.0595) → {ok, status, detail} |
| GET | /api/weather?lat=&lon= |
proxy to Open-Meteo (default 32.0158,-81.0595) |
| GET | /api/routes/{id}/conditions |
per-route summary from TomTom incidentDetails over the route's school corridor |
3.2 External APIs¶
- TomTom Traffic — key already exists (hardcoded
TOMTOM_KEYinadmin.html:856, used for flow tile + flowSegmentData + incidentDetails). Reused; no new key. Flow tile overlay:https://api.tomtom.com/traffic/map/4/tile/flow/relative0/{z}/{x}/{y}.png?key=KEY. - Weather = Open-Meteo (free, keyless), not NWS. Correction from the original ask: the
existing weather is Open-Meteo; NWS
api.weather.govis a keyless alternative if a government feed is later required. - Route corridor: route geometry is not stored, so conditions use the route's associated
schools (bounding box); Savannah metro bbox fallback
31.95,-81.20,32.15,-80.95.
3.3 Frontend (fleet/dashboard.html)¶
- TomTom flow
L.tileLayer+ 🚦 toggle (mirrorsadmin.htmlpattern). - Weather widget (current temp/conditions/wind) via
/api/weather. - "Driving conditions" section per route via
/api/routes/{id}/conditions.
4. Feature C — Billing¶
Requirement. Household-aware billing with auto-calculated totals, per-child trip type, payer split-billing (including a parent outside the household), Square + Stripe provider abstraction, and an outstanding-payments report.
4.1 Pricing (locked)¶
| Trip type | transport-need mapping | Rate |
|---|---|---|
| one-way | morning only OR afternoon only |
$120/wk |
| two-way | both |
$175/wk |
- Household weekly total = Σ per-child rate.
- Billing interval is a tenant-level config (
billing_cycle, defaultweekly) — not hardcoded.
4.2 Data model (billing_routes.py)¶
households—id PK, tenant_id, name, email, phone, address, billing_cycle DEFAULT 'weekly', created_atpayers—id PK, tenant_id, household_id, name, email, phone, is_primary DEFAULT 0, created_atbilling_allocations—id PK, tenant_id, household_id, payer_id, child_id NULL, split_type ('percentage'|'fixed'|'per_child'), value, created_atinvoices—id PK, tenant_id, household_id, payer_id, cycle_start, cycle_end, amount, status ('draft'|'sent'|'unpaid'|'paid'|'partial'), provider ('square'|'stripe'|'none'), provider_invoice_id NULL, created_atpayments—id PK, tenant_id, invoice_id, amount, method, provider_ref NULL, paid_atchildren— extended (idempotent ALTER):trip_type TEXT,household_id TEXT,rate_weekly REAL
4.3 Split-bill model¶
- Household = billing unit (group of children, one contact, one total).
- Payer = an adult who is invoiced — may live outside the household.
- Allocation = how a household's charges split across payers:
percentage/fixed/per_child.
A co-parent not in the household is added as a payer with an allocation (e.g. 50%). Each payer
receives their own invoice, so Square invoicing stays correct.
4.4 Endpoints¶
| Method | Path | Notes |
|---|---|---|
| POST | /api/registrations/full |
persist full registration form (parent + children with transport_need + emergency contacts) → household + primary payer + children (trip_type mapped) |
| GET | /api/billing/households |
list + computed weekly total |
| GET | /api/billing/households/{id} |
detail: children, payers, allocations, per-payer split |
| POST | /api/billing/households/{id}/payers |
add co-parent payer + allocation (split-bill) |
| POST | /api/billing/generate |
generate cycle invoices (draft), split per allocation |
| GET | /api/billing/outstanding |
unpaid/partial invoices grouped by household + payer |
| GET | /api/billing/rates |
{one_way:120, two_way:175, cycle:'weekly'} |
| GET | /api/billing/provider-status |
{square_connected, stripe_connected} |
4.5 Payment provider abstraction¶
PaymentProvider interface (create_invoice, get_status):
- SquareProvider — Square Invoices API: POST https://connect.squareup.com/v2/invoices,
OAuth INVOICES_WRITE + ORDERS_WRITE, GET /v2/invoices/{id} for status. Creds from env
SQUARE_ACCESS_TOKEN / SQUARE_LOCATION_ID.
- StripeProvider — stub (NotImplementedError with clear message). Creds STRIPE_SECRET_KEY.
SquareProvider.is_connected() returns False when SQUARE_ACCESS_TOKEN is unset; generate()
falls back to provider: 'none' + status: 'draft' so the app runs end-to-end without keys.
4.6 Frontend¶
- New
billing.html— household list (students/parents/trip-type/auto-total), payer+allocation editor, outstanding-payments table, "Square not connected" banner. signin.htmlregister flow additionally POSTs the full payload to/api/registrations/full(auth2 registration preserved).
5. Cross-cutting decisions (locked)¶
- Weather = Open-Meteo (existing, keyless), not NWS.
- TomTom key reused from
admin.html; no new key. transport-need(both/morning/afternoon) is the one-way/two-way source of truth, now persisted.- Split-bill =
payers+billing_allocations(percentage/fixed/per-child), each payer invoiced separately. - Square primary, Stripe option, both key-gated behind a
PaymentProviderinterface. - Billing interval is tenant config, not hardcoded.
- Assume-identity is
platform_admin-only, DB-backed 15-min token, fully audited.
6. Blockers / open items¶
- Square OAuth credentials (access token + location_id from Village Express's Square account) required for real invoice creation. Everything else builds now.
app.transitpin.comvhost/cert (carried from prior work).- NDA before demo (see
CUSTOMER-ONBOARDING.md/NDA.md).