Prd/components/backend/test/test_reporting.rb
Lucy Doupalů be9f14ce34 Helpdesk - operator console + patched GrapheneOS Dialer for call handling
A small helpdesk system: an office Pixel running a patched GrapheneOS Dialer
answers technician calls, records both call legs as separate channels, and a
Ruby backend transcribes them through Whisper and files an AI summary against
the caller.

Squashed to a single commit for sharing. No credentials are included; secrets
live outside the repo in /etc/helpdesk/env on the server or a gitignored
.claude/env.local locally. See .claude/env.local.example for the shape.

Start at README.md, then docs/architecture.md.
2026-07-27 18:50:32 +02:00

66 lines
3.1 KiB
Ruby

# 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)