"""Calendar commands — create, update, delete entries via Command pattern.""" from __future__ import annotations import logging import uuid from datetime import UTC, datetime from typing import Any import redis.asyncio as aioredis from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.commands.base import BaseCommand, CommandResult from app.core.outbox import enqueue_outbox_event logger = logging.getLogger(__name__) class CreateCalendarEntryCommand(BaseCommand): """Create a new calendar entry (appointment, task, reminder).""" permission = "calendar:write" def __init__(self, calendar_id: str, title: str, start_at: str, end_at: str | None = None, description: str | None = None, location: str | None = None, entry_type: str = "appointment", status: str = "open"): self.calendar_id = calendar_id self.title = title self.start_at = start_at self.end_at = end_at self.description = description self.location = location self.entry_type = entry_type self.status = status async def run(self, db: AsyncSession, redis: aioredis.Redis, current_user: dict[str, Any]) -> CommandResult: from app.plugins.builtins.calendar.models import Calendar, CalendarEntry tenant_id = self._tenant_id(current_user) user_id = self._user_id(current_user) try: cal_id = uuid.UUID(self.calendar_id) except ValueError: return CommandResult.fail("Invalid calendar_id") # Verify calendar belongs to tenant cal_result = await db.execute( select(Calendar).where(Calendar.id == cal_id, Calendar.tenant_id == tenant_id) ) if cal_result.scalar_one_or_none() is None: return CommandResult.fail("Calendar not found") entry_id = uuid.uuid4() entry = CalendarEntry( id=entry_id, tenant_id=tenant_id, calendar_id=cal_id, title=self.title, description=self.description, location=self.location, start_at=datetime.fromisoformat(self.start_at), end_at=datetime.fromisoformat(self.end_at) if self.end_at else None, entry_type=self.entry_type, status=self.status, created_by=user_id, ) db.add(entry) await db.flush() await enqueue_outbox_event(db, tenant_id, "calendar.entry.created", { "entry_id": str(entry_id), "title": self.title, "start_at": self.start_at, }) return CommandResult.ok({ "id": str(entry_id), "title": self.title, "start_at": self.start_at, "status": self.status, }) async def audit(self, db: AsyncSession, current_user: dict[str, Any]) -> None: from app.core.audit import log_audit await log_audit( db, self._tenant_id(current_user), self._user_id(current_user), action="calendar.entry.create", entity_type="calendar_entry", changes={"title": self.title, "start_at": self.start_at}, ) class UpdateCalendarEntryCommand(BaseCommand): """Update an existing calendar entry.""" permission = "calendar:write" def __init__(self, entry_id: str, data: dict[str, Any]): self.entry_id = entry_id self.data = data async def run(self, db: AsyncSession, redis: aioredis.Redis, current_user: dict[str, Any]) -> CommandResult: from app.plugins.builtins.calendar.models import CalendarEntry tenant_id = self._tenant_id(current_user) try: eid = uuid.UUID(self.entry_id) except ValueError: return CommandResult.fail("Invalid entry_id") result = await db.execute( select(CalendarEntry).where(CalendarEntry.id == eid, CalendarEntry.tenant_id == tenant_id) ) entry = result.scalar_one_or_none() if entry is None: return CommandResult.fail("Calendar entry not found") # Apply updates for key, value in self.data.items(): if hasattr(entry, key) and key not in ("id", "tenant_id", "created_at"): if key in ("start_at", "end_at") and isinstance(value, str): value = datetime.fromisoformat(value) setattr(entry, key, value) await db.flush() await enqueue_outbox_event(db, tenant_id, "calendar.entry.updated", { "entry_id": self.entry_id, "changes": self.data, }) return CommandResult.ok({"id": self.entry_id, "updated": True}) async def audit(self, db: AsyncSession, current_user: dict[str, Any]) -> None: from app.core.audit import log_audit await log_audit( db, self._tenant_id(current_user), self._user_id(current_user), action="calendar.entry.update", entity_type="calendar_entry", changes={"entry_id": self.entry_id, "fields": list(self.data.keys())}, ) class DeleteCalendarEntryCommand(BaseCommand): """Delete a calendar entry.""" permission = "calendar:delete" def __init__(self, entry_id: str): self.entry_id = entry_id async def run(self, db: AsyncSession, redis: aioredis.Redis, current_user: dict[str, Any]) -> CommandResult: from app.plugins.builtins.calendar.models import CalendarEntry tenant_id = self._tenant_id(current_user) try: eid = uuid.UUID(self.entry_id) except ValueError: return CommandResult.fail("Invalid entry_id") result = await db.execute( select(CalendarEntry).where(CalendarEntry.id == eid, CalendarEntry.tenant_id == tenant_id) ) entry = result.scalar_one_or_none() if entry is None: return CommandResult.fail("Calendar entry not found") await db.delete(entry) await db.flush() await enqueue_outbox_event(db, tenant_id, "calendar.entry.deleted", {"entry_id": self.entry_id}) return CommandResult.ok({"id": self.entry_id, "deleted": True}) async def audit(self, db: AsyncSession, current_user: dict[str, Any]) -> None: from app.core.audit import log_audit await log_audit( db, self._tenant_id(current_user), self._user_id(current_user), action="calendar.entry.delete", entity_type="calendar_entry", changes={"entry_id": self.entry_id}, )