695 lines
25 KiB
Python
695 lines
25 KiB
Python
"""
|
|
QDRO API
|
|
|
|
Endpoints:
|
|
- POST /api/qdros
|
|
- GET /api/qdros/{file_no}
|
|
- GET /api/qdros/item/{qdro_id}
|
|
- PUT /api/qdros/{qdro_id}
|
|
- DELETE /api/qdros/{qdro_id}
|
|
- POST /api/qdros/{qdro_id}/versions
|
|
- GET /api/qdros/{qdro_id}/versions
|
|
- POST /api/qdros/{qdro_id}/calculate-division
|
|
- POST /api/qdros/{qdro_id}/generate-document
|
|
- GET /api/qdros/{qdro_id}/communications
|
|
- POST /api/qdros/{qdro_id}/communications
|
|
|
|
Plan Info:
|
|
- POST /api/plan-info
|
|
- GET /api/plan-info
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from typing import Any, Dict, List, Optional, Union
|
|
from datetime import date, datetime
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
|
from pydantic import BaseModel, ConfigDict, Field
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.database.base import get_db
|
|
from app.auth.security import get_current_user
|
|
from app.models.user import User
|
|
from app.models.files import File
|
|
from app.models.qdro import QDRO, QDROVersion, QDROCommunication
|
|
from app.models.lookups import PlanInfo
|
|
from app.services.audit import audit_service
|
|
from app.services.query_utils import apply_sorting, paginate_with_total
|
|
from app.services.notification import notification_service, resolve_qdro_routes
|
|
from app.models.templates import DocumentTemplate, DocumentTemplateVersion
|
|
from app.services.storage import get_default_storage
|
|
from app.services.template_merge import extract_tokens_from_bytes, build_context, resolve_tokens, render_docx
|
|
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
class QDROBase(BaseModel):
|
|
file_no: str
|
|
version: str = "01"
|
|
plan_id: Optional[str] = None
|
|
form_name: Optional[str] = None
|
|
status: str = "DRAFT"
|
|
case_number: Optional[str] = None
|
|
notes: Optional[str] = None
|
|
# Dates
|
|
judgment_date: Optional[date] = None
|
|
valuation_date: Optional[date] = None
|
|
married_on: Optional[date] = None
|
|
# Parties
|
|
pet: Optional[str] = None
|
|
res: Optional[str] = None
|
|
# Award info (percent string like "50%" or free text)
|
|
percent_awarded: Optional[str] = None
|
|
|
|
|
|
class QDROCreate(QDROBase):
|
|
pass
|
|
|
|
|
|
class QDROUpdate(BaseModel):
|
|
version: Optional[str] = None
|
|
plan_id: Optional[str] = None
|
|
form_name: Optional[str] = None
|
|
status: Optional[str] = None
|
|
case_number: Optional[str] = None
|
|
notes: Optional[str] = None
|
|
judgment_date: Optional[date] = None
|
|
valuation_date: Optional[date] = None
|
|
married_on: Optional[date] = None
|
|
pet: Optional[str] = None
|
|
res: Optional[str] = None
|
|
percent_awarded: Optional[str] = None
|
|
approved_date: Optional[date] = None
|
|
filed_date: Optional[date] = None
|
|
|
|
|
|
class QDROResponse(QDROBase):
|
|
id: int
|
|
approval_status: Optional[str] = None
|
|
approved_date: Optional[date] = None
|
|
filed_date: Optional[date] = None
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
|
|
class PaginatedQDROResponse(BaseModel):
|
|
items: List[QDROResponse]
|
|
total: int
|
|
|
|
|
|
@router.post("/qdros", response_model=QDROResponse, summary="Create a new QDRO linked to a file")
|
|
async def create_qdro(
|
|
payload: QDROCreate,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
file_obj = db.query(File).filter(File.file_no == payload.file_no).first()
|
|
if not file_obj:
|
|
raise HTTPException(status_code=404, detail="File not found")
|
|
allowed = {c.name for c in QDRO.__table__.columns}
|
|
data = {k: v for k, v in payload.model_dump(exclude_unset=True).items() if k in allowed}
|
|
qdro = QDRO(**data)
|
|
db.add(qdro)
|
|
db.commit()
|
|
db.refresh(qdro)
|
|
try:
|
|
audit_service.log_action(db, action="CREATE", resource_type="QDRO", user=current_user, resource_id=qdro.id, details={"file_no": qdro.file_no})
|
|
except Exception:
|
|
pass
|
|
return qdro
|
|
|
|
|
|
@router.get("/qdros/{file_no}", response_model=Union[List[QDROResponse], PaginatedQDROResponse], summary="List QDROs by file")
|
|
async def list_qdros_by_file(
|
|
file_no: str,
|
|
skip: int = Query(0, ge=0),
|
|
limit: int = Query(50, ge=1, le=200),
|
|
sort_by: Optional[str] = Query("updated", description="Sort by: updated|created|version|status"),
|
|
sort_dir: Optional[str] = Query("desc", description="Sort direction: asc|desc"),
|
|
include_total: bool = Query(False),
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
q = db.query(QDRO).filter(QDRO.file_no == file_no)
|
|
q = apply_sorting(
|
|
q,
|
|
sort_by,
|
|
sort_dir,
|
|
allowed={
|
|
"updated": [QDRO.updated_at, QDRO.id],
|
|
"created": [QDRO.created_at, QDRO.id],
|
|
"version": [QDRO.version],
|
|
"status": [QDRO.status],
|
|
},
|
|
)
|
|
items, total = paginate_with_total(q, skip, limit, include_total)
|
|
if include_total:
|
|
return {"items": items, "total": total or 0}
|
|
return items
|
|
|
|
|
|
@router.get("/qdros/item/{qdro_id}", response_model=QDROResponse, summary="Get a QDRO by id")
|
|
async def get_qdro(
|
|
qdro_id: int,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
qdro = db.query(QDRO).filter(QDRO.id == qdro_id).first()
|
|
if not qdro:
|
|
raise HTTPException(status_code=404, detail="QDRO not found")
|
|
return qdro
|
|
|
|
|
|
@router.put("/qdros/{qdro_id}", response_model=QDROResponse, summary="Update a QDRO")
|
|
async def update_qdro(
|
|
qdro_id: int,
|
|
payload: QDROUpdate,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
qdro = db.query(QDRO).filter(QDRO.id == qdro_id).first()
|
|
if not qdro:
|
|
raise HTTPException(status_code=404, detail="QDRO not found")
|
|
allowed = {c.name for c in QDRO.__table__.columns}
|
|
changes = {}
|
|
for k, v in payload.model_dump(exclude_unset=True).items():
|
|
if k in allowed:
|
|
setattr(qdro, k, v)
|
|
changes[k] = v
|
|
db.commit()
|
|
db.refresh(qdro)
|
|
try:
|
|
audit_service.log_action(db, action="UPDATE", resource_type="QDRO", user=current_user, resource_id=qdro.id, details=changes)
|
|
except Exception:
|
|
pass
|
|
return qdro
|
|
|
|
|
|
# -----------------------------
|
|
# Workflow: status transitions
|
|
# -----------------------------
|
|
|
|
|
|
class TransitionRequest(BaseModel):
|
|
target_status: str
|
|
reason: Optional[str] = None
|
|
notify: bool = False
|
|
# Optional dates to set on certain transitions
|
|
draft_out_date: Optional[date] = None
|
|
approved_date: Optional[date] = None
|
|
filed_date: Optional[date] = None
|
|
|
|
|
|
class SimpleWorkflowRequest(BaseModel):
|
|
reason: Optional[str] = None
|
|
notify: bool = False
|
|
effective_date: Optional[date] = None
|
|
|
|
|
|
# Allowed transitions graph
|
|
ALLOWED_TRANSITIONS: Dict[str, set[str]] = {
|
|
"DRAFT": {"APPROVAL_PENDING"},
|
|
"APPROVAL_PENDING": {"APPROVED"},
|
|
"APPROVED": {"FILED"},
|
|
}
|
|
|
|
|
|
def _emit_qdro_notification(db: Session, event_type: str, payload: Dict[str, Any]) -> None:
|
|
try:
|
|
# Enrich with routing from DB (SystemSetup)
|
|
routes = resolve_qdro_routes(
|
|
db,
|
|
file_no=str(payload.get("file_no")) if payload.get("file_no") else None,
|
|
plan_id=str(payload.get("plan_id")) if payload.get("plan_id") else None,
|
|
)
|
|
enriched = dict(payload)
|
|
if routes.get("email_to"):
|
|
enriched["__notify_to"] = routes["email_to"]
|
|
enriched["__notify_override"] = True
|
|
if routes.get("webhook_url"):
|
|
enriched["__webhook_url"] = routes["webhook_url"]
|
|
if routes.get("webhook_secret"):
|
|
enriched["__webhook_secret"] = routes["webhook_secret"]
|
|
enriched["__webhook_override"] = True
|
|
notification_service.emit(event_type, enriched)
|
|
except Exception:
|
|
# Never block on notifications
|
|
pass
|
|
|
|
|
|
def _perform_transition(db: Session, qdro: QDRO, target_status: str, meta: Dict[str, Any]) -> Dict[str, Any]:
|
|
current = (qdro.status or "DRAFT").upper()
|
|
target = (target_status or "").upper()
|
|
if target not in ALLOWED_TRANSITIONS.get(current, set()):
|
|
raise HTTPException(status_code=400, detail=f"Transition not allowed: {current} -> {target}")
|
|
|
|
# Apply status and relevant dates
|
|
qdro.status = target
|
|
qdro.approval_status = target
|
|
|
|
now = date.today()
|
|
changes: Dict[str, Any] = {"from": current, "to": target}
|
|
|
|
if target == "APPROVAL_PENDING":
|
|
set_date = meta.get("draft_out_date") or now
|
|
if not isinstance(set_date, date):
|
|
raise HTTPException(status_code=400, detail="Invalid draft_out_date")
|
|
if qdro.draft_out != set_date:
|
|
qdro.draft_out = set_date
|
|
changes["draft_out"] = set_date.isoformat()
|
|
|
|
elif target == "APPROVED":
|
|
set_date = meta.get("approved_date") or now
|
|
if not isinstance(set_date, date):
|
|
raise HTTPException(status_code=400, detail="Invalid approved_date")
|
|
if qdro.approved_date != set_date:
|
|
qdro.approved_date = set_date
|
|
changes["approved_date"] = set_date.isoformat()
|
|
# Mirror to legacy draft_apr
|
|
if qdro.draft_apr != set_date:
|
|
qdro.draft_apr = set_date
|
|
changes["draft_apr"] = set_date.isoformat()
|
|
|
|
elif target == "FILED":
|
|
set_date = meta.get("filed_date") or now
|
|
if not isinstance(set_date, date):
|
|
raise HTTPException(status_code=400, detail="Invalid filed_date")
|
|
if qdro.filed_date != set_date:
|
|
qdro.filed_date = set_date
|
|
changes["filed_date"] = set_date.isoformat()
|
|
|
|
# Persist
|
|
db.commit()
|
|
db.refresh(qdro)
|
|
return changes
|
|
|
|
|
|
@router.post("/qdros/{qdro_id}/transition", response_model=QDROResponse, summary="Transition QDRO status with validation")
|
|
async def transition_qdro(
|
|
qdro_id: int,
|
|
payload: TransitionRequest,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
qdro = db.query(QDRO).filter(QDRO.id == qdro_id).first()
|
|
if not qdro:
|
|
raise HTTPException(status_code=404, detail="QDRO not found")
|
|
|
|
# Authorization: approving/file transitions require approver or admin
|
|
target_upper = (payload.target_status or "").upper()
|
|
if target_upper in {"APPROVED", "FILED"} and not (getattr(current_user, "is_admin", False) or getattr(current_user, "is_approver", False)):
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not enough permissions")
|
|
|
|
changes = _perform_transition(
|
|
db,
|
|
qdro,
|
|
payload.target_status,
|
|
{
|
|
"draft_out_date": payload.draft_out_date,
|
|
"approved_date": payload.approved_date,
|
|
"filed_date": payload.filed_date,
|
|
},
|
|
)
|
|
|
|
details = {
|
|
**changes,
|
|
"reason": payload.reason,
|
|
}
|
|
try:
|
|
audit_service.log_action(
|
|
db,
|
|
action="STATUS_TRANSITION",
|
|
resource_type="QDRO",
|
|
user=current_user,
|
|
resource_id=qdro.id,
|
|
details=details,
|
|
)
|
|
except Exception:
|
|
pass
|
|
|
|
if payload.notify:
|
|
_emit_qdro_notification(
|
|
db,
|
|
"QDRO_STATUS_CHANGED",
|
|
{
|
|
"qdro_id": qdro.id,
|
|
"file_no": qdro.file_no,
|
|
"plan_id": qdro.plan_id,
|
|
**details,
|
|
},
|
|
)
|
|
|
|
return qdro
|
|
|
|
|
|
@router.post("/qdros/{qdro_id}/submit-for-approval", response_model=QDROResponse, summary="Move QDRO to APPROVAL_PENDING")
|
|
async def submit_for_approval(
|
|
qdro_id: int,
|
|
payload: SimpleWorkflowRequest,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
qdro = db.query(QDRO).filter(QDRO.id == qdro_id).first()
|
|
if not qdro:
|
|
raise HTTPException(status_code=404, detail="QDRO not found")
|
|
changes = _perform_transition(db, qdro, "APPROVAL_PENDING", {"draft_out_date": payload.effective_date})
|
|
details = {**changes, "reason": payload.reason}
|
|
try:
|
|
audit_service.log_action(db, action="STATUS_TRANSITION", resource_type="QDRO", user=current_user, resource_id=qdro.id, details=details)
|
|
except Exception:
|
|
pass
|
|
if payload.notify:
|
|
_emit_qdro_notification(db, "QDRO_STATUS_CHANGED", {"qdro_id": qdro.id, "file_no": qdro.file_no, "plan_id": qdro.plan_id, **details})
|
|
return qdro
|
|
|
|
|
|
@router.post("/qdros/{qdro_id}/approve", response_model=QDROResponse, summary="Approve QDRO")
|
|
async def approve_qdro(
|
|
qdro_id: int,
|
|
payload: SimpleWorkflowRequest,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
qdro = db.query(QDRO).filter(QDRO.id == qdro_id).first()
|
|
if not qdro:
|
|
raise HTTPException(status_code=404, detail="QDRO not found")
|
|
# Authorization: approver or admin
|
|
if not (getattr(current_user, "is_admin", False) or getattr(current_user, "is_approver", False)):
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not enough permissions")
|
|
changes = _perform_transition(db, qdro, "APPROVED", {"approved_date": payload.effective_date})
|
|
details = {**changes, "reason": payload.reason}
|
|
try:
|
|
audit_service.log_action(db, action="STATUS_TRANSITION", resource_type="QDRO", user=current_user, resource_id=qdro.id, details=details)
|
|
except Exception:
|
|
pass
|
|
if payload.notify:
|
|
_emit_qdro_notification(db, "QDRO_STATUS_CHANGED", {"qdro_id": qdro.id, "file_no": qdro.file_no, "plan_id": qdro.plan_id, **details})
|
|
return qdro
|
|
|
|
|
|
@router.post("/qdros/{qdro_id}/file", response_model=QDROResponse, summary="Mark QDRO as filed")
|
|
async def file_qdro(
|
|
qdro_id: int,
|
|
payload: SimpleWorkflowRequest,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
qdro = db.query(QDRO).filter(QDRO.id == qdro_id).first()
|
|
if not qdro:
|
|
raise HTTPException(status_code=404, detail="QDRO not found")
|
|
# Authorization: approver or admin
|
|
if not (getattr(current_user, "is_admin", False) or getattr(current_user, "is_approver", False)):
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not enough permissions")
|
|
changes = _perform_transition(db, qdro, "FILED", {"filed_date": payload.effective_date})
|
|
details = {**changes, "reason": payload.reason}
|
|
try:
|
|
audit_service.log_action(db, action="STATUS_TRANSITION", resource_type="QDRO", user=current_user, resource_id=qdro.id, details=details)
|
|
except Exception:
|
|
pass
|
|
if payload.notify:
|
|
_emit_qdro_notification(db, "QDRO_STATUS_CHANGED", {"qdro_id": qdro.id, "file_no": qdro.file_no, "plan_id": qdro.plan_id, **details})
|
|
return qdro
|
|
|
|
|
|
@router.delete("/qdros/{qdro_id}", summary="Delete a QDRO")
|
|
async def delete_qdro(
|
|
qdro_id: int,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
qdro = db.query(QDRO).filter(QDRO.id == qdro_id).first()
|
|
if not qdro:
|
|
raise HTTPException(status_code=404, detail="QDRO not found")
|
|
db.delete(qdro)
|
|
db.commit()
|
|
try:
|
|
audit_service.log_action(db, action="DELETE", resource_type="QDRO", user=current_user, resource_id=qdro_id, details={"file_no": qdro.file_no})
|
|
except Exception:
|
|
pass
|
|
return {"message": "QDRO deleted"}
|
|
|
|
|
|
class VersionCreate(BaseModel):
|
|
version_label: str = Field(default="01", max_length=20)
|
|
status: Optional[str] = Field(default="DRAFT", max_length=45)
|
|
|
|
|
|
class VersionResponse(BaseModel):
|
|
id: int
|
|
qdro_id: int
|
|
version_label: str
|
|
status: Optional[str] = None
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
|
|
@router.post("/qdros/{qdro_id}/versions", response_model=VersionResponse, summary="Create a new version snapshot of a QDRO")
|
|
async def create_qdro_version(
|
|
qdro_id: int,
|
|
payload: VersionCreate,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
qdro = db.query(QDRO).filter(QDRO.id == qdro_id).first()
|
|
if not qdro:
|
|
raise HTTPException(status_code=404, detail="QDRO not found")
|
|
ver = QDROVersion(qdro_id=qdro.id, version_label=payload.version_label, status=payload.status, content=qdro.content)
|
|
db.add(ver)
|
|
db.commit()
|
|
db.refresh(ver)
|
|
try:
|
|
audit_service.log_action(db, action="VERSION_CREATE", resource_type="QDRO", user=current_user, resource_id=qdro_id, details={"version": payload.version_label})
|
|
except Exception:
|
|
pass
|
|
return ver
|
|
|
|
|
|
@router.get("/qdros/{qdro_id}/versions", response_model=List[VersionResponse], summary="List versions for a QDRO")
|
|
async def list_qdro_versions(
|
|
qdro_id: int,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
qdro = db.query(QDRO).filter(QDRO.id == qdro_id).first()
|
|
if not qdro:
|
|
raise HTTPException(status_code=404, detail="QDRO not found")
|
|
versions = db.query(QDROVersion).filter(QDROVersion.qdro_id == qdro_id).order_by(QDROVersion.created_at.desc()).all()
|
|
return versions
|
|
|
|
|
|
class DivisionCalcRequest(BaseModel):
|
|
account_balance: float
|
|
percent: Optional[float] = Field(default=None, ge=0.0, le=100.0)
|
|
amount: Optional[float] = Field(default=None, ge=0.0)
|
|
save_percent_string: bool = False
|
|
|
|
|
|
class DivisionCalcResponse(BaseModel):
|
|
percent: float
|
|
amount: float
|
|
|
|
|
|
@router.post("/qdros/{qdro_id}/calculate-division", response_model=DivisionCalcResponse, summary="Calculate division by percent or amount")
|
|
async def calculate_division(
|
|
qdro_id: int,
|
|
payload: DivisionCalcRequest,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
qdro = db.query(QDRO).filter(QDRO.id == qdro_id).first()
|
|
if not qdro:
|
|
raise HTTPException(status_code=404, detail="QDRO not found")
|
|
if (payload.percent is None) == (payload.amount is None):
|
|
raise HTTPException(status_code=400, detail="Provide exactly one of percent or amount")
|
|
if payload.percent is not None:
|
|
amount = round((payload.percent / 100.0) * payload.account_balance, 2)
|
|
percent = float(payload.percent)
|
|
else:
|
|
amount = float(payload.amount or 0.0)
|
|
if payload.account_balance <= 0:
|
|
raise HTTPException(status_code=400, detail="account_balance must be > 0 when amount provided")
|
|
percent = round((amount / payload.account_balance) * 100.0, 4)
|
|
if payload.save_percent_string:
|
|
try:
|
|
qdro.percent_awarded = f"{percent:.4g}%"
|
|
db.commit()
|
|
except Exception:
|
|
db.rollback()
|
|
try:
|
|
audit_service.log_action(db, action="CALCULATE", resource_type="QDRO", user=current_user, resource_id=qdro_id, details={"percent": percent, "amount": amount})
|
|
except Exception:
|
|
pass
|
|
return DivisionCalcResponse(percent=percent, amount=amount)
|
|
|
|
|
|
class GenerateRequest(BaseModel):
|
|
template_id: int
|
|
version_id: Optional[int] = None
|
|
context: Dict[str, Any] = Field(default_factory=dict)
|
|
|
|
|
|
class GenerateResponse(BaseModel):
|
|
resolved: Dict[str, Any]
|
|
unresolved: List[str]
|
|
output_mime_type: str
|
|
output_size: int
|
|
|
|
|
|
@router.post("/qdros/{qdro_id}/generate-document", response_model=GenerateResponse, summary="Generate a QDRO document using the template system")
|
|
async def generate_qdro_document(
|
|
qdro_id: int,
|
|
payload: GenerateRequest,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
qdro = db.query(QDRO).filter(QDRO.id == qdro_id).first()
|
|
if not qdro:
|
|
raise HTTPException(status_code=404, detail="QDRO not found")
|
|
# Locate template and version
|
|
tpl = db.query(DocumentTemplate).filter(DocumentTemplate.id == payload.template_id).first()
|
|
if not tpl:
|
|
raise HTTPException(status_code=404, detail="Template not found")
|
|
version_id = payload.version_id or tpl.current_version_id
|
|
if not version_id:
|
|
raise HTTPException(status_code=400, detail="Template has no versions")
|
|
ver = db.query(DocumentTemplateVersion).filter(DocumentTemplateVersion.id == version_id).first()
|
|
if not ver:
|
|
raise HTTPException(status_code=404, detail="Version not found")
|
|
|
|
storage = get_default_storage()
|
|
content = storage.open_bytes(ver.storage_path)
|
|
tokens = extract_tokens_from_bytes(content)
|
|
|
|
# Build a rich context with file and qdro details
|
|
file_obj = db.query(File).filter(File.file_no == qdro.file_no).first()
|
|
base_ctx: Dict[str, Any] = {
|
|
"FILE_NO": qdro.file_no,
|
|
"QDRO_VERSION": qdro.version,
|
|
"QDRO_STATUS": qdro.status,
|
|
"CASE_NUMBER": qdro.case_number,
|
|
"PETITIONER": qdro.pet,
|
|
"RESPONDENT": qdro.res,
|
|
"PERCENT_AWARDED": qdro.percent_awarded,
|
|
}
|
|
if file_obj and file_obj.owner:
|
|
base_ctx.update({
|
|
"CLIENT_FIRST": getattr(file_obj.owner, "first", ""),
|
|
"CLIENT_LAST": getattr(file_obj.owner, "last", ""),
|
|
"CLIENT_FULL": f"{getattr(file_obj.owner, 'first', '') or ''} {getattr(file_obj.owner, 'last', '')}".strip(),
|
|
"MATTER": file_obj.regarding,
|
|
})
|
|
# Merge with provided context
|
|
context = build_context({**base_ctx, **(payload.context or {})})
|
|
resolved, unresolved = resolve_tokens(db, tokens, context)
|
|
|
|
output_bytes = content
|
|
output_mime = ver.mime_type
|
|
if ver.mime_type == "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
|
|
output_bytes = render_docx(content, resolved)
|
|
output_mime = ver.mime_type
|
|
try:
|
|
audit_service.log_action(db, action="GENERATE", resource_type="QDRO", user=current_user, resource_id=qdro_id, details={"template_id": payload.template_id, "version_id": version_id, "unresolved": unresolved})
|
|
except Exception:
|
|
pass
|
|
return GenerateResponse(resolved=resolved, unresolved=unresolved, output_mime_type=output_mime, output_size=len(output_bytes))
|
|
|
|
|
|
class PlanInfoCreate(BaseModel):
|
|
plan_id: str
|
|
plan_name: str
|
|
plan_type: Optional[str] = None
|
|
sponsor: Optional[str] = None
|
|
administrator: Optional[str] = None
|
|
address1: Optional[str] = None
|
|
address2: Optional[str] = None
|
|
city: Optional[str] = None
|
|
state: Optional[str] = None
|
|
zip_code: Optional[str] = None
|
|
phone: Optional[str] = None
|
|
notes: Optional[str] = None
|
|
|
|
|
|
class PlanInfoResponse(PlanInfoCreate):
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
|
|
@router.post("/plan-info", response_model=PlanInfoResponse, summary="Create plan information")
|
|
async def create_plan_info(
|
|
payload: PlanInfoCreate,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
exists = db.query(PlanInfo).filter(PlanInfo.plan_id == payload.plan_id).first()
|
|
if exists:
|
|
raise HTTPException(status_code=400, detail="Plan already exists")
|
|
plan = PlanInfo(**payload.model_dump())
|
|
db.add(plan)
|
|
db.commit()
|
|
db.refresh(plan)
|
|
try:
|
|
audit_service.log_action(db, action="CREATE", resource_type="PLAN_INFO", user=current_user, resource_id=plan.plan_id)
|
|
except Exception:
|
|
pass
|
|
return plan
|
|
|
|
|
|
@router.get("/plan-info", response_model=List[PlanInfoResponse], summary="List plan information")
|
|
async def list_plan_info(
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
rows = db.query(PlanInfo).order_by(PlanInfo.plan_name.asc()).all()
|
|
return rows
|
|
|
|
|
|
class CommunicationCreate(BaseModel):
|
|
channel: Optional[str] = Field(default=None, description="email|phone|letter|fax|portal")
|
|
subject: Optional[str] = None
|
|
message: Optional[str] = None
|
|
contact_name: Optional[str] = None
|
|
contact_email: Optional[str] = None
|
|
contact_phone: Optional[str] = None
|
|
status: Optional[str] = None
|
|
|
|
|
|
class CommunicationResponse(CommunicationCreate):
|
|
id: int
|
|
qdro_id: int
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
|
|
@router.get("/qdros/{qdro_id}/communications", response_model=List[CommunicationResponse], summary="List QDRO communications")
|
|
async def list_communications(
|
|
qdro_id: int,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
qdro = db.query(QDRO).filter(QDRO.id == qdro_id).first()
|
|
if not qdro:
|
|
raise HTTPException(status_code=404, detail="QDRO not found")
|
|
rows = db.query(QDROCommunication).filter(QDROCommunication.qdro_id == qdro_id).order_by(QDROCommunication.created_at.desc()).all()
|
|
return rows
|
|
|
|
|
|
@router.post("/qdros/{qdro_id}/communications", response_model=CommunicationResponse, summary="Create QDRO communication entry")
|
|
async def create_communication(
|
|
qdro_id: int,
|
|
payload: CommunicationCreate,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
qdro = db.query(QDRO).filter(QDRO.id == qdro_id).first()
|
|
if not qdro:
|
|
raise HTTPException(status_code=404, detail="QDRO not found")
|
|
comm = QDROCommunication(qdro_id=qdro_id, **payload.model_dump(exclude_unset=True))
|
|
db.add(comm)
|
|
db.commit()
|
|
db.refresh(comm)
|
|
try:
|
|
audit_service.log_action(db, action="COMM_CREATE", resource_type="QDRO", user=current_user, resource_id=qdro_id, details={"comm_id": comm.id, "channel": comm.channel})
|
|
except Exception:
|
|
pass
|
|
return comm
|
|
|
|
|