"""Draft persistence and the send/undo flow (SPEC ยง6.5).""" from __future__ import annotations import uuid from datetime import UTC, datetime, timedelta from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.config import get_settings from app.models import Account, Draft, Outbox from app.outbox.schemas import DraftIn class DraftError(Exception): """Draft could not be created/updated (e.g. account not owned).""" async def upsert_draft( session: AsyncSession, *, user_id: uuid.UUID, body: DraftIn, draft_id: uuid.UUID | None ) -> Draft: """Create or update a draft owned by ``user_id``.""" account = await session.scalar( select(Account).where(Account.id == body.account_id, Account.user_id == user_id) ) if account is None: raise DraftError("account not found") payload = body.payload.model_dump(mode="json") if draft_id is None: draft = Draft( user_id=user_id, account_id=body.account_id, thread_id=body.thread_id, reply_to_message_id=body.reply_to_message_id, kind=body.kind, payload=payload, ) session.add(draft) else: fetched = await session.scalar( select(Draft).where(Draft.id == draft_id, Draft.user_id == user_id) ) if fetched is None: raise DraftError("draft not found") draft = fetched draft.account_id = body.account_id draft.thread_id = body.thread_id draft.reply_to_message_id = body.reply_to_message_id draft.kind = body.kind draft.payload = payload draft.updated_at = datetime.now(UTC) await session.flush() return draft async def create_outbox(session: AsyncSession, *, user_id: uuid.UUID, draft: Draft) -> Outbox: """Create an outbox row in the undo window for a draft.""" send_after = datetime.now(UTC) + timedelta(seconds=get_settings().undo_seconds) outbox = Outbox(user_id=user_id, draft_id=draft.id, state="undo_window", send_after=send_after) session.add(outbox) await session.flush() return outbox async def cancel_outbox(session: AsyncSession, *, user_id: uuid.UUID, outbox_id: uuid.UUID) -> bool: """Cancel a send if still in the undo window. Returns False if too late/unknown.""" outbox = await session.scalar( select(Outbox).where(Outbox.id == outbox_id, Outbox.user_id == user_id) ) if outbox is None or outbox.state != "undo_window": return False outbox.state = "cancelled" await session.flush() return True