# Domain Architecture — Mangolê

> **Data:** 2026-07-21 (actualizado FASE 15/16)
> **Base:** Auditoria real da base de dados `mangole` (23 tabelas, 9 com dados)
> **Propósito:** Definir domínios, entidades, responsabilidades e relações antes de criar código.

---

## ⚠️ Actualizações pós FASE 15/16

### Novos domínios
| # | Domínio | Entidade Principal | Tabela | Dados |
|---|---------|-------------------|--------|-------|
| K | Oportunidades | Opportunity | `opportunities` | Seed data |

### Alterações em domínios existentes

| Domínio | Alteração |
|---------|-----------|
| E (Leads) | `StoreMessage` → `BusinessInquiry`; adicionado `province_id`, `business_type`, novos status |
| B (Lojas) | `province_code` removido; agora usa FK `province_id` |
| C (Produtos) | `product_families` → `categories`; `category` (string) → `category_id` (FK) |
| D (Campanhas) | Adicionado `image`, campos SEO |
| F (Notícias) | Adicionados campos SEO |
| G (Parceiros) | Adicionado `slug`, campos SEO |
| H (Vagas) | Adicionado `slug`, campos SEO |

### Decisões arquitecturais implementadas
- Enums PHP tipados (`ArticleStatus`, `CampaignStatus`, `InquiryStatus`, etc.) com método `label()`
- Foundation migration com `Schema::hasTable()` para compatibilidade legada
- Factories para todos os modelos
- RefreshDatabase + SQLite `:memory:` para testes
- Cache `Cache::remember()` nas queries públicas
- Rate limiting (`throttle:form`) no formulário B2B

---

## Sumário dos Domínios

| # | Domínio | Entidade Principal | Tabela Existente | Dados |
|---|---------|-------------------|------------------|-------|
| A | Identidade e Configuração | Setting | `settings` | 6 registos |
| B | Rede de Lojas | Store | `stores` | 9 registos |
| C | Produtos e Categorias | Product / ProductFamily | `products`, `product_families` | 5 + 6 registos |
| D | Campanhas e Promoções | Campaign | `campaigns` + asset/cta | Vazia |
| E | Clientes / Leads Empresariais | StoreMessage | `store_messages` | 2 registos |
| F | Conteúdo Editorial | NewsArticle | `news_articles` | Vazia |
| G | Parceiros | Partner | `partners` | Vazia |
| H | Carreiras | Vacancy | `vacancies` | Vazia |
| I | SEO | SeoMetadata | `seo_metadata` | Vazia |
| J | Administração | User | `users` | 2 registos |

---

## A. Identidade e Configuração

### Entidades
- **Setting** — par chave-valor agrupado por contexto

### Responsabilidades
- Armazenar configurações dinâmicas do portal (títulos, subtítulos, textos institucionais)
- Permitir edição via Filament sem deploy
- Servir dados para o frontend (hero da homepage, secção "Sobre")

### Relações
- Nenhuma (tabela independente)

### Estados
- Activo por existência (não há soft delete nem status)

### Dados essenciais (existentes na BD)
| Grupo | Chave | Valor | Uso |
|-------|-------|-------|-----|
| `home` | `hero_title` | "Mangolê — O Supermercado da Família Angolana" | Hero da homepage |
| `home` | `hero_subtitle` | "Preços baixos todos os dias..." | Subtítulo do hero |
| `home` | `hero_cta_label` | "Ver Lojas" | Texto do botão CTA |
| `about` | `hero_title` | "Mangolê — O Supermercado da Família Angolana" | Página sobre |
| `about` | `hero_subtitle` | "Há mais de 10 anos..." | Subtítulo sobre |
| `about` | `quem_somos_title` | "Compromisso com Angola" | Título secção |

### Dados opcionais (a criar)
- `home.hero_cta_url` — URL do botão CTA
- `contact.email`, `contact.phone` — Informações de contacto globais
- `social.facebook`, `social.instagram` — Redes sociais
- `business.hero_title` — Título da página B2B

### Filament
- Resource `SettingResource` com agrupamento por `group`
- Interface tipo "accordion" por grupo

### Frontend
- Consumido pelo `HomepageService` e `PageService`
- Injetado nas views como variáveis tipadas

---

## B. Rede de Lojas

### Entidades
- **Store** — loja física da rede Mangolê

### Responsabilidades
- Representar cada loja da rede (hipermercados e formato mix)
- Fornecer dados de localização, contacto e serviços
- Suportar geolocalização (latitude/longitude) para mapas e pesquisa por proximidade
- Servir como referência para mensagens de contacto

### Relações
- `Store` ← `StoreMessage` (1:N, FK opcional via `store_id`)
- `Store` → `province_code` (string 3 caracteres — sem FK, a normalizar)

### Estados
| Campo | Valores | Descrição |
|-------|---------|-----------|
| `is_active` | 0/1 | Loja visível no portal |
| `status` | "open" / "closed" / "renovation" | Status operacional |
| `deleted_at` | timestamp ou NULL | Soft delete |

### Dados essenciais (existentes na BD)
- 9 lojas: 8 hiper (Luanda) + 1 mix (Bengo)
- Cada loja com: nome, slug, tipo, localização, contactos, geolocalização

### Dados opcionais (a melhorar)
- `services` — JSON com lista de serviços (talho, padaria, caixa rápido, etc.)
- `opening_hours` — JSON com horários por dia da semana
- `photo` — URL da foto da loja

### Normalização necessária
- `province_code` (varchar 3) → idealmente FK para tabela `provinces`
- `municipality` (string) → idealmente FK para tabela `municipalities`

### Filament
- `StoreResource` com tabs: Informação Básica, Localização, Contactos, Serviços, Horários
- Mapa integrado no formulário de edição
- Filtros por província, status, tipo

### Frontend
- `/lojas` — listagem com filtro por província e mapa
- `/lojas/{slug}` — página individual com info, mapa, horários, formulário de contacto

---

## C. Produtos e Categorias

### Entidades
- **ProductFamily** — família/categoria de produtos (ex: Açúcar, Arroz, Óleo de Palma)
- **Product** — produto individual com preço e descrição

### Responsabilidades
- Organizar o catálogo de produtos por famílias
- Apresentar produtos no frontend com preço, badge promocional e imagem
- Suportar exibição em grelha na homepage e página de produtos

### Relações
- `ProductFamily` → `Product` (1:N — FK a criar: `products.product_family_id`)
- **Problema actual:** `products.category` é string livre, sem FK

### Estados
| Entidade | Campo | Valores | Descrição |
|----------|-------|---------|-----------|
| ProductFamily | `is_active` | 0/1 | Família visível |
| ProductFamily | `show_in_hero` | 0/1 | Exibir na vitrine da homepage |
| Product | `is_active` | 0/1 | Produto visível |
| Product | `featured` | 0/1 | Produto em destaque |
| Product | `deleted_at` | timestamp/NULL | Soft delete |

### Dados essenciais (existentes na BD)
- 6 famílias: Açúcar, Arroz, Farinhas, Grãos, Milho, Óleo de Palma
- 5 produtos: todos da categoria "Azeite" (marca "Tempero Boa Mesa")
- Produtos com preço, preço original (promocional), badge, imagem

### Dados opcionais
- `description` — descrição detalhada do produto
- `badge_color` — cor personalizada do badge promocional

### Normalização necessária
- **CRÍTICA:** Adicionar FK `product_family_id` em `products`
- Criar migration para popular a FK baseada em `category` → `product_families.name`
- Tornar `category` obsoleto após migração

### Filament
- `ProductFamilyResource` — CRUD com imagem e ordenação
- `ProductResource` — CRUD com preço, badge, imagem, selecção de família
- Relação aninhada products dentro de cada família

### Frontend
- Secção "Universo de Produtos" na homepage
- Página `/produtos` com grelha filtrada por família
- Destaques na homepage (produtos com `featured = 1`)

---

## D. Campanhas e Promoções

### Entidades
- **Campaign** — campanha promocional com período, prioridade e SEO próprio
- **CampaignAsset** — asset multimédia (imagem) por campanha e propósito
- **CampaignCta** — call-to-action por campanha

### Responsabilidades
- Gerir campanhas temporárias com início e fim
- Associar múltiplos assets (banners, hero images) com propósitos distintos
- Associar múltiplos CTAs (links, botões) ordenáveis
- Controlar visibilidade por prioridade e pinagem

### Relações
- `Campaign` → `CampaignAsset` (1:N)
- `Campaign` → `CampaignCta` (1:N)

### Estados
| Campo | Valores | Descrição |
|-------|---------|-----------|
| `status` | "draft" / "published" / "archived" | Estado da campanha |
| `is_pinned` | 0/1 | Fixa no topo independentemente da data |
| `priority` | 0-999 | Ordem de exibição |
| `starts_at` | timestamp | Início da validade |
| `ends_at` | timestamp | Fim da validade |

### Dados existentes
- **Nenhum** — tabelas vazias, schema pronto

### Dados essenciais a criar
- Nome, slug, objectivo, período, status
- Assets por propósito (hero, banner, thumbnail) com dimensões e alt text
- CTAs com label e destino

### Observações
- `campaign_assets` tem unique constraint em `(campaign_id, purpose, sort_order)`
- `campaign_ctas` tem unique constraint em `(campaign_id, sort_order)`

### Filament
- `CampaignResource` com relação aninhada Assets e CTAs (repeater)
- Dashboard widget para campanhas activas

### Frontend
- Hero dinâmico na homepage conforme campanha activa
- Banner rotativo com assets da campanha
- Quando sem campanhas activas: fallback para conteúdo estático das settings

---

## E. Clientes / Leads Empresariais

### Entidades
- **StoreMessage** — mensagem de contacto de um visitante (potencial lead)

### Responsabilidades
- Receber pedidos de informação de clientes
- Associar a loja específica quando aplicável
- Servir como sistema simples de leads

### Relações
- `StoreMessage` → `Store` (N:1, FK opcional via `store_id`)

### Estados
| Campo | Valores | Descrição |
|-------|---------|-----------|
| `status` | "pending" / "read" / "replied" / "archived" | Estado do lead |

### Dados existentes
- 2 registos de teste: `teste@example.com` e `maria@teste.com`
- Ambos com `store_id = NULL` — leads genéricos sem loja específica
- Ambos com `status = pending`

### Dados essenciais
- Nome, email, mensagem
- Telefone (opcional)
- Loja associada (opcional)

### Dados opcionais a adicionar
- `phone` — já existe na tabela
- `company` — nome da empresa (para B2B)
- `subject` — assunto da mensagem (para categorizar)

### Filament
- `StoreMessageResource` — apenas leitura e gestão de status
- Widget com últimas mensagens pendentes no dashboard
- Acções: marcar como lido, responder, arquivar
- Export CSV

### Frontend
- Formulário `/contacto` que cria StoreMessage
- Formulário específico por loja em `/lojas/{slug}/contactar`
- Formulário B2B em `/para-o-seu-negocio` (pode usar a mesma entidade ou nova)

---

## F. Conteúdo Editorial

### Entidades
- **NewsArticle** — artigo de notícias ou conteúdo editorial

### Responsabilidades
- Publicar notícias, comunicados, artigos institucionais
- Suportar categorização, destaque e agendamento de publicação
- Exibir no blog/notícias do portal

### Relações
- Nenhuma (tabela independente)

### Estados
| Campo | Valores | Descrição |
|-------|---------|-----------|
| `status` | "draft" / "published" / "archived" | Estado da publicação |
| `featured` | 0/1 | Artigo em destaque |
| `published_at` | timestamp | Data de publicação |
| `category` | string | Categoria editorial |

### Dados existentes
- **Nenhum** — tabela vazia

### Dados essenciais a criar
- Título, slug, conteúdo, categoria, status
- Excerpt (resumo), imagem destacada, autor
- `published_at` para agendamento
- `reading_time` em minutos

### Filament
- `NewsArticleResource` com editor rich text (Tiptap do Filament)
- Slug automático a partir do título
- Agendamento de publicação
- Filtros por status, categoria, destaque

### Frontend
- `/noticias` — listagem com paginação
- `/noticias/{slug}` — artigo completo com breadcrumbs
- Secção "Conteúdo Editorial" na homepage (artigos recentes)

---

## G. Parceiros

### Entidades
- **Partner** — parceiro institucional ou comercial

### Responsabilidades
- Gerir logotipos e informações de parceiros
- Exibir na página de parceiros com ordenação e níveis

### Relações
- Nenhuma (tabela independente)

### Estados
| Campo | Valores | Descrição |
|-------|---------|-----------|
| `is_active` | 0/1 | Visível no portal |
| `level` | "standard" / "gold" / "platinum" | Nível do parceiro |

### Dados existentes
- **Nenhum** — tabela vazia

### Dados essenciais a criar
- Nome, logo, nível, segmento
- Link (href), desde (ano), ordem de exibição

### Filament
- `PartnerResource` com upload de logo
- Ordenação drag-and-drop

### Frontend
- `/parceiros` — grelha com logos por nível

---

## H. Carreiras

### Entidades
- **Vacancy** — vaga de emprego

### Responsabilidades
- Publicar vagas abertas na Mangolê
- Permitir candidatura ou redirecionamento externo

### Relações
- Nenhuma (tabela independente)

### Estados
| Campo | Valores | Descrição |
|-------|---------|-----------|
| `status` | "draft" / "published" / "closed" | Estado da vaga |
| `type` | "full-time" / "part-time" / "internship" | Tipo |
| `remote` | 0/1 | Permite trabalho remoto |
| `deadline` | date | Data limite |

### Dados existentes
- **Nenhum** — tabela vazia

### Dados essenciais a criar
- Título, área, localização, tipo, descrição
- Link externo para candidatura (href)
- Deadline e indicador remote

### Filament
- `VacancyResource` com status workflow
- Filtros por área, tipo, status

### Frontend
- `/carreiras` — listagem com filtros
- `/carreiras/{slug}` — página individual da vaga
- Botão de candidatura (link externo ou formulário)

---

## I. SEO

### Entidades
- **SeoMetadata** — metadados SEO por página estática

### Responsabilidades
- Gerir title, description, Open Graph, Twitter Cards por página
- Servir meta tags dinâmicas no frontend

### Relações
- Nenhuma (tabela independente, chave `page_name`)

### Estados
- Activo por existência

### Dados existentes
- **Nenhum** — tabela vazia

### Dados essenciais a criar
- `page_name` — identificador único (ex: "home", "about", "stores")
- Title, description, keywords
- OG title, OG description, OG image
- Twitter title, Twitter description
- Canonical URL, robots

### Observação
- `campaigns` tem campos SEO embutidos — decisão necessária: usar SeoMetadata polimórfica ou manter campos na tabela campaigns

### Filament
- `SeoMetadataResource` — formulário completo SEO
- Preview de como aparece no Google e redes sociais

### Frontend
- Meta tags dinâmicas injectadas no `<head>` do layout
- Serviço `SeoService` que busca metadata por `page_name`

---

## J. Administração

### Entidades
- **User** — administrador do sistema

### Responsabilidades
- Autenticação no painel Filament
- Gestão de conteúdo do portal
- Dois users existentes: admin (admin@mangole.ao) e marketing (marketing@mangole.ao)

### Relações
- Nenhuma com tabelas de negócio

### Estados
- `email_verified_at` — verificação de email

### Dados existentes
- 2 users com email verificado

### Filament
- Login com guard Filament
- Configuração de cores do painel (actual: Amber → mudar para azul Mangolê)
- Dashboard com widgets de métricas

### Observação
- Neste momento o User model não tem `Filament\Panel` trait nem `CanResetPassword` para Filament
- Será necessário adicionar na FASE 2:
  - `use Filament\Models\Contracts\FilamentUser;`
  - `use Filament\Panel;`
  - Método `canAccessPanel(Panel $panel): bool`

---

## Mapa de Relações Entre Domínios

```
┌────────────────────────────────────────────────────────┐
│                   ADMINISTRAÇÃO (J)                     │
│                    User (Filament)                      │
└──────────┬─────────────────────────────────────────────┘
           │ gere
           ▼
┌─────────────────────────────────────────────────────────┐
│                   IDENTIDADE (A)                        │
│  Setting ─────────── fornece dados para ──► Frontend   │
└─────────────────────────────────────────────────────────┘
           │
           │
 ┌─────────┴──────────┬──────────────┬──────────────────┐
 ▼                    ▼              ▼                  ▼
┌──────────┐    ┌───────────┐  ┌──────────┐    ┌────────────┐
│ LOJAS (B)│    │PRODUTOS(C)│  │CAMPANHAS │    │ CLIENTES   │
│          │    │           │  │  (D)     │    │   (E)      │
│ Store    │    │ProductFam │  │Campaign  │    │StoreMessage│
│  ┌───────┤    │  ┌──────┐ │  │  ┌──────┐│    │   ┌───────┤
│  │Store  │    │  │Prod. │ │  │  │Asset ││    │   │Store  │
│  │Message│    │  └──────┘ │  │  ├──────┤│    │   │(opc.) │
│  └───────┤    └───────────┘  │  │ CTA  ││    └───────────┘
└──────────┘                   │  └──────┘│
                               └──────────┘

 ┌──────────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐
 │ CONTEÚDO (F) │  │PARCEIROS │  │CARREIRAS │  │  SEO (I) │
 │              │  │   (G)    │  │   (H)    │  │          │
 │ NewsArticle  │  │ Partner  │  │ Vacancy  │  │SeoMeta   │
 └──────────────┘  └──────────┘  └──────────┘  └──────────┘
```

---

## Decisões Arquitecturais para a FASE 2

### Pendentes de aprovação

| Decisão | Opções | Recomendação |
|---------|--------|-------------|
| FK `products → product_families` | Sim / Não | **Sim** — adicionar `product_family_id` |
| Tabelas `provinces`/`municipalities` | Sim / Manter em string | **Sim** — normalizar geolocalização |
| Soft delete uniforme | Todas as tabelas / Apenas actuais | **Todas as tabelas** para consistência |
| ENUM vs VARCHAR + validação | ENUM nativo / String + Rule | **String + validação Laravel** para portabilidade |
| SEO em campaigns | Campos embutidos / SeoMetadata polimórfica | **Manter embutido** em campaigns (já implementado) |
| Leads B2B | StoreMessage / Nova entidade B2BLead | **StoreMessage** com campo `subject` adicional |
| `homepage_blocks` | Manter / Substituir por secções fixas | **Substituir** por secções fixas da welcome.blade.php |

### Confirmadas
- Rota `/` → `welcome.blade.php` via `HomepageController`
- Painel admin `/admin` com Filament
- Paleta azul (estrutural) / amarelo (CTA) / vermelho (promoção) / branco (fundo)
- Arquitectura Controller → Service → Model → View

---

## Checklist de Preparação para FASE 2

- [ ] Decisão sobre FK `products → product_families` aprovada
- [ ] Decisão sobre normalização geográfica aprovada
- [ ] Decisão sobre soft delete aprovada
- [ ] Decisão sobre tipo de enum aprovada
- [ ] Decisão sobre leads B2B aprovada
- [ ] Decisão sobre `homepage_blocks` aprovada
- [ ] Backup da BD executado

> **Nota:** Nenhum código será escrito até que todas as decisões acima estejam aprovadas.
