feat(incidents): add incident lifecycle api and tests

This commit is contained in:
2026-01-03 10:18:21 +00:00
parent ad94833830
commit f427d191e0
10 changed files with 1456 additions and 2 deletions

115
app/services/org.py Normal file
View File

@@ -0,0 +1,115 @@
"""Organization service providing org-scoped operations."""
from __future__ import annotations
from typing import cast
from uuid import UUID, uuid4
import asyncpg
from asyncpg.pool import PoolConnectionProxy
from app.api.deps import CurrentUser
from app.core import exceptions as exc
from app.db import Database, db
from app.repositories import NotificationRepository, OrgRepository, ServiceRepository
from app.schemas.org import (
MemberResponse,
NotificationTargetCreate,
NotificationTargetResponse,
OrgResponse,
ServiceCreate,
ServiceResponse,
)
def _as_conn(conn: asyncpg.Connection | PoolConnectionProxy) -> asyncpg.Connection:
"""Helper to satisfy typing when a pool proxy is returned."""
return cast(asyncpg.Connection, conn)
class OrgService:
"""Encapsulates organization-level operations within the active org context."""
def __init__(self, database: Database | None = None) -> None:
self.db = database or db
async def get_current_org(self, current_user: CurrentUser) -> OrgResponse:
"""Return the active organization summary for the current user."""
async with self.db.connection() as conn:
org_repo = OrgRepository(_as_conn(conn))
org = await org_repo.get_by_id(current_user.org_id)
if org is None:
raise exc.NotFoundError("Organization not found")
return OrgResponse(**org)
async def get_members(self, current_user: CurrentUser) -> list[MemberResponse]:
"""List members of the active organization."""
async with self.db.connection() as conn:
org_repo = OrgRepository(_as_conn(conn))
members = await org_repo.get_members(current_user.org_id)
return [MemberResponse(**member) for member in members]
async def create_service(self, current_user: CurrentUser, data: ServiceCreate) -> ServiceResponse:
"""Create a new service within the active organization."""
async with self.db.connection() as conn:
service_repo = ServiceRepository(_as_conn(conn))
if await service_repo.slug_exists(current_user.org_id, data.slug):
raise exc.ConflictError("Service slug already exists in this organization")
try:
service = await service_repo.create(
service_id=uuid4(),
org_id=current_user.org_id,
name=data.name,
slug=data.slug,
)
except asyncpg.UniqueViolationError as err: # pragma: no cover - race protection
raise exc.ConflictError("Service slug already exists in this organization") from err
return ServiceResponse(**service)
async def get_services(self, current_user: CurrentUser) -> list[ServiceResponse]:
"""List services for the active organization."""
async with self.db.connection() as conn:
service_repo = ServiceRepository(_as_conn(conn))
services = await service_repo.get_by_org(current_user.org_id)
return [ServiceResponse(**svc) for svc in services]
async def create_notification_target(
self,
current_user: CurrentUser,
data: NotificationTargetCreate,
) -> NotificationTargetResponse:
"""Create a notification target for the active organization."""
if data.target_type == "webhook" and data.webhook_url is None:
raise exc.BadRequestError("webhook_url is required for webhook targets")
async with self.db.connection() as conn:
notification_repo = NotificationRepository(_as_conn(conn))
target = await notification_repo.create_target(
target_id=uuid4(),
org_id=current_user.org_id,
name=data.name,
target_type=data.target_type,
webhook_url=str(data.webhook_url) if data.webhook_url else None,
enabled=data.enabled,
)
return NotificationTargetResponse(**target)
async def get_notification_targets(self, current_user: CurrentUser) -> list[NotificationTargetResponse]:
"""List notification targets for the active organization."""
async with self.db.connection() as conn:
notification_repo = NotificationRepository(_as_conn(conn))
targets = await notification_repo.get_targets_by_org(current_user.org_id)
return [NotificationTargetResponse(**target) for target in targets]
__all__ = ["OrgService"]