# External Integrations

## Rentals United API (`app/Services/RentalsUnitedApiService.php`)

- **Protocol:** XML-based (not JSON). Use `app/Helpers/XmlToArray.php` and `app/Helpers/ArrayToXml.php`.
- **Auth:** API token stored in `api_tokens` table, per user.
- **Sync direction:**
  - Pull: cron jobs → `app:sync-properties-with-rentals` and `app:sync-reservations-with-rentals`
  - Push: webhook at `POST /rlnm/rentals` → `Api/ReservationController`
- **Payload building:** Always use `RentalsPayloadBuilder` service. Never construct XML directly in controllers.
- **CRITICAL:** `rentals_subuser_id` (Rentals United's ID) ≠ `co_host_id` (internal ID). Never confuse them.

### If Rentals United sync breaks:
1. Check `storage/logs/laravel.log` for the exact XML response.
2. Verify `api_tokens` table has a valid token for the affected user.
3. Do NOT re-run the sync command blindly — identify the failing payload first.
4. Use `RentalsUnitedApiService` directly in `tinker` to test in isolation.

### RU messaging — co-host resolution contract

All messaging code paths (`MessagingHistorySyncService`, `MessagingWebhookController`, sync commands) MUST resolve the owning co-host through `App\Services\Messaging\CoHostResolver`. Do not write ad-hoc resolution logic; extend the resolver instead.

Five ordered strategies (first match wins):
1. **ReservationProperty** — `Reservation → Property → users` (handles both `properties.co_host_id` = `users.id` and = `users.rentals_subuser_id`).
2. **RuSubuserId** — `Sender.ID` / `Receiver.ID` / `Contacts[].ID` ↔ `users.rentals_subuser_id`.
3. **ContactName** — exact name match; ambiguous matches return null and fall through.
4. **InboxOwner** — used ONLY when the payload carries no contradicting signal (anti-leak guard). If the payload contains an unmatched RU ID or candidate name, this strategy is skipped to prevent attributing the discussion to the wrong tenant.
5. **PrimaryFallback** — `env('RENTALS_UNITED_USERNAME')`, logged at WARN level.

Logs go to the `messaging_sync` channel: `[CoHostResolver] strategy={X} resolved=Y context={...}`. A high rate of `PrimaryFallback` indicates a gap in strategies 1–4.

Cross-tenant guard: `Chat::upsertFromApiMessage($msg, $discussionId, ?int $expectedCoHostId)` blocks attaches when `$expectedCoHostId` is provided and the discussion's `co_host_id` differs.

---

## Twilio SDK

- **Purpose:** Outbound SMS/WhatsApp to guests only. Never for internal team messages.
- **Config:** `TWILIO_SID`, `TWILIO_TOKEN`, `TWILIO_FROM` in `.env`.
- **Templates:** `app/Constants/WhatsAppTemplates.php`

---

## Email (`app/Mail/`, `app/Services/SendMailService.php`)

- **Always use `SendMailService`** — never call `Mail::send()` directly in controllers.
- **7 Mailable classes:** `ContractLinkMail`, `AssuranceMail`, `QuizMail`, `UrgentMail`, `OwnerMail`, `RendezVousMail`, `ConfirmationMail`
- **Templates:** Modern Blade in `resources/views/emails/`, legacy in `resources/views/mail/`
- **Queued sends:** Large batches use `SendNewsletterEmailJob` / `SendQuizEmailJob` — never send synchronously in bulk.
