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:
@@ -4,12 +4,12 @@ Endpoint pratiche di rendicontazione (lato beneficiario).
|
||||
import copy
|
||||
from datetime import datetime, timezone
|
||||
from decimal import Decimal
|
||||
from typing import List
|
||||
from typing import List, Optional
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy import text, func
|
||||
|
||||
from ..db import get_db
|
||||
from ..auth import AuthUser, get_current_user
|
||||
@@ -23,8 +23,10 @@ from ..schemas import (
|
||||
UlaEmployeeCreate, UlaEmployeeOut,
|
||||
DocumentUpsert, DocumentOut,
|
||||
GateCheckResult,
|
||||
ApplicationTranchesSummary, CopyUlaOption,
|
||||
ApiResponse
|
||||
)
|
||||
from ..templates import upgrade_schema_to_v2
|
||||
|
||||
router = APIRouter(prefix="/api/remission-practices", tags=["remission-practices"])
|
||||
|
||||
@@ -51,7 +53,7 @@ def _ensure_editable(practice: RemissionPractice):
|
||||
)
|
||||
|
||||
|
||||
def _compute_gate_check(practice: RemissionPractice) -> GateCheckResult:
|
||||
def _compute_gate_check(db: Session, practice: RemissionPractice) -> GateCheckResult:
|
||||
"""Valuta le gate_rules dello schema snapshot contro il contenuto della pratica.
|
||||
Calcola:
|
||||
- per_category_declared: totali dichiarati dal beneficiario (sempre)
|
||||
@@ -117,12 +119,43 @@ def _compute_gate_check(practice: RemissionPractice) -> GateCheckResult:
|
||||
amt_erogato = practice.amount_erogato
|
||||
cap_pct = Decimal(str(rules.get("cap_pct_erogato", 0.5)))
|
||||
cap_abs = Decimal(str(rules.get("cap_absolute", 12500)))
|
||||
max_remission = min(cap_pct * amt_erogato, cap_abs)
|
||||
# Cap assoluto per l'application (somma di tutte le tranche ammissibili)
|
||||
max_remission_global = min(cap_pct * amt_erogato, cap_abs)
|
||||
|
||||
# Cumulativo multi-tranche v2: sommo remission approvate delle tranche precedenti
|
||||
# della stessa application per calcolare il residuo disponibile.
|
||||
already_approved = db.query(
|
||||
func.coalesce(func.sum(RemissionPractice.approved_remission), 0)
|
||||
).filter(
|
||||
RemissionPractice.application_id == practice.application_id,
|
||||
RemissionPractice.sequence_number < practice.sequence_number,
|
||||
RemissionPractice.status == 'APPROVED'
|
||||
).scalar() or 0
|
||||
already_approved = Decimal(str(already_approved))
|
||||
|
||||
max_remission_this_tranche = max(Decimal("0"), max_remission_global - already_approved)
|
||||
|
||||
# Legacy: max_remission = questo tranche (usato dai check sotto).
|
||||
max_remission = max_remission_this_tranche
|
||||
|
||||
# 5 VOCI CECILIA:
|
||||
# (1) max_remission_global
|
||||
# (2) pre_check_admissible = min(grand_total_declared, max_remission_this_tranche)
|
||||
# (3) remission_due = min(effective_total, max_remission_this_tranche)
|
||||
# (4) amount_erogato
|
||||
# (5) residuo_da_restituire = amt_erogato - SUM(approvata) (post-controllo su tutte le tranche)
|
||||
pre_check_admissible = min(grand_total, max_remission_this_tranche)
|
||||
|
||||
# Se almeno 1 verifica fatta -> uso grand_total_verified per remission_due
|
||||
# altrimenti uso grand_total (dichiarato) per preview pre-istruttoria
|
||||
effective_total = grand_total_verified if any_verified else grand_total
|
||||
remission_due = min(effective_total, max_remission)
|
||||
remission_due = min(effective_total, max_remission_this_tranche)
|
||||
|
||||
# Conteggio tranche totali per questa application (per info UI/PDF)
|
||||
tranches_count = db.query(RemissionPractice).filter(
|
||||
RemissionPractice.application_id == practice.application_id
|
||||
).count()
|
||||
tranches_max = int(rules.get("max_tranches", 1))
|
||||
|
||||
# Per compatibilità: per_category e grand_total restano "dichiarato"
|
||||
per_category = per_category_declared
|
||||
@@ -209,9 +242,17 @@ def _compute_gate_check(practice: RemissionPractice) -> GateCheckResult:
|
||||
"amount_erogato": float(amt_erogato),
|
||||
"any_verified": any_verified,
|
||||
"all_verified": all_verified,
|
||||
"residuo_da_restituire": float(max(amt_erogato - Decimal(str(remission_due)), Decimal("0"))),
|
||||
"residuo_da_restituire": float(max(amt_erogato - already_approved - Decimal(str(remission_due)), Decimal("0"))),
|
||||
"amount_basis": amount_basis,
|
||||
"use_taxable_only": use_taxable_only
|
||||
"use_taxable_only": use_taxable_only,
|
||||
# multi-tranche v2
|
||||
"max_remission_global": float(max_remission_global),
|
||||
"already_approved_previous_tranches": float(already_approved),
|
||||
"max_remission_this_tranche": float(max_remission_this_tranche),
|
||||
"pre_check_admissible": float(pre_check_admissible),
|
||||
"sequence_number": practice.sequence_number,
|
||||
"tranches_count": tranches_count,
|
||||
"tranches_max": tranches_max
|
||||
}
|
||||
)
|
||||
|
||||
@@ -235,48 +276,132 @@ def _enrich_list_item(db: Session, p: RemissionPractice) -> PracticeListItem:
|
||||
return item
|
||||
|
||||
|
||||
|
||||
def _read_original_instructor(db: Session, application_id: int) -> Optional[int]:
|
||||
"""Legge l'istruttore originariamente assegnato alla domanda nel BE Gepafin.
|
||||
Restituisce user_id solo se l'utente e ancora attivo con ruolo PRE_INSTRUCTOR o INSTRUCTOR_MANAGER.
|
||||
Altrimenti None (finira in coda 'da assegnare' per il manager).
|
||||
"""
|
||||
row = db.execute(text("""
|
||||
SELECT aa.user_id, r.role_type, u.is_deleted
|
||||
FROM gepafin_schema.assigned_applications aa
|
||||
JOIN gepafin_schema.gepafin_user u ON u.id = aa.user_id
|
||||
JOIN gepafin_schema.role r ON r.id = u.role_id
|
||||
WHERE aa.application_id = :aid
|
||||
AND aa.is_deleted = false
|
||||
AND u.is_deleted = false
|
||||
ORDER BY aa.assigned_at DESC
|
||||
LIMIT 1
|
||||
"""), {"aid": application_id}).mappings().first()
|
||||
if not row:
|
||||
return None
|
||||
if row["role_type"] not in ("ROLE_PRE_INSTRUCTOR", "ROLE_INSTRUCTOR_MANAGER"):
|
||||
return None
|
||||
return row["user_id"]
|
||||
|
||||
|
||||
def _get_schema_published(db: Session, call_id: int) -> Optional[CallRemissionSchema]:
|
||||
return db.query(CallRemissionSchema).filter(CallRemissionSchema.call_id == call_id).first()
|
||||
|
||||
|
||||
# ---------- endpoints ----------
|
||||
|
||||
@router.get("/mine", response_model=ApiResponse)
|
||||
def list_my_practices(db: Session = Depends(get_db), user: AuthUser = Depends(get_current_user)):
|
||||
"""Lista pratiche del beneficiario corrente + applications CONTRACT_SIGNED pronte per start."""
|
||||
# pratiche esistenti
|
||||
practices = db.query(RemissionPractice).filter(RemissionPractice.user_id == user.user_id).all()
|
||||
existing_app_ids = {p.application_id for p in practices}
|
||||
"""Lista pratiche del beneficiario raggruppate per application_id (v2 multi-tranche).
|
||||
Ogni application ha il riepilogo cumulativo + elenco tranche esistenti + stato apertura nuova tranche.
|
||||
"""
|
||||
# Tutte le pratiche del beneficiario ordinate per application+sequence
|
||||
practices = db.query(RemissionPractice).filter(
|
||||
RemissionPractice.user_id == user.user_id
|
||||
).order_by(
|
||||
RemissionPractice.application_id, RemissionPractice.sequence_number
|
||||
).all()
|
||||
|
||||
# applications CONTRACT_SIGNED del beneficiario che non hanno ancora una pratica
|
||||
# Raggruppo per application_id
|
||||
by_app = {}
|
||||
for p in practices:
|
||||
by_app.setdefault(p.application_id, []).append(p)
|
||||
|
||||
# Applications CONTRACT_SIGNED del beneficiario
|
||||
rows = db.execute(text("""
|
||||
SELECT a.id as application_id, a.call_id, a.company_id, a.amount_accepted,
|
||||
a.status, c.name as call_name, comp.company_name as company_name
|
||||
SELECT a.id as application_id, a.call_id, a.company_id, a.amount_accepted, a.status,
|
||||
c.name as call_name, comp.company_name as company_name
|
||||
FROM gepafin_schema.application a
|
||||
JOIN gepafin_schema.call c ON c.id = a.call_id
|
||||
LEFT JOIN gepafin_schema.company comp ON comp.id = a.company_id
|
||||
WHERE a.user_id = :uid AND a.status = 'CONTRACT_SIGNED' AND a.is_deleted = false
|
||||
ORDER BY a.id
|
||||
"""), {"uid": user.user_id}).mappings().all()
|
||||
|
||||
pending = []
|
||||
applications = []
|
||||
for r in rows:
|
||||
if r["application_id"] not in existing_app_ids:
|
||||
pending.append({
|
||||
"application_id": r["application_id"],
|
||||
"call_id": r["call_id"],
|
||||
"company_id": r["company_id"],
|
||||
"amount_erogato": float(r["amount_accepted"] or 0),
|
||||
"call_name": r["call_name"],
|
||||
"company_name": r["company_name"],
|
||||
"status": "NOT_STARTED"
|
||||
})
|
||||
app_id = r["application_id"]
|
||||
trs = by_app.get(app_id, [])
|
||||
|
||||
return ApiResponse(data={
|
||||
"practices": [_enrich_list_item(db, p).model_dump(mode="json") for p in practices],
|
||||
"ready_to_start": pending
|
||||
})
|
||||
# leggo schema del bando per max_tranches e cap
|
||||
schema = _get_schema_published(db, r["call_id"])
|
||||
rules = (schema.schema_json.get("gate_rules", {}) if schema else {}) or {}
|
||||
max_tranches = int(rules.get("max_tranches", 1))
|
||||
cap_pct = Decimal(str(rules.get("cap_pct_erogato", 0.5)))
|
||||
cap_abs = Decimal(str(rules.get("cap_absolute", 12500)))
|
||||
amt_erogato = Decimal(str(r["amount_accepted"] or 0))
|
||||
max_remission_global = min(cap_pct * amt_erogato, cap_abs)
|
||||
|
||||
already_approved_sum = sum(
|
||||
(t.approved_remission or Decimal("0")) for t in trs if t.status == "APPROVED"
|
||||
)
|
||||
max_remission_next = max(Decimal("0"), max_remission_global - already_approved_sum)
|
||||
|
||||
# Stato apertura nuova tranche
|
||||
can_start = True
|
||||
reason = None
|
||||
if len(trs) >= max_tranches:
|
||||
can_start = False
|
||||
reason = f"Limite tranches raggiunto ({max_tranches})"
|
||||
elif len(trs) > 0 and trs[-1].status not in ("APPROVED", "REJECTED"):
|
||||
can_start = False
|
||||
reason = "Completa prima la rendicontazione in corso"
|
||||
elif max_remission_next <= 0:
|
||||
can_start = False
|
||||
reason = f"Remissione massima gia raggiunta (euro {float(already_approved_sum):.2f})"
|
||||
|
||||
# Summary tranche (serialize with enriched fields)
|
||||
tranche_items = []
|
||||
for t in trs:
|
||||
item = _enrich_list_item(db, t).model_dump(mode="json")
|
||||
tranche_items.append(item)
|
||||
|
||||
applications.append({
|
||||
"application_id": app_id,
|
||||
"call_id": r["call_id"],
|
||||
"call_name": r["call_name"],
|
||||
"company_id": r["company_id"],
|
||||
"company_name": r["company_name"],
|
||||
"amount_erogato": float(amt_erogato),
|
||||
"max_tranches": max_tranches,
|
||||
"tranches": tranche_items,
|
||||
"can_start_new": can_start,
|
||||
"start_blocked_reason": reason,
|
||||
"already_approved_sum": float(already_approved_sum),
|
||||
"max_remission_global": float(max_remission_global),
|
||||
"max_remission_next_tranche": float(max_remission_next),
|
||||
})
|
||||
|
||||
return ApiResponse(data={"applications": applications})
|
||||
|
||||
|
||||
@router.post("/start", response_model=ApiResponse)
|
||||
def start_practice(body: PracticeStartRequest, db: Session = Depends(get_db),
|
||||
user: AuthUser = Depends(get_current_user)):
|
||||
"""Avvia una pratica di rendicontazione per una application CONTRACT_SIGNED."""
|
||||
"""Avvia una nuova pratica o tranche N+1 per una application CONTRACT_SIGNED.
|
||||
Validazioni server-side v2:
|
||||
- count(tranches) < max_tranches
|
||||
- last tranche in {APPROVED, REJECTED} oppure count==0
|
||||
- max_remission_this_tranche > 0
|
||||
Se sequence_number > 1 e copy_ula_from_previous=True: bulk copy ULA dalla tranche N-1
|
||||
con reset verification_*.
|
||||
"""
|
||||
# Verifica application
|
||||
app_row = db.execute(text("""
|
||||
SELECT id, call_id, company_id, user_id, status, amount_accepted
|
||||
@@ -286,28 +411,69 @@ def start_practice(body: PracticeStartRequest, db: Session = Depends(get_db),
|
||||
|
||||
if not app_row:
|
||||
raise HTTPException(status_code=404, detail=f"Application {body.application_id} non trovata")
|
||||
|
||||
if app_row["status"] != "CONTRACT_SIGNED":
|
||||
raise HTTPException(status_code=409,
|
||||
detail=f"Application in stato {app_row['status']}, richiesto CONTRACT_SIGNED")
|
||||
|
||||
if user.is_beneficiary() and app_row["user_id"] != user.user_id:
|
||||
raise HTTPException(status_code=403, detail="Application non di tua proprietà")
|
||||
raise HTTPException(status_code=403, detail="Application non di tua proprieta")
|
||||
|
||||
# Schema del bando: richiede PUBLISHED (o DRAFT se superadmin per test)
|
||||
schema = db.query(CallRemissionSchema).filter(CallRemissionSchema.call_id == app_row["call_id"]).first()
|
||||
# Schema del bando
|
||||
schema = _get_schema_published(db, app_row["call_id"])
|
||||
if not schema:
|
||||
raise HTTPException(status_code=409,
|
||||
detail="Nessuno schema di rendicontazione configurato per questo bando. "
|
||||
"Contatta l'ente gestore.")
|
||||
detail="Nessuno schema di rendicontazione configurato per questo bando.")
|
||||
if schema.status != "PUBLISHED" and user.is_beneficiary():
|
||||
raise HTTPException(status_code=409,
|
||||
detail="Lo schema di rendicontazione non è ancora stato pubblicato.")
|
||||
detail="Lo schema di rendicontazione non e ancora stato pubblicato.")
|
||||
|
||||
# Pratica esistente?
|
||||
exists = db.query(RemissionPractice).filter(RemissionPractice.application_id == body.application_id).first()
|
||||
if exists:
|
||||
raise HTTPException(status_code=409, detail="Pratica già esistente")
|
||||
# Tranche esistenti
|
||||
existing_tranches = db.query(RemissionPractice).filter(
|
||||
RemissionPractice.application_id == body.application_id
|
||||
).order_by(RemissionPractice.sequence_number).all()
|
||||
|
||||
rules = (schema.schema_json.get("gate_rules", {}) or {})
|
||||
max_tranches = int(rules.get("max_tranches", 1))
|
||||
cap_pct = Decimal(str(rules.get("cap_pct_erogato", 0.5)))
|
||||
cap_abs = Decimal(str(rules.get("cap_absolute", 12500)))
|
||||
amt_erogato = Decimal(str(app_row["amount_accepted"] or 0))
|
||||
max_remission_global = min(cap_pct * amt_erogato, cap_abs)
|
||||
|
||||
# VALIDAZIONI v2
|
||||
if len(existing_tranches) >= max_tranches:
|
||||
raise HTTPException(status_code=400,
|
||||
detail=f"Limite tranches raggiunto (max {max_tranches})")
|
||||
|
||||
if existing_tranches:
|
||||
last = existing_tranches[-1]
|
||||
if last.status not in ("APPROVED", "REJECTED"):
|
||||
raise HTTPException(status_code=400,
|
||||
detail="Completa prima la rendicontazione in corso")
|
||||
|
||||
already_approved = sum(
|
||||
(t.approved_remission or Decimal("0")) for t in existing_tranches if t.status == "APPROVED"
|
||||
)
|
||||
max_remission_this = max(Decimal("0"), max_remission_global - already_approved)
|
||||
if max_remission_this <= 0:
|
||||
raise HTTPException(status_code=400,
|
||||
detail=f"Remissione massima gia raggiunta (euro {float(already_approved):.2f})")
|
||||
|
||||
# Nuovo sequence_number
|
||||
next_seq = (existing_tranches[-1].sequence_number + 1) if existing_tranches else 1
|
||||
|
||||
# suggested_instructor: solo alla tranche 1 leggo da assigned_applications
|
||||
suggested_instructor_id = None
|
||||
assigned_instructor_id = None
|
||||
if next_seq == 1:
|
||||
suggested_instructor_id = _read_original_instructor(db, body.application_id)
|
||||
assigned_instructor_id = suggested_instructor_id
|
||||
else:
|
||||
# tranche successiva: eredita suggested dalla tranche 1, assegnato ricomincia NULL
|
||||
first = existing_tranches[0]
|
||||
suggested_instructor_id = first.suggested_instructor_id
|
||||
|
||||
# Snapshot schema aggiornato a v2 se schema_version < 2
|
||||
snapshot = copy.deepcopy(schema.schema_json)
|
||||
snapshot = upgrade_schema_to_v2(snapshot)
|
||||
|
||||
practice = RemissionPractice(
|
||||
call_id=app_row["call_id"],
|
||||
@@ -315,15 +481,70 @@ def start_practice(body: PracticeStartRequest, db: Session = Depends(get_db),
|
||||
company_id=app_row["company_id"],
|
||||
user_id=app_row["user_id"],
|
||||
status="DRAFT",
|
||||
schema_snapshot=copy.deepcopy(schema.schema_json),
|
||||
amount_erogato=app_row["amount_accepted"] or Decimal("0"),
|
||||
schema_snapshot=snapshot,
|
||||
amount_erogato=amt_erogato,
|
||||
sequence_number=next_seq,
|
||||
period_label=body.period_label,
|
||||
suggested_instructor_id=suggested_instructor_id,
|
||||
assigned_instructor_id=assigned_instructor_id,
|
||||
)
|
||||
db.add(practice)
|
||||
db.flush()
|
||||
|
||||
# Copy ULA da tranche precedente
|
||||
if next_seq > 1 and body.copy_ula_from_previous:
|
||||
prev = existing_tranches[-1]
|
||||
for prev_emp in prev.ula_employees:
|
||||
new_emp = RemissionUlaEmployee(
|
||||
practice_id=practice.id,
|
||||
codice_fiscale=prev_emp.codice_fiscale,
|
||||
full_name=prev_emp.full_name,
|
||||
contract_type=prev_emp.contract_type,
|
||||
role_description=prev_emp.role_description,
|
||||
fte_pct=prev_emp.fte_pct,
|
||||
period_start_date=prev_emp.period_start_date,
|
||||
period_end_date=prev_emp.period_end_date,
|
||||
supporting_doc_type=prev_emp.supporting_doc_type,
|
||||
# reset verification: non copiare status/notes/verified_by/verified_at
|
||||
verification_status="PENDING",
|
||||
)
|
||||
db.add(new_emp)
|
||||
|
||||
db.commit()
|
||||
db.refresh(practice)
|
||||
|
||||
return ApiResponse(message="Pratica avviata",
|
||||
data=PracticeOut.model_validate(practice).model_dump(mode="json"))
|
||||
return ApiResponse(
|
||||
message=f"Tranche {next_seq}/{max_tranches} avviata",
|
||||
data=PracticeOut.model_validate(practice).model_dump(mode="json")
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{practice_id}/copy-ula-options", response_model=ApiResponse)
|
||||
def copy_ula_options(practice_id: UUID, db: Session = Depends(get_db),
|
||||
user: AuthUser = Depends(get_current_user)):
|
||||
"""Preview dei dipendenti ULA della tranche N-1 copiabili in questa tranche N.
|
||||
Usato dal FE al click su "+Nuova rendicontazione" per mostrare il pre-fill."""
|
||||
p = _get_practice_or_404(db, practice_id, user)
|
||||
if p.sequence_number <= 1:
|
||||
return ApiResponse(data={"options": [], "previous_sequence": None})
|
||||
prev = db.query(RemissionPractice).filter(
|
||||
RemissionPractice.application_id == p.application_id,
|
||||
RemissionPractice.sequence_number == p.sequence_number - 1
|
||||
).first()
|
||||
if not prev:
|
||||
return ApiResponse(data={"options": [], "previous_sequence": None})
|
||||
options = [CopyUlaOption(
|
||||
codice_fiscale=e.codice_fiscale,
|
||||
full_name=e.full_name,
|
||||
contract_type=e.contract_type,
|
||||
role_description=e.role_description,
|
||||
fte_pct=float(e.fte_pct),
|
||||
period_start_date=e.period_start_date,
|
||||
period_end_date=e.period_end_date,
|
||||
supporting_doc_type=e.supporting_doc_type,
|
||||
).model_dump(mode="json") for e in prev.ula_employees]
|
||||
return ApiResponse(data={"options": options, "previous_sequence": prev.sequence_number,
|
||||
"previous_id": str(prev.id)})
|
||||
|
||||
|
||||
@router.get("/{practice_id}", response_model=ApiResponse)
|
||||
@@ -451,7 +672,7 @@ def clear_document(practice_id: UUID, doc_code: str,
|
||||
def gate_check(practice_id: UUID, db: Session = Depends(get_db),
|
||||
user: AuthUser = Depends(get_current_user)):
|
||||
p = _get_practice_or_404(db, practice_id, user)
|
||||
result = _compute_gate_check(p)
|
||||
result = _compute_gate_check(db, p)
|
||||
return ApiResponse(data=result.model_dump(mode="json"))
|
||||
|
||||
|
||||
@@ -461,7 +682,7 @@ def submit_practice(practice_id: UUID, db: Session = Depends(get_db),
|
||||
p = _get_practice_or_404(db, practice_id, user)
|
||||
_ensure_editable(p)
|
||||
|
||||
check = _compute_gate_check(p)
|
||||
check = _compute_gate_check(db, p)
|
||||
if not check.passed:
|
||||
raise HTTPException(status_code=422, detail={
|
||||
"message": "Gate rules non soddisfatte",
|
||||
|
||||
Reference in New Issue
Block a user