-- Risk flags (red flags) for procurement transparency, based on OCP indicators. -- Idempotent: safe to re-run. BEGIN; -- ── Column on announcements ── ALTER TABLE seap.announcements ADD COLUMN IF NOT EXISTS risk_flags JSONB; CREATE INDEX IF NOT EXISTS idx_ann_risk_flags ON seap.announcements USING gin(risk_flags) WHERE risk_flags IS NOT NULL AND jsonb_array_length(risk_flags) > 0; -- ── Materialized view: per-CPV-division median awarded value ── DROP MATERIALIZED VIEW IF EXISTS seap.mv_cpv_median_value CASCADE; CREATE MATERIALIZED VIEW seap.mv_cpv_median_value AS SELECT cpv_division, count(*)::int AS contracts, percentile_cont(0.5) WITHIN GROUP (ORDER BY awarded_value)::numeric(15,2) AS median_value, avg(awarded_value)::numeric(15,2) AS avg_value, percentile_cont(0.95) WITHIN GROUP (ORDER BY awarded_value)::numeric(15,2) AS p95_value FROM seap.announcements WHERE awarded_value IS NOT NULL AND awarded_value > 0 AND cpv_division IS NOT NULL GROUP BY cpv_division HAVING count(*) >= 5; CREATE UNIQUE INDEX IF NOT EXISTS idx_mv_cpv_median_pk ON seap.mv_cpv_median_value(cpv_division); -- ── Materialized view: authority supplier concentration (top supplier % of yearly value) ── DROP MATERIALIZED VIEW IF EXISTS seap.mv_authority_concentration CASCADE; CREATE MATERIALIZED VIEW seap.mv_authority_concentration AS WITH yearly_pairs AS ( SELECT a.authority_cui, MIN(a.authority_name) AS authority_name, EXTRACT(YEAR FROM a.publication_date)::int AS year, a.supplier_cui, MIN(a.supplier_name) AS supplier_name, SUM(a.awarded_value)::numeric(15,2) AS total_value, COUNT(*)::int AS contracts FROM seap.announcements a WHERE a.authority_cui IS NOT NULL AND a.supplier_cui IS NOT NULL AND a.awarded_value IS NOT NULL AND a.awarded_value > 0 AND a.publication_date IS NOT NULL AND a.publication_date >= now() - interval '36 months' GROUP BY a.authority_cui, EXTRACT(YEAR FROM a.publication_date), a.supplier_cui ), yearly_totals AS ( SELECT authority_cui, year, SUM(total_value) AS year_total, SUM(contracts) AS year_contracts FROM yearly_pairs GROUP BY authority_cui, year ), ranked AS ( SELECT p.authority_cui, p.authority_name, p.year, p.supplier_cui, p.supplier_name, p.total_value, p.contracts, t.year_total, t.year_contracts, ROW_NUMBER() OVER (PARTITION BY p.authority_cui, p.year ORDER BY p.total_value DESC) AS rn, (p.total_value / NULLIF(t.year_total, 0))::numeric(6,4) AS share FROM yearly_pairs p JOIN yearly_totals t USING (authority_cui, year) ) SELECT authority_cui, authority_name, year, supplier_cui AS top_supplier_cui, supplier_name AS top_supplier_name, total_value AS top_supplier_value, contracts AS top_supplier_contracts, year_total, year_contracts, share AS top_supplier_share FROM ranked WHERE rn = 1 AND year_total >= 100000; -- skip tiny totals (noise) CREATE UNIQUE INDEX IF NOT EXISTS idx_mv_auth_conc_pk ON seap.mv_authority_concentration(authority_cui, year); CREATE INDEX IF NOT EXISTS idx_mv_auth_conc_share ON seap.mv_authority_concentration(top_supplier_share DESC NULLS LAST); -- ── View: single-bidder contracts ── DROP VIEW IF EXISTS seap.v_single_bidder CASCADE; CREATE VIEW seap.v_single_bidder AS SELECT a.* FROM seap.announcements a WHERE a.type = 'ca_notice' AND ( a.num_offers = 1 OR ( a.details IS NOT NULL AND jsonb_typeof(a.details->'all_winners') = 'array' AND jsonb_array_length(a.details->'all_winners') = 1 ) ); -- ── Function: compute risk flags for a single announcement ── -- Returns JSONB array of { code, severity, label, detail? } CREATE OR REPLACE FUNCTION seap.compute_announcement_flags( p_id BIGINT ) RETURNS JSONB LANGUAGE plpgsql AS $$ DECLARE rec RECORD; flags JSONB := '[]'::jsonb; v_median NUMERIC; BEGIN SELECT a.id, a.type, a.publication_date, a.deadline_submission, a.awarded_value, a.estimated_value, a.cpv_division, a.num_offers, a.details INTO rec FROM seap.announcements a WHERE a.id = p_id; IF NOT FOUND THEN RETURN NULL; END IF; -- 1) Single bidder (only meaningful for ca_notice with winner data) IF rec.type = 'ca_notice' THEN IF rec.num_offers = 1 THEN flags := flags || jsonb_build_object( 'code', 'single_bidder', 'severity', 'high', 'label', 'Un singur ofertant' ); ELSIF rec.details IS NOT NULL AND jsonb_typeof(rec.details->'all_winners') = 'array' AND jsonb_array_length(rec.details->'all_winners') = 1 THEN flags := flags || jsonb_build_object( 'code', 'single_bidder', 'severity', 'high', 'label', 'Un singur câștigător' ); END IF; END IF; -- 2) Short deadline (only c_notice / rfq_invitation have submission deadlines) IF rec.type IN ('c_notice','rfq_invitation') AND rec.publication_date IS NOT NULL AND rec.deadline_submission IS NOT NULL AND (rec.deadline_submission - rec.publication_date) < interval '10 days' THEN flags := flags || jsonb_build_object( 'code', 'short_deadline', 'severity', 'medium', 'label', 'Termen scurt', 'detail', EXTRACT(EPOCH FROM (rec.deadline_submission - rec.publication_date))/86400.0 ); END IF; -- 3) Suspicious savings: awarded_value < 50% of estimated IF rec.awarded_value IS NOT NULL AND rec.estimated_value IS NOT NULL AND rec.awarded_value > 0 AND rec.estimated_value > 0 AND rec.awarded_value < 0.5 * rec.estimated_value THEN flags := flags || jsonb_build_object( 'code', 'suspicious_savings', 'severity', 'medium', 'label', 'Economii suspecte', 'detail', round(100.0 * (1 - rec.awarded_value / rec.estimated_value))::int ); END IF; -- 5) Overprice: awarded_value > 2 * median per CPV division IF rec.awarded_value IS NOT NULL AND rec.awarded_value > 0 AND rec.cpv_division IS NOT NULL THEN SELECT median_value INTO v_median FROM seap.mv_cpv_median_value WHERE cpv_division = rec.cpv_division; IF v_median IS NOT NULL AND v_median > 0 AND rec.awarded_value > 2 * v_median THEN flags := flags || jsonb_build_object( 'code', 'overprice', 'severity', 'medium', 'label', 'Peste piață', 'detail', round((rec.awarded_value / v_median)::numeric, 1) ); END IF; END IF; RETURN flags; END; $$; -- ── Function: refresh all risk-related materialized views ── CREATE OR REPLACE FUNCTION seap.refresh_risk_views() RETURNS VOID LANGUAGE plpgsql AS $$ BEGIN REFRESH MATERIALIZED VIEW CONCURRENTLY seap.mv_cpv_median_value; EXCEPTION WHEN OTHERS THEN REFRESH MATERIALIZED VIEW seap.mv_cpv_median_value; END; $$; CREATE OR REPLACE FUNCTION seap.refresh_concentration() RETURNS VOID LANGUAGE plpgsql AS $$ BEGIN REFRESH MATERIALIZED VIEW CONCURRENTLY seap.mv_authority_concentration; EXCEPTION WHEN OTHERS THEN REFRESH MATERIALIZED VIEW seap.mv_authority_concentration; END; $$; COMMIT; -- Initial population (non-transactional) REFRESH MATERIALIZED VIEW seap.mv_cpv_median_value; REFRESH MATERIALIZED VIEW seap.mv_authority_concentration;