"""Service for closing Forgejo issues and updating local cache.""" from __future__ import annotations from typing import TYPE_CHECKING from uuid import UUID from sqlmodel import select from app.core.logging import get_logger from app.core.time import utcnow from app.db import crud from app.models.forgejo_connections import ForgejoConnection from app.models.forgejo_issues import ForgejoIssue from app.models.forgejo_repositories import ForgejoRepository from app.services.forgejo_client import get_forgejo_client if TYPE_CHECKING: from sqlalchemy.ext.asyncio.session import AsyncSession logger = get_logger(__name__) class CloseIssueError(Exception): """Base exception for close issue errors.""" class CloseIssueNotFoundError(CloseIssueError): """Raised when issue or repository is not found.""" class CloseIssueAccessError(CloseIssueError): """Raised when access is denied to close issue.""" class CloseIssueRemoteError(CloseIssueError): """Raised when remote Forgejo API call fails.""" async def close_cached_issue( session: AsyncSession, issue: ForgejoIssue, actor_agent_id: UUID | None = None, actor_user_id: UUID | None = None, ) -> dict[str, object]: """ Close a Forgejo issue remotely and update local cache. Args: session: Async session for database operations issue: ForgejoIssue to close actor_agent_id: Optional agent ID performing the close actor_user_id: Optional user ID performing the close Returns: Normalized result dict with success status and details Raises: CloseIssueNotFoundError: If repository or connection not found CloseIssueAccessError: If organization mismatch CloseIssueRemoteError: If Forgejo API call fails """ # Load repository and connection repository = await crud.get_by_id(session, ForgejoRepository, issue.repository_id) if repository is None: raise CloseIssueNotFoundError("Repository not found") connection = await crud.get_by_id(session, ForgejoConnection, repository.connection_id) if connection is None: raise CloseIssueNotFoundError("Connection not found") # Verify organization matches if issue.organization_id != repository.organization_id: raise CloseIssueAccessError("Organization mismatch") if issue.organization_id != connection.organization_id: raise CloseIssueAccessError("Organization mismatch") # Close issue on Forgejo first try: async with get_forgejo_client(connection) as client: result = await client.close_issue( owner=repository.owner, repo=repository.repo, issue_number=issue.forgejo_issue_number, ) except Exception as e: raise CloseIssueRemoteError(f"Failed to close issue on Forgejo: {e}") # Only update local cache if Forgejo call succeeded issue.state = "closed" issue.forgejo_closed_at = utcnow() issue.last_synced_at = utcnow() # Update the issue in the session session.add(issue) await session.flush() logger.info( "forgejo.issue.closed", extra={ "issue_id": str(issue.id), "forgejo_issue_number": issue.forgejo_issue_number, "repository_id": str(repository.id), "organization_id": str(issue.organization_id), "actor_agent_id": str(actor_agent_id) if actor_agent_id else None, "actor_user_id": str(actor_user_id) if actor_user_id else None, }, ) return { "success": True, "issue_id": str(issue.id), "forgejo_issue_number": issue.forgejo_issue_number, "state": "closed", "forgejo_closed_at": issue.forgejo_closed_at.isoformat() if issue.forgejo_closed_at else None, "last_synced_at": issue.last_synced_at.isoformat(), } async def close_issue_by_id( session: AsyncSession, issue_id: UUID, actor_agent_id: UUID | None = None, actor_user_id: UUID | None = None, ) -> dict[str, object]: """ Close a Forgejo issue by ID. Args: session: Async session for database operations issue_id: LocalForgejoIssue ID to close actor_agent_id: Optional agent ID performing the close actor_user_id: Optional user ID performing the close Returns: Normalized result dict with success status and details Raises: CloseIssueNotFoundError: If issue not found CloseIssueAccessError: If access is denied CloseIssueRemoteError: If Forgejo API call fails """ # Find the issue statement = select(ForgejoIssue).where(ForgejoIssue.id == issue_id) issue = (await session.exec(statement)).first() if issue is None: raise CloseIssueNotFoundError("Issue not found") # Close the issue return await close_cached_issue( session=session, issue=issue, actor_agent_id=actor_agent_id, actor_user_id=actor_user_id, )