""" Pydantic schemas per API. """ from typing import Optional, Any, List from datetime import datetime, date from decimal import Decimal from uuid import UUID from enum import Enum 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 ====================== # Stati formali amendment (speculare BE Gepafin ApplicationAmendmentRequestEnum, ridotti) class AmendmentStatus(str, Enum): DRAFT = "DRAFT" # istruttore prepara, PEC non ancora partita AWAITING = "AWAITING" # PEC inviata, attendo risposta benef RESPONSE_RECEIVED = "RESPONSE_RECEIVED" # benef ha risposto, istruttore deve valutare EXPIRED = "EXPIRED" # deadline passata senza risposta (scheduler) CLOSED = "CLOSED" # istruttore chiude dopo response o comunque class AmendmentRequestCreate(BaseModel): request_text: str deadline: date scope: Optional[dict] = None response_days: Optional[int] = None # pre-compilato default 15gg, variabile per istruttore internal_note: Optional[str] = None # visibile solo istruttore class AmendmentRequestUpdate(BaseModel): """Modifica amendment solo in stato DRAFT.""" request_text: Optional[str] = None deadline: Optional[date] = None scope: Optional[dict] = None response_days: Optional[int] = None internal_note: Optional[str] = None class AmendmentExtend(BaseModel): """Prolunga la deadline di un amendment AWAITING.""" extended_days: int = Field(..., gt=0, le=60) motivation: Optional[str] = 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 # soccorso v3 response_days: Optional[int] = None extended_days: Optional[int] = None extension_date: Optional[datetime] = None internal_note: Optional[str] = None amendment_document_path: Optional[str] = None amendment_document_type: Optional[str] = None response_document_path: Optional[str] = None response_document_type: Optional[str] = None protocol_id: Optional[str] = None pec_sent_at: Optional[datetime] = None pec_failed_reason: Optional[str] = None model_config = {"from_attributes": True} # ===== schemas per endpoint interni chiamati dal BE Gepafin ===== class AmendmentPendingPecOut(BaseModel): """Lista amendment che il BE deve processare per invio PEC.""" id: UUID practice_id: UUID application_id: int request_text: str deadline: date response_days: Optional[int] = None amendment_document_path: Optional[str] = None created_at: datetime class AmendmentPecDetail(BaseModel): """Dettaglio richiesto dal BE per comporre PEC.""" id: UUID practice_id: UUID application_id: int company_id: int call_id: int sequence_number: int period_label: Optional[str] = None request_text: str deadline: date response_days: Optional[int] = None amendment_document_path: Optional[str] = None class MarkPecSent(BaseModel): protocol_id: str email_log_id: Optional[int] = None user_action_id: Optional[int] = None pec_sent_at: Optional[datetime] = None class MarkPecFailed(BaseModel): reason: str retry_after: Optional[datetime] = None 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