finishing QDRO section

This commit is contained in:
HotSwapp
2025-08-15 17:19:51 -05:00
parent 006ef3d7b1
commit abc7f289d1
22 changed files with 2753 additions and 46 deletions

View File

@@ -73,6 +73,7 @@ class UserCreate(BaseModel):
last_name: Optional[str] = None
is_admin: bool = False
is_active: bool = True
is_approver: bool = False
class UserUpdate(BaseModel):
"""Update user information"""
@@ -82,6 +83,7 @@ class UserUpdate(BaseModel):
last_name: Optional[str] = None
is_admin: Optional[bool] = None
is_active: Optional[bool] = None
is_approver: Optional[bool] = None
class UserResponse(BaseModel):
"""User response model"""
@@ -92,6 +94,7 @@ class UserResponse(BaseModel):
last_name: Optional[str]
is_admin: bool
is_active: bool
is_approver: bool
last_login: Optional[datetime]
created_at: Optional[datetime]
updated_at: Optional[datetime]
@@ -103,6 +106,44 @@ class PasswordReset(BaseModel):
new_password: str = Field(..., min_length=6)
confirm_password: str = Field(..., min_length=6)
# Approver management
class ApproverToggle(BaseModel):
is_approver: bool
@router.post("/users/{user_id}/approver", response_model=UserResponse)
async def set_user_approver(
user_id: int,
payload: ApproverToggle,
request: Request,
db: Session = Depends(get_db),
current_user: User = Depends(get_admin_user),
):
"""Admin-only toggle for user approver role with audit logging."""
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
previous = bool(getattr(user, "is_approver", False))
user.is_approver = bool(payload.is_approver)
user.updated_at = datetime.now(timezone.utc)
db.commit()
db.refresh(user)
if previous != user.is_approver:
try:
audit_service.log_user_action(
db=db,
action="UPDATE",
target_user=user,
acting_user=current_user,
changes={"is_approver": {"from": previous, "to": user.is_approver}},
request=request,
)
except Exception:
pass
return user
class SystemSetting(BaseModel):
"""System setting model"""
setting_key: str
@@ -115,6 +156,56 @@ class SettingUpdate(BaseModel):
setting_value: str
description: Optional[str] = None
# ------------------------------
# QDRO Notification Route Models
# ------------------------------
class NotificationRoute(BaseModel):
scope: str = Field(description="file or plan")
identifier: str = Field(description="file_no when scope=file, plan_id when scope=plan")
email_to: Optional[str] = None
webhook_url: Optional[str] = None
webhook_secret: Optional[str] = None
def _route_keys(scope: str, identifier: str) -> dict[str, str]:
if scope not in {"file", "plan"}:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid scope; expected 'file' or 'plan'")
return {
"email": f"notifications.qdro.email.to.{scope}.{identifier}",
"webhook_url": f"notifications.qdro.webhook.url.{scope}.{identifier}",
"webhook_secret": f"notifications.qdro.webhook.secret.{scope}.{identifier}",
}
def _get_setting(db: Session, key: str) -> Optional[str]:
row = db.query(SystemSetup).filter(SystemSetup.setting_key == key).first()
return row.setting_value if row else None
def _upsert_setting(db: Session, key: str, value: Optional[str]) -> None:
row = db.query(SystemSetup).filter(SystemSetup.setting_key == key).first()
if value is None or value == "":
if row:
db.delete(row)
db.commit()
return
if row:
row.setting_value = value
db.commit()
return
row = SystemSetup(setting_key=key, setting_value=value, description=f"Auto: {key}")
db.add(row)
db.commit()
def _delete_setting(db: Session, key: str) -> None:
row = db.query(SystemSetup).filter(SystemSetup.setting_key == key).first()
if row:
db.delete(row)
db.commit()
class AuditLogEntry(BaseModel):
"""Audit log entry"""
id: int
@@ -726,6 +817,7 @@ async def create_user(
hashed_password=hashed_password,
is_admin=user_data.is_admin,
is_active=user_data.is_active,
is_approver=user_data.is_approver,
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc)
)
@@ -807,7 +899,8 @@ async def update_user(
"first_name": user.first_name,
"last_name": user.last_name,
"is_admin": user.is_admin,
"is_active": user.is_active
"is_active": user.is_active,
"is_approver": user.is_approver,
}
# Update user fields
@@ -1063,6 +1156,85 @@ async def delete_setting(
return {"message": "Setting deleted successfully"}
# ------------------------------
# QDRO Notification Routing CRUD
# ------------------------------
@router.get("/qdro/notification-routes")
async def list_qdro_notification_routes(
scope: Optional[str] = Query(None, description="Optional filter: file or plan"),
db: Session = Depends(get_db),
current_user: User = Depends(get_admin_user),
):
q = db.query(SystemSetup).filter(SystemSetup.setting_key.like("notifications.qdro.%"))
rows = q.all()
# Build map of identifier -> route
route_map: dict[tuple[str, str], dict[str, Optional[str]]] = {}
for r in rows:
key = r.setting_key
parts = key.split(".")
# notifications qdro <type> <to|url|secret> <scope> <identifier>
if len(parts) < 7:
# Example: notifications.qdro.email.to.file.{id}
# parts: [notifications, qdro, email, to, file, {id}]
pass
if len(parts) >= 6 and parts[0] == "notifications" and parts[1] == "qdro":
typ = parts[2]
field = parts[3]
sc = parts[4]
ident = ".".join(parts[5:]) # support dots in identifiers just in case
if scope and sc != scope:
continue
route = route_map.setdefault((sc, ident), {"scope": sc, "identifier": ident, "email_to": None, "webhook_url": None, "webhook_secret": None})
if typ == "email" and field == "to":
route["email_to"] = r.setting_value
elif typ == "webhook" and field == "url":
route["webhook_url"] = r.setting_value
elif typ == "webhook" and field == "secret":
route["webhook_secret"] = r.setting_value
# Format list
out = [
{
"scope": sc,
"identifier": ident,
"email_to": data.get("email_to"),
"webhook_url": data.get("webhook_url"),
"webhook_secret": data.get("webhook_secret"),
}
for (sc, ident), data in route_map.items()
]
return {"items": out, "total": len(out)}
@router.post("/qdro/notification-routes")
async def upsert_qdro_notification_route(
payload: NotificationRoute,
db: Session = Depends(get_db),
current_user: User = Depends(get_admin_user),
):
keys = _route_keys(payload.scope, payload.identifier)
_upsert_setting(db, keys["email"], payload.email_to)
_upsert_setting(db, keys["webhook_url"], payload.webhook_url)
# Preserve existing secret unless a new value is provided
if payload.webhook_secret is not None and payload.webhook_secret != "":
_upsert_setting(db, keys["webhook_secret"], payload.webhook_secret)
return {"message": "Route saved"}
@router.delete("/qdro/notification-routes/{scope}/{identifier}")
async def delete_qdro_notification_route(
scope: str,
identifier: str,
db: Session = Depends(get_db),
current_user: User = Depends(get_admin_user),
):
keys = _route_keys(scope, identifier)
_delete_setting(db, keys["email"])
_delete_setting(db, keys["webhook_url"])
_delete_setting(db, keys["webhook_secret"])
return {"message": "Route deleted"}
# Database Maintenance and Lookup Management
@router.get("/lookups/tables")