Approval Workflow System
A flexible, multi-step approval system that integrates seamlessly with CRUD operations across all business modules.
System Overview
The Approval Workflow System provides configurable, multi-step approval processes for any document type in the ERP. It supports CREATE, UPDATE, and DELETE operations with customizable approval chains.
Multi-Step Approvals
Configure sequential approval chains with multiple approvers per step.
Notifications
Automated notifications to approvers with configurable templates.
Audit Trail
Complete history of approval actions with timestamps and remarks.
System Components
| Component | Purpose | Key Fields |
|---|---|---|
| ApprovalSetup | Defines which operations require approval for a model type |
model_type, action, is_active
|
| ApprovalSetupStep | Defines approvers and step order in the chain |
setup_id, step_order, user_id
|
| ApprovalRequest | Individual approval request instance for a document |
setup_id, record_id, status
|
| ApprovalRequestStep | Tracks each approver's action and timestamp |
request_id, acted_at, remarks
|
Workflow States
stateDiagram-v2
[*] --> PENDING: Document Created
PENDING --> APPROVED: All Steps Approved
PENDING --> REJECTED: Any Step Rejected
APPROVED --> AWAITING_UPDATE_APPROVAL: Update Requested
AWAITING_UPDATE_APPROVAL --> APPROVED: Update Approved
AWAITING_UPDATE_APPROVAL --> APPROVED: Update Rejected (Reverts)
APPROVED --> AWAITING_DELETE_APPROVAL: Delete Requested
AWAITING_DELETE_APPROVAL --> [*]: Delete Approved
AWAITING_DELETE_APPROVAL --> APPROVED: Delete Rejected (Reverts)
REJECTED --> [*]
PENDING
Document created, waiting for approval chain completion.
APPROVED
All approval steps completed. Events fired, side effects executed.
REJECTED
Approval denied by any approver in the chain.
AWAITING_*_APPROVAL
Document update or delete pending approval.
Approval Sequence Flow
sequenceDiagram
autonumber
participant U as User
participant LW as Livewire
participant S as Service
participant AE as ApprovalEngine
participant M as Model
participant N as Notification
U->>LW: Submit Form
LW->>LW: Validate Input
LW->>S: create(data)
S->>S: DB::transaction()
S->>M: Repository.create()
S->>AE: checkSetupExists('CREATE', Model)
alt Approval Required
AE-->>S: Setup Found
S->>AE: createRequest(setupId, type, recordId)
AE->>N: Notify First Approver
S-->>LW: Return (Status: PENDING)
else No Approval Required
AE-->>S: null
S->>M: status = 'APPROVED'
S->>M: fireCreateEvent()
S-->>LW: Return (Status: APPROVED)
end
LW-->>U: Success Toast
Critical Rule
Events are ONLY fired after final approval. If approval is required, the
fireCreateEvent() method is NOT called until the approval
chain completes. This prevents side effects (GL posting, stock updates) from executing prematurely.
ApprovalEngineService
The ApprovalEngineService is the central service managing all approval workflows.
It provides methods to check setup existence, create requests, and process approvals/rejections.
Core Methods
// Check if approval is required for an operation
$setup = $this->approvalEngine->checkSetupExists(
'CREATE', // Action: CREATE, UPDATE, DELETE
Transfer::class // Model class
);
// Create an approval request
$this->approvalEngine->createRequest(
$setupId, // ApprovalSetup ID
'TRANSFER_CREATE_APPROVAL', // Notification type key
$recordId // The document's ID
);
// Approve a step (called by approver)
$this->approvalEngine->actOnRequest(
$request, // ApprovalRequest instance
$remarks, // Approver's comments
$signature // Optional digital signature
);
// Reject a request
$this->approvalEngine->rejectRequest(
$request, // ApprovalRequest instance
$remarks // Rejection reason
);
Service Integration Pattern
public function create(array $data): Model
{
return DB::transaction(function () use ($data) {
$record = $this->repository->create($data);
if ($setup = $this->approvalEngine->checkSetupExists('CREATE', Model::class)) {
// ⚠️ Approval required - create request, DON'T fire event
$this->approvalEngine->createRequest(
$setup->id,
'MODEL_CREATE_APPROVAL',
$record->id
);
} else {
// ✅ No approval - auto-approve and fire event
$record->status = 'APPROVED';
$record->save();
$record->fireCreateEvent();
}
return $record;
});
}
Notification Configuration
Approval notification templates are defined in config/approvals.php.
Each notification type has a title, action URL, and message template with placeholders.
config/approvals.php
return [
'TRANSFER_CREATE_APPROVAL' => [
'title' => 'Transfer Approval Required',
'action_url' => '/approvals/{id}',
'message' => '{causer} requests approval for transfer #{display}',
'type' => 'TRANSFER_CREATE_APPROVAL',
],
'TRANSFER_CREATE_REJECTED' => [
'title' => 'Transfer Rejected',
'action_url' => '/approvals/{id}',
'message' => 'Transfer #{display} has been rejected',
'type' => 'TRANSFER_CREATE_REJECTED',
],
'TRANSFER_CREATE_APPROVED' => [
'title' => 'Transfer Approved',
'action_url' => '/transfers/{id}',
'message' => 'Transfer #{display} has been approved',
'type' => 'TRANSFER_CREATE_APPROVED',
],
// ... more notification types
];
Placeholders
{id}— Approval request ID{causer}— User who initiated{display}— Document display name
File Location
config/approvals.php
ProvidesApprovalData Interface
Models supporting the approval workflow must implement the ProvidesApprovalData
interface. This provides the approval system with the data needed for display and audit purposes.
Interface Definition
interface ProvidesApprovalData
{
/**
* Returns data to display in the approval UI
* and store in the audit trail.
*/
public function getApprovalData(): array;
}
Model Implementation
class Transfer extends Model implements ProvidesApprovalData
{
/**
* Display name shown in approval notifications
*/
public function getApprovalDisplayNameAttribute(): string
{
return $this->transfer_number ?? 'TRANSFER-' . $this->getKey();
}
/**
* Full data for approval display and audit
*/
public function getApprovalData(): array
{
// Ensure relationships are loaded
$this->loadMissing(['items', 'fromWarehouse', 'toWarehouse']);
return array_merge($this->toArray(), [
'items' => $this->items->toArray(),
'from_warehouse_name' => $this->fromWarehouse->name,
'to_warehouse_name' => $this->toWarehouse->name,
]);
}
}