Skip to content
ERPNext Data Model
Esc
navigateopen⌘Jpreview
On this page

Telephony and Call-Center Integration Layer

A design reference for capturing call events, attaching them to a known party, and notifying the right agent

1. Requirements

1.1 Functional requirements

  • Capture every call attempt as a call record: counterpart number, direction, timestamps, duration, a recording reference, and a status.
  • Auto-attach a new call record to a matching Contact or Lead by phone-number lookup, and resolve which agent (Employee) an inbound call reached by the dialed number.
  • Let an agent tag a finished call with a controlled call-type list plus a free-text summary.
  • Notify the right agent(s) in real time on an inbound ring, per which employee group is scheduled for that channel right now.
  • Support per-agent voice settings and an org-wide inbound-routing configuration (routing mode, messages, a weekly agent-group schedule).
  • Provide a general-purpose channel abstraction — voice, email, or chat — each with its own weekly timeslot table and a catch-all fallback group.

1.2 Non-functional requirements

  • Idempotent party links: repeated saves must not duplicate Contact/Lead links.
  • Non-blocking notification: the realtime “who should see this” push must not hold up the record’s own save.
  • No invented dialing/routing logic: this layer records call events and configuration only; an external telephony connector places/answers calls and is merely a source of events here.

1.3 Constraints

  • The record’s own “call missed” heuristic admits, in a code comment, that it assumes one specific external provider’s conventions and does not generalize to every provider.
  • Two structurally similar day/time-to-group schedules exist side by side (§3.3) and are not the same mechanism.

2. High-Level Design

2.1 Component diagram

2.2 Data flow — an inbound call arrives

  1. The provider creates a call record (direction Incoming) with a counterpart number, dialed number, and initial status.
  2. Party attachment runs before the first save: the caller’s number is normalized (leading +/0 stripped) and checked against Contact and Lead lookups; any hit is appended to a polymorphic link table, duplicate-guarded.
  3. Agent resolution runs separately, matching the dialed number against Employee records and writing a “received by” field directly, not through the link table.
  4. After insert, the free-text channel identifier is matched against the timeslot table for the current day/time, producing a scheduled-employee set intersected with step 3’s matches; only that intersection gets a realtime popup.
  5. No employee matched → no popup. Matched but none scheduled → a comment notes it; the call is logged either way.

3. Deep Dive

3.1 Data model and status lifecycle

  • Call Record — from/to, direction, medium (free text), timestamps, duration, recording reference, status, a call-type link, a summary, an unused Customer link, and a polymorphic link table for Contact/Lead attachment.
  • Call-Type List — a flat, submittable list of named types; attaching one is its only use.
  • Voice Settings / Routing Configuration — per-user messages/device, plus an org-wide routing mode and a weekly day/time/agent-group schedule (overlap- and ordering-validated at save).
  • Channel Abstraction — channel type, provider reference, catch-all group, and its own weekly day/time/employee-group schedule.

Status has eight literal options, but no code in this layer ever assigns one — every transition is written externally via the ordinary save API. In-tree logic only reacts to status, via two hand-maintained classification lists:

Queued sits outside both classification lists, so entering or leaving it fires neither the “ended” nor “missed” event the other transitions trigger. A historical migration patch that rewrites records stored with the misspelled “Canceled” to “Cancelled” confirms that value is genuinely assigned externally, just never by code in this layer. A “Missed” state shown in the popup UI is client-side-only, synthesized when the dialed number changes mid-call — it is never one of the eight stored options. The call-type list is purely descriptive: assignment is a manual, post-call action, and an untyped record is a valid, permanent end state.

3.2 Party attachment: mechanism and multi-match behavior

The counterpart number is normalized and checked, independently, against a Contact lookup and a Lead lookup — same shape, different match policies. The Lead lookup matches three phone-type fields with OR semantics, returns at most one row, and breaks ties by taking the most recently created Lead. The Contact lookup is a framework-level function outside this layer’s own code; its tie-break could not be verified here. Agent resolution (dialed number → Employee) is a third, independent lookup: a partial-string match returning every hit with no explicit order, taking whichever row comes first — materially less deterministic than the Lead lookup.

No match: neither lookup returns a result, so no link is added; the call is logged “unknown,” and its popup offers to create a Contact, Customer, or Lead from the number by hand — attaching it to a pipeline that did not exist at call time is the pre-sales funnel’s concern, documented separately. Multiple matches: deterministic for Leads, unverified for Contacts, effectively arbitrary for agent resolution.

3.3 Coupled, or merely adjacent?

The routing configuration’s weekly schedule and the channel abstraction’s weekly schedule look like the same idea duplicated, but nothing here reads the routing configuration’s schedule back at runtime — it is write-once setup data with no in-tree reader.

The channel abstraction, however, is genuinely wired to the call record, narrowly: the popup trigger passes the free-text medium field to a lookup matching that string against the timeslot table’s parent, filtered to the current day/time, returning the on-duty group(s), then intersects that with the agent-resolution match. This one connecting function lives in neither module — a shared CRM utility called from the call record’s post-insert handling — and the link is a plain string match, not a formal reference: a mistyped value silently yields zero notified employees rather than an error. The coupling answers “who should see a popup,” not “how is this call routed”; otherwise the two schedules share no code or data. Per-agent voice settings share the same fate as the routing configuration: configuration for an external connector to consume, with no reader found here.


4. Scale and Reliability

  • Load is bursty and event-driven: one write per status transition plus a realtime fan-out per ring; every lookup is an indexed point query, so per-call cost stays flat as volume grows.
  • The agent-resolution lookup is cached per dialed number, invalidated when an Employee’s own cell number changes, avoiding a repeated scan without going stale on the one write that would cause it.
  • Because medium is free text, there is no referential-integrity backstop if a channel record is renamed or removed — the popup path degrades silently to “no one scheduled” rather than failing loudly.
  • Concurrent calls scale independently; there is no shared mutable state beyond the read-only lookups above.

5. Trade-off Analysis

Decision Trade-off
Party attachment by phone-number match, not explicit reference Zero setup for the common case, but three different tie-break policies govern Contact vs. Lead vs. Employee resolution.
Free-text channel identifier, not a formal reference field Logging never depends on a channel record existing first, at the cost of a silent no-op instead of a validation error on mismatch.
Two independent weekly-schedule mechanisms, not one shared model Each evolves independently, but an admin naturally expects configuring one to affect notification routing like the other — only the channel abstraction’s does.
Status populated entirely externally, none in-tree A thin, passive event recipient rather than a call-control system, but the option list cannot be verified end-to-end from source alone.
Configuration-only agent/voice settings, no in-tree reader Simple, provider-agnostic surface, but a changed setting has no effect this layer can confirm.

6. What to Revisit as the System Grows

  • Make medium a formal reference to the channel abstraction, so a renamed or mistyped channel raises a visible error rather than a silently empty notification set.
  • Reconcile the two scheduling mechanisms, or document why they must stay separate, before another integration assumes configuring one configures both.
  • Give agent resolution the same deterministic tie-break as the Lead lookup on a multi-match.
  • Surface auto-linked vs. manually-attributed calls, and untyped calls, as a report rather than something visible only per record.

Was this page helpful?