feat(docs): link documenti dal repository company + gate submit su EXPIRED

Implementa il riutilizzo dei documenti caricati in fase domanda (gepafin_schema.company_document):
il benef puo selezionarli dal picker repository invece di caricarli dal PC, ereditando
filename/expires_at/storage_path. Tracciato via source_company_document_id per lookup
live dello stato (VALID/DUE/EXPIRED).

Modifiche:
- migrations.py: ALTER TABLE remission_document ADD source_company_document_id + index partial
- models.py: aggiunto campo source_company_document_id su RemissionDocument
- schemas.py: esposto source_company_document_id in DocumentUpsert + DocumentOut
- routers/files.py: nuovo POST /document/{id}/link-from-repository — verifica ownership
  company, pulisce file PC precedente, copia metadati dal sorgente, ritorna source_status
- routers/practices.py: nuovo check documents_not_expired in _compute_gate_check —
  JOIN live su gepafin_schema.company_document.status per doc linkati, controllo expires_at
  per upload diretti. Gate hard: documento EXPIRED blocca submit (422).

Test E2E verificati via curl/JWT offline:
- link VALID → metadati copiati, gate passed
- link EXPIRED → gate overall FAIL con detail 'Scaduti: DURC'
- re-link VALID → gate torna passed
- submit bloccato solo su check non-doc (fatture/altri doc mancanti), docs_not_expired OK

Seed sandbox: 4 document_category + 5 company_document su NAPOLI SAS (3 VALID / 1 DUE / 1 EXPIRED).
This commit is contained in:
BFLOWS
2026-04-20 18:47:03 +02:00
parent a3f863ecdb
commit 7c8de6aec8
5 changed files with 138 additions and 0 deletions

View File

@@ -117,6 +117,7 @@ class DocumentUpsert(BaseModel):
uploaded_at: Optional[datetime] = None
expires_at: Optional[date] = None
notes: Optional[str] = None
source_company_document_id: Optional[int] = None
class DocumentOut(BaseModel):
@@ -127,6 +128,7 @@ class DocumentOut(BaseModel):
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