# Deep dive #12b — Sistema de soporte integrado al portal cliente

**Status**: ✅ Sprint 1 técnico DONE 2026-05-06. Live en producción.
**Frente master**: #12b NEXT (priorizado después del #4 user portal).
**Tiempo invertido**: ~5 hr.

---

## Por qué este frente

Con clientes reales pagando (coke, surnuevonorte, pronto los que entren por los ads), TODO el soporte hoy iba a `mailto:soporte@bewpro.com` sin trazabilidad: no sabemos cuántos tickets hay abiertos, cuál fue la 1ª respuesta, cuáles quedaron sin atender.

Decidimos integrarlo dentro del portal `/my-projects` (no Linear/Zendesk) porque:
- El cliente ya está logueado en bewpro.com cuando aparece un problema.
- Auto-asociación al `tenant_project` (no hay que preguntar "qué sitio?").
- Trazabilidad single-source-of-truth + métricas en panel propio.
- Asignable a Leandro vía Spatie permission `support.manage`.

---

## Modelo conceptual

```
SupportTicket (table support_tickets)
  user_id, tenant_project_id (nullable), subject, category, priority,
  status (open|waiting_staff|waiting_client|resolved|closed),
  first_response_at, resolved_at, closed_at

SupportMessage (table support_messages)
  ticket_id, user_id (cliente o staff), body
```

### Status lifecycle

```
[Cliente abre]   → open
[Staff responde] → waiting_client (+ first_response_at si era 1a)
[Cliente responde]→ waiting_staff
[Staff resuelve] → resolved (+ resolved_at + email al cliente)
[Cliente reopen] → waiting_staff (responde a un ticket resolved)
[Staff/cliente]  → closed (+ closed_at, no más reopen)
```

### Categorías

`technical` (sitio no funciona), `billing`, `refund`, `feature_request`, `general`.

### Prioridades

`low | normal | high | urgent`. Los `urgent` van a Slack con 🚨.

---

## Archivos creados (Sprint 1)

```
app/Modules/Support/
  ├─ Controllers/
  │   ├─ Client/SupportController.php       index, create, store, show, reply, close
  │   └─ Admin/SupportController.php        index (con filtros + counters), dashboard, show, reply, updateStatus, resolve
  ├─ Models/
  │   ├─ SupportTicket.php                  status_badge, category_label, priority_badge accessors
  │   └─ SupportMessage.php                 isFromStaff()
  └─ Services/
      └─ SupportService.php                 createTicket, replyAsClient, replyAsStaff, resolve, close + notifs

app/Mail/
  ├─ SupportTicketOpenedMail.php            (a staff)
  ├─ SupportMessageFromStaffMail.php        (a cliente)
  ├─ SupportMessageFromClientMail.php       (a staff)
  └─ SupportTicketResolvedMail.php          (a cliente)

resources/views/
  ├─ emails/support/{ticket-opened,message-from-staff,message-from-client,ticket-resolved}.blade.php
  └─ modules/support/
      ├─ client/{index,new,show}.blade.php
      └─ admin/{index,show,dashboard}.blade.php

routes/modules/support.php                  (importado desde routes/cd-system.php)
config/support.php                          (staff_email override)

database/migrations/
  ├─ 2026_05_06_180000_create_support_tickets_table.php
  └─ 2026_05_06_180001_create_support_messages_table.php

database/seeders/SupportPermissionsSeeder.php  (permission 'support.manage' a Super Admin + System Admin)

app/Policies/SupportTicketPolicy.php        (registrada en AuthServiceProvider)
```

### Integración portal `/my-projects`

- Header del index: nuevo botón **"Soporte"** entre "Mis pagos" y "Comenzar nuevo proyecto".
- Footer del index: "Soporte" reemplaza el `mailto`.
- Show del proyecto: los 3 mailto reemplazados:
  - "Solicitar soporte" → `/my-projects/support/new?project={id}&category=technical`
  - "Solicitar reembolso" → `/my-projects/support/new?project={id}&category=refund`
  - "¿Querés más?" → `/my-projects/support/new?project={id}&category=feature_request`

---

## Routes (todas verificadas con 302 redirect a login en producción)

### Cliente — `client.support.*`

```
GET  /my-projects/support              index (bandeja propia)
GET  /my-projects/support/new          create
POST /my-projects/support               store
GET  /my-projects/support/{ticket}     show
POST /my-projects/support/{ticket}/reply
POST /my-projects/support/{ticket}/close
```

Middleware: `['ensure.main.site', 'auth', 'verified', 'role:Client']`.

### Staff — `admin.support.*`

```
GET  /admin/support                    index (con filtros + counters)
GET  /admin/support/dashboard          dashboard métricas
GET  /admin/support/{ticket}           show
POST /admin/support/{ticket}/reply
POST /admin/support/{ticket}/status    cambiar status/priority/category
POST /admin/support/{ticket}/resolve   marcar resolved + notif cliente
```

Middleware: `['auth', 'permission:support.manage', 'block.clients']`.

---

## Notificaciones

| Evento | Destinatario | Canal email | Slack |
|---|---|---|---|
| Cliente abre ticket | staff (`SUPPORT_STAFF_EMAIL` env or `mail.from.address`) | `SupportTicketOpenedMail` | ✅ con emoji por prioridad |
| Staff responde | cliente | `SupportMessageFromStaffMail` | — |
| Cliente responde | staff | `SupportMessageFromClientMail` | — |
| Staff resuelve | cliente | `SupportTicketResolvedMail` | — |

Todos los `Mail::send` envueltos en `try/catch` con `Log::warning` para no romper flow si SMTP falla.

---

## Permisos

```php
Permission: 'support.manage' (guard: web)
  ├─ Asignado: Super Admin, System Admin (via SupportPermissionsSeeder)
  └─ Asignable manualmente: Administrator (e.g. Leandro)
```

Para dar acceso a Leandro:
```bash
php artisan tinker
> Spatie\Permission\Models\Permission::findByName('support.manage')->assignRole(
>   Spatie\Permission\Models\Role::findByName('Administrator')
> );
```
O por user específico:
```php
User::where('email','leandro@lacompaniadigital.com')->first()->givePermissionTo('support.manage');
```

---

## Métricas dashboard (`/admin/support/dashboard`)

- Total open (open + waiting_staff + waiting_client)
- Avg time to first response (últimos 30 días) — formato humano (s / m / h / d)
- By category last 7 days
- By priority last 7 days
- Resolved last 7 days

Queries simples sobre `support_tickets` con `groupBy` y `AVG(TIMESTAMPDIFF(SECOND, created_at, first_response_at))`.

---

## Verificación E2E (post-deploy producción)

| Endpoint | Resultado |
|---|---|
| `/my-projects/support` (sin auth) | 302 → /login |
| `/my-projects/support/{id}` (sin auth) | 302 → /login |
| `/admin/support` (sin auth) | 302 → /login |
| `/admin/support/dashboard` (sin auth) | 302 → /login |
| Crear ticket via tinker | ✅ #1 creado, status=open, 1 message, status_badge correcto |
| Render vistas client (index, show, new) | ✅ 90+kb cada uno |
| Render vistas admin (index, dashboard, show) | ✅ all OK |

---

## Sprint 2 (futuro escalado, no priorizado)

| # | Feature | Esfuerzo |
|---|---|---|
| 1 | Asignación a staff específico (`assigned_to`) + notas internas (`is_internal`) | 1.5 hr |
| 2 | Attachments (Cloudinary) | 1.5 hr |
| 3 | Email forwarder soporte@bewpro.com → IMAP/Mailgun parsing → auto-create ticket | 2-3 hr |
| 4 | Auto-close si waiting_client > 14 días sin respuesta (cron) | 30 min |
| 5 | Slack respuesta inline (responder ticket desde Slack) | 2 hr |
| 6 | Búsqueda full-text Elasticsearch sobre body de mensajes | 2 hr |
| 7 | Templates de respuestas frecuentes (canned responses) | 1 hr |
| 8 | Tests automatizados (12 tests escritos siguiendo patrón Sprint 2 user portal) | 1 hr |

---

## Decisiones registradas

| Pregunta | Decisión | Razón |
|---|---|---|
| Standalone (Linear/Zendesk) o integrado al portal? | **Integrado** | El cliente ya vive en `/my-projects`. Single-source-of-truth + auto-asociación al proyecto |
| Email forwarder a soporte@? | **No, solo portal** | Forzamos canal único = trazabilidad total. Mailto queda como fallback solo en flows fuera del login (OrderReceived) |
| Quién accede a la bandeja staff? | **System Admin + permiso `support.manage` asignable** | Leandro recibe el permiso sin acceso destructivo al resto del panel |
| Notas internas (is_internal)? | **Sprint 2** | MVP simple primero. Necesario solo cuando hay múltiples staff |
| Attachments? | **Sprint 2** | Cloudinary suma complejidad. Texto + screenshots inline (markdown image) cubren 80% del caso |

---

*Iniciado y cerrado: 2026-05-06*. Sprint 1 técnico ✅ DONE + deployado a producción.
