Browse documentationOpen

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 request

resubmissions 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

StateRevisable
returnedyes
rejectedyes
withdrawnyes
cancelledyes
conflictedyes
approvedno
pendingno
supersededno

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
ConstructorResult
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".