from __future__ import annotations from .shared import * from .helpers import _cleanup_project_dependencies, _fetch_project_member_user_ids, _get_project_or_404, _serialize_project_row from .schemas import ProjectCreateRequest, ProjectMembersAssignRequest, ProjectUpdateRequest @router.post("/") async def create_project( body: ProjectCreateRequest, db: AsyncSession = Depends(get_db), current_user: User = Depends(require_roles("OWNER", "REVIEWER")), ): tenant_id = int(current_user.tenant_id) title = compact_title(body.title, max_len=24) if not title: raise HTTPException(status_code=400, detail="title required") try: member_user_ids = await resolve_project_member_ids( db, tenant_id=tenant_id, owner_id=int(current_user.id), assigned_role=body.assigned_role, requested_member_user_ids=body.member_user_ids, require_requested_members=False, include_owner=True, ) except ProjectMemberAssignmentError as exc: await notify_member_assignment_failure( db, tenant_id=tenant_id, actor_user_id=int(current_user.id), project_title=title, reason=str(exc), interface_path="/projects/", commit=True, ) raise HTTPException(status_code=400, detail=str(exc)) from exc await ensure_project_id_sequence(db) project = Project( tenant_id=tenant_id, name=title, title=title, description=body.description, status=body.status or "active", domain_id=body.domain_id, assigned_role=body.assigned_role, owner_id=int(current_user.id), signal_count=0, ) db.add(project) try: await db.flush() await replace_project_members( db, tenant_id=tenant_id, project_id=int(project.id), member_user_ids=member_user_ids, ) await log_operation( db, tenant_id=tenant_id, user_id=int(current_user.id), project_id=int(project.id), entity_type="project", entity_id=str(project.id), action="project.create", before_data=None, after_data={ "title": project.title, "status": project.status, "member_user_ids": member_user_ids, }, interface_path="/projects/", caller="user", ) await db.commit() except Exception: await db.rollback() raise await db.refresh(project) await context_cache_service.invalidate_system_context(tenant_id=tenant_id) await AIPMOKernelService.invalidate_world_model_cache(tenant_id=tenant_id, project_id=int(project.id)) return _serialize_project_row(project) @router.put("/{project_id}/members") async def assign_project_members( project_id: int, body: ProjectMembersAssignRequest, db: AsyncSession = Depends(get_db), current_user: User = Depends(require_roles("OWNER", "REVIEWER")), ): tenant_id = int(current_user.tenant_id) project = await _get_project_or_404(db, tenant_id=tenant_id, project_id=project_id) before_member_user_ids = await _fetch_project_member_user_ids( db, tenant_id=tenant_id, project_id=int(project.id), ) try: member_user_ids = await resolve_project_member_ids( db, tenant_id=tenant_id, owner_id=None, assigned_role=project.assigned_role, requested_member_user_ids=body.member_user_ids, require_requested_members=True, include_owner=False, ) await replace_project_members( db, tenant_id=tenant_id, project_id=int(project.id), member_user_ids=member_user_ids, ) await log_operation( db, tenant_id=tenant_id, user_id=int(current_user.id), project_id=int(project.id), entity_type="project", entity_id=str(project.id), action="project.members.assign", before_data={"member_user_ids": before_member_user_ids}, after_data={"member_user_ids": member_user_ids}, interface_path=f"/projects/{project_id}/members", caller="user", ) await db.commit() except ProjectMemberAssignmentError as exc: await db.rollback() await notify_member_assignment_failure( db, tenant_id=tenant_id, actor_user_id=int(current_user.id), project_id=int(project.id), project_title=project.title or project.name, reason=str(exc), interface_path=f"/projects/{project_id}/members", commit=True, ) raise HTTPException(status_code=400, detail=str(exc)) from exc except Exception: await db.rollback() raise await context_cache_service.invalidate_system_context(tenant_id=tenant_id) await AIPMOKernelService.invalidate_world_model_cache(tenant_id=tenant_id, project_id=int(project.id)) return { "project_id": str(project.id), "member_user_ids": [str(user_id) for user_id in member_user_ids], } @router.patch("/{project_id}") async def update_project( project_id: int, body: ProjectUpdateRequest, db: AsyncSession = Depends(get_db), current_user: User = Depends(require_roles("OWNER", "REVIEWER")), ): tenant_id = int(current_user.tenant_id) project = await _get_project_or_404(db, tenant_id=tenant_id, project_id=project_id) if body.title is not None: title = compact_title(body.title, max_len=24) if not title: raise HTTPException(status_code=400, detail="title cannot be empty") project.title = title project.name = title if body.description is not None: project.description = body.description if body.status is not None: project.status = body.status if body.domain_id is not None: project.domain_id = body.domain_id if body.assigned_role is not None: project.assigned_role = body.assigned_role await db.commit() await db.refresh(project) await context_cache_service.invalidate_system_context(tenant_id=tenant_id) await AIPMOKernelService.invalidate_world_model_cache(tenant_id=tenant_id, project_id=int(project.id)) return _serialize_project_row(project) @router.delete("/{project_id}") async def delete_project( project_id: int, db: AsyncSession = Depends(get_db), current_user: User = Depends(require_roles("OWNER", "REVIEWER")), ): tenant_id = int(current_user.tenant_id) _ = current_user try: await _get_project_or_404(db, tenant_id=tenant_id, project_id=project_id) await _cleanup_project_dependencies( db, tenant_id=tenant_id, project_id=project_id, ) await db.execute( delete(Project).where( and_( Project.tenant_id == tenant_id, Project.id == project_id, ) ) ) await db.commit() except IntegrityError as exc: await db.rollback() raise HTTPException(status_code=409, detail="Project has related records and cannot be deleted") from exc except Exception: await db.rollback() raise await context_cache_service.invalidate_system_context(tenant_id=tenant_id) await AIPMOKernelService.invalidate_world_model_cache(tenant_id=tenant_id, project_id=project_id) return {"message": "Project deleted"}