Purchase Request Approval

Alpha notice: This walkthrough mirrors the hosted demo. Status names and thresholds are demo-specific.

What you will build

Draft / Rejected → Submit for internal review
    → Procurement Manager Review → Amount Gate
        → [amount >= 10000] Finance Review → Approved
        → [default] → Approved

After approval, host actions such as archive become available. ERP, PO creation, and payment logic stay outside DBFlow Core.

Business object

Item Value
Model App\Models\ProcurementRequest
Workflow key procurement_request_approval
Constants class App\DBFlow\ProcurementRequest\ProcurementRequestWorkflow

ProcurementRequest implements Workflowable and WorkflowContextInterface:

public function getWorkflowVariables(): array
{
    return [
        'amount' => (float) $this->amount,
        'department' => $this->department,
        // ...
    ];
}

Demo business status (ProcurementRequestStatus enum):

Status Meaning
draft Editable request, not in review
pending_review Submitted; workflow running or awaiting completion
approved Workflow approved; ready for downstream purchasing steps
rejected Workflow rejected; may return to draft
archived Host archival after approval (not a DBFlow terminal state)

Demo source paths:

Area Path
Model app/Models/ProcurementRequest.php
Workflow constants app/DBFlow/ProcurementRequest/ProcurementRequestWorkflow.php
Hooks app/DBFlow/ProcurementRequest/ProcurementRequestWorkflowHooks.php
Status guard (optional) app/DBFlow/ProcurementRequest/ProcurementRequestStatusGuard.php
Definition seeder database/seeders/Demo/ProcurementRequestWorkflowSeeder.php
Integration test tests/Feature/ProcurementRequestDbflowIntegrationTest.php

Workflow

Node keys:

  • procurement_manager_review
  • amount_gate
  • finance_review (high amount only)
  • end_approved

Amount condition: amount >= 10000 (ProcurementRequestWorkflow::CONDITION_HIGH_AMOUNT).

ProcurementRequestWorkflowSeeder builds the JSON graph, validates, and publishes — same pipeline as refund disputes.

Hooks keep DBFlow and host columns aligned:

Hook Host update
onStarted draft or rejectedpending_review, set submitted_at
onApproved approved, set reviewed_at
onRejected rejected, set reviewed_at
onCancelled conservative return to draft (unless already archived)

Implementation: app/DBFlow/ProcurementRequest/ProcurementRequestWorkflowHooks.php.

Why this matters

  • Clear draft → review → approved lifecycle on a familiar business object
  • Amount-based escalation without ERP complexity
  • Shows guarded downstream actions after approval
  • Builds on the same patterns as Refund Approval

Minimal code

Submit for internal review is visible when status is draft or rejected, no running workflow exists, and the record is not archived.

DBFlow::start(
    ProcurementRequestWorkflow::KEY,
    $record,
    Auth::user(),
);

Assignees use My Tasks. Finance reviewers receive tasks only when amount >= 10000 routes through finance_review.

Guard post-approval business actions on host status — not workflow internals:

->visible(fn (ProcurementRequest $record): bool =>
    $record->status === ProcurementRequestStatus::Approved
)

DBFlow does not ship ERP adapters. If you sync to an external system, invoke that from WorkflowHooks::onApproved() or a host job — keep integration code in app/, not in Core.

Filament integration

Area Path
Resource actions app/Filament/Resources/ProcurementRequestResource/Concerns/HasLifecycleActions.php
Presenter app/Filament/Resources/ProcurementRequestResource/Support/ProcurementRequestWorkflowPresenter.php

ProcurementRequestResource composes HasLifecycleActions for submit, return-to-draft, and archive actions. ProcurementRequestWorkflowPresenter surfaces workflow metadata on the resource view.

Use Workflow Instances or the presenter link to inspect dbflow_workflow_logs through the Standard timeline. See Workflow Timeline.

What to try next

Alpha caveats:

  • Threshold (10000) and assignee user IDs are demo constants
  • archived is a host-only state after approval — not an DBFlow end node
  • Cancel hook rolls back to draft; adjust for your production policy
  • Published definition must exist before DBFlow::start() — run Host Integration sync on deploy
  • permission / callback assignees require registerAssigneeResolver() — not Laravel permission names
Something wrong? Open an issue on GitHub