# Reflect 3 Crm Support Console

> Resolve CRM support-console tasks (SCN_003) — ticket-batch classification, mobile-data triage, and enterprise export-complaint response packages — against a remote support-console API. Use when a task asks to classify service tickets / queue snapshots, triage mobile data cases, or build an enterprise export-failure response package and references a support console API.

- Skill: `prism-shadow/reflect-3-crm-support-console` (Agent Skill)
- Install (CLI): `npx skillmds@latest add prism-shadow/reflect-3-crm-support-console`
- Raw SKILL.md: https://api.skillmd.com/api/skills/prism-shadow/reflect-3-crm-support-console/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: prism-shadow (https://skillmd.com/u/prism-shadow)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/prism-shadow/reflect-3-crm-support-console

---


# SCN_003 CRM Support Console — Resolution Skill

Reusable rules for three task families driven by a shared support-console API.
**Base URL**: use the base URL supplied by the harness/environment (the prompts mention a 127.0.0.1 address — ALWAYS substitute the provided base URL). Records are the source of truth; never assume field values — fetch them.

## API MAP (substitute real ids)

| Endpoint | Use |
|---|---|
| `GET /health` | liveness |
| `GET /api/catalog` | endpoint + record-count catalog (public) |
| `GET /api/accounts`, `GET /api/accounts/<account_id>` | account status, authentication, service_area, tier |
| `GET /api/tickets`, `GET /api/tickets/<ticket_id>` | ticket: account_id, service_area, service_type, subscribed_mbps |
| `GET /api/outages?service_area=<area>` | outages: active, service_area, service_types[], outage_id |
| `GET /api/diagnostics/<ticket_id>` | bandwidth_mbps, latency_ms, jitter_ms, root_causes[] |
| `GET /api/troubleshooting/<ticket_id>` | post_bandwidth_mbps, post_latency_ms, post_jitter_ms, steps[] |
| `GET /api/customers`, `GET /api/lines`, `GET /api/lines/<line_id>` | line: status, roaming_enabled, plan_id, data_used_gb, suspension_reason |
| `GET /api/devices/<device_id>` | device: sim_status, mobile_data_enabled, phone_roaming_enabled, data_saver_mode, network_mode_preference, vpn_connected, speed_test, messaging_permissions |
| `GET /api/plans`, `GET /api/plans/<plan_id>` | data_limit_gb, data_refueling_price_per_gb, monthly_price_usd |
| `GET /api/bills` | bills: customer_id, status, amount_due_usd (match by customer_id for overdue) |
| `GET /api/cases`, `GET /api/cases/<case_id>` | case: customer_id, line_id, device_id, customer_location, issue_type |
| `GET /api/enterprise/accounts` | enterprise_account_id, name, account_owner, finance_owner, tier |
| `GET /api/enterprise/incidents` | incident_id, enterprise_account_id, product, severity, status, engineering_owner, account_owner |
| `GET /api/enterprise/export-runs?incident_id=<id>` | run_date, status (FAILED/SUCCEEDED), failure_code, exported_record_count |
| `GET /api/enterprise/messages?query=<text>` | author, body, channel, created_at |
| `GET /api/enterprise/sla/<enterprise_account_id>` | credit_trigger, monthly_export_credit_percent |

Ignore generated/distractor records (names like "Generated ...", ticket ids TCK-8xxx, incident ids INC-9xxx, plans PLAN-G*): they are never task targets. Real task records have descriptive names/ids.

---

## FAMILY 1 — TICKET BATCH / QUEUE CLASSIFICATION

Two variants exist with different output schemas (always conform to the given template).

### CONVERGED FLOORS (exact)
- **bandwidth_issue = `bandwidth_mbps < 0.90 × subscribed_mbps`** (bandwidth floor ratio = **0.90**)
- **latency_issue = `latency_ms > 100`**
- **stability_issue (jitter) = `jitter_ms > 30`**
- **RESOLVED** (via auto-troubleshooting) iff ALL post metrics clear floors: `post_bandwidth_mbps >= 0.90×subscribed_mbps` AND `post_latency_ms <= 100` AND `post_jitter_ms <= 30`; otherwise **ESCALATED** to the root-cause team.
- Pre-troubleshooting issue flags (latency_issue/stability_issue/bandwidth_issue) are scored ONLY for tickets that actually ran diagnostics (RESOLVED/ESCALATED). They are **NOT scored** for outage-wait or account/auth-gated tickets (set them false there).

> Rationale for 0.90: the auto-resolve tickets whose RESOLVED status hinges on post_bw (CONFIGURATION_DRIFT/VOICE_PROFILE_STALE cases with post_latency<100 & post_jitter<30) have post_bw placed just above 0.90×subscribed (e.g., 272 vs 270, 93 vs 90). Latency/jitter absolute thresholds 100/30 are pinned by RESOLVED vs ESCALATED boundaries.

### GATING ORDER (apply in this order; first match wins; gates produce FAILED/PENDING with NO diagnostics)
1. **Account existence**: `GET /api/accounts/<account_id>` returns `{"error":"not_found"}` → FAILED, key_blocker/route = **INVALID_ACCOUNT**, team NONE.
2. **Account status**: `account.status != "Active"`:
   - `Suspended` + `suspension_reason` indicates overdue → FAILED, blocker **OVERDUE_SUSPENSION**, team **ACCOUNTS_PAYABLE**.
   - `Suspended` + fraud → FAILED, blocker **FRAUD_SUSPENSION**, team (billing/security).
3. **Auth gate**: `account.authentication.last_login_status == "FAILURE"` OR `account_recovery_status == "FAILURE"` → FAILED, blocker **AUTH_FAILED**, team **NONE** (auth failures route to NO team — confirmed: assigning TIER2_SUPPORT drops score).
4. **Active outage**: find outage where `outage.active==true` AND `outage.service_area == ticket.service_area` AND `ticket.service_type IN outage.service_types` → **PENDING_ACTION**, blocker **ACTIVE_OUTAGE**, team NONE, set `outage_id` to that outage_id, diagnostic_required=false. (Service-type match is required; otherwise the outage does not gate.)
5. **Else**: run diagnostics + auto-troubleshooting → RESOLVED (all post clear) or ESCALATED (any post fails). diagnostic_required=true.

### ROOT-CAUSE → TEAM / KEY_BLOCKER MAP
| diagnostics.root_causes | escalation_team (train_001) / route_team (train_004) | key_blocker (train_004) |
|---|---|---|
| FIBER_DROP_DAMAGE, SIGNAL_LOSS | FIELD_OPS | PHYSICAL_LINE_FAULT |
| BACKBONE_CAPACITY | NETWORK_ENGINEERING | NETWORK_CAPACITY |
| PROVISIONING_STALE | TIER2_SUPPORT | PROVISIONING_STALE |
| billing root cause | ACCOUNTS_PAYABLE | (OVERDUE/billing) |
| CONFIGURATION_DRIFT, GENERATED_NOISE, VOICE_PROFILE_STALE (non-mappable) | NONE | NONE |

Non-mappable root causes auto-RESOLVE when post metrics clear (confirmed: marking them ESCALATED drops score). They are the only candidates for RESOLVED. Mappable root causes with all-post-clear are unobserved in train; treat auto-troubleshoot as running first and RESOLVED only if all post clear regardless of root cause, else ESCALATED to the mapped team.

### OUTPUT FIELD DEFINITIONS
**train_001 ticket-batch schema** (`ticket_decisions[]` + `batch_summary`):
- `ticket_id`, `account_id` (preserve payload order)
- `final_resolution_status`: RESOLVED | PENDING_ACTION | ESCALATED | FAILED
- `diagnostic_needed`: true iff diagnostics ran (RESOLVED/ESCALATED); false for gates
- `latency_issue`, `stability_issue`, `bandwidth_issue`: PRE-troubleshooting flags per floors (false for gated tickets)
- `outage_id`: the gating outage_id for outage-wait; empty string `""` otherwise
- `escalation_team`: NONE | TIER2_SUPPORT | FIELD_OPS | NETWORK_ENGINEERING | ACCOUNTS_PAYABLE
- `resolution_route`: AUTO_TROUBLESHOOTING | OUTAGE_WAIT | ESCALATION | INELIGIBLE_ACCOUNT (suspended/invalid account) | AUTH_FAILED | INVALID_ACCOUNT
- `batch_summary`: counts of RESOLVED/PENDING_ACTION/ESCALATED/FAILED + `tickets_requiring_customer_wait` = count of OUTAGE_WAIT tickets only (account-gated FAILED excluded).

**train_004 queue-snapshot schema** (`ticket_decisions[]` + `queue_summary`):
- `ticket_id`, `final_resolution_status` (same enum, no account_id field)
- `route_team`: NONE | TIER2_SUPPORT | FIELD_OPS | NETWORK_ENGINEERING | ACCOUNTS_PAYABLE
- `key_blocker`: NONE | ACTIVE_OUTAGE | INVALID_ACCOUNT | AUTH_FAILED | OVERDUE_SUSPENSION | FRAUD_SUSPENSION | NETWORK_CAPACITY | PROVISIONING_STALE | PHYSICAL_LINE_FAULT
- `diagnostic_required`: true iff diagnostics ran
- `queue_summary`: counts FAILED/PENDING_ACTION/RESOLVED/ESCALATED + counts of TIER2_SUPPORT/FIELD_OPS/NETWORK_ENGINEERING/ACCOUNTS_PAYABLE (NONE not counted).

### TICKET SOP
For each ticket id in the payload (preserve order):
1. `GET /api/tickets/<id>` → account_id, service_area, service_type, subscribed_mbps.
2. `GET /api/accounts/<account_id>` → apply gates 1–3 (invalid/suspended/auth).
3. `GET /api/outages?service_area=<area>` → apply gate 4 (active + service_type match).
4. Else `GET /api/diagnostics/<id>` and `GET /api/troubleshooting/<id>` → compute pre flags per floors; decide RESOLVED vs ESCALATED from post floors; pick team/blocker from root_cause map.
5. Aggregate summary counts.

---

## FAMILY 2 — MOBILE TRIAGE / DATA RECOVERY

Two variants: `case_queue` (train_002: includes sim/billing/messaging/VPN issues, routes SELF_SERVICE/BILLING_RECOVERY/CARRIER_UPDATE/HUMAN_TRANSFER) and `mobile_data_worklist` (train_005: data-only, routes DATA_RECOVERY/CARRIER_UPDATE/DEVICE_SETTING_FIX/HUMAN_TRANSFER). Fetch case → customer/line/device/plan, then apply the decision tree (first match wins).

### DECISION TREE (checked in order)
| Condition | primary_action | secondary_action | route | carrier_update_required | charge / extra |
|---|---|---|---|---|---|
| `device.sim_status == "missing"` | RESEAT_SIM | NO_ACTION | SELF_SERVICE | false | — |
| `line.status == "Suspended"` + `suspension_reason == "OVERDUE_BILL"` | SEND_PAYMENT_REQUEST | RESUME_LINE_REBOOT | BILLING_RECOVERY | false | bill_id=overdue bill (match customer_id in /api/bills, status Overdue); charge=bill.amount_due_usd |
| `customer_location == "abroad"` AND `line.roaming_enabled == false` AND `device.phone_roaming_enabled == true` | ENABLE_LINE_ROAMING | NO_ACTION | CARRIER_UPDATE | **true** | — |
| `customer_location == "abroad"` AND `line.roaming_enabled == true` AND `device.phone_roaming_enabled == false` | TOGGLE_ROAMING | NO_ACTION | SELF_SERVICE / DEVICE_SETTING_FIX | false | — |
| `device.vpn_connected == true` AND speed_test poor/slow | DISCONNECT_VPN | NO_ACTION | SELF_SERVICE / DEVICE_SETTING_FIX | false | — |
| `device.data_saver_mode == true` AND slow | TOGGLE_DATA_SAVER | NO_ACTION | DEVICE_SETTING_FIX | false | — |
| `device.network_mode_preference` in {`2g`,`3g_only`,legacy} AND poor speed | SET_NETWORK_MODE | NO_ACTION | DEVICE_SETTING_FIX | **false** (confirmed: CARRIER_UPDATE drops score) | — |
| `device.mobile_data_enabled == false` | TOGGLE_MOBILE_DATA | NO_ACTION | DEVICE_SETTING_FIX | false | — |
| `line.data_used_gb > plan.data_limit_gb` (cap hit) AND customer accepts refuel | REFUEL_DATA | NO_ACTION | DATA_RECOVERY | false | data_refuel_gb=accepted amount; charge = gb × plan.data_refueling_price_per_gb |
| messaging gap (`can_send_mms == false`, missing perms) | GRANT_MESSAGING_PERMISSION | NO_ACTION | SELF_SERVICE | false | permission = exact missing perm(s): "sms" if sms=false, "storage" if storage=false, "sms_and_storage" if both false, "NONE" if both true |
| none of the above / unrecoverable | TRANSFER_HUMAN | NO_ACTION | HUMAN_TRANSFER | — | — |

### ROUTE MAPPING (across both variants)
- REFUEL_DATA → DATA_RECOVERY (data-cap recovery)
- ENABLE_LINE_ROAMING → **CARRIER_UPDATE** + carrier_update_required=true (line-side roaming provisioning is a carrier action — confirmed: DATA_RECOVERY drops score)
- All device-side toggles (TOGGLE_ROAMING, TOGGLE_DATA_SAVER, SET_NETWORK_MODE, TOGGLE_MOBILE_DATA, DISCONNECT_VPN, RESEAT_SIM) → SELF_SERVICE (train_002) / DEVICE_SETTING_FIX (train_005); carrier_update_required=false
- SEND_PAYMENT_REQUEST + RESUME_LINE_REBOOT → BILLING_RECOVERY
- TRANSFER_HUMAN → HUMAN_TRANSFER

### MOBILE AUDIT FORMULAS
- charge_amount_usd (billing case) = overdue bill's `amount_due_usd`.
- data_refuel charge = `data_refuel_gb × plan.data_refueling_price_per_gb`.
- queue/worklist summary: count cases per route; `total_estimated_customer_charge_usd` = sum of all case charges (refuel + billing).
- `permission` field lists ONLY the missing permission(s), not already-granted ones.

### MOBILE SOP
1. For each case_id: `GET /api/cases/<id>` → customer_id, line_id, device_id, customer_location, issue_type.
2. `GET /api/lines/<line_id>` (status, roaming_enabled, plan_id, data_used_gb, suspension_reason).
3. `GET /api/devices/<device_id>` (all device flags).
4. `GET /api/plans/<plan_id>` (limits/prices) for refuel cases; `GET /api/bills` for billing cases.
5. Apply decision tree; preserve ascending case_id order.

---

## FAMILY 3 — ENTERPRISE EXPORT-COMPLAINT RESPONSE PACKAGE

Build a structured response for an enterprise client export-failure incident.

### ENTERPRISE SOP
1. From the complaint email: client name, product, approximate incident reference (e.g., INC-7301).
2. `GET /api/enterprise/incidents` → match incident_id (and product/client). Captures: enterprise_account_id, severity, status, engineering_owner, account_owner, summary.
3. `GET /api/enterprise/accounts` → confirm enterprise_account_id (match name); captures finance_owner.
4. `GET /api/enterprise/export-runs?incident_id=<id>` → FAILED runs define the failure window; the following SUCCEEDED run ends the incident.
5. `GET /api/enterprise/messages?query=<term>` → search for root-cause explanation (query the client name, failure code, "alert", "credential", "credit", "export", author names). The message `channel` reveals the contributing alert issue.
6. `GET /api/enterprise/sla/<enterprise_account_id>` → SLA credit percent.

### FIELD DERIVATION RULES
| Field | Rule |
|---|---|
| incident_id | from complaint reference + incidents match |
| enterprise_account_id | incident.enterprise_account_id (confirm via accounts name) |
| root_cause_category | concise category from export-run `failure_code` cross-referenced with message evidence (e.g., failure_code STALE_CREDENTIAL + message "scheduler pod references old secret") |
| contributing_alert_issue | `ARCHIVED_ALERT_ROUTE` if a related message's `channel` contains "archive"; else `NONE`; `UNKNOWN` if no message found |
| failure_window.start_date | first FAILED run_date |
| failure_window.end_date | last FAILED run_date (the subsequent SUCCEEDED run is excluded) |
| failure_window.failed_days | count of FAILED runs (consecutive) |
| backfill_days | number of failed days backfilled (typically = failed_days) |
| sla_credit_percent | `/api/enterprise/sla/<ent_id>.monthly_export_credit_percent` for monthly_export product (also confirmed by account-escalations message) |
| severity | incident.severity |
| engineering_owner | incident.engineering_owner |
| account_owner | incident.account_owner |
| channel_name | per `naming_style`: lowercase-hyphen channel name derived from client |
| evidence_folder | per `naming_style`: "<client-slug>-<date>" investigation folder |
| report_title | per `naming_style`: "<Client> Export Failure Report" |
| share_permissions | users from `response_requirements.permission_users_to_include`, in listed order; assign permission (view/edit/upload_only) per role — finance_owner typically edit/view; non-role users view/upload_only |
| response_status | reflects review need before sending: NEEDS_ENGINEERING_REVIEW when an engineering root cause must be confirmed; NEEDS_FINANCE_REVIEW when SLA credit needs finance signoff; READY_TO_SEND when complete; UNDER_INVESTIGATION when mirroring an unresolved incident |

### ENTERPRISE AUDIT FORMULAS
- SLA credit trigger: "N consecutive failed export runs" (from sla.credit_trigger); credit % = sla.monthly_export_credit_percent.
- backfill scope = the failed-run days.

### ENTERPRISE NAMING CONVENTIONS (from `response_requirements.naming_style`)
"lowercase hyphen channel; client-date investigation folder; client export failure report title" — derive channel_name, evidence_folder, report_title mechanically from the client (enterprise account name) and the incident date. The exact strings must match the grader's expected format; derive the client token from the enterprise account name and use the incident received/failure-start date for the folder. Include only the users listed in `permission_users_to_include`, ordered as listed.

---

## PITFALLS
- **Bandwidth ratio**: use **0.90**, not 0.70/0.80/0.85. A naive 1.0 score on ticket batches is consistent with any ratio in (0.697, 0.907] because the train tickets' bandwidth values fall outside the discriminating range — do not infer the ratio from a single 1.0; use the calibration of RESOLVED tickets' post_bw.
- **Outage gate requires service_type match**: only an active outage whose `service_types` includes the ticket's `service_type` gates the ticket.
- **Auth gate beats outage**: account/auth FAILED before outage PENDING before diagnostics.
- **Auth-failed route**: team = NONE ( assigning TIER2_SUPPORT is wrong).
- **VOICE_PROFILE_STALE / CONFIGURATION_DRIFT / GENERATED_NOISE are non-mappable** → RESOLVED when post clear (NOT escalated).
- **Pre-troubleshooting flags unscored for gated tickets** (outage/account/auth) — set false, don't compute.
- **tickets_requiring_customer_wait** counts OUTAGE_WAIT only, not FAILED account-gated tickets.
- **ENABLE_LINE_ROAMING** (line roaming off, abroad) → CARRIER_UPDATE + carrier_update_required=true (NOT DATA_RECOVERY).
- **SET_NETWORK_MODE** (3g_only/legacy) → DEVICE_SETTING_FIX + carrier_update_required=false (NOT CARRIER_UPDATE).
- **permission field**: only the MISSING permission(s), not all permissions.
- **Ignore generated/distractor records** (TCK-8xxx, INC-9xxx, PLAN-G*, "Generated ...") — never task targets.
- **Enterprise naming strings** are the hardest to pin; derive mechanically from naming_style and the client/account name. If a field is convention-based and ambiguous, prefer the literal template interpretation (client slug lowercase-hyphen; client-date folder; "<Client> Export Failure Report" title).
- Always substitute the provided base URL for any 127.0.0.1 address in prompts; records are source of truth.

