›
byrcsc/laravel-assignment · 1.x
A declined or expired offer moves to the next candidate, skipping everyone who already turned it down.
An offer nobody answers is work that has stopped moving. A cascade keeps it moving: when an offer is declined or expires, the engine re-runs the selection against the remaining candidates and offers it to the next one.
offer to Eli -> declined
offer to Fay -> expired
offer to Ivo -> acceptedEach step is a new row, so the assignable's history shows every crew who was asked and what they did.
A cascade needs to resolve candidates again later, which is exactly what a profile provides. So:
autoAssign() on a model in offer mode gets a
cascade_id and cascades.$model->offer($assignee) has
no cascade_id. It ends when it is declined or expires, and nothing follows.That is deliberate. An ad-hoc offer has no candidate list to fall back on, and guessing one would be worse than stopping.
Re-offers exclude every assignee who declined or let an offer expire in the
same cascade. The exclusion is tracked by the cascade_id on the rows, so it
survives the candidate list changing between steps.
The exclusion does not persist beyond the cascade. Once the cascade ends, a later attempt starts fresh with everyone eligible again, including the people who said no last time.
The cascade stops. Two things then happen:
queueWhenEmpty() returns true.OffersExhausted fires, carrying the last offer.public function handle(OffersExhausted $event): void
{
$event->lastOffer->assignable; // what nobody would take
$event->lastOffer->role;
}Exhaustion is the signal that your candidate pool is too small, that everybody is busy, or that the work is unattractive. A listener is where an escalation belongs.
A later flush starts a new cascade with a new id, so the people who declined the first round are asked again.
A tick can exhaust and re-offer in one run.
assignment:tickexpires overdue offers first and flushes the queue second. An offer that exhausts its cascade during the expiry step is queued, and the flush in the same command can start a fresh cascade immediately, which may re-offer to somebody who declined moments earlier. Split the two steps if that is not what you want.
A decline advances the cascade synchronously, inside the call:
$crew->decline($offer);
$callout->openAssignment('crew'); // already the next crew's offerThe assignee who declined gets no "assignment removed" notification, because they chose not to take it. The next candidate gets an "offer received" notification as usual.
An expired offer stops holding its slot the moment expires_at passes. The
row still reads offered until something ends it, and the cascade has not
moved on yet.
Two things advance it:
// routes/console.php
Schedule::command('assignment:tick')->everyMinute();// config/assignment.php
'dispatch_expiry_jobs' => true,The tick sweeps every overdue offer. The job option additionally dispatches one delayed job per offer at the moment it expires, which gives second-level precision instead of cron granularity. The tick stays as the safety net for jobs that were lost.
Delayed jobs are dispatched only for offers that have both a cascade_id and
an expires_at, so an ad-hoc offer never queues one. A job whose offer was
answered before it ran does nothing.
OffersExhausted and the rest.