Skip to main content
Wintertrace
Browse the documentation

Building modules

Events reference

The domain events Wintertrace dispatches and how a module listens for them to react to shifts, tracks, reports and other actions.

Wintertrace dispatches domain events throughout the application — when a job is completed, a customer is created, or a shift ends. Modules can listen for these events and react to them without touching a single core file.

Why events rather than direct modification

A module that needs to act on every completed job could, in theory, modify the core code at the relevant point. The event system exists precisely to avoid that: the core announces “this job is finished”, and your module decides whether and how to respond. The core knows nothing about your module; your module does not reach into the core. This keeps updates conflict-free and lets multiple modules react to the same event independently.

Listening to events

Register listeners in the boot() method of your ServiceProvider (for the distinction between register() and boot(), see The ServiceProvider pattern). A closure is enough for simple cases:

$this->app['events']->listen(JobCompleted::class, function (JobCompleted $event) {
    // React to job completion
    Log::info('Job completed', ['job_id' => $event->job->id]);
});

Once the reaction grows more complex, move the logic into a dedicated listener class. This keeps the ServiceProvider readable and makes the listener testable:

$this->app['events']->listen(JobCompleted::class, MyJobCompletedListener::class);

Job events

The job events cover the core of day-to-day operations — from the moment work begins to the completion of a job.

JobStarted

Class: App\Events\JobStarted Triggered: when a service job is started (driver begins work)

PropertyTypeDescription
$jobApp\Models\Jobthe started job

JobCompleted

Class: App\Events\JobCompleted Triggered: when a service job is completed (driver marks work as done)

PropertyTypeDescription
$jobApp\Models\Jobthe completed job
$weatherAvailableboolwhether weather data was fetched for this job
$isWeatherUpdateboolwhether this is a weather-only update (default: false)

This is the most commonly used event. The core listener SendJobCompletedNotification fires when it is dispatched and sends notifications through the NotificationChannelRegistry. This is also a good example of how the core itself works through events — your module hooks in at the same point.

Customer events

CustomerCreated

Class: App\Events\CustomerCreated Triggered: when a new customer is created

PropertyTypeDescription
$customerApp\Models\Customerthe new customer

CustomerUpdated

Class: App\Events\Customer\CustomerUpdated Triggered: when a customer record is modified

PropertyTypeDescription
$customerApp\Models\Customerthe updated customer

CustomerDeleted

Class: App\Events\Customer\CustomerDeleted Triggered: when a customer is deleted

PropertyTypeDescription
$customerApp\Models\Customerthe deleted customer

User events

UserCreated

Class: App\Events\User\UserCreated Triggered: when a new user (admin or driver) is created

PropertyTypeDescription
$userApp\Models\Userthe new user

UserLoggedIn

Class: App\Events\User\UserLoggedIn Triggered: when a user logs in

PropertyTypeDescription
$userApp\Models\Userthe logged-in user

UserLoggedOut

Class: App\Events\User\UserLoggedOut Triggered: when a user logs out

PropertyTypeDescription
$userApp\Models\Userthe logged-out user

Work shift events

Shift events carry two properties: the shift itself and the driver. This lets you identify immediately who started or ended a shift.

WorkShiftStarted

Class: App\Events\Shift\WorkShiftStarted Triggered: when a driver begins a work shift

PropertyTypeDescription
$workShiftApp\Models\WorkShiftthe shift that started
$userApp\Models\Userthe driver

WorkShiftEnded

Class: App\Events\Shift\WorkShiftEnded Triggered: when a driver ends a work shift

PropertyTypeDescription
$workShiftApp\Models\WorkShiftthe shift that ended
$userApp\Models\Userthe driver

Module events

These two events fire when a module is enabled or disabled through the admin UI. They are the clean place for one-time setup or teardown steps — see also Module lifecycle.

ModuleEnabled

Class: App\Events\Module\ModuleEnabled Triggered: when a module is enabled through the admin UI

PropertyTypeDescription
$moduleApp\Models\Modulethe enabled module

ModuleDisabled

Class: App\Events\Module\ModuleDisabled Triggered: when a module is disabled through the admin UI

PropertyTypeDescription
$moduleApp\Models\Modulethe disabled module

Weather events

WeatherSnapshotCreated

Class: App\Events\WeatherSnapshotCreated Triggered: when weather data is fetched and stored for a job

PropertyTypeDescription
$snapshotApp\Models\WeatherSnapshotthe new weather snapshot

GPS / location events

GpsPointReceived

Class: App\Events\GpsPointReceived. Added in 1.1.6. Triggered: for every valid OwnTracks location ping — whether or not the driver currently has an active job. Dispatched in OwnTracksController::store().

PropertyTypeDescription
$userApp\Models\Userthe driver who sent the ping
$latfloatlatitude
$lonfloatlongitude
$timestampintUnix timestamp of the fix
$accuracy?intreported accuracy in metres (nullable)
$activeJob?App\Models\Jobthe driver’s active job, or null if idle

This event exists for a specific reason: it lets a geofencing module watch a driver’s live position before a job exists and auto-start one (for instance through JobLifecycleService). That is also why it fires on idle pings, not just during a job.

One thing to know: the core does not persist idle GPS points — GpsPoint storage stays job-scoped. This event is the only place idle pings are exposed, so if your module needs them, capture them here.

$this->app['events']->listen(GpsPointReceived::class, function (GpsPointReceived $event) {
    if ($event->activeJob !== null) {
        return; // already working
    }
    // Check whether ($event->lat, $event->lon) entered a geofence and start a job...
});

Common patterns

Logging all job completions

A common use case: a module that records every completed job in its own log. $event->job gives you the full job model and its relationships:

$this->app['events']->listen(JobCompleted::class, function (JobCompleted $event) {
    ModuleLogger::make()->info('my-module', 'Job completed', [
        'job_id' => $event->job->id,
        'customer' => $event->job->customer?->name,
        'type' => $event->job->type->value,
        'weather' => $event->weatherAvailable,
    ]);
});

Sending shift-start notifications

$this->app['events']->listen(WorkShiftStarted::class, function (WorkShiftStarted $event) {
    // Notify admin that a driver has started their shift
    $this->sendShiftAlert($event->user, $event->workShift);
});

Reacting to the module lifecycle

If your module needs a one-time setup step on activation, check the slug in the listener — this way the module only reacts to its own activation, not to that of other modules:

$this->app['events']->listen(ModuleEnabled::class, function (ModuleEnabled $event) {
    if ($event->module->slug === 'my-module') {
        // Perform post-activation setup
        $this->runSetup();
    }
});