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)
| Property | Type | Description |
|---|---|---|
$job | App\Models\Job | the started job |
JobCompleted
Class: App\Events\JobCompleted
Triggered: when a service job is completed (driver marks work as done)
| Property | Type | Description |
|---|---|---|
$job | App\Models\Job | the completed job |
$weatherAvailable | bool | whether weather data was fetched for this job |
$isWeatherUpdate | bool | whether 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
| Property | Type | Description |
|---|---|---|
$customer | App\Models\Customer | the new customer |
CustomerUpdated
Class: App\Events\Customer\CustomerUpdated
Triggered: when a customer record is modified
| Property | Type | Description |
|---|---|---|
$customer | App\Models\Customer | the updated customer |
CustomerDeleted
Class: App\Events\Customer\CustomerDeleted
Triggered: when a customer is deleted
| Property | Type | Description |
|---|---|---|
$customer | App\Models\Customer | the deleted customer |
User events
UserCreated
Class: App\Events\User\UserCreated
Triggered: when a new user (admin or driver) is created
| Property | Type | Description |
|---|---|---|
$user | App\Models\User | the new user |
UserLoggedIn
Class: App\Events\User\UserLoggedIn
Triggered: when a user logs in
| Property | Type | Description |
|---|---|---|
$user | App\Models\User | the logged-in user |
UserLoggedOut
Class: App\Events\User\UserLoggedOut
Triggered: when a user logs out
| Property | Type | Description |
|---|---|---|
$user | App\Models\User | the 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
| Property | Type | Description |
|---|---|---|
$workShift | App\Models\WorkShift | the shift that started |
$user | App\Models\User | the driver |
WorkShiftEnded
Class: App\Events\Shift\WorkShiftEnded
Triggered: when a driver ends a work shift
| Property | Type | Description |
|---|---|---|
$workShift | App\Models\WorkShift | the shift that ended |
$user | App\Models\User | the 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
| Property | Type | Description |
|---|---|---|
$module | App\Models\Module | the enabled module |
ModuleDisabled
Class: App\Events\Module\ModuleDisabled
Triggered: when a module is disabled through the admin UI
| Property | Type | Description |
|---|---|---|
$module | App\Models\Module | the disabled module |
Weather events
WeatherSnapshotCreated
Class: App\Events\WeatherSnapshotCreated
Triggered: when weather data is fetched and stored for a job
| Property | Type | Description |
|---|---|---|
$snapshot | App\Models\WeatherSnapshot | the 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().
| Property | Type | Description |
|---|---|---|
$user | App\Models\User | the driver who sent the ping |
$lat | float | latitude |
$lon | float | longitude |
$timestamp | int | Unix timestamp of the fix |
$accuracy | ?int | reported accuracy in metres (nullable) |
$activeJob | ?App\Models\Job | the 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();
}
});