122 lines
7 KiB
MySQL
122 lines
7 KiB
MySQL
|
|
-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||
|
|
-- 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 = <city> 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;
|