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 .db import engine, Base
|
||||
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")
|
||||
log = logging.getLogger("rendicontazione-api")
|
||||
@@ -42,7 +42,7 @@ async def lifespan(app: FastAPI):
|
||||
app = FastAPI(
|
||||
title="rendicontazione-api",
|
||||
description="Microservizio rendicontazione per Gepafin — sviluppato da BFLOWS",
|
||||
version="0.3.0",
|
||||
version="0.4.0",
|
||||
lifespan=lifespan,
|
||||
)
|
||||
|
||||
@@ -61,13 +61,15 @@ app.include_router(debug.router)
|
||||
app.include_router(instructor.router)
|
||||
app.include_router(files.router)
|
||||
app.include_router(verbale.router)
|
||||
app.include_router(custom_checks.router)
|
||||
app.include_router(assignment.router)
|
||||
|
||||
|
||||
@app.get("/", tags=["root"])
|
||||
def root():
|
||||
return {
|
||||
"service": "rendicontazione-api",
|
||||
"version": "0.3.0",
|
||||
"version": "0.4.0",
|
||||
"docs": "/docs",
|
||||
"health": "/health",
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ from app.models import (
|
||||
RemissionUlaEmployee,
|
||||
RemissionDocument,
|
||||
RemissionAmendmentRequest,
|
||||
RemissionCustomCheckValue,
|
||||
)
|
||||
from app.storage import save_upload, BASE_PATH
|
||||
from app.templates import RESTART_TEMPLATE
|
||||
@@ -44,6 +45,7 @@ CALL_ID = 1
|
||||
COMPANY_ID = 1
|
||||
BENEFICIARY_USER_ID = 9 # beneficiario@sandbox.local
|
||||
INSTRUCTOR_USER_ID = 10 # istruttore@sandbox.local
|
||||
MANAGER_USER_ID = 11 # manager@sandbox.local
|
||||
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)
|
||||
|
||||
|
||||
|
||||
|
||||
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
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -120,6 +143,7 @@ def do_reset(scope: str):
|
||||
if scope == 'all':
|
||||
conn.execute(text("""
|
||||
TRUNCATE
|
||||
gepafin_rendic.remission_custom_check_value,
|
||||
gepafin_rendic.remission_amendment_request,
|
||||
gepafin_rendic.remission_document,
|
||||
gepafin_rendic.remission_ula_employee,
|
||||
@@ -396,6 +420,207 @@ def scenario_napoli_sas(db, advance='draft'):
|
||||
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
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -403,7 +628,7 @@ def main():
|
||||
ap = argparse.ArgumentParser(description="Seed sandbox Gepafin rendicontazione")
|
||||
ap.add_argument('--reset', action='store_true',
|
||||
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',
|
||||
help='Stato finale della pratica dopo il seed')
|
||||
args = ap.parse_args()
|
||||
@@ -417,11 +642,17 @@ def main():
|
||||
pid = scenario_napoli_sas(db, advance=args.advance)
|
||||
print(f"\n✓ Scenario napoli-sas completato. practice_id={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':
|
||||
pid = scenario_napoli_sas(db, advance=args.advance)
|
||||
# placeholder per futuri scenari ROMA-SRL / BOLOGNA-SPA
|
||||
print(f"\n✓ Scenario 'full' eseguito (solo napoli-sas disponibile).")
|
||||
print(f" practice_id={pid}")
|
||||
pid1, pid2 = scenario_napoli_sas_multi(db)
|
||||
print(f"\n✓ Scenario full = napoli-sas-multi (solo questo disponibile).")
|
||||
print(f" tranche 1={pid1} tranche 2={pid2}")
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user