PERM Performance Management Module

Performance Management

Enterprise appraisal lifecycle, multi-evaluator scoring, bell curve analytics, PIP & TNA automation — GB5 PERM module

15
Database Tables
26+
API Endpoints
360°
Multi-evaluator
5
Dapr Events Published

Vision

PERM replaces the legacy WCF/NHibernate appraisal system with a cloud-native, multi-tenant performance platform on .NET 9. It supports routine, annual, and 360-degree review cycles with configurable KRA/Measure weighting, automated bell curve distribution, PIP auto-creation, TNA generation, and pay revision triggers — all inside a single finalization transaction.

Business Value

Consistent Scoring

Weighted formula ensures objective scoring across all evaluators; removes subjectivity bias

Automated Compliance

TNA and PIP triggers fire automatically at finalization; no manual HR intervention needed

Pay Revision Safety

PayRevisionTriggered flag prevents duplicate salary changes across system restarts

Bell Curve Analytics

Real-time grade distribution across org, department, or grade — never cached, always live

Scope Boundaries

PERM OwnsOut of Scope (handled by)
KRA, Measure, RatingScale, AppraisalType mastersGoal/OKR tracking → OKR module
Appraisal cycle creation & activationPeer feedback requests → FBCK module
Multi-evaluator (Self/Manager/Peer/Subordinate)Competency profiling → CMGM module
Weighted scoring engine + grade mappingLearning course enrollment → future Learning module
PIP plans + action checkpointsSkill level management → SkillManagement module
TNA request generation
Pay revision trigger (via IPayRevisionBLL)
Performance trend time-series
Employee ESS appraisal view

Key Concepts Glossary

TermDefinition
KRAKey Result Area — a named performance dimension (e.g., Revenue Target, Customer Satisfaction). Defined in MKRA master. Each KRA has a weightage in a given AppraisalType.
MeasureA specific, measurable sub-criterion under a KRA (e.g., "Monthly Revenue Achieved"). Can be Quantitative (numeric target vs achieved) or Qualitative (rating).
RatingScaleGrade bands (e.g., A+=90-100%, B=60-70%) stored in MRATINGSCALE. Used to map FinalPercentage to a GradeLetter.
AppraisalTypeTemplate defining which KRAs and Measures appear in an appraisal, their weightages, and the Nature (Self/360/Peer).
AppraisalPlanA cycle instance: "FY2026 Annual Review" with PerformanceFrom/To dates, linked to an AppraisalType. One plan generates one TAPPRAISAL per employee.
Appraisal (TAPPRAISAL)One row per employee per plan. Stores consolidated FinalPercentage, grade, increment amounts, and PayRevisionTriggered flag.
AppraisalEvalOne row per evaluator per appraisal. EvalType: 0=Self, 1=Manager, 2=Peer, 3=Subordinate.
FinalPercentageWeighted consolidation of all evaluator scores: (SelfScore × SelfWeight) + (ManagerScore × (1-SelfWeight)).
PIPPerformance Improvement Plan — auto-created when FinalPercentage < PIPTriggerThreshold (default 40%).
TNATraining Needs Analysis — auto-generated when KRAScore < TNAGapThreshold (default 60%). Feeds into future Learning module via Dapr.
Bell CurveDistribution of employee grades across rating bands. Read-only aggregation; never cached.
PayRevisionTriggeredBoolean flag on TAPPRAISAL preventing duplicate salary changes on retry/re-finalization.
ESSEmployee Self-Service — read-only view for employees of their own appraisal data. EmployeeId always from login.UserId.

Database Schema — Master Tables

MAPPRAISALTYPE — Appraisal Form Templates

ColumnTypeNotes
AppraisalTypeIdint PK
Codenvarchar(20)Unique per tenant
Namenvarchar(200)
Naturesmallint0=Self 1=ReportingTo 2=360 3=Peer
Statussmallint0=Inactive, 1=Active
TenantIdintMulti-tenant isolation

MKRA — Key Result Area Master

ColumnTypeNotes
KRAIdint PK
Code / Namenvarchar
Naturesmallint0=PerformanceBased, 1=PersonalDevelopment
TenantIdint

MMEASURE — Performance Measure Master

ColumnTypeNotes
MeasureIdint PK
Code / Namenvarchar
MeasureTypesmallint0=Quantitative, 1=Qualitative
Unitnvarchar(50)%, $, count, etc.
TenantIdint

MRATINGSCALE — Grade Bands

ColumnTypeNotes
RatingScaleIdint PK
GradeLetter / GradeNamenvarchare.g., A+, Outstanding
SlabFrom / SlabTodecimal(5,2)Percentage band. Index on (TenantId, SlabFrom, SlabTo)
SortOrderint

MAPPRAISALTYPEKRA — KRAs in a Form Template

ColumnTypeNotes
AppraisalTypeKRAIdint PK
AppraisalTypeIdint FK
KRAIdint FK MKRAUnique per (AppraisalTypeId, KRAId, TenantId)
KRAMeasureWeightagedecimal(5,2)% weight of Measure scores for this KRA
KRAGoalWeightagedecimal(5,2)% weight of OKR goal progress for this KRA
SortOrderint

MPERMCONFIG — Configurable Thresholds

ConfigKeyDefault ValuePurpose
PIPTriggerThreshold40Auto-create PIP when FinalPercentage below this
TNAGapThreshold60Auto-generate TNA when KRAScore below this
SelfWeightDefault30Self-evaluation weight % in final score consolidation

Transactional Tables

MAPPRAISALPLAN — Appraisal Cycle

ColumnTypeNotes
AppraisalPlanIdint PK
Code / Namenvarchare.g., "FY2026-ANNUAL"
AppraisalTypeIdint FK
PerformanceFrom / PerformanceTodatePerformance window
EntryFrom / EntryTodateSelf-evaluation entry window
NaturesmallintAnnual / Mid-year / Confirmation
Statussmallint0=Pending 1=Active 2=Closed 3=Archived
Index: (TenantId, Status, PerformanceFrom, PerformanceTo) — critical for active plan queries

TAPPRAISAL — Appraisal Instance (one per employee per plan)

ColumnTypeNotes
AppraisalIdint PK
AppraisalPlanId / EmployeeId / OUIDintUnique: (AppraisalPlanId, EmployeeId, TenantId)
OverallMeasureScore / OverallGoalScoredecimalComputed by scoring engine
FinalPercentagedecimal(5,2)Consolidated final score 0-100
PercentIncrease / AmountIncreasedecimalIncrement awarded
ToDesignationId / ToGradeIdintPromotion target
PayRevisionTriggeredbitIdempotency flag — prevents duplicate pay revision
Statussmallint0=Pending 1=Submitted 2=Accepted 3=Rejected 4=Finalized

TAPPRAISALEVAL — Per-Evaluator Assessment

ColumnTypeNotes
AppraisalEvalIdint PK
AppraisalId / EvaluatorIdint
EvalTypesmallint0=Self, 1=Manager, 2=Peer, 3=Subordinate
AppraisalAcceptancesmallint0=Pending, 1=Accepted, -1=Rejected
OverallScore / Remarks / SubmittedOn

TPIPPLAN — Performance Improvement Plan

ColumnNotes
PIPPlanId, EmployeeId, ManagerIdOne PIP per underperforming employee
StartDate / EndDateAuto-set: Today to Today+90d on auto-create
ReasonAuto-populated from appraisal grade
Status0=Draft 1=Active 2=Closed 3=Cancelled

TTNAREQUEST — Training Needs Analysis

ColumnNotes
TNARequestId, AppraisalId, EmployeeId, KRAId, MeasureIdOne row per KRA that scored below threshold
GapScoreTNAGapThreshold - KRAScore (magnitude of gap)
TrainingSuggestionFree-text or auto-populated
Status0=Pending, 1=SentToLearning, 2=Closed

Appraisal Lifecycle

Plan Created
→
Plan Activated
→
Appraisals Generated
→
Self Evaluation
→
Manager / Peer Eval
→
WIP Submit
→
Finalized
→
Archived

Each AppraisalPlan generates one TAPPRAISAL row per employee. Evaluators (self + manager + optional peers) each get a TAPPRAISALEVAL row. The WIP workflow gates SubmitAppraisal. Finalization runs the cascade (scoring → PIP → TNA → PayRevision → Dapr events) inside a single transaction.

Weighted Scoring Formula

Per Evaluator:
KRAMeasureScore = Σ(MeasureScore[i] × MeasureWeightage[i]) / ΣWeightages × KRAMeasureWeightage
KRAGoalScore = GoalCurrentProgress × KRAGoalWeightage
EvalOverallScore = Σ(KRAMeasureScore_weighted) + Σ(KRAGoalScore_weighted)

Consolidation (from MPERMCONFIG.SelfWeightDefault = 30%):
FinalPercentage = (SelfEvalScore × 0.30) + (ManagerEvalScore × 0.70)

Grade Assignment:
SELECT GradeLetter FROM MRATINGSCALE
WHERE FinalPercentage BETWEEN SlabFrom AND SlabTo AND TenantId = @TenantId
The goal score component (KRAGoalScore) reads live OKR progress via IGoalDAL.GetGoalProgressForEmployee(employeeId, okrCycleId, kraId) — a DAL-level cross-module call to OKRDAL avoiding circular BLL references.

FinalizeAppraisal Cascade

The most critical flow — runs inside BeginTransactionAsync. All 6 steps must succeed or the entire transaction rolls back.

① CalculateScores

Run weighted scoring engine → store FinalPercentage + GradeLetter on TAPPRAISAL

② PIP Check

If FinalPercentage < MPERMCONFIG['PIPTriggerThreshold'] AND no active TPIPPLAN for employee → auto-create TPIPPLAN (Status=Draft, EndDate=Today+90d) → publish perm.pip.created

③ TNA Check

For each TAPPRAISALEVALKRA where KRAScore < MPERMCONFIG['TNAGapThreshold'] → INSERT TTNAREQUEST (KRAId, GapScore, Status=0) → publish perm.tna.generated

④ Pay Revision Trigger

If PercentIncrease > 0 AND PayRevisionTriggered = false → call IPayRevisionBLL.SavePayRevision(...) → set PayRevisionTriggered = 1. On PayRevision failure: log error, mark TAPPRAISAL.PayRevisionError = true, do NOT abort finalization (re-trigger endpoint available)

⑤ Commit Transaction

All database writes committed atomically

⑥ Publish Dapr Events (outside transaction)

Publish perm.appraisal.finalized and perm.score.finalized to notify Analytics, CMGM

Bell Curve Distribution

-- BellCurveQB.GET_BELL_CURVE_REPORT
SELECT rs.GradeLetter, rs.GradeName,
       COUNT(a.AppraisalId) AS EmployeeCount,
       CAST(COUNT(a.AppraisalId)*100.0 / SUM(COUNT(a.AppraisalId)) OVER() AS decimal(5,1)) AS Percentage
FROM   TAPPRAISAL a
JOIN   MRATINGSCALE rs ON a.FinalPercentage BETWEEN rs.SlabFrom AND rs.SlabTo
WHERE  a.AppraisalPlanId = @PlanId AND a.TenantId = @TenantId
  AND  (@OUID = 0 OR a.OUID = @OUID)
  AND  (@GradeId = 0 OR a.ToGradeId = @GradeId)
GROUP BY rs.GradeLetter, rs.GradeName, rs.SlabFrom
ORDER BY rs.SlabFrom DESC
Cache policy: NOT_REQUIRED — bell curve data changes throughout an active appraisal window. Never cache financial-adjacent HR data.

ESS — Employee Self-Service

GetMyPerformanceSummary is an aggregation endpoint that assembles one unified view from multiple modules:

DataSource
Current appraisal + KRA scores + gradePERMDAL direct query
Active OKR goals progressOKR module (TGOAL)
Feedback summary (received, pending)FBCK module via IFeedbackBLL.GetFeedbackSummaryForESS
Active PIP statusPERMDAL TPIPPLAN
Recent recognitions (last 30 days)FBCK module TRECOGNITION
Unacknowledged memosFBCK module TMEMO Status=1
Security: EmployeeId is always taken from login.UserId — never from the request body or URL parameter. Passing a different EmployeeId in the request is silently ignored.

API Endpoints Reference

Masters

MethodRouteDescriptionCache
GET/PerfMgmt/KRA/GetKRAListAll KRAs for tenantCLIENT_LEVEL 10 min
POST/PerfMgmt/KRA/SaveKRACreate/update KRAnull (invalidates)
DELETE/PerfMgmt/KRA/DeleteKRADelete KRAnull
GET/PerfMgmt/Measure/GetMeasureListAll measuresCLIENT_LEVEL 10 min
GET/PerfMgmt/RatingScale/GetRatingScaleListGrade bandsCLIENT_LEVEL 30 min
GET/PerfMgmt/AppraisalType/GetAppraisalTypeListForm templatesCLIENT_LEVEL 10 min

Appraisal Plan (Cycle)

MethodRouteDescriptionCache
GET/PerfMgmt/AppraisalPlan/GetAppraisalPlanListAll cycles, pagedCLIENT_LEVEL 5 min
POST/PerfMgmt/AppraisalPlan/SaveAppraisalPlanCreate/update plannull
POST/PerfMgmt/AppraisalPlan/ActivateAppraisalPlanActivate → generates TAPPRAISAL rowsnull
POST/PerfMgmt/AppraisalPlan/CloseAppraisalPlanClose cyclenull

Appraisal Instance

MethodRouteDescriptionCache
GET/PerfMgmt/Appraisal/GetAppraisalListPaged; filter by PlanId/OUID/StatusNOT_REQUIRED
POST/PerfMgmt/Appraisal/SaveAppraisalUpdate appraisal fieldsnull
POST/PerfMgmt/Appraisal/SubmitAppraisalTriggers WIP workflownull
POST/PerfMgmt/Appraisal/AcceptAppraisalManager acceptsnull
POST/PerfMgmt/Appraisal/RejectAppraisalManager rejects with remarksnull
POST/PerfMgmt/Appraisal/FinalizeAppraisalRuns full cascade (scoring→PIP→TNA→PayRevision)null

Analytics & ESS

MethodRouteParamsCache
GET/PerfMgmt/BellCurve/GetBellCurveReportAppraisalPlanId, GroupBy(0=Org/1=Dept/2=Grade), OUID, GradeIdNOT_REQUIRED
GET/PerfMgmt/Trend/GetPerformanceTrendEmployeeId, LastNCycles(default 5)USER_LEVEL
GET/PerfMgmt/ESS/GetMyAppraisal—NOT_REQUIRED
GET/PerfMgmt/ESS/GetMyPerformanceSummary—NOT_REQUIRED

Sample Scenarios

Scenario A — Annual Appraisal with 2 KRAs

Plan Setup

FieldValue
Plan NameFY2026 Annual Review
PerformanceFrom2025-04-01
PerformanceTo2026-03-31
EntryWindow2026-04-01 to 2026-04-30
KRA 1 (Revenue Target)Weight: 60%
KRA 2 (Customer Satisfaction)Weight: 40%

Scoring Calculation

EvaluatorRevenue KRACustomer KRATotal
Self78%70%74.8%
Manager72%68%70.4%
FinalPercentage = (74.8 × 0.30) + (70.4 × 0.70)
= 22.44 + 49.28 = 71.72%
Grade: B+ (70–80%) → "Above Average"

Scenario B — Low Performer: PIP Auto-Trigger

-- Employee: Sarah Lee, FinalPercentage = 36% (threshold = 40%)
-- FinalizeAppraisal cascade step 2:

IF @FinalPercentage < 40 AND NOT EXISTS (
    SELECT 1 FROM TPIPPLAN WHERE EmployeeId=@EmployeeId AND Status IN (0,1) AND TenantId=@TenantId
)
BEGIN
    INSERT INTO TPIPPLAN (EmployeeId, ManagerId, StartDate, EndDate, Reason, Status, TenantId)
    VALUES (@EmployeeId, @ManagerId, GETUTCDATE(), DATEADD(d, 90, GETUTCDATE()),
            'Below performance threshold (36%)', 0, @TenantId)

    -- Dapr publish: perm.pip.created
END

Scenario C — Bell Curve Visualization

FY2026 Annual Review — Grade Distribution (102 employees)

A+ (90-100)
5
5 employees
A (80-90)
18
18 employees
B+ (70-80)
42
42 employees (largest band)
B (60-70)
28
28 employees
C (40-60)
7
7 employees
PIP (<40)
2
2 employees → PIP auto-created

Scenario D — Performance Trend (Last 5 Cycles)

CycleFinalPercentageGradeIncrement %Trend
FY202261.2%B5%—
FY202365.8%B6%↑ +4.6%
FY202470.4%B+8%↑ +4.6%
FY202574.1%B+9%↑ +3.7%
FY202671.7%B+8%↓ -2.4%

Integration Map

OKR Module
Provides goal progress scores via IGoalDAL.GetGoalProgressForEmployee (DAL-level, no BLL-BLL circular ref)
PERM
Port 5121
/perm/ proxy prefix
PayRoll Module
IPayRevisionBLL.SavePayRevision called on finalization (direct BLL injection — only cross-module direct call)
FBCK Module
IFeedbackBLL.GetFeedbackSummaryForESS called by PermESSBLL for GetMyPerformanceSummary aggregation
CMGM Module
Subscribes to perm.appraisal.finalized → auto-regenerates competency profile. Reads KRA scores via IAppraisalEvalDAL
Analytics
Dapr: perm.score.finalized → PerformanceKPISubscriber ingests into TPERFKPIFEED
Dapr Events Published
perm.appraisal.submitted perm.appraisal.finalized perm.score.finalized perm.pip.created perm.tna.generated

SignalR — PermNotificationHub

PropertyValue
Group strategyperm:user:{userId} — per-user (not session)
Events pushedEvaluator submitted, appraisal accepted, appraisal rejected, PIP created
Hub typeHub<IPermNotificationClient> — strongly typed
Server pushPermBLL injects IHubContext<PermNotificationHub, IPermNotificationClient>

Caching Strategy

DataCache LevelTTLInvalidated By
MAPPRAISALTYPE / MKRA / MMEASURE mastersCLIENT_LEVEL10 minSave, Delete
MRATINGSCALE bandsCLIENT_LEVEL30 minSave, Delete
MAPPRAISALPLAN listCLIENT_LEVEL5 minSave, Activate, Close
TAPPRAISAL single + TAPPRAISALEVALNOT_REQUIRED—Live: evaluation changes constantly
Bell Curve reportNOT_REQUIRED—Live: financial-adjacent HR data
Performance Trend per employeeUSER_LEVEL—On new cycle finalization

Security & Compliance

Multi-Tenancy

Every SQL query includes AND TenantId = @TenantId (or DatabaseName = @DatabaseName). Cross-tenant data access will never occur by design.

ESS Isolation

All ESS endpoints extract EmployeeId from login.UserId server-side. Request parameters for EmployeeId are ignored — employees cannot view each other's appraisals.

Idempotency

PayRevisionTriggered flag prevents double salary changes. FinalizeAppraisal can be retried safely; only un-triggered pay revisions will fire.

Sensitive Data

LoginDTO fields, salary amounts, and appraisal scores are never logged. Stack traces never returned to client — generic error message only.

Implementation Phases

PhaseScopeDuration
Phase 1KRA, Measure, RatingScale, AppraisalType CRUD; constants; MEVENTTYPE seed; legacy master data import2 weeks
Phase 2AppraisalPlan CRUD + status transitions; TAPPRAISAL generation; multi-evaluator CRUD; paged list; WIP config2 weeks
Phase 3CalculateScores weighted algorithm; FinalizeAppraisal; AcceptAppraisal/RejectAppraisal; grade mapping; Bell Curve1.5 weeks
Phase 4FinalizeAppraisal cascade (PIP auto-create, TNA insert, PayRevision trigger); full PIP CRUD; TNA endpoints; Dapr events1.5 weeks
Phase 5GetPerformanceTrend time-series; PermESSBLL aggregation; ESS endpoints; PermNotificationHub SignalR1 week