feat: endpoint pratiche rendicontazione (lato beneficiario)

- 4 nuove tabelle: remission_practice, remission_invoice, remission_ula_employee, remission_document
  con cascade delete e FK
- 13 endpoint /api/remission-practices/*:
  GET /mine (lista pratiche user + applications CONTRACT_SIGNED ready_to_start)
  POST /start (avvia pratica da application_id, richiede schema PUBLISHED)
  GET /{id}, PUT /{id} (regime IVA + note)
  POST/DELETE /{id}/invoices
  POST/DELETE /{id}/ula-employees
  PUT/DELETE /{id}/documents/{doc_code}
  GET /{id}/gate-check (valida gate rules contro pratica, ritorna totali + checks)
  POST /{id}/submit (gate-check obbligatorio, status DRAFT -> SUBMITTED)
- 1 endpoint debug /api/debug/impersonate (sandbox-only, genera JWT per utente
  - necessario perche' /v1/user/login del BE Spring esclude ROLE_BENEFICIARY)
- Gate check calcola: totali per categoria, grand_total, max_remission = min(cap_pct*erogato, cap_abs),
  remission_due = min(grand_total, max_remission), applica iva_ordinario_imponibile_only
This commit is contained in:
BFLOWS
2026-04-18 09:51:06 +02:00
parent 63fd2f66e6
commit e217f15e5a
5 changed files with 728 additions and 5 deletions

View File

@@ -4,9 +4,10 @@ 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
from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, UniqueConstraint, Numeric, Boolean, Date
from sqlalchemy.dialects.postgresql import UUID, JSONB
from sqlalchemy.sql import func
from sqlalchemy.orm import relationship
from .db import Base
@@ -32,3 +33,111 @@ class CallRemissionSchema(Base):
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", name="uq_remission_practice_application"),
{"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=True)
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)
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")
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) # per ora solo nome, upload vero dopo
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)
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)
uploaded_at = Column(DateTime(timezone=True), nullable=True)
expires_at = Column(Date, nullable=True)
notes = Column(Text, nullable=True)
practice = relationship("RemissionPractice", back_populates="documents")