---
name: novo-modelo
description: >
  Cria um Django Model completo no padrão Fat Model do SurveyHub: UUID PK, TextChoices para
  status, soft delete (deleted_at), state machine (ativar/pausar/encerrar com guards), propriedades
  computadas, Manager customizado com métodos em português (ativos, do_usuario, nao_deletados),
  factory com Traits e testes de model. Segue .claude/rules/architecture/django-models.md.
  Use quando o usuário disser "criar model", "novo model", "criar entidade", "adicionar model",
  "preciso de um model para X", "criar Survey", "criar Question", "nova entidade de domínio",
  "adicionar campo ao model", "criar modelo Django".
  Invoke proativamente quando o sprint plan especifica criação de um novo model Django.
  DO NOT USE para criar um novo app inteiro — use /novo-app.
  DO NOT USE para criar apenas views — use /novas-views.
argument-hint: "<app> <NomeDoModel>"
---

# /novo-modelo $ARGUMENTS

Cria um Fat Model Django completo no padrão SurveyHub.

## Informações Necessárias

| Pergunta | Exemplo |
|----------|---------|
| App (`$1`) | `surveys`, `users`, `notifications` |
| Nome do Model (`$2`) | `Survey`, `Question`, `Notification` |
| Tem status/state machine? | Sim (quais estados?) / Não |
| Tem soft delete? | Sim (padrão) / Não |
| Pertence a um User? | Sim (FK para User) / Não |
| Campos principais | `title`, `description`, `link_hash` |

**Derivar:**
- `{app}` = `$1` snake_case
- `{Model}` = `$2` PascalCase
- `{model}` = `$2` snake_case

---

## Fase 1 — Ler Contexto

```bash
cat apps/{app}/models.py
cat apps/{app}/managers.py
```

---

## Fase 2 — Model (Fat Model)

Adicionar a `apps/{app}/models.py`:

```python
import uuid
from django.db import models
from django.utils import timezone
from apps.{app}.managers import {Model}Manager


class {Model}(models.Model):
    """{Model} — [descrição do domínio em uma linha].

    [Invariantes e regras de negócio principais.]
    """

    class Status(models.TextChoices):
        DRAFT = "draft", "Rascunho"
        ACTIVE = "active", "Ativo"
        PAUSED = "paused", "Pausado"
        CLOSED = "closed", "Encerrado"

    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    # user = models.ForeignKey("users.User", on_delete=models.CASCADE, related_name="{model}s")
    title = models.CharField(max_length=255)
    description = models.TextField(blank=True)
    status = models.CharField(
        max_length=20, choices=Status.choices, default=Status.DRAFT, db_index=True
    )
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    deleted_at = models.DateTimeField(null=True, blank=True, db_index=True)

    objects = {Model}Manager()

    class Meta:
        ordering = ["-created_at"]
        verbose_name = "{Model}"
        verbose_name_plural = "{Model}s"
        indexes = [models.Index(fields=["status", "deleted_at"])]

    def __str__(self) -> str:
        return self.title

    # ── State Machine ─────────────────────────────────────────────────────

    def pode_ser_ativado(self) -> bool:
        """Verifica se o {model} pode ser ativado agora."""
        return self.status == self.Status.DRAFT

    def ativar(self) -> None:
        """Ativa o {model}, validando pré-condições antes da transição.

        Raises:
            ValueError: Se o {model} não pode ser ativado no estado atual.
        """
        if not self.pode_ser_ativado():
            raise ValueError(
                f"{Model} não pode ser ativado com status '{self.status}'."
            )
        self.status = self.Status.ACTIVE
        self.save(update_fields=["status", "updated_at"])

    def pausar(self) -> None:
        """Pausa o {model} (apenas se ativo)."""
        if self.status != self.Status.ACTIVE:
            raise ValueError(
                f"{Model} só pode ser pausado quando ativo (atual: '{self.status}')."
            )
        self.status = self.Status.PAUSED
        self.save(update_fields=["status", "updated_at"])

    def encerrar(self) -> None:
        """Encerra o {model}."""
        if self.status == self.Status.CLOSED:
            raise ValueError("{Model} já está encerrado.")
        self.status = self.Status.CLOSED
        self.save(update_fields=["status", "updated_at"])

    # ── Soft Delete ───────────────────────────────────────────────────────

    def soft_delete(self) -> None:
        """Marca como deletado sem remover do banco."""
        self.deleted_at = timezone.now()
        self.save(update_fields=["deleted_at", "updated_at"])

    @property
    def is_deleted(self) -> bool:
        """Verifica se o registro foi deletado."""
        return self.deleted_at is not None

    @property
    def is_active(self) -> bool:
        """Verifica se o {model} está ativo e acessível."""
        return self.status == self.Status.ACTIVE and not self.is_deleted
```

---

## Fase 3 — Manager Customizado

Em `apps/{app}/managers.py`:

```python
from django.db import models


class {Model}QuerySet(models.QuerySet):
    def ativos(self):
        return self.filter(status="active", deleted_at__isnull=True)

    def nao_deletados(self):
        return self.filter(deleted_at__isnull=True)

    def do_usuario(self, user):
        return self.filter(user=user, deleted_at__isnull=True)


class {Model}Manager(models.Manager):
    """Manager customizado para {Model} com filtros frequentes."""

    def get_queryset(self):
        return {Model}QuerySet(self.model, using=self._db)

    def ativos(self):
        return self.get_queryset().ativos()

    def nao_deletados(self):
        return self.get_queryset().nao_deletados()

    def do_usuario(self, user):
        return self.get_queryset().do_usuario(user)
```

---

## Fase 4 — Migration

```bash
python manage.py makemigrations {app}
python manage.py migrate
```

---

## Fase 5 — Factory com Traits

Em `apps/{app}/tests/factories.py`:

```python
import factory
from factory.django import DjangoModelFactory
from apps.{app}.models import {Model}


class {Model}Factory(DjangoModelFactory):
    class Meta:
        model = {Model}

    title = factory.Faker("sentence", locale="pt_BR", nb_words=5)
    description = factory.Faker("paragraph", locale="pt_BR")
    status = {Model}.Status.DRAFT

    class Params:
        """Traits — usar em vez de subclasses."""
        active = factory.Trait(status={Model}.Status.ACTIVE)
        paused = factory.Trait(status={Model}.Status.PAUSED)
        closed = factory.Trait(status={Model}.Status.CLOSED)

# {Model}Factory()             → draft
# {Model}Factory(active=True)  → active
# NUNCA: class Active{Model}Factory({Model}Factory)
```

---

## Fase 6 — Testes do Model

Em `apps/{app}/tests/test_models.py`:

```python
import pytest
from apps.{app}.tests.factories import {Model}Factory


@pytest.mark.django_db
class Test{Model}StateMachine:
    def test_pode_ser_ativado_quando_draft(self):
        assert {Model}Factory().pode_ser_ativado() is True

    def test_nao_pode_ser_ativado_quando_ativo(self):
        assert {Model}Factory(active=True).pode_ser_ativado() is False

    def test_ativar_muda_status(self):
        m = {Model}Factory()
        m.ativar()
        m.refresh_from_db()
        assert m.status == {Model}.Status.ACTIVE

    def test_ativar_falha_se_ja_ativo(self):
        with pytest.raises(ValueError):
            {Model}Factory(active=True).ativar()


@pytest.mark.django_db
class Test{Model}SoftDelete:
    def test_soft_delete_marca_deleted_at(self):
        m = {Model}Factory()
        m.soft_delete()
        assert m.deleted_at is not None
        assert m.is_deleted is True

    def test_permanece_no_banco_apos_soft_delete(self):
        from apps.{app}.models import {Model} as M
        m = {Model}Factory()
        pk = m.pk
        m.soft_delete()
        assert M.objects.filter(pk=pk).exists()
```

---

## Checklist

- [ ] UUID PK com `default=uuid.uuid4, editable=False`
- [ ] `TextChoices` para status com display em português
- [ ] `deleted_at` DateTimeField nullable com `db_index=True`
- [ ] `__str__` descritivo
- [ ] `Meta.ordering`, `verbose_name`, `indexes`
- [ ] State machine: `pode_ser_X()` + ativar/pausar/encerrar com guard
- [ ] Manager: `ativos()`, `nao_deletados()`, `do_usuario()`
- [ ] Migration gerada e aplicada
- [ ] Factory com `class Params` + Traits
- [ ] Testes de state machine e soft delete

## Referências

- `.claude/rules/architecture/django-models.md`
- `.claude/rules/naming-conventions.md`
- `.claude/rules/testing.md`
