# CDR Processing Rules

> How the SAFE Billing Platform turns each carrier's CDR and fixed fee files into billable calls and charges, and the YAML rules language that describes a carrier's file format.
>
> Plain-Markdown version of https://documentation.billingplatform.uk/telecommunications/cdr-processing-rules/ for AI assistants.

## Overview

Every carrier sends call data in its own format: different columns, different date styles, different ways of marking call direction. Processing rules tell the platform how to read each carrier's files, so the calls inside them can be rated and billed.

Each carrier has a set of processing rules per file type (daily CDRs, monthly CDRs, fixed fee files). The rules cover:

- **Recognising files** as they arrive: which carrier and billing period a file belongs to, and whether it should be billed, held or rejected
- **Reading the file**: encoding, delimiters, header rows and junk lines
- **Mapping columns** to call fields such as the number, date, duration, charge band and cost
- **Producing call records**: some files describe two call legs per row, or need calls split by direction

Processing rules are platform configuration, managed by your administrator or the support team. They are not a self-service screen: if a carrier changes its file format, or you add a new carrier, support will set up or adjust the rules. This page explains how they behave, and documents the rules language itself.

## Two Formats: Legacy and YAML

The platform supports two rules formats, and each carrier ruleset uses one or the other:

- A **legacy format** used by long-standing carrier configurations. It still works unchanged.
- The **YAML rules language** documented here, shared with [Bulk Update rules](https://documentation.billingplatform.uk/bulk-operations/rules-reference/). New rulesets use YAML, and legacy rulesets are migrated one at a time. A converter produces a draft YAML ruleset from a legacy one, together with a review report listing anything that needs a human decision.

The platform detects the format automatically, so migration is gradual and per carrier.

## File Recognition and Safety Holds

Before a file is read, the `recognise:` rules classify it:

- **Monthly, daily or reject** rules match on the file's name, path or content. A rejected file is marked Rejected and not processed. The billing setting decides whether a matched file feeds billing, statistics or is held for review.
- **Required headers** protect you from silent format changes. If a supplier renames or drops an expected column, the file is **Held** rather than half-processed, and the queue report says why. This stops a quiet supplier-side change producing wrong bills.
- **Duplicate handling** controls what happens when a file arrives with a familiar name or identical contents: import, ignore, hold or reject. See [Potential Duplicate Files](https://documentation.billingplatform.uk/billing-invoicing/billing-run/supply-cdrs/#potential-duplicate-files) for the operator's view.

---

## The Rules Language for CDR Files

The rest of this page is a reference for the YAML rules language as it applies to CDR and fixed fee files. It is written so that an AI assistant can draft a ruleset from a sample file, given the file's header row and a few sample rows.


**For the AI assistant**: you are writing a `kind: cdr` (or `kind: transactions`) YAML ruleset for the SAFE Billing Platform's CDR import engine. The shared language basics are identical to the [Update Rules Reference](https://documentation.billingplatform.uk/bulk-operations/rules-reference/): `file:` framing, `fields:` with sources (`header`, `headers`, `column`, `value`, `source: filename`), `translations`, `skipRowValues`, `transforms`, `rowRules` (including `warn` groups, which flag rows in the processing report without skipping them), `ignore`, and the condition grammar (`anyOf`/`allOf`/`noneOf`; operators `equals`, `prefix`, `suffix`, `contains`, `matches`, `over`, `under`, `atLeast`, `atMost`, `before`, `after`, `onOrBefore`, `onOrAfter`, `lengthOver`, `lengthUnder`, `sameAs`, `empty`, `present`, `always`, `columnCountAbove`, `columnCountBelow`). The date operators take a fixed date or a named expression such as `first of last month`, anchored to the file's upload date. Validation is strict: unknown keys are errors, so never invent keys. `version:` and `kind:` are optional; if the ruleset is for fixed fee (carrier transaction) files use `kind: transactions`, otherwise `cdr`.

What differs for CDR rules:

1. **Field names are call fields**, and you may use their friendly spellings: `Date`, `Time`, `Duration`, `Target` (the billed number), `Called Number`, `Calling Number`, `Charge Band`, `Call Type`, `Cost`, `Extension`, and so on. A field name the call record does not use, such as `Temporary Field 1` or any `temp.`-prefixed name, is scratch space: you can read, transform and test it in conditions, but it is not written to the call.
2. **`records:` produces the output calls.** Without it, each row produces one call. With it, one row can produce several call legs, each with its own direction and column bindings.
3. **`flags:`, `settings:` and `recognise:`** are available; `lookups:` and `emit:` are not (they belong to bulk update rules).

### `records:` - One Row, Several Calls

Each entry describes one output record. All shared `fields:` apply to every record; a record can override or add its own.

| Key | Meaning |
|-----|---------|
| `name` | Required, unique. Used in the drop report. |
| `prefix` | Column-prefix sugar: with `prefix: A`, a shared field bound to header `Number` also binds to a column headed `A Number` for this record. This is the usual way to map per-leg columns. |
| `fields` | Per-record field overrides. An override that declares a source replaces the shared one; an override with only transforms keeps the shared source. |
| `direction` | `inbound`, `outbound`, `aleg`, `bleg`, `neither`, `determine`, or `determine b leg`. The `determine` forms compare the target against the called and calling numbers. |
| `directionWhen` | A list of `{direction: ..., when: <conditions>}` entries; the first match wins over the static `direction`. |
| `when` | Emit this record only when the conditions match. |
| `skip` | Drop this record when the conditions match (the row's other records still emit). |
| `formatted` | `{when: ...}` and/or `{skip: ...}` evaluated against the **final** values just before insert, after transforms. |
| `useAlternate` | Swap in another field's value conditionally: `Charge Band: {from: Alternate Charge Band, when: [...]}`. |
| `surcharge` | `{when: <conditions>}`: marks the record as a surcharge leg; the platform derives the surcharge charge band from the call's origin and destination. |

Records that are declared but not emitted are counted per reason in the processing report, so nothing is dropped silently.

### `flags:` - Per-Call Booleans

Each flag is `{when: <conditions>}`, evaluated per record. Declared flags are set true or false; undeclared flags are left for the platform to work out where it can.

Available flags: `inbound`, `outbound`, `dataCall`, `mobileOrigination`, `payphoneOrigination`, `callRecording`, `sms`, `surcharge`, `numberWithheld`.

```yaml
flags:
  mobileOrigination:
    when:
      - {field: 'Call Type', equals: ['M', 'MOB']}
```

### `settings:` - File-Wide Behaviour

| Key | Meaning |
|-----|---------|
| `autoMapHeaders` | `true` binds any column whose heading matches the platform's built-in call field names automatically. Explicit `fields:` always win. Most rulesets set this and map only the exceptions. |
| `cost` | `{scale: 100, decimalSeparator: ','}`: divide costs supplied in pence, or fix a decimal comma. |
| `numbers` | `{mode: normalise}` (also `international`, `e164`, `remove44`): normalisation applied to the call's number fields. |
| `dates` | `{date: '%d/%m/%Y', time: '%H:%M:%S', datetime: ...}`: the file's date and time patterns. |
| `data` | Data-call handling: `{round: ..., scale: ..., useDuration: true}` treats a data volume as the duration. |
| `normalise` | Per number-role fine tuning (`billed`, `origination`, `termination`, `called`): lookup order and minimum lengths. |
| `generateCallIDs` | `default`, `per-line` or `grouped-lines`, for files without a usable unique call id. |
| `skipOldCallsBefore` | A date expression; calls starting before it are dropped and reported. |
| `timezone` | The file's timezone, where it differs from the platform's. |

### `recognise:` - Classifying Arriving Files

| Key | Meaning |
|-----|---------|
| `monthly` / `daily` / `reject` | Match blocks with `filename`, `exceptFilename`, `path`, `exceptPath` (regular expressions), `headerMatches` (regex tested against the first lines), `contentMatches: {line: N, pattern: ...}`, and `billing` (`billing`, `stats`, `hold`, `manual` or `none`). |
| `requiredHeaders` | Column headings that must be present, or the file is held for review. |
| `duplicates` | `{filename: ..., contents: ...}` each one of `import`, `ignore`, `reject`, `hold`; plus `minimumSize` in bytes. |
| `housekeeping` | Retention of processed files, for example `{dailyFiles: delete}`. |

### Worked Example: Two Call Legs with Direction Logic

One row per call, with `A`-prefixed and `B`-prefixed columns for the two legs. The B leg is only produced when its column is filled, and an alternate charge band is used for CPS and WLR calls:

```yaml
kind: cdr
settings:
  autoMapHeaders: true
  cost:
    scale: 100
  numbers:
    mode: normalise
  dates:
    date: '%d/%m/%Y'
fields:
  Duration:
    header: Call Length
    transforms:
      - duration: normalise
  Call Status:
    header: Status
    translations:
      OK: answered
      ENGAGED: busy
rowRules:
  skip:
    - name: Internal calls
      when:
        - {field: 'Charge Band', equals: ['Internal', '']}
records:
  - name: aLeg
    prefix: A
    fields:
      Target:
        header: Caller
    directionWhen:
      - direction: inbound
        when:
          - {field: 'Charge Band', prefix: ['IN']}
    useAlternate:
      Charge Band:
        from: Alternate Charge Band
        when:
          - {field: 'Charge Band', equals: ['CPS', 'WLR']}
  - name: bLeg
    prefix: B
    when:
      - {field: 'B Number', present: true}
    fields:
      Target:
        header: Called
```

### Worked Example: Header-less File with Conditional Direction

```yaml
kind: cdr
file:
  header: false
fields:
  Call Type: {column: 1}
  Target: {column: 2}
records:
  - name: leg
    direction: outbound
    directionWhen:
      - direction: inbound
        when:
          - {field: 'Call Type', equals: ['I', 'A']}
```

### Fixed Fee Files (`kind: transactions`)

Fixed fee (SDR) files use the same language with `kind: transactions`. The field roster is the carrier transaction's fields, including the number references used to match each charge to a customer, number or feature, and the transaction value, which respects `settings.cost`. The `records:`, `flags:` and `recognise:` keys work the same way. See [Fixed Fee Billing Files](https://documentation.billingplatform.uk/billing-invoicing/billing-run/supply-cdrs/#fixed-fee-billing-files) for how these files are matched and billed.

---

## Related Pages

- [Supply Files](https://documentation.billingplatform.uk/billing-invoicing/billing-run/supply-cdrs/): how CDR and fixed fee files reach the platform, duplicates and bundles
- [CDR Processing](https://documentation.billingplatform.uk/user-guide/cdrs/): the end-to-end processing workflow
- [Call and CDR Configuration](https://documentation.billingplatform.uk/telecommunications/call-configuration/): call types, overrides and the CDR file records
- [Update Rules Reference](https://documentation.billingplatform.uk/bulk-operations/rules-reference/): the shared rules language in full, applied to bulk update files
