from pydantic import BaseModel from typing import Optional, List from datetime import datetime, time from enum import Enum class TaskTypeEnum(str, Enum): ISSUE = "issue" MAINTENANCE = "maintenance" RESEARCH = "research" REVIEW = "review" STORY = "story" TEST = "test" RESOLUTION = "resolution" # P7.1: 'task' type removed — defect subtype migrated to issue/defect class TaskStatusEnum(str, Enum): OPEN = "open" PENDING = "pending" UNDERGOING = "undergoing" COMPLETED = "completed" CLOSED = "closed" class TaskPriorityEnum(str, Enum): LOW = "low" MEDIUM = "medium" HIGH = "high" CRITICAL = "critical" # Task schemas class TaskBase(BaseModel): title: str description: Optional[str] = None task_type: TaskTypeEnum = TaskTypeEnum.ISSUE task_subtype: Optional[str] = None priority: TaskPriorityEnum = TaskPriorityEnum.MEDIUM tags: Optional[str] = None estimated_effort: Optional[int] = None estimated_working_time: Optional[str] = None class TaskCreate(TaskBase): project_code: Optional[str] = None milestone_code: Optional[str] = None reporter_id: Optional[int] = None assignee_id: Optional[int] = None type: Optional[TaskTypeEnum] = None # Resolution specific resolution_summary: Optional[str] = None positions: Optional[str] = None pending_matters: Optional[str] = None class TaskUpdate(BaseModel): title: Optional[str] = None description: Optional[str] = None task_type: Optional[TaskTypeEnum] = None type: Optional[TaskTypeEnum] = None task_subtype: Optional[str] = None status: Optional[TaskStatusEnum] = None priority: Optional[TaskPriorityEnum] = None assignee_id: Optional[int] = None taken_by: Optional[str] = None tags: Optional[str] = None estimated_effort: Optional[int] = None # Resolution specific resolution_summary: Optional[str] = None positions: Optional[str] = None pending_matters: Optional[str] = None class TaskResponse(TaskBase): status: TaskStatusEnum task_code: Optional[str] = None code: Optional[str] = None type: Optional[str] = None due_date: Optional[datetime] = None project_code: Optional[str] = None milestone_code: Optional[str] = None reporter_id: int assignee_id: Optional[int] = None taken_by: Optional[str] = None created_by_id: Optional[int] = None estimated_working_time: Optional[time] = None resolution_summary: Optional[str] = None positions: Optional[str] = None pending_matters: Optional[str] = None # BE-PR-008: Proposal Accept tracking source_proposal_code: Optional[str] = None source_essential_code: Optional[str] = None created_at: datetime updated_at: Optional[datetime] = None class Config: from_attributes = True # Comment schemas class CommentBase(BaseModel): content: str class CommentCreate(CommentBase): task_id: int author_id: int class CommentUpdate(BaseModel): content: Optional[str] = None class CommentResponse(CommentBase): id: int task_id: int author_id: int created_at: datetime updated_at: Optional[datetime] = None class Config: from_attributes = True # Project schemas class ProjectBase(BaseModel): name: str owner_name: Optional[str] = None description: Optional[str] = None repo: Optional[str] = None sub_projects: Optional[list[str]] = None related_projects: Optional[list[str]] = None class ProjectCreate(ProjectBase): owner_id: int class ProjectUpdate(BaseModel): description: Optional[str] = None owner_name: Optional[str] = None repo: Optional[str] = None sub_projects: Optional[list[str]] = None related_projects: Optional[list[str]] = None class ProjectResponse(BaseModel): id: int name: str owner_name: Optional[str] = None project_code: Optional[str] = None description: Optional[str] = None repo: Optional[str] = None sub_projects: Optional[list[str]] = None related_projects: Optional[list[str]] = None owner_id: int created_at: datetime class Config: from_attributes = True # User schemas class UserBase(BaseModel): username: str email: str full_name: Optional[str] = None class UserCreate(UserBase): password: Optional[str] = None role_id: Optional[int] = None discord_user_id: Optional[str] = None # Agent binding (both must be provided or both omitted) agent_id: Optional[str] = None claw_identifier: Optional[str] = None class UserUpdate(BaseModel): full_name: Optional[str] = None email: Optional[str] = None password: Optional[str] = None role_id: Optional[int] = None is_active: Optional[bool] = None discord_user_id: Optional[str] = None class UserResponse(UserBase): id: int is_active: bool is_admin: bool role_id: Optional[int] = None role_name: Optional[str] = None agent_id: Optional[str] = None discord_user_id: Optional[str] = None created_at: datetime class Config: from_attributes = True # Project Member schemas class ProjectMemberBase(BaseModel): user_id: int role: str = "dev" class ProjectMemberCreate(ProjectMemberBase): pass class ProjectMemberResponse(BaseModel): id: int user_id: int username: Optional[str] = None full_name: Optional[str] = None project_id: int role: str = "dev" class Config: from_attributes = True class MilestoneStatusEnum(str, Enum): OPEN = "open" FREEZE = "freeze" UNDERGOING = "undergoing" COMPLETED = "completed" CLOSED = "closed" # Milestone schemas class MilestoneBase(BaseModel): title: str description: Optional[str] = None status: Optional[MilestoneStatusEnum] = MilestoneStatusEnum.OPEN due_date: Optional[datetime] = None planned_release_date: Optional[datetime] = None depend_on_milestones: Optional[List[str]] = None depend_on_tasks: Optional[List[int]] = None class MilestoneCreate(MilestoneBase): project_id: Optional[int] = None pass class MilestoneUpdate(BaseModel): title: Optional[str] = None description: Optional[str] = None status: Optional[MilestoneStatusEnum] = None due_date: Optional[datetime] = None planned_release_date: Optional[datetime] = None depend_on_milestones: Optional[List[str]] = None depend_on_tasks: Optional[List[int]] = None class MilestoneResponse(MilestoneBase): milestone_code: Optional[str] = None code: Optional[str] = None project_code: Optional[str] = None created_by_id: Optional[int] = None started_at: Optional[datetime] = None created_at: datetime updated_at: Optional[datetime] = None class Config: from_attributes = True # Proposal schemas (renamed from Propose) class ProposalStatusEnum(str, Enum): OPEN = "open" ACCEPTED = "accepted" REJECTED = "rejected" class ProposalBase(BaseModel): title: str description: Optional[str] = None class ProposalCreate(ProposalBase): pass class ProposalUpdate(BaseModel): title: Optional[str] = None description: Optional[str] = None class ProposalResponse(ProposalBase): proposal_code: Optional[str] = None # preferred name propose_code: Optional[str] = None # backward compat alias (same value) status: ProposalStatusEnum project_code: Optional[str] = None created_by_id: Optional[int] = None created_by_username: Optional[str] = None feat_task_id: Optional[str] = None # DEPRECATED (BE-PR-010): legacy field, read-only. Use generated_tasks instead. created_at: datetime updated_at: Optional[datetime] = None class Config: from_attributes = True # --------------------------------------------------------------------------- # Essential schemas (under Proposal) # --------------------------------------------------------------------------- class EssentialTypeEnum(str, Enum): FEATURE = "feature" IMPROVEMENT = "improvement" REFACTOR = "refactor" class EssentialBase(BaseModel): title: str type: EssentialTypeEnum description: Optional[str] = None class EssentialCreate(EssentialBase): """Create a new Essential under a Proposal. ``proposal_id`` is inferred from the URL path, not the body. """ pass class EssentialUpdate(BaseModel): title: Optional[str] = None type: Optional[EssentialTypeEnum] = None description: Optional[str] = None class EssentialResponse(EssentialBase): essential_code: str proposal_code: Optional[str] = None created_by_id: Optional[int] = None created_at: datetime updated_at: Optional[datetime] = None class Config: from_attributes = True class GeneratedTaskBrief(BaseModel): """Brief info about a story task generated from Proposal Accept.""" task_code: Optional[str] = None task_type: str task_subtype: Optional[str] = None title: str status: Optional[str] = None source_essential_code: Optional[str] = None class ProposalDetailResponse(ProposalResponse): """Extended Proposal response that embeds its Essential list and generated tasks.""" essentials: List[EssentialResponse] = [] generated_tasks: List[GeneratedTaskBrief] = [] class Config: from_attributes = True class GeneratedTaskSummary(BaseModel): """Brief summary of a task generated from a Proposal Essential.""" task_code: str task_type: str task_subtype: str title: str essential_code: str class ProposalAcceptResponse(ProposalResponse): """Response for Proposal Accept — includes the generated story tasks.""" essentials: List[EssentialResponse] = [] generated_tasks: List[GeneratedTaskSummary] = [] class Config: from_attributes = True # --------------------------------------------------------------------------- # Agent schemas (BE-CAL-003) # --------------------------------------------------------------------------- class AgentStatusEnum(str, Enum): IDLE = "idle" ON_CALL = "on_call" BUSY = "busy" EXHAUSTED = "exhausted" OFFLINE = "offline" class ExhaustReasonEnum(str, Enum): RATE_LIMIT = "rate_limit" BILLING = "billing" class AgentResponse(BaseModel): """Read-only representation of an Agent.""" id: int user_id: int agent_id: str claw_identifier: str status: AgentStatusEnum last_heartbeat: Optional[datetime] = None exhausted_at: Optional[datetime] = None recovery_at: Optional[datetime] = None exhaust_reason: Optional[ExhaustReasonEnum] = None created_at: datetime class Config: from_attributes = True class AgentStatusUpdate(BaseModel): """Payload for updating an agent's runtime status.""" status: AgentStatusEnum exhaust_reason: Optional[ExhaustReasonEnum] = None recovery_at: Optional[datetime] = None # Backward-compatible aliases ProposeStatusEnum = ProposalStatusEnum ProposeBase = ProposalBase ProposeCreate = ProposalCreate ProposeUpdate = ProposalUpdate ProposeResponse = ProposalResponse # Paginated response from typing import Generic, TypeVar T = TypeVar("T") class PaginatedResponse(BaseModel, Generic[T]): items: List[T] total: int page: int page_size: int total_pages: int