# Premium Chat Visibility Changes — Implementation Log

## Date: 2026-03-10

## Architecture Summary

### Key Models & Relationships
- `Conversation` model: `article_id`, `initiator_id`, `receiver_id`, `premium_chat_payment_id` (nullable FK → payments)
- `isPremiumChatUnlocked()`: returns `premium_chat_payment_id !== null`
- `getObjectOwnerId()`: lost → `article.user_id`; found → the OTHER participant (not publisher)
- `getFinderId()`: lost → the OTHER participant; found → `article.user_id` (publisher)

### Key Files
- Backend Controller: `app/Http/Controllers/App/ChatController.php` (~1542 lines)
- Backend Controller: `app/Http/Controllers/App/PremiumChatController.php` (~210 lines)  
- Backend Model: `app/Models/Conversation.php`
- Backend Model: `app/Models/Payment.php` (type enum includes `premium_chat`)
- Backend Trait: `app/Traits/PushNotificationsManagement.php`
- Backend Routes: `routes/app.php`
- Frontend Chat List: `src/app/views/chat/chat/chat.page.ts`
- Frontend Messages: `src/app/views/chat/messages/messages.page.ts`
- Frontend Premium Lock: `src/app/components/chat/premium-chat-lock/`
- Frontend Chat Service: `src/app/services/api/chat.service.ts`
- Frontend Premium Chat Service: `src/app/services/api/premium-chat.service.ts`

### Notification Types
- Type 3: New conversation started
- Type 4: New chat message
- Type 6: NEW — Premium chat request (for owner on "lost" posts)

### Push Notification Pattern
```php
// Set locale to recipient's language
$_prevLocale = app()->getLocale();
if ($recipient && !empty($recipient->language)) {
    app()->setLocale($recipient->language);
}
$localizedTitle = __('key');
$localizedMessage = __('key');
app()->setLocale($_prevLocale);

$this->sendPushNotificationWithType($recipientId, $title, $message, $type, $conversationId);
```

## Plan

### Backend Changes

1. **`Conversation.php`** — New scope `excludeLockedForFinder($userId)`
   - Exclude conversations where user is the finder AND `premium_chat_payment_id IS NULL`
   - Logic: For each conversation, determine if user is finder based on article type
   - Implementation: subquery joining articles table

2. **`ChatController::inbox()`** — Apply `excludeLockedForFinder` scope to query

3. **`ChatController::showConversation()`** — Return 403 if user is finder AND premium not unlocked

4. **`ChatController::store()`** — After owner sends first free message (premium not unlocked):
   - Send email to finder via new `OwnerFirstMessageMail`
   - Send push notification to finder with message content

5. **`ChatController::startConversation()`** — For `type=lost` articles:
   - Send notification type 6 to owner (object owner = publisher)
   - Return success with feedback flag for frontend toast

6. **New Mail class: `OwnerFirstMessageMail`**
   - Elaborate design with logo, brand colors, CTA
   - Content: owner name, message content, article title, finder name
   - Subject translated to finder's language

7. **New Blade template: `resources/views/emails/owner_first_message.blade.php`**

8. **Translation keys in `lang/*.json`** for all 7 languages (en, es, fr, de, it, pt, ca)

### Frontend Changes

1. **New `PremiumChatOptionsModal`** — Modal for owner on "lost" posts
   - Two options: Pay premium chat / Send free message with text field
   - On completion: navigate to chat

2. **Notification handler** — Type 6 opens `PremiumChatOptionsModal`

3. **`startConversation` flow** — For "lost" posts: show toast after creating conversation

4. **`MessagesPage`** — Handle 403 from `showConversation` (redirect)

5. **i18n translations** — New keys for all languages

## Critical Code References

### Conversation.php — Key methods to reference
```php
// getFinderId() determines the "finder" user
public function getFinderId(): ?int {
    $article = $this->article ?? Article::find($this->article_id);
    if (!$article) return null;
    if ($article->type === 'lost') {
        return ($this->initiator_id === $article->user_id) ? $this->receiver_id : $this->initiator_id;
    }
    return $article->user_id;
}

// getObjectOwnerId() — for "lost": article.user_id; for "found": the other participant
public function getObjectOwnerId(): ?int {
    $article = $this->article ?? Article::find($this->article_id);
    if (!$article) return null;
    if ($article->type === 'lost') return $article->user_id;
    return ($this->initiator_id === $article->user_id) ? $this->receiver_id : $this->initiator_id;
}
```

### ChatController::inbox() — Current query pattern
```php
$conversations = Conversation::forUser($userId)
    ->excludeBlockedFor($userId)
    ->excludeHiddenFor($userId)
    ->with(['messages' => function ($query) {
        $query->orderBy('sent_at', 'desc');
    }])
    ->get();
```

### ChatController::store() — Premium gate pattern (lines ~380-405)
```php
if (!$conversation->isPremiumChatUnlocked()) {
    $objectOwnerId = $conversation->getObjectOwnerId();
    $finderId = $conversation->getFinderId();
    if ((int) $userId === (int) $finderId) {
        return response()->json(['success' => false, 'message' => __('messages.premium_chat_finder_blocked')], 403);
    }
    if ((int) $userId === (int) $objectOwnerId) {
        $ownerMessageCount = $conversation->messages()->where('sender_id', $objectOwnerId)->count();
        if ($ownerMessageCount >= 1) {
            return response()->json(['success' => false, 'message' => __('messages.premium_chat_owner_limit')], 403);
        }
    }
}
```

### ChatController::showConversation() — Response structure (lines ~1345-1460)
Returns JSON with: conversation_id, article_id, is_payer, is_blocked, is_refunded, 
premium_chat_unlocked, premium_chat_amount, is_object_owner, contact{}, messages[]

### ChatController::startConversation() — Push notification pattern
```php
$receiver = User::find($receiverId);
$_prevLocale = app()->getLocale();
if ($receiver && !empty($receiver->language)) {
    app()->setLocale($receiver->language);
}
$this->sendPushNotificationWithType($receiverId, $receiver->name ?? '', __('messages.conversation_has_been_started'), 3, $conversation->id);
app()->setLocale($_prevLocale);
```

### Existing Mail classes pattern
- `OperationValidatedEmail` — takes `$payload` array, renders `emails.operation_validated` view
- `InvoiceEmail` — takes `$emailData`, optional `$pdfPath`, `$invoiceNumber`

### Frontend paths
- Chat list: `Foundek_app/src/app/views/chat/chat/chat.page.ts` (729 lines)
- Messages: `Foundek_app/src/app/views/chat/messages/messages.page.ts` (~1300+ lines)
- Premium lock: `Foundek_app/src/app/components/chat/premium-chat-lock/`
- Chat service: `Foundek_app/src/app/services/api/chat.service.ts`
- Premium chat service: `Foundek_app/src/app/services/api/premium-chat.service.ts`
- Notification API service: `Foundek_app/src/app/services/api/notification.service.ts`
- i18n: `Foundek_app/src/assets/i18n/es.json` (and en, fr, de, it, pt, ca)
- App routes: `Foundek_app/src/app/app.routes.ts`

### Frontend messages.page.ts key state
- `premiumChatUnlocked`, `isObjectOwner`, `premiumChatAmount`
- `ownerFreeMsgConsumed`, `ownerFreeMessage`
- `onPremiumChatUnlocked()` — reloads chat
- `onDirectClose()` — marks exchange closed
- Template: `<app-premium-chat-lock>` shows when `!premiumChatUnlocked && !isExchangeClosed`

### Frontend chat.page.ts key patterns
- `goToChat(id)` → mobile: `router.navigate(['/app/messages', id])`, web: sets `selectedId` signal
- `updateInbox()` → calls `chatService.getInbox()`, maps results to `chats` signal
- Pusher channels: `MessageSent.{userId}`, `ConversationsListUpdated.{userId}`

### Existing test files
- Backend: `tests/` directory with Feature and Unit folders
- Frontend: `*.spec.ts` files alongside components

### Languages supported
en, es, fr, de, it, pt, ca (7 languages total)

### User decisions (confirmed)
1. Finder gets feedback: Toast "Se ha notificado al propietario" (for lost posts)
2. Owner writes free message: From a MODAL/screen (NOT from the chat directly)
3. Email design: Elaborate with logo, brand colors, CTA
4. Notification → opens chat AFTER the owner writes/pays (both options create chat if it doesn't exist)

## Implementation Progress

- [x] Backend: Tests written (PremiumChatVisibilityTest.php — RED phase)
- [x] Backend: Existing PremiumChatTest.php tests corrected (owner↔finder roles fixed)
- [x] Backend: Conversation scope `excludeLockedForFinder` — DONE in Conversation.php
- [x] Backend: inbox() filter — DONE, added ->excludeLockedForFinder($userId)
- [x] Backend: showConversation() 403 — DONE, added finder-blocked check
- [x] Backend: store() email/push — DONE, added $isOwnerFirstFreeMessage flag + email/push logic
- [x] Backend: startConversation() — DONE, added type 6 notification for lost + owner_notified in response
- [x] Backend: OwnerFirstMessageMail class — DONE (app/Mail/OwnerFirstMessageMail.php)
- [x] Backend: Blade template — DONE (resources/views/emails/owner_first_message.blade.php)
- [~] Backend: Translation keys (7 langs) — Script created at scripts/add_translations.py, NEED TO RUN IT
- [ ] Backend: Run tests and fix any issues
- [ ] Frontend: PremiumChatOptionsModal — NEW modal for owner on "lost" posts
- [ ] Frontend: Notification handler type 6 — open modal on type 6 notification
- [ ] Frontend: startConversation toast — show toast after creating conversation for lost posts
- [ ] Frontend: MessagesPage 403 handling — redirect on 403 from showConversation
- [ ] Frontend: i18n translations — add keys to all 7 language files
- [ ] Frontend: Build validation

## COMPLETED BACKEND CHANGES SUMMARY

### Files created:
- `tests/Feature/PremiumChatVisibilityTest.php` — new test file with scope, inbox, showConversation, store, startConversation tests
- `app/Mail/OwnerFirstMessageMail.php` — new Mailable class
- `resources/views/emails/owner_first_message.blade.php` — email template with brand design
- `scripts/add_translations.py` — script to add translation keys to all 7 lang/*.json files

### Files modified:
- `app/Models/Conversation.php` — added `scopeExcludeLockedForFinder($query, $userId)` after `scopeExcludeHiddenFor`
- `app/Http/Controllers/App/ChatController.php`:
  - `inbox()` line ~257: added `->excludeLockedForFinder($userId)` to query chain
  - `showConversation()` line ~1362: added 403 check for finder when premium locked
  - `store()` line ~405: added `$isOwnerFirstFreeMessage = false;` flag, set to true inside owner block when count === 0
  - `store()` line ~510: added email+push to finder after message saved when $isOwnerFirstFreeMessage
  - `startConversation()` line ~144: added type 6 notification for lost articles to owner, added `owner_notified` to response JSON
- `tests/Feature/PremiumChatTest.php` — corrected test names and assertions to match actual code behavior (owner can send 1, finder blocked)

## REMAINING WORK DETAILS

### Step 1: Run translation script
```bash
cd c:\Users\eloym\Desktop\Proyectos\Foundek_backend
python scripts/add_translations.py
```

### Step 2: Run backend tests
```bash
cd c:\Users\eloym\Desktop\Proyectos\Foundek_backend
php artisan test --filter=PremiumChatVisibilityTest
php artisan test --filter=PremiumChatTest
```

### Step 3: Frontend changes needed

#### 3a. MessagesPage 403 handling
File: `Foundek_app/src/app/views/chat/messages/messages.page.ts`
In the loadConversation() method, add error handler for 403:
```typescript
// When chatService.showConversation() returns 403, navigate back
if (error.status === 403) {
  this.router.navigate(['/app/chats']);
  // Optionally show toast
}
```

#### 3b. StartConversation toast for lost posts
File: Where startConversation is called (likely a post detail page)
After calling chatService.startConversation(), check `response.owner_notified`:
```typescript
if (response.owner_notified) {
  // Show toast: "Se ha notificado al propietario"
  // Do NOT navigate to chat (finder can't see it)
} else {
  // Navigate to chat as usual (found scenario)
}
```

#### 3c. PremiumChatOptionsModal
Create: `Foundek_app/src/app/components/chat/premium-chat-options-modal/`
- premium-chat-options-modal.component.ts
- premium-chat-options-modal.component.html
- premium-chat-options-modal.component.scss
Purpose: For owner on "lost" posts — shows two options:
1. "Pagar chat premium" → triggers existing premium payment flow → navigates to full chat
2. "Enviar mensaje gratis" → text input + send → navigates to locked chat
Modal receives: conversationId, articleId, premiumChatAmount

#### 3d. Notification handler type 6
File: Wherever push notifications are handled (check app.component.ts or notification service)
When notification type === 6, navigate to chat with conversationId from notification data.
The chat page will show PremiumChatLockComponent with pay/send options.

#### 3e. i18n translations needed (frontend - src/assets/i18n/)
Keys to add in all 7 language JSON files:
- CHAT.OWNER_NOTIFIED_TOAST: "Se ha notificado al propietario. Recibirás una notificación cuando se desbloquee el chat."
- CHAT.PREMIUM_OPTIONS_TITLE: "Opciones de Chat Premium"
- CHAT.PREMIUM_OPTIONS_PAY: "Desbloquear Chat Premium"
- CHAT.PREMIUM_OPTIONS_FREE_MSG: "Enviar mensaje gratuito"
- CHAT.PREMIUM_OPTIONS_FREE_MSG_PLACEHOLDER: "Escribe tu mensaje..."
- CHAT.PREMIUM_OPTIONS_SEND: "Enviar"
- CHAT.FINDER_BLOCKED_TOAST: "No puedes acceder a esta conversación hasta que el propietario desbloquee el chat."

### Key frontend files to read/modify:
- `Foundek_app/src/app/views/chat/messages/messages.page.ts` — add 403 handling
- `Foundek_app/src/app/services/api/chat.service.ts` — check startConversation return type
- `Foundek_app/src/app/views/chat/chat/chat.page.ts` — chat list, check if anything needs adjusting
- `Foundek_app/src/app/components/chat/premium-chat-lock/` — existing component, may need to integrate modal option
- `Foundek_app/src/app/app.component.ts` or notification service — handle type 6
- Look for where startConversation() is called in the UI (post detail pages)

## CRITICAL IMPLEMENTATION NOTES

### Scope excludeLockedForFinder — exact code to add to Conversation.php
User is "finder" when:
- Article type=lost AND user is NOT article.user_id
- Article type=found AND user IS article.user_id
Logic: KEEP conversations where premium is unlocked OR user is the owner (not finder)

### ChatController::inbox() — exact change
Add `->excludeLockedForFinder($userId)` after `->excludeHiddenFor($userId)` in the query chain.

### ChatController::showConversation() — exact change
After finding the conversation (~line 1360), before mapping messages, add:
```php
if (!$conversation->isPremiumChatUnlocked()) {
    $finderId = $conversation->getFinderId();
    if ((int) $userId === (int) $finderId) {
        return response()->json(['success' => false, 'message' => __('messages.finder_chat_hidden')], 403);
    }
}
```

### ChatController::store() — exact change
After the existing owner message count check (after line ~430, before "Determinar quién es el otro usuario"), add:
```php
// Flag: owner is sending first free message (premium not yet unlocked)
$isOwnerFirstFreeMessage = !$conversation->isPremiumChatUnlocked() 
    && (int) $userId === (int) $objectOwnerId 
    && $ownerMessageCount === 0;
```
Then after the message is saved and broadcast (after line ~470), add:
```php
if (isset($isOwnerFirstFreeMessage) && $isOwnerFirstFreeMessage) {
    $finderId = $conversation->getFinderId();
    $finderUser = User::find($finderId);
    if ($finderUser && $finderUser->email) {
        // Send email
        $_prevLocale = app()->getLocale();
        if (!empty($finderUser->language)) app()->setLocale($finderUser->language);
        $payload = [
            'ownerName' => $userEmit->name,
            'finderName' => $finderUser->name,
            'articleTitle' => $conversation->article?->title ?? '',
            'messageContent' => $request->message,
            'subject' => __('emails.owner_first_message.subject'),
        ];
        app()->setLocale($_prevLocale);
        try {
            Mail::to($finderUser->email)->send(new \App\Mail\OwnerFirstMessageMail($payload));
        } catch (\Throwable $e) {
            Log::warning('Could not send OwnerFirstMessageMail', ['error' => $e->getMessage()]);
        }
        // Send push
        $this->sendPushNotificationWithType($finderId, $userEmit->name, $request->message, 4, $conversation->id);
    }
}
```
NOTE: Need to move $isOwnerFirstFreeMessage flag BEFORE the premium gate, and set it inside the owner block.

### ChatController::startConversation() — exact change  
After the existing push notification block (~line 140), add for lost articles:
```php
// For lost articles: notify the owner (publisher) with type 6
if ($article->type === 'lost') {
    $ownerId = $article->user_id;
    $ownerUser = User::find($ownerId);
    $_prevLocale2 = app()->getLocale();
    if ($ownerUser && !empty($ownerUser->language)) app()->setLocale($ownerUser->language);
    $title6 = __('notifications.premium_chat_request_title');
    $message6 = __('notifications.premium_chat_request_message', ['title' => $article->title]);
    app()->setLocale($_prevLocale2);
    $this->sendPushNotificationWithType($ownerId, $title6, $message6, 6, $conversation->id);
}
```
Also add `'owner_notified' => $article->type === 'lost'` to the response JSON.

### Route paths (prefix /app/)
- POST /app/conversations/start → startConversation
- GET /app/getInbox → inbox
- POST /app/conversations/{id}/messages → store
- GET /app/conversations/{id} → showConversation

### Mail classes follow this pattern (see OperationValidatedEmail):
```php
class OwnerFirstMessageMail extends Mailable {
    use Queueable, SerializesModels;
    public array $payload;
    public function __construct(array $payload) { $this->payload = $payload; }
    public function build(): self {
        return $this->subject($this->payload['subject'] ?? 'New message')
            ->view('emails.owner_first_message')->with($this->payload);
    }
}
```

### Translation keys needed (all 7 languages: en, es, fr, de, it, pt, ca):
- messages.finder_chat_hidden
- emails.owner_first_message.subject
- emails.owner_first_message.greeting
- emails.owner_first_message.intro
- emails.owner_first_message.message_label
- emails.owner_first_message.article_label
- emails.owner_first_message.closing
- notifications.premium_chat_request_title
- notifications.premium_chat_request_message

## EXACT CODE TO COPY — Conversation.php scope (add after scopeExcludeHiddenFor method around line 200)

```php
/**
 * Scope: excluir conversaciones bloqueadas para el finder.
 * El finder NO ve la conversación hasta que premium_chat_payment_id sea asignado.
 * Mantiene conversaciones donde: premium desbloqueado OR usuario es el owner (no finder).
 */
public function scopeExcludeLockedForFinder($query, $userId)
{
    if (!$userId) {
        return $query;
    }

    return $query->where(function ($q) use ($userId) {
        // Mantener si premium ya está desbloqueado
        $q->whereNotNull('premium_chat_payment_id')
        // O si el usuario es el object owner (no el finder)
        ->orWhere(function ($q2) use ($userId) {
            $q2->whereHas('article', function ($aq) use ($userId) {
                $aq->where(function ($innerQ) use ($userId) {
                    // lost: owner = article.user_id → si user ES article.user_id, es owner
                    $innerQ->where('type', 'lost')->where('user_id', $userId);
                })->orWhere(function ($innerQ) use ($userId) {
                    // found: owner = el otro participante → si user NO es article.user_id, es owner
                    $innerQ->where('type', 'found')->where('user_id', '!=', $userId);
                });
            });
        });
    });
}
```

## EXACT LINE REFERENCES FOR ChatController.php EDITS

### inbox() — line ~257 (find "->excludeHiddenFor($userId)")
Change:
```php
->excludeHiddenFor($userId)
```
To:
```php
->excludeHiddenFor($userId)
->excludeLockedForFinder($userId)
```

### showConversation() — line ~1362 (after finding conversation, before "$contactId = ...")
Insert BEFORE the line "$contactId = ($conversation->initiator_id == $userId)":
```php
// ── Premium Chat: finder no puede acceder si premium no está desbloqueado ──
if (!$conversation->isPremiumChatUnlocked()) {
    $finderId = $conversation->getFinderId();
    if ((int) $userId === (int) $finderId) {
        return response()->json([
            'success' => false,
            'message' => __('messages.finder_chat_hidden'),
        ], 403);
    }
}
```

### store() — line ~420 (inside the objectOwnerId if block, after $ownerMessageCount is calculated)
Inside the block `if ((int) $userId === (int) $objectOwnerId)`, after `$ownerMessageCount` line, ADD before the `if ($ownerMessageCount >= 1)` check:
```php
// Flag para enviar email/push al finder tras el primer mensaje gratis del owner
$isOwnerFirstFreeMessage = ($ownerMessageCount === 0);
```
Then AFTER the message is saved and broadcast events are fired (after the push notification block around line ~490), ADD:
```php
// ── Enviar email + push al finder cuando el owner envía su primer mensaje gratis ──
if (isset($isOwnerFirstFreeMessage) && $isOwnerFirstFreeMessage) {
    try {
        $finderId = $conversation->getFinderId();
        $finderUser = User::find($finderId);
        if ($finderUser && $finderUser->email) {
            $_prevLocale = app()->getLocale();
            if (!empty($finderUser->language)) {
                app()->setLocale($finderUser->language);
            }

            $emailPayload = [
                'ownerName' => $userEmit->name ?? '',
                'finderName' => $finderUser->name ?? '',
                'articleTitle' => $conversation->article?->title ?? '',
                'messageContent' => $request->message,
                'subject' => __('emails.owner_first_message.subject'),
            ];

            app()->setLocale($_prevLocale);

            Mail::to($finderUser->email)->send(new \App\Mail\OwnerFirstMessageMail($emailPayload));

            // Push notification al finder con el contenido del mensaje
            $this->sendPushNotificationWithType(
                $finderId,
                $userEmit->name ?? '',
                $request->message,
                4,
                $conversation->id
            );
        }
    } catch (\Throwable $e) {
        Log::warning('Error sending owner first message notification to finder', [
            'error' => $e->getMessage(),
            'conversation_id' => $conversation->id,
        ]);
    }
}
```

### startConversation() — line ~142 (after "app()->setLocale($_prevLocale);" and before closing "}" of the isNew block)
Add BEFORE the closing `}` of the `if ($isNew)` block:
```php
// Para artículos "lost": notificar al owner (publisher) con tipo 6
if ($article->type === 'lost') {
    try {
        $ownerUser = User::find($article->user_id);
        $_prevLocale2 = app()->getLocale();
        if ($ownerUser && !empty($ownerUser->language)) {
            app()->setLocale($ownerUser->language);
        }
        $title6 = __('notifications.premium_chat_request_title');
        $message6 = __('notifications.premium_chat_request_message', ['title' => $article->title]);
        app()->setLocale($_prevLocale2);

        $this->sendPushNotificationWithType(
            $article->user_id,
            $title6,
            $message6,
            6,
            $conversation->id
        );
    } catch (\Throwable $e) {
        Log::warning('Error sending type 6 notification to owner', [
            'error' => $e->getMessage(),
            'article_id' => $article->id,
        ]);
    }
}
```

And change the response JSON from:
```php
return response()->json([
    'success' => true,
    'conversation_id' => $conversation->id,
    'is_new' => $isNew,
], 200);
```
To:
```php
return response()->json([
    'success' => true,
    'conversation_id' => $conversation->id,
    'is_new' => $isNew,
    'owner_notified' => $isNew && $article->type === 'lost',
], 200);
```

## FILES TO CREATE

### app/Mail/OwnerFirstMessageMail.php
(Follows OperationValidatedEmail pattern)

### resources/views/emails/owner_first_message.blade.php
(Email elaborado con logo, marca, CTA — similar a operation_validated.blade.php)

## FRONTEND CHANGES NEEDED

### Files to modify:
1. `Foundek_app/src/app/views/chat/messages/messages.page.ts` — handleError on loadConversation (403 → navigate back)
2. `Foundek_app/src/app/services/api/chat.service.ts` — startConversation return type may need owner_notified
3. Create new modal: `Foundek_app/src/app/components/chat/premium-chat-options-modal/`
4. `Foundek_app/src/app/views/chat/chat/chat.page.ts` or notification handler — type 6 opens modal
5. i18n files (all 7 languages): add new keys for toast, modal texts, etc.

### Frontend notification handling path:
- Look for notification type handling in the app (likely in a notification service or app.component.ts)
- Type 6 should navigate to the chat with conversation_id from notification data

## FRONTEND IMPLEMENTATION STATUS

### COMPLETED:
- [x] messages.page.ts: Added 403 handling in both catchError and error callback
  - catchError: re-throws 403 so error handler catches it
  - error handler: checks err.status === 403, shows toast with CHAT.FINDER_BLOCKED_403, navigates to /app/chats
  
### IN PROGRESS (details.page.ts startConversation toast):
The current code at lines 346-370 of details.page.ts is:
```typescript
this.chatService.startConversation(this.articleId, receiverId).subscribe({
  next: (data) => {
    loading.dismiss();
    if (data.success && data.conversation_id) {
      this.conversationId = data.conversation_id;
      this.navigate('/app/messages/' + data.conversation_id);
    } else {
      this.presentToast({
        message: data.msg || this.translateService.getItemTranslated('DETAILS.CONTACT_ERROR'),
        color: 'danger',
        duration: 3000,
      });
    }
  },
```
CHANGE NEEDED: After `if (data.success && data.conversation_id)`, check `data.owner_notified`:
- If true: show toast "Se ha notificado al propietario" and DO NOT navigate to chat
- If false: navigate as usual

### REMAINING:
- [x] details.page.ts: Added owner_notified check after startConversation — DONE
- [ ] notifications.page.ts line ~445: Same owner_notified check — search for startConversation in this file and add same pattern
- [ ] web-notifications.component.ts line ~307: Same owner_notified check
- [ ] app.component.ts: Add type '6' in notification handler blocks
  - Look for existing type '4' handling and add '6' with same navigation behavior to /app/messages/{id}
  - Three blocks: pushNotificationReceived (foreground), notificationActionPerformed (tap), localNotificationActionPerformed
  - In each, when type==='6', navigate to /app/messages/{id} where id comes from notification data
- [ ] i18n keys in 7 frontend languages

### NOTIFICATIONS.PAGE.TS — EXACT CODE TO CHANGE (lines 448-465):
Current code:
```typescript
        next: (result: any) => {
          loading.dismiss();
          if (result?.success && result?.conversation_id) {
            // Delete originating notification to avoid duplicates
            const notifId = this.notificationSelected?.id;
```
Change to:
```typescript
        next: (result: any) => {
          loading.dismiss();
          if (result?.success && result?.conversation_id) {
            // Para artículos "perdido": el finder recibe toast, NO navega al chat
            if (result.owner_notified) {
              this.presentToast(
                this.translateService.getItemTranslated('CHAT.OWNER_NOTIFIED_TOAST')
                || 'Se ha notificado al propietario. Recibirás una notificación cuando se desbloquee el chat.'
              );
              this.closeDialog();
              return;
            }
            // Delete originating notification to avoid duplicates
            const notifId = this.notificationSelected?.id;
```
IMPORTANT: The toast method in notifications.page.ts uses `this.presentToast(msg)` with just a string, not an object.

### WEB-NOTIFICATIONS — SEARCH PATTERN:
File path: search for web-notifications.component.ts in Foundek_app/src/app
Look for startConversation call and add same owner_notified check.

### APP.COMPONENT.TS — TYPE 6 HANDLING:
File: c:\Users\eloym\Desktop\Proyectos\Foundek_app\src\app\app.component.ts
Search for existing `type.*4` or `'4'` handling in three blocks:
1. Foreground push (pushNotificationReceived): type '6' → same as type '4' (suppress local if in chat)
2. Tap handler (notificationActionPerformed): type '6' → navigate to /app/messages/{id}
3. Local notification tap (localNotificationActionPerformed): type '6' → navigate to /app/messages/{id}
In each block, add `|| type === '6'` alongside the `type === '4'` condition.

### FRONTEND I18N KEYS TO ADD — ALL 7 LANGUAGES:
es: CHAT.FINDER_BLOCKED_403 = "No puedes acceder a esta conversación hasta que el propietario desbloquee el chat."
es: CHAT.OWNER_NOTIFIED_TOAST = "Se ha notificado al propietario. Recibirás una notificación cuando se desbloquee el chat."
en: CHAT.FINDER_BLOCKED_403 = "You cannot access this conversation until the owner unlocks premium chat."
en: CHAT.OWNER_NOTIFIED_TOAST = "The owner has been notified. You will receive a notification when the chat is unlocked."
fr: CHAT.FINDER_BLOCKED_403 = "Vous ne pouvez pas accéder à cette conversation tant que le propriétaire n'a pas débloqué le chat premium."
fr: CHAT.OWNER_NOTIFIED_TOAST = "Le propriétaire a été notifié. Vous recevrez une notification lorsque le chat sera débloqué."
de: CHAT.FINDER_BLOCKED_403 = "Sie können auf dieses Gespräch erst zugreifen, wenn der Eigentümer den Premium-Chat freigeschaltet hat."
de: CHAT.OWNER_NOTIFIED_TOAST = "Der Eigentümer wurde benachrichtigt. Sie erhalten eine Benachrichtigung, wenn der Chat freigeschaltet wird."
it: CHAT.FINDER_BLOCKED_403 = "Non puoi accedere a questa conversazione finché il proprietario non sblocca la chat premium."
it: CHAT.OWNER_NOTIFIED_TOAST = "Il proprietario è stato notificato. Riceverai una notifica quando la chat sarà sbloccata."
pt: CHAT.FINDER_BLOCKED_403 = "Não pode aceder a esta conversa até que o proprietário desbloqueie o chat premium."
pt: CHAT.OWNER_NOTIFIED_TOAST = "O proprietário foi notificado. Receberá uma notificação quando o chat estiver desbloqueado."
ca: CHAT.FINDER_BLOCKED_403 = "No pots accedir a aquesta conversa fins que el propietari desbloquegi el xat premium."
ca: CHAT.OWNER_NOTIFIED_TOAST = "S'ha notificat al propietari. Rebràs una notificació quan es desbloquegi el xat."

### I18N FILE CHECK:
The frontend i18n uses FLAT keys in a JSON object. The "CHAT." prefix means these keys are nested inside a "CHAT" object.
Need to verify the exact i18n structure before adding keys. Check if CHAT is a nested object or if keys use dot notation.
Path: c:\Users\eloym\Desktop\Proyectos\Foundek_app\src\assets\i18n\es.json

### OVERALL STATUS:
Backend: 100% complete
Frontend: ~50% complete
- [x] messages.page.ts 403 handling
- [x] details.page.ts owner_notified check
- [ ] notifications.page.ts owner_notified check (code determined above)
- [ ] web-notifications.component.ts owner_notified check
- [ ] app.component.ts type '6' notification handling (3 blocks)
- [ ] i18n keys (7 frontend languages)
- [ ] Build validation

### messages.page.ts (1408 lines)
- `loadChat()` method at lines 407-575: calls `chatService.loadChat(customerId)` with 10s timeout
- catchError block returns empty fallback — NO 403 handling exists yet
- error callback (lines ~557-575) handles "no query results" 500s and user-deleted
- Premium state props: premiumChatUnlocked, isObjectOwner, premiumChatAmount, ownerFreeMsgConsumed, ownerFreeMessage
- `onPremiumChatUnlocked()` at line 666: reloads chat
- `onDirectClose()` at line 674: calls exchangeService.directClose()

### chat.service.ts (168 lines)
- `startConversation(articleId, receiverId?)` at line 157: POST /conversations/start, returns Observable<any>
- `loadChat(customerId)` / `getChat(customerId)`: GET /conversations/{id}
- `getInbox()`: GET /getInbox

### premium-chat-lock.component.ts (108 lines)
- Inputs: conversationId, isObjectOwner, premiumChatAmount, currentUserId, ownerFreeMessage
- Outputs: premiumChatUnlocked (emits on payment success), directClose (emits when owner skips)
- Owner view: amount + Stripe pay button + direct close button
- Finder view: waiting for payment chip

### startConversation callers (3 UI locations):
1. notifications.page.ts line 445 — from notification dialog, navigates to messages
2. details.page.ts line 346 — from article detail, navigates on success
3. web-notifications.component.ts line 307 — same as notifications but for web

### app.component.ts notification handling (lines 650-760):
- Push received foreground:
  - Type '4' (chat): suppresses local notif if in chat, updates count (line 667)
  - Type '3' (general): updates notification count (line 680)
- Tap handler (notificationActionPerformed):
  - Type '3': navigate to /app/notifications (line 700)
  - Type '4': navigate to /app/messages/{id} (line 703)
- Local push tap (localNotificationActionPerformed) line 733: same type 3→notifs, type 4→messages
- NO type '6' handling exists

### es.json i18n structure (1016 lines):
- CHAT block: lines 133-190
- PREMIUM_CHAT block: lines 999-1016 (end of file)
- DETAILS block: lines ~260-293
- Existing PREMIUM_CHAT keys: TITLE, DESCRIPTION_OWNER, DESCRIPTION_FINDER, PAY_BUTTON, AMOUNT_LABEL, PROCESSING, PAYMENT_SUCCESS, ERROR, WAITING_FOR_PAYMENT, FINDER_LIMIT, OWNER_FREE_MSG_LABEL, FINDER_MSG_FROM_OWNER, OWNER_LIMIT, DIRECT_CLOSE_BUTTON

### Changes needed:
1. messages.page.ts: Add 403 handling in loadChat() error → navigate to /app/chats + show toast
2. details.page.ts line 346: After startConversation response, check owner_notified → show toast, don't navigate
3. notifications.page.ts line 445: Same owner_notified check
4. web-notifications.component.ts line 307: Same owner_notified check
5. app.component.ts: Add type '6' handling in all 3 notification listener blocks → navigate to /app/messages/{id}
6. Create PremiumChatOptionsModal component
7. Add i18n keys to CHAT and PREMIUM_CHAT blocks in all 7 languages

---

## Implementation Status — COMPLETED (2026-03-10)

### Backend (100%)
All changes implemented and tested:
- `Conversation::scopeExcludeLockedForFinder()` — hides locked premium conversations from finder's inbox
- `ChatController::inbox()` — applies scope
- `ChatController::showConversation()` — returns 403 for finder when premium locked
- `ChatController::store()` — sends `OwnerFirstMessageMail` + push notification on owner's first free message
- `ChatController::startConversation()` — sends type 6 notification to owner for lost articles, returns `owner_notified` flag
- `OwnerFirstMessageMail` + Blade template created
- Translation keys added to all 7 backend languages (es, en, fr, de, it, pt, ca)

### Backend Tests (100% — 43 passing)
- `PremiumChatVisibilityTest`: 18 tests, 27 assertions — ALL PASS
- `PremiumChatTest`: 14 tests, 24 assertions — ALL PASS (no regressions)
- `StartConversationTest`: 11 tests, 32 assertions — ALL PASS (no regressions)

### Frontend (100%)
- `messages.page.ts`: 403 error handling (toast + redirect to /app/chats)
- `details.page.ts`: `owner_notified` check after startConversation (toast, no chat navigation)
- `notifications.page.ts`: `owner_notified` check (toast, close dialog, no navigation) + `TranslateConfigService` injected
- `web-notifications.component.ts`: `owner_notified` check (toast, hide dialog, no navigation) + `TranslateConfigService` injected
- `app.component.ts`: Type '6' notification handling in all 3 blocks (foreground, tap, local push)
- i18n keys (`CHAT.FINDER_BLOCKED_403`, `CHAT.OWNER_NOTIFIED_TOAST`) added to all 7 frontend languages

### Frontend Build
- TypeScript compilation: `npx tsc --noEmit --project tsconfig.app.json` — NO ERRORS

### Pending / Deferred
- `PremiumChatOptionsModal` (new modal component for owner on lost posts) — deferred; existing `PremiumChatLockComponent` may suffice
