Differences
This shows you the differences between two versions of the page.
| Next revision | Previous revision | ||
| vendors:cisco:routers:radius:radiusvoicecalls [2026/10/27 20:47] – created gerardorourke | vendors:cisco:routers:radius:radiusvoicecalls [2026/11/12 15:33] (current) – [SQL Queries] gerardorourke | ||
|---|---|---|---|
| Line 1: | Line 1: | ||
| - | # Radius Voice Records - Low Level Design | + | ====== |
| - | Oct 27, 2026 · @Gerard | + | ===== 1. Purpose and scope ===== |
| - | ## 1. Purpose | + | '' |
| - | `radiusVoiceCalls.php` keeps one row per voice call in `dbo.radiusVoiceCalls` | + | * **Interim-Update** (FreeRADIUS) inserts the row when the call is first reported. |
| + | * **Stop** | ||
| + | * **finesse-start** (Cisco Finesse) adds the agent, dialog and queue details to the existing row. | ||
| - | - **Interim-Update** (FreeRADIUS) inserts | + | Another system reads the table, filtering on '' |
| - | - **Stop** (FreeRADIUS) closes the call: disconnect time, cause, duration, `callActive | + | |
| - | - **finesse-start** (Cisco Finesse) adds the agent, dialog | + | |
| - | Another system reads the table, filtering on `callActive` and `customerPhoneNumber`, | + | **Runtime: |
| - | + | ||
| - | **Runtime: | + | |
| **Out of scope:** FreeRADIUS Start records, any other Finesse event type, retries of failed requests, and purging or archiving old rows. | **Out of scope:** FreeRADIUS Start records, any other Finesse event type, retries of failed requests, and purging or archiving old rows. | ||
| - | ## 2. Architecture and data flow | + | ===== 2. Architecture and data flow ===== |
| &# | &# | ||
| Line 25: | Line 23: | ||
| **How one call's row is built up** | **How one call's row is built up** | ||
| - | 1. The first Interim-Update for a call arrives from FreeRADIUS. The script inserts the row with `callActive = 1`. | + | - The first Interim-Update for a call arrives from FreeRADIUS. The script inserts the row with '' |
| - | 2. While the call is active, a `finesse-start` from the Finesse server adds the agent, dialog and queue details to that row. | + | - While the call is active, a '' |
| - | 3. The Stop from FreeRADIUS closes the call: disconnect time, cause, duration, and `callActive = 0`. | + | - The Stop from FreeRADIUS closes the call: disconnect time, cause, duration, and '' |
| The reading system can query the row at any point in this sequence. | The reading system can query the row at any point in this sequence. | ||
| - | ## 3. Request interface | + | ===== 3. Request interface |
| - | The script accepts HTTP `PUT` with a JSON object body at one URL, from allowed source IPs only. Every response is JSON in the form `{" | + | The script accepts HTTP '' |
| **Checks, in the order they run** | **Checks, in the order they run** | ||
| - | | # | Check | Rule | Failure | + | ^ # ^ Check ^ Rule ^ Failure |
| - | | --- | --- | --- | --- | | + | | 1 | Client IP | '' |
| - | | 1 | Client IP | `REMOTE_ADDR` must be in `$allowed_ips` (an IPv4-mapped | + | | 2 | Method | Must be '' |
| - | | 2 | Method | Must be `PUT`. | 405 `Method not allowed`, with an `Allow: PUT` header | | + | | 3 | api-key | The '' |
| - | | 3 | api-key | The `api-key` header must equal `$api_key` (compared with `hash_equals`). Not required when the client is `127.0.0.1`. | 401 `Invalid api-key` | | + | |
| | 4 | Body | Not empty, at most 65,536 bytes, valid JSON, and a JSON object (not a list). | 400 or 413 | | | 4 | Body | Not empty, at most 65,536 bytes, valid JSON, and a JSON object (not a list). | 400 or 413 | | ||
| - | | 5 | Status type | Must name one of the three types below. | 422 `Missing or unknown Acct-Status-Type` | | + | | 5 | Status type | Must name one of the three types below. | 422 '' |
| **Request types** | **Request types** | ||
| - | | Status type | Key it arrives in | Sent by | Effect on the table | | + | ^ Status type ^ Key it arrives in ^ Sent by ^ Effect on the table ^ |
| - | | --- | --- | --- | --- | | + | | '' |
| - | | `Interim-Update` | `Acct-Status-Type` | FreeRADIUS rest module | Inserts the row, only the first time an `h323-conf-id` is seen | | + | | '' |
| - | | `Stop` | `Acct-Status-Type` | FreeRADIUS rest module | Updates the existing row and closes the call | | + | | '' |
| - | | `finesse-start` | `acctStatusType` | Cisco Finesse server | Updates the existing row with agent details | | + | |
| - | The script reads `Acct-Status-Type` first and falls back to `acctStatusType`. The value is matched case-insensitively. Any other value, including the earlier | + | The script reads '' |
| All values are read as text. A field that is missing, empty or not a plain value (an object or list) is treated as absent. | All values are read as text. A field that is missing, empty or not a plain value (an object or list) is treated as absent. | ||
| Line 59: | Line 55: | ||
| **Example Interim-Update body** | **Example Interim-Update body** | ||
| - | ```json | + | < |
| { | { | ||
| " | " | ||
| Line 73: | Line 69: | ||
| " | " | ||
| } | } | ||
| - | ``` | + | </ |
| **Example finesse-start body** | **Example finesse-start body** | ||
| - | ```json | + | < |
| { | { | ||
| " | " | ||
| Line 90: | Line 86: | ||
| " | " | ||
| } | } | ||
| - | ``` | + | </ |
| - | ## 4. Processing flow | + | ===== 4. Processing flow ===== |
| &# | &# | ||
| Line 98: | Line 94: | ||
| A failed check stops the request and returns the code shown in its box. Section 8 lists every code and message. | A failed check stops the request and returns the code shown in its box. Section 8 lists every code and message. | ||
| - | ### Interim-Update | + | ==== Interim-Update |
| - | 1. Read the call ID (spaces removed), the gateway and the session ID. If any of the three is missing, return 422. | + | - Read the call ID (spaces removed), the gateway and the session ID. If any of the three is missing, return 422. |
| - | 2. Work out the customer phone number from the call direction and the E.164 rules (section 7). | + | - Work out the customer phone number from the call direction and the E.164 rules (section 7). |
| - | 3. Open a transaction and look up the call ID (Q1). If a row exists, log the request at DEBUG and return 200 without writing. | + | - Open a transaction and look up the call ID (Q1). If a row exists, log the request at DEBUG and return 200 without writing. |
| - | 4. Otherwise insert the row (Q2) and commit. A duplicate-key error from a simultaneous request is retried once. | + | - Otherwise insert the row (Q2) and commit. A duplicate-key error from a simultaneous request is retried once. |
| - | ### Stop | + | ==== Stop ==== |
| - | 1. If `h323-conf-id` is missing, return 422. | + | - If '' |
| - | 2. Convert the times, work out `callDuration`, and convert the disconnect cause from hex (section 7). | + | - Convert the times, work out '' |
| - | 3. Look up the call ID (Q3). If there is no row, log an ERROR with the request and return 404. | + | - Look up the call ID (Q3). If there is no row, log an ERROR with the request and return 404. |
| - | 4. Update the row (Q4), which sets `callActive = 0`, and commit. | + | - Update the row (Q4), which sets '' |
| - | ### finesse-start | + | ==== finesse-start |
| - | 1. Decide how to find the call: by a real `h323ConfId`, or by phone number when `h323ConfId` is `not_set`, empty or missing. With neither a real ID nor a phone number, return 422. | + | - Decide how to find the call: by a real '' |
| - | 2. Prepare the agent fields, cutting each to its column length. | + | - Prepare the agent fields, cutting each to its column length. |
| - | 3. Find the row (Q5, or Q6 and Q7). | + | - Find the row (Q5, or Q6 and Q7). |
| - | 4. One row: update it (Q8) and return 200. No rows: return 404. More than one row left after the window: return 422 and update nothing. | + | - One row: update it (Q8) and return 200. No rows: return 404. More than one row left after the window: return 422 and update nothing. |
| - | ## 5. Data model | + | ===== 5. Data model ===== |
| - | One table, | + | One table, |
| **Columns and which request writes them** | **Columns and which request writes them** | ||
| - | | Column | + | ^ Column |
| - | | --- | --- | --- | --- | | + | | '' |
| - | | `id` | int, identity, primary key | database | Row number | | + | | '' |
| - | | `DbDateTime` | datetime, required | Interim-Update | `GETDATE()` on the SQL Server, at insert | | + | | '' |
| - | | `Acct_Session_Id` | varchar(50), | + | | '' |
| - | | `h323_conf_id` | varchar(50), | + | | '' |
| - | | `h323_gw_id` | varchar(100), | + | | '' |
| - | | `customerPhoneNumber` | varchar(50) | Interim-Update | Inbound (`answer`): `User-Name`. Outbound (`originate`): `Called-Station-Id` converted to E.164. Any other origin: NULL | | + | | '' |
| - | | `inboundCall` | bit | Interim-Update | 1 if `h323-call-origin` is `answer`, otherwise 0 | | + | | '' |
| - | | `callActive` | bit | Interim-Update, | + | | '' |
| - | | `h323_setup_time` | datetime | Interim-Update | `h323-setup-time`, Irish local time | | + | | '' |
| - | | `h323_connect_time` | datetime | Interim-Update | `h323-connect-time`, Irish local time | | + | | '' |
| - | | `h323_disconnect_time` | datetime | Stop | `h323-disconnect-time`, Irish local time | | + | | '' |
| - | | `callDuration` | int | Stop | Seconds from connect to disconnect (see section 7) | | + | | '' |
| - | | `h323_disconnect_cause` | int | Stop | `h323-disconnect-cause`, converted from hex to decimal | | + | | '' |
| - | | `GW_CalledNumber` | varchar(50) | Interim-Update | `gw-cdn` | | + | | '' |
| - | | `Called_Station_Id` | varchar(50) | Interim-Update | `Called-Station-Id`, as received | | + | | '' |
| - | | `User_Name` | varchar(50) | Interim-Update | `User-Name` | | + | | '' |
| - | | `NAS_IP_Address` | varchar(15) | Interim-Update | `NAS-IP-Address` | | + | | '' |
| - | | `h323_remote_address` | varchar(50) | Stop | `h323-remote-address` | | + | | '' |
| - | | `Acct_Session_Time` | int | Stop | `Acct-Session-Time` | | + | | '' |
| - | | `contactCentreCall` | bit | Interim-Update | 1 if `h323-gw-id` is in `$CCE_CUBES` (case-insensitive), | + | | '' |
| - | | `contactCentreAgentConnected` | bit, default 0 | finesse-start | Set to 1 | | + | | '' |
| - | | `dialogId` | varchar(50) | finesse-start | `dialogId`, kept as text (may have a leading 0) | | + | | '' |
| - | | `agentId` | varchar(32) | finesse-start | `agentId` | | + | | '' |
| - | | `agentFirstName` | varchar(50) | finesse-start | `agentFirstName` | | + | | '' |
| - | | `agentLastName` | varchar(50) | finesse-start | `agentLastName` | | + | | '' |
| - | | `agentExtension` | varchar(20) | finesse-start | `agentExtension`, kept as text | | + | | '' |
| - | | `cceCallType` | varchar(50) | finesse-start | `callType` | | + | | '' |
| - | | `queueName` | varchar(50) | finesse-start | `queueName`, including the literal | + | | '' |
| - | | `agentUpdates` | int, default 0 | finesse-start | Increased by 1 on every Finesse update | | + | | '' |
| - | | `recordUpdates` | int | all three | 1 at insert, then increased by 1 on every Stop and Finesse update | | + | |
| **Keys and indexes** | **Keys and indexes** | ||
| - | | Name | Columns | + | ^ Name ^ Columns |
| - | | --- | --- | --- | | + | | '' |
| - | | `PK_radiusVoiceCalls` (clustered) | `id` | Updates by row | | + | | '' |
| - | | `UQ_h323_conf_id` (unique) | `h323_conf_id` | Every lookup by call ID, and the guard against duplicate inserts | | + | | '' |
| - | | `IX_radiusVoiceCalls_phone_active` (recommended, | + | |
| - | ```sql | + | < |
| CREATE NONCLUSTERED INDEX [IX_radiusVoiceCalls_phone_active] | CREATE NONCLUSTERED INDEX [IX_radiusVoiceCalls_phone_active] | ||
| ON [dbo].[radiusVoiceCalls] ([customerPhoneNumber], | ON [dbo].[radiusVoiceCalls] ([customerPhoneNumber], | ||
| INCLUDE ([h323_connect_time], | INCLUDE ([h323_connect_time], | ||
| - | ``` | + | </ |
| - | ## 6. SQL queries | + | ===== 6. SQL queries |
| - | Each request opens its own connection (`getDbConnection()`), runs all its statements inside one transaction, | + | Each request opens its own connection ('' |
| - | ### Interim-Update (`insertInterimIfNew`) | + | ==== Interim-Update ('' |
| **Q1. Does the call already have a row?** | **Q1. Does the call already have a row?** | ||
| - | ```sql | + | < |
| SELECT id | SELECT id | ||
| FROM dbo.radiusVoiceCalls WITH (UPDLOCK, HOLDLOCK) | FROM dbo.radiusVoiceCalls WITH (UPDLOCK, HOLDLOCK) | ||
| WHERE h323_conf_id = :conf | WHERE h323_conf_id = :conf | ||
| - | ``` | + | </ |
| - | The lookup uses `UQ_h323_conf_id`. `UPDLOCK, HOLDLOCK` keeps the looked-up key range locked until the transaction ends, so a second request for the same call waits instead of inserting in the gap between this check and the insert. If a row comes back, nothing is written and the script logs the request at DEBUG and returns 200. | + | The lookup uses '' |
| **Q2. Insert the row (only when Q1 found nothing)** | **Q2. Insert the row (only when Q1 found nothing)** | ||
| - | ```sql | + | < |
| INSERT INTO dbo.radiusVoiceCalls | INSERT INTO dbo.radiusVoiceCalls | ||
| (DbDateTime, | (DbDateTime, | ||
| Line 199: | Line 193: | ||
| :usr, :nas, :setup, :conn, :inb, | :usr, :nas, :setup, :conn, :inb, | ||
| :cpn, 1, :cce, 1) | :cpn, 1, :cce, 1) | ||
| - | ``` | + | </ |
| - | `callActive` and `recordUpdates` are set to 1 in the statement. Optional values that were absent from the request are bound as NULL. | + | '' |
| - | **Duplicate-key retry.** If two requests for the same new call overlap and the insert fails with a duplicate-key error on `UQ_h323_conf_id`, the transaction is rolled back and the whole function runs once more. The second attempt finds the row in Q1 and returns without writing. Any other database error, or a second failure, returns 500. | + | **Duplicate-key retry.** If two requests for the same new call overlap and the insert fails with a duplicate-key error on '' |
| - | ### Stop (`updateStop`) | + | ==== Stop ('' |
| **Q3. Find the row** | **Q3. Find the row** | ||
| - | ```sql | + | < |
| SELECT id, h323_disconnect_time | SELECT id, h323_disconnect_time | ||
| FROM dbo.radiusVoiceCalls WITH (UPDLOCK) | FROM dbo.radiusVoiceCalls WITH (UPDLOCK) | ||
| WHERE h323_conf_id = :conf | WHERE h323_conf_id = :conf | ||
| - | ``` | + | </ |
| No row means the Stop arrived without an Interim-Update (for example a call that never connected): the script logs an ERROR with the request and returns 404. A row that already has a disconnect time logs a WARNING and is still updated. | No row means the Stop arrived without an Interim-Update (for example a call that never connected): the script logs an ERROR with the request and returns 404. A row that already has a disconnect time logs a WARNING and is still updated. | ||
| Line 219: | Line 213: | ||
| **Q4. Close the call** | **Q4. Close the call** | ||
| - | ```sql | + | < |
| UPDATE dbo.radiusVoiceCalls SET | UPDATE dbo.radiusVoiceCalls SET | ||
| h323_disconnect_time | h323_disconnect_time | ||
| Line 229: | Line 223: | ||
| recordUpdates | recordUpdates | ||
| WHERE id = :id | WHERE id = :id | ||
| - | ``` | + | </ |
| - | `COALESCE` means a value that could not be read from the Stop request leaves the stored value as it was. | + | '' |
| - | ### finesse-start (`findFinesseRow`, then update) | + | ==== finesse-start ('' |
| - | **Q5. Find by call ID** (when `h323ConfId` is a real value) | + | **Q5. Find by call ID** (when '' |
| - | ```sql | + | < |
| SELECT id FROM dbo.radiusVoiceCalls WITH (UPDLOCK) WHERE h323_conf_id = :conf | SELECT id FROM dbo.radiusVoiceCalls WITH (UPDLOCK) WHERE h323_conf_id = :conf | ||
| - | ``` | + | </ |
| No row returns 404. There is no fallback to the phone number. | No row returns 404. There is no fallback to the phone number. | ||
| - | **Q6. Find by phone number** (when `h323ConfId` is `not_set`, empty or missing) | + | **Q6. Find by phone number** (when '' |
| - | ```sql | + | < |
| SELECT id FROM dbo.radiusVoiceCalls WITH (UPDLOCK) | SELECT id FROM dbo.radiusVoiceCalls WITH (UPDLOCK) | ||
| WHERE customerPhoneNumber = :ph AND callActive = 1 | WHERE customerPhoneNumber = :ph AND callActive = 1 | ||
| - | ``` | + | </ |
| No rows returns 404. One row is updated. More than one row goes to Q7. | No rows returns 404. One row is updated. More than one row goes to Q7. | ||
| Line 254: | Line 248: | ||
| **Q7. Narrow by connect time** (only when Q6 returned more than one row) | **Q7. Narrow by connect time** (only when Q6 returned more than one row) | ||
| - | ```sql | + | < |
| SELECT id FROM dbo.radiusVoiceCalls WITH (UPDLOCK) | SELECT id FROM dbo.radiusVoiceCalls WITH (UPDLOCK) | ||
| WHERE customerPhoneNumber = :ph AND callActive = 1 AND h323_connect_time >= :cutoff | WHERE customerPhoneNumber = :ph AND callActive = 1 AND h323_connect_time >= :cutoff | ||
| - | ``` | + | </ |
| - | `:cutoff` is the current time in Europe/ | + | '' |
| **Q8. Write the agent details** | **Q8. Write the agent details** | ||
| - | ```sql | + | < |
| UPDATE dbo.radiusVoiceCalls SET | UPDATE dbo.radiusVoiceCalls SET | ||
| dialogId = :dlg, agentId = :aid, agentFirstName = :fn, agentLastName = :ln, | dialogId = :dlg, agentId = :aid, agentFirstName = :fn, agentLastName = :ln, | ||
| Line 271: | Line 265: | ||
| recordUpdates = ISNULL(recordUpdates, | recordUpdates = ISNULL(recordUpdates, | ||
| WHERE id = :id | WHERE id = :id | ||
| - | ``` | + | </ |
| - | Values are written exactly as sent, including | + | Values are written exactly as sent, including |
| - | ## 7. Data conversions | + | ===== 7. Data conversions |
| Request values are cleaned before they reach SQL. Anything that cannot be converted is stored as NULL and logged at WARNING, and the rest of the request still goes ahead. | Request values are cleaned before they reach SQL. Anything that cannot be converted is stored as NULL and logged at WARNING, and the rest of the request still goes ahead. | ||
| - | | Value | Rule | Example | + | ^ Value ^ Rule ^ Example |
| - | | --- | --- | --- | | + | | Text fields | Trimmed. Empty becomes NULL. Longer than the column: cut to the column length and logged at WARNING with the column name and original length. | '' |
| - | | Text fields | Trimmed. Empty becomes NULL. Longer than the column: cut to the column length and logged at WARNING with the column name and original length. | `" | + | | Call ID ('' |
| - | | Call ID (`h323-conf-id`, `h323ConfId`) | All whitespace removed. For Finesse, a missing, empty or `not_set` value (any case) means "no call ID". | `DB0B4B6B CBBB11F1 AE7699CA 3DE394D9` becomes | + | | Cisco times | Format |
| - | | Cisco times | Format | + | | Disconnect cause | Sent as hex. A leading |
| - | | Disconnect cause | Sent as hex. A leading | + | | Whole numbers ('' |
| - | | Whole numbers (`Acct-Session-Time`) | Must be an integer from 0 to 2, | + | | Gateway match | '' |
| - | | Gateway match | `h323-gw-id` compared with `$CCE_CUBES` ignoring case. | Match sets `contactCentreCall` to 1 | | + | |
| - | ### Call duration | + | ==== Call duration |
| - | Duration is only worked out when the Stop request carries | + | Duration is only worked out when the Stop request carries |
| - | 1. Both times are read as time-zone-aware timestamps, so the clocks going back in autumn cannot distort the result. | + | - Both times are read as time-zone-aware timestamps, so the clocks going back in autumn cannot distort the result. |
| - | 2. `callDuration` is the difference in seconds, rounded to a whole second (8.945 seconds is stored as 9). | + | - '' |
| - | 3. If either time is unreadable, or the disconnect is before the connect, the script uses `Acct-Session-Time` instead and logs the reason. The repeated hour at the autumn clock change (01:00 to 02:00) is ambiguous, and a call spanning it can hit this fallback. | + | - If either time is unreadable, or the disconnect is before the connect, the script uses '' |
| - | ### Customer phone number | + | ==== Customer phone number |
| - | The value stored in `customerPhoneNumber` depends on the call direction, taken from `h323-call-origin` (case-insensitive). | + | The value stored in '' |
| - | | Origin | + | ^ Origin |
| - | | --- | --- | --- | | + | | '' |
| - | | `answer` | Inbound | `User-Name`, unchanged | | + | | '' |
| - | | `originate` | Outbound | `Called-Station-Id`, converted to E.164 with the rules below | | + | |
| | Anything else | Unknown | NULL (logged at DEBUG) | | | Anything else | Unknown | NULL (logged at DEBUG) | | ||
| - | Outbound numbers arrive with an extra leading | + | Outbound numbers arrive with an extra leading |
| - | | Starts with | Becomes | + | ^ Starts with ^ Becomes |
| - | | --- | --- | --- | | + | | '' |
| - | | `000` | `+` followed by the rest | `000442012345678` becomes | + | | '' |
| - | | `00` | `+353` followed by the rest | `00858769707` becomes | + | | '' |
| - | | `0` | `+353` followed by the rest | `01800123123` becomes | + | | Anything else (an extension, or already |
| - | | Anything else (an extension, or already | + | |
| - | ## 8. Configuration, | + | ===== 8. Configuration, |
| - | ### Configuration | + | ==== Configuration |
| - | All settings are variables and constants at the top of `radiusVoiceCalls.php`. | + | All settings are variables and constants at the top of '' |
| - | | Setting | + | ^ Setting |
| - | | --- | --- | --- | | + | | '' |
| - | | `$DEBUG` | `false` | `true` writes DEBUG lines to the PHP error log. Errors and warnings are always written. | | + | | '' |
| - | | `$allowed_ips` | `[' | + | | '' |
| - | | `$api_key` | `1234` (placeholder) | Required in the `api-key` header from every client except | + | | '' |
| - | | `$CCE_CUBES` | one test gateway | `h323-gw-id` values that belong to Cisco CCE. A match sets `contactCentreCall` to 1. | | + | | '' |
| - | | `$FINESSE_MATCH_SECONDS` | `5` | Window for narrowing a phone-number lookup that matches several active calls (section 6, Q7). | | + | | '' |
| - | | `MAX_BODY_BYTES` | 65,536 | Largest request body accepted. | | + | | '' |
| - | | `LOCAL_TZ` | `Europe/ | + | |
| - | The database host, name, user and password live in `include/ | + | The database host, name, user and password live in '' |
| - | ### Logging | + | ==== Logging |
| - | Lines go to the PHP error log in the form `|radiusVoiceCalls.php|LEVEL|{json}`. The JSON always has a `msg`, plus `client_ip`, and `acct_session_id`, `h323_conf_id` and `dialog_id` once the request has supplied them, so one call can be followed across lines. Error lines include the request body (cut to 2,000 characters when the JSON itself is invalid). The `api-key` value is never logged. | + | Lines go to the PHP error log in the form '' |
| - | | Level | Used for | | + | ^ Level ^ Used for ^ |
| - | | --- | --- | | + | | '' |
| - | | `DEBUG` (only when `$DEBUG` is true) | Every request received, results of each handler, repeat Interim-Updates that were ignored, the connect-time window being applied, fields absent from a Finesse update | | + | | '' |
| - | | `WARNING` | Rejected requests (wrong IP, method or api-key), values truncated or converted to NULL, a Stop for a call that already has a disconnect time, a negative duration | | + | | '' |
| - | | `ERROR` | Invalid JSON, missing required fields, unknown status type, no matching row, more than one matching row, database failures, unhandled exceptions | | + | |
| - | ### Response codes | + | ==== Response codes ==== |
| - | | Code | Message | + | ^ Code ^ Message |
| - | | --- | --- | --- | | + | | 200 | '' |
| - | | 200 | `OK` | Request processed. This includes an Interim-Update that was ignored because the row already exists. | | + | | 400 | '' |
| - | | 400 | `Empty request body`, `Invalid JSON`, `JSON body must be an object` | Body problem | | + | | 401 | '' |
| - | | 401 | `Invalid api-key` | Header missing or wrong | | + | | 403 | '' |
| - | | 403 | `Forbidden` | Client IP not allowed | | + | | 404 | '' |
| - | | 404 | `Record not found` | Stop or finesse-start found no matching row | | + | | 405 | '' |
| - | | 405 | `Method not allowed` | Anything other than PUT | | + | | 413 | '' |
| - | | 413 | `Request body too large` | Over 65,536 bytes | | + | | 422 | '' |
| - | | 422 | `Missing required field(s): ...`, `Missing or unknown Acct-Status-Type`, `More than one matching record` | Validation failed, or the phone-number lookup was ambiguous | | + | | 500 | '' |
| - | | 500 | `Database error`, `Internal error` | Any database or unexpected failure. The transaction is rolled back, so nothing partial is stored. | | + | |
| - | **Required fields.** An Interim-Update needs `Acct-Session-Id`, `h323-conf-id` and `h323-gw-id`, because those columns cannot be NULL. A Stop needs `h323-conf-id`. A finesse-start needs either a real `h323ConfId` or a `phoneNumber`. Every other field is optional. | + | **Required fields.** An Interim-Update needs '' |
| - | ## 9. Operations, assumptions and known limitations | + | ===== 9. Operations, assumptions and known limitations |
| - | ### Deployment checklist | + | ==== Deployment checklist |
| - | - Apache must pass `PUT` requests to PHP. Check that no `Limit` or `LimitExcept` rule blocks the method. | + | * Apache must pass '' |
| - | - PHP needs the `pdo_sqlsrv` extension and the Microsoft ODBC driver for SQL Server. | + | * PHP needs the '' |
| - | - The database login needs `SELECT`, `INSERT` and `UPDATE` on `dbo.radiusVoiceCalls`. | + | * The database login needs '' |
| - | - `include/ | + | * '' |
| - | - Fill in `$allowed_ips`, `$api_key` and `$CCE_CUBES`. Add the FreeRADIUS server and the Finesse server to `$allowed_ips`. | + | * Fill in '' |
| - | - Serve the script directly from Apache. Behind a local reverse proxy every request looks like `127.0.0.1` and would skip the api-key check. | + | * Serve the script directly from Apache. Behind a local reverse proxy every request looks like '' |
| - | - Use HTTPS at Apache, because the api-key travels in a header. | + | * Use HTTPS at Apache, because the api-key travels in a header. |
| - | - Create the index in section 5. | + | * Create the index in section 5. |
| - | - Read the PHP error log for lines starting | + | * Read the PHP error log for lines starting |
| - | ### Known behaviours | + | ==== Known behaviours |
| - | | Behaviour | + | ^ Behaviour |
| - | | --- | --- | | + | | The row is only inserted by the first Interim-Update for an '' |
| - | | The row is only inserted by the first Interim-Update for an `h323-conf-id`. Later ones are ignored. | Changes in later Interim-Updates (for example a different | + | |
| | A Stop with no row returns 404 and logs an ERROR. | Expected for calls that never connected, because no Interim-Update was sent. The ERROR lines can be noisy. | | | A Stop with no row returns 404 and logs an ERROR. | Expected for calls that never connected, because no Interim-Update was sent. The ERROR lines can be noisy. | | ||
| | A finesse-start that arrives before the Interim-Update returns 404. | There is no retry. The Finesse side must send after the call has been reported. | | | A finesse-start that arrives before the Interim-Update returns 404. | There is no retry. The Finesse side must send after the call has been reported. | | ||
| - | | The `not_set` lookup compares the stored connect time with the web server' | + | | The '' |
| - | | The phone-number lookup is an exact text match. | It relies on `customerPhoneNumber` and the Finesse | + | | The phone-number lookup is an exact text match. | It relies on '' |
| | finesse-start overwrites its columns with whatever it is sent. | A later update replaces earlier agent details, and a missing field becomes NULL. | | | finesse-start overwrites its columns with whatever it is sent. | A later update replaces earlier agent details, and a missing field becomes NULL. | | ||
| - | | `contactCentreAgentConnected` is a bit. | It means "a Finesse update has been received" | + | | '' |
| - | | Times are stored as Irish local time with no zone. | At the autumn clock change, times in the repeated hour are ambiguous. Durations fall back to `Acct-Session-Time` when needed. | | + | | Times are stored as Irish local time with no zone. | At the autumn clock change, times in the repeated hour are ambiguous. Durations fall back to '' |
| - | | If a Stop is lost, `callActive` stays 1. | The reading system sees a call that never ends, and the phone-number lookup can match more than one row. | | + | | If a Stop is lost, '' |
| + | |||
| + | **Recommended housekeeping (not built).** A scheduled SQL job that sets '' | ||
| - | **Recommended housekeeping (not built).** A scheduled | + | ===== Troubleshooting ===== |
| + | ==== SQL Queries | ||
| + | === Find Active Calls older than 3 hours ==== | ||
| + | <code sql> | ||
| + | SELECT * FROM radiusVoiceCalls | ||
| + | WHERE h323_connect_time < DATEADD(HOUR,-3, | ||
| + | AND callActive = 1 | ||
| + | order by id | ||
| + | </ | ||
| - | ### Test status | + | === Update invalid Active Calls ==== |
| + | === STEP 1 - Update stale calls (more than 3 hours active) as non active with a disconnect cause of 128=== | ||
| + | Why disconnect cause of 128? | ||
| + | * Valid Disconnect causes are from 1 - 127, so 128 will never be sent by Radius - so we KNOW any record that has this connect cause was updated by this SQL query (which will run as a SQL job ever ~10 mins) | ||
| + | * We can find these calls later - by checking how many rows have a disconnect cause of 128 | ||
| + | <code sql> | ||
| + | UPDATE radiusVoiceCalls | ||
| + | SET | ||
| + | callActive = 0, | ||
| + | h323_disconnect_cause = 128, | ||
| + | h323_disconnect_time = GetDate() | ||
| - | The script passes a PHP syntax check. The conversion functions, the Finesse row lookup and the request handling were exercised with a stand-in database that returned scripted rows and recorded each statement. It has not yet been run against a real SQL Server, so the first live Interim-Update, | + | WHERE h323_connect_time < DATEADD(HOUR,-3,GETDATE()) |
| + | AND callActive = 1 | ||
| + | </ | ||
| + | === STEP 2 - Update the calls as non active with a disconnect cause of 128 === | ||
| + | We can find these calls later - by checking how many rows have a disconnect cause of 128 | ||
| + | <code sql> | ||
| + | UPDATE radiusVoiceCalls | ||
| + | SET | ||
| + | callDuration = DATEDIFF(SECOND, | ||
| + | Acct_Session_Time = DATEDIFF(SECOND, | ||
| + | WHERE | ||
| + | h323_disconnect_cause = 128 | ||
| + | AND callDuration is Null | ||
| + | </ | ||