A1 migrations.py: - remission_practice DROP uq_application + ADD sequence_number/period_label/suggested_instructor_id - UNIQUE composita (application_id, sequence_number) - partial index idx_remission_practice_unassigned su assigned_instructor_id NULL - nuova tabella remission_custom_check_value (storage_path/mime/size/sha256 allineata adapter) A2 models.py + templates.py: - RemissionPractice: UniqueConstraint composita, campi multi-tranche, relationship custom_checks - classe RemissionCustomCheckValue - RESTART_TEMPLATE schema_version=2, max_tranches=2, custom_checks esempio (antiriciclaggio required no-doc, polizza_fidejussoria optional con-doc) - upgrade_schema_to_v2 idempotente per snapshot v1 esistenti A3 _compute_gate_check(db, practice) CUMULATIVO: - max_remission_global = min(cap_pct * erogato, cap_abs) - already_approved = func.sum(approved_remission) su tranche APPROVED precedenti dello stesso application_id con sequence_number < corrente - max_remission_this_tranche = max(0, global - already_approved) - pre_check_admissible = min(grand_total_declared, this_tranche) [voce 2 Cecilia] - remission_due = min(effective_total, this_tranche) - residuo_da_restituire = erogato - already_approved - remission_due (cumulativo) - output totals esteso: sequence_number, tranches_count, tranches_max - signature (db, practice) - aggiornati 6 call site in practices/instructor/verbale Test su NAPOLI SAS: erogato 17K, cap 8500, tranche 1 approvata 467.14EUR, tranche 2 vuota -> residuo disponibile 8032.86EUR, residuo_da_restituire 16532.86EUR.
111 lines
4.6 KiB
Python
111 lines
4.6 KiB
Python
"""
|
|
Migration idempotente per sandbox.
|
|
Base.metadata.create_all() crea solo tabelle mancanti, non colonne aggiuntive.
|
|
Qui gestiamo ALTER TABLE ADD COLUMN IF NOT EXISTS per l'evoluzione dello schema.
|
|
|
|
Ogni migration è una stringa SQL che puo essere eseguita piu volte senza errori.
|
|
"""
|
|
from sqlalchemy import text
|
|
import logging
|
|
|
|
log = logging.getLogger("rendicontazione-api.migrations")
|
|
|
|
|
|
MIGRATIONS = [
|
|
# 2026-04-18: colonne file upload su remission_invoice
|
|
"""
|
|
ALTER TABLE gepafin_rendic.remission_invoice
|
|
ADD COLUMN IF NOT EXISTS storage_path varchar(1024),
|
|
ADD COLUMN IF NOT EXISTS mime varchar(128),
|
|
ADD COLUMN IF NOT EXISTS size_bytes bigint,
|
|
ADD COLUMN IF NOT EXISTS sha256 varchar(64),
|
|
ADD COLUMN IF NOT EXISTS uploaded_by integer,
|
|
ADD COLUMN IF NOT EXISTS uploaded_at timestamptz;
|
|
""",
|
|
# 2026-04-18: colonne file upload su remission_ula_employee
|
|
"""
|
|
ALTER TABLE gepafin_rendic.remission_ula_employee
|
|
ADD COLUMN IF NOT EXISTS storage_path varchar(1024),
|
|
ADD COLUMN IF NOT EXISTS mime varchar(128),
|
|
ADD COLUMN IF NOT EXISTS size_bytes bigint,
|
|
ADD COLUMN IF NOT EXISTS sha256 varchar(64),
|
|
ADD COLUMN IF NOT EXISTS uploaded_by integer,
|
|
ADD COLUMN IF NOT EXISTS uploaded_at timestamptz;
|
|
""",
|
|
# 2026-04-18: colonne file upload su remission_document
|
|
"""
|
|
ALTER TABLE gepafin_rendic.remission_document
|
|
ADD COLUMN IF NOT EXISTS storage_path varchar(1024),
|
|
ADD COLUMN IF NOT EXISTS mime varchar(128),
|
|
ADD COLUMN IF NOT EXISTS size_bytes bigint,
|
|
ADD COLUMN IF NOT EXISTS sha256 varchar(64),
|
|
ADD COLUMN IF NOT EXISTS uploaded_by integer;
|
|
""",
|
|
# 2026-04-18 v2: multi-tranche su remission_practice
|
|
# DROP UNIQUE su application_id (permette piu tranche per stessa domanda)
|
|
# aggiunge sequence_number, period_label, suggested_instructor_id
|
|
# nuova UNIQUE (application_id, sequence_number)
|
|
# partial index su assigned_instructor_id IS NULL per coda "da assegnare"
|
|
"""
|
|
ALTER TABLE gepafin_rendic.remission_practice
|
|
DROP CONSTRAINT IF EXISTS uq_remission_practice_application;
|
|
ALTER TABLE gepafin_rendic.remission_practice
|
|
ADD COLUMN IF NOT EXISTS sequence_number integer NOT NULL DEFAULT 1,
|
|
ADD COLUMN IF NOT EXISTS period_label varchar(100),
|
|
ADD COLUMN IF NOT EXISTS suggested_instructor_id integer;
|
|
DO $$
|
|
BEGIN
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM pg_constraint
|
|
WHERE conname = 'uq_remission_practice_app_seq'
|
|
AND conrelid = 'gepafin_rendic.remission_practice'::regclass
|
|
) THEN
|
|
ALTER TABLE gepafin_rendic.remission_practice
|
|
ADD CONSTRAINT uq_remission_practice_app_seq UNIQUE (application_id, sequence_number);
|
|
END IF;
|
|
END$$;
|
|
CREATE INDEX IF NOT EXISTS idx_remission_practice_unassigned
|
|
ON gepafin_rendic.remission_practice(assigned_instructor_id)
|
|
WHERE assigned_instructor_id IS NULL;
|
|
""",
|
|
# 2026-04-18 v2: tabella custom checks
|
|
# allineata allo storage adapter esistente (storage_path + mime + size + sha256)
|
|
# NON segue le specs RAG p1 che usavano document_filename (v1 obsoleta)
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS gepafin_rendic.remission_custom_check_value (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
practice_id uuid NOT NULL REFERENCES gepafin_rendic.remission_practice(id) ON DELETE CASCADE,
|
|
check_code varchar(64) NOT NULL,
|
|
beneficiary_declared boolean NOT NULL DEFAULT false,
|
|
declared_at timestamptz,
|
|
storage_path varchar(1024),
|
|
mime varchar(128),
|
|
size_bytes bigint,
|
|
sha256 varchar(64),
|
|
document_uploaded_at timestamptz,
|
|
uploaded_by integer,
|
|
verification_status varchar(20) NOT NULL DEFAULT 'PENDING',
|
|
verification_notes text,
|
|
verified_by integer,
|
|
verified_at timestamptz,
|
|
created_at timestamptz NOT NULL DEFAULT NOW(),
|
|
updated_at timestamptz NOT NULL DEFAULT NOW(),
|
|
CONSTRAINT uq_custom_check_practice_code UNIQUE (practice_id, check_code)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_custom_check_practice
|
|
ON gepafin_rendic.remission_custom_check_value(practice_id);
|
|
""",
|
|
]
|
|
|
|
|
|
def run_migrations(engine) -> None:
|
|
"""Esegue tutte le migration in transazione. Log su ciascuna."""
|
|
with engine.begin() as conn:
|
|
for i, sql in enumerate(MIGRATIONS, 1):
|
|
try:
|
|
conn.execute(text(sql))
|
|
log.info(f"migration {i}/{len(MIGRATIONS)} OK")
|
|
except Exception as e:
|
|
log.error(f"migration {i} FAILED: {e}")
|
|
raise
|