226 lines
10 KiB
MySQL
226 lines
10 KiB
MySQL
|
|
-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||
|
|
-- 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;
|