tracksolid_timescale_grafan.../06_business_analytics_migration.sql

226 lines
10 KiB
MySQL
Raw Permalink Normal View History

-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
-- Migration 06 — Business Analytics Schema Support
-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
-- Adds the schema objects referenced by 01_BusinessAnalytics.md:
-- • tracksolid.devices.assigned_city (§3.7 Geographic Drift)
-- • tracksolid.dispatch_log (§4.4, §4.5 Field-Service SLAs)
-- • ops schema (external ops integration namespace)
-- • ops.service_log (§10 Service-Interval Forecaster)
-- • ops.odometer_readings (§3.8 Odometer Divergence)
-- • ops.tickets (§2.4 Cost-per-Ticket — skeleton)
-- • ops.vw_service_forecast (§10 weekly booking view)
--
-- Run after migration 05. Safe to re-run (uses IF NOT EXISTS / DO NOTHING /
-- CREATE OR REPLACE).
-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
BEGIN;
-- ── 1. City cohort column (§3.7) ─────────────────────────────────────────────
ALTER TABLE tracksolid.devices
ADD COLUMN IF NOT EXISTS assigned_city TEXT;
COMMENT ON COLUMN tracksolid.devices.assigned_city
IS 'Operating territory code: NBO (Nairobi) | MBA (Mombasa) | KLA (Kampala). '
'Used for city-cohort analytics and geographic drift detection.';
CREATE INDEX IF NOT EXISTS idx_devices_assigned_city
ON tracksolid.devices (assigned_city)
WHERE assigned_city IS NOT NULL;
-- ── 2. Dispatch log (§4.4, §4.5) ──────────────────────────────────────────────
-- One row per ticket dispatch. Populated by n8n / ops integration at
-- assignment; back-filled by nightly job using trips + live_positions.
CREATE TABLE IF NOT EXISTS tracksolid.dispatch_log (
dispatch_id BIGSERIAL PRIMARY KEY,
ticket_id TEXT NOT NULL,
imei TEXT NOT NULL REFERENCES tracksolid.devices(imei),
driver_name TEXT,
job_lat DOUBLE PRECISION NOT NULL,
job_lng DOUBLE PRECISION NOT NULL,
job_geom geometry(Point, 4326),
assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
first_movement_at TIMESTAMPTZ,
on_site_at TIMESTAMPTZ,
resolved_at TIMESTAMPTZ,
cancelled_at TIMESTAMPTZ,
distance_km NUMERIC(8, 2),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_dispatch_log_ticket
ON tracksolid.dispatch_log (ticket_id);
CREATE INDEX IF NOT EXISTS idx_dispatch_log_imei_assigned
ON tracksolid.dispatch_log (imei, assigned_at DESC);
CREATE INDEX IF NOT EXISTS idx_dispatch_log_assigned_at
ON tracksolid.dispatch_log (assigned_at DESC);
CREATE INDEX IF NOT EXISTS idx_dispatch_log_job_geom
ON tracksolid.dispatch_log USING GIST (job_geom);
COMMENT ON TABLE tracksolid.dispatch_log
IS 'Persistent record of every dispatch decision. Powers SLA metrics: '
'dispatch latency, depart delay, time-to-site, wrench time.';
COMMENT ON COLUMN tracksolid.dispatch_log.first_movement_at
IS 'First trip start after assigned_at. Back-filled nightly from trips.';
COMMENT ON COLUMN tracksolid.dispatch_log.on_site_at
IS 'Time vehicle entered 150 m radius of job_geom. Back-filled nightly.';
COMMENT ON COLUMN tracksolid.dispatch_log.resolved_at
IS 'Ticket close time from the ops system (ops.tickets.closed_at).';
-- ── 3. ops schema namespace ───────────────────────────────────────────────────
-- Separates Fireside operations domain (tickets, services, odometers) from
-- the tracksolid telematics namespace so ownership / grants can diverge.
CREATE SCHEMA IF NOT EXISTS ops;
COMMENT ON SCHEMA ops
IS 'Fireside operations domain: tickets, service logs, odometer readings. '
'Distinct from tracksolid.* which holds telematics data.';
-- ── 4. Service log (§10) ──────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS ops.service_log (
service_id BIGSERIAL PRIMARY KEY,
imei TEXT NOT NULL REFERENCES tracksolid.devices(imei),
service_date DATE NOT NULL,
odometer_km INTEGER NOT NULL,
service_type TEXT,
cost_kes INTEGER,
notes TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_service_log_imei_date
ON ops.service_log (imei, service_date DESC);
COMMENT ON TABLE ops.service_log
IS 'Workshop service history. Powers §10 Service-Interval Forecaster.';
COMMENT ON COLUMN ops.service_log.service_type
IS 'scheduled | repair | tyre | bodywork | inspection | other';
COMMENT ON COLUMN ops.service_log.odometer_km
IS 'Physical odometer reading at service time (integer km).';
-- ── 5. Odometer readings (§3.8) ───────────────────────────────────────────────
-- Periodic physical odometer captures from service events, fuel card receipts,
-- or manual driver entry. Divergence vs tracker-computed distance flags
-- sensor drift or tamper.
CREATE TABLE IF NOT EXISTS ops.odometer_readings (
reading_id BIGSERIAL PRIMARY KEY,
imei TEXT NOT NULL REFERENCES tracksolid.devices(imei),
reading_date DATE NOT NULL,
reading_km INTEGER NOT NULL,
source TEXT,
recorded_by TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (imei, reading_date)
);
CREATE INDEX IF NOT EXISTS idx_odometer_readings_imei_date
ON ops.odometer_readings (imei, reading_date DESC);
COMMENT ON TABLE ops.odometer_readings
IS 'Physical odometer captures from service, fuel card, or manual entry. '
'Powers §3.8 Odometer Divergence audit.';
COMMENT ON COLUMN ops.odometer_readings.source
IS 'service | fuel_card | driver_manual | workshop_form';
-- ── 6. Tickets skeleton (§2.4) ───────────────────────────────────────────────
-- MINIMAL skeleton so the Cost-per-Ticket query is runnable. In production,
-- this table is expected to be populated by the Fireside ticketing system
-- (Zoho/Freshdesk/job-management export) via n8n or a direct feed. Schema
-- is intentionally narrow — extend with columns specific to your source.
CREATE TABLE IF NOT EXISTS ops.tickets (
ticket_id TEXT PRIMARY KEY,
assigned_imei TEXT REFERENCES tracksolid.devices(imei),
driver_name TEXT,
customer TEXT,
job_type TEXT,
priority TEXT,
status TEXT NOT NULL DEFAULT 'open',
created_at TIMESTAMPTZ NOT NULL,
assigned_at TIMESTAMPTZ,
closed_at TIMESTAMPTZ,
job_lat DOUBLE PRECISION,
job_lng DOUBLE PRECISION,
job_geom geometry(Point, 4326),
ingested_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_tickets_status_created
ON ops.tickets (status, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_tickets_assigned_imei
ON ops.tickets (assigned_imei)
WHERE assigned_imei IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_tickets_closed_at
ON ops.tickets (closed_at DESC NULLS LAST);
COMMENT ON TABLE ops.tickets
IS 'Skeleton for ticket data sourced from the Fireside ops system. '
'Replace or extend to match the actual feed (Zoho Desk, Freshdesk, etc).';
COMMENT ON COLUMN ops.tickets.status
IS 'open | assigned | in_progress | resolved | cancelled';
-- ── 7. Service forecast view (§10) ────────────────────────────────────────────
-- Wraps the §10 forecaster CTE so the weekly booking query in
-- 01_BusinessAnalytics.md references a stable object.
CREATE OR REPLACE VIEW ops.vw_service_forecast AS
WITH last_service AS (
SELECT DISTINCT ON (imei)
imei,
service_date,
odometer_km
FROM ops.service_log
WHERE service_type = 'scheduled'
ORDER BY imei, service_date DESC
),
current_odometer AS (
SELECT imei, current_mileage_km
FROM tracksolid.devices
),
trailing_rate AS (
SELECT
imei,
SUM(distance_km) / 30.0 AS km_per_day_30d
FROM tracksolid.trips
WHERE start_time > NOW() - INTERVAL '30 days'
AND end_time IS NOT NULL
GROUP BY imei
)
SELECT
d.imei,
d.driver_name,
d.vehicle_number,
ls.service_date AS last_service_date,
ls.odometer_km AS last_service_odo,
co.current_mileage_km AS current_odo,
(co.current_mileage_km - COALESCE(ls.odometer_km, 0)) AS km_since_service,
GREATEST(
0,
10000 - (co.current_mileage_km - COALESCE(ls.odometer_km, 0))
) AS km_to_next_service,
ROUND(tr.km_per_day_30d, 1) AS km_per_day_30d,
CASE
WHEN tr.km_per_day_30d > 0 THEN
CURRENT_DATE + (
GREATEST(0, 10000 - (co.current_mileage_km - COALESCE(ls.odometer_km, 0)))
/ tr.km_per_day_30d
)::INT
ELSE NULL
END AS projected_service_date
FROM tracksolid.devices d
LEFT JOIN last_service ls ON ls.imei = d.imei
LEFT JOIN current_odometer co ON co.imei = d.imei
LEFT JOIN trailing_rate tr ON tr.imei = d.imei
WHERE d.enabled_flag = 1;
COMMENT ON VIEW ops.vw_service_forecast
IS 'Projected next-service date per vehicle based on 30-day km rate. '
'Service interval default 10,000 km — override at query time if needed.';
COMMIT;