# CEM Vtiger Read-only Adapter

Phase 9B supplies real CRM facts to the CEM CRM Production Consumer without creating any external effect. The legacy listener remains authoritative for production CRM behavior. Related architecture: MPBX-002, MPBX-004, MPBX-006 and MPBX-CONF-001.

## Trust boundary

The dependency direction is CEM Core → Consumer → Integration Adapter → Vtiger. Vtiger tables, identifiers and lookup behavior do not enter the canonical envelope, Call Aggregate or Event Journal.

Use a dedicated database account restricted to `SELECT` on the required Vtiger tables. Database privileges are authoritative; the application query guard is defense in depth. It accepts only `SELECT`, `SHOW`, `DESCRIBE` and `EXPLAIN`, rejects multiple statements and write/locking/procedure forms, and uses fixed parameterized queries. Credentials are read from an ignored restrictive local file and errors expose no connection string, row content or caller identity.

## Observed legacy schema semantics

The authoritative listener uses `vtiger_users`, `vtiger_user2role` and `vtiger_role` to resolve `phone_crm_extension`. With configured inbound roles it requires an exact extension, `Active` status and exact role-name membership. With no configured roles, the legacy helper accepts any nonempty extension before querying CRM; Phase 9B deliberately uses the safer active-user registry even when its role allowlist is empty. This is a documented parity difference.

Legacy contact resolution normalizes the caller digits, suffix-matches contact phone/mobile fields, excludes deleted contacts/accounts, and returns numeric contact ID, trimmed first/last display name, account name and the open HelpDesk ticket count. Its `LIMIT 1` does not expose ambiguity. The read-only adapter reads at most two ordered matches and reports ambiguity explicitly. For a unique match it exposes only opaque references and the minimum popup-compatible display fields; numeric `contact_id` is adapter compatibility data, not canonical identity.

The legacy activity-owner lookup is a separate extension-to-user query without the same role/status filter. Phase 9B does not reproduce that side-effect path because effects remain disabled.

## Registry and lookup behavior

Only active users with normalized numeric extensions are eligible. Exact role filtering is applied when roles are configured. Duplicate extensions are excluded deterministically. The registry is cached for a bounded TTL; explicitly bounded stale data may be used after refresh failure, while absence of usable cache is retryable infrastructure failure.

Contact outcomes are `MATCHED`, `NOT_FOUND`, `AMBIGUOUS` and `UNAVAILABLE`. Unknown extensions, inactive users and role mismatch are policy outcomes; database failure is not silently converted to not-applicable. Contact no-match does not necessarily prevent action planning, matching proven legacy behavior.

Only `MATCHED` may carry contact display name, organization display name, open-ticket count and numeric compatibility ID. `NOT_FOUND`, `AMBIGUOUS` and `UNAVAILABLE` expose none of those identity fields. Display strings are trimmed, control characters are removed and length is bounded. They remain transient between adapter, planner and popup publisher and never enter canonical events, journals, audits, checkpoints, idempotency state or metrics.

The lookup input is canonical ringing `caller_identity`, not a value parsed by the CRM adapter from Grandstream evidence. It is normalized upstream from inbound root-leg `CallerIDNum`. No-match is a normal result: the eligible ring popup still carries the validated number but has null `contact_id`, empty name/organization and zero open tickets. Ambiguous or unavailable lookup follows the same unknown-contact behavior and never guesses a record.

## Configuration and preflight

Keep `crm_registry_mode=static` and the consumer `enabled=false` by default. To validate Vtiger reads, create an ignored local configuration, set the adapter to `vtiger-readonly`, supply a dedicated read-only account through the password-file reference, and run:

```text
php tools/cem_crm_consumer.php --config <local-config> --crm-preflight
```

The safe output contains connection/schema status and aggregate user/extension counts only. A subsequent journal run must use `--dry-run`; expected external execution and committed-action counts remain zero.

Rollback is configuration-only: disable the Vtiger section and return `crm_registry_mode` to `static`. No production listener, AMI, WebSocket or Vtiger API behavior is changed by this adapter.
