This document describes the approval trail archiving system that ensures clean approval restarts when matrices or memos are returned by HOD.
When a Head of Division (HOD) returns a matrix to draft or returns a memo, the system automatically archives all previous approval trails to ensure the approval process can restart cleanly without interference from old approval records.
is_archived: boolean (default: 0)['is_archived', 'activity_id']is_archived: boolean (default: 0)['is_archived', 'model_id', 'model_type']All approval trail creation points now explicitly set is_archived = 0:
protected $fillable = [
// ... existing fields
'is_archived',
];
protected $casts = [
// ... existing casts
'is_archived' => 'boolean',
];
// New scope methods
public function scopeActive($query)
{
return $query->where('is_archived', 0);
}
public function scopeArchived($query)
{
return $query->where('is_archived', 1);
}
protected $fillable = [
// ... existing fields
'is_archived',
];
protected function casts(): array
{
return [
// ... existing casts
'is_archived' => 'boolean',
];
}
// New scope methods
public function scopeActive($query)
{
return $query->where('is_archived', 0);
}
public function scopeArchived($query)
{
return $query->where('is_archived', 1);
}
function archive_approval_trails($model)
{
// For matrices, only archive when approval_order = 0 (draft/returned state)
if ($modelType === 'App\Models\Matrix') {
// Only archive if matrix is at approval_order 0 (draft or returned state)
if ($model->approval_level != 0) {
return 0; // Skip archiving for matrices not at approval_order 0
}
// Archive approval trails for the matrix
$archivedCount = ApprovalTrail::where('model_id', $model->id)
->where('model_type', get_class($model))
->where('is_archived', 0)
->update(['is_archived' => 1]);
// Also archive activity approval trails
$activityArchivedCount = ActivityApprovalTrail::where('matrix_id', $model->id)
->where('is_archived', 0)
->update(['is_archived' => 1]);
return $archivedCount;
} else {
// For memos, archive when returned (any approval level)
$archivedCount = ApprovalTrail::where('model_id', $model->id)
->where('model_type', get_class($model))
->where('is_archived', 0)
->update(['is_archived' => 1]);
return $archivedCount;
}
}
if($request->action !=='approved'){
$matrix->forward_workflow_id = (intval($matrix->approval_level)==1)?null:1;
$matrix->approval_level = ($matrix->approval_level==1)?0:1;
$matrix->overall_status ='returned';
// Archive approval trails to restart approval process
archive_approval_trails($matrix);
$notification_type = 'returned';
}
if ($action !== 'approved') {
$model->forward_workflow_id = intval($model->approval_level)==1?NULL:$model->forward_workflow_id;
$model->approval_level = intval($model->approval_level)==1?0:1;
$model->overall_status = 'returned';
// Archive approval trails to restart approval process
archive_approval_trails($model);
}
All approval checking functions now exclude archived trails:
function done_approving($matrix)
{
$approval = ApprovalTrail::where('model_id', $matrix->id)
->where('model_type', get_class($matrix))
->where('approval_order', '>=', $matrix->approval_level)
->where('staff_id', $user['staff_id'])
->where('is_archived', 0) // Only consider non-archived trails
->orderByDesc('id')
->first();
return $approval && $approval->action === 'approved';
}
public function hasUserApproved(Model $model, int $userId): bool
{
$approval = ApprovalTrail::where('model_id', $model->id)
->where('model_type', get_class($model))
->where('approval_order', $model->approval_level)
->where('staff_id', $userId)
->where('is_archived', 0) // Only consider non-archived trails
->first();
return $approval !== null && $approval->action === 'approved';
}
function done_approving_activty($activity)
{
$latest_approval = ActivityApprovalTrail::where('activity_id', $activity->id)
->where('matrix_id', $activity->matrix_id)
->where('approval_order', $activity->matrix->approval_level)
->where('staff_id', $user['staff_id'])
->where('action', 'passed')
->where('is_archived', 0) // Only consider non-archived trails
->orderByDesc('id')
->first();
return isset($latest_approval->action);
}
approval_order = 0 (draft/returned state)$matrix->approval_level == 0approval_trails for the matrix + all activity_approval_trails for activities in the matrixapproval_trails for the memo// Get only non-archived approval trails
$activeTrails = ApprovalTrail::active()->get();
// Get only archived approval trails
$archivedTrails = ApprovalTrail::archived()->get();
// Get active trails for specific model
$modelTrails = ApprovalTrail::active()
->forModelInstance($matrix)
->get();
// These functions automatically exclude archived trails
$canApprove = can_take_action($matrix);
$hasApproved = done_approving($matrix);
$userApproved = $approvalService->hasUserApproved($matrix, $userId);
// Manually archive trails for a specific model
$archivedCount = archive_approval_trails($matrix);
echo "Archived {$archivedCount} approval trails";
// Test the archiving system
$matrix = Matrix::find(7);
$archivedCount = archive_approval_trails($matrix);
echo "Archived {$archivedCount} trails";
// Test that approval functions ignore archived trails
$matrix = Matrix::find(7);
echo "Can take action: " . (can_take_action($matrix) ? 'Yes' : 'No');
echo "Done approving: " . (done_approving($matrix) ? 'Yes' : 'No');
php artisan migrate
php artisan migrate:rollback --step=2
-- Check archived vs active trails
SELECT
is_archived,
COUNT(*) as count
FROM approval_trails
GROUP BY is_archived;
-- Check activity approval trails
SELECT
is_archived,
COUNT(*) as count
FROM activity_approval_trails
GROUP BY is_archived;
The system logs archiving activities:
Log::info("Archived approval trails for matrix return", [
'matrix_id' => $modelId,
'approval_trails_archived' => $archivedCount,
'activity_approval_trails_archived' => $activityArchivedCount
]);
- Check that all approval functions include ->where('is_archived', 0)
- Verify the scope methods are being used correctly
- Ensure archive_approval_trails($model) is called in return logic
- Check that the function is available in the scope
- Ensure indexes are created on is_archived columns
- Consider adding composite indexes for common queries
// Check if trails are being archived
$matrix = Matrix::find(7);
$trails = ApprovalTrail::where('model_id', $matrix->id)->get();
echo "Total: " . $trails->count();
echo "Active: " . $trails->where('is_archived', 0)->count();
echo "Archived: " . $trails->where('is_archived', 1)->count();
This system ensures that approval processes can restart cleanly after returns while maintaining complete historical records for audit purposes.