# Auditoría dry-run: Bloque 2 (Collabs) — Colab vs offer vs collabable

Solo inspección. Sin cambios de código.

---

## Evidencia del modelo Colab

- **`app/Models/Colab.php`**: `fillable` incluye `offer_id`, `influencer_id`, `collabable_id`, `collabable_type`, `date`, `comment`, `note`. Relación `company()` definida como `HasOneThrough(Company::class, Offer::class, ...)`.
- **Migración `create_colabs_table`**: tabla `colabs` tiene `date` (nullable), `comment`; no hay columnas `address`, `people_count`, `observations`, `time`.
- **Conclusión**: `$colab->company` existe (relación). `$colab->date` existe (columna). `$colab->address`, `$colab->people_count`, `$colab->observations`, `$colab->time` no son columnas de `colabs`; en Eloquent devuelven `null` (riesgo de dato vacío si se usan como fuente de verdad).

---

## Evidencia del modelo Influencer

- **`app/Models/Influencer.php`**: `fillable` incluye `name`, `surnames`, etc. No hay `instagram_username`. Relación `users()` (MorphToMany), método `getUser()`. No existe relación `user()` (singular).
- **Conclusión**: `$colab->influencer->user` no existe como relación → riesgo de crash si se accede cuando `$colab->influencer` existe. `$colab->influencer->instagram_username` no es atributo del modelo → siempre `null` (dato vacío); el origen canónico es `socialMedia()->first()?->pivot?->account_name`.

---

## Evidencia del modelo Company

- **`app/Models/Company.php`**: `fillable` incluye `company`, `company_name`, etc. No hay atributo `name`.
- **Conclusión**: `$colab->company->name` accede a un atributo no definido → Eloquent devuelve `null` (dato vacío o inconsistente). Fuente canónica: `$colab->offer?->company?->company` o `company_name`.

---

## Tabla A — Sospechosos encontrados (Bloque 2)

| Mailable (archivo) | Línea / snippet | Tipo de dato | Riesgo | Fuente canónica sugerida |
|--------------------|-----------------|--------------|--------|---------------------------|
| ClientCanceledPendingCompanyEmail | 45 `$colab?->influencer?->instagram_username` | instagram | Dato vacío (atributo no existe en Influencer) | `$colab?->influencer?->socialMedia()->first()?->pivot?->account_name` |
| ClientCanceledPendingCompanyEmail | 47 `$colab?->company?->company` | company | No riesgo real (Colab tiene relación `company()`) | — |
| ClientCanceledPendingCompanyEmail | 49 `$colab?->address` | address | Dato vacío (Colab no tiene columna address) | `$colab?->collabable?->address` + AddressHelper::formatAddress |
| ClientCanceledPendingCompanyEmail | 50 `$colab?->date` | date | No riesgo real (columna existe en colabs) | Para formato día/hora: `$colab->getDay()`, `$colab->getTime()` |
| ClientCanceledPendingCompanyEmail | 51 `$colab?->people_count` | people_count | Dato vacío (Colab no tiene columna) | `$colab?->collabable?->guests` |
| ClientCanceledPendingCompanyEmail | 52 `$colab?->observations` | observations | Dato vacío (Colab no tiene columna) | `$colab?->collabable?->observations` o `$colab->comment` según negocio |
| ClientCanceledPendingInfluencerEmail | 37 `$colab?->company?->company` | company | No riesgo real | — |
| ClientCanceledPendingInfluencerEmail | 38-41 `$colab?->address`, `$colab?->date`, `$colab?->people_count`, `$colab?->observations` | address, date, people_count, observations | Mismo que arriba | collabable + getDay/getTime para fecha |
| ClientCanceledAcceptedCompanyEmail | 43 `$colab?->influencer?->instagram_username` | instagram | Dato vacío | socialMedia()->first()?->pivot?->account_name |
| ClientCanceledAcceptedCompanyEmail | 46 `$colab?->offer?->company?->company ?? $colab?->company?->company` | company | No riesgo real | — |
| ClientCanceledAcceptedCompanyEmail | 47-50 `$colab?->address`, `$colab?->date`, `$colab?->people_count`, `$colab?->observations` | address, date, people_count, observations | Dato vacío en address, people_count, observations; date existe | collabable + getDay/getTime |
| ClientCanceledAcceptedInfluencerEmail | 36-40 `$colab?->company?->company`, address, date, people_count, observations | idem | Igual que anteriores | offer?->company?->company; collabable para resto |
| InfluencerCanceledPendingNomadeEmail | 34-38 `$colab?->company?->company`, address, date, people_count, observations | idem | company OK; resto dato vacío | offer?->company; collabable; getDay/getTime |
| ConfirmedModificationCompanyEmail | 35 `$colab->influencer->user->name` / `->user->surname` | influencer user | Crash (Influencer no tiene relación `user`, tiene `users()` y `getUser()`) | `$colab->influencer?->getUser()?->name` y surname |
| ConfirmedModificationCompanyEmail | 36 `$colab->influencer->instagram_username` | instagram | Dato vacío | socialMedia()->first()?->pivot?->account_name |
| ConfirmedModificationCompanyEmail | 39 `$colab->company->name` | company name | Dato vacío (Company no tiene atributo `name`) | `$colab->offer?->company?->company` o `company_name` |
| ConfirmedModificationCompanyEmail | 40-44 `$colab->address`, `$colab->date`, `$colab->time`, `$colab->people_count`, `$colab->observations` | address, date, time, people_count, observations | date OK; address/time/people_count/observations no existen en Colab → dato vacío | collabable + getDay/getTime |
| ConfirmedModificationInfluencerEmail | 35-44 | Idéntico a ConfirmedModificationCompanyEmail | Mismos riesgos | Mismas fuentes canónicas |

Otros mailables del bloque que usan solo `instagram_username` (mismo riesgo dato vacío, sin crash):

- ClientCanceledAcceptedInternalEmail, ClientCanceledModificationCompanyEmail, ClientCanceledModificationInfluencerEmail, ClientCanceledModificationInternalEmail, ClientCanceledPendingInternalEmail, InfluencerCanceledPendingCustomerEmail, InfluencerCanceledPendingInternalEmail, InfluencerCanceledPendingNomadeInternalEmail, AutoCanceledModificationCompanyEmail, AutoCanceledModificationInternalEmail: usan `$colab?->influencer?->instagram_username` → fuente canónica: `socialMedia()->first()?->pivot?->account_name`.

---

## Tabla B — Mailables OK (patrón offer / collabable / getDate / socialMedia pivot)

Estos mailables del Bloque 2 ya usan fuentes canónicas:

| Mailable | Uso correcto observado |
|----------|-------------------------|
| NewColabInfluencerEmail | getDay(), getTime(); clientName vía offer->company->company; address vía collabable (AddressHelper); guests/observations desde collabable/comment |
| NewColabNomadeEmail | offer?->company; collabable?->address; getDay(), getTime(); collabable->guests, ->observations |
| AcceptedColabCompanyEmail | offer->company->getUser(); influencer->socialMedia()->first()?->pivot?->account_name; collabable->address; getDay(), getTime(); collabable->guests, ->observations |
| ClientAcceptedColabCompanyEmail | offer->company; influencer->socialMedia()->first()?->pivot; collabable + AddressHelper; getDay(), getTime(); collabable->guests, ->observations |
| ClientRejectedColabCompanyEmail | offer->company; influencer->socialMedia()->first()?->pivot?->account_name; collabable + getDay/getTime; collabable->guests, ->observations |
| RejectedColabInfluencerEmail | offer?->company; collabable?->address; dateText desde day/time del collabable |
| RejectedColabNomadeEmail | influencer->socialMedia()->first()?->pivot?->account_name; collabable para reason |
| PendingColabReminder1CompanyEmail | build() usa colab->influencer, colab->offer->company; collabable->address, getDate(); collabable->guests, ->observations; socialMedia()->first()?->pivot?->account_name |
| PendingColabReminder2CompanyEmail | Idem |
| AutoCanceledColabNomadeEmail | offer?->company; collabable para address, guests, observations; getDay(), getTime() |
| AutoCanceledColabCompanyEmail | offer?->company; influencer->getUser(); socialMedia (pero usa pivot sin account_name en vista, solo en subject) |
| AutoCanceledColabInfluencerEmail | offer?->company; collabable para address, guests; getDay(), getTime() |
| NomadeCanceledPendingClientCompanyEmail | offer?->company?->getUser(); collabable?->address; getDay(), getTime(); collabable->guests, ->observations; socialMedia pivot account_name |
| NomadeCanceledPendingClientInfluencerEmail | offer?->company?->company; collabable?->address; getDay(), getTime(); collabable->guests, ->observations; socialMedia pivot |
| NomadeCanceledPendingInfluencerEmail | offer?->company?->company; collabable; getDay(), getTime(); collabable->guests, ->observations |
| CanceledColabEmail | build() usa colab?->offer?->company; influencer?->socialMedia()?->first()?->pivot?->account_name |

---

## Tabla C — Hipótesis por qué no se vieron fallos en QA

1. **Colab tiene relación `company()`**: Quien usa `$colab->company` (p. ej. ClientCanceledPending*, InfluencerCanceledPendingNomade) no rompe; solo `$colab->company->name` en ConfirmedModification* devuelve null (Company sin `name`).
2. **Fallbacks con `??`**: La mayoría de accesos sospechosos usan `?? 'valor'`, por lo que no hay crash, solo contenido por defecto (ej. "Dirección del establecimiento", "2", "15 de marzo de 2025").
3. **ConfirmedModification* con `$colab->influencer->user`**: Si en QA siempre se pasó `$colab` con `influencer` cargado pero no se ejecutó el constructor con un Influencer sin relación `user` definida, Laravel podría haber devuelto null en lugar de lanzar (depende de si `user` está definido como relación o no). En el modelo Influencer no existe `user()`, por tanto `$influencer->user` es acceso a propiedad inexistente → null en PHP 8; el crash ocurriría al encadenar `->user->name` si `user` es null (sí lanzaría). Hipótesis: rutas de QA que disparan ConfirmedModification* no se ejecutaron, o siempre se mockeó/evitó esa rama.
4. **Seeds/fixtures**: Si en entorno de prueba `colabs` o modelos relacionados tienen atributos extra (p. ej. por mutadores o tablas distintas), podría explicar que algunos tests no detecten el uso de `address`/`people_count`/`observations` en Colab.
5. **Uso de `$colab->date`**: La columna existe en `colabs`, por lo que ese acceso es correcto; no invalida la auditoría para el resto.

---

## Resumen

- **No riesgo real** donde se usa solo `$colab->company` (relación existe) o `$colab->date` (columna existe).
- **Riesgo de dato vacío/inconsistente**: `$colab->address`, `$colab->people_count`, `$colab->observations`, `$colab->time`; `$colab->influencer->instagram_username`; `$colab->company->name`. En la práctica suelen quedar cubiertos por `??` con valores por defecto.
- **Riesgo de crash**: ConfirmedModificationCompanyEmail y ConfirmedModificationInfluencerEmail con `$colab->influencer->user->name` (Influencer no tiene relación `user`); si `$colab->influencer` existe y `->user` es null, el acceso `->user->name` lanza.
- **Fuente canónica** para nombre de empresa: `$colab->offer?->company?->company` (o `company_name`). Para detalle de collab: `$colab->collabable?->...` y `$colab->getDay()`/`getTime()`/`getDate()`. Para Instagram: `$colab->influencer?->socialMedia()->first()?->pivot?->account_name`. Para usuario del influencer: `$colab->influencer?->getUser()`.
