Skip to content

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() (yields sqlite3.Row), _jwt_claims(), _slug_from_path()
  • get_current_user(request) — validates auth2 via Authorization: Bearer, demo fallback
  • get_tenant_id(request, user) — resolution order: JWT tenant_id → URL slug → X-Tenant-Id header → default demo

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_sessionstoken TEXT PK, actor_id, actor_email, tenant_id, target_role, exp, created_at
  • audit_logid 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 a get_impersonated_user dependency. 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_log with 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_KEY in admin.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.gov is 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 (mirrors admin.html pattern).
  • 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, default weekly) — not hardcoded.

4.2 Data model (billing_routes.py)

  • householdsid PK, tenant_id, name, email, phone, address, billing_cycle DEFAULT 'weekly', created_at
  • payersid PK, tenant_id, household_id, name, email, phone, is_primary DEFAULT 0, created_at
  • billing_allocationsid PK, tenant_id, household_id, payer_id, child_id NULL, split_type ('percentage'|'fixed'|'per_child'), value, created_at
  • invoicesid 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_at
  • paymentsid PK, tenant_id, invoice_id, amount, method, provider_ref NULL, paid_at
  • childrenextended (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.html register flow additionally POSTs the full payload to /api/registrations/full (auth2 registration preserved).

5. Cross-cutting decisions (locked)

  1. Weather = Open-Meteo (existing, keyless), not NWS.
  2. TomTom key reused from admin.html; no new key.
  3. transport-need (both/morning/afternoon) is the one-way/two-way source of truth, now persisted.
  4. Split-bill = payers + billing_allocations (percentage/fixed/per-child), each payer invoiced separately.
  5. Square primary, Stripe option, both key-gated behind a PaymentProvider interface.
  6. Billing interval is tenant config, not hardcoded.
  7. 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.com vhost/cert (carried from prior work).
  • NDA before demo (see CUSTOMER-ONBOARDING.md / NDA.md).