Newer
Older
gnexus-tasks / backend / app / services / closing.py
"""Общий путь закрытия задачи (ТЗ 3.5, 3.13): спавн регулярной + награды.

Один и тот же код используется HTTP-API и MCP-инструментами: закрытие задачи
агентом должно давать ровно те же XP/монеты/растение, что и закрытие из UI.
Идемпотентность: XP — один раз на задачу (grant_task_xp), спавн регулярной —
один раз на цепочку (метка spawned_at). Коммит делает вызывающий код.
"""

from dataclasses import dataclass

from sqlalchemy.orm import Session

from app.models import CoinEvent, Task, XpEvent, utcnow
from app.services import xp as xp_service
from app.services.garden import coins_for_xp, level_bonus_coins
from app.services.recurrence import spawn_next


@dataclass
class CloseOutcome:
    """Результат закрытия: что начислено/создано (для заголовков и SSE)."""

    event: XpEvent | None
    spawned: Task | None
    coins: int  # монеты за задачу, включая бонус за уровень


def handle_task_closed(db: Session, task: Task, via_options: bool = False) -> CloseOutcome:
    """Задача перешла в done (done_at уже поставлен) — начислить всё положенное.

    Спавн следующего экземпляра регулярной — только если ещё не спавнили
    (spawned_at is None); XP/монеты grant_task_xp не даст повторно и сам.
    """
    outcome = CloseOutcome(event=None, spawned=None, coins=0)
    if task.spawned_at is None:
        spawned = spawn_next(db, task)
        if spawned is not None:
            task.spawned_at = utcnow()
            outcome.spawned = spawned
    event = xp_service.grant_task_xp(db, task)
    if event is not None:
        event.via_options = via_options
        outcome.event = event
        # Монеты — вторая награда за закрытие (валюта сада, рядом с XP)
        old_level = xp_service.level_from_xp(xp_service.total_xp(db) - event.amount)
        coins = coins_for_xp(event.amount)
        db.add(CoinEvent(task_id=task.id, source="task", amount=coins))
        new_level = xp_service.level_from_xp(xp_service.total_xp(db))
        if new_level > old_level:
            coins += level_bonus_coins(new_level)
            db.add(CoinEvent(source="level", level=new_level, amount=level_bonus_coins(new_level)))
        outcome.coins = coins
    return outcome