The KPI Matrix module exposes two JSON APIs, both built on ERPat's stateless
JWT-bearer surfaces and both reusing the module's src/Services/* (the same
business logic the web controllers use). All data access goes through models; every
response row is projected through the kpi_api_shape_*() helpers
(helpers/kpi_matrix_helper.php) — no internal columns, display names only.
Shared response envelope (from App\Core\ApiController / EndUserApiController):
// success
{ "ok": true, "data": { /* ... */ } }
// error
{ "ok": false, "error": { "code": "<snake_case>", "message": "<human>", "details": {} } }
1. Client (Integration) API — /api/v1/kpi/*
- Controller:
controllers/Kpi_matrix_api.php(extends App\Core\ApiAuthController) - Audience: external/programmatic systems (reporting/BI).
- Auth: OAuth-style bearer JWT +
X-Company-Key(tenant). Scopekpi:read(declared inconfig/api_scope.php, granted per API client). - Capability: READ-ONLY.
- Routes:
config/api_routes.php· Spec:public/api/v1/openapi.json(php erpat api:openapi --postman).
| Method & path | Purpose |
|---|---|
GET /api/v1/kpi/cycles |
Review cycles (with template name + counts) |
GET /api/v1/kpi/cycles/{id} |
A single cycle |
GET /api/v1/kpi/templates |
KPI templates |
GET /api/v1/kpi/templates/{id} |
A single template plus its weighted metrics |
GET /api/v1/kpi/results?cycle_id= |
Composite results for a cycle (defaults to the active cycle) |
GET /api/v1/kpi/employees/{id}/scores?cycle_id= |
One employee's scores + composite in a cycle |
GET /api/v1/kpi/salary-recommendations?cycle_id=&status= |
Salary recommendations |
A missing record returns 404 kpi_not_found; a missing/insufficient scope returns
403 forbidden (handled by authorizeScope).
2. End-User API — /v1/api/kpi/*
- Controller:
controllers/Kpi_matrix_euapi.php(extends App\Core\EndUserApiAuthController) - Audience: the authenticated employee (first-party self-service / mobile).
- Auth: stateless HS256 JWT; identity is the employee
users.idin the JWTsub, resolved self-only viaselfUserId()— a request-supplied user id is never trusted. - Gating:
requireModule('module_kpi_matrix')(404module_unavailablewhen the tenant has the module off) andrequirePermit('kpi_matrix')(KPI is a permissioned HR feature, matching the web "My KPIs" access model). - Capability: READ-ONLY, SELF-ONLY.
GETonly (405 otherwise). - Routes:
config/euapi_routes.php· Spec:public/v1/api/openapi.json(php erpat euapi:openapi --postman).
| Method & path | Purpose |
|---|---|
GET /v1/api/kpi/my/scores?cycle_id= |
My scores by category + composite (active cycle by default) |
GET /v1/api/kpi/my/history |
My composite-score trend |
GET /v1/api/kpi/my/assignments |
My KPI assignments |
GET /v1/api/kpi/my/results/{cycle_id} |
My finalized results for a cycle (404 if none) |
GET /v1/api/kpi/my/salary-recommendation?cycle_id= |
My salary recommendation(s) |
Design
- No logic duplication. Both controllers delegate to the same services
(
CycleService,TemplateService,OverviewAnalyticsService,EmployeeKpiService,SalaryRecommendationService) that the web controllers use. The controllers only handle transport (auth + request parsing + envelope). - Projection. Rows never leave a model raw —
kpi_api_shape_*()casts with null-coalescing, exposes display names only (no PII/email), and turns arating_bandkey into a{key,label,color}object via the module's rating-band config. - Regeneration. After changing routes/endpoints run
php erpat api:openapi --postmanandphp erpat euapi:openapi --postman, and keepmodule.jsonapi.endpoints[]in sync.