KPI Matrix How-to Guide Public

04 — Salary Integration: KPI-Driven Compensation Progression

04 — Salary Integration: KPI-Driven Compensation Progression Status: Implemented (recommendation generation) — June 2026 reconciliation. Reconciliation notes: Recommendation…

Guide version: r1 Module version: 1.4.0 Updated: 2026-07-22 Estimated time: 23 min 2 views

Status: Implemented (recommendation generation) — June 2026 reconciliation. Reconciliation notes:

  • Recommendation generation is live in Kpi_calculation_engine::_generate_salary_recommendation() and gated by the kpi_salary_auto_trigger setting. It now reads the employee's real grade/step/salary via get_current_salary() and prices steps with Salary_grades_model::compute_salary_for_step() (the earlier 0/0 placeholder stub, doc 02 item C-5, is resolved).
  • Recommendations persist to kpi_salary_recommendations keyed by (employee_id, cycle_id) (see 03_DATABASE_SCHEMA.md §3.9).
  • The approval → salary_history write, consecutive-rating bonuses, annual cap, effective-date resolution, and dual-approval described below are the target spec; verify each against Kpi_salary_recommendations_model before relying on it. Rating→step mapping and flags are config-driven in $config['kpi_default_step_mapping'].

Table of Contents

  1. Overview
  2. Integration Architecture
  3. Rating-to-Step Mapping
  4. Salary Grade Mechanics Recap
  5. Progression Engine
  6. Approval Workflow
  7. Integration with Existing Models
  8. Multi-Period Progression Rules
  9. Edge Cases and Safeguards
  10. Retroactive and Prorated Adjustments
  11. Reporting and Audit Trail
  12. Configuration Settings
  13. Database Changes for Integration
  14. Implementation Sequence

1. Overview

The KPI Matrix module's most strategic output is its ability to automatically recommend salary step progressions based on objective performance data. This document describes how KPI composite scores feed into ERPat's existing salary grade/step system to create a transparent, data-driven compensation progression path.

Design Principles

Principle Description
Recommendation, Not Override KPI scores generate salary change recommendations that require human approval
Grade-Aware Progression respects salary grade boundaries (min/max/step limits)
Backward Compatible Uses existing salary_history and salary_grades infrastructure
Auditable Every recommendation includes the full calculation trail
Configurable Mappings, caps, and rules are admin-configurable, not hardcoded

Data Flow

┌─────────────────────────────────────────────────────────────────┐
│                    KPI Composite Score                          │
│            (from kpi_composite_results table)                   │
└──────────────────┬──────────────────────────────────────────────┘
                   │
                   ▼
┌─────────────────────────────────────────────────────────────────┐
│               Rating Band Mapping                               │
│     4.50–5.00 → Outstanding    │  3.50–4.49 → Exceeds          │
│     2.50–3.49 → Meets          │  1.50–2.49 → Needs Improve    │
│     1.00–1.49 → Unsatisfactory │                                │
└──────────────────┬──────────────────────────────────────────────┘
                   │
                   ▼
┌─────────────────────────────────────────────────────────────────┐
│            Step Recommendation Engine                            │
│                                                                  │
│  Rating Band    │ Step Change │ Cap Check │ Grade Boundary Check │
│  Outstanding    │    +2       │    ✓      │         ✓            │
│  Exceeds        │    +1       │    ✓      │         ✓            │
│  Meets          │     0       │    —      │         —            │
│  Needs Improve  │     0 + PIP │    —      │         —            │
│  Unsatisfactory │     0 + PIP │    —      │    Demotion Flag     │
└──────────────────┬──────────────────────────────────────────────┘
                   │
                   ▼
┌─────────────────────────────────────────────────────────────────┐
│          Salary Recommendation Record                            │
│   (kpi_salary_recommendations table)                             │
│                                                                  │
│   • Current grade + step + salary                                │
│   • Recommended grade + step + salary                            │
│   • Dollar/peso difference                                       │
│   • Full computation log (JSON)                                  │
│   • Status: pending → approved/rejected/deferred                 │
└──────────────────┬──────────────────────────────────────────────┘
                   │  (on approval)
                   ▼
┌─────────────────────────────────────────────────────────────────┐
│         Salary History Record                                    │
│   (existing ci_salary_history table)                             │
│                                                                  │
│   • New monthly_salary, daily_rate, hourly_rate                  │
│   • Status: draft (requires separate approval)                   │
│   • Reason: "KPI Review [cycle_name] — Rating: [band]"          │
│   • salary_grade_id + salary_step populated                      │
└─────────────────────────────────────────────────────────────────┘

2. Integration Architecture

Touchpoints with Existing ERPat Modules

Existing Module Integration Point Direction
Salary Grades (Salary_grades_model) Read grade structure (min/mid/max, steps, increment) KPI → reads
Salary History (Salary_history_model) Read current salary; Write new salary record on approval KPI → reads & writes
Payroll Helper (payroll_helper.php) get_current_salary(), get_salary_grade() for current state KPI → reads
Job Info (team_member_job_info) Synced automatically via sync_to_job_info() on salary_history approval Indirect (via salary_history)
System Logs (system_logs) Audit trail for all KPI-driven salary changes KPI → writes
Roles/Permissions salary_history_create permission required for auto-generated records KPI → checks

New Components

Component Type Purpose
kpi_salary_recommendations DB Table Stores generated salary recommendations with full context
Kpi_salary_recommendations_model Model CRUD + recommendation generation logic
KpiSalaryIntegration Trait Shared logic between KPI and Salary controllers
Settings entries Config Admin-configurable mapping rules

3. Rating-to-Step Mapping

Default Mapping Table

The core mapping between KPI rating bands and salary step changes:

Rating Band Score Range Step Change Additional Action Consecutive Bonus
Outstanding 4.50 – 5.00 +2 steps Eligible for merit bonus flag 3× consecutive = grade promotion eligibility
Exceeds Expectations 3.50 – 4.49 +1 step None 3× consecutive = +1 extra step
Meets Expectations 2.50 – 3.49 0 steps (maintain) None None
Needs Improvement 1.50 – 2.49 0 steps (maintain) PIP triggered 2× consecutive = salary freeze
Unsatisfactory 1.00 – 1.49 0 steps (maintain) Mandatory PIP 2× consecutive = demotion review

Configurable Mapping Structure

Stored in settings table as serialized JSON:

{
    "kpi_step_mapping": {
        "outstanding": {
            "min_score": 4.50,
            "max_score": 5.00,
            "step_change": 2,
            "flags": ["merit_bonus_eligible"],
            "consecutive_rule": {
                "count": 3,
                "action": "grade_promotion_eligible"
            }
        },
        "exceeds": {
            "min_score": 3.50,
            "max_score": 4.49,
            "step_change": 1,
            "flags": [],
            "consecutive_rule": {
                "count": 3,
                "action": "extra_step"
            }
        },
        "meets": {
            "min_score": 2.50,
            "max_score": 3.49,
            "step_change": 0,
            "flags": [],
            "consecutive_rule": null
        },
        "needs_improvement": {
            "min_score": 1.50,
            "max_score": 2.49,
            "step_change": 0,
            "flags": ["pip_triggered"],
            "consecutive_rule": {
                "count": 2,
                "action": "salary_freeze"
            }
        },
        "unsatisfactory": {
            "min_score": 1.00,
            "max_score": 1.49,
            "step_change": 0,
            "flags": ["mandatory_pip"],
            "consecutive_rule": {
                "count": 2,
                "action": "demotion_review"
            }
        }
    }
}

Step Change Calculation Formula

recommended_step = current_step + step_change

// Boundary enforcement
IF recommended_step > grade.total_steps THEN
    IF grade_promotion_allowed THEN
        recommended_grade = next_higher_grade
        recommended_step = 1  // Start at step 1 of new grade
    ELSE
        recommended_step = grade.total_steps  // Cap at max step
        flag = "at_grade_ceiling"
    END IF
END IF

IF recommended_step < 1 THEN
    recommended_step = 1  // Floor at step 1
END IF

// Compute new salary
new_monthly_salary = grade.min_salary + (recommended_step - 1) × grade.step_increment
// Cap check
new_monthly_salary = MIN(new_monthly_salary, grade.max_salary)

4. Salary Grade Mechanics Recap

Current ERPat Salary Grade Structure

From Salary_grades_model::compute_salary_for_step():

Grade Structure:
┌──────────────────────────────────────────────────────────┐
│ Grade N: "Senior Engineer"                                │
│                                                           │
│   min_salary ─── step_increment ─── max_salary           │
│   ₱50,000    ─── ₱5,000 per step ─── ₱70,000           │
│                                                           │
│   Step 1: ₱50,000  (min_salary)                          │
│   Step 2: ₱55,000  (min + 1 × increment)                │
│   Step 3: ₱60,000  (min + 2 × increment)                │
│   Step 4: ₱65,000  (min + 3 × increment)                │
│   Step 5: ₱70,000  (max_salary, capped)                  │
│                                                           │
│   total_steps: 5                                          │
│   step_increment: (max - min) / (total_steps - 1)        │
│                 = (70000 - 50000) / 4 = ₱5,000           │
└──────────────────────────────────────────────────────────┘

Key Salary Helper Functions Used

Function Returns Used For
get_current_salary($user_id) Array with monthly_salary, salary_grade_id, salary_step, source Determining current position
get_salary_grade($user_id) Array with grade_id, grade_name, step, total_steps, min_salary, max_salary, step_increment Grade boundary checks
get_latest_salary_record($user_id) Latest approved salary_history row Checking effective dates

Grade Progression Path

Salary Grades (ordered by grade_level):

Grade 1: Entry Level        ₱15,000 – ₱20,000  (5 steps)
Grade 2: Associate           ₱20,000 – ₱30,000  (5 steps)
Grade 3: Senior              ₱30,000 – ₱45,000  (5 steps)
Grade 4: Lead                ₱45,000 – ₱65,000  (4 steps)
Grade 5: Manager             ₱65,000 – ₱90,000  (4 steps)
Grade 6: Senior Manager      ₱90,000 – ₱120,000 (3 steps)
Grade 7: Director            ₱120,000 – ₱160,000 (3 steps)
Grade 8: Executive           ₱160,000 – ₱220,000 (3 steps)

Within-grade progression: Step changes (horizontal movement)
Cross-grade progression: Grade promotion (vertical movement)

5. Progression Engine

Algorithm: generate_salary_recommendation()

FUNCTION generate_salary_recommendation(user_id, cycle_id):

    // Step 1: Get KPI composite result
    composite = kpi_composite_results WHERE user_id AND cycle_id
    IF composite IS NULL OR composite.status != 'finalized' THEN
        RETURN error("No finalized KPI result for this user/cycle")
    END IF

    // Step 2: Get current salary state
    current = get_current_salary(user_id)
    grade_info = get_salary_grade(user_id)

    IF current.source == 'job_info' AND current.salary_grade_id IS NULL THEN
        // Employee not on graded pay structure
        RETURN warning("Employee not assigned to salary grade — manual review required")
    END IF

    // Step 3: Determine rating band
    band = resolve_rating_band(composite.composite_score)
    mapping = get_step_mapping(band)

    // Step 4: Check consecutive history
    consecutive = count_consecutive_ratings(user_id, band)
    bonus_action = NULL
    IF mapping.consecutive_rule AND consecutive >= mapping.consecutive_rule.count THEN
        bonus_action = mapping.consecutive_rule.action
    END IF

    // Step 5: Calculate new step
    new_step = current.salary_step + mapping.step_change

    // Step 5a: Apply consecutive bonus
    IF bonus_action == 'extra_step' THEN
        new_step = new_step + 1
    END IF

    // Step 6: Boundary checks
    new_grade_id = current.salary_grade_id

    IF new_step > grade_info.total_steps THEN
        IF bonus_action == 'grade_promotion_eligible' OR auto_promote_enabled() THEN
            next_grade = get_next_grade(current.salary_grade_id)
            IF next_grade IS NOT NULL THEN
                new_grade_id = next_grade.id
                new_step = 1  // Start at step 1 of new grade
                grade_info = next_grade  // Use new grade for salary calc
            ELSE
                new_step = grade_info.total_steps  // At highest grade, cap step
                SET flag "at_highest_grade_ceiling"
            END IF
        ELSE
            new_step = grade_info.total_steps  // Cap at max step
            SET flag "at_grade_ceiling"
        END IF
    END IF

    IF new_step < 1 THEN
        new_step = 1
    END IF

    // Step 7: Compute new salary
    new_monthly = grade_info.min_salary + (new_step - 1) * grade_info.step_increment
    new_monthly = MIN(new_monthly, grade_info.max_salary)

    salary_change = new_monthly - current.monthly_salary
    change_percentage = (salary_change / current.monthly_salary) * 100

    // Step 8: Build recommendation record
    recommendation = {
        user_id: user_id,
        cycle_id: cycle_id,
        composite_result_id: composite.id,
        composite_score: composite.composite_score,
        rating_band: band,

        current_grade_id: current.salary_grade_id,
        current_step: current.salary_step,
        current_monthly_salary: current.monthly_salary,

        recommended_grade_id: new_grade_id,
        recommended_step: new_step,
        recommended_monthly_salary: new_monthly,

        salary_change: salary_change,
        change_percentage: change_percentage,

        step_change: mapping.step_change,
        consecutive_count: consecutive,
        bonus_action: bonus_action,
        flags: mapping.flags,

        computation_log: {
            algorithm_version: "1.0",
            timestamp: UTC_NOW,
            inputs: { composite_score, current_step, grade_info },
            mapping_used: mapping,
            boundary_checks: { ... },
            result: { new_step, new_grade_id, new_monthly }
        },

        status: 'pending',
        effective_date: cycle.review_end_date + 1 month (start of next month)
    }

    RETURN recommendation
END FUNCTION

Batch Generation

When a review cycle is finalized, recommendations are generated for ALL employees in that cycle:

FUNCTION generate_batch_recommendations(cycle_id):

    assignments = kpi_employee_assignments WHERE cycle_id AND status = 'active'
    results = { generated: 0, skipped: 0, errors: 0, warnings: 0 }

    FOR EACH assignment IN assignments:
        TRY:
            recommendation = generate_salary_recommendation(
                assignment.user_id, cycle_id
            )

            IF recommendation.type == 'warning' THEN
                results.warnings++
                log_warning(recommendation)
            ELSE
                save_recommendation(recommendation)
                results.generated++
            END IF

        CATCH error:
            results.errors++
            log_error(assignment.user_id, error)
        END TRY
    END FOR

    RETURN results
END FUNCTION

6. Approval Workflow

Recommendation Status Flow

                    ┌──────────┐
                    │ Pending  │ ← auto-generated
                    └────┬─────┘
                         │
              ┌──────────┼──────────┐
              ▼          ▼          ▼
        ┌──────────┐ ┌────────┐ ┌──────────┐
        │ Approved │ │Rejected│ │ Deferred │
        └────┬─────┘ └────────┘ └────┬─────┘
             │                       │
             ▼                       │ (next cycle)
    ┌────────────────┐               │
    │ Salary History │               ▼
    │   Record       │         Re-evaluated
    │ (status=draft) │
    └────────────────┘

Approval Levels

Level Who What They Do
Level 1: HR Manager Users with salary_history_create permission Reviews recommendation, approves/rejects/defers
Level 2: Department Head (optional) Department manager Co-signs if salary change > configurable threshold
Level 3: Admin (optional) System admin Final approval for grade promotions

Approval Actions

Approve:

  1. Recommendation status → approved
  2. New salary_history record created automatically:
    • user_id = recommendation.user_id
    • monthly_salary = recommendation.recommended_monthly_salary
    • daily_rate = computed from monthly ÷ (days_per_year / 12)
    • hourly_rate = daily_rate ÷ hours_per_day
    • salary_grade_id = recommendation.recommended_grade_id
    • salary_step = recommendation.recommended_step
    • status = 'draft' (follows existing salary_history approval flow)
    • reason = "KPI Review: [cycle_name] — Rating: [band] ([score])"
    • effective_date = recommendation.effective_date
  3. System log entry created
  4. Notification sent to employee (optional)

Reject:

  1. Recommendation status → rejected
  2. rejected_by, rejected_at, rejection_reason populated
  3. System log entry created
  4. No salary_history record generated

Defer:

  1. Recommendation status → deferred
  2. deferred_reason, deferred_until populated
  3. Will be re-evaluated in next cycle (or manually before then)
  4. System log entry created

Override Capability

HR can override the recommended step change before approval:

// In Kpi_salary_recommendations controller
function approve_with_override() {
    $id = $this->input->post('id');
    $override_step = (int) $this->input->post('override_step');
    $override_reason = $this->input->post('override_reason');

    // Recalculate salary based on override step
    $recommendation = $this->Kpi_salary_recommendations_model->get_one($id);
    $grade = $this->Salary_grades_model->get_one($recommendation->recommended_grade_id);

    $new_monthly = $grade->min_salary + ($override_step - 1) * $grade->step_increment;
    $new_monthly = min($new_monthly, $grade->max_salary);

    // Save override
    $update_data = array(
        'recommended_step' => $override_step,
        'recommended_monthly_salary' => $new_monthly,
        'override_reason' => $override_reason,
        'overridden_by' => $this->login_user->id,
        'status' => 'approved'
    );

    $this->Kpi_salary_recommendations_model->save($update_data, $id);

    // Create salary history record with overridden values
    $this->_create_salary_history_from_recommendation($id);
}

7. Integration with Existing Models

Reading Current State

// In KPI Salary Integration trait/library

/**
 * Get employee's current salary position for KPI recommendation
 */
function get_employee_salary_position($user_id) {
    // Uses existing payroll helper functions
    $salary_info = get_current_salary($user_id);
    $grade_info = get_salary_grade($user_id);

    return array(
        'has_grade' => !empty($grade_info),
        'grade_id' => $grade_info ? $grade_info['grade_id'] : null,
        'grade_name' => $grade_info ? $grade_info['grade_name'] : null,
        'grade_level' => $grade_info ? $grade_info['grade_level'] : null,
        'current_step' => $grade_info ? $grade_info['step'] : null,
        'total_steps' => $grade_info ? $grade_info['total_steps'] : null,
        'step_increment' => $grade_info ? $grade_info['step_increment'] : null,
        'min_salary' => $grade_info ? $grade_info['min_salary'] : null,
        'max_salary' => $grade_info ? $grade_info['max_salary'] : null,
        'current_monthly' => $salary_info['monthly_salary'],
        'current_daily' => $salary_info['daily_rate'],
        'current_hourly' => $salary_info['hourly_rate'],
        'hours_per_day' => $salary_info['hours_per_day'],
        'days_per_year' => $salary_info['days_per_year'],
        'salary_source' => $salary_info['source'],
        'is_at_ceiling' => ($grade_info && $grade_info['step'] >= $grade_info['total_steps']),
    );
}

Writing Salary History Record

/**
 * Create salary_history record from approved KPI recommendation
 * Follows existing Salary_history::save() pattern
 */
function create_salary_record_from_kpi($recommendation) {
    $position = $this->get_employee_salary_position($recommendation->user_id);

    // Compute derived rates (matching existing salary_history pattern)
    $monthly = $recommendation->recommended_monthly_salary;
    $days_per_year = $position['days_per_year'] ?: 261;
    $hours_per_day = $position['hours_per_day'] ?: 8;
    $days_per_month = $days_per_year / 12;

    $daily_rate = $monthly / $days_per_month;
    $hourly_rate = $daily_rate / $hours_per_day;

    $salary_data = array(
        'user_id' => $recommendation->user_id,
        'effective_date' => $recommendation->effective_date,
        'payroll_basis' => 'salary_based',
        'salary_term' => 'regular',
        'salary_grade_id' => $recommendation->recommended_grade_id,
        'salary_step' => $recommendation->recommended_step,
        'monthly_salary' => round($monthly, 2),
        'daily_rate' => round($daily_rate, 2),
        'hourly_rate' => round($hourly_rate, 2),
        'hours_per_day' => $hours_per_day,
        'days_per_year' => $days_per_year,
        'reason' => sprintf(
            "KPI Review: %s — Rating: %s (%.2f) — Step %d→%d",
            $recommendation->cycle_name,
            ucfirst($recommendation->rating_band),
            $recommendation->composite_score,
            $recommendation->current_step,
            $recommendation->recommended_step
        ),
        'status' => 'draft',  // Follows existing approval workflow
        'created_by' => $this->login_user->id,
        'created_at' => get_current_utc_time()
    );

    $save_id = $this->Salary_history_model->save($salary_data);

    if ($save_id) {
        // Link recommendation to salary_history record
        $this->Kpi_salary_recommendations_model->save(array(
            'salary_history_id' => $save_id
        ), $recommendation->id);

        // System log
        set_system_logs(
            lang('kpi_salary_recommendation_approved') . " for " . get_user_legal_name($recommendation->user_id, false),
            obj_to_arr_serialization((object) $salary_data, array("deleted"), "kpi_salary"),
            "create", "kpi_salary",
            array(
                "user_id" => $this->login_user->id,
                "remarks" => "Auto-generated from KPI cycle " . $recommendation->cycle_id
            ),
            $save_id
        );
    }

    return $save_id;
}

Getting Next Grade

/**
 * Find the next higher salary grade for promotion scenarios
 */
function get_next_grade($current_grade_id) {
    $current = $this->Salary_grades_model->get_one($current_grade_id);

    if (!$current || !$current->grade_level) {
        return null;
    }

    $grades_table = $this->db->dbprefix('salary_grades');

    $sql = "SELECT * FROM $grades_table 
            WHERE grade_level > ? 
            AND status = 'active' 
            AND deleted = 0 
            ORDER BY grade_level ASC 
            LIMIT 1";

    return $this->db->query($sql, array($current->grade_level))->row();
}

8. Multi-Period Progression Rules

Consecutive Rating Tracking

The system tracks consecutive identical (or same-band) ratings to apply bonus/penalty rules:

/**
 * Count consecutive identical rating bands for a user
 * Looks backward from the most recent finalized cycle
 */
function count_consecutive_ratings($user_id, $current_band) {
    $table = $this->db->dbprefix('kpi_composite_results');
    $cycles_table = $this->db->dbprefix('kpi_review_cycles');

    $sql = "SELECT cr.rating_band, c.period_end
            FROM $table cr
            JOIN $cycles_table c ON c.id = cr.cycle_id
            WHERE cr.user_id = ?
            AND cr.status = 'finalized'
            AND cr.deleted = 0
            ORDER BY c.period_end DESC
            LIMIT 10";  // Look back maximum 10 cycles

    $results = $this->db->query($sql, array($user_id))->result();

    $count = 0;
    foreach ($results as $result) {
        if ($result->rating_band === $current_band) {
            $count++;
        } else {
            break;  // Streak broken
        }
    }

    return $count;
}

Progressive Rules Matrix

Scenario Consecutive Count Action Example
Outstanding × 3 3 cycles of 4.50+ Grade promotion eligible Grade 3 Step 5 → Grade 4 Step 1
Exceeds × 3 3 cycles of 3.50–4.49 Extra +1 step +1 normal + 1 bonus = +2 total
Needs Improvement × 2 2 cycles of 1.50–2.49 Salary freeze flag No step change even if rating improves
Unsatisfactory × 2 2 cycles of 1.00–1.49 Demotion review flag HR must review; possible grade demotion

Annual Cap

To prevent excessive salary growth in a single year (e.g., quarterly reviews giving 4× step increases):

annual_step_cap = get_setting('kpi_annual_step_cap', 3)

// Calculate steps already given this calendar year
ytd_steps = SUM of step_changes from approved recommendations 
            WHERE user_id AND YEAR(effective_date) = CURRENT_YEAR

remaining_allowance = annual_step_cap - ytd_steps

// Apply cap
actual_step_change = MIN(recommended_step_change, remaining_allowance)

IF actual_step_change < recommended_step_change THEN
    SET flag "annual_cap_applied"
    computation_log.cap_details = {
        recommended: recommended_step_change,
        allowed: actual_step_change,
        ytd_total: ytd_steps,
        annual_cap: annual_step_cap
    }
END IF

9. Edge Cases and Safeguards

Edge Case 1: Employee Not on Graded Pay

Scenario: Employee has salary in job_info or salary_history but no salary_grade_id.

Handling:

  • Generate recommendation with status = 'manual_review'
  • Flag: "no_salary_grade"
  • Display warning in HR dashboard
  • HR must manually assign grade/step or skip salary recommendation

Edge Case 2: At Grade Ceiling (Max Step)

Scenario: Employee is at Step 5 of 5 in their grade and earns "Exceeds" rating (+1 step).

Handling:

  1. Check kpi_auto_grade_promotion setting
  2. If enabled → attempt grade promotion to next grade, Step 1
  3. If disabled → cap at current max step, set flag "at_grade_ceiling"
  4. If already at highest grade → cap at max step, set flag "at_highest_grade_ceiling"

Edge Case 3: Grade Promotion Salary Decrease

Scenario: Step 5 of Grade 3 = ₱45,000 but Step 1 of Grade 4 = ₱45,000. No actual raise.

Handling:

  • Compare new salary vs current salary
  • If new salary ≤ current salary, set flag "promotion_no_raise"
  • HR can review and optionally start at Step 2+ of new grade
  • UI highlights this condition for reviewer attention

Edge Case 4: Salary Grade Structure Changed Mid-Cycle

Scenario: Admin modifies salary grade min/max/steps between cycle start and recommendation generation.

Handling:

  • Always use current (latest) grade structure at time of recommendation
  • Include grade_snapshot in computation log:
    {
        "grade_snapshot": {
            "grade_id": 3,
            "min_salary": 30000,
            "max_salary": 45000,
            "total_steps": 5,
            "step_increment": 3750,
            "captured_at": "2026-03-15 10:30:00"
        }
    }
  • If grade was modified after recommendation but before approval, show warning

Edge Case 5: Employee Transferred Between Grades

Scenario: Employee was promoted to a new grade between cycle start and end.

Handling:

  • Use the salary position at cycle end date
  • Set flag "grade_changed_during_cycle"
  • Include both old and new grade in computation log

Edge Case 6: No Active Salary Grades in System

Scenario: Organization hasn't configured any salary grades.

Handling:

  • KPI scoring works independently (composite scores always calculated)
  • Salary recommendation generation is skipped
  • Admin dashboard shows setup prompt: "Configure Salary Grades to enable compensation progression"

Edge Case 7: Negative Step Change (Demotion)

Scenario: Policy allows explicit demotion for sustained poor performance.

Handling:

  • Default: Step changes are never negative (maintain at minimum)
  • Optional setting kpi_allow_step_demotion (default: false)
  • If enabled, demotion requires double approval (HR + Admin)
  • System log severity: critical

Edge Case 8: Part-Time or Probationary Employees

Scenario: Employees on probation or part-time may have different progression rules.

Handling:

  • Check salary_term from salary_history (regular, probationary, contractual)
  • Probationary employees: recommendations flagged with "probationary_employee"
  • Configurable: kpi_exclude_probationary setting (default: false)
  • Part-time: Salary computations respect prorated rates

10. Retroactive and Prorated Adjustments

Effective Date Logic

Recommendations use the following effective date calculation:

DEFAULT: First day of month following cycle end date

EXAMPLE:
  Cycle period: Jan 1, 2026 – Mar 31, 2026
  Cycle finalized: Apr 15, 2026
  Recommendation effective_date: May 1, 2026

CONFIGURABLE:
  kpi_effective_date_rule = "next_month_start"  (default)
                          | "cycle_end_plus_days:30"
                          | "manual"  (HR sets date during approval)

Delayed Approval Scenario

If a recommendation is approved after the effective date has passed:

Recommendation effective_date: May 1, 2026
Approval date: Jun 10, 2026

OPTION A (Default): Use original effective_date (May 1)
  → Salary history record effective May 1
  → Payroll may need retroactive adjustment

OPTION B (Configurable): Use approval date
  → Salary history record effective Jun 10
  → No retroactive adjustment needed

Setting: kpi_delayed_approval_policy = "original_date" | "approval_date"

Prorating for Mid-Period Changes

Not directly managed by KPI module — the payroll system handles prorating via existing Salary_history_model::get_rate_for_date() which returns the applicable rate for any given date.


11. Reporting and Audit Trail

System Logs Configuration

Add to application/config/system_logs.php:

// KPI Salary Integration events
'create:kpi_salary' => array(
    'category' => 'Human Resource',
    'module' => 'KPI Salary Integration',
    'component' => 'kpi_salary',
    'action' => 'create',
    'severity' => 'warning',
    'description' => 'KPI salary recommendation was created'
),
'update:kpi_salary' => array(
    'category' => 'Human Resource',
    'module' => 'KPI Salary Integration',
    'component' => 'kpi_salary',
    'action' => 'update',
    'severity' => 'warning',
    'description' => 'KPI salary recommendation was updated'
),
'approve:kpi_salary' => array(
    'category' => 'Human Resource',
    'module' => 'KPI Salary Integration',
    'component' => 'kpi_salary',
    'action' => 'approve',
    'severity' => 'critical',
    'description' => 'KPI salary recommendation was approved (salary record created)'
),
'reject:kpi_salary' => array(
    'category' => 'Human Resource',
    'module' => 'KPI Salary Integration',
    'component' => 'kpi_salary',
    'action' => 'reject',
    'severity' => 'warning',
    'description' => 'KPI salary recommendation was rejected'
),

Salary Progression Report

A new report showing KPI-driven salary changes over time:

Column Source
Employee users table
Department team via FIND_IN_SET
Cycle kpi_review_cycles.cycle_name
Score kpi_composite_results.composite_score
Rating kpi_salary_recommendations.rating_band
Previous Salary kpi_salary_recommendations.current_monthly_salary
New Salary kpi_salary_recommendations.recommended_monthly_salary
Change (%) Computed
Status kpi_salary_recommendations.status
Approved By users table (approver)
Effective Date kpi_salary_recommendations.effective_date

Compensation Impact Summary

Aggregate report for budget planning:

SELECT 
    c.cycle_name,
    COUNT(r.id) as total_recommendations,
    SUM(CASE WHEN r.status = 'approved' THEN 1 ELSE 0 END) as approved_count,
    SUM(CASE WHEN r.status = 'rejected' THEN 1 ELSE 0 END) as rejected_count,
    SUM(CASE WHEN r.status = 'pending' THEN 1 ELSE 0 END) as pending_count,
    SUM(CASE WHEN r.status = 'approved' THEN r.salary_change ELSE 0 END) as total_monthly_impact,
    SUM(CASE WHEN r.status = 'approved' THEN r.salary_change ELSE 0 END) * 12 as total_annual_impact,
    AVG(CASE WHEN r.status = 'approved' THEN r.change_percentage ELSE NULL END) as avg_increase_pct,
    AVG(r.composite_score) as avg_kpi_score
FROM kpi_salary_recommendations r
JOIN kpi_review_cycles c ON c.id = r.cycle_id
WHERE r.deleted = 0
GROUP BY r.cycle_id
ORDER BY c.period_end DESC;

12. Configuration Settings

New Settings (stored in settings table)

Setting Key Default Type Description
kpi_salary_integration_enabled 0 Boolean Master toggle for salary integration
kpi_step_mapping (see Section 3) JSON Rating band → step change mapping
kpi_auto_grade_promotion 0 Boolean Allow automatic grade promotion at ceiling
kpi_annual_step_cap 3 Integer Maximum step increases per calendar year
kpi_allow_step_demotion 0 Boolean Allow negative step changes
kpi_effective_date_rule next_month_start String How to calculate effective date
kpi_delayed_approval_policy original_date String Behavior when approved after effective date
kpi_exclude_probationary 0 Boolean Skip probationary employees
kpi_require_dual_approval 0 Boolean Require two-level approval
kpi_dual_approval_threshold 10 Decimal % increase threshold for dual approval
kpi_auto_generate_on_finalize 1 Boolean Auto-generate recommendations when cycle finalized
kpi_send_recommendation_notification 1 Boolean Notify employees of recommendations
kpi_salary_recommendation_visibility hr_only String hr_only, manager_and_hr, employee_visible

Settings UI Location

Added as a subsection in Settings > Payroll > KPI Salary Integration:

Settings > Payroll
├── General Payroll Config
├── Payslip Templates
├── KPI Salary Integration    ← NEW
│   ├── Enable/Disable toggle
│   ├── Step Mapping Editor (JSON visual editor)
│   ├── Annual Cap setting
│   ├── Auto-promotion toggle
│   ├── Approval settings
│   └── Notification settings
└── Other payroll settings

13. Database Changes for Integration

New Table: kpi_salary_recommendations

CREATE TABLE IF NOT EXISTS `ci_kpi_salary_recommendations` (
    `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,

    -- KPI Context
    `user_id` INT(11) NOT NULL,
    `cycle_id` INT(11) NOT NULL,
    `composite_result_id` INT(11) NOT NULL,
    `composite_score` DECIMAL(4,2) NOT NULL,
    `rating_band` VARCHAR(30) NOT NULL COMMENT 'outstanding, exceeds, meets, needs_improvement, unsatisfactory',

    -- Current State (snapshot at recommendation time)
    `current_grade_id` INT(11) NULL,
    `current_step` INT(11) NULL,
    `current_monthly_salary` DECIMAL(18,2) NOT NULL,

    -- Recommendation
    `recommended_grade_id` INT(11) NULL,
    `recommended_step` INT(11) NULL,
    `recommended_monthly_salary` DECIMAL(18,2) NOT NULL,
    `salary_change` DECIMAL(18,2) NOT NULL DEFAULT 0.00,
    `change_percentage` DECIMAL(6,2) NOT NULL DEFAULT 0.00,

    -- Calculation Context
    `step_change` INT(11) NOT NULL DEFAULT 0,
    `consecutive_count` INT(11) NOT NULL DEFAULT 0,
    `bonus_action` VARCHAR(50) NULL,
    `flags` TEXT NULL COMMENT 'JSON array of flags',
    `computation_log` LONGTEXT NULL COMMENT 'Full calculation audit trail (JSON)',

    -- Override (if HR modifies recommendation before approval)
    `override_step` INT(11) NULL,
    `override_salary` DECIMAL(18,2) NULL,
    `override_reason` TEXT NULL,
    `overridden_by` INT(11) NULL,

    -- Workflow
    `status` VARCHAR(20) NOT NULL DEFAULT 'pending' COMMENT 'pending, approved, rejected, deferred, manual_review',
    `effective_date` DATE NULL,
    `approved_by` INT(11) NULL,
    `approved_at` DATETIME NULL,
    `rejected_by` INT(11) NULL,
    `rejected_at` DATETIME NULL,
    `rejection_reason` TEXT NULL,
    `deferred_reason` TEXT NULL,
    `deferred_until` DATE NULL,

    -- Link to created salary record
    `salary_history_id` INT(11) NULL COMMENT 'FK to salary_history (populated on approval)',

    -- Trailing columns (ERPat standard)
    `created_by` INT(11) NULL,
    `date_created` DATETIME NULL,
    `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    `deleted` TINYINT(1) NOT NULL DEFAULT 0,

    PRIMARY KEY (`id`),
    INDEX `idx_ksr_user` (`user_id`),
    INDEX `idx_ksr_cycle` (`cycle_id`),
    INDEX `idx_ksr_status` (`status`),
    INDEX `idx_ksr_deleted` (`deleted`),
    UNIQUE INDEX `idx_ksr_user_cycle` (`user_id`, `cycle_id`, `deleted`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

Column Additions to Existing Tables

-- Add to kpi_composite_results (link to recommendation)
ALTER TABLE `ci_kpi_composite_results` 
ADD `salary_recommendation_id` INT(11) NULL AFTER `salary_step_recommendation`;

-- Add to kpi_review_cycles (track batch generation)
ALTER TABLE `ci_kpi_review_cycles`
ADD `recommendations_generated` TINYINT(1) NOT NULL DEFAULT 0 AFTER `status`,
ADD `recommendations_generated_at` DATETIME NULL AFTER `recommendations_generated`;

14. Implementation Sequence

Phase Dependencies

Prerequisite: Salary Grades configured (existing feature)
Prerequisite: KPI Review Cycles finalized (from 05_CALCULATION_ENGINE)

Phase 1: Database & Model
    → Create kpi_salary_recommendations table (migration)
    → Create Kpi_salary_recommendations_model
    → Add settings entries

Phase 2: Recommendation Engine
    → Implement generate_salary_recommendation()
    → Implement batch generation
    → Implement consecutive rating logic

Phase 3: Approval Workflow
    → Create recommendation review UI
    → Implement approve/reject/defer actions
    → Implement salary_history integration
    → Add system log events

Phase 4: Reporting
    → Salary progression report
    → Compensation impact summary
    → Budget projection tools

Phase 5: Configuration UI
    → Settings page for mapping editor
    → Visual step mapping builder
    → Threshold configuration

Integration Testing Checklist

  • [ ] Employee with grade/step gets correct recommendation
  • [ ] Employee without grade shows "manual review" flag
  • [ ] At-ceiling employee triggers grade promotion logic
  • [ ] At-highest-grade ceiling caps correctly
  • [ ] Consecutive rating bonus applies after 3 cycles
  • [ ] Annual step cap prevents excessive increases
  • [ ] Approval creates salary_history record with correct values
  • [ ] Rejection does not create salary_history record
  • [ ] Override allows HR to modify before approval
  • [ ] System logs capture all actions
  • [ ] Deferred recommendations carry to next cycle
  • [ ] Probationary exclusion works when enabled
  • [ ] Daily/hourly rates derive correctly from monthly
  • [ ] Effective date calculation follows configured rule

Appendix: Worked Example

Scenario

Employee: Maria Santos, Software Developer
Current Position: Grade 3 (Senior), Step 3 of 5
Current Salary: ₱37,500/month
Grade 3 Range: ₱30,000 – ₱45,000 (step increment: ₱3,750)

Q1 2026 KPI Result:

  • Composite Score: 4.62 (Outstanding)
  • Previous Q4 2025: 4.55 (Outstanding)
  • Previous Q3 2025: 4.71 (Outstanding)

Calculation

1. Rating Band: Outstanding (4.62 is in 4.50-5.00 range)
2. Step Change: +2 (Outstanding mapping)
3. Consecutive Check: 3 consecutive Outstanding ratings!
   → Consecutive rule: grade_promotion_eligible
4. New Step: 3 + 2 = 5 (within Grade 3's 5 steps)
5. But: consecutive bonus triggers grade promotion check

Since at Step 5 of Grade 3 after +2 increase:
   → Check auto_grade_promotion setting: enabled
   → Next grade: Grade 4 (Lead), ₱45,000 – ₱65,000, 4 steps
   → Grade promotion available!

Decision: Promote to Grade 4, Step 1
   → New salary: ₱45,000 (Grade 4, Step 1)
   → Increase: ₱45,000 - ₱37,500 = ₱7,500/month (+20%)

Alternative without promotion:
   → Stay at Grade 3, Step 5: ₱45,000 (cap at max)
   → Increase: ₱45,000 - ₱37,500 = ₱7,500/month (+20%)
   → Flag: at_grade_ceiling (same salary but no promotion path)

Recommendation record:
   current_grade: 3, current_step: 3, current_salary: 37,500
   recommended_grade: 4, recommended_step: 1, recommended_salary: 45,000
   salary_change: +7,500, change_percentage: +20.00%
   flags: ["merit_bonus_eligible", "grade_promotion"]
   bonus_action: "grade_promotion_eligible"
   consecutive_count: 3

What HR Sees

┌─────────────────────────────────────────────────────────┐
│  SALARY RECOMMENDATION                                   │
│                                                           │
│  Employee: Maria Santos (Software Developer)              │
│  KPI Cycle: Q1 2026 — Score: 4.62 ★★★★★ Outstanding     │
│  Consecutive Outstanding: 3 cycles                        │
│                                                           │
│  ┌─────────────┐    ┌─────────────┐                      │
│  │   CURRENT    │ →  │ RECOMMENDED  │                     │
│  │ Grade 3 / S3 │    │ Grade 4 / S1 │    ▲ +₱7,500      │
│  │ ₱37,500/mo   │    │ ₱45,000/mo   │    (+20.00%)      │
│  └─────────────┘    └─────────────┘                      │
│                                                           │
│  ???? Grade Promotion (3× consecutive Outstanding)          │
│  ⭐ Merit Bonus Eligible                                  │
│                                                           │
│  Effective Date: May 1, 2026                              │
│                                                           │
│  [Approve]  [Approve with Override]  [Reject]  [Defer]   │
└─────────────────────────────────────────────────────────┘

Document Version: 1.0
Last Updated: Feb 2026
Module: KPI Matrix — Salary Integration
Depends on: 02_KPI_CATEGORIES_AND_METRICS.md, 03_DATABASE_SCHEMA.md
Next: 05_CALCULATION_ENGINE.md

Was this guide helpful?

Report a content problem