Add HR agent onboarding model and actor enforcement
This commit is contained in:
@@ -3,10 +3,10 @@ from __future__ import annotations
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from app.api.utils import log_activity
|
||||
from app.api.utils import log_activity, get_actor_employee_id
|
||||
from app.db.session import get_session
|
||||
from app.models.hr import EmploymentAction, HeadcountRequest
|
||||
from app.schemas.hr import EmploymentActionCreate, HeadcountRequestCreate, HeadcountRequestUpdate
|
||||
from app.models.hr import EmploymentAction, HeadcountRequest, AgentOnboarding
|
||||
from app.schemas.hr import EmploymentActionCreate, HeadcountRequestCreate, HeadcountRequestUpdate, AgentOnboardingCreate, AgentOnboardingUpdate
|
||||
|
||||
router = APIRouter(prefix="/hr", tags=["hr"])
|
||||
|
||||
@@ -17,18 +17,18 @@ def list_headcount_requests(session: Session = Depends(get_session)):
|
||||
|
||||
|
||||
@router.post("/headcount", response_model=HeadcountRequest)
|
||||
def create_headcount_request(payload: HeadcountRequestCreate, session: Session = Depends(get_session)):
|
||||
def create_headcount_request(payload: HeadcountRequestCreate, session: Session = Depends(get_session), actor_employee_id: int = Depends(get_actor_employee_id)):
|
||||
req = HeadcountRequest(**payload.model_dump())
|
||||
session.add(req)
|
||||
session.commit()
|
||||
session.refresh(req)
|
||||
log_activity(session, actor_employee_id=req.requested_by_manager_id, entity_type="headcount_request", entity_id=req.id, verb="submitted")
|
||||
log_activity(session, actor_employee_id=actor_employee_id, entity_type="headcount_request", entity_id=req.id, verb="submitted")
|
||||
session.commit()
|
||||
return req
|
||||
|
||||
|
||||
@router.patch("/headcount/{request_id}", response_model=HeadcountRequest)
|
||||
def update_headcount_request(request_id: int, payload: HeadcountRequestUpdate, session: Session = Depends(get_session)):
|
||||
def update_headcount_request(request_id: int, payload: HeadcountRequestUpdate, session: Session = Depends(get_session), actor_employee_id: int = Depends(get_actor_employee_id)):
|
||||
req = session.get(HeadcountRequest, request_id)
|
||||
if not req:
|
||||
raise HTTPException(status_code=404, detail="Request not found")
|
||||
@@ -40,7 +40,7 @@ def update_headcount_request(request_id: int, payload: HeadcountRequestUpdate, s
|
||||
session.add(req)
|
||||
session.commit()
|
||||
session.refresh(req)
|
||||
log_activity(session, actor_employee_id=req.requested_by_manager_id, entity_type="headcount_request", entity_id=req.id, verb="updated", payload=data)
|
||||
log_activity(session, actor_employee_id=actor_employee_id, entity_type="headcount_request", entity_id=req.id, verb="updated", payload=data)
|
||||
session.commit()
|
||||
return req
|
||||
|
||||
@@ -51,11 +51,47 @@ def list_employment_actions(session: Session = Depends(get_session)):
|
||||
|
||||
|
||||
@router.post("/actions", response_model=EmploymentAction)
|
||||
def create_employment_action(payload: EmploymentActionCreate, session: Session = Depends(get_session)):
|
||||
def create_employment_action(payload: EmploymentActionCreate, session: Session = Depends(get_session), actor_employee_id: int = Depends(get_actor_employee_id)):
|
||||
action = EmploymentAction(**payload.model_dump())
|
||||
session.add(action)
|
||||
session.commit()
|
||||
session.refresh(action)
|
||||
log_activity(session, actor_employee_id=action.issued_by_employee_id, entity_type="employment_action", entity_id=action.id, verb=action.action_type, payload={"employee_id": action.employee_id})
|
||||
log_activity(session, actor_employee_id=actor_employee_id, entity_type="employment_action", entity_id=action.id, verb=action.action_type, payload={"employee_id": action.employee_id})
|
||||
session.commit()
|
||||
return action
|
||||
|
||||
@router.get("/onboarding", response_model=list[AgentOnboarding])
|
||||
def list_agent_onboarding(session: Session = Depends(get_session)):
|
||||
return session.exec(select(AgentOnboarding).order_by(AgentOnboarding.id.desc())).all()
|
||||
|
||||
|
||||
@router.post("/onboarding", response_model=AgentOnboarding)
|
||||
def create_agent_onboarding(payload: AgentOnboardingCreate, session: Session = Depends(get_session), actor_employee_id: int = Depends(get_actor_employee_id)):
|
||||
item = AgentOnboarding(**payload.model_dump())
|
||||
session.add(item)
|
||||
session.commit()
|
||||
session.refresh(item)
|
||||
log_activity(session, actor_employee_id=actor_employee_id, entity_type="agent_onboarding", entity_id=item.id, verb="created", payload={"agent_name": item.agent_name, "status": item.status})
|
||||
session.commit()
|
||||
return item
|
||||
|
||||
|
||||
@router.patch("/onboarding/{onboarding_id}", response_model=AgentOnboarding)
|
||||
def update_agent_onboarding(onboarding_id: int, payload: AgentOnboardingUpdate, session: Session = Depends(get_session), actor_employee_id: int = Depends(get_actor_employee_id)):
|
||||
item = session.get(AgentOnboarding, onboarding_id)
|
||||
if not item:
|
||||
raise HTTPException(status_code=404, detail="Onboarding record not found")
|
||||
|
||||
data = payload.model_dump(exclude_unset=True)
|
||||
for k, v in data.items():
|
||||
setattr(item, k, v)
|
||||
from datetime import datetime
|
||||
item.updated_at = datetime.utcnow()
|
||||
|
||||
session.add(item)
|
||||
session.commit()
|
||||
session.refresh(item)
|
||||
log_activity(session, actor_employee_id=actor_employee_id, entity_type="agent_onboarding", entity_id=item.id, verb="updated", payload=data)
|
||||
session.commit()
|
||||
return item
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from app.api.utils import log_activity
|
||||
from app.api.utils import log_activity, get_actor_employee_id
|
||||
from app.db.session import get_session
|
||||
from app.models.org import Department, Employee
|
||||
from app.schemas.org import DepartmentCreate, DepartmentUpdate, EmployeeCreate, EmployeeUpdate
|
||||
@@ -17,18 +17,18 @@ def list_departments(session: Session = Depends(get_session)):
|
||||
|
||||
|
||||
@router.post("/departments", response_model=Department)
|
||||
def create_department(payload: DepartmentCreate, session: Session = Depends(get_session)):
|
||||
def create_department(payload: DepartmentCreate, session: Session = Depends(get_session), actor_employee_id: int = Depends(get_actor_employee_id)):
|
||||
dept = Department(name=payload.name, head_employee_id=payload.head_employee_id)
|
||||
session.add(dept)
|
||||
session.commit()
|
||||
session.refresh(dept)
|
||||
log_activity(session, actor_employee_id=None, entity_type="department", entity_id=dept.id, verb="created", payload={"name": dept.name})
|
||||
log_activity(session, actor_employee_id=actor_employee_id, entity_type="department", entity_id=dept.id, verb="created", payload={"name": dept.name})
|
||||
session.commit()
|
||||
return dept
|
||||
|
||||
|
||||
@router.patch("/departments/{department_id}", response_model=Department)
|
||||
def update_department(department_id: int, payload: DepartmentUpdate, session: Session = Depends(get_session)):
|
||||
def update_department(department_id: int, payload: DepartmentUpdate, session: Session = Depends(get_session), actor_employee_id: int = Depends(get_actor_employee_id)):
|
||||
dept = session.get(Department, department_id)
|
||||
if not dept:
|
||||
raise HTTPException(status_code=404, detail="Department not found")
|
||||
@@ -40,7 +40,7 @@ def update_department(department_id: int, payload: DepartmentUpdate, session: Se
|
||||
session.add(dept)
|
||||
session.commit()
|
||||
session.refresh(dept)
|
||||
log_activity(session, actor_employee_id=None, entity_type="department", entity_id=dept.id, verb="updated", payload=data)
|
||||
log_activity(session, actor_employee_id=actor_employee_id, entity_type="department", entity_id=dept.id, verb="updated", payload=data)
|
||||
session.commit()
|
||||
return dept
|
||||
|
||||
@@ -51,18 +51,18 @@ def list_employees(session: Session = Depends(get_session)):
|
||||
|
||||
|
||||
@router.post("/employees", response_model=Employee)
|
||||
def create_employee(payload: EmployeeCreate, session: Session = Depends(get_session)):
|
||||
def create_employee(payload: EmployeeCreate, session: Session = Depends(get_session), actor_employee_id: int = Depends(get_actor_employee_id)):
|
||||
emp = Employee(**payload.model_dump())
|
||||
session.add(emp)
|
||||
session.commit()
|
||||
session.refresh(emp)
|
||||
log_activity(session, actor_employee_id=None, entity_type="employee", entity_id=emp.id, verb="created", payload={"name": emp.name, "type": emp.employee_type})
|
||||
log_activity(session, actor_employee_id=actor_employee_id, entity_type="employee", entity_id=emp.id, verb="created", payload={"name": emp.name, "type": emp.employee_type})
|
||||
session.commit()
|
||||
return emp
|
||||
|
||||
|
||||
@router.patch("/employees/{employee_id}", response_model=Employee)
|
||||
def update_employee(employee_id: int, payload: EmployeeUpdate, session: Session = Depends(get_session)):
|
||||
def update_employee(employee_id: int, payload: EmployeeUpdate, session: Session = Depends(get_session), actor_employee_id: int = Depends(get_actor_employee_id)):
|
||||
emp = session.get(Employee, employee_id)
|
||||
if not emp:
|
||||
raise HTTPException(status_code=404, detail="Employee not found")
|
||||
@@ -74,6 +74,6 @@ def update_employee(employee_id: int, payload: EmployeeUpdate, session: Session
|
||||
session.add(emp)
|
||||
session.commit()
|
||||
session.refresh(emp)
|
||||
log_activity(session, actor_employee_id=None, entity_type="employee", entity_id=emp.id, verb="updated", payload=data)
|
||||
log_activity(session, actor_employee_id=actor_employee_id, entity_type="employee", entity_id=emp.id, verb="updated", payload=data)
|
||||
session.commit()
|
||||
return emp
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from app.api.utils import log_activity
|
||||
from app.api.utils import log_activity, get_actor_employee_id
|
||||
from app.db.session import get_session
|
||||
from app.models.projects import Project, ProjectMember
|
||||
from app.schemas.projects import ProjectCreate, ProjectUpdate
|
||||
@@ -17,18 +17,18 @@ def list_projects(session: Session = Depends(get_session)):
|
||||
|
||||
|
||||
@router.post("", response_model=Project)
|
||||
def create_project(payload: ProjectCreate, session: Session = Depends(get_session)):
|
||||
def create_project(payload: ProjectCreate, session: Session = Depends(get_session), actor_employee_id: int = Depends(get_actor_employee_id)):
|
||||
proj = Project(**payload.model_dump())
|
||||
session.add(proj)
|
||||
session.commit()
|
||||
session.refresh(proj)
|
||||
log_activity(session, actor_employee_id=None, entity_type="project", entity_id=proj.id, verb="created", payload={"name": proj.name})
|
||||
log_activity(session, actor_employee_id=actor_employee_id, entity_type="project", entity_id=proj.id, verb="created", payload={"name": proj.name})
|
||||
session.commit()
|
||||
return proj
|
||||
|
||||
|
||||
@router.patch("/{project_id}", response_model=Project)
|
||||
def update_project(project_id: int, payload: ProjectUpdate, session: Session = Depends(get_session)):
|
||||
def update_project(project_id: int, payload: ProjectUpdate, session: Session = Depends(get_session), actor_employee_id: int = Depends(get_actor_employee_id)):
|
||||
proj = session.get(Project, project_id)
|
||||
if not proj:
|
||||
raise HTTPException(status_code=404, detail="Project not found")
|
||||
@@ -40,7 +40,7 @@ def update_project(project_id: int, payload: ProjectUpdate, session: Session = D
|
||||
session.add(proj)
|
||||
session.commit()
|
||||
session.refresh(proj)
|
||||
log_activity(session, actor_employee_id=None, entity_type="project", entity_id=proj.id, verb="updated", payload=data)
|
||||
log_activity(session, actor_employee_id=actor_employee_id, entity_type="project", entity_id=proj.id, verb="updated", payload=data)
|
||||
session.commit()
|
||||
return proj
|
||||
|
||||
@@ -53,7 +53,7 @@ def list_project_members(project_id: int, session: Session = Depends(get_session
|
||||
|
||||
|
||||
@router.post("/{project_id}/members", response_model=ProjectMember)
|
||||
def add_project_member(project_id: int, payload: ProjectMember, session: Session = Depends(get_session)):
|
||||
def add_project_member(project_id: int, payload: ProjectMember, session: Session = Depends(get_session), actor_employee_id: int = Depends(get_actor_employee_id)):
|
||||
existing = session.exec(select(ProjectMember).where(ProjectMember.project_id == project_id, ProjectMember.employee_id == payload.employee_id)).first()
|
||||
if existing:
|
||||
raise HTTPException(status_code=409, detail="Member already added")
|
||||
@@ -63,7 +63,7 @@ def add_project_member(project_id: int, payload: ProjectMember, session: Session
|
||||
session.refresh(member)
|
||||
log_activity(
|
||||
session,
|
||||
actor_employee_id=None,
|
||||
actor_employee_id=actor_employee_id,
|
||||
entity_type="project_member",
|
||||
entity_id=member.id,
|
||||
verb="added",
|
||||
@@ -74,7 +74,7 @@ def add_project_member(project_id: int, payload: ProjectMember, session: Session
|
||||
|
||||
|
||||
@router.delete("/{project_id}/members/{member_id}")
|
||||
def remove_project_member(project_id: int, member_id: int, session: Session = Depends(get_session)):
|
||||
def remove_project_member(project_id: int, member_id: int, session: Session = Depends(get_session), actor_employee_id: int = Depends(get_actor_employee_id)):
|
||||
member = session.get(ProjectMember, member_id)
|
||||
if not member or member.project_id != project_id:
|
||||
raise HTTPException(status_code=404, detail="Project member not found")
|
||||
@@ -82,7 +82,7 @@ def remove_project_member(project_id: int, member_id: int, session: Session = De
|
||||
session.commit()
|
||||
log_activity(
|
||||
session,
|
||||
actor_employee_id=None,
|
||||
actor_employee_id=actor_employee_id,
|
||||
entity_type="project_member",
|
||||
entity_id=member_id,
|
||||
verb="removed",
|
||||
@@ -93,7 +93,7 @@ def remove_project_member(project_id: int, member_id: int, session: Session = De
|
||||
|
||||
|
||||
@router.patch("/{project_id}/members/{member_id}", response_model=ProjectMember)
|
||||
def update_project_member(project_id: int, member_id: int, payload: ProjectMember, session: Session = Depends(get_session)):
|
||||
def update_project_member(project_id: int, member_id: int, payload: ProjectMember, session: Session = Depends(get_session), actor_employee_id: int = Depends(get_actor_employee_id)):
|
||||
member = session.get(ProjectMember, member_id)
|
||||
if not member or member.project_id != project_id:
|
||||
raise HTTPException(status_code=404, detail="Project member not found")
|
||||
@@ -106,7 +106,7 @@ def update_project_member(project_id: int, member_id: int, payload: ProjectMembe
|
||||
session.refresh(member)
|
||||
log_activity(
|
||||
session,
|
||||
actor_employee_id=None,
|
||||
actor_employee_id=actor_employee_id,
|
||||
entity_type="project_member",
|
||||
entity_id=member.id,
|
||||
verb="updated",
|
||||
|
||||
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from fastapi import Header, HTTPException
|
||||
from sqlmodel import Session
|
||||
|
||||
from app.models.activity import Activity
|
||||
@@ -26,3 +27,11 @@ def log_activity(
|
||||
payload_json=json.dumps(payload) if payload is not None else None,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def get_actor_employee_id(
|
||||
x_actor_employee_id: int | None = Header(default=None, alias="X-Actor-Employee-Id"),
|
||||
) -> int:
|
||||
if x_actor_employee_id is None:
|
||||
raise HTTPException(status_code=400, detail="X-Actor-Employee-Id required")
|
||||
return x_actor_employee_id
|
||||
|
||||
@@ -5,7 +5,7 @@ from datetime import datetime
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from app.api.utils import log_activity
|
||||
from app.api.utils import log_activity, get_actor_employee_id
|
||||
from app.db.session import get_session
|
||||
from app.models.work import Task, TaskComment
|
||||
from app.schemas.work import TaskCommentCreate, TaskCreate, TaskUpdate
|
||||
@@ -24,7 +24,9 @@ def list_tasks(project_id: int | None = None, session: Session = Depends(get_ses
|
||||
|
||||
|
||||
@router.post("/tasks", response_model=Task)
|
||||
def create_task(payload: TaskCreate, session: Session = Depends(get_session)):
|
||||
def create_task(payload: TaskCreate, session: Session = Depends(get_session), actor_employee_id: int = Depends(get_actor_employee_id)):
|
||||
if payload.created_by_employee_id is None:
|
||||
payload = TaskCreate(**{**payload.model_dump(), "created_by_employee_id": actor_employee_id})
|
||||
task = Task(**payload.model_dump())
|
||||
if task.status not in ALLOWED_STATUSES:
|
||||
raise HTTPException(status_code=400, detail="Invalid status")
|
||||
@@ -34,7 +36,7 @@ def create_task(payload: TaskCreate, session: Session = Depends(get_session)):
|
||||
session.refresh(task)
|
||||
log_activity(
|
||||
session,
|
||||
actor_employee_id=task.created_by_employee_id,
|
||||
actor_employee_id=actor_employee_id,
|
||||
entity_type="task",
|
||||
entity_id=task.id,
|
||||
verb="created",
|
||||
@@ -45,7 +47,7 @@ def create_task(payload: TaskCreate, session: Session = Depends(get_session)):
|
||||
|
||||
|
||||
@router.patch("/tasks/{task_id}", response_model=Task)
|
||||
def update_task(task_id: int, payload: TaskUpdate, session: Session = Depends(get_session)):
|
||||
def update_task(task_id: int, payload: TaskUpdate, session: Session = Depends(get_session), actor_employee_id: int = Depends(get_actor_employee_id)):
|
||||
task = session.get(Task, task_id)
|
||||
if not task:
|
||||
raise HTTPException(status_code=404, detail="Task not found")
|
||||
@@ -60,19 +62,19 @@ def update_task(task_id: int, payload: TaskUpdate, session: Session = Depends(ge
|
||||
session.add(task)
|
||||
session.commit()
|
||||
session.refresh(task)
|
||||
log_activity(session, actor_employee_id=None, entity_type="task", entity_id=task.id, verb="updated", payload=data)
|
||||
log_activity(session, actor_employee_id=actor_employee_id, entity_type="task", entity_id=task.id, verb="updated", payload=data)
|
||||
session.commit()
|
||||
return task
|
||||
|
||||
|
||||
@router.delete("/tasks/{task_id}")
|
||||
def delete_task(task_id: int, session: Session = Depends(get_session)):
|
||||
def delete_task(task_id: int, session: Session = Depends(get_session), actor_employee_id: int = Depends(get_actor_employee_id)):
|
||||
task = session.get(Task, task_id)
|
||||
if not task:
|
||||
raise HTTPException(status_code=404, detail="Task not found")
|
||||
session.delete(task)
|
||||
session.commit()
|
||||
log_activity(session, actor_employee_id=None, entity_type="task", entity_id=task_id, verb="deleted")
|
||||
log_activity(session, actor_employee_id=actor_employee_id, entity_type="task", entity_id=task_id, verb="deleted")
|
||||
session.commit()
|
||||
return {"ok": True}
|
||||
|
||||
@@ -83,11 +85,13 @@ def list_task_comments(task_id: int, session: Session = Depends(get_session)):
|
||||
|
||||
|
||||
@router.post("/task-comments", response_model=TaskComment)
|
||||
def create_task_comment(payload: TaskCommentCreate, session: Session = Depends(get_session)):
|
||||
def create_task_comment(payload: TaskCommentCreate, session: Session = Depends(get_session), actor_employee_id: int = Depends(get_actor_employee_id)):
|
||||
if payload.author_employee_id is None:
|
||||
payload = TaskCommentCreate(**{**payload.model_dump(), "author_employee_id": actor_employee_id})
|
||||
c = TaskComment(**payload.model_dump())
|
||||
session.add(c)
|
||||
session.commit()
|
||||
session.refresh(c)
|
||||
log_activity(session, actor_employee_id=c.author_employee_id, entity_type="task", entity_id=c.task_id, verb="commented")
|
||||
log_activity(session, actor_employee_id=actor_employee_id, entity_type="task", entity_id=c.task_id, verb="commented")
|
||||
session.commit()
|
||||
return c
|
||||
|
||||
@@ -33,3 +33,23 @@ class EmploymentAction(SQLModel, table=True):
|
||||
notes: str | None = None
|
||||
|
||||
created_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
|
||||
|
||||
class AgentOnboarding(SQLModel, table=True):
|
||||
__tablename__ = "agent_onboardings"
|
||||
|
||||
id: int | None = Field(default=None, primary_key=True)
|
||||
agent_name: str
|
||||
role_title: str
|
||||
prompt: str
|
||||
cron_interval_ms: int | None = None
|
||||
tools_json: str | None = None
|
||||
owner_hr_id: int | None = Field(default=None, foreign_key="employees.id")
|
||||
|
||||
status: str = Field(default="planned") # planned|spawning|spawned|verified|blocked
|
||||
spawned_agent_id: str | None = None
|
||||
session_key: str | None = None
|
||||
|
||||
notes: str | None = None
|
||||
created_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
updated_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
|
||||
@@ -22,3 +22,30 @@ class EmploymentActionCreate(SQLModel):
|
||||
issued_by_employee_id: int
|
||||
action_type: str
|
||||
notes: str | None = None
|
||||
|
||||
|
||||
class AgentOnboardingCreate(SQLModel):
|
||||
agent_name: str
|
||||
role_title: str
|
||||
prompt: str
|
||||
cron_interval_ms: int | None = None
|
||||
tools_json: str | None = None
|
||||
owner_hr_id: int | None = None
|
||||
status: str = "planned"
|
||||
spawned_agent_id: str | None = None
|
||||
session_key: str | None = None
|
||||
notes: str | None = None
|
||||
|
||||
|
||||
class AgentOnboardingUpdate(SQLModel):
|
||||
agent_name: str | None = None
|
||||
role_title: str | None = None
|
||||
prompt: str | None = None
|
||||
cron_interval_ms: int | None = None
|
||||
tools_json: str | None = None
|
||||
owner_hr_id: int | None = None
|
||||
status: str | None = None
|
||||
spawned_agent_id: str | None = None
|
||||
session_key: str | None = None
|
||||
notes: str | None = None
|
||||
|
||||
|
||||
Reference in New Issue
Block a user