FKITDEV-8639: SL Discrepancy Between Report UI and Excel Export

Summary

The service level (SL) ratio shown in the video calls report UI differs significantly from the SL value in the exported Excel file — beyond what rounding could explain. Reported by Zsuzsa on the NUSZ deployment. She notes it wasn’t a problem before, only noticed today.

Related Ticket

A related report states that lost + served calls ≠ incoming calls count in the daily statistics export.

Not a Regression

RCA confirmed this is a latent design flaw since commit 1a8c1ef561 (FKDEV-284 — “Add sum column for service level report download”). The user noticed it today likely because recent data had unusually uneven call distribution across periods, making the discrepancy visible.

Root Cause (5 Whys)

Why 1: SL “Sum” value disagrees with per-period values

CallsReportService.js line 98 builds the Excel SL row by concatenating per-period SL values (chartData.service_level) with the overall SL (report.report.service_level). These use different formulas:

  • Per-period (line 682): Math.round(((periodCalls - periodLateAnswers) / periodCalls) * 100) — independently rounded
  • Overall (line 570): Math.round(((totalCalls - totalLateAnswers) / totalCalls) * 100) — volume-weighted

Why 2: These are mathematically different (Simpson’s Paradox)

Example: Period A = 2 calls, 1 late (SL=50%). Period B = 100 calls, 0 late (SL=100%). Naive average = 75%. Weighted overall = 99/102 = 97%. A 22-point gap.

Why 3: The Sum column was bolted on after the original design

Original design (commit f3abed8670, FKITDEV-10) only had per-period values. The Sum column was added later (commit 1a8c1ef561, FKDEV-284) using the weighted overall calculation, without reconciling with the per-period display.

Why 4: No reconciliation guard exists

No unit test asserts that the Sum column value and per-period values are mathematically consistent. No column-count assertion ensures all Excel rows have the same width.

All Bugs Found (4)

Bug A: SL Discrepancy (PRIMARY)

Per-period SL values are independently Math.round’d. The Sum column uses the volume-weighted overall. With uneven call volumes across periods, discrepancy can be 30+ percentage points.

Files: CallsReportService.js lines 570-572, 681-683, 98

Bug B: Excel Column Misalignment

Header row adds ‘Sum’ column (line 89-90), but only service_level row has a Sum value. All other rows (calls, calltimes, late_answers, etc.) are one cell shorter, causing visual misalignment in Excel.

Files: CallsReportService.js lines 88-98

Bug C: Locale Parameter Swallowed

ReportsService.js:63 passes true (intended as isDownload) to CallsReportService.prepareReportData:142 which receives it as locale. The actual locale from getDownloadableResult is never forwarded. Dictionary falls back to default, masking the bug in Hungarian-only deployments.

Files: ReportsService.js line 63, CallsReportService.js line 142

waiting_calls = history.length (all entries). exits requires leaveAt to be set. Customers who close browser without triggering leave event are counted in waiting_calls but neither calls nor exits. Hence: waiting_calls > calls + exits.

Files: CallsReportService.js lines 742-744, 843-844

NUSZ Customization Check

NUSZ branch has no overrides for report calculation. Only translation entries in customization/translations/reports.trans.js. The callOverride('reportCallsDownload:rows') hook at line 88 has no NUSZ handler registered.

Fixes

Bug A (SL discrepancy)

Keep the weighted overall Sum (mathematically correct). Add Sum values for all rows so users can verify the math themselves.

Bug B (column alignment)

Append Sum values to all rows in reportRows() (lines 92-110), not just service_level. Every row must have the same column count as the header.

Bug C (locale parameter)

Change ReportsService.js:63 from:

this.prepareReportData(reportParams, format, ..., filterUserId, true)

to:

this.prepareReportData(reportParams, format, ..., filterUserId, locale)

Bug D (counting gap)

Count history entries where !chatStarted && !leaveAt (older than a timeout threshold) as implicit exits. Or add abandoned as a new metric.

Execution Order

  1. Bug C (one-line fix, zero risk)
  2. Bug B (add missing Sum cells)
  3. Bug A (add Sum values + verify weighted SL is correct)
  4. Bug D (decide on ghost entry policy)

Blast Radius

  • Bug A/B: No downstream impact — additive changes to Excel export
  • Bug C: Could change translation output for non-Hungarian locales — test with locale='en'
  • Bug D: Will increase exits count on dashboards. SL formula does NOT use exits (confirmed line 570), so SL unaffected

Prevention

  1. Unit test with uneven call volumes asserting Sum == weighted average
  2. Column count assertion in reportRows() — all rows same .length
  3. Migrate CallsReportService.js to TypeScript to catch parameter mismatches

Implementation Log

Task 1 (Bug C) — DONE — commit on feature/FKITDEV-8639

ReportsService.js:63 — changed true to locale. Test: prepareReportData locale forwarding passes.

Task 2 (Bug B + A partial) — DONE — commit 543f293c38

CallsReportService.js lines 92–115: appended .concat(report.report.X) to every data row so all rows are the same width as the header. Tests:

  • all rows should have the same number of columns as the header row — passes
  • Sum column should contain correct aggregate values — passes

Gotchas discovered during implementation:

  • node_modules was absent — had to install deps with yarn install
  • yarn was not on PATH — installed globally via npm install -g yarn
  • Tests require NODE_ENV=dev DEV_DOMAIN=test.local to load getconfig config file
  • Module exports as { CallsReportService, ActivitySessionInfo } — Task 1 test had wrong require; fixed to const { CallsReportService } = require(...)