""" Pydantic schemas per API. """ from typing import Optional, Any, List from datetime import datetime, date from decimal import Decimal from uuid import UUID from pydantic import BaseModel, Field # ====================== Schema di rendicontazione (bando) ====================== class RemissionSchemaBase(BaseModel): schema_json: dict class RemissionSchemaCreate(RemissionSchemaBase): pass class RemissionSchemaUpdate(BaseModel): schema_json: Optional[dict] = None class RemissionSchemaOut(BaseModel): id: UUID call_id: int schema_version: int status: str schema_json: dict created_by: int created_at: datetime updated_at: datetime published_at: Optional[datetime] = None published_by: Optional[int] = None model_config = {"from_attributes": True} # ====================== Pratica di rendicontazione (beneficiario) ====================== class PracticeStartRequest(BaseModel): """Input per avviare una (nuova) pratica o tranche.""" application_id: int period_label: Optional[str] = None # es "I trimestre 2021" — libero copy_ula_from_previous: bool = True # ignorato se e la prima tranche class PracticeUpdate(BaseModel): iva_regime: Optional[str] = None notes_beneficiario: Optional[str] = None # Fattura class InvoiceCreate(BaseModel): category_code: str invoice_number: str invoice_date: date payment_date: date supplier_name: str supplier_vat: str description: str taxable: Decimal vat: Decimal = Decimal("0") total: Decimal pdf_filename: Optional[str] = None class InvoiceOut(InvoiceCreate): id: UUID practice_id: UUID created_at: datetime # istruttoria taxable_verified: Optional[Decimal] = None vat_verified: Optional[Decimal] = None total_verified: Optional[Decimal] = None verification_status: str = "PENDING" verification_notes: Optional[str] = None date_checks: Optional[dict] = None verified_by: Optional[int] = None verified_at: Optional[datetime] = None model_config = {"from_attributes": True} # ULA Employee class UlaEmployeeCreate(BaseModel): codice_fiscale: str full_name: str contract_type: str role_description: Optional[str] = None fte_pct: Decimal = Decimal("1") period_start_date: date period_end_date: date supporting_doc_type: Optional[str] = None supporting_doc_filename: Optional[str] = None class UlaEmployeeOut(UlaEmployeeCreate): id: UUID practice_id: UUID created_at: datetime # istruttoria fte_pct_verified: Optional[Decimal] = None verification_status: str = "PENDING" verification_notes: Optional[str] = None verified_by: Optional[int] = None verified_at: Optional[datetime] = None model_config = {"from_attributes": True} # Document class DocumentUpsert(BaseModel): doc_code: str filename: Optional[str] = None uploaded_at: Optional[datetime] = None expires_at: Optional[date] = None notes: Optional[str] = None source_company_document_id: Optional[int] = None class DocumentOut(BaseModel): id: UUID practice_id: UUID doc_code: str filename: Optional[str] = None uploaded_at: Optional[datetime] = None expires_at: Optional[date] = None notes: Optional[str] = None source_company_document_id: Optional[int] = None # istruttoria verification_status: str = "PENDING" verification_notes: Optional[str] = None verified_by: Optional[int] = None verified_at: Optional[datetime] = None model_config = {"from_attributes": True} # Pratica dettagliata class PracticeOut(BaseModel): id: UUID call_id: int application_id: int company_id: int user_id: int status: str schema_snapshot: dict iva_regime: Optional[str] = None amount_erogato: Decimal notes_beneficiario: Optional[str] = None created_at: datetime updated_at: datetime submitted_at: Optional[datetime] = None # istruttoria assigned_instructor_id: Optional[int] = None reviewed_at: Optional[datetime] = None reviewed_by: Optional[int] = None rejection_reason: Optional[str] = None approved_remission: Optional[Decimal] = None instructor_final_notes: Optional[str] = None instructor_checklist: Optional[dict] = None verbale_date: Optional[date] = None # v2 multi-tranche sequence_number: int = 1 period_label: Optional[str] = None suggested_instructor_id: Optional[int] = None invoices: List[InvoiceOut] = [] ula_employees: List[UlaEmployeeOut] = [] documents: List[DocumentOut] = [] model_config = {"from_attributes": True} class PracticeListItem(BaseModel): """Riga leggera per liste.""" id: UUID call_id: int application_id: int company_id: int status: str amount_erogato: Decimal created_at: datetime submitted_at: Optional[datetime] = None # v2 multi-tranche sequence_number: int = 1 period_label: Optional[str] = None suggested_instructor_id: Optional[int] = None # campi denormalizzati aggiunti a runtime call_name: Optional[str] = None company_name: Optional[str] = None invoice_count: int = 0 ula_count: int = 0 document_count: int = 0 model_config = {"from_attributes": True} # Gate check class GateCheckResult(BaseModel): passed: bool checks: List[dict] # [{id, label, passed, detail}] totals: dict # {per_category: {B1: 1234.56, ...}, grand_total, max_remission_due, ...} # ====================== Istruttoria ====================== class AmendmentRequestCreate(BaseModel): request_text: str deadline: date scope: Optional[dict] = None class AmendmentResponseSubmit(BaseModel): response_text: str class AmendmentRequestOut(BaseModel): id: UUID practice_id: UUID requested_by: int request_text: str scope: Optional[dict] = None deadline: date status: str response_text: Optional[str] = None response_at: Optional[datetime] = None closed_at: Optional[datetime] = None closed_by: Optional[int] = None created_at: datetime model_config = {"from_attributes": True} class ReviewRejectBody(BaseModel): rejection_reason: str class ReviewApproveBody(BaseModel): approved_remission: Optional[Decimal] = None notes: Optional[str] = None class InstructorQueueItem(BaseModel): id: UUID call_id: int application_id: int sequence_number: int = 1 period_label: Optional[str] = None suggested_instructor_id: Optional[int] = None company_id: int status: str amount_erogato: Decimal submitted_at: Optional[datetime] = None assigned_instructor_id: Optional[int] = None call_name: Optional[str] = None company_name: Optional[str] = None invoice_count: int = 0 ula_count: int = 0 document_count: int = 0 open_amendments: int = 0 remission_due: Optional[float] = None model_config = {"from_attributes": True} # Verifica singola fattura class InvoiceVerifyBody(BaseModel): verification_status: str # AMMESSA | PARZIALE | RESPINTA taxable_verified: Optional[Decimal] = None vat_verified: Optional[Decimal] = None total_verified: Optional[Decimal] = None verification_notes: Optional[str] = None # Verifica singolo dipendente ULA class UlaVerifyBody(BaseModel): verification_status: str # AMMESSA | PARZIALE | RESPINTA fte_pct_verified: Optional[Decimal] = None verification_notes: Optional[str] = None # Verifica singolo documento class DocumentVerifyBody(BaseModel): verification_status: str # VALIDO | NON_VALIDO | SCADUTO verification_notes: Optional[str] = None # Note finali istruttore + checklist class InstructorFinalNotesBody(BaseModel): instructor_final_notes: Optional[str] = None instructor_checklist: Optional[dict] = None # ====================== Wrapper ====================== class ApiResponse(BaseModel): status: str = "SUCCESS" message: Optional[str] = None data: Optional[Any] = None # ====================== v2 Custom checks ====================== class CustomCheckDeclareBody(BaseModel): beneficiary_declared: bool class CustomCheckVerifyBody(BaseModel): verification_status: str # PENDING | VALIDO | NON_VALIDO verification_notes: Optional[str] = None class CustomCheckOut(BaseModel): """Vista merged di definition (da schema) + value (dal DB).""" code: str label: str description: Optional[str] = None requires_document: bool = False required: bool = False # valori beneficiary_declared: bool = False declared_at: Optional[datetime] = None filename_original: Optional[str] = None storage_path: Optional[str] = None size_bytes: Optional[int] = None document_uploaded_at: Optional[datetime] = None verification_status: str = "PENDING" verification_notes: Optional[str] = None verified_by: Optional[int] = None verified_at: Optional[datetime] = None # ====================== v2 Reassign istruttore ====================== class PracticeReassignBody(BaseModel): new_instructor_id: Optional[int] = None # None = unassign ritorno in coda reassignment_reason: Optional[str] = None # ====================== v2 Tranches ====================== class ApplicationTranchesSummary(BaseModel): """Riepilogo pratiche/tranche per una application.""" application_id: int call_id: int call_name: Optional[str] = None company_id: int company_name: Optional[str] = None amount_erogato: float max_tranches: int = 1 # summary tranche esistenti tranches: List[PracticeListItem] = [] # stato apertura nuova tranche can_start_new: bool = False start_blocked_reason: Optional[str] = None # importi cumulativi already_approved_sum: float = 0 max_remission_global: float = 0 max_remission_next_tranche: float = 0 class CopyUlaOption(BaseModel): """Dipendente copiabile da tranche precedente.""" codice_fiscale: str full_name: str contract_type: str role_description: Optional[str] = None fte_pct: float period_start_date: date period_end_date: date supporting_doc_type: Optional[str] = None