feat(v2): seed scenario napoli-sas-multi + main include routers
A7 scripts/seed_sandbox.py: - ensure_assigned_application() popola gepafin_schema.assigned_applications - scenario napoli-sas-multi: tranche 1 APPROVED + tranche 2 DRAFT vuota Tranche 1 caso reale Cecilia: 1 fattura B3 524.50 EUR con rettifica 57.36 EUR (storno assicurazione), ammesso 467.14 EUR. 1 ULA T_IND AMMESSA. 4 documenti VALIDO. 2 custom_checks VALIDO (antiriciclaggio + polizza con PDF). Tranche 2 DRAFT: assigned_instructor_id=NULL (simula workflow capo) - TRUNCATE include remission_custom_check_value (CASCADE gia la gestiva) main.py: include router custom_checks + assignment, version bump 0.4.0 Test: seed --reset --scenario=napoli-sas-multi -> 2 tranche create in 6s, PDF polizza 10KB generato in custom_checks/<T1>/polizza_fidejussoria/.
This commit is contained in:
@@ -15,7 +15,7 @@ from sqlalchemy import text
|
|||||||
from .config import get_settings
|
from .config import get_settings
|
||||||
from .db import engine, Base
|
from .db import engine, Base
|
||||||
from .migrations import run_migrations
|
from .migrations import run_migrations
|
||||||
from .routers import health, schemas, practices, debug, instructor, files, verbale
|
from .routers import health, schemas, practices, debug, instructor, files, verbale, custom_checks, assignment
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s")
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s")
|
||||||
log = logging.getLogger("rendicontazione-api")
|
log = logging.getLogger("rendicontazione-api")
|
||||||
@@ -42,7 +42,7 @@ async def lifespan(app: FastAPI):
|
|||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="rendicontazione-api",
|
title="rendicontazione-api",
|
||||||
description="Microservizio rendicontazione per Gepafin — sviluppato da BFLOWS",
|
description="Microservizio rendicontazione per Gepafin — sviluppato da BFLOWS",
|
||||||
version="0.3.0",
|
version="0.4.0",
|
||||||
lifespan=lifespan,
|
lifespan=lifespan,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -61,13 +61,15 @@ app.include_router(debug.router)
|
|||||||
app.include_router(instructor.router)
|
app.include_router(instructor.router)
|
||||||
app.include_router(files.router)
|
app.include_router(files.router)
|
||||||
app.include_router(verbale.router)
|
app.include_router(verbale.router)
|
||||||
|
app.include_router(custom_checks.router)
|
||||||
|
app.include_router(assignment.router)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/", tags=["root"])
|
@app.get("/", tags=["root"])
|
||||||
def root():
|
def root():
|
||||||
return {
|
return {
|
||||||
"service": "rendicontazione-api",
|
"service": "rendicontazione-api",
|
||||||
"version": "0.3.0",
|
"version": "0.4.0",
|
||||||
"docs": "/docs",
|
"docs": "/docs",
|
||||||
"health": "/health",
|
"health": "/health",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ from app.models import (
|
|||||||
RemissionUlaEmployee,
|
RemissionUlaEmployee,
|
||||||
RemissionDocument,
|
RemissionDocument,
|
||||||
RemissionAmendmentRequest,
|
RemissionAmendmentRequest,
|
||||||
|
RemissionCustomCheckValue,
|
||||||
)
|
)
|
||||||
from app.storage import save_upload, BASE_PATH
|
from app.storage import save_upload, BASE_PATH
|
||||||
from app.templates import RESTART_TEMPLATE
|
from app.templates import RESTART_TEMPLATE
|
||||||
@@ -44,6 +45,7 @@ CALL_ID = 1
|
|||||||
COMPANY_ID = 1
|
COMPANY_ID = 1
|
||||||
BENEFICIARY_USER_ID = 9 # beneficiario@sandbox.local
|
BENEFICIARY_USER_ID = 9 # beneficiario@sandbox.local
|
||||||
INSTRUCTOR_USER_ID = 10 # istruttore@sandbox.local
|
INSTRUCTOR_USER_ID = 10 # istruttore@sandbox.local
|
||||||
|
MANAGER_USER_ID = 11 # manager@sandbox.local
|
||||||
APPLICATION_ID = 1
|
APPLICATION_ID = 1
|
||||||
|
|
||||||
|
|
||||||
@@ -110,6 +112,27 @@ def attach_pdf(db, entity, entity_type: str, application_id: int,
|
|||||||
entity.uploaded_at = datetime.now(timezone.utc)
|
entity.uploaded_at = datetime.now(timezone.utc)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_assigned_application(db):
|
||||||
|
"""Popola gepafin_schema.assigned_applications per abilitare suggested_instructor_id
|
||||||
|
alla creazione della prima tranche. Idempotente."""
|
||||||
|
from sqlalchemy import text
|
||||||
|
existing = db.execute(text("""
|
||||||
|
SELECT id FROM gepafin_schema.assigned_applications
|
||||||
|
WHERE application_id = :aid AND user_id = :uid AND is_deleted = false
|
||||||
|
"""), {"aid": APPLICATION_ID, "uid": INSTRUCTOR_USER_ID}).scalar()
|
||||||
|
if existing:
|
||||||
|
print(f"[assigned_applications] gia presente (id={existing})")
|
||||||
|
return
|
||||||
|
db.execute(text("""
|
||||||
|
INSERT INTO gepafin_schema.assigned_applications
|
||||||
|
(user_id, assigned_by, application_id, status, is_deleted, assigned_at, created_date, updated_date)
|
||||||
|
VALUES (:uid, :admin, :aid, 'ASSIGNED', false, NOW(), NOW(), NOW())
|
||||||
|
"""), {"uid": INSTRUCTOR_USER_ID, "admin": 8, "aid": APPLICATION_ID})
|
||||||
|
db.commit()
|
||||||
|
print(f"[assigned_applications] user={INSTRUCTOR_USER_ID} assegnato a application={APPLICATION_ID}")
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Reset
|
# Reset
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -120,6 +143,7 @@ def do_reset(scope: str):
|
|||||||
if scope == 'all':
|
if scope == 'all':
|
||||||
conn.execute(text("""
|
conn.execute(text("""
|
||||||
TRUNCATE
|
TRUNCATE
|
||||||
|
gepafin_rendic.remission_custom_check_value,
|
||||||
gepafin_rendic.remission_amendment_request,
|
gepafin_rendic.remission_amendment_request,
|
||||||
gepafin_rendic.remission_document,
|
gepafin_rendic.remission_document,
|
||||||
gepafin_rendic.remission_ula_employee,
|
gepafin_rendic.remission_ula_employee,
|
||||||
@@ -396,6 +420,207 @@ def scenario_napoli_sas(db, advance='draft'):
|
|||||||
return practice.id
|
return practice.id
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def scenario_napoli_sas_multi(db):
|
||||||
|
"""Scenario multi-tranche:
|
||||||
|
- tranche 1 APPROVED con 1 fattura B3 524.50€, rettifica 57.36€ assicurazione, ammesso 467.14€
|
||||||
|
(caso reale pratica 5888 di Cecilia)
|
||||||
|
- tranche 2 DRAFT vuota, pronta per la demo
|
||||||
|
|
||||||
|
Popola anche:
|
||||||
|
- assigned_applications (istruttore originariamente assegnato)
|
||||||
|
- 2 custom_checks dichiarati + polizza con PDF allegato su tranche 1
|
||||||
|
"""
|
||||||
|
schema_row = ensure_schema_published(db)
|
||||||
|
ensure_assigned_application(db)
|
||||||
|
|
||||||
|
# ---------- Tranche 1 APPROVED ----------
|
||||||
|
practice1 = RemissionPractice(
|
||||||
|
call_id=CALL_ID,
|
||||||
|
application_id=APPLICATION_ID,
|
||||||
|
company_id=COMPANY_ID,
|
||||||
|
user_id=BENEFICIARY_USER_ID,
|
||||||
|
status="APPROVED",
|
||||||
|
schema_snapshot=schema_row.schema_json,
|
||||||
|
iva_regime="ORDINARIO",
|
||||||
|
amount_erogato=Decimal("17000"),
|
||||||
|
sequence_number=1,
|
||||||
|
period_label="I fase 2021",
|
||||||
|
suggested_instructor_id=INSTRUCTOR_USER_ID,
|
||||||
|
assigned_instructor_id=INSTRUCTOR_USER_ID,
|
||||||
|
approved_remission=Decimal("467.14"),
|
||||||
|
reviewed_at=datetime.now(timezone.utc),
|
||||||
|
reviewed_by=INSTRUCTOR_USER_ID,
|
||||||
|
instructor_final_notes="Pratica tranche I: ammessa 1 fattura B3 con rettifica quota assicurativa.",
|
||||||
|
submitted_at=datetime.now(timezone.utc),
|
||||||
|
)
|
||||||
|
db.add(practice1)
|
||||||
|
db.flush()
|
||||||
|
print(f"[practice] tranche 1 APPROVED id={practice1.id}")
|
||||||
|
|
||||||
|
# 1 fattura B3 con PARZIALE (storno 57.36)
|
||||||
|
inv1 = RemissionInvoice(
|
||||||
|
practice_id=practice1.id,
|
||||||
|
category_code="B3",
|
||||||
|
invoice_number="2021/042",
|
||||||
|
invoice_date=date(2021, 4, 15),
|
||||||
|
payment_date=date(2021, 4, 30),
|
||||||
|
supplier_name="Formazione Digitale S.r.l.",
|
||||||
|
supplier_vat="IT03521460542",
|
||||||
|
description="Corso di formazione digitale 40h + quota assicurazione partecipanti",
|
||||||
|
taxable=Decimal("524.50"),
|
||||||
|
vat=Decimal("115.39"),
|
||||||
|
total=Decimal("639.89"),
|
||||||
|
taxable_verified=Decimal("467.14"),
|
||||||
|
vat_verified=Decimal("102.77"),
|
||||||
|
total_verified=Decimal("569.91"),
|
||||||
|
verification_status="PARZIALE",
|
||||||
|
verification_notes="Storno di 57.36 EUR per quota assicurazione partecipanti non ammissibile (non rientra nelle spese formative dirette).",
|
||||||
|
verified_by=INSTRUCTOR_USER_ID,
|
||||||
|
verified_at=datetime.now(timezone.utc),
|
||||||
|
)
|
||||||
|
db.add(inv1)
|
||||||
|
db.flush()
|
||||||
|
attach_pdf(
|
||||||
|
db, inv1, "invoice", APPLICATION_ID,
|
||||||
|
filename="ft_2021_042_formazione.pdf",
|
||||||
|
title=f"Fattura n. {inv1.invoice_number}",
|
||||||
|
subtitle=f"{inv1.supplier_name}",
|
||||||
|
lines=[
|
||||||
|
f"Fornitore: {inv1.supplier_name} P.IVA {inv1.supplier_vat}",
|
||||||
|
f"Descrizione: {inv1.description}",
|
||||||
|
f"Imponibile: EUR {inv1.taxable}",
|
||||||
|
f"IVA 22%: EUR {inv1.vat}",
|
||||||
|
f"Totale: EUR {inv1.total}",
|
||||||
|
],
|
||||||
|
uploader_id=BENEFICIARY_USER_ID,
|
||||||
|
)
|
||||||
|
print(f"[invoice T1] B3 2021/042 PARZIALE + PDF {inv1.size_bytes}b")
|
||||||
|
|
||||||
|
# 1 ULA T_IND 1.0 AMMESSA
|
||||||
|
emp1 = RemissionUlaEmployee(
|
||||||
|
practice_id=practice1.id,
|
||||||
|
codice_fiscale="RSSMRA85T10H501Z",
|
||||||
|
full_name="Mario Rossi",
|
||||||
|
contract_type="T_IND",
|
||||||
|
role_description="Sviluppatore senior",
|
||||||
|
fte_pct=Decimal("1.0000"),
|
||||||
|
fte_pct_verified=Decimal("1.0000"),
|
||||||
|
period_start_date=date(2021, 1, 27),
|
||||||
|
period_end_date=date(2021, 12, 31),
|
||||||
|
supporting_doc_type="LUL",
|
||||||
|
verification_status="AMMESSA",
|
||||||
|
verified_by=INSTRUCTOR_USER_ID,
|
||||||
|
verified_at=datetime.now(timezone.utc),
|
||||||
|
)
|
||||||
|
db.add(emp1)
|
||||||
|
db.flush()
|
||||||
|
attach_pdf(
|
||||||
|
db, emp1, "ula", APPLICATION_ID,
|
||||||
|
filename="lul_rossi_2021_t1.pdf",
|
||||||
|
title=f"LUL {emp1.full_name}",
|
||||||
|
subtitle=f"{emp1.period_start_date} to {emp1.period_end_date}",
|
||||||
|
lines=[f"CF: {emp1.codice_fiscale}", f"FTE: 1.00", "Contratto: T_IND"],
|
||||||
|
uploader_id=BENEFICIARY_USER_ID,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Documenti validati
|
||||||
|
for code, label in [("DURC", "DURC"), ("VISURA_CAMERALE", "Visura"),
|
||||||
|
("BILANCIO", "Bilancio 2021"), ("ANTIRICICLAGGIO", "Antiriciclaggio")]:
|
||||||
|
doc = RemissionDocument(
|
||||||
|
practice_id=practice1.id,
|
||||||
|
doc_code=code,
|
||||||
|
verification_status="VALIDO",
|
||||||
|
verified_by=INSTRUCTOR_USER_ID,
|
||||||
|
verified_at=datetime.now(timezone.utc),
|
||||||
|
)
|
||||||
|
db.add(doc)
|
||||||
|
db.flush()
|
||||||
|
attach_pdf(
|
||||||
|
db, doc, "document", APPLICATION_ID,
|
||||||
|
filename=f"{code.lower()}_napoli_t1.pdf",
|
||||||
|
title=label,
|
||||||
|
subtitle="Tranche I — NAPOLI SAS",
|
||||||
|
lines=["Documento valido", "Approvato dall istruttore"],
|
||||||
|
uploader_id=BENEFICIARY_USER_ID,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Custom checks tranche 1: antiriciclaggio dichiarato + polizza con PDF
|
||||||
|
cc_antir = RemissionCustomCheckValue(
|
||||||
|
practice_id=practice1.id,
|
||||||
|
check_code="antiriciclaggio",
|
||||||
|
beneficiary_declared=True,
|
||||||
|
declared_at=datetime.now(timezone.utc),
|
||||||
|
verification_status="VALIDO",
|
||||||
|
verified_by=INSTRUCTOR_USER_ID,
|
||||||
|
verified_at=datetime.now(timezone.utc),
|
||||||
|
)
|
||||||
|
db.add(cc_antir)
|
||||||
|
|
||||||
|
cc_polizza = RemissionCustomCheckValue(
|
||||||
|
practice_id=practice1.id,
|
||||||
|
check_code="polizza_fidejussoria",
|
||||||
|
beneficiary_declared=True,
|
||||||
|
declared_at=datetime.now(timezone.utc),
|
||||||
|
verification_status="VALIDO",
|
||||||
|
verified_by=INSTRUCTOR_USER_ID,
|
||||||
|
verified_at=datetime.now(timezone.utc),
|
||||||
|
)
|
||||||
|
db.add(cc_polizza)
|
||||||
|
db.flush()
|
||||||
|
# Genero PDF polizza e lo salvo direttamente in custom_checks/
|
||||||
|
from pathlib import Path as _P
|
||||||
|
pdf = make_pdf_bytes(
|
||||||
|
"Polizza fidejussoria tranche I",
|
||||||
|
"NAPOLI SAS Sandbox — garanzia bando RE-START",
|
||||||
|
[
|
||||||
|
"Compagnia: Generali Assicurazioni",
|
||||||
|
"Importo garantito: EUR 17.000",
|
||||||
|
"Data emissione: 15/01/2021",
|
||||||
|
"Scadenza: 31/12/2022",
|
||||||
|
"N. polizza: FID-2021-NS-0042",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
import hashlib
|
||||||
|
digest = hashlib.sha256(pdf).hexdigest()
|
||||||
|
target_dir = BASE_PATH / "custom_checks" / str(practice1.id) / "polizza_fidejussoria"
|
||||||
|
target_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
target_file = target_dir / f"{digest[:12]}-polizza_fidejussoria.pdf"
|
||||||
|
target_file.write_bytes(pdf)
|
||||||
|
cc_polizza.storage_path = str(target_file.relative_to(BASE_PATH))
|
||||||
|
cc_polizza.mime = "application/pdf"
|
||||||
|
cc_polizza.size_bytes = len(pdf)
|
||||||
|
cc_polizza.sha256 = digest
|
||||||
|
cc_polizza.document_uploaded_at = datetime.now(timezone.utc)
|
||||||
|
cc_polizza.uploaded_by = BENEFICIARY_USER_ID
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
print(f"[custom_checks T1] antiriciclaggio VALIDO, polizza VALIDO + PDF {len(pdf)}b")
|
||||||
|
|
||||||
|
# ---------- Tranche 2 DRAFT vuota ----------
|
||||||
|
practice2 = RemissionPractice(
|
||||||
|
call_id=CALL_ID,
|
||||||
|
application_id=APPLICATION_ID,
|
||||||
|
company_id=COMPANY_ID,
|
||||||
|
user_id=BENEFICIARY_USER_ID,
|
||||||
|
status="DRAFT",
|
||||||
|
schema_snapshot=schema_row.schema_json,
|
||||||
|
iva_regime="ORDINARIO",
|
||||||
|
amount_erogato=Decimal("17000"),
|
||||||
|
sequence_number=2,
|
||||||
|
period_label="II fase 2021",
|
||||||
|
suggested_instructor_id=INSTRUCTOR_USER_ID,
|
||||||
|
assigned_instructor_id=None, # non ancora assegnata (simulo workflow capo)
|
||||||
|
)
|
||||||
|
db.add(practice2)
|
||||||
|
db.flush()
|
||||||
|
print(f"[practice] tranche 2 DRAFT id={practice2.id} (vuota, pronta demo)")
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
return practice1.id, practice2.id
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Main
|
# Main
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -403,7 +628,7 @@ def main():
|
|||||||
ap = argparse.ArgumentParser(description="Seed sandbox Gepafin rendicontazione")
|
ap = argparse.ArgumentParser(description="Seed sandbox Gepafin rendicontazione")
|
||||||
ap.add_argument('--reset', action='store_true',
|
ap.add_argument('--reset', action='store_true',
|
||||||
help='Cancella tutti i dati remission_* e pulisci storage prima del seed')
|
help='Cancella tutti i dati remission_* e pulisci storage prima del seed')
|
||||||
ap.add_argument('--scenario', choices=['napoli-sas', 'full'], default='napoli-sas')
|
ap.add_argument('--scenario', choices=['napoli-sas', 'napoli-sas-multi', 'full'], default='napoli-sas')
|
||||||
ap.add_argument('--advance', choices=['draft', 'submitted', 'under_review'], default='under_review',
|
ap.add_argument('--advance', choices=['draft', 'submitted', 'under_review'], default='under_review',
|
||||||
help='Stato finale della pratica dopo il seed')
|
help='Stato finale della pratica dopo il seed')
|
||||||
args = ap.parse_args()
|
args = ap.parse_args()
|
||||||
@@ -417,11 +642,17 @@ def main():
|
|||||||
pid = scenario_napoli_sas(db, advance=args.advance)
|
pid = scenario_napoli_sas(db, advance=args.advance)
|
||||||
print(f"\n✓ Scenario napoli-sas completato. practice_id={pid}")
|
print(f"\n✓ Scenario napoli-sas completato. practice_id={pid}")
|
||||||
print(f" Accedi a: http://78.46.41.91:18072/istruttoria/{pid}")
|
print(f" Accedi a: http://78.46.41.91:18072/istruttoria/{pid}")
|
||||||
|
elif args.scenario == 'napoli-sas-multi':
|
||||||
|
pid1, pid2 = scenario_napoli_sas_multi(db)
|
||||||
|
print(f"\n✓ Scenario napoli-sas-multi completato")
|
||||||
|
print(f" tranche 1 APPROVED id={pid1}")
|
||||||
|
print(f" tranche 2 DRAFT id={pid2}")
|
||||||
|
print(f" Istruttoria T1: http://78.46.41.91:18072/istruttoria/{pid1}")
|
||||||
|
print(f" Rendicontazione T2: http://78.46.41.91:18072/rendicontazioni/{pid2}")
|
||||||
elif args.scenario == 'full':
|
elif args.scenario == 'full':
|
||||||
pid = scenario_napoli_sas(db, advance=args.advance)
|
pid1, pid2 = scenario_napoli_sas_multi(db)
|
||||||
# placeholder per futuri scenari ROMA-SRL / BOLOGNA-SPA
|
print(f"\n✓ Scenario full = napoli-sas-multi (solo questo disponibile).")
|
||||||
print(f"\n✓ Scenario 'full' eseguito (solo napoli-sas disponibile).")
|
print(f" tranche 1={pid1} tranche 2={pid2}")
|
||||||
print(f" practice_id={pid}")
|
|
||||||
finally:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user