Skip to content

What you'll learn: How to work with the messaging system, extend it with new features, integrate Vertex AI, and add custom reminder types.

Prerequisites

  • Familiarity with Laravel events and listeners
  • Basic understanding of polymorphic relationships
  • Knowledge of queue workers and scheduled tasks
  • Access to Google Cloud Platform for AI features

Working with Messages

Creating Messages Programmatically

Use the MessageService for all message creation to ensure proper threading, authorization, and event dispatching:

php
use Modules\Messages\Services\MessageService;

public function __construct(private MessageService $messageService) {}

public function sendInquiry(MessageThread $thread, User $sender, string $body): Message
{
    return $this->messageService->sendMessage($thread, $sender, $body);
}

Why use the service?

  • Handles thread creation/assignment automatically
  • Dispatches events for reminder scheduling
  • Validates sender/recipient permissions
  • Maintains conversation integrity

Listening to Message Events

Subscribe to message events to build custom integrations:

php
use Modules\Messages\Events\ParticipantRespondedToThread;
use Illuminate\Support\Facades\Event;

Event::listen(ParticipantRespondedToThread::class, function ($event) {
    $thread = $event->thread;
    $sender = $event->sender;
    $senderType = $event->senderType; // 'vendor' or 'couple'

    // Your custom logic
});

Key event:

  • ParticipantRespondedToThread: Fired when any participant sends a message

Extending Message Model

Add custom scopes or methods to the Message model:

php
// modules/Message/Models/Message.php

public function scopeUnreadByUser(Builder $query, User $user): Builder
{
    return $query->where('recipient_id', $user->id)
        ->where('recipient_type', User::class)
        ->whereNull('read_at');
}

public function markAsUrgent(): void
{
    $this->update(['priority' => 'urgent']);
    $this->reminders()->update(['scheduled_for' => now()->addHours(1)]);
}

Integrating Vertex AI

Configuring AI Service

The VertexAiScanningService handles all AI interactions. Configuration lives in modules/Messages/Config/config.php:

php
'vertex_ai' => [
    'enabled' => env('VERTEX_AI_ENABLED', true),
    'project_id' => env('GOOGLE_CLOUD_PROJECT'),
    'location' => env('VERTEX_AI_LOCATION', 'us-central1'),
    'model' => env('VERTEX_AI_MODEL', 'gemini-2.0-flash'),
    'api_key' => env('VERTEX_AI_API_KEY'),
],

'reminders' => [
    'ai_prompt' => "The client wrote a message: ':message'. Analyze whether the client is waiting for an answer...",
],

Using the AI Service

The service provides three main scanning methods:

php
use Modules\Messages\Services\VertexAiScanningService;

public function __construct(private VertexAiScanningService $aiService) {}

// Check if message needs response (used by reminders)
$result = $this->aiService->scanForNeedsResponse($message->body);
// Returns: ['needs_response' => bool, 'confidence' => 'heuristic'|'ai'|'default']

// Check for disintermediation (contact info)
$result = $this->aiService->scanForDisintermediation($message->body);
// Returns: ['detected' => bool, 'confidence' => string, 'analysis' => string]

// Check for availability issues
$result = $this->aiService->scanForAvailability($message->body);

Testing AI Features

Mock the AI service in tests to avoid API calls:

php
use Modules\Messages\Services\VertexAiScanningService;

it('schedules reminders when AI detects response needed', function () {
    $this->mock(VertexAiScanningService::class)
        ->shouldReceive('scanForNeedsResponse')
        ->andReturn(['needs_response' => true, 'confidence' => 'ai']);

    // Dispatch the job that triggers AI check
    CheckMessageNeedsResponseJob::dispatch($message);

    expect($message->thread->reminders)->toHaveCount(3);
});

Adding Custom Reminder Types

Step 1: Add Enum Value

Update the reminder type enum:

php
// modules/Message/Enums/MessageReminderType.php

enum MessageReminderType: string
{
    case VENDOR_NO_RESPONSE = 'vendor_no_response';
    case COUPLE_NO_RESPONSE = 'couple_no_response';
    case PAYMENT_PENDING = 'payment_pending';  // New
    case REVIEW_REQUEST = 'review_request';    // New
}

Step 2: Create Migration

Add the new enum value to the database:

bash
docker-compose exec -T wedissimo-api php artisan make:migration add_new_reminder_types_to_message_reminders
php
public function up(): void
{
    DB::statement("ALTER TYPE message_reminder_type ADD VALUE 'payment_pending'");
    DB::statement("ALTER TYPE message_reminder_type ADD VALUE 'review_request'");
}

Step 3: Update Service Logic

Extend MessageReminderService to handle new types:

php
public function schedulePaymentReminder(Message $message, int $hoursDelay): MessageReminder
{
    return MessageReminder::create([
        'message_id' => $message->id,
        'reminder_type' => MessageReminderType::PAYMENT_PENDING,
        'scheduled_for' => now()->addHours($hoursDelay),
    ]);
}

Step 4: Add Notification Template

Create notification class for the new reminder type:

bash
docker-compose exec -T wedissimo-api php artisan make:notification PaymentReminderNotification
php
public function toMail($notifiable): MailMessage
{
    return (new MailMessage)
        ->subject('Payment Pending for Your Booking')
        ->line('Your booking payment is still pending.')
        ->action('Complete Payment', url('/bookings/'.$this->booking->id.'/payment'))
        ->line('Complete payment to confirm your booking.');
}

Step 5: Update Scheduler

Modify the reminder delivery command to handle new types:

php
// modules/Message/Console/SendScheduledMessageReminders.php

protected function getNotificationForReminder(MessageReminder $reminder): Notification
{
    return match($reminder->reminder_type) {
        MessageReminderType::VENDOR_NO_RESPONSE => new VendorNoResponseNotification($reminder),
        MessageReminderType::COUPLE_NO_RESPONSE => new CoupleNoResponseNotification($reminder),
        MessageReminderType::PAYMENT_PENDING => new PaymentReminderNotification($reminder),
        MessageReminderType::REVIEW_REQUEST => new ReviewRequestNotification($reminder),
    };
}

Performance Considerations

N+1 Query Prevention

Always eager load relationships when working with message collections:

php
// WRONG - N+1 queries
$messages = Message::all();
foreach ($messages as $message) {
    echo $message->sender->name; // N+1 query here
}

// CORRECT - eager loading
$messages = Message::with(['sender', 'recipient', 'thread'])->get();
foreach ($messages as $message) {
    echo $message->sender->name; // No additional query
}

Partial Indexes

The message_thread_reminders table uses a PostgreSQL partial index for performance:

sql
CREATE INDEX reminders_due_idx ON message_thread_reminders (scheduled_for)
WHERE sent_at IS NULL AND cancelled_at IS NULL;

This index is automatically used when querying pending reminders:

php
// Efficiently uses partial index
$pending = MessageThreadReminder::query()
    ->whereNull('sent_at')
    ->whereNull('cancelled_at')
    ->where('scheduled_for', '<=', now())
    ->get();

Queue Management

Message processing happens asynchronously via queues:

php
// AI check dispatched after message creation
CheckMessageNeedsResponseJob::dispatch($message);

// Individual reminder jobs dispatched by scheduler
SendSingleReminderJob::dispatch($reminder);

Queue processing:

  • Jobs run on the default queue
  • messages:send-reminders command runs every 5 minutes
  • Individual jobs have retry logic with exponential backoff

Testing Strategies

Unit Testing Message Logic

Test message creation and relationships:

php
it('creates message with correct relationships', function () {
    $sender = User::factory()->create();
    $recipient = Vendor::factory()->create();

    $message = Message::factory()->create([
        'sender_id' => $sender->id,
        'sender_type' => User::class,
        'recipient_id' => $recipient->id,
        'recipient_type' => Vendor::class,
    ]);

    expect($message->sender)->toBeInstanceOf(User::class);
    expect($message->recipient)->toBeInstanceOf(Vendor::class);
    expect($message->sender->id)->toBe($sender->id);
});

Feature Testing Reminder Flow

Test end-to-end reminder scheduling:

php
it('schedules and sends reminders when vendor does not respond', function () {
    Queue::fake();

    $message = Message::factory()->create([
        'sender_type' => User::class,
        'recipient_type' => Vendor::class,
    ]);

    // AI determines response is needed
    $this->mock(VertexAIService::class)
        ->shouldReceive('needsResponse')
        ->andReturn(true);

    // Trigger reminder scheduling
    event(new MessageCreated($message));

    // Verify reminders scheduled
    Queue::assertPushed(ScheduleMessageReminders::class);

    expect($message->fresh()->reminders)->toHaveCount(3);
});

Testing AI Integration

Mock Vertex AI to test without API calls:

php
it('correctly interprets AI response for message analysis', function () {
    $this->mock(VertexAIService::class)
        ->shouldReceive('analyze')
        ->with(Mockery::pattern('/needs.*response/i'))
        ->andReturn('YES - This message contains a question requiring response');

    $result = $this->messageReminderService->analyzeMessage($message);

    expect($result)->toBeTrue();
});

Common Patterns

Creating Threaded Conversations

Always assign messages to threads for context:

php
public function sendReply(Message $originalMessage, string $content): Message
{
    return $this->messageService->create([
        'sender_id' => auth()->id(),
        'sender_type' => User::class,
        'recipient_id' => $originalMessage->sender_id,
        'recipient_type' => $originalMessage->sender_type,
        'thread_id' => $originalMessage->thread_id,
        'content' => $content,
    ]);
}

Bulk Operations

Process multiple messages efficiently:

php
public function markThreadAsRead(MessageThread $thread, User $user): void
{
    $thread->markAsReadBy($user);
}

Authorization Checks

Always verify permissions before message operations:

php
public function sendMessage(Request $request): JsonResponse
{
    $this->authorize('create', Message::class);

    $recipient = Vendor::findOrFail($request->recipient_id);

    if (!$this->canMessageVendor(auth()->user(), $recipient)) {
        abort(403, 'You cannot message this vendor');
    }

    $message = $this->messageService->create($request->validated());

    return new MessageResource($message);
}

Debugging Tips

Enable Query Logging

Debug N+1 queries or slow operations:

php
DB::enableQueryLog();

$messages = Message::with('reminders')->get();

dd(DB::getQueryLog());

Monitor Queue Processing

Check queue worker logs for reminder processing:

bash
docker-compose logs -f wedissimo-api | grep "ScheduleMessageReminders"

Test AI Prompts in Console

Validate AI responses before deploying:

bash
docker-compose exec -T wedissimo-api php artisan tinker
php
$ai = app(VertexAIService::class);
$response = $ai->analyze("Does this message need a response? 'Thanks for the info!'");
echo $response;

Next Steps

  • Review Operations Guide for monitoring and troubleshooting
  • Check OpenAPI docs for API endpoint details
  • Explore Vertex AI documentation for advanced AI features
  • Read Laravel Event documentation for custom event handling

Wedissimo API Documentation