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

Since 1.1.8, UserCreated is dispatched after the driver’s role has been assigned and saved, so a listener always sees the user in their final, fully-provisioned state. Prefer it over Eloquent’s User::created model hook whenever your reaction depends on the role: the Eloquent hook fires mid-creation, before the role is set, so a listener bound to it can see a driver who does not yet have their role.

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

UserAnonymized

Class: App\Events\User\UserAnonymized. Added in 1.1.8. Triggered: after a driver is anonymised for data-protection (GDPR) reasons — dispatched after the anonymisation transaction commits, so a listener sees the final, anonymised state.

PropertyTypeDescription
$userApp\Models\Userthe anonymised user
$reasonstringthe anonymisation reason

The anonymisation service saves quietly — it deliberately suppresses Eloquent’s model events — so this explicit domain event is the only reliable signal that a driver was anonymised. Listen for it rather than trying to detect the change another way.

Vehicle events

Added in 1.1.8, these give a module reliable signals around the vehicle fleet without polling the database.

VehicleCreated

Class: App\Events\Vehicle\VehicleCreated. Added in 1.1.8. Triggered: after a vehicle is created.

PropertyTypeDescription
$vehicleApp\Models\Vehiclethe new vehicle

VehicleDeleted

Class: App\Events\Vehicle\VehicleDeleted. Added in 1.1.8. Triggered: when a vehicle is deleted — dispatched before the row is removed, so a listener can still read the vehicle’s data one last time.

PropertyTypeDescription
$vehicleApp\Models\Vehiclethe vehicle about to be deleted

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();
    }
});