# FASE 19 — Homepage Editorial Architecture & Content Control

## Architecture Audit Report

> **Data:** 2026-07-21
> **Fases anteriores:** FASE 18 (Settings → Homepage) concluída
> **Estado actual:** 68 testes, 209 assertions, 0 falhas

---

## 1. Estado Actual da Homepage

### 1.1 Rota

```
GET / → routes/web.php:15 (Closure)
         → view('welcome')
```

Sem Controller, sem Service, sem View Composer. Toda a lógica de preparação de dados está no `@php` block da view.

### 1.2 Data Flow Actual

```
Request GET /
  ↓
Route Closure (routes/web.php:15)
  ↓
view('welcome')
  ↓
@php block (welcome.blade.php:3-18)
  ├── Category::showcase()->get()
  ├── Campaign::current()->take(2)->get()
  ├── NewsArticle::published()->latest()->take(3)->get()
  ├── Store::count()
  ├── Province::orderBy('name')->get()
  ├── Setting::group('home')
  ├── Setting::group('contact')
  └── Setting::group('seo')
  ↓
HTML output
```

### 1.3 Secções da Homepage

| # | Secção | Linhas | Tipo | Fonte | Conteúdo |
|---|--------|--------|------|-------|----------|
| 1 | Hero | 27-54 | Configuração | Settings `home.*` | badge, title (HTML), subtitle, CTA primário, CTA secundário |
| 2 | Localizador | 59-112 | Entidade + Config | `Province`, Settings `home.locator_*` | título, texto (:count), help title/text, phone, whatsapp |
| 3 | Categorias | 117-146 | Entidade | `Category::showcase()` | badge, título, texto, link label (Settings) + lista de categorias (Model) |
| 4 | Campanhas | 151-188 | Entidade | `Campaign::current()` | badge, título, texto (Settings) + lista de campanhas (Model) |
| 5 | Experiência | 193-226 | Configuração | Settings `home.*` | badge, título, texto, 3 cards (title + text cada) |
| 6 | Negócios | 231-279 | Configuração | Settings `home.business_*` | badge, título, texto, 3 bullets, CTA, anos value/text |
| 7 | Rede | 284-335 | Entidade | `Store::active()`, Settings `home.network_*` | badge, título, texto (Settings) + lista de lojas (Model) |
| 8 | Notícias | 340-388 | Entidade | `NewsArticle::published()`, Settings `home.news_*` | badge, título, texto (Settings) + lista de artigos (Model) |
| 9 | CTA Final | 393-410 | Configuração | Settings `home.cta_*` | título, texto, CTA primário, CTA secundário |

### 1.4 Queries Executadas

| # | Query | Cache | TTL | Fonte |
|---|-------|-------|-----|-------|
| 1 | `Category::showcase()->get()` | ❌ | — | @php block |
| 2 | `Campaign::current()->take(2)->get()` | ❌ | — | @php block |
| 3 | `NewsArticle::published()->latest()->take(3)->get()` | ❌ | — | @php block |
| 4 | `Store::count()` | ❌ | — | @php block |
| 5 | `Province::orderBy('name')->get()` | ❌ | — | @php block |
| 6 | `Setting::group('home')` | ❌ | — | @php block |
| 7 | `Setting::group('contact')` | ❌ | — | @php block |
| 8 | `Setting::group('seo')` | ❌ | — | @php block |
| 9 | `Store::active()->orderBy('sort_order')->take(4)->get()` | ❌ | — | @php block (featuredStores) |

**Nota:** Cache foi removida na FASE 18 devido a erro de unserialização no PHP 8.5 com driver `database`.

---

## 2. Inventário Completo de Settings

### 2.1 Grupo `home` (FASE 18)

| Key | Valor actual | Classificação actual | Deveria ser? | Acção |
|-----|-------------|---------------------|-------------|-------|
| `hero_badge` | "Rede de supermercados em Angola" | Configuração | ✅ Setting | Manter |
| `hero_title` | "O teu supermercado<br><span..."> | Configuração (HTML) | ✅ Setting | Manter |
| `hero_subtitle` | "Frescura, qualidade..." | Configuração | ✅ Setting | Manter |
| `hero_cta_primary` | "Encontrar uma loja" | Configuração | ✅ Setting | Manter |
| `hero_cta_secondary` | "Para Negócios" | Configuração | ✅ Setting | Manter |
| `locator_title` | "Encontre a loja mais próxima" | Configuração | ✅ Setting | Manter |
| `locator_subtitle` | "Estamos em :count lojas..." | Configuração | ✅ Setting | Manter |
| `locator_help_title` | "Precisa de ajuda..." | Configuração | ✅ Setting | Manter |
| `locator_help_text` | "Ligue-nos ou envie..." | Configuração | ✅ Setting | Manter |
| `categories_badge` | "Universo de produtos" | Configuração | ✅ Setting | Manter |
| `categories_title` | "O que encontra no Mangolê" | Configuração | ✅ Setting | Manter |
| `categories_text` | "Do básico ao especial..." | Configuração | ✅ Setting | Manter |
| `categories_link` | "Ver todos os produtos" | Configuração | ✅ Setting | Manter |
| `campaigns_badge` | "Em destaque" | Configuração | ✅ Setting | Manter |
| `campaigns_title` | "Campanhas activas" | Configuração | ✅ Setting | Manter |
| `campaigns_text` | "Aproveite as nossas..." | Configuração | ✅ Setting | Manter |
| `experience_badge` | "Para si" | Configuração | ✅ Setting | Manter |
| `experience_title` | "Porque escolher o Mangolê?" | Configuração | ✅ Setting | Manter |
| `experience_text` | "Cuidamos de cada detalhe..." | Configuração | ✅ Setting | Manter |
| `card1_title` | "Frescura garantida" | Configuração | ✅ Setting | Manter |
| `card1_text` | "Produtos frescos todos os dias..." | Configuração | ✅ Setting | Manter |
| `card2_title` | "Melhor preço" | Configuração | ✅ Setting | Manter |
| `card2_text` | "Trabalhamos todos os dias..." | Configuração | ✅ Setting | Manter |
| `card3_title` | "Atendimento humano" | Configuração | ✅ Setting | Manter |
| `card3_text` | "Equipas dedicadas..." | Configuração | ✅ Setting | Manter |
| `business_badge` | "Para empresas" | Configuração | ✅ Setting | Manter |
| `business_title` | "Parceiro da sua empresa" | Configuração | ✅ Setting | Manter |
| `business_text` | "Do pequeno restaurante..." | Configuração | ✅ Setting | Manter |
| `business_bullet1` | "Fornecimento B2B..." | Configuração | ✅ Setting | Manter |
| `business_bullet2` | "Entrega programada..." | Configuração | ✅ Setting | Manter |
| `business_bullet3` | "Parcerias com produtores..." | Configuração | ✅ Setting | Manter |
| `business_cta` | "Fale connosco" | Configuração | ✅ Setting | Manter |
| `business_years_value` | "4+" | Configuração | ✅ Setting | Manter |
| `business_years_text` | "Anos de parcerias<br>..." | Configuração (HTML) | ✅ Setting | Manter |
| `network_badge` | "A nossa rede" | Configuração | ✅ Setting | Manter |
| `network_title` | "Lojas Mangolê" | Configuração | ✅ Setting | Manter |
| `network_text` | "Espalhadas por todo o país..." | Configuração | ✅ Setting | Manter |
| `news_badge` | "Mangolê Notícias" | Configuração | ✅ Setting | Manter |
| `news_title` | "Fique por dentro" | Configuração | ✅ Setting | Manter |
| `news_text` | "Acompanhe as novidades..." | Configuração | ✅ Setting | Manter |
| `cta_title` | "Venha conhecer o Mangolê" | Configuração | ✅ Setting | Manter |
| `cta_text` | "Estamos à sua espera..." | Configuração | ✅ Setting | Manter |
| `cta_primary` | "Encontrar uma loja" | Configuração | ✅ Setting | Manter |
| `cta_secondary` | "Sou empresa" | Configuração | ✅ Setting | Manter |

### 2.2 Grupo `contact`

| Key | Uso na homepage | Classificação |
|-----|----------------|--------------|
| `phone` | `tel:{{ $phoneDigits }}` | ✅ Global Setting |
| `whatsapp` | `https://wa.me/{{ $whatsappDigits }}` | ✅ Global Setting |
| `email` | Não usado na homepage | ✅ Global Setting |

### 2.3 Grupo `seo`

| Key | Uso na homepage | Classificação |
|-----|----------------|--------------|
| `default_title` | `@section('title')` | ✅ Global Setting |
| `default_description` | `@section('description')` | ✅ Global Setting |

### 2.4 Grupo `company`

| Key | Uso na homepage | Classificação |
|-----|----------------|--------------|
| `name` | Não usado | ✅ Global Setting |
| `tagline` | Não usado | ✅ Global Setting |
| `legal_name` | Não usado | ✅ Global Setting |
| `nif` | Não usado | ✅ Global Setting |

---

## 3. Classificação das Settings

### 3.1 Critérios

Uma setting é **adequada** se:
- Existe apenas uma versão activa
- Não possui identidade própria
- Não possui relações com outras entidades
- Não necessita de listagem
- Não possui ciclo de vida editorial
- É apenas texto (ou texto simples com formatação mínima)

### 3.2 Veredicto

**Todas as ~45 settings do grupo `home` são adequadas como Settings.**

Nenhuma delas:
- Possui identidade própria (são textos, não entidades)
- Possui relações (estão isoladas na tabela `settings`)
- Possui ciclo de vida editorial (não têm rascunho/publicado/arquivado)
- Necessita de listagem (são consumidas apenas na homepage)
- Necessita de ser pesquisável (são chaves únicas, não entidades pesquisáveis)
- Duplica dados de entidades existentes

**Conclusão:** NÃO há Settings a migrar para entidades. O grupo `home` está correctamente classificado.

### 3.3 Não-Settings Identificados

Os seguintes dados na homepage NÃO devem ser Settings:

| Dado | Fonte actual | Deveria ser | Justificação |
|------|-------------|-------------|-------------|
| Lista de categorias | `Category::showcase()->get()` | Model | Entidade de negócio com identidade própria |
| Lista de campanhas | `Campaign::current()->take(2)->get()` | Model | Entidade editorial com ciclo de vida |
| Lista de notícias | `NewsArticle::published()->latest()->take(3)->get()` | Model | Entidade editorial com ciclo de vida |
| Lista de lojas (rede) | `Store::active()->orderBy('sort_order')->take(4)->get()` | Model | Entidade de negócio com identidade própria |
| Lista de províncias | `Province::orderBy('name')->get()` | Model | Entidade de negócio (lookup) |
| Contagem de lojas | `Store::count()` | Model | Derivado de entidade |

Todos estes já estão correctamente implementados como Models. **Nada a migrar.**

---

## 4. Entidades Utilizadas pela Homepage

| Entidade | Uso na homepage | Query | Já suporta destaque? |
|----------|----------------|-------|---------------------|
| `Category` | Secção 3 — grelha de categorias | `showcase()` scope (`show_in_hero=1`, `is_active=1`, ordered) | ✅ Já tem `show_in_hero` |
| `Campaign` | Secção 4 — campanhas activas | `current()` scope (published, within dates or pinned, ordered) | ✅ Já tem `is_pinned` + `priority` |
| `NewsArticle` | Secção 8 — notícias recentes | `published()->latest()->take(3)` | ✅ Já tem `featured` + `published` scope |
| `Store` | Secção 7 — rede de lojas | `active()->orderBy('sort_order')->take(4)` | ✅ Já tem `sort_order` + `is_active` |
| `Province` | Secção 2 — filtro de províncias | `orderBy('name')->get()` | ✅ Lookup simples |

**Nenhuma entidade precisa de novos campos.** Todas já suportam os conceitos de destaque, visibilidade e ordenação necessários.

---

## 5. Conteúdo Editorial

O conteúdo editorial na homepage é actualmente:

| Secção | Entidade | Critério | Estado |
|--------|----------|----------|--------|
| Campanhas | `Campaign` | `current()` scope | ✅ Publicadas, within dates, ou pinned |
| Notícias | `NewsArticle` | `published()->latest()` | ✅ Publicadas, ordenadas por data |
| Categorias | `Category` | `showcase()` scope | ✅ Activas, com `show_in_hero=1` |

**Problema identificado:** A homepage não permite ao editor seleccionar manualmente quais campanhas ou notícias aparecem — usa critérios puramente automáticos (data, status). Isto é aceitável para o momento actual, mas pode ser melhorado.

**Acção:** Não implementar selecção manual neste momento. A prioridade da FASE 19 é arquitectura, não funcionalidades. A selecção editorial será tratada na FASE 21 (Content Curation).

---

## 6. Dados Hardcoded Encontrados

| Local | Dados hardcoded | Justificação |
|-------|----------------|-------------|
| `welcome.blade.php:33` | "Pesquisar por localidade ou loja..." | Placeholder de input — não justifica Setting |
| `welcome.blade.php:73` | "Pesquisar" | Label de botão — não justifica Setting |
| `welcome.blade.php:80` | "Ou escolha por província:" | Label estático — não justifica Setting |
| `welcome.blade.php:103` | "Ligar" | Label de botão — não justifica Setting |
| `welcome.blade.php:106` | "WhatsApp" | Label de botão — não justifica Setting |
| `welcome.blade.php:137` | "Categorias em breve." | Mensagem de fallback — não justifica Setting |
| `welcome.blade.php:166` | "Destaque" | Badge fixo de pinned campaign — não justifica Setting |
| `welcome.blade.php:300-303` | "Aberto", "Em renovação", "Fechado" | Status labels de Store — vêm de enum, não de Setting |
| `welcome.blade.php:309` | "Hipermercado" / "Supermercado" | Labels de tipo de loja |
| `welcome.blade.php:317` | "Detalhes" | Label de link — não justifica Setting |
| `welcome.blade.php:320` | "Ligar" (segunda ocorrência) | Label de link — não justifica Setting |
| `welcome.blade.php:330` | "Ver todas as lojas" | Label de link — não justifica Setting |
| `welcome.blade.php:350` | "Ver todas" | Label de link — não justifica Setting |

**Decisão:** Manter todos como hardcoded. São labels de interface, não conteúdo editorial. Transformá-los em Settings seria over-engineering. A FASE 20 (Visual Experience) pode reconsiderar se fizer sentido.

---

## 7. Problemas Arquitecturais Identificados

### 7.1 CRÍTICO: Lógica de dados no @php da view (welcome.blade.php)

**Problema:** As 9 queries que preparam os dados da homepage estão no `@php` block da view. Isto:
- Viola a separação de responsabilidades
- Impede teste unitário da lógica de preparação de dados
- Impede reutilização das queries
- Torna a view difícil de manter

**Solução:** Criar um HomepageService ou ViewComposer que prepara os dados e os injecta na view.

**Risco:** Médio. A view continua funcional, mas a arquitectura é frágil.

### 7.2 MÉDIO: Cache removida sem alternativa

**Problema:** A FASE 18 removeu `Cache::remember` das queries de modelos porque o driver `database` causava erro de unserialização no PHP 8.5. Mas não foi implementada alternativa.

**Solução:** Reintroduzir cache com driver `file` (em vez de `database`) ou usar um wrapper que serializa/deserializa manualmente.

**Risco:** Baixo. As queries são rápidas, mas cache beneficiaria a homepage sob carga.

### 7.3 BAIXO: Settings sem cache

**Problema:** `Setting::group()` é chamado em cada request sem cache. Como a tabela `settings` tem ~50 registos, a query é rápida (<5ms), mas com 3 chamadas de grupo por request, há 3 queries SQL.

**Solução:** Adicionar cache nas settings (retornam arrays, seguros para serialização).

**Risco:** Baixo. Arrays são seguros para cache.

### 7.4 BAIXO: Rota Closure sem Controller

**Problema:** `GET /` usa Closure em vez de Controller. Isto impede middleware específico por rota, impede teste isolado da lógica e torna a rota menos visível no `route:list`.

**Solução:** Manter por agora (a arquitectura existente usa Closures em todas as rotas públicas). Reavaliar na FASE 20.

**Risco:** Baixo. É uma decisão arquitectural previamente tomada.

---

## 8. Recomendações

### 8.1 Implementar Agora (Baixo Risco)

| # | Acção | Ficheiro | Risco |
|---|-------|---------|-------|
| 1 | Criar `App\Services\HomepageService` com método `data(): array` que prepara todos os dados | `app/Services/HomepageService.php` | Baixo |
| 2 | Mover as 9 queries do `@php` block para o `HomepageService` | `welcome.blade.php` + `routes/web.php` | Baixo |
| 3 | Manter query directa a `Campaign`, `NewsArticle`, `Category`, `Store`, `Province` (sem cache por enquanto) | `HomepageService.php` | Baixo |
| 4 | Adicionar cache nas settings com `Cache::remember()` (arrays são seguros) | `HomepageService.php` | Baixo |
| 5 | Adicionar método `refresh()` no `Setting` model para invalidar cache (chamado via Observer ou hook Filament) | `app/Models/Setting.php` | Baixo |

### 8.2 Implementar na FASE 20 (Médio Risco)

| # | Acção | Risco |
|---|-------|-------|
| 1 | Reintroduzir cache nas queries de modelos com driver `file` | Médio |
| 2 | Substituir Closure route por Controller | Médio |
| 3 | Criar testes específicos para HomepageService | Médio |

### 8.3 Não Implementar (Alto Risco / Desnecessário)

| # | Acção | Motivo |
|---|-------|--------|
| 1 | Migrar Settings para entidades | Nenhuma Setting actual justifica migração |
| 2 | Criar Page Builder | Sem requisito de negócio |
| 3 | Adicionar `featured_store_ids` à tabela Settings | Stores já têm `sort_order` |
| 4 | Criar tabela `homepage_sections` | Composição fixa é suficiente |
| 5 | Duplicar `Campaign.title` em Settings | Campaign já tem título próprio |

---

## 9. Decisão sobre Composição

### Estratégia Escolhida: **ESTRATÉGIA A — Composição Fixa**

A ordem das secções é fixa no código:

```
Hero → Localizador → Categorias → Campanhas → Experiência → Negócios → Rede → Notícias → CTA Final
```

**Justificação:**
- Não existe requisito de negócio para ordem configurável
- A ordem actual é a ordem natural de navegação (topo → fundo)
- A Estratégia B (Page Builder) adicionaria complexidade sem benefício real
- A FASE 20 (Visual Experience) pode reordenar visualmente sem precisar de Page Builder

**O CMS controla apenas:**
- Textos (Settings)
- Visibilidade (via Settings, ex: `home.campaigns_enabled`)
- Conteúdo seleccionado (via entidades: `show_in_hero`, `is_pinned`, `featured`, `sort_order`)

---

## 10. Modelo Editorial Recomendado

```
CONFIGURAÇÃO (Settings)
  ↓
  ├── Textos editoriais (home.*)
  ├── SEO (seo.*)
  ├── Contacto (contact.*)
  └── Empresa (company.*)

ENTIDADES DE NEGÓCIO (Models)
  ↓
  ├── Category    → Vitrine (show_in_hero)
  ├── Campaign    → Destaques (is_pinned, status, dates)
  ├── NewsArticle → Notícias (featured, status, published_at)
  ├── Store       → Rede (is_active, sort_order)
  └── Province    → Filtro (lookup)

CONTEÚDO EDITORIAL
  ↓
  ├── Campaign (gestão editorial completa)
  └── NewsArticle (gestão editorial completa)

HOMEPAGE
  ↓
  ├── Lê Settings para textos
  ├── Lê Models para dados
  └── Composição fixa no código
```

---

## 11. Cache — Auditoria e Proposta

### 11.1 Estado Actual

| Cache Key | Driver | TTL | Status | Problema |
|-----------|--------|-----|--------|----------|
| (todas removidas) | — | — | ❌ Removidas | Erro unserialização no driver `database` |

### 11.2 Proposta

| Cache Key | Dados | Driver | TTL | Seguro? |
|-----------|-------|--------|-----|---------|
| `homepage.data` | Array (settings + model IDs) | `database` | 600s (10 min) | Sim (array) |
| `settings.home` | Array | `database` | 3600s | ✅ Sim (array puro) |
| `settings.contact` | Array | `database` | 3600s | ✅ Sim (array puro) |
| `settings.seo` | Array | `database` | 3600s | ✅ Sim (array puro) |

Settings retornam arrays através de `Setting::group()` — são **seguros para serialização** e podem ser cacheados sem risco.

### 11.3 Invalidação

Para garantir que Settings editadas no Filament invalidam o cache:

```php
// app/Observers/SettingObserver.php
public function saved(Setting $setting): void
{
    Cache::forget('settings.' . $setting->group);
}
```

E registar no `AppServiceProvider::boot()`:

```php
\App\Models\Setting::observe(\App\Observers\SettingObserver::class);
```

---

## 12. Alterações a Implementar

### 12.1 HomepageService (NOVO)

Criar `app/Services/HomepageService.php` com método `build()` que:
1. Lê Settings (com cache)
2. Lê dados dos modelos (sem cache até FASE 20)
3. Retorna array com todos os dados da homepage

### 12.2 routes/web.php (ALTERAR)

Substituir:
```php
Route::get('/', function () { return view('welcome'); });
```
Por:
```php
Route::get('/', function () {
    $data = app(\App\Services\HomepageService::class)->build();
    return view('welcome', $data);
});
```

### 12.3 welcome.blade.php (ALTERAR)

Substituir o `@php` block de 18 linhas por variáveis injectadas pelo Service.

### 12.4 SettingObserver (NOVO)

Criar `app/Observers/SettingObserver.php` que invalida cache na save/delete.

### 12.5 Setting Model (ALTERAR)

Adicionar método `static::refresh(string $group): void` para invalidar cache.

### 12.6 Cache de Settings (REINTRODUZIR)

Adicionar `Cache::remember('settings.' . $group, 3600, ...)` no `HomepageService`.

---

## 13. Alterações Que NÃO Serão Feitas

| Alteração | Motivo |
|-----------|--------|
| Migrar Settings para entidades | Nenhuma Setting actual justifica migração |
| Adicionar `featured_store_ids` em Settings | Stores já têm `sort_order` |
| Criar tabela `homepage_sections` | Composição fixa suficiente |
| Criar Page Builder | Sem requisito de negócio |
| Remover Settings do grupo `home` | Todas são legítimas como configuração de apresentação |
| Adicionar cache nas queries de modelos | Driver `database` não é seguro para serialização de Collections. Reavaliar na FASE 20 com driver `file` |
| Substituir Closure route por Controller | Decisão arquitectural pré-existente. Reavaliar na FASE 20 |
| Adicionar selecção manual de campanhas/notícias na homepage | FASE 21 (Content Curation) |
| Mover labels de interface (Pesquisar, Ligar, etc.) para Settings | São UI, não conteúdo editorial |

---

## 14. Resumo de Risco

| Risco | Alterações |
|-------|-----------|
| 🟢 Baixo | HomepageService, cache de settings, SettingObserver |
| 🟡 Médio | Mover lógica do @php para o Service, alterar route |
| 🔴 Alto | Nenhuma |

---

## 15. Veredicto

**A homepage actual tem uma arquitectura de conteúdo sólida.**

- As Settings do grupo `home` estão correctamente classificadas como configuração de apresentação
- As entidades de negócio (Category, Campaign, NewsArticle, Store, Province) são usadas directamente sem duplicação em Settings
- O conteúdo editorial (Campaign, NewsArticle) mantém o seu ciclo de vida editorial independente
- Não há dados de entidades duplicados em Settings
- Não há Settings que devam ser entidades
- O único problema arquitectural real é a lógica de dados no `@php` da view

**A implementação prioritária é mover a lógica de preparação de dados para um HomepageService.**

---

*Documento criado para a FASE 19 — Homepage Editorial Architecture & Content Control*
