Store associates share accounts and iPads, so the existing
findMyTickets (keyed by reporter email) misses tickets a coworker
opened for the same store earlier in the shift. This adds a
store-keyed lookup so a store caller can see everything open at
their location.
- New service function searchOpenTicketsByStoreNumber(): normalizes
to 5 digits, validates against the stores cache (warn-only on miss),
runs the same Grok-enriched search as the reporter path.
- New route GET /wxccai/open-tickets-by-store?storeNumber=782 —
accepts padded or unpadded input, degrade-gracefully 200 on
runtime failures to match the sibling reporter route.
- JQL note: the CMDB "Store Number" field only matches on the
object *label* (the 5-digit padded string). Neither the objectId,
ASSET-<id> objectKey, workspace-qualified id, nor raw digits
match. Probed all variants before landing.
- WxCC AI Agent docs updated: findMyStoreTickets promoted to the
primary lookup for store callers; findMyTickets reframed as the
corporate-caller path. System prompt Step 1 rewritten to route
store vs corporate callers to the right tool.
Verified end-to-end against store 782 — returns 5 open tickets
including two from other reporters (SS-20380 wireless phone,
SS-11943 Zipline training) that email-based lookup misses.
Co-authored-by: Cursor <cursoragent@cursor.com>
Adds a shareable reference doc covering everything the Contact Center
AI agent needs to open, look up, comment on, and close SS tickets via
this service:
1. Inventory of the 9 AI-facing endpoints (read / create / update / close)
2. The 14 supported SS subTypes (copy-pasteable)
3. A drop-in system prompt (call-flow discipline, safety rules)
4. Tool definitions in OpenAI/JSON-schema function-calling format
5. Two worked call examples
6. Endpoints deliberately kept out of the AI's tool loop
Companion to the code shipped in this branch — no code changes.
Co-authored-by: Cursor <cursoragent@cursor.com>
Extends the app so a Contact Center AI agent can close SS tickets
end-to-end (customer-confirmed fix, cancellation, duplicate) without a
human touch — the natural next step after ticket creation landed.
The SS Resolved (161) workflow validator requires four fields on the
ticket at execution time (components + 3 CMDB customfields for Business
Service / System / Cause). The transition screen itself only exposes
resolution, so the closer has to PUT these into the ticket first and
*then* fire the transition. This change wires up the whole flow with
sensible per-subType defaults, so the AI can close a typical ticket
with just { subType, comment }.
Design (see Forgejo #10 for the full write-up):
- Generalized services/jira/storesCache into a reusable factory
createAssetsObjectCache. Same in-memory Map + on-disk JSON snapshot +
startAt/maxResults pagination + personal-PAT auth. Store cache becomes
one instance; three new caches join it for the CMDB validator fields
(businessServices=100, systems=103, causes=107 — all in schema 68, so
the same permission workaround from Forgejo #8 keeps working).
- Boot-time init warms all four caches in parallel; a new
services/jira/caches.js barrel exposes them for lifecycle (init) and
by-name lookup (admin routes). Full sync takes ~5s in the tenant
(2661 stores dominate — the three CMDB caches together hold ~653
entries and finish in ~1s).
- Admin surface rebuilt around a plural /admin/caches path:
GET /admin/caches — status of all four
GET /admin/caches/:name/status — one cache
POST /admin/caches/:name/refresh — force resync of one
POST /admin/caches/refreshAll — parallel refresh
Old /admin/storesCache/{status,refresh} are kept as aliases.
- Config renamed: config.storesCache -> config.caches (dir instead of
path; filenames auto-derived per cache). Env vars renamed to
CACHES_* (dir/refresh-hours/stale-hours/page-size/max-pages).
.env.example updated.
- ssCloseDefaults.js maps every supported SS subType to a
{component, businessService, system, cause} tuple. Values verified
against the live caches so they resolve at runtime. Falls back to a
safe __default__ tuple (Help Desk / Store Technology / "I can't find
my option - Misc" / Unknown) for un-mapped subTypes — those catch-all
values are the tenant's designed "I don't know" escape valves.
- issues.js closeTicket completely reworked:
* setSSValidatorFields helper: reads current ticket state, resolves
each of the four fields via caller > existing-on-ticket > default,
PUTs them using the Cloud CMDB shape [{id: "<ws>:<objectId>"}]
(same fix as Forgejo #8 for Store Number).
* Only writes fields that need writing; preserves human triage by
default (preserveExistingClassification=true).
* Fixes Forgejo #9: only sends the resolution field when the
chosen transition targets statusCategory=done. Non-done
transitions (e.g. "Waiting for customer") no longer 400 on
"Field 'resolution' cannot be set."
- closeHelpers.js adds three CC-agent intent wrappers:
confirmFixed(key, {subType, ...}) → resolution=Done
customerCancelled(key, {subType, ...}) → resolution=Won't Do
markDuplicate(key, {primaryKey, ...}) → resolution=Duplicate +
formal Duplicate issueLink
to the primary
Each posts a standardized internal audit-trail comment ("Closed via
WxCC AI agent: …") that documents the automated action for the
humans who inherit the ticket.
- New routes: POST /ticket/:key/{confirmFixed,customerCancelled,
duplicate} plus an expanded body on the existing /close route.
Verified end-to-end: POST /ticket/SS-20948/confirmFixed with just
subType=Report a Technology issue transitioned the ticket to Resolved
with all four validator fields populated from defaults (Help Desk /
Store Technology / I can't find my option - Misc / Unknown), plus
resolution=Done. Direct Jira REST GET confirms every field persisted.
Closes Forgejo #9. Refs Forgejo #10.
Co-authored-by: Cursor <cursoragent@cursor.com>
Two bugs found during end-to-end smoke test on the stores cache
workaround (Forgejo #8):
1. Wrong pagination params on POST /object/aql. The endpoint uses
startAt/maxResults as URL query params, not page/resultPerPage in the
body. Passing the wrong param names caused the API to silently fall
back to the default maxResults=25, so the first sync only cached 25
stores. After fix: 2,661 stores paginated across 6 pages in 5.3s.
2. Wrong request-field shape for CMDB-object custom fields on Jira
Cloud. resolveStoreAssetReference was returning
[{ objectId: "75974" }]
which is the legacy Data Center / Server shape. Cloud requires
[{ id: "<workspaceId>:<objectId>" }]
The old shape is silently accepted (HTTP 204) by REST and by
POST /rest/servicedeskapi/request, but the field is never actually
persisted \u2014 verified via direct REST GET showing customfield_10261:[].
After fix: SS-20948 shows the store correctly populated.
- assetsSyncClient.js: use URLSearchParams to pass
startAt/maxResults/includeAttributes; drop the old page/resultPerPage
opts; jsdoc updated with pointer to Atlassian Assets API v1 docs.
- storesCache.js: pagination loop now advances by startAt +=
values.length and trusts isLast (Atlassian caps `total` at 1000 as a
hint on this endpoint, so we can't rely on it).
- assets.js: new buildStoreFieldRef(objectId) helper produces the
correct Cloud shape from config.jira.assetsWorkspaceId; all three
resolution paths (cache / live PAT / service-account fallback) route
through it. Falls back to legacy shape with a loud error log if
workspaceId is unset (misconfig).
End-to-end verified: SS-20948 was created for store 00782 (objectId
75974) via cache-only lookup and shows the field populated on both the
JSM request echo and a direct REST GET.
Co-authored-by: Cursor <cursoragent@cursor.com>
The bot runs on a Linux host where macOS Keychain isn't available, so .env
is the default supported storage for the personal PAT. Both paths land in
the same process.env slot, but the previous README framing implied Keychain
was mandatory.
- .env.example: promote ASSETS_SYNC_TOKEN from a comment to a real
REPLACE_ME field; note chmod 600 and rotation guidance
- README: split the setup section into "Setup A - production/Linux (.env)"
and "Setup B - local dev on macOS (Keychain)"; clarify that the wrapper
is a no-op if ASSETS_SYNC_TOKEN is already exported
- bin/load-assets-sync-secret.sh: soften the header comment to match
Co-authored-by: Cursor <cursoragent@cursor.com>
The service account is silently filtered out of Object Type 109 (Store
Address / Hierarchy) despite having schema-level read on schema 68, so
every AQL against the store type returns total=0. Until that permission
is granted, resolve store numbers from a local cache populated by a
personal PAT (different auth path, different account, has the role).
- new: src/services/jira/assetsSyncClient.js — Basic-auth axios against
api.atlassian.com/jsm/assets/workspace/{ws}/v1, credentials sourced
from ASSETS_SYNC_EMAIL / ASSETS_SYNC_TOKEN (loaded from Keychain by
bin/load-assets-sync-secret.sh so the PAT never touches .env)
- new: src/services/jira/storesCache.js — in-memory Map + on-disk JSON
at data/stores.json (gitignored), atomic write, paginated full sync
via AQL (objectTypeId=N), boot-time load + background refresh if
stale, periodic setInterval every STORES_CACHE_REFRESH_HOURS
- new: bin/load-assets-sync-secret.sh — Keychain -> env var wrapper
(security find-generic-password -s jira-assets-sync -a <email>)
- change: resolveStoreAssetReference now tries cache -> live PAT -> the
existing service-account AQL, in that order; the fallback path is
preserved so this cleanly deactivates once the permission on #1 is
fixed. Error message names all three routes and points at the refresh
endpoint.
- new admin routes: GET /api/wxccai/admin/storesCache/status,
POST /api/wxccai/admin/storesCache/refresh
- app.js kicks off storesCache.init() after listen()
- config: STORES_CACHE_ENABLED / _PATH / _REFRESH_HOURS /
_STALE_AFTER_HOURS / _PAGE_SIZE / _MAX_PAGES, plus intFromEnv /
boolFromEnv helpers
- .gitignore adds data/; .env.example documents the new vars; README
adds an "Admin" endpoints section and a "Stores cache" setup guide
Co-authored-by: Cursor <cursoragent@cursor.com>
Pure move + one-way rewire, no logic changes. The old 1467-line
monolithic services/jiraService.js becomes a thin barrel that re-exports
the same public surface so both current consumers keep working unchanged:
- src/routes/wxccRoutes.js: `import * as jiraService`
- src/services/healthService.js: `import { jiraClient }`
New module layout (deps flow one-way, no cycles):
client.js — jiraClient, downloadClient, plainTextToAdf (foundational)
issues.js — fetch/search/status/update/transitions/close
comments.js — fetchPublicComments, addComment, postWebexSummaryComment
attachments.js — attachFileToJira, attachReadableTranscript
assets.js — AQL, resolveStoreAssetReference, probeAssetsForStore
jsmRequests.js — REQUEST_TYPE_MAP, createSSRequest, subtype helpers
Verified: barrel re-exports every original name (23 named + default with
same 17 members), REQUEST_TYPE_MAP still has 14 entries, jiraClient still
instantiates against the configured baseURL, and both consumers import
without errors under the real ES module loader.
New code should import from services/jira/* directly; the barrel is only
for backward compatibility with existing callers.
Co-authored-by: Cursor <cursoragent@cursor.com>
- New SUBTYPES_REQUIRING_STORE_NUMBER set (seeded with every current
subType, all of which mark Store Number required in JSM). Adding a
future subType that does NOT require Store Number is a one-line omit.
- createSSRequest now trims storeNumber (rejects whitespace-only) and
throws a 400 with a clear message ("storeNumber is required for
subType ...") before making any Jira API call.
- Validation errors now carry err.status = 400 so future non-route
callers can distinguish client errors from Jira failures.
Co-authored-by: Cursor <cursoragent@cursor.com>
- wxccRoutes.js no longer mutates the default axios instance, which was
causing every bare axios call in the codebase (S3 downloads, Assets
diagnostics) to inherit retries as a side-effect of route file load order.
- jiraService.js now defines a private downloadClient (own timeout, own
retry policy) used by attachFileToJira and fetchAndConvertTranscript for
fetching pre-signed S3 URLs.
- Assets AQL/GET remain bare axios calls; they're one-shot diagnostics
and should not auto-retry.
Co-authored-by: Cursor <cursoragent@cursor.com>