›
byrcsc/laravel-approval · 1.x
Set stage deadlines, reminders, and actions for overdue approvals.
A stage with sla_minutes gets a deadline. The deadline counts from the moment
the stage becomes active and starts waiting on its approvers, not from the
moment the requester submitted the request.
$workflow->stages()->create([
'sequence' => 1,
'name' => 'Finance',
'required_approvals' => 1,
'sla_minutes' => 1440,
'remind_every_minutes' => 240,
'escalation_strategy' => EscalationAction::Escalate,
'escalation_config' => ['resolver' => App\Org\ManagerOf::class],
]);The package records deadlines whether or not anything acts on them.
Nothing escalates until the sweep runs. Schedule it:
Schedule::command('approval:escalate')->everyFifteenMinutes();
The interval sets how soon the sweep notices a late stage. It does not affect correctness. The sweep is idempotent, so running it every minute and running it every hour produce the same escalations at different resolutions.
Each stage names one action, and the approval.escalation.strategies config
map says which class runs it.
| Action | Shipped strategy | Behavior |
|---|---|---|
record_only | RecordOnlyStrategy | Records the overdue state and emits the event |
escalate | EscalateStrategy | Adds further approvers to the stage |
auto_approve | AutoDecideStrategy | Approves the stage and advances the request |
auto_reject | AutoDecideStrategy | Rejects the request |
record_only is the default, and its strategy is empty on purpose. The engine
marks the stage, fires StageOverdue, and writes the audit line before any
strategy runs. By the time the engine calls RecordOnlyStrategy, all of the
record-only behavior has already happened.
record_only sends no message. Turn on notifications or listen for the event
to turn the recorded fact into a message. Deciding an approval automatically
because it is late is a policy each organization chooses. A package should not
assume it.
escalate resolves further approvers, typically the approver's manager or a
duty officer:
'escalation_config' => [
'resolver' => App\Org\ManagerOf::class,
'resolver_config' => ['levels' => 1],
],The resolver is an ordinary ApproverResolver.
The engine skips people already represented on the stage and records the new
assignments with the escalated source.
One rule needs care. A stage with a null required_approvals requires all of
its assignments, so adding one would raise the number of approvals needed and
make an already-late stage harder to finish. To avoid that, the engine pins a
null rule to the assignment count the stage had before the addition. The added
approvers act as alternatives to those who have not responded, not as extra
requirements.
Set required_approvals inside escalation_config to pin a different number:
'escalation_config' => [
'resolver' => App\Org\DutyOfficer::class,
'required_approvals' => 1,
],The engine leaves existing assignments in place. An approver who is only slow may still respond, and removing their assignment would discard work in progress.
ApproversEscalated fires after the assignments exist, so recipient
calculation can only ever see the new holders.
auto_approve completes the stage and advances the request exactly as an
approver's decision would. If the stage was the last one, the engine applies
the draft with the same stale check. auto_reject closes the request. The
requester may then revise and resubmit it.
Both record their action with no actor, because there was none. "The
deadline passed and the stage was approved" is a different fact from "an
approver approved it", and the history has to distinguish them. metadata
carries automatic: true.
Override the recorded comment through the config:
'escalation_config' => ['comment' => 'Auto-approved under the 24-hour SLA.'],A stage with remind_every_minutes keeps emitting after it first goes overdue:
StageOverdue fires once, when the deadline first passes, before the
strategy acts, so a listener sees the state that went overdue.StageOverdueReminder fires at the configured cadence afterwards.Reminders never rerun the strategy. escalated_at makes each strategy run
exactly once. last_reminded_at paces the reminders. Each reminder writes a
reminded audit action.
Implement the contract and map an action to the class in the config:
use ByRcsc\LaravelApproval\Contracts\EscalationStrategy;
use ByRcsc\LaravelApproval\Enums\EscalationAction;
final class NotifyDutyManager implements EscalationStrategy
{
public function handle(
ApprovalRequest $request,
ApprovalRequestStage $stage,
EscalationAction $action,
array $config,
): void {
// ...
}
}// config/approval.php
'escalation' => [
'strategies' => [
'record_only' => NotifyDutyManager::class,
],
],The engine resolves strategies from the container and runs them inside the transaction for that stage. If a strategy throws, the engine commits nothing about the stage, including the escalation mark, so the next run tries again. That retry is the right behavior for transient failures, which are the most common failures here.
$action names the action that called the strategy, so one class can handle
several actions.
The package validates the configuration at boot. An unknown case, a missing
class, or a class that does not implement the contract raises
InvalidConfigurationException.
Each stage gets its own transaction, so one misconfigured stage cannot stop the sweep from handling the other four hundred. The command reports what happened and exits non-zero if anything threw:
php artisan approval:escalate[
'escalated' => ['12', '31'], // stage ids
'reminded' => ['8'],
'stranded' => ['44'],
'failed' => ['19' => 'Stage 3 escalates by bringing somebody in, but its escalation_config names no resolver to find them.'],
]Approval::escalateOverdue() returns the same array, for applications that run
the sweep from their own job.
$requests = Approval::overdue()->withProgress()->get();overdue() returns pending requests whose active stage is past its deadline,
independent of whether the escalation policy has run. A stage remains overdue
until it is decided.
The sweep also scans active stages whose living assignment holders can no
longer meet the required approvals, marks them, and fires StageStranded.
These stages are not overdue. Nobody can finish them, and no deadline will fix
them.
See delegation and reassignment.