feat(v2): multi-tranche DB schema + gate cumulativo 5 voci Cecilia

A1 migrations.py:
- remission_practice DROP uq_application + ADD sequence_number/period_label/suggested_instructor_id
- UNIQUE composita (application_id, sequence_number)
- partial index idx_remission_practice_unassigned su assigned_instructor_id NULL
- nuova tabella remission_custom_check_value (storage_path/mime/size/sha256 allineata adapter)

A2 models.py + templates.py:
- RemissionPractice: UniqueConstraint composita, campi multi-tranche, relationship custom_checks
- classe RemissionCustomCheckValue
- RESTART_TEMPLATE schema_version=2, max_tranches=2, custom_checks esempio
  (antiriciclaggio required no-doc, polizza_fidejussoria optional con-doc)
- upgrade_schema_to_v2 idempotente per snapshot v1 esistenti

A3 _compute_gate_check(db, practice) CUMULATIVO:
- max_remission_global = min(cap_pct * erogato, cap_abs)
- already_approved = func.sum(approved_remission) su tranche APPROVED precedenti
  dello stesso application_id con sequence_number < corrente
- max_remission_this_tranche = max(0, global - already_approved)
- pre_check_admissible = min(grand_total_declared, this_tranche)  [voce 2 Cecilia]
- remission_due = min(effective_total, this_tranche)
- residuo_da_restituire = erogato - already_approved - remission_due (cumulativo)
- output totals esteso: sequence_number, tranches_count, tranches_max
- signature (db, practice) - aggiornati 6 call site in practices/instructor/verbale

Test su NAPOLI SAS: erogato 17K, cap 8500, tranche 1 approvata 467.14EUR,
tranche 2 vuota -> residuo disponibile 8032.86EUR, residuo_da_restituire 16532.86EUR.
This commit is contained in:
BFLOWS
2026-04-18 17:35:56 +02:00
parent 6c089fb7b2
commit 25215f388b
7 changed files with 520 additions and 57 deletions

View File

@@ -64,7 +64,7 @@ def _enrich_queue_item(db: Session, p: RemissionPractice) -> InstructorQueueItem
# calcolo remissione due dalla schema_snapshot
try:
check = _compute_gate_check(p)
check = _compute_gate_check(db, p)
item.remission_due = check.totals.get("remission_due", 0)
except Exception:
item.remission_due = None
@@ -109,7 +109,7 @@ def instructor_view_practice(practice_id: UUID, db: Session = Depends(get_db),
"""Vista completa della pratica per istruttore (readonly + gate check + amendments)."""
p = _get_practice_or_404(db, practice_id)
check = _compute_gate_check(p)
check = _compute_gate_check(db, p)
amendments = [AmendmentRequestOut.model_validate(a).model_dump(mode="json") for a in p.amendment_requests]
return ApiResponse(data={
@@ -152,7 +152,7 @@ def approve_practice(practice_id: UUID, body: ReviewApproveBody,
if body.approved_remission is not None:
p.approved_remission = body.approved_remission
else:
check = _compute_gate_check(p)
check = _compute_gate_check(db, p)
p.approved_remission = Decimal(str(check.totals.get("remission_due", 0)))
p.status = "APPROVED"