Campaign e Tracking Links

TLDR: cria as tabelas campaign e tracking_links para gerenciar campanhas de marketing e seus links de rastreamento (UTM), com múltiplos links por campanha.

Status: proposed Created: 2026-08-07 Owner: @diegoFranciscoo


Context

Marketing precisa cadastrar campanhas e gerar/armazenar links de rastreamento (UTM) associados a cada uma. Uma mesma campanha pode ter vários links. Hoje cada campanha usa um único canal (source_channel), mas isso pode virar múltiplos canais no futuro.

A campanha referencia opcionalmente um Trial.

Mesmo com o cenário atual sendo 1 canal por campanha, source_channel fica na tabela tracking_links (e não em campaign):

  • É a mesma natureza dos demais campos do link (utm_source, utm_medium) — atributo do link, não da campanha.
  • Suporta o futuro multi-canal sem migração: hoje todos os links de uma campanha compartilham o mesmo source_channel e a listagem faz DISTINCT; amanhã passam a divergir naturalmente.
  • Colocar em campaign exigiria uma migração dolorosa (mover coluna + migrar dados + reescrever lógica) quando o negócio evoluir.

A listagem de campanhas exibe o(s) canal(is) via agregação (DISTINCT dos source_channel dos links da campanha) — problema de apresentação, não de modelagem.

Objectives

  • Criar o app Django campaigns com os models Campaign e TrackingLink.
  • Registrar o app em INSTALLED_APPS.
  • Gerar a migration inicial.

Non-goals

  • Registro no Django admin — escopo futuro.
  • Endpoints/serializers de API (DRF) — não faz parte deste escopo.
  • Tabela intermediária entre usuário trial e campanha (TODO separado no Trial).
  • Geração automática de final_link a partir dos UTMs — o link é persistido como veio; a montagem/validação de UTM fica para escopo futuro.
  • UI/tela de listagem de campanhas.

Changes

apps/campaigns/ (novo app)

  • apps/campaigns/__init__.py
  • apps/campaigns/apps.py — CampaignsConfig (padrão do TrialsConfig).
  • apps/campaigns/models/__init__.py — reexporta Campaign, TrackingLink.
  • apps/campaigns/models/campaign.py — model Campaign(BaseModel):
    • name — CharField(max_length=255), obrigatório.
    • status — CharField com TextChoices: active (“Ativa”), inactive (“Inativa”), archived (“Arquivada”); default inactive.
    • start_date — DateField(null=True, blank=True).
    • end_date — DateField(null=True, blank=True).
    • trial — ForeignKey("trials.Trial", on_delete=SET_NULL, null=True, blank=True, related_name="campaigns").
    • slug — SlugField(max_length=255, unique=True), obrigatório. Auto-gerado a partir de name quando vazio, seguindo o padrão _generate_unique_slug do Course (checa contra all_objects para não colidir com soft-deleted).
    • Meta: verbose_name/plural, ordering = ["-created_at"].
    • clean(): valida end_date > start_date quando ambos preenchidos (padrão do Trial).
  • apps/campaigns/models/tracking_link.py — model TrackingLink(BaseModel):
    • campaign — ForeignKey(Campaign, on_delete=CASCADE, related_name="tracking_links"), obrigatório.
    • source_channel — CharField(max_length=100, blank=True).
    • utm_source — CharField(max_length=255), obrigatório.
    • utm_medium — CharField(max_length=255), obrigatório.
    • utm_content — CharField(max_length=255, blank=True).
    • utm_term — CharField(max_length=255, blank=True).
    • final_link — URLField(max_length=2048), obrigatório.
    • Meta: verbose_name/plural, ordering = ["-created_at"].
  • apps/campaigns/migrations/__init__.py
  • apps/campaigns/migrations/0001_initial.py — gerada via makemigrations (depende de trials).

config/settings/base.py

  • Adicionar "apps.campaigns.apps.CampaignsConfig" em INSTALLED_APPS, junto aos demais apps locais.

How to verify

  • python manage.py makemigrations campaigns gera 0001_initial sem prompts inesperados.
  • python manage.py migrate aplica sem erro.
  • python manage.py check passa.
  • No shell: criar uma Campaign sem slug gera slug único a partir do name; criar duas com o mesmo name gera slugs distintos.
  • Criar duas TrackingLink para a mesma Campaign funciona (múltiplos links por campanha).
  • end_date <= start_date levanta ValidationError no full_clean().

Documentation

  • Sem novas regras de negócio a documentar em .project/docs/ além do próprio spec. Caso seeds/factories sejam necessários, aplicar wehive:seed em escopo posterior.