-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -- Migration 08 — Analytics Configuration Tables -- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -- Adds reference data driving the three-stakeholder analytics redesign -- (Phase 0.3 of the plan). These tables let downstream views monetise idle -- and fuel costs, and apply traffic-light targets to KPIs without hard-coding -- thresholds in SQL. -- -- • ops.cost_rates — fuel price per litre by city, labour rate by role. -- • ops.kpi_targets — green / amber / red thresholds per KPI per scope. -- -- Run after migration 07. Safe to re-run (CREATE TABLE IF NOT EXISTS, -- INSERT ... ON CONFLICT DO NOTHING). -- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ BEGIN; -- ── 1. ops.cost_rates ─────────────────────────────────────────────────────── -- Reference rates that power monetisation in analytics views. Lookup pattern -- in views: WHERE scope_type = 'city' AND scope_value = AND metric = ... -- ORDER BY effective_from DESC LIMIT 1. CREATE TABLE IF NOT EXISTS ops.cost_rates ( rate_key TEXT PRIMARY KEY, scope_type TEXT NOT NULL, -- 'city' | 'role' | 'global' scope_value TEXT, -- 'nairobi' | 'driver' | NULL for global metric TEXT NOT NULL, -- 'fuel_per_litre' | 'labour_per_hour' amount NUMERIC(12,2) NOT NULL, currency TEXT NOT NULL, -- 'KES' | 'UGX' effective_from DATE NOT NULL DEFAULT CURRENT_DATE, notes TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_cost_rates_lookup ON ops.cost_rates (scope_type, scope_value, metric, effective_from DESC); COMMENT ON TABLE ops.cost_rates IS 'Reference rates for analytics monetisation: fuel price per litre by city, ' 'labour cost per hour by role. Resolution order in views: scope_type=city ' '> scope_type=role > scope_type=global.'; COMMENT ON COLUMN ops.cost_rates.metric IS 'fuel_per_litre | labour_per_hour'; COMMENT ON COLUMN ops.cost_rates.scope_type IS 'city | role | global'; -- ── 2. ops.kpi_targets ────────────────────────────────────────────────────── -- Traffic-light thresholds per KPI. Same KPI can have global + per-CC + per-city -- rows; views use a CASE / COALESCE chain to pick the most specific match. CREATE TABLE IF NOT EXISTS ops.kpi_targets ( target_id BIGSERIAL PRIMARY KEY, kpi_key TEXT NOT NULL, -- e.g. 'utilisation_pct' scope_type TEXT NOT NULL, -- 'global' | 'city' | 'cost_centre' | 'vehicle_category' scope_value TEXT, -- NULL for global target_value NUMERIC(12,2) NOT NULL, amber_threshold NUMERIC(12,2), -- between target and red red_threshold NUMERIC(12,2), -- worse than amber direction TEXT NOT NULL DEFAULT 'higher_is_better', -- 'higher_is_better' | 'lower_is_better' effective_from DATE NOT NULL DEFAULT CURRENT_DATE, notes TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE (kpi_key, scope_type, scope_value, effective_from) ); CREATE INDEX IF NOT EXISTS idx_kpi_targets_lookup ON ops.kpi_targets (kpi_key, scope_type, scope_value, effective_from DESC); COMMENT ON TABLE ops.kpi_targets IS 'Traffic-light targets per KPI per scope. Resolution order in views: ' 'cost_centre > vehicle_category > city > global.'; COMMENT ON COLUMN ops.kpi_targets.direction IS 'higher_is_better -> green when value >= target. ' 'lower_is_better -> green when value <= target.'; -- ── 3. Seed cost rates ────────────────────────────────────────────────────── -- Placeholder values — confirm with Finance and update via a follow-up insert -- with a later effective_from date (do NOT mutate historical rows). INSERT INTO ops.cost_rates (rate_key, scope_type, scope_value, metric, amount, currency, notes) VALUES ('fuel.nairobi', 'city', 'nairobi', 'fuel_per_litre', 195.00, 'KES', 'Placeholder pump price — confirm with Finance.'), ('fuel.mombasa', 'city', 'mombasa', 'fuel_per_litre', 195.00, 'KES', 'Placeholder pump price — confirm with Finance.'), ('fuel.kampala', 'city', 'kampala', 'fuel_per_litre', 5200.00, 'UGX', 'Placeholder pump price — confirm with Finance.') ON CONFLICT (rate_key) DO NOTHING; -- ── 4. Seed KPI targets ───────────────────────────────────────────────────── -- Initial Exco-relevant targets. Calibrate after one month of clean data. INSERT INTO ops.kpi_targets (kpi_key, scope_type, scope_value, target_value, amber_threshold, red_threshold, direction, notes) VALUES ('utilisation_pct', 'global', NULL, 70, 60, 50, 'higher_is_better', 'Fleet utilisation: drive_hours / engine_on_hours.'), ('idle_pct', 'global', NULL, 15, 20, 25, 'lower_is_better', 'Idle as % of engine-on time.'), ('idle_pct', 'cost_centre', 'osp patrol', 15, 20, 25, 'lower_is_better', 'OSP patrol idle target — same as global until calibrated.'), ('fuel_kes_per_100km', 'global', NULL, 12, 14, 16, 'lower_is_better', 'Fuel litres per 100km equivalent — uses fuel_100km on devices.'), ('mttr_hours', 'global', NULL, 4, 6, 8, 'lower_is_better', 'Mean Time To Resolve, field-service ticket.'), ('alarms_per_100km', 'global', NULL, 2, 3, 5, 'lower_is_better', 'Safety event density.') ON CONFLICT (kpi_key, scope_type, scope_value, effective_from) DO NOTHING; -- ── 5. Read access for Grafana ────────────────────────────────────────────── GRANT USAGE ON SCHEMA ops TO grafana_ro; GRANT SELECT ON ops.cost_rates TO grafana_ro; GRANT SELECT ON ops.kpi_targets TO grafana_ro; COMMIT;