Skip to the content.

← Back to Activities Plugin

5.6.8 AuthorizationApproval Entity Reference

Last Updated: December 3, 2025
Status: Complete
Scope: Activities Plugin - AuthorizationApproval Entity

Comprehensive technical reference for the AuthorizationApproval entity, covering data model, approval workflow architecture, security implementation, and integration patterns with the multi-level authorization approval system.

Table of Contents

AuthorizationApproval Entity Overview

The AuthorizationApproval entity represents an individual approver’s response within the authorization approval workflow. Each AuthorizationApproval tracks a single approver’s decision (approve/deny) for a specific authorization request, including timing, notes, and secure token validation. This entity enables multi-level approval workflows where multiple approvers may be required.

Core Responsibilities:

Location: plugins/Activities/src/Model/Entity/AuthorizationApproval.php
Extends: App\Model\Entity\BaseEntity
Table Class: Activities\Model\Table\AuthorizationApprovalsTable

Database Schema

Table: activities_authorization_approvals

The AuthorizationApproval entity maps to the activities_authorization_approvals database table with the following structure:

Column Type Null Default Notes
id INT(11) NO Auto Primary key, auto-incrementing
authorization_id INT(11) NO   Foreign key to Authorization entity
approver_id INT(11) NO   Foreign key to Member entity (approver)
authorization_token VARCHAR(255) NO   Secure token for email-based approval validation
requested_on DATE NO   When approval was requested from this approver
responded_on DATE YES NULL When approver provided their response
approved TINYINT(1) NO   Approver’s decision (true = approved, false = denied)
approver_notes VARCHAR(255) YES ’’ Optional notes from approver explaining decision
created TIMESTAMP NO CURRENT Creation timestamp (inherited from BaseEntity)
created_by INT(11) YES NULL User who created record (inherited from BaseEntity)
modified TIMESTAMP NO CURRENT Last modification timestamp (inherited from BaseEntity)
modified_by INT(11) YES NULL User who last modified record (inherited from BaseEntity)

Indexes:

Foreign Keys:

Notes:

Entity Properties

Core Identification Properties

id

authorization_id

approver_id

Approval Workflow Properties

authorization_token

requested_on

responded_on

approved

Notes and Communication Properties

approver_notes

Audit Properties

The following properties are inherited from BaseEntity:

created

created_by

modified

modified_by

Approval Workflow Architecture

Request → Review → Decision → Completion Cycle

1. Request Phase

Trigger: AuthorizationApprovalsTable creates record for each required approver

1. AuthorizationManager identifies required approvers
2. For each approver, create AuthorizationApproval entity:
   - authorization_id: linking to Authorization
   - approver_id: identified approver
   - authorization_token: unique secure token generated
   - requested_on: current date
   - responded_on: NULL (pending response)
3. Save approval record to database
4. Send email notification with approval link (includes token)

State After Request Phase:

Database Entry Example:

INSERT INTO activities_authorization_approvals (
    authorization_id,
    approver_id,
    authorization_token,
    requested_on,
    approved,
    created,
    created_by
) VALUES (
    123,                                -- Authorization ID
    456,                                -- Approver Member ID
    'a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6', -- Secure token
    CURDATE(),                          -- Today's date
    NULL,                               -- No decision yet (actually boolean, set to NULL initially)
    NOW(),                              -- Current timestamp
    789                                 -- User creating record
);

2. Notification Phase

Email system sends notification with secure approval link:

Email URL Example:
https://kmp.example.org/activities/authorization-approvals/respond?
    token=a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6&
    decision=approve

Query Pattern:
SELECT * FROM activities_authorization_approvals
WHERE authorization_token = 'a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6'
  AND responded_on IS NULL

3. Review Phase

Approver evaluates authorization request:

Approver's Actions:
1. Receive email with secure approval link
2. Review authorization details via email or web link
3. Evaluate against activity requirements
4. Make approval or denial decision
5. Optionally add notes explaining decision

4. Decision Phase

Approver submits decision:

When Approver Clicks "Approve" or "Deny":
1. Controller retrieves approval by token
2. Validate token matches pending approval
3. Verify approver_id matches authenticated user
4. Update approval record:
   - responded_on = current date
   - approved = true or false
   - approver_notes = optional text provided
5. Save updated record
6. Trigger AuthorizationManager to process decision

Update Example:

$approval->responded_on = FrozenDate::now();
$approval->approved = true;  // or false for denial
$approval->approver_notes = "Qualifications verified";
$authorizationApprovalsTable->save($approval);

5. Completion Phase

Authorization workflow processes approval outcome:

For Each Approval Response:
1. Check if all required approvals received
2. Count approvals vs. denials vs. pending
3. Transition Authorization status:
   - If unanimous approval → APPROVED status
   - If any denial → DENIED status
   - If pending expires → EXPIRED status
4. Assign role if APPROVED
5. Send notification emails to requestor
6. Update dashboard and navigation badges

Multi-Level Approval Support

The table architecture supports complex approval workflows:

Sequential Approvals

Authorization Request
    ↓
Create AuthorizationApproval #1 for Approver A
    ↓
Approver A reviews and responds
    ↓
Create AuthorizationApproval #2 for Approver B (only after A approves)
    ↓
Approver B reviews and responds
    ↓
Check all responses and determine final status

Parallel Approvals

Authorization Request
    ↓
Create AuthorizationApproval #1 for Approver A
Create AuthorizationApproval #2 for Approver B
Create AuthorizationApproval #3 for Approver C
    ↓
All three approvers review simultaneously
    ↓
Collect responses as they arrive
    ↓
When all responded, check:
  - Unanimous approval → APPROVED
  - Any denial → DENIED

Security Architecture

Token-Based Security

Secure Token System

Token Generation:

$authorization_token = Security::randomString(32);
// Generates cryptographically secure 32-character token

Token Characteristics:

Security Benefits:

Email Approval Workflow

1. Authorization requires approval
2. Generate unique token for each approver
3. Create email-safe approval URLs:
   - Approve: /activities/authorization-approvals/respond?token=XXX&decision=approve
   - Deny: /activities/authorization-approvals/respond?token=XXX&decision=deny
4. Send emails with secure links
5. Approver clicks link and responds
6. Validate token, verify user, record decision

Access Control Integration

Approver Validation:

Audit Trail:

Relationships

Parent Relationships (belongsTo)

authorization

member (as approver)

Query Patterns with Relationships

Get approval with full context:

$approval = $approvalsTable->get($id, [
    'contain' => [
        'Authorizations' => [
            'Activities',
            'Members' => ['Branches']
        ]
    ]
]);

// Access context
echo $approval->authorization->member->sca_name;      // Requester
echo $approval->authorization->activity->name;        // Activity being approved
echo $approval->member->sca_name;                      // Approver
echo $approval->authorization->member->branch->name;  // Branch

Mass Assignment Security

The AuthorizationApproval entity uses CakePHP’s accessible field system to prevent mass assignment vulnerabilities.

Accessible Fields

The following fields can be safely mass assigned through newEntity() or patchEntity():

protected array $_accessible = [
    "authorization_id" => true,
    "approver_id" => true,
    "authorization_token" => true,
    "requested_on" => true,
    "responded_on" => true,
    "approved" => true,
    "approver_notes" => true,
    "authorization" => true,
    "member" => true,
];

Protected Fields

These fields are NOT accessible via mass assignment:

Safe Entity Creation

// Safe: Only accessible fields can be set
$approval = $approvalsTable->newEntity([
    'authorization_id' => $auth->id,
    'approver_id' => $approver->id,
    'authorization_token' => Security::randomString(32),
    'requested_on' => FrozenDate::now()
]);

// Safe: Responding to approval
$approval = $approvalsTable->patchEntity($existing, [
    'responded_on' => FrozenDate::now(),
    'approved' => true,
    'approver_notes' => 'Approved'
]);

Usage Examples

Creating Approval Requests

// Create approval request for single approver
$approval = $authorizationApprovalsTable->newEntity([
    'authorization_id' => $authorization->id,
    'approver_id' => $approver->id,
    'authorization_token' => Security::randomString(32),
    'requested_on' => FrozenDate::now(),
    'approved' => null,
    'approver_notes' => null
]);

if ($authorizationApprovalsTable->save($approval)) {
    // Send email notification with token
    $this->queueMail('Activities', 'approvalRequest', 
        $approver->email_address, 
        [
            'approval' => $approval,
            'authorization' => $authorization,
            'token' => $approval->authorization_token
        ]
    );
}

Creating Multi-Level Approvals

// Create approval for each required approver
$approvers = $authorization->activity->getApproversQuery()
    ->select(['id', 'email_address'])
    ->all();

$approvalRecords = [];
foreach ($approvers as $approver) {
    $approval = $authorizationApprovalsTable->newEntity([
        'authorization_id' => $authorization->id,
        'approver_id' => $approver->id,
        'authorization_token' => Security::randomString(32),
        'requested_on' => FrozenDate::now()
    ]);
    
    if ($authorizationApprovalsTable->save($approval)) {
        $approvalRecords[] = $approval;
        
        // Queue email notification
        $this->queueMail('Activities', 'approvalRequest',
            $approver->email_address,
            ['approval' => $approval, 'authorization' => $authorization]
        );
    }
}

Processing Approval Responses

// Find approval by secure token
$approval = $authorizationApprovalsTable->find()
    ->where(['authorization_token' => $token])
    ->contain(['Authorizations.Activities', 'Authorizations.Members'])
    ->first();

if ($approval && empty($approval->responded_on)) {
    // Update with approver decision
    $approval = $authorizationApprovalsTable->patchEntity($approval, [
        'responded_on' => FrozenDate::now(),
        'approved' => $decision,  // true or false
        'approver_notes' => $notes
    ]);
    
    if ($authorizationApprovalsTable->save($approval)) {
        // Process approval outcome
        $authorizationManager->processApprovalResponse($approval);
        return true;
    }
}

Checking Approval Status

// Get all approvals for authorization
$approvals = $authorizationApprovalsTable->find()
    ->where(['authorization_id' => $authorization->id])
    ->contain(['Approvers'])
    ->all();

// Analyze approval status
$totalApprovals = $approvals->count();
$approvedCount = $approvals->countBy(fn($a) => $a->approved === true);
$deniedCount = $approvals->countBy(fn($a) => $a->approved === false);
$pendingCount = $approvals->countBy(fn($a) => empty($a->responded_on));

// Determine authorization status
if ($deniedCount > 0) {
    $status = 'DENIED';
} elseif ($approvedCount === $totalApprovals && $pendingCount === 0) {
    $status = 'APPROVED';
} else {
    $status = 'PENDING';
}

echo sprintf(
    "Status: %s (Approved: %d, Denied: %d, Pending: %d)",
    $status, $approvedCount, $deniedCount, $pendingCount
);

Finding Pending Approvals

// Get pending approvals for member (as approver)
$pendingApprovals = $authorizationApprovalsTable->find()
    ->where([
        'approver_id' => $member->id,
        'responded_on IS' => null
    ])
    ->contain([
        'Authorizations' => [
            'Activities',
            'Members'
        ]
    ])
    ->orderBy(['requested_on' => 'ASC'])
    ->all();

// Display pending approvals
foreach ($pendingApprovals as $approval) {
    $hoursWaiting = $approval->requested_on->diffInHours(FrozenDate::now());
    echo sprintf(
        "%s requests %s approval for %s (pending %d hours)",
        $approval->authorization->member->sca_name,
        $approval->authorization->activity->name,
        $approval->member->sca_name,
        $hoursWaiting
    );
}

Approval Queue Analytics

// Get member's pending approval count
$pendingCount = AuthorizationApprovalsTable::memberAuthQueueCount($member->id);

// Response time statistics
$stats = $authorizationApprovalsTable->find()
    ->select([
        'count' => $query->func()->count('*'),
        'avg_hours' => $query->func()->avg(
            $query->func()->timestampdiff(
                'HOUR',
                'requested_on',
                'responded_on'
            )
        ),
        'approval_rate' => $query->func()->avg('approved')
    ])
    ->where(['responded_on IS NOT' => null])
    ->first();

// Approval success rate
$successRate = round($stats->approval_rate * 100, 2);
echo sprintf(
    "Approval Analytics: %d responses, %.1f avg hours, %.1f%% approval rate",
    $stats->count,
    $stats->avg_hours,
    $successRate
);

Overdue Approval Tracking

// Find overdue approvals (pending > 7 days)
$overdueApprovals = $authorizationApprovalsTable->find()
    ->where([
        'responded_on IS' => null,
        'requested_on <' => FrozenDate::now()->subDays(7)
    ])
    ->contain([
        'Authorizations' => ['Activities', 'Members'],
        'Approvers'
    ])
    ->orderBy(['requested_on' => 'ASC'])
    ->all();

// Send reminder notifications
foreach ($overdueApprovals as $approval) {
    $daysPending = $approval->requested_on->diffInDays(FrozenDate::now());
    $this->queueMail('Activities', 'approvalReminder',
        $approval->member->email_address,
        [
            'approval' => $approval,
            'days_pending' => $daysPending,
            'authorization' => $approval->authorization
        ]
    );
}

Integration Points

AuthorizationManager Service

The AuthorizationManager service handles approval workflow:

// Request creates approvals
$manager->request($activityId, $memberId, $isRenewal);
// Creates AuthorizationApproval records for each required approver

// Process approval response
$manager->approve($authorizationId, $approverId, $data);
// Updates AuthorizationApproval with decision

// Process denial
$manager->deny($authorizationId, $approverId, $denyReason);
// Updates AuthorizationApproval and sets DENIED status

Email System

Notification delivery for approval workflow:

// Send initial approval request
$this->queueMail('Activities', 'approvalRequest', $email, ['approval' => $approval]);

// Send approval decision notification
$this->queueMail('Activities', 'approvalResponded', $email, ['approval' => $approval]);

// Send reminder for overdue approvals
$this->queueMail('Activities', 'approvalReminder', $email, ['approval' => $approval]);

Pending approval badges and workflow navigation:

// In navigation cell
$pendingCount = AuthorizationApprovalsTable::memberAuthQueueCount($member->id);
if ($pendingCount > 0) {
    echo $this->Html->badge($pendingCount, ['class' => 'bg-warning']);
}

MemberAuthorizationsTrait Integration

The MemberAuthorizationsTrait extends Member entities with authorization-related functionality, specifically for tracking pending approval responsibilities.

Trait Location: plugins/Activities/src/Model/Entity/MemberAuthorizationsTrait.php

Usage in Member Entity:

// In Member entity class
use Activities\Model\Entity\MemberAuthorizationsTrait;

class Member extends KMPIdentity {
    use MemberAuthorizationsTrait;
}

Key Method - getPendingApprovalsCount():

Returns the count of pending authorization approvals where this member is the designated approver.

// Get pending approvals for navigation badge
$pendingCount = $currentMember->getPendingApprovalsCount();
if ($pendingCount > 0) {
    echo "<span class='badge badge-warning'>{$pendingCount}</span>";
}

Query Pattern:

// Filters on approver_id and null responded_on (pending requests)
$approvalsTable->find()
    ->where([
        "approver_id" => $this->id,
        "responded_on is" => null,
    ])
    ->count();

Integration Points:

Activity Management

Approver discovery and requirement validation:

// From Activity entity
$approvers = $activity->getApproversQuery($branchId)->all();

// Number of required approvals
$required = $activity->num_required_authorizors;
$renewalRequired = $activity->num_required_renewers;

Member Management

Approver authentication and authorization validation:

// Verify approver identity and permission
$member = $membersTable->get($approverId);
if (!$member->hasPermission('approve_authorizations')) {
    throw new ForbiddenException('Member cannot approve authorizations');
}

Performance Considerations

Query Optimization

Indexed Queries:

// Uses index on approver_id + responded_on
$pending = $approvalsTable->find()
    ->where([
        'approver_id' => $memberId,
        'responded_on IS' => null
    ]);

// Uses index on authorization_id
$auths = $approvalsTable->find()
    ->where(['authorization_id' => $authId]);

// Uses unique index on authorization_token
$approval = $approvalsTable->find()
    ->where(['authorization_token' => $token])
    ->first();

Efficient Association Loading:

// Bad: N+1 queries
foreach ($approvals as $approval) {
    echo $approval->authorization->member->sca_name;
}

// Good: Single query with contains
$approvals = $approvalsTable->find()
    ->contain(['Authorizations.Members'])
    ->all();

Caching Strategy

// Cache pending count for navigation
$cacheKey = "auth_queue_{$memberId}";
$pending = Cache::remember($cacheKey, function () use ($memberId) {
    return AuthorizationApprovalsTable::memberAuthQueueCount($memberId);
}, 'short');

Source Files