# AUDITORÍA TÉCNICA — Recordatorios y cancelación automática PENDING_COMPANY

**Fecha:** 2025-02  
**Objetivo:** Mapear con evidencias la lógica real de recordatorios 1/2, cancelación automática y envíos (email + push).  
**Actualización:** Fix aplicado (reminder 2 correcto + destinatario email influencer).

---

## PASO 1 — RECORDATORIOS PENDING_COMPANY (EMAIL) — LÓGICA FINAL (POST-FIX)

### 1.1 Dónde se programa y quién llama

| Elemento | Archivo | Líneas | Evidencia |
|----------|---------|--------|-----------|
| Entrada al flujo | `app/Traits/HandlesHistoryTransitions.php` | 126-158 | `handlePendingCompanyState($colab)` solo se ejecuta si `in_array($colab->collabable_type, CollabableConstants::DATABLE_COLLABABLE_TYPES)` |
| Cálculo de tiempos | `HandlesHistoryTransitions.php` | 132-137 | `$colabDate = $colab->getDate();`, `$diffInMinutes = now()->diffInMinutes($colabDate);`, `$fourHours = 4*60`, `$sixHours = 6*60`, `$nineHours = 9*60` |
| Rama >= 9h | `HandlesHistoryTransitions.php` | 139-150 | Reminder 1 con `$sixHours` (6h antes), Reminder 2 con `$fourHours` (4h antes). Sin reutilizar variable entre ambos. |
| Rama < 9h | `HandlesHistoryTransitions.php` | 151-161 | Solo Reminder 1 con `$halfTime = ceil($diffInMinutes/2)`; Reminder 2 no se programa. |

### 1.2 Fórmula literal del código (sendTime) — LÓGICA FINAL

**Recordatorio 1**

- Si `$diffInMinutes >= 9*60`: se programa con `advanceMinutes = 6*60` → **sendTime = getDate() − 6h** (6h antes del evento).
- Si `$diffInMinutes < 9*60`: se programa con `advanceMinutes = ceil($diffInMinutes/2)` → **sendTime = getDate() − halfTime** (mitad del tiempo restante). Solo si `$halfTime > 0`.

**Recordatorio 2**

- Solo si `$diffInMinutes >= 9*60`. Se programa con `advanceMinutes = 4*60` → **sendTime = getDate() − 4h** (4h antes del evento). Nunca depende del reminder 1; nunca queda en now()+4h.

### 1.3 Condiciones para NO programar

- **Recordatorio 1:** si `< 9h` solo se programa con mitad de tiempo; si `$halfTime <= 0` no se programa. Si `>= 9h` siempre se programa (6h antes).
- **Recordatorio 2:** solo si `$diffInMinutes >= 9*60`. Si faltan &lt;9h no se programa (evita cruces).
- **MailingService** (460-477): sendTime = getDate() − advanceMinutes; no hay guard adicional para “en el pasado” en estos métodos.

### 1.4 Swapping mailables

- `schedulePendingColabReminder1CompanyEmail` usa **PendingColabReminder1CompanyEmail** (MailingService.php línea 467).
- `schedulePendingColabReminder2CompanyEmail` usa **PendingColabReminder2CompanyEmail** (MailingService.php línea 477).  
No hay swapping: cada método usa el mailable correcto.

### 1.5 Definición de los mailables

| Mailable | Archivo | Subject (línea) | Vista |
|----------|---------|------------------|-------|
| PendingColabReminder1CompanyEmail | `app/Mails/PendingColabReminder1CompanyEmail.php` | `->subject('Recordatorio: Tienes una colaboración pendiente de aceptación')` (71) | `emails.pending-colab-reminder-company` (70) |
| PendingColabReminder2CompanyEmail | `app/Mails/PendingColabReminder2CompanyEmail.php` | `->subject('⏳ Último recordatorio: Tienes una solicitud de colaboración pendiente')` (70) | `emails.pending-colab-reminder-2-company` (69) |

### 1.6 sendEmailAt y delay

- **MailingService::sendEmailAt** (1230-1256):  
  `$delay = Carbon::now()->diffInSeconds($sendTime);` (1239).  
  Job: `SendEmailJob` con `->delay($delay)` (1240-1242).  
  Idempotencia por `reminder:email:{$emailKeyIndex}:{$collabId}:{$sendTime->timestamp}` (1233).  
  expectedStateId: `COLAB_PENDING_COMPANY_STATE` (467, 477). Si al ejecutar el job la collab ya no está en PENDING_COMPANY, el job no envía (SendEmailJob 77-85).

### 1.7 getDate() y timezone

- **Colab::getDate()** (app/Models/Colab.php 72-78): delega en `$this->collabable->getDateTime()` para RESTAURANT, LODGING, ACTIVITY, DELIVERY.
- Cálculo en HandlesHistoryTransitions: `now()->diffInMinutes($colabDate)`. Si `now()` y `getDate()` usan distinta zona (servidor vs almacenamiento sin zona), el diff puede desviarse. No se usa `startOfDay` en este flujo de recordatorios PENDING_COMPANY.

### 1.8 Constantes

- `HANDLE_PENDING_COMPANY_STATE_COMPANY_EMAIL_KEYS`: `app/Contracts/ScheduledTasksConstants.php` línea 14: `'handlePendingCompanyState_company_email_keys_'`.
- Cancelación de estos emails al aceptar/rechazar/cancelar: `HandlesHistoryTransitions.php` 236 (handleAcceptedState), y vía `cancelScheduledEmails($colab, [...HANDLE_PENDING_COMPANY_STATE_COMPANY_EMAIL_KEYS])` en 375.

---

## PASO 2 — NOTIFICACIONES PUSH EQUIVALENTES

### 2.1 Programación (post-fix)

- **HandlesHistoryTransitions.php**: mismo criterio que emails: si `>= 9h` se programa Reminder 1 con `$sixHours` y Reminder 2 con `$fourHours`; si `< 9h` solo Reminder 1 con `$halfTime`.
- **NotificationService** (1572-1585): sendTime = getDate() − advanceMinutes. Reminder 2 siempre 4h antes del evento.

### 2.2 sendNotificationAt

- **NotificationService::sendNotificationAt** (1861-1885):  
  `$delay = Carbon::now()->diffInSeconds($sendTime);` (1869).  
  Job: `SendScheduledNotificationJob` con `->delay($delay)` (1870).  
  expectedStateId se pasa; en estos métodos se usa `HANDLE_PENDING_COMPANY_STATE_COMPANY_NOTIFICATION_KEYS` y el job valida estado (SendScheduledNotificationJob 69-78).

### 2.3 Caso “mitad del tiempo”

- Cuando `$diffInMinutes < 9*60` se entra en la rama `else`: solo se programa recordatorio 1 (email + push) con `$halfTime = ceil($diffInMinutes/2)`. Reminder 2 no se programa. Sin cruces.

---

## PASO 3 — CANCELACIÓN AUTOMÁTICA PENDING_COMPANY

### 3.1 Comando y schedule

- **Comando:** `nomade:cancel-expired-collaborations`  
  - Clase: `App\Console\Commands\CancelExpiredCollaborations` (app/Console/Commands/CancelExpiredCollaborations.php 13-15).
- **Schedule:** `ScheduleServiceProvider.php` 54-56:  
  `$schedule->command('nomade:cancel-expired-collaborations')->everyMinute()->between('8:00', '22:00');`  
  Frecuencia: cada minuto. Ventana: 8:00–22:00.

### 3.2 Query de colaboraciones “vencidas”

- **CancelExpiredCollaborations::getExpiredCollaborations** (88-109):
  - Filtros: `to_date => Carbon::now()->addHours(2)->format('Y-m-d H:i:s')`, `collabable_type => TYPE1_COLLABABLE_TYPES`, `states => [COLAB_PENDING_NOMADE_STATE, COLAB_PENDING_COMPANY_STATE, COLAB_MODIFICATION_IN_PROGRESS_STATE]`.
  - Llamada: `$this->colabService->get(null, null, $filters, null, false)` (104).
- **ColabService::addToDateFilter** (345-374): para restaurant/delivery/activity usa `addDateByDayTimeFilter($query, $type, '<=', $to_date)` (367); para lodging usa `addFomDayDateFilter($query, '<=', $to_date)` (373).
- **addDateByDayTimeFilter** (699-701): `whereRaw("CONCAT(collabable_".$type.".day, ' ', collabable_".$type.".time) <= '".$date->format('Y-m-d H:i:s')."'")`.
- Por tanto se seleccionan colabs cuya **fecha/hora de la colaboración (day+time o from_day)** sea **<= now()+2h** (no “<= now()”). Es decir, se incluyen colabs con fecha hasta 2 horas en el futuro.

### 3.3 User que llega al listener y rama en handleCancelledState

- **CancelExpiredCollaborations::handle** (63-66): `$this->colabService->pushHistory($colab, $cancelledState)` — no se pasa usuario.
- **ColabService::pushHistory** (226-260): construye `$data` con `user_id => Auth::id() ?? ... admin ...` (232-235) y llama `$model->collabable->attachHistory($state, $data)` (259). No pasa `User` al evento.
- **HasHistoryEvents::attachHistory** (37): `event(new HistoryAttachedEvent($this, $state, $data))` — solo 3 argumentos; el cuarto (user) es null.
- **HistoryAttachedEvent::__construct** (52-57): `$this->user = $user ?? Auth::user();` → en consola `Auth::user()` es null, luego **event->user = null**.
- **HistoryNotifier::handle** (42): `$event->user ?? Auth::user()` → **user = null**.
- **handleCancelledState($colab, $user)** (574-617): con `$user === null` y `prevStateId === COLAB_PENDING_COMPANY_STATE` se entra en el `else` (610-617): **rama “Cancelado automáticamente”**.

### 3.4 Emails y pushes en la rama “auto cancel” (PENDING_COMPANY)

- **HandlesHistoryTransitions.php** 610-617:
  - `sendAutoCanceledColabNomadeEmail($colab)` (612)
  - `sendAutoCanceledColabCompanyEmail($colab)` (614)
  - `sendAutoCanceledColabInfluencerEmail($colab)` (615)
  - `sendAutoCanceledColabCompanyNotification($colab)` (616)
  - `sendAutoCanceledColabInfluencerNotification($colab)` (617)

**Fix aplicado (email influencer):**  
`MailingService::sendAutoCanceledColabInfluencerEmail`: destinatario corregido a `$recipient = $colab->influencer->getUser();` y `$to = $recipient->email`. Company e influencer reciben cada uno su email y su push.

### 3.5 cancelScheduledEmails al cancelar

- En `handleCancelledState` **no** se llama `cancelScheduledEmails` en la rama de cancelación automática (610-617). Los jobs de recordatorios 1 y 2 siguen en cola; al ejecutarse, `SendEmailJob` / `SendScheduledNotificationJob` comprueban `expectedStateId === COLAB_PENDING_COMPANY_STATE` y, al ser el estado actual CANCELLED, **no envían** (SendEmailJob 77-85, SendScheduledNotificationJob 69-78). No hay cancelación explícita de jobs en ese punto.

---

## PASO 4 — CONCLUSIÓN (POST-FIX)

1. **Recordatorios (fix aplicado):**  
   Reminder 1: si faltan ≥9h → 6h antes del evento; si &lt;9h → mitad del tiempo (solo este recordatorio). Reminder 2: solo si faltan ≥9h → 4h antes del evento. Variables independientes; Reminder 2 siempre con `$fourHours`, nunca `$diffInMinutes - $fourHours`. No hay cruces.

2. **Cancelación automática (fix aplicado):**  
   `sendAutoCanceledColabInfluencerEmail` envía al influencer (`$colab->influencer->getUser()->email`). Company e influencer reciben email + push correctamente.

3. **Solo 1 recordatorio cuando aplica:**  
   Si faltan &lt;9h para el evento solo se programa el recordatorio 1 (mitad del tiempo). Los jobs validan `expectedStateId`; si la collab ya no está en PENDING_COMPANY no envían.

---

## Ruta de ejecución resumida

**Recordatorios (al entrar en PENDING_COMPANY):**  
HistoryAttachedEvent (CANCELLED no aplica aquí) → HistoryNotifier::handle → handleStateTransition(PENDING_COMPANY) → handlePendingCompanyState → [cálculo reminderTime] → schedulePendingColabReminder1CompanyEmail / schedulePendingColabReminder1CompanyNotification (y si aplica, schedulePendingColabReminder2*) → MailingService/NotificationService sendEmailAt/sendNotificationAt → SendEmailJob / SendScheduledNotificationJob (delay).

**Cancelación automática:**  
Cron → nomade:cancel-expired-collaborations → ColabService::get(..., to_date: now()+2h, states: PENDING_*) → pushHistory($colab, cancelledState) → attachHistory → HistoryAttachedEvent(model, state, data) [user=null] → HistoryNotifier::handle → handleCancelledState($colab, null) → rama else (610-617) → sendAutoCanceledColab* (email company, email influencer al influencer, push company, push influencer).
