initial: split from gov-agreg — vreau.digital standalone platform
Moved from gov-agreg/src/pages/achizitii/* to root (drop prefix). - 22 pages migrated, 127 files total - All internal links: /achizitii/X → /X (176 occurrences fixed) - AchizitiiLayout subnav rewritten: /X paths, top-right link to vreaudigital.ro hub - BaseLayout new (vreau.digital branding, OG tags, site URL) - astro.config.mjs: site https://vreau.digital, server output (was static) - docker-compose: port 5096 (vreaudigital is 5095), container vreau-digital - deploy.sh: paths /opt/vreau-digital, log /var/log/vreau-digital-deploy.log Backend shared with gov-agreg: - PostgreSQL satra (same schemas: seap, firms, anaf, anre, ...) - Photon, Martin tiles - Infisical /vreaudigital path (DATABASE_URL etc. shared) build: PASS (npx astro check 0 errors, npm run build 5s vite + 10s server)
This commit is contained in:
@@ -0,0 +1,233 @@
|
||||
-- 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;
|
||||
Reference in New Issue
Block a user