# CEM CRM Production Consumer

Phase 9A introduces a production-capable CRM consumer architecture while keeping every external side effect disabled by default. The legacy listener remains authoritative for production CRM effects. No cutover is authorized.

Related architecture: MPBX-002, MPBX-004, MPBX-006 and MPBX-CONF-001.

## Boundary

```text
Event Journal
→ CRM Production Consumer
→ shared CRM Shadow eligibility policy
→ CRM Action Planner
→ CRM Idempotency Guard
→ CRM Adapter contracts
```

The planner reuses the Phase 7 CRM Shadow decision handler for direction, tenant, environment, queue and CRM-extension eligibility. It translates applicable decisions into typed actions without copying those business rules.

Supported actions are `POPUP_RING`, `POPUP_ANSWERED`, `ACTIVITY_COMPLETED`, `ACTIVITY_MISSED` and `RECORDING_LINK`.

## Safety mode

`enabled` defaults to `false`. In disabled mode the consumer may validate journal records, create plans and write safe `PLANNED` audit/idempotency state, but it never invokes an external adapter and never marks an action `COMMITTED`. Dry-run writes no checkpoint, action audit, idempotency state or dead letter.

All adapters supplied by Phase 9A are disabled implementations. Enabling the consumer without a separately reviewed adapter therefore fails safely rather than creating an external effect.

Phase 9B adds an optional read-only Vtiger adapter. It may perform parameterized MySQL reads when explicitly configured, but it cannot write CRM data, publish WebSocket messages or invoke Vtiger APIs. All effect adapters remain disabled.

## Phase 9B read-only enrichment

`crm_registry_mode` selects `static` (the default) or `vtiger-readonly`. The Vtiger mode converts a read-only user snapshot into the same Phase 7 extension-registry shape consumed by the shared eligibility policy. Vtiger schema concepts therefore remain below the Consumer/Integration boundary and never enter CEM, the Event Journal or the Call Aggregate.

Contact lookup is enrichment only. A unique match returns opaque contact/account references, bounded contact and organization display names, the numeric contact ID required by existing browser navigation, and an open-ticket count. No match, ambiguity and database failure remain distinct and provide no display identity. No-match does not suppress an otherwise valid action because the observed legacy path permits popup/activity handling without a resolved contact. Caller and CRM display identity are never canonical identity or written to action/authority audit, checkpoint or idempotency state.

The extension registry uses a bounded TTL snapshot rather than querying for every event. A refresh failure may use an existing snapshot only within the configured stale interval; otherwise processing fails safely. Duplicate extensions are excluded as ambiguous and malformed extensions are rejected.

## Phase 9C0 side-effect authority

Eligibility answers whether an action is applicable; authority independently answers which runtime owns that action. Phase 9C0 resolves every planned action through the authority policy and writes a minimized authority audit before execution is considered.

The safe default is `LEGACY`. Even an explicit `CEM` rule cannot execute unless the consumer, cutover interlock and adapter gates are all independently enabled and idempotency permits the attempt. The supplied configuration enables none of those gates and contains no CEM-owned scope. See Implementation Guide: CEM_CRM_AUTHORITY.

Phase 9C0.1 makes the expiring authority snapshot the runtime decision source for both CEM and the legacy listener. Missing, invalid, stale or integrity-failed state blocks CEM execution while preserving legacy effects. Configuration alone does not switch ownership; it must be validated and explicitly exported to the shared runtime artifact.

Phase 9C2.1 makes legacy suppression conditional on a second, short-lived runtime health lease bound to the exact authority generation and scope. Only non-dry-run follow mode refreshes it. The lease is ready when consumer, adapter, popup adapter, journal, caught-up checkpoint and passive bridge readiness are all true. Dry-run, Vtiger preflight and authority export do not write it. Controlled shutdown writes a non-ready lease; missing or expired health restores legacy ownership. CEM still applies all original authority, adapter and idempotency gates independently.

Phase 9C2.2 separates queue-aware CEM execution from queue-unaware legacy suppression. CEM continues to require the canonical queue and extension. Legacy can suppress the linked extension only when the same reviewed rule explicitly defines a suppression scope, the health lease matches, and a dedicated-extension operational constraint is recorded. No queue parsing or PBX normalization is added to the listener. A rule without explicit suppression metadata leaves legacy authoritative.

Phase 9C2.3 atomically pins one `POPUP_RING` action to one immutable authority generation using a SHA-256 hash of the shared PBX call reference plus action and extension. Both legacy suppression and CEM execution resolve against that pinned snapshot. Rotation changes ownership for the next action, not an already in-flight action. Pin state contains no caller/contact data and is not a substitute for adapter idempotency or reconciliation.

## Phase 9C1 ring popup adapter

Phase 9C1 adds the first selectable real adapter, limited to `POPUP_RING` over the existing local TCP-to-WebSocket bridge. It remains disabled in the sample. `POPUP_ANSWERED`, activity and recording adapters remain disabled. A complete newline-framed bridge write is the commit signal; it proves only local bridge acceptance, not browser display.

Caller identity, when present in validated canonical context, is held transiently in the action and bridge frame. It is never used as canonical identity and is excluded from action, authority and idempotency state. See Implementation Guide: CEM_CRM_POPUP_ADAPTER.

Phase 9C1.1 makes `payload.agent_extension` authoritative for ringing. Grandstream destination-channel parsing belongs to the Gateway adapter and is no longer required for newly normalized events; the older consumer fallback remains compatibility-only. Queue eligibility is unchanged and still requires reliable canonical queue context.

Phase 9C1.2b makes `payload.queue_id` authoritative when explicit AMI `Queue` evidence reached the aggregate before ringing. Queue-family event ordering may enrich the aggregate before a `DialBegin` that lacks the field. Consumers use the PBX queue identity directly for registry lookup and do not parse dialplan, caller, connected-line or channel fields. Late queue evidence does not retroactively make an already emitted ringing action eligible.

Phase 9C1.3 enriches the transient `POPUP_RING` payload from a unique Vtiger read-only contact match. It preserves the existing browser field names and unknown-contact behavior without modifying the canonical event or journal. Ambiguous, missing or unavailable lookup results do not block an otherwise eligible ring popup and never select an arbitrary CRM record.

Phase 9C1.4 provides the planner with canonical `payload.caller_identity` derived upstream from validated inbound root-leg evidence. The planner does not inspect AMI or PBX channel fields. `MATCHED` produces the transient popup display context; `NOT_FOUND`, `AMBIGUOUS` and `UNAVAILABLE` preserve popup eligibility with null/empty contact fields. Caller identity is persisted in the canonical event and journal for this replayable DEV path, but is excluded from action/authority audits, checkpoints owned by the CRM consumer and action idempotency records.

## Idempotency and recovery

Logical action identities are stable per correlation and action target. State transitions are:

```text
PLANNED → EXECUTING → COMMITTED
                    ↘ FAILED_RETRYABLE
                    ↘ FAILED_TERMINAL
```

State is written atomically with restrictive permissions. `EXECUTING` is persisted before adapter invocation. An interrupted `EXECUTING` action requires reconciliation and is not blindly replayed. A successful adapter result is recorded before the action becomes `COMMITTED`.

The journal checkpoint, action state and an external system cannot form one local transaction. Phase 9A therefore makes no distributed exactly-once claim. Future adapters must reconcile their external action identity before retrying an uncertain outcome.

The append-only action audit contains identifiers, action type, status, attempt, timestamps and a safe category only. It excludes the canonical event, raw AMI, caller number, CRM payload, recording path and credentials.

## Configuration and operation

Copy the production-consumer sample to its ignored local configuration name and keep `enabled` false unless a separately approved later phase authorizes otherwise. Runtime directories and files must remain outside Git and be accessible only to the runtime identity.

The CLI accepts a journal, consumer checkpoint, dead-letter destination and local configuration. `--dry-run` is the preferred Phase 9A validation mode. `--once` and `--follow` are mutually exclusive.

`--crm-preflight` performs a read-only connection/schema/registry check and prints aggregate counts only. It never prints users, contacts, SQL rows or credentials.

## Planned progression

- Phase 9B: read-only Vtiger integration (implemented; explicit configuration required).
- Phase 9C1: controlled DEV ring-popup adapter (implemented, disabled by default, no rollout authorization).
- Phase 9D: evidence-based cutover.

None of these later phases is authorized by Phase 9A. The legacy listener remains active and authoritative until an independently reviewed cutover.
