""" ORM models per rendicontazione-api. Schema: gepafin_rendic (stesso DB del BE Gepafin sandbox). """ import uuid from datetime import datetime from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, UniqueConstraint, Numeric, Boolean, Date, BigInteger from sqlalchemy.dialects.postgresql import UUID, JSONB from sqlalchemy.sql import func from sqlalchemy.orm import relationship from .db import Base class CallRemissionSchema(Base): """ Schema di rendicontazione per un bando. Uno per call_id. status: DRAFT (modificabile) -> PUBLISHED (visibile ai beneficiari). """ __tablename__ = "call_remission_schema" __table_args__ = ( UniqueConstraint("call_id", name="uq_call_remission_schema_call_id"), {"schema": "gepafin_rendic"}, ) id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) call_id = Column(Integer, nullable=False, unique=True) schema_version = Column(Integer, nullable=False, default=1) status = Column(String(32), nullable=False, default="DRAFT") schema_json = Column(JSONB, nullable=False) created_by = Column(Integer, nullable=False) created_at = Column(DateTime(timezone=True), nullable=False, server_default=func.now()) updated_at = Column(DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()) published_at = Column(DateTime(timezone=True), nullable=True) published_by = Column(Integer, nullable=True) class RemissionPractice(Base): """ Pratica di rendicontazione di un beneficiario per una specifica application in CONTRACT_SIGNED. Uno schema_snapshot congelato alla creazione: se il superadmin modifica lo schema del bando dopo, la pratica continua a usare la versione snapshot. """ __tablename__ = "remission_practice" __table_args__ = ( UniqueConstraint("application_id", "sequence_number", name="uq_remission_practice_app_seq"), {"schema": "gepafin_rendic"}, ) id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) call_id = Column(Integer, nullable=False) application_id = Column(Integer, nullable=False) # unique (application_id, sequence_number) company_id = Column(Integer, nullable=False) user_id = Column(Integer, nullable=False) # beneficiario che compila status = Column(String(32), nullable=False, default="DRAFT") # DRAFT -> SUBMITTED -> UNDER_REVIEW -> APPROVED | REJECTED | AWAITING_AMENDMENT schema_snapshot = Column(JSONB, nullable=False) # copia schema al momento start iva_regime = Column(String(32), nullable=True) # ORDINARIO | FORFETTARIO | ESENTE amount_erogato = Column(Numeric(14, 2), nullable=False) # copiato da application.amount_accepted notes_beneficiario = Column(Text, nullable=True) # Multi-tranche v2 (2026-04-18) sequence_number = Column(Integer, nullable=False, default=1) period_label = Column(String(100), nullable=True) # libero, es "I trimestre 2021" suggested_instructor_id = Column(Integer, nullable=True) # letto da BE assigned_applications # colonne istruttoria assigned_instructor_id = Column(Integer, nullable=True) reviewed_at = Column(DateTime(timezone=True), nullable=True) reviewed_by = Column(Integer, nullable=True) rejection_reason = Column(Text, nullable=True) approved_remission = Column(Numeric(14, 2), nullable=True) instructor_final_notes = Column(Text, nullable=True) instructor_checklist = Column(JSONB, nullable=True, default=dict) verbale_date = Column(Date, nullable=True) created_at = Column(DateTime(timezone=True), nullable=False, server_default=func.now()) updated_at = Column(DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()) submitted_at = Column(DateTime(timezone=True), nullable=True) # relazioni invoices = relationship("RemissionInvoice", back_populates="practice", cascade="all, delete-orphan") ula_employees = relationship("RemissionUlaEmployee", back_populates="practice", cascade="all, delete-orphan") documents = relationship("RemissionDocument", back_populates="practice", cascade="all, delete-orphan") amendment_requests = relationship("RemissionAmendmentRequest", back_populates="practice", cascade="all, delete-orphan") custom_checks = relationship("RemissionCustomCheckValue", back_populates="practice", cascade="all, delete-orphan") class RemissionInvoice(Base): """Fattura rendicontata dentro una pratica, assegnata a una categoria.""" __tablename__ = "remission_invoice" __table_args__ = ({"schema": "gepafin_rendic"},) id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) practice_id = Column(UUID(as_uuid=True), ForeignKey("gepafin_rendic.remission_practice.id", ondelete="CASCADE"), nullable=False) category_code = Column(String(16), nullable=False) # B1 / B2 / B3 / custom invoice_number = Column(String(128), nullable=False) invoice_date = Column(Date, nullable=False) payment_date = Column(Date, nullable=False) supplier_name = Column(String(255), nullable=False) supplier_vat = Column(String(32), nullable=False) description = Column(Text, nullable=False) taxable = Column(Numeric(14, 2), nullable=False) # imponibile vat = Column(Numeric(14, 2), nullable=False, default=0) total = Column(Numeric(14, 2), nullable=False) pdf_filename = Column(String(512), nullable=True) # nome originale # File upload (bind mount /var/uploads dentro container) storage_path = Column(String(1024), nullable=True) # relativo a /var/uploads mime = Column(String(128), nullable=True) size_bytes = Column(BigInteger, nullable=True) sha256 = Column(String(64), nullable=True) uploaded_by = Column(Integer, nullable=True) uploaded_at = Column(DateTime(timezone=True), nullable=True) # Campi istruttoria (dual declared/verified) taxable_verified = Column(Numeric(14, 2), nullable=True) vat_verified = Column(Numeric(14, 2), nullable=True) total_verified = Column(Numeric(14, 2), nullable=True) verification_status = Column(String(16), nullable=False, default="PENDING") # PENDING | AMMESSA | PARZIALE | RESPINTA verification_notes = Column(Text, nullable=True) date_checks = Column(JSONB, nullable=True, default=dict) verified_by = Column(Integer, nullable=True) verified_at = Column(DateTime(timezone=True), nullable=True) created_at = Column(DateTime(timezone=True), nullable=False, server_default=func.now()) practice = relationship("RemissionPractice", back_populates="invoices") class RemissionUlaEmployee(Base): """Dipendente conteggiato nel calcolo ULA.""" __tablename__ = "remission_ula_employee" __table_args__ = ({"schema": "gepafin_rendic"},) id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) practice_id = Column(UUID(as_uuid=True), ForeignKey("gepafin_rendic.remission_practice.id", ondelete="CASCADE"), nullable=False) codice_fiscale = Column(String(16), nullable=False) full_name = Column(String(255), nullable=False) contract_type = Column(String(64), nullable=False) # T_IND / T_DET / APPR / ... role_description = Column(String(255), nullable=True) fte_pct = Column(Numeric(5, 4), nullable=False, default=1) # 0..1 period_start_date = Column(Date, nullable=False) period_end_date = Column(Date, nullable=False) supporting_doc_type = Column(String(64), nullable=True) supporting_doc_filename = Column(String(512), nullable=True) # nome originale # File upload storage_path = Column(String(1024), nullable=True) mime = Column(String(128), nullable=True) size_bytes = Column(BigInteger, nullable=True) sha256 = Column(String(64), nullable=True) uploaded_by = Column(Integer, nullable=True) uploaded_at = Column(DateTime(timezone=True), nullable=True) # Campi istruttoria fte_pct_verified = Column(Numeric(5, 4), nullable=True) verification_status = Column(String(16), nullable=False, default="PENDING") verification_notes = Column(Text, nullable=True) verified_by = Column(Integer, nullable=True) verified_at = Column(DateTime(timezone=True), nullable=True) created_at = Column(DateTime(timezone=True), nullable=False, server_default=func.now()) practice = relationship("RemissionPractice", back_populates="ula_employees") class RemissionDocument(Base): """Documento associato alla pratica (DURC, visura, ecc.).""" __tablename__ = "remission_document" __table_args__ = ({"schema": "gepafin_rendic"},) id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) practice_id = Column(UUID(as_uuid=True), ForeignKey("gepafin_rendic.remission_practice.id", ondelete="CASCADE"), nullable=False) doc_code = Column(String(64), nullable=False) # DURC / VISURA_CAMERALE / ... filename = Column(String(512), nullable=True) # nome originale uploaded_at = Column(DateTime(timezone=True), nullable=True) expires_at = Column(Date, nullable=True) notes = Column(Text, nullable=True) # File upload storage_path = Column(String(1024), nullable=True) mime = Column(String(128), nullable=True) size_bytes = Column(BigInteger, nullable=True) sha256 = Column(String(64), nullable=True) uploaded_by = Column(Integer, nullable=True) # Link al repository documenti della company (gepafin_schema.company_document). # Se valorizzato, il documento e stato selezionato dal picker repository invece # che caricato dal PC. filename/expires_at vengono copiati al momento del link. source_company_document_id = Column(Integer, nullable=True) # Campi istruttoria verification_status = Column(String(16), nullable=False, default="PENDING") # PENDING | VALIDO | NON_VALIDO | SCADUTO verification_notes = Column(Text, nullable=True) verified_by = Column(Integer, nullable=True) verified_at = Column(DateTime(timezone=True), nullable=True) practice = relationship("RemissionPractice", back_populates="documents") class RemissionAmendmentRequest(Base): """Richiesta di soccorso istruttorio: istruttore chiede integrazioni al beneficiario.""" __tablename__ = "remission_amendment_request" __table_args__ = ({"schema": "gepafin_rendic"},) id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) practice_id = Column(UUID(as_uuid=True), ForeignKey("gepafin_rendic.remission_practice.id", ondelete="CASCADE"), nullable=False) requested_by = Column(Integer, nullable=False) request_text = Column(Text, nullable=False) scope = Column(JSONB, nullable=True, default=dict) deadline = Column(Date, nullable=False) status = Column(String(32), nullable=False, default="AWAITING") # AWAITING -> RESPONSE_RECEIVED -> CLOSED | EXPIRED | REJECTED response_text = Column(Text, nullable=True) response_at = Column(DateTime(timezone=True), nullable=True) closed_at = Column(DateTime(timezone=True), nullable=True) closed_by = Column(Integer, nullable=True) # soccorso v3: extended/document/PEC tracking response_days = Column(Integer, nullable=True) extended_days = Column(Integer, nullable=True) extension_date = Column(DateTime(timezone=True), nullable=True) internal_note = Column(Text, nullable=True) amendment_document_path = Column(String(1024), nullable=True) amendment_document_type = Column(String(128), nullable=True) amendment_initial_document_path = Column(String(1024), nullable=True) response_document_path = Column(String(1024), nullable=True) response_document_type = Column(String(128), nullable=True) # popolati dal BE via endpoint interni mark-pec-sent / mark-pec-failed protocol_id = Column(String(128), nullable=True) email_log_id = Column(Integer, nullable=True) user_action_id = Column(Integer, nullable=True) pec_sent_at = Column(DateTime(timezone=True), nullable=True) pec_failed_reason = Column(Text, nullable=True) pec_retry_after = Column(DateTime(timezone=True), nullable=True) created_at = Column(DateTime(timezone=True), nullable=False, server_default=func.now()) updated_at = Column(DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()) practice = relationship("RemissionPractice", back_populates="amendment_requests") class RemissionExpirationConfig(Base): """Config data-driven per reminder scadenze amendment (speculare a BE Gepafin expiration_config). Ogni riga con type='AMENDMENT' e interval_days=N triggera un reminder esattamente N giorni prima della scadenza. Multipli row = multipli reminder.""" __tablename__ = "remission_expiration_config" __table_args__ = ({"schema": "gepafin_rendic"},) id = Column(Integer, primary_key=True, autoincrement=True) type = Column(String(50), nullable=False) interval_days = Column(Integer, nullable=False) is_deleted = Column(Boolean, nullable=False, default=False) created_at = Column(DateTime(timezone=True), nullable=False, server_default=func.now()) class RemissionCustomCheckValue(Base): """Valore di un controllo custom configurato dallo schema del bando. Schema custom_checks[] nel template definisce code/label/description/requires_document/required. Qui salviamo dichiarazione beneficiario + eventuale documento + verifica istruttore. """ __tablename__ = "remission_custom_check_value" __table_args__ = ( UniqueConstraint("practice_id", "check_code", name="uq_custom_check_practice_code"), {"schema": "gepafin_rendic"}, ) id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) practice_id = Column(UUID(as_uuid=True), ForeignKey("gepafin_rendic.remission_practice.id", ondelete="CASCADE"), nullable=False) check_code = Column(String(64), nullable=False) # es "antiriciclaggio", "polizza_fidejussoria" # Dichiarazione beneficiario beneficiary_declared = Column(Boolean, nullable=False, default=False) declared_at = Column(DateTime(timezone=True), nullable=True) # Documento allegato (se requires_document) storage_path = Column(String(1024), nullable=True) mime = Column(String(128), nullable=True) size_bytes = Column(BigInteger, nullable=True) sha256 = Column(String(64), nullable=True) document_uploaded_at = Column(DateTime(timezone=True), nullable=True) uploaded_by = Column(Integer, nullable=True) # Verifica istruttore verification_status = Column(String(20), nullable=False, default="PENDING") # PENDING | VALIDO | NON_VALIDO verification_notes = Column(Text, nullable=True) verified_by = Column(Integer, nullable=True) verified_at = Column(DateTime(timezone=True), nullable=True) created_at = Column(DateTime(timezone=True), nullable=False, server_default=func.now()) updated_at = Column(DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()) practice = relationship("RemissionPractice", back_populates="custom_checks")