Reference
The exact moving parts of Tenant Analytics — controllers, routes, models and libraries, the migration and its three tables, the seventeen metric groups, the cron job, and the public API contract. Written for developers and technical administrators.
Architecture at a glance
Tenant Analytics is a main-database-only module. Its storage lives only in the platform's primary database; the nightly job briefly switches into each tenant database to read metrics, but always writes to the main database and always restores the main connection afterwards.
Controllers
| Controller | Base | Role |
|---|---|---|
Tenant_analytics |
App_Controller (staff) |
The operator UI and its AJAX endpoints. Gated main-DB-only + tenant_analytics
read permission in the constructor. |
Tenant_analytics_api |
App\Core\ApiController (public) |
The keyless public API. No JWT/tenant/CSRF middleware; still gets the
{ok,data} envelope, request-id, CORS and request logging. |
Staff UI methods (Tenant_analytics)
| Method | Purpose | Permission |
|---|---|---|
tab_index | Render the embedded Analytics tab body. | tenant_analytics |
kpi_data | Refresh the four KPI numbers for a date (AJAX). | tenant_analytics |
list_data/{group}/{date} | Server-side snapshots DataTable. | tenant_analytics |
aggregate_panel | The platform aggregate for a date + group. | tenant_analytics |
snapshot_modal/{id} | One tenant's full snapshot payload. | tenant_analytics |
run_sweep_modal_form | The sweep console modal. | tenant_analytics_run |
run_sweep_start | Open a run, return the tenant queue + run id. | tenant_analytics_run |
run_sweep_batch | Process one chunked batch of the sweep. | tenant_analytics_run |
resweep_tenant | Re-sweep a single tenant + rebuild rollups. | tenant_analytics_run |
rebuild_rollups | Rebuild the platform rollups for a date. | tenant_analytics_run |
settings_modal | The settings form. | tenant_analytics_configure |
save_settings | Persist the five settings + audit log. | tenant_analytics_configure |
Routes
The module owns its whole route surface. Staff UI routes live in
config/routes.php; the public API routes ride the client-API stack via
config/api_routes.php.
| Route | Target |
|---|---|
tenant_analytics | Tenant_analytics/tab_index |
tenant_analytics/(:any) | Tenant_analytics/$1 |
GET api/v1/analytics/groups | Tenant_analytics_api::groups |
GET api/v1/analytics/summary | Tenant_analytics_api::summary |
GET api/v1/analytics/{group} | Tenant_analytics_api::group ({group} matches [a-z_]+) |
Models & libraries
| Class | Kind | Role |
|---|---|---|
Tenant_analytics_base_model | Model base | Pins the primary
(default) connection, so storage writes always land in the main DB even
mid-sweep. All storage models extend it. |
Tenant_analytics_snapshots_model | Storage model | Reads/writes per-tenant snapshots; DataTable source; prune past retention. |
Tenant_analytics_rollups_model | Storage model | Reads/writes the identifier-free platform aggregate; date helpers. |
Tenant_analytics_runs_model | Storage model | Sweep audit
ledger + resume cursor; resolves the fleet's sweep targets from the main
clients registry. |
Tenant_analytics_probe_model | Probe model | Runs against the
active (tenant) connection; hosts the seventeen metric collectors. Every query guards
table_exists / field_exists so a drifted tenant degrades to
zeros rather than erroring. |
Tenant_analytics_collector | Library | Per-tenant orchestration: switch into the tenant DB, probe each group, always restore the main connection (isolation invariant), upsert results to the main snapshot table. |
Tenant_analytics_rollup | Library | Collapses a date's snapshots into the aggregate: deep-sums numeric leaves, unions scalar lists, and drops every string/boolean leaf so no identifier can survive. |
Migration & tables
One migration, 20260726065412_create_tenant_analytics_tables.php, creates three
tables in the main database only (a no-op on tenant connections). It is idempotent
(table & index guards) and reversible.
tenant_analytics_snapshots
One row per tenant × date × group. Natural key:
client_id, snapshot_date, group_key, deleted.
| Column | Purpose |
|---|---|
client_id, company_slug | Which tenant (from the main clients registry). |
snapshot_date, group_key | The date and metric group. |
payload | The JSON measurement (with its meta block). |
status | ok / partial / unavailable / failed. |
tenant_status | The tenant's platform status as of the sweep. |
row_count, build_duration_ms | Scale and timing of the measurement. |
error_message, run_id | Failure text; the run that produced it. |
created_at, updated_at, deleted | Audit block (soft delete). |
tenant_analytics_rollups
The platform aggregate, one row per date × group — the only table the public
API reads. Natural key: snapshot_date, group_key, deleted.
| Column | Purpose |
|---|---|
snapshot_date, group_key | The date and metric group. |
payload | Aggregate JSON — numeric totals only, no tenant identifiers. |
tenants_counted | How many tenants contributed (drives small-n suppression). |
tenants_total, tenants_failed | Fleet size and failure count for the date. |
created_at, updated_at, deleted | Audit block. |
tenant_analytics_runs
The sweep audit ledger — one row per sweep (nightly or manual).
| Column | Purpose |
|---|---|
run_date, source | The swept date; cron / manual / demo. |
status | running / completed / partial / failed. |
tenants_total, _succeeded, _failed, _skipped | Per-run counters. |
groups_written | How many group-snapshots were written. |
cursor | JSON resume cursor for chunked manual sweeps. |
started_at, finished_at, duration_ms | Timing. |
triggered_by, log | Operator (manual runs) and a summary line. |
The seventeen metric groups
Each snapshot payload also carries a meta block:
group, available, row_count, module_enabled
(a map of which module_* settings are on for that tenant), tables_present,
and collected_at. Money reflects pending/approved payslips only; count and
money metrics are sliced into windows (total, today, mtd,
ytd, last_30d).
| # | Group | Key metrics |
|---|---|---|
| 1 | overview | Tenant status, enabled-module map (total / enabled), live database size (bytes / MB / human), free space, table count, top-10 largest tables, last DB snapshot date. |
| 2 | payslips | Count and by-status; pending/approved money — gross, net, deductions, tax, government contributions (and taxable when present) across windows. |
| 3 | payrolls | Count and by-status, by tax table, latest pay date, and payroll adjustments (count and by type). |
| 4 | employees | Active / total staff, total users, admins, breakdowns by user type / status / gender, job-info and schedule coverage, new users by window. |
| 5 | attendance | Records total, by status / log type; summed metric hours (worked, late, over, under, overtime variants, night differential, PTO, absent), present/absent counts, raw taps, batches. |
| 6 | leaves | Applications total and by status, approved count / days / hours, leave credits (granted / consumed / balance), active leave types. |
| 7 | establishments | Total, active, remote, geofenced, with coordinates, by location type, and assigned employees. |
| 8 | schedules | Templates (total / active / archived), users assigned / without a schedule, planning entries, publications, coverage rules. |
| 9 | departments | Departments and groups, average members per department, members and heads covered, department profiles, nested departments. |
| 10 | deminimis | Ledger amount / exempt / taxable, by source, YTD exempt against the BIR ₱90,000 ceiling, and payslip-side de-minimis earnings. |
| 11 | loans | Runtime-detected sources (core payroll loans and/or the Lending module): counts, active principal, outstanding (amortization-driven), overdue installments, payments. |
| 12 | storage | Files & footprint: general_files (+ project_files) count and total size (bytes / human), by service type (local / s3 / missing), by owner (team / client / vendor), public vs private, largest file, files & bytes added per window, and the combined DB + file footprint. |
| 13 | finance | Invoices (count, by status), invoiced total (Σ invoice items), collected total (Σ payments), outstanding (AR), and expenses (count, total, by status), windowed. Money fields small-n suppressed in the public API. |
| 14 | assets | asset_entries count, total cost (suppressed), by type / status, warranty expiring within 90 days, acquisitions per window. |
| 15 | crm | Clients (non-lead), leads (client is_lead + legacy leads), lead funnel by status, vendors (total / by status), new clients per window. |
| 16 | projects | Projects and tasks by status, overdue counts (past deadline & not done), new projects per window. |
| 17 | engagement | Active users (last-online 7 / 30 days), timeline posts + comments + reactions, notifications, active announcements, and activity-log volume by action. |
Cron job
| Property | Value |
|---|---|
| Class | Modules\TenantAnalytics\Jobs\TenantAnalyticsSnapshotJob |
| Slug | tenant_analytics_snapshot |
| Schedule | 0 3 * * * (daily 03:00) |
| Scope | Global (runs once on main; loops the fleet itself) |
| Timeout / memory | 1800 s / 512 MB |
| Steps | Sweep → rebuild rollups → prune past retention → close run |
| Manual run | php erpat cron:run tenant_analytics_snapshot |
Public API endpoints
All three are anonymous (no bearer token, no company key) and return the standard API envelope
— { "ok": true, "data": { … } } on success, or
{ "ok": false, "error": { "code", "message", "details" } } on error. Every endpoint
returns 404 while the public API is disabled.
GET /api/v1/analytics/groups
Lists the available metric groups and dates.
GET /api/v1/analytics/groups
200 OK
{
"ok": true,
"data": {
"groups": ["overview", "payslips", "employees", ...],
"latest_date": "2026-07-25",
"available_dates": ["2026-07-25", "2026-07-24", ...]
}
}GET /api/v1/analytics/summary
Headline numbers for every group on one date. ?date=YYYY-MM-DD is optional (defaults
to the latest available date).
GET /api/v1/analytics/summary?date=2026-07-25
200 OK
{
"ok": true,
"data": {
"date": "2026-07-25",
"groups": {
"payslips": {
"tenants_counted": 18,
"suppressed": false,
"headline": { "count": 4200, "gross": 15200000 }
},
"employees": { "tenants_counted": 18, "suppressed": false,
"headline": { "staff_active": 1600, "staff_total": 1850, "users_total": 2100 } }
}
}
}GET /api/v1/analytics/{group}
One group's full platform aggregate. {group} must be one of the seventeen keys;
?date= is optional.
GET /api/v1/analytics/payslips?date=2026-07-25
200 OK
{
"ok": true,
"data": {
"group": "payslips",
"date": "2026-07-25",
"tenants_counted": 18,
"tenants_total": 20,
"tenants_failed": 0,
"suppressed": false,
"aggregate": { "count": { "total": 4200 }, "money": { "gross": { "total": 15200000 }, ... } }
}
}Errors & validation
| Status | Code | When |
|---|---|---|
| 404 | not_found | The public API is disabled; or there is no data for the date; or an unknown group. |
| 422 | validation_failed | date is not YYYY-MM-DD, or falls outside the [today − retention, today] window. |
| 429 | rate_limited | The anonymous rate limit was exceeded. |
tenants_counted is below
tenant_analytics_min_aggregate_tenants, money fields are stripped from the payload
and the response carries "suppressed": true.
Settings keys
| Key | Default |
|---|---|
module_tenant_analytics | set by Manage Modules |
tenant_analytics_retention_days | 400 |
tenant_analytics_include_inactive | 1 |
tenant_analytics_sweep_batch_size | 3 |
tenant_analytics_public_api_enabled | 0 |
tenant_analytics_min_aggregate_tenants | 1 (recommend 3) |