# Módulo: Convênios (GAP)

## 1. O que é o módulo

Gerencia os convênios que a SUCESU oferece aos seus associados. Possui hierarquia de 3 níveis no portal:

1. **Categorias** (`/convenios`) — grade de 9 categorias: Comércio, Educação, Saúde, Gastronomia, Serviços, Turismo, Lazer, Bem Estar, Fitness. Cada uma com ícone, descrição e link "Saiba Mais".
2. **Instituições por categoria** (`/convenios-detalhes?categoria={slug}`) — listagem de instituições dentro de uma categoria, com logo, nome, descrição, badge de desconto e link para detalhe.
3. **Detalhe da instituição** (`/convenio?categoria={slug}&instituicao={slug}`) — página de detalhe com logo, nome, badge de desconto, descrição do convênio, lista de cursos/produtos contemplados, site, galeria de imagens, e link de volta.

No CMS, deve permitir CRUD de categorias, instituições, upload de logos e imagens de galeria, e configuração dos descontos.

## 2. Estrutura de dados

### Categoria de Convênio (`convenio_categoria`)
- `id` (UUID, PK)
- `name` (string, 255) — ex: "Educação"
- `slug` (string, 255, unique)
- `description` (text)
- `icon` (string, 255) — caminho do ícone SVG/PNG
- `sort_order` (integer, default 0)
- `is_active` (boolean, default true)
- `created_at`, `updated_at`, `deleted_at`

### Instituição Conveniada (`convenio_instituicao`)
- `id` (UUID, PK)
- `categoria_id` (uuid, FK → `v2_convenio_categorias.id`)
- `name` (string, 255) — ex: "UNIPE"
- `slug` (string, 255, unique)
- `description` (text) — descrição da instituição
- `convenio_description` (text) — descrição específica do convênio/desconto
- `logo` (string, 255)
- `url` (string, 255, nullable) — site da instituição
- `discount_badge` (string, 255, nullable) — texto do badge de desconto (ex: "Desconto de até 55,76%")
- `benefits_list` (text, nullable) — lista de cursos/produtos contemplados (separados por quebra de linha)
- `sort_order` (integer, default 0)
- `is_active` (boolean, default true)
- `created_at`, `updated_at`, `deleted_at`

### Galeria de Imagens (`convenio_instituicao_gallery`)
- `id` (UUID, PK)
- `instituicao_id` (uuid, FK → `v2_convenio_instituicoes.id`)
- `image` (string, 255) — caminho da imagem
- `sort_order` (integer, default 0)
- `created_at`, `updated_at`

## 3. Dependências

- **Módulos:** Nenhum específico
- **Serviços:** `ActivityLoggerService`
- **Pacotes:** Nenhum adicional
- **Storage:** `public/upload/convenios/categorias/` para ícones, `public/upload/convenios/instituicoes/` para logos, `public/upload/convenios/gallery/` para galeria

## 4. Tabelas necessárias

### `v2_convenio_categorias`

| Coluna | Tipo | Restrições |
|---|---|---|
| id | uuid | PK |
| name | varchar(255) | NOT NULL |
| slug | varchar(255) | UNIQUE |
| description | text | NULL |
| icon | varchar(255) | NULL |
| sort_order | integer | DEFAULT 0 |
| is_active | boolean | DEFAULT true |
| created_at | timestamp | NULL |
| updated_at | timestamp | NULL |
| deleted_at | timestamp | NULL |

### `v2_convenio_instituicoes`

| Coluna | Tipo | Restrições |
|---|---|---|
| id | uuid | PK |
| categoria_id | uuid | FK → v2_convenio_categorias, NOT NULL |
| name | varchar(255) | NOT NULL |
| slug | varchar(255) | UNIQUE |
| description | text | NULL |
| convenio_description | text | NULL |
| logo | varchar(255) | NULL |
| url | varchar(255) | NULL |
| discount_badge | varchar(255) | NULL |
| benefits_list | text | NULL |
| sort_order | integer | DEFAULT 0 |
| is_active | boolean | DEFAULT true |
| created_at | timestamp | NULL |
| updated_at | timestamp | NULL |
| deleted_at | timestamp | NULL |

### `v2_convenio_instituicao_gallery`

| Coluna | Tipo | Restrições |
|---|---|---|
| id | uuid | PK |
| instituicao_id | uuid | FK → v2_convenio_instituicoes, NOT NULL |
| image | varchar(255) | NOT NULL |
| sort_order | integer | DEFAULT 0 |
| created_at | timestamp | NULL |
| updated_at | timestamp | NULL |

## 5. Arquivos necessários

```
V2/Modules/Convenios/Convenios/
├── Controllers/
│   ├── CategoriaCmsController.php          # CRUD de categorias (CMS)
│   ├── InstituicaoCmsController.php        # CRUD de instituições (CMS)
│   ├── ConveniosPortalController.php       # Páginas públicas
│   └── ConvenioPortalController.php        # Detalhe da instituição
├── Models/
│   ├── ConvenioCategoria.php
│   ├── ConvenioInstituicao.php
│   └── ConvenioInstituicaoGallery.php
├── Requests/
│   ├── CategoriaStoreRequest.php
│   ├── CategoriaUpdateRequest.php
│   ├── InstituicaoStoreRequest.php
│   └── InstituicaoUpdateRequest.php
├── Views/
│   ├── cms/
│   │   ├── categorias/
│   │   │   ├── index.blade.php
│   │   │   ├── create.blade.php
│   │   │   ├── edit.blade.php
│   │   │   └── _form.blade.php
│   │   └── instituicoes/
│   │       ├── index.blade.php
│   │       ├── create.blade.php
│   │       ├── edit.blade.php
│   │       ├── _form.blade.php
│   │       └── gallery.blade.php           # Gerenciamento de galeria
│   └── portal/
│       ├── categorias.blade.php             # /convenios (grade de categorias)
│       ├── instituicoes.blade.php           # /convenios-detalhes (instituições)
│       └── instituicao.blade.php            # /convenio (detalhe)
├── Migrations/
│   ├── 2024_01_01_000003_create_v2_convenio_categorias_table.php
│   ├── 2024_01_01_000004_create_v2_convenio_instituicoes_table.php
│   └── 2024_01_01_000005_create_v2_convenio_instituicao_gallery_table.php
├── Routes/
│   ├── cms.php
│   └── web.php
└── ModuleProvider.php
```

**Total estimado: 25 arquivos**

## 6. Sugestão de implementação

### Models e relacionamentos

```php
class ConvenioCategoria extends Model {
    public function instituicoes() {
        return $this->hasMany(ConvenioInstituicao::class, 'categoria_id');
    }
}

class ConvenioInstituicao extends Model {
    public function categoria() {
        return $this->belongsTo(ConvenioCategoria::class, 'categoria_id');
    }
    public function gallery() {
        return $this->hasMany(ConvenioInstituicaoGallery::class, 'instituicao_id');
    }
}
```

### ModuleProvider

```php
return [
    'title'          => 'Convênios',
    'description'    => 'Gerenciamento de categorias e instituições conveniadas.',
    'icon'           => 'bi bi-award',
    'position'       => 5,
    'active_pattern' => ['cms/convenios*'],
    'route'          => 'v2.convenios.cms.categorias.index',
    'subModulesLevel1' => [
        [
            'title'    => 'Categorias',
            'icon'     => 'bi bi-tags',
            'route'    => 'v2.convenios.cms.categorias.index',
            'position' => 1,
        ],
        [
            'title'    => 'Instituições',
            'icon'     => 'bi bi-building',
            'route'    => 'v2.convenios.cms.instituicoes.index',
            'position' => 2,
        ],
    ],
];
```

### Rotas públicas (web.php)

```php
Route::get('/convenios', 'ConveniosPortalController@categorias')->name('v2.convenios.portal.categorias');
Route::get('/convenios-detalhes', 'ConveniosPortalController@instituicoes')->name('v2.convenios.portal.instituicoes');
Route::get('/convenio', 'ConvenioPortalController@show')->name('v2.convenios.portal.show');
```

### Rotas CMS (cms.php)

Prefix `cms/convenios` com middleware `CmsAuthenticate`. Sub-rotas para `categorias/` e `instituicoes/` com CRUD completo.

### Implementação enxuta (sem Service Layer)

- Os controllers do portal fazem query direta: `Categoria::where('is_active', true)->orderBy('sort_order')->get()` para a página de categorias
- Instituições por categoria: `Instituicao::where('categoria_id', $id)->where('is_active', true)->orderBy('sort_order')->paginate(12)`
- Detalhe: `Instituicao::with('gallery')->where('slug', $slug)->firstOrFail()`
- Galeria gerenciada via upload múltiplo no form de edição da instituição
- Migration safe-rollback: `Schema::rename('v2_convenio_categorias', 'backup_v2_convenio_categorias')` (e análogo para as demais)
- Categorias iniciais seedadas no DB seeder (Comércio, Educação, Saúde, Gastronomia, Serviços, Turismo, Lazer, Bem Estar, Fitness)

## 7. Detalhamento de Implementação V2

### a) ModuleProvider.php

```php
<?php

namespace V2\Modules\Convenios\Convenios;

use V2\Core\Contracts\ModuleProviderInterface;

class ModuleProvider implements ModuleProviderInterface
{
    public function __construct()
    {
    }

    public function getConfig(): array
    {
        return [
            'title'          => 'Convênios',
            'description'    => 'Gerenciamento de categorias e instituições conveniadas.',
            'icon'           => 'bi bi-award',
            'position'       => 5,
            'active_pattern' => ['cms/convenios*'],
            'route'          => 'v2.convenios.cms.categorias.index',
            'subModulesLevel1' => [
                [
                    'title'    => 'Categorias',
                    'icon'     => 'bi bi-tags',
                    'route'    => 'v2.convenios.cms.categorias.index',
                    'position' => 1,
                ],
                [
                    'title'    => 'Instituições',
                    'icon'     => 'bi bi-building',
                    'route'    => 'v2.convenios.cms.instituicoes.index',
                    'position' => 2,
                ],
            ],
        ];
    }
}
```

### b) Migrations (safe-rollback)

**Migration 1 — Categorias:**

```php
<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;

class CreateV2ConvenioCategoriasTable extends Migration
{
    public function up()
    {
        DB::beginTransaction();
        DB::statement("SET lock_timeout = '3s';");

        try {
            Schema::create('v2_convenio_categorias', function (Blueprint $table) {
                $table->uuid('id')->primary();
                $table->string('name', 255);
                $table->string('slug', 255)->unique();
                $table->text('description')->nullable();
                $table->string('icon', 255)->nullable();
                $table->integer('sort_order')->default(0);
                $table->boolean('is_active')->default(true);
                $table->string('created_by', 36)->nullable();
                $table->timestamps();
                $table->softDeletes();
                $table->index('is_active', 'idx_v2_conv_cat_active');
                $table->index('sort_order', 'idx_v2_conv_cat_sort');
            });

            DB::commit();
        } catch (\Exception $e) {
            DB::rollBack();
            throw $e;
        }
    }

    public function down()
    {
        DB::beginTransaction();
        try {
            $backupTable = 'backup_v2_convenio_categorias_' . date('Ymd_His');
            Schema::rename('v2_convenio_categorias', $backupTable);
            DB::commit();
        } catch (\Exception $e) {
            DB::rollBack();
            throw $e;
        }
    }
}
```

**Migration 2 — Instituições:**

```php
<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;

class CreateV2ConvenioInstituicoesTable extends Migration
{
    public function up()
    {
        DB::beginTransaction();
        DB::statement("SET lock_timeout = '3s';");

        try {
            Schema::create('v2_convenio_instituicoes', function (Blueprint $table) {
                $table->uuid('id')->primary();
                $table->uuid('categoria_id');
                $table->string('name', 255);
                $table->string('slug', 255)->unique();
                $table->text('description')->nullable();
                $table->text('convenio_description')->nullable();
                $table->string('logo', 255)->nullable();
                $table->string('url', 255)->nullable();
                $table->string('discount_badge', 255)->nullable();
                $table->text('benefits_list')->nullable();
                $table->integer('sort_order')->default(0);
                $table->boolean('is_active')->default(true);
                $table->string('created_by', 36)->nullable();
                $table->timestamps();
                $table->softDeletes();

                $table->foreign('categoria_id')
                      ->references('id')->on('v2_convenio_categorias')
                      ->onDelete('cascade');

                $table->index('categoria_id', 'idx_v2_conv_inst_categoria');
                $table->index('is_active', 'idx_v2_conv_inst_active');
                $table->index('sort_order', 'idx_v2_conv_inst_sort');
            });

            DB::commit();
        } catch (\Exception $e) {
            DB::rollBack();
            throw $e;
        }
    }

    public function down()
    {
        DB::beginTransaction();
        try {
            $backupTable = 'backup_v2_convenio_instituicoes_' . date('Ymd_His');
            Schema::rename('v2_convenio_instituicoes', $backupTable);
            DB::commit();
        } catch (\Exception $e) {
            DB::rollBack();
            throw $e;
        }
    }
}
```

**Migration 3 — Galeria:**

```php
<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;

class CreateV2ConvenioInstituicaoGalleryTable extends Migration
{
    public function up()
    {
        DB::beginTransaction();
        DB::statement("SET lock_timeout = '3s';");

        try {
            Schema::create('v2_convenio_instituicao_gallery', function (Blueprint $table) {
                $table->uuid('id')->primary();
                $table->uuid('instituicao_id');
                $table->string('image', 255);
                $table->integer('sort_order')->default(0);
                $table->timestamps();

                $table->foreign('instituicao_id')
                      ->references('id')->on('v2_convenio_instituicoes')
                      ->onDelete('cascade');

                $table->index('instituicao_id', 'idx_v2_conv_gal_instituicao');
            });

            DB::commit();
        } catch (\Exception $e) {
            DB::rollBack();
            throw $e;
        }
    }

    public function down()
    {
        DB::beginTransaction();
        try {
            $backupTable = 'backup_v2_convenio_instituicao_gallery_' . date('Ymd_His');
            Schema::rename('v2_convenio_instituicao_gallery', $backupTable);
            DB::commit();
        } catch (\Exception $e) {
            DB::rollBack();
            throw $e;
        }
    }
}
```

### c) Models

**ConvenioCategoria.php:**

```php
<?php

namespace V2\Modules\Convenios\Convenios\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class ConvenioCategoria extends Model
{
    use SoftDeletes;

    protected $table = 'v2_convenio_categorias';

    public $incrementing = false;

    protected $keyType = 'string';

    protected $fillable = [
        'name', 'slug', 'description', 'icon',
        'sort_order', 'is_active',
    ];

    protected $casts = [
        'is_active' => 'boolean',
        'metadata'  => 'array',
    ];

    protected $dates = ['created_at', 'updated_at', 'deleted_at'];

    protected static function boot()
    {
        parent::boot();

        static::creating(function ($model) {
            if (empty($model->id)) {
                $model->id = v2_uuid();
            }
            if (empty($model->slug) && !empty($model->name)) {
                $model->slug = str_slug($model->name);
            }
        });

        static::saving(function ($model) {
            // Garantir unicidade do slug
            $baseSlug = $model->slug ?: str_slug($model->name);
            $slug = $baseSlug;
            $counter = 1;
            if (!empty($model->id)) {
                while (static::where('slug', $slug)->where('id', '!=', $model->id)->exists()) {
                    $slug = $baseSlug . '-' . $counter;
                    $counter++;
                }
            } else {
                while (static::where('slug', $slug)->exists()) {
                    $slug = $baseSlug . '-' . $counter;
                    $counter++;
                }
            }
            $model->slug = $slug;
        });

        static::saved(function ($model) {
            try {
                app(\V2\Services\Search\Services\PublicIndexerService::class)->index(
                    new \V2\Services\Search\DTOs\PublicSearchDTO(
                        $model->title ?? $model->name,
                        $model->slug,
                        'Convenios',
                        'convenio_categoria',
                        $model->id,
                        $model->description ?? '',
                        null,
                        ['tags' => []]
                    )
                );
            } catch (\Exception $e) {
                \Log::error('[Module] Falha ao sincronizar busca: ' . $e->getMessage());
            }
        });

        static::deleted(function ($model) {
            try {
                app(\V2\Services\Search\Services\PublicIndexerService::class)->remove('convenio_categoria', $model->id);
            } catch (\Exception $e) {
                \Log::error('[Module] Falha ao remover da busca: ' . $e->getMessage());
            }
        });
    }

    public function instituicoes()
    {
        return $this->hasMany(ConvenioInstituicao::class, 'categoria_id');
    }

    public function scopeActive($query)
    {
        return $query->where('is_active', true);
    }

    public function scopeOrdered($query)
    {
        return $query->orderBy('sort_order')->orderBy('name');
    }
}
```

**ConvenioInstituicao.php:**

```php
<?php

namespace V2\Modules\Convenios\Convenios\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class ConvenioInstituicao extends Model
{
    use SoftDeletes;

    protected $table = 'v2_convenio_instituicoes';

    public $incrementing = false;

    protected $keyType = 'string';

    protected $fillable = [
        'categoria_id', 'name', 'slug', 'description',
        'convenio_description', 'logo', 'url',
        'discount_badge', 'benefits_list',
        'sort_order', 'is_active',
    ];

    protected $casts = [
        'is_active' => 'boolean',
        'metadata'  => 'array',
    ];

    protected $dates = ['created_at', 'updated_at', 'deleted_at'];

    protected static function boot()
    {
        parent::boot();

        static::creating(function ($model) {
            if (empty($model->id)) {
                $model->id = v2_uuid();
            }
            if (empty($model->slug) && !empty($model->name)) {
                $model->slug = str_slug($model->name);
            }
        });

        static::saving(function ($model) {
            // Garantir unicidade do slug
            $baseSlug = $model->slug ?: str_slug($model->name);
            $slug = $baseSlug;
            $counter = 1;
            if (!empty($model->id)) {
                while (static::where('slug', $slug)->where('id', '!=', $model->id)->exists()) {
                    $slug = $baseSlug . '-' . $counter;
                    $counter++;
                }
            } else {
                while (static::where('slug', $slug)->exists()) {
                    $slug = $baseSlug . '-' . $counter;
                    $counter++;
                }
            }
            $model->slug = $slug;
        });

        static::saved(function ($model) {
            try {
                app(\V2\Services\Search\Services\PublicIndexerService::class)->index(
                    new \V2\Services\Search\DTOs\PublicSearchDTO(
                        $model->title ?? $model->name,
                        $model->slug,
                        'Convenios',
                        'convenio_instituicao',
                        $model->id,
                        $model->description ?? '',
                        null,
                        ['tags' => []]
                    )
                );
            } catch (\Exception $e) {
                \Log::error('[Module] Falha ao sincronizar busca: ' . $e->getMessage());
            }
        });

        static::deleted(function ($model) {
            try {
                app(\V2\Services\Search\Services\PublicIndexerService::class)->remove('convenio_instituicao', $model->id);
            } catch (\Exception $e) {
                \Log::error('[Module] Falha ao remover da busca: ' . $e->getMessage());
            }
        });
    }

    public function categoria()
    {
        return $this->belongsTo(ConvenioCategoria::class, 'categoria_id');
    }

    public function gallery()
    {
        return $this->hasMany(ConvenioInstituicaoGallery::class, 'instituicao_id');
    }

    public function scopeActive($query)
    {
        return $query->where('is_active', true);
    }

    public function scopeOrdered($query)
    {
        return $query->orderBy('sort_order')->orderBy('name');
    }

    public function scopeByCategoria($query, $categoriaId)
    {
        return $query->where('categoria_id', $categoriaId);
    }
}
```

**ConvenioInstituicaoGallery.php:**

```php
<?php

namespace V2\Modules\Convenios\Convenios\Models;

use Illuminate\Database\Eloquent\Model;

class ConvenioInstituicaoGallery extends Model
{
    protected $table = 'v2_convenio_instituicao_gallery';

    public $incrementing = false;

    protected $keyType = 'string';

    protected $fillable = ['instituicao_id', 'image', 'sort_order'];

    protected $casts = ['metadata' => 'array'];

    protected $dates = ['created_at', 'updated_at'];

    protected static function boot()
    {
        parent::boot();

        static::creating(function ($model) {
            if (empty($model->id)) {
                $model->id = v2_uuid();
            }
        });
    }

    public function instituicao()
    {
        return $this->belongsTo(ConvenioInstituicao::class, 'instituicao_id');
    }
}
```

### d) CMS Controllers

**CategoriaCmsController.php:**

```php
<?php

namespace V2\Modules\Convenios\Convenios\Controllers;

use V2\Core\Controllers\BaseController;
use Illuminate\Http\Request;
use V2\Modules\Convenios\Convenios\Models\ConvenioCategoria;
use V2\Services\ActivityLogger\ActivityLoggerService;
use V2\Services\SEO\Services\SEOService;
use Sentinel;
use Str;

class CategoriaCmsController extends BaseController
{
    /** @var ActivityLoggerService */
    protected $activityLogger;

    /** @var SEOService */
    protected $seo;

    public function __construct(
        ActivityLoggerService $activityLogger,
        SEOService $seo
    ) {
        $this->activityLogger = $activityLogger;
        $this->seo = $seo;
    }

    public function index(Request $request)
    {
        $query = ConvenioCategoria::query();

        if ($request->has('search') && !empty($request->input('search'))) {
            $searchTerm = $request->input('search');
            $query->where(function ($q) use ($searchTerm) {
                $q->where('name', 'ilike', '%' . $searchTerm . '%');
            });
        }

        $items = $query->orderBy('sort_order')->orderBy('name')->paginate(15);

        return view('v2-convenios::cms.categorias.index', compact('items'));
    }

    public function create()
    {
        return view('v2-convenios::cms.categorias.create');
    }

    public function store(Request $request)
    {
        $data = $request->validate([
            'name'        => 'required|string|max:255',
            'slug'        => 'nullable|string|max:255',
            'description' => 'nullable|string',
            'icon'        => 'nullable|image|mimes:svg,jpeg,png,jpg,webp|max:1024',
            'sort_order'  => 'nullable|integer|min:0',
            'is_active'   => 'nullable|boolean',
        ]);

        $model = new ConvenioCategoria();
        $model->id = v2_uuid();
        $model->name = $data['name'];
        $model->slug = !empty($data['slug']) ? $data['slug'] : Str::slug($data['name']);
        $model->description = $data['description'] ?? null;
        $model->sort_order = $data['sort_order'] ?? 0;
        $model->is_active = $data['is_active'] ?? true;
        $model->author_id = Sentinel::getUser()->id ?? null;

        if ($request->hasFile('icon')) {
            $model->icon = $request->file('icon')->store('public/upload/convenios/categorias');
        }

        $model->save();

        $this->activityLogger->log($model, 'created', ['type' => 'convenio_categoria_create']);

        flash('Categoria criada com sucesso!')->success();
        return redirect()->route('v2.convenios.cms.categorias.edit', ['id' => $model->id]);
    }

    public function edit($id)
    {
        $model = ConvenioCategoria::findOrFail($id);
        return view('v2-convenios::cms.categorias.edit', compact('model'));
    }

    public function update(Request $request, $id)
    {
        $model = ConvenioCategoria::findOrFail($id);

        $data = $request->validate([
            'name'        => 'required|string|max:255',
            'slug'        => 'nullable|string|max:255|unique:v2_convenio_categorias,slug,' . $id . ',id',
            'description' => 'nullable|string',
            'icon'        => 'nullable|image|mimes:svg,jpeg,png,jpg,webp|max:1024',
            'sort_order'  => 'nullable|integer|min:0',
            'is_active'   => 'nullable|boolean',
        ]);

        $model->name = $data['name'];
        if (!empty($data['slug'])) {
            $model->slug = $data['slug'];
        }
        $model->description = $data['description'] ?? null;
        $model->sort_order = $data['sort_order'] ?? 0;
        $model->is_active = $data['is_active'] ?? true;

        if ($request->hasFile('icon')) {
            $model->icon = $request->file('icon')->store('public/upload/convenios/categorias');
        }

        $model->save();

        $this->activityLogger->log($model, 'updated', ['type' => 'convenio_categoria_update']);

        flash('Categoria atualizada com sucesso!')->success();
        return redirect()->route('v2.convenios.cms.categorias.edit', ['id' => $model->id]);
    }

    public function destroy($id)
    {
        $model = ConvenioCategoria::findOrFail($id);
        $name = $model->name;
        $model->delete();

        $this->activityLogger->log($model, 'archived', ['type' => 'convenio_categoria_archive']);

        flash('"' . $name . '" arquivada com sucesso!')->success();
        return redirect()->route('v2.convenios.cms.categorias.index');
    }
}
```

**InstituicaoCmsController.php:**

```php
<?php

namespace V2\Modules\Convenios\Convenios\Controllers;

use V2\Core\Controllers\BaseController;
use Illuminate\Http\Request;
use V2\Modules\Convenios\Convenios\Models\ConvenioInstituicao;
use V2\Modules\Convenios\Convenios\Models\ConvenioCategoria;
use V2\Services\ActivityLogger\ActivityLoggerService;
use V2\Services\SEO\Services\SEOService;
use Sentinel;
use Str;

class InstituicaoCmsController extends BaseController
{
    /** @var ActivityLoggerService */
    protected $activityLogger;

    /** @var SEOService */
    protected $seo;

    public function __construct(
        ActivityLoggerService $activityLogger,
        SEOService $seo
    ) {
        $this->activityLogger = $activityLogger;
        $this->seo = $seo;
    }

    public function index(Request $request)
    {
        $query = ConvenioInstituicao::with('categoria');

        if ($request->has('search') && !empty($request->input('search'))) {
            $searchTerm = $request->input('search');
            $query->where(function ($q) use ($searchTerm) {
                $q->where('name', 'ilike', '%' . $searchTerm . '%');
            });
        }

        if ($request->has('categoria_id')) {
            $query->where('categoria_id', $request->input('categoria_id'));
        }

        $items = $query->orderBy('sort_order')->orderBy('name')->paginate(15);
        $categorias = ConvenioCategoria::ordered()->get();

        return view('v2-convenios::cms.instituicoes.index', compact('items', 'categorias'));
    }

    public function create()
    {
        $categorias = ConvenioCategoria::ordered()->get();
        return view('v2-convenios::cms.instituicoes.create', compact('categorias'));
    }

    public function store(Request $request)
    {
        $data = $request->validate([
            'categoria_id'      => 'required|uuid|exists:v2_convenio_categorias,id',
            'name'              => 'required|string|max:255',
            'slug'              => 'nullable|string|max:255',
            'description'       => 'nullable|string',
            'convenio_description' => 'nullable|string',
            'logo'              => 'nullable|image|mimes:jpeg,png,jpg,webp|max:2048',
            'url'               => 'nullable|url|max:255',
            'discount_badge'    => 'nullable|string|max:255',
            'benefits_list'     => 'nullable|string',
            'sort_order'        => 'nullable|integer|min:0',
            'is_active'         => 'nullable|boolean',
        ]);

        $model = new ConvenioInstituicao();
        $model->id = v2_uuid();
        $model->categoria_id = $data['categoria_id'];
        $model->name = $data['name'];
        $model->slug = !empty($data['slug']) ? $data['slug'] : Str::slug($data['name']);
        $model->description = $data['description'] ?? null;
        $model->convenio_description = $data['convenio_description'] ?? null;
        $model->url = $data['url'] ?? null;
        $model->discount_badge = $data['discount_badge'] ?? null;
        $model->benefits_list = $data['benefits_list'] ?? null;
        $model->sort_order = $data['sort_order'] ?? 0;
        $model->is_active = $data['is_active'] ?? true;
        $model->author_id = Sentinel::getUser()->id ?? null;

        if ($request->hasFile('logo')) {
            $model->logo = $request->file('logo')->store('public/upload/convenios/instituicoes');
        }

        $model->save();

        $this->activityLogger->log($model, 'created', ['type' => 'convenio_instituicao_create']);

        flash('Instituição criada com sucesso!')->success();
        return redirect()->route('v2.convenios.cms.instituicoes.edit', ['id' => $model->id]);
    }

    public function edit($id)
    {
        $model = ConvenioInstituicao::with('gallery', 'categoria')->findOrFail($id);
        $categorias = ConvenioCategoria::ordered()->get();
        return view('v2-convenios::cms.instituicoes.edit', compact('model', 'categorias'));
    }

    public function update(Request $request, $id)
    {
        $model = ConvenioInstituicao::findOrFail($id);

        $data = $request->validate([
            'categoria_id'      => 'required|uuid|exists:v2_convenio_categorias,id',
            'name'              => 'required|string|max:255',
            'slug'              => 'nullable|string|max:255|unique:v2_convenio_instituicoes,slug,' . $id . ',id',
            'description'       => 'nullable|string',
            'convenio_description' => 'nullable|string',
            'logo'              => 'nullable|image|mimes:jpeg,png,jpg,webp|max:2048',
            'url'               => 'nullable|url|max:255',
            'discount_badge'    => 'nullable|string|max:255',
            'benefits_list'     => 'nullable|string',
            'sort_order'        => 'nullable|integer|min:0',
            'is_active'         => 'nullable|boolean',
        ]);

        $model->categoria_id = $data['categoria_id'];
        $model->name = $data['name'];
        if (!empty($data['slug'])) {
            $model->slug = $data['slug'];
        }
        $model->description = $data['description'] ?? null;
        $model->convenio_description = $data['convenio_description'] ?? null;
        $model->url = $data['url'] ?? null;
        $model->discount_badge = $data['discount_badge'] ?? null;
        $model->benefits_list = $data['benefits_list'] ?? null;
        $model->sort_order = $data['sort_order'] ?? 0;
        $model->is_active = $data['is_active'] ?? true;

        if ($request->hasFile('logo')) {
            $model->logo = $request->file('logo')->store('public/upload/convenios/instituicoes');
        }

        $model->save();

        $this->activityLogger->log($model, 'updated', ['type' => 'convenio_instituicao_update']);

        flash('Instituição atualizada com sucesso!')->success();
        return redirect()->route('v2.convenios.cms.instituicoes.edit', ['id' => $model->id]);
    }

    public function destroy($id)
    {
        $model = ConvenioInstituicao::findOrFail($id);
        $name = $model->name;
        $model->delete();

        $this->activityLogger->log($model, 'archived', ['type' => 'convenio_instituicao_archive']);

        flash('"' . $name . '" arquivada com sucesso!')->success();
        return redirect()->route('v2.convenios.cms.instituicoes.index');
    }

    public function uploadGallery(Request $request, $id)
    {
        $model = ConvenioInstituicao::findOrFail($id);

        $request->validate([
            'images'   => 'required|array',
            'images.*' => 'image|mimes:jpeg,png,jpg,webp|max:4096',
        ]);

        foreach ($request->file('images') as $image) {
            $gallery = new ConvenioInstituicaoGallery();
            $gallery->id = v2_uuid();
            $gallery->instituicao_id = $model->id;
            $gallery->image = $image->store('public/upload/convenios/gallery');
            $gallery->save();
        }

        flash('Imagens adicionadas com sucesso!')->success();
        return redirect()->route('v2.convenios.cms.instituicoes.edit', ['id' => $model->id]);
    }

    public function destroyGallery($galleryId)
    {
        $gallery = ConvenioInstituicaoGallery::findOrFail($galleryId);
        $instituicaoId = $gallery->instituicao_id;
        $gallery->delete();

        flash('Imagem removida com sucesso!')->success();
        return redirect()->route('v2.convenios.cms.instituicoes.edit', ['id' => $instituicaoId]);
    }
}
```

### e) Portal Controller (ConveniosPortalController.php)

```php
<?php

namespace V2\Modules\Convenios\Convenios\Controllers;

use V2\Core\Controllers\BaseController;
use Illuminate\Http\Request;
use V2\Modules\Convenios\Convenios\Models\ConvenioCategoria;
use V2\Modules\Convenios\Convenios\Models\ConvenioInstituicao;
use V2\Services\SEO\Services\SEOService;

class ConveniosPortalController extends BaseController
{
    protected $seo;

    public function __construct(SEOService $seo)
    {
        $this->seo = $seo;
    }

    public function categorias()
    {
        $categorias = ConvenioCategoria::active()->ordered()->get();

        $this->seo->setTitle('Convênios — ' . config('app.name'));
        $this->seo->setDescription('Convênios oferecidos pela SUCESU aos seus associados.');

        return view('v2-convenios::portal.categorias', compact('categorias'));
    }

    public function instituicoes(Request $request)
    {
        $categoriaSlug = $request->query('categoria');
        $categoria = ConvenioCategoria::where('slug', $categoriaSlug)->active()->firstOrFail();

        $instituicoes = ConvenioInstituicao::with('categoria')
            ->byCategoria($categoria->id)
            ->active()
            ->ordered()
            ->paginate(12);

        $this->seo->setTitle($categoria->name . ' — Convênios SUCESU');

        return view('v2-convenios::portal.instituicoes', compact('instituicoes', 'categoria'));
    }

    public function show(Request $request)
    {
        $categoriaSlug = $request->query('categoria');
        $instituicaoSlug = $request->query('instituicao');

        $categoria = ConvenioCategoria::where('slug', $categoriaSlug)->active()->firstOrFail();
        $model = ConvenioInstituicao::with('gallery', 'categoria')
            ->where('slug', $instituicaoSlug)
            ->byCategoria($categoria->id)
            ->active()
            ->firstOrFail();

        $benefitsList = !empty($model->benefits_list)
            ? array_filter(explode("\n", $model->benefits_list))
            : [];

        $this->seo->setTitle($model->name . ' — Convênios SUCESU');
        $this->seo->setDescription($model->convenio_description ?? $model->description);

        return view('v2-convenios::portal.instituicao', compact('model', 'categoria', 'benefitsList'));
    }
}
```

### f) Rotas CMS (Routes/cms.php)

```php
<?php

Route::group([
    'middleware' => [\V2\Core\Middleware\CmsAuthenticate::class],
    'prefix'     => 'cms/convenios',
], function () {

    // Categorias
    Route::get('/categorias', [
        'as'   => 'v2.convenios.cms.categorias.index',
        'uses' => 'CategoriaCmsController@index',
    ]);

    Route::get('/categorias/novo', [
        'as'   => 'v2.convenios.cms.categorias.create',
        'uses' => 'CategoriaCmsController@create',
    ]);

    Route::post('/categorias/novo', [
        'as'   => 'v2.convenios.cms.categorias.store',
        'uses' => 'CategoriaCmsController@store',
    ]);

    Route::get('/categorias/editar/{id}', [
        'as'   => 'v2.convenios.cms.categorias.edit',
        'uses' => 'CategoriaCmsController@edit',
    ]);

    Route::match(['PUT', 'POST'], '/categorias/editar/{id}', [
        'as'   => 'v2.convenios.cms.categorias.update',
        'uses' => 'CategoriaCmsController@update',
    ]);

    Route::delete('/categorias/{id}', [
        'as'   => 'v2.convenios.cms.categorias.destroy',
        'uses' => 'CategoriaCmsController@destroy',
    ]);

    // Instituições
    Route::get('/instituicoes', [
        'as'   => 'v2.convenios.cms.instituicoes.index',
        'uses' => 'InstituicaoCmsController@index',
    ]);

    Route::get('/instituicoes/novo', [
        'as'   => 'v2.convenios.cms.instituicoes.create',
        'uses' => 'InstituicaoCmsController@create',
    ]);

    Route::post('/instituicoes/novo', [
        'as'   => 'v2.convenios.cms.instituicoes.store',
        'uses' => 'InstituicaoCmsController@store',
    ]);

    Route::get('/instituicoes/editar/{id}', [
        'as'   => 'v2.convenios.cms.instituicoes.edit',
        'uses' => 'InstituicaoCmsController@edit',
    ]);

    Route::match(['PUT', 'POST'], '/instituicoes/editar/{id}', [
        'as'   => 'v2.convenios.cms.instituicoes.update',
        'uses' => 'InstituicaoCmsController@update',
    ]);

    Route::delete('/instituicoes/{id}', [
        'as'   => 'v2.convenios.cms.instituicoes.destroy',
        'uses' => 'InstituicaoCmsController@destroy',
    ]);

    // Galeria
    Route::post('/instituicoes/{id}/gallery', [
        'as'   => 'v2.convenios.cms.instituicoes.gallery.upload',
        'uses' => 'InstituicaoCmsController@uploadGallery',
    ]);

    Route::delete('/gallery/{galleryId}', [
        'as'   => 'v2.convenios.cms.instituicoes.gallery.destroy',
        'uses' => 'InstituicaoCmsController@destroyGallery',
    ]);
});
```

### g) Rotas Web (Routes/web.php)

```php
<?php

Route::group(['middleware' => ['web']], function () {

    Route::get('/convenios', [
        'as'   => 'v2.convenios.portal.categorias',
        'uses' => 'ConveniosPortalController@categorias',
    ]);

    Route::get('/convenios-detalhes', [
        'as'   => 'v2.convenios.portal.instituicoes',
        'uses' => 'ConveniosPortalController@instituicoes',
    ]);

    Route::get('/convenio', [
        'as'   => 'v2.convenios.portal.show',
        'uses' => 'ConveniosPortalController@show',
    ]);
});
```

### h) View CMS — Categorias (cms/categorias/index.blade.php)

```blade
@extends('v2-cms::layouts.app')

@section('title', 'Categorias de Convênios')
@section('header-title', 'Categorias')
@section('header-description', 'Gerenciamento de categorias de convênios')

@section('content')
    <div class="card">
        <div class="card-header d-flex justify-content-between align-items-center">
            <form method="GET" class="d-flex gap-2">
                <input type="text" name="search" class="form-control form-control-sm"
                       placeholder="Buscar..." value="{{ request('search') }}">
                <button type="submit" class="btn btn-sm btn-outline-secondary">
                    <i class="bi bi-search"></i>
                </button>
            </form>
            @v2can('Convenios', 'write')
                <a href="{{ route('v2.convenios.cms.categorias.create') }}" class="btn btn-sm btn-primary">
                    <i class="bi bi-plus-circle"></i> Nova Categoria
                </a>
            @endv2can
        </div>
        <div class="card-body">
            @if(count($items) > 0)
                <table class="table table-hover">
                    <thead>
                        <tr>
                            <th>Ícone</th>
                            <th>Nome</th>
                            <th>Ordem</th>
                            <th>Ativo</th>
                            <th>Ações</th>
                        </tr>
                    </thead>
                    <tbody>
                        @foreach($items as $item)
                        <tr>
                            <td>
                                @if($item->icon)
                                    <img src="{{ Storage::url($item->icon) }}" alt="{{ $item->name }}" style="max-height: 32px;">
                                @endif
                            </td>
                            <td>{{ $item->name }}</td>
                            <td>{{ $item->sort_order }}</td>
                            <td>
                                <span class="badge bg-{{ $item->is_active ? 'success' : 'secondary' }}">
                                    {{ $item->is_active ? 'Ativo' : 'Inativo' }}
                                </span>
                            </td>
                            <td>
                                <a href="{{ route('v2.convenios.cms.categorias.edit', ['id' => $item->id]) }}"
                                   class="btn btn-sm btn-outline-primary">
                                    <i class="bi bi-pencil"></i>
                                </a>
                                <form action="{{ route('v2.convenios.cms.categorias.destroy', ['id' => $item->id]) }}"
                                      method="POST" class="d-inline"
                                      onsubmit="return confirm('Arquivar esta categoria?');">
                                    {{ csrf_field() }}
                                    {{ method_field('DELETE') }}
                                    <button type="submit" class="btn btn-sm btn-outline-danger">
                                        <i class="bi bi-archive"></i>
                                    </button>
                                </form>
                            </td>
                        </tr>
                        @endforeach
                    </tbody>
                </table>
                {{ $items->links() }}
            @else
                <div class="text-center py-4 text-muted">
                    <i class="bi bi-inbox fs-1"></i>
                    <p class="mt-2">Nenhuma categoria encontrada.</p>
                </div>
            @endif
        </div>
    </div>
@endsection
```

### i) View Portal — Categorias (portal/categorias.blade.php)

```blade
@extends('v2-portal::layouts.app')

@section('seo')
    @include('v2-portal::partials.seo')
@endsection

@section('content')
    <section class="convenios-section section-padding fix">
        <div class="container">
            <div class="row g-4">
                @foreach($categorias as $categoria)
                <div class="col-xl-4 col-lg-4 col-md-6 wow fadeInUp">
                    <a href="{{ route('v2.convenios.portal.instituicoes', ['categoria' => $categoria->slug]) }}"
                       class="text-decoration-none">
                        <div class="categoria-card text-center p-4">
                            @if($categoria->icon)
                                <img src="{{ Storage::url($categoria->icon) }}" alt="{{ $categoria->name }}"
                                     style="max-height: 64px;">
                            @endif
                            <h3 class="mt-3">{{ $categoria->name }}</h3>
                            <p class="text-muted">{{ Str::limit($categoria->description, 100) }}</p>
                            <span class="link-btn">
                                Saiba Mais <i class="fa-light fa-arrow-right-long"></i>
                            </span>
                        </div>
                    </a>
                </div>
                @endforeach
            </div>
        </div>
    </section>
@endsection
```

### j) Dependências e Integrações

- **ActivityLoggerService**: Injetado em `CategoriaCmsController` e `InstituicaoCmsController` para auditoria.
- **SEOService**: Injetado em todos os controllers para metadados de página.
- **Storage**: Upload de ícones de categorias (`public/upload/convenios/categorias/`), logos de instituições (`public/upload/convenios/instituicoes/`) e imagens de galeria (`public/upload/convenios/gallery/`).
- **Permissões**: `@v2can('Convenios', 'write')` nas views CMS para controle de acesso.
- **Relacionamentos**: Categoria `hasMany` Instituições; Instituição `hasMany` Gallery; Instituição `belongsTo` Categoria.
- **Galeria**: Upload múltiplo no formulário de edição da instituição. Gerenciamento via rotas dedicadas.
