NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages

Set an expiry with expiresIn() (seconds) or expiringAt() (a Carbon instance) — on an ask, an approval or a whole request:

use RoundlyConsulting\Approvals\Facades\Approvals;

// A pending sign-off that lapses if legal has not decided within 48 hours.
Approvals::for($contract)->as($legal)->expiresIn(172800)->ask();

// An approval valid for a day.
Approvals::for($budget)->as($cfo)->expiresIn(86400)->approve();

// A request that must be decided within a week.
Approvals::request($budget)->from([$cfo, $ceo])->expiresIn(604800)->open();

What expiry does

An approval, ask or request stops counting the moment its expiry passes. Reads such as isApproved(), hasApproved(), approvalCount() and a request’s tally ignore it, and a request past its expiry accepts no more decisions: the next one resolves it as expired and is refused with ClosedApprovalRequestException. An expired approval frees its actor to approve again.

Set approvals.expiry.default to give every approval a lifetime unless the decision sets its own. Answering an ask never inherits the ask’s deadline.

Recording the lapse

Approvals::expire() records what has lapsed: decisions move to expired (ApprovalExpired plus ApprovalStatusChanged), requests resolve as expired (ApprovalRequestResolved plus ApprovalStatusChanged), and it returns how many of both it lapsed. Given a subjectType — a model class or its morph alias — it lapses only decisions on that type of approvable and requests for that type of subject, so a package sweeping its own approvals leaves the rest of the app alone:

Approvals::expire();                             // int — decisions and requests lapsed
Approvals::expire(subjectType: Budget::class);   // only those on budgets (class or morph alias)
Approvals::expire(now()->addHour());             // lapse everything due within the next hour

The approvals:expire command

The app-wide sweep as an Artisan command — no arguments or options:

php artisan approvals:expire
# Expired 3 approval(s) and request(s).

Schedule it so overdue decisions and requests are recorded as expired on their own:

use Illuminate\Support\Facades\Schedule;

Schedule::command('approvals:expire')->hourly();

Querying by expiry

The Approval model’s inForce() scope keeps only decisions whose expiry has not passed; expiringBefore() finds the overdue ones:

use RoundlyConsulting\Approvals\Models\Approval;

// Approvals that still count — none past its expiry.
Approval::query()->approved()->inForce()->whereMorphedTo('approvable', $budget)->count();

// Overdue asks and approvals the next sweep will lapse.
Approval::query()->active()->expiringBefore(now())->get();

Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

More ways to support, including crypto

By donating, you agree to our donation terms.

Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.