Events, Jobs, Schedules and Notifications

React to things that happen elsewhere in TruCMS, move slow work out of the request, and run housekeeping on a timer. All of it is wired from your plugin's service provider.

Events you can listen to

These are the events the current code actually fires. Listening to anything else in the codebase is not safe until you have checked that something dispatches it.

EventPayloadFired when
App\Events\AdminBootingnoneEvery admin request, after the core sidebar and settings are registered. The hook for sidebar items, settings sections and menu-builder sources
Illuminate\Auth\Events\Registered$event->userA visitor registers. The core listener creates their team and assigns the default role
Konekt\User\Events\UserWasActivated / UserWasInactivatedthe userUser::activate() / inactivate()
App\Events\TicketOpenEvent$event->ticketA support ticket is created (fired after the transaction commits)
App\Events\TicketMessageEvent$event->ticket, the messageA reply is posted
App\Events\TicketCloseEvent$event->ticketA ticket is closed. The core has no listener for it, so a closing notification is yours to add
Vanilo\Payment\Events\PaymentCreated, PaymentCompleted, PaymentPartiallyReceived, PaymentDeclined, PaymentTimedOut$event->getPayment()TruPayments applies a gateway response or a manual status change. Check getPayable() to see whose payment it is
Modules\TruPayments\Events\RefundCreated$event->refundA refund record is created (after commit)
Modules\TruPayments\Events\RefundStatusChangedrefund, from, to (RefundStatus enums)A refund moves between states (after commit)
Eloquent model eventsthe modelcreated, updated, deleted… on any model, core or module. The most reliable way to follow bookings, orders and products
cache:clearedstore, tagsphp artisan cache:clear. The core listens to flush the response cache

Booking's own events are not dispatched

The Booking module declares BookingCreated, BookingUpdated, BookingStatusChanged, BookingCheckedIn and BookingPaymentStatusChanged and maps them to a listener, but nothing in the current code fires them: booking notifications are sent directly by NotificationService. To react to bookings, observe the Modules\Booking\Models\Booking model (created, updated, and wasChanged('status') inside updated), or listen to the payment events above for payment changes.

Listening from a plugin

For a handful of listeners, register them in boot():

Modules/Testimonials/app/Providers/TestimonialsServiceProvider.php

use Illuminate\Support\Facades\Event;
use Modules\Booking\Models\Booking;
use Modules\Testimonials\Listeners\AskForTestimonial;
use Nwidart\Modules\Facades\Module;

public function boot(): void
{
    parent::boot();

    Event::listen(TicketCloseEvent::class, NotifyTicketClosed::class);

    // Only touch another module's classes when that module is enabled.
    if (Module::isEnabled('Booking')) {
        Booking::updated(function (Booking $booking): void {
            if ($booking->wasChanged('status') && $booking->status === 'completed') {   // AppointmentStatus::Completed
                app(AskForTestimonial::class)->handle($booking);
            }
        });
    }
}

For many listeners, add a child EventServiceProvider with a $listen map and list it in your provider's $providers array, as Booking does (Modules/Booking/app/Providers/EventServiceProvider.php).

Firing your own events

Define plain event classes in app/Events with Dispatchable and SerializesModels, and dispatch them with MyEvent::dispatch($model). Document them for other plugin authors, and keep the payload stable once released.

Background jobs

Queues run synchronously by default

QUEUE_CONNECTION ships as sync, so a queued job runs inside the request that dispatched it. Site owners can choose Database, Redis, Beanstalkd or SQS in Settings → General → Queue, and must then run a worker. Write jobs that are correct in both modes.

Modules/Testimonials/app/Jobs/SendTestimonialRequest.php

use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;

class SendTestimonialRequest implements ShouldQueue
{
    use Queueable;

    public function __construct(public int $customerId) {}    // pass ids, not models with big relations

    public function handle(): void
    {
        // ...
    }
}

// dispatching from a transaction: wait until it commits
SendTestimonialRequest::dispatch($customer->id)->afterCommit();

Scheduled tasks

Override configureSchedules() in your module provider. The base provider calls it once the application has booted, so your tasks appear in php artisan schedule:list only while the plugin is enabled.

Modules/Testimonials/app/Providers/TestimonialsServiceProvider.php

use Illuminate\Console\Scheduling\Schedule;
use Modules\Testimonials\Console\PruneRejectedTestimonials;

protected array $commands = [
    PruneRejectedTestimonials::class,          // registered by parent::boot()
];

protected function configureSchedules(Schedule $schedule): void
{
    $schedule->command('testimonials:prune-rejected')->dailyAt('04:10')->withoutOverlapping();
}

Name commands {alias}:{verb}-{noun}, use withoutOverlapping() for anything that can run long, and pick a time that does not collide with the bundled tasks:

CommandOwnerSchedule
pages:refresh-boundCore (routes/console.php)Hourly
booking:cleanup-payment-intentsBookingHourly
booking:renew-calendar-watchesBookingDaily 03:20
trucommerce:checkout-intents:expire, trucommerce:stock:release-holdsTruCommerceEvery 15 minutes
trucommerce:cleanup, trucommerce:optimize, trucommerce:media:generate-missing-conversionsTruCommerceDaily 02:10, 02:30, 03:00

None of this runs unless the server's cron calls php artisan schedule:run every minute; the end-user installation guide tells site owners how to set it up. dotartisan:tickets:autoclose exists but is not scheduled by default.

Notifications and mail

Testing

use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Facades\Event;

it('queues a thank-you job when a testimonial is published', function (): void {
    Bus::fake();

    $testimonial = Testimonial::factory()->create(['is_published' => false]);
    $testimonial->update(['is_published' => true]);

    Bus::assertDispatched(SendThankYou::class);
});

it('registers the prune task', function (): void {
    $this->artisan('schedule:list')->expectsOutputToContain('testimonials:prune-rejected');
});

The test environment forces QUEUE_CONNECTION=sync, so jobs you do not fake run inline. Booking has no model factories; when your plugin reacts to bookings, create the rows you need in the test itself.