# frozen_string_literal: true # Tests the reporting/KPI engine against a synthetic Event stream with known answers. # Run: ruby components/backend/test/test_reporting.rb require_relative "../lib/helpdesk/reporting" $pass = 0; $fail = 0 def ok(d); $pass += 1; puts " ok #{d}"; end def bad(d, g = nil); $fail += 1; puts " FAIL #{d}#{g.nil? ? '' : " (#{g.inspect})"}"; end def eq(a, b, d); a == b ? ok(d) : bad(d, a); end def ev(id, op, dur, res, person:, at:, prior: nil, answered: true, camp: "T-Mobile jamt", channel: nil) { id: id, person_id: person, operator: op, campaign: camp, started_at: at, answered_at: (answered ? at : nil), duration_s: (answered ? dur : nil), resolution: res, prior_event_id: prior, channel: channel } end EVENTS = [ ev(1, "tmobile", 120, "solved", person: 1, at: "2026-07-06T09:00:00Z"), # first, solved, no callback → FCR+ ev(2, "tmobile", 60, "solved", person: 2, at: "2026-07-06T09:10:00Z"), # first, solved, BUT called back (e3) → FCR- ev(3, "tmobile", 90, "partial", person: 2, at: "2026-07-07T10:00:00Z", prior: 2), # repeat of e2 → RCR+ ev(4, "vodafone", 30, "unsolved", person: 3, at: "2026-07-06T11:00:00Z", camp: "Vodafone jamt", channel: "whatsapp"), ev(5, "tmobile", 0, nil, person: 4, at: "2026-07-06T12:00:00Z", answered: false), # missed + untriaged ev(6, "tmobile", 45, "solved", person: 5, at: "2026-07-06T13:00:00Z"), # first, solved, no callback → FCR+ ] FROM = "2026-07-06T00:00:00Z"; TO = "2026-07-08T00:00:00Z" r = Helpdesk::Report.build(EVENTS, from: FROM, to: TO, retainer_czk: 200_000) puts "volume" eq(r[:calls_handled], 6, "6 calls in period") eq(r[:answered], 5, "5 answered") eq(r[:missed], 1, "1 missed") puts "AHT (mean talk time over answered)" eq(r[:aht_s], 69, "AHT = (120+60+90+30+45)/5 = 69s") puts "FCR (internal method) + repeat-call rate" eq(r[:fcr], 50.0, "FCR = 2 of 4 triaged first-calls solved-without-callback = 50%") eq(r[:repeat_call_rate], 16.7, "RCR = 1 repeat of 6 = 16.7%") puts "resolution breakdown" eq(r[:resolution_breakdown], { "solved" => 3, "partial" => 1, "unsolved" => 1, "untriaged" => 1 }, "3/1/1/1") puts "surcharges + retainer" eq(r[:surcharges][:whatsapp][:count], 1, "one WhatsApp call") eq(r[:surcharges][:whatsapp][:total_czk], 100, "+100 CZK surcharge") eq(r[:retainer_czk], 200_000, "retainer passed through") puts "per-operator rollup" eq(r[:by_operator]["tmobile"][:calls_handled], 5, "tmobile handled 5") eq(r[:by_operator]["vodafone"][:calls_handled], 1, "vodafone handled 1") eq(r[:by_operator]["vodafone"][:resolution_breakdown]["unsolved"], 1, "vodafone: 1 unsolved") puts "small-N suppression" r2 = Helpdesk::Report.build(EVENTS.first(2), from: FROM, to: TO, min_n: 5) eq(r2[:fcr], nil, "FCR suppressed below min_n") eq(r2[:repeat_call_rate], nil, "RCR suppressed below min_n") puts "operator filter" r3 = Helpdesk::Report.build(EVENTS, from: FROM, to: TO, operator: "vodafone") eq(r3[:calls_handled], 1, "filter to vodafone → 1 call") puts "\n#{$pass} passed, #{$fail} failed" exit($fail.zero? ? 0 : 1)