byrcsc/laravel-approval · 1.x
Returns and resubmission.
Send a request back to its requester, revise the draft, and resubmit it as a new linked request against a fresh workflow snapshot.
A returned request is waiting on its requester rather than on an approver:
Approval::returnForRevision($request, $financeUser, 'Attach the supplier quote');The request state becomes returned and ApprovalReturned fires with the
stage, the actor, and the reason. The deciding stage is marked returned and
the stages that never ran are marked skipped — the attempt is over. Nothing
resubmits automatically; the requester decides what to do next.
Revising and resubmitting
use ByRcsc\LaravelApproval\Data\RevisedDraft;
$replacement = Approval::reviseAndResubmit($request, RevisedDraft::withAttributes([
'amount' => 8_000,
]));The old request is not reopened and not edited. It is what happened. The
replacement is a new request, started from the first applicable stage of a
fresh workflow snapshot, and the two are joined by resubmitted_from_id.
$replacement->resubmittedFrom; // the request it replaces
$request->resubmissions; // every replacement raised from this requestresubmissions is a list rather than a single record, because a replacement
can itself be returned or rejected and revised again.
Starting at stage one is deliberate
The stages that approved the first attempt approved that version of it. A revision is a different proposal, and asking them again is the point rather than the cost.
Because the snapshot is fresh, a workflow edited since the original submission takes effect on the replacement. Conditions are re-evaluated against the new proposal too, so a smaller revised amount can skip a board stage the original triggered.
Which states can be revised
| State | Revisable |
|---|---|
returned | yes |
rejected | yes |
withdrawn | yes |
cancelled | yes |
conflicted | yes |
approved | no |
pending | no |
superseded | no |
approved is missing on purpose: that one succeeded. Wanting to change the
record again is an ordinary new submission, not a revision of an old one.
Anything else raises AlreadyDecidedException::notRevisable().
Revising a conflicted request marks it superseded and records a
superseded action naming the replacement. That also clears the unresolved
request, which is what lets the replacement be submitted at all.
Choosing the new draft
RevisedDraft has three named constructors, because null cannot carry three
distinct meanings:
RevisedDraft::withAttributes(['amount' => 8_000]); // a revised draft
RevisedDraft::inherit(); // carry the old one forward
RevisedDraft::none(); // record-level replacement| Constructor | Result |
|---|---|
withAttributes() | Replaces the previous draft entirely — keys may be added, changed, or left out |
inherit() | Carries the previous draft forward unchanged |
none() | Submits the record itself rather than an edit to it |
inherit() on a request that held no draft produces a record-level
replacement.
Revised attributes are revalidated against the model's
approvableAttributes(), exactly as an original submission is.
Requester and workflow
Both are inherited from the request being revised unless given:
Approval::reviseAndResubmit(
$request,
RevisedDraft::inherit(),
requester: $delegateBuyer,
workflow: 'purchase-order-expedited',
);Events and history
ApprovalResubmitted fires with the replacement as $request and the original
as $previous. The replacement's trail opens as every submission does, with a
submitted action, followed by a resubmitted action naming the original's
ULID; a superseded original gets a matching superseded action naming the
replacement.
Each request keeps its own timeline. Walk resubmittedFrom() to reconstruct
the full chain:
$chain = collect();
for ($node = $replacement; $node !== null; $node = $node->resubmittedFrom) {
$chain->push($node);
}Returning versus rejecting
Both close the current attempt and both can be revised. The difference is what they say:
- Return means the proposal is fixable and the requester should fix it. The state reads as "waiting on the requester", and the shipped notification tells them so.
- Reject means the proposal is refused. Revising is still possible, but nothing in the package suggests it.
Keeping them distinct is what lets a report separate "sent back for a missing attachment" from "declined".