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:
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:
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:
// 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:
'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:
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:
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:
// 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:
docker-compose exec -T wedissimo-api php artisan make:migration add_new_reminder_types_to_message_reminderspublic 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:
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:
docker-compose exec -T wedissimo-api php artisan make:notification PaymentReminderNotificationpublic 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:
// 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:
// 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:
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:
// 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:
// 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-reminderscommand runs every 5 minutes- Individual jobs have retry logic with exponential backoff
Testing Strategies
Unit Testing Message Logic
Test message creation and relationships:
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:
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:
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:
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:
public function markThreadAsRead(MessageThread $thread, User $user): void
{
$thread->markAsReadBy($user);
}Authorization Checks
Always verify permissions before message operations:
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:
DB::enableQueryLog();
$messages = Message::with('reminders')->get();
dd(DB::getQueryLog());Monitor Queue Processing
Check queue worker logs for reminder processing:
docker-compose logs -f wedissimo-api | grep "ScheduleMessageReminders"Test AI Prompts in Console
Validate AI responses before deploying:
docker-compose exec -T wedissimo-api php artisan tinker$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