feat(lookup): add store-based ticket search for store callers
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>
This commit is contained in:
parent
e9a32e31c1
commit
c20f387081
5 changed files with 176 additions and 25 deletions
|
|
@ -36,7 +36,8 @@ Base path: `/api/wxccai`.
|
||||||
### Read
|
### Read
|
||||||
|
|
||||||
- `GET /getticket?jiraKey=CS-1234` — Grok-summarized single ticket.
|
- `GET /getticket?jiraKey=CS-1234` — Grok-summarized single ticket.
|
||||||
- `GET /open-tickets-by-reporter?email=user@example.com` — Grok-summarized list of open tickets a person reported.
|
- `GET /open-tickets-by-reporter?email=user@example.com` — Grok-summarized list of open tickets a person reported. Best for corporate callers (unique per-user emails).
|
||||||
|
- `GET /open-tickets-by-store?storeNumber=00782` — Grok-summarized list of open SS tickets filed for a given store, regardless of reporter. Best for store callers (shared accounts). Store number can be unpadded; service pads to 5 digits.
|
||||||
- `GET /ticket/:key/status` — raw status fields (no Grok).
|
- `GET /ticket/:key/status` — raw status fields (no Grok).
|
||||||
- `GET /ticket/:key/transitions` — available workflow transitions.
|
- `GET /ticket/:key/transitions` — available workflow transitions.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,31 +22,32 @@ This doc covers the caller-facing surface only — admin/debug endpoints
|
||||||
### Read / lookup (safe, side-effect-free)
|
### Read / lookup (safe, side-effect-free)
|
||||||
|
|
||||||
| # | Tool | Endpoint | What it does |
|
| # | Tool | Endpoint | What it does |
|
||||||
| - | ------------------- | --------------------------------------------------- | ------------ |
|
| - | --------------------- | --------------------------------------------------- | ------------ |
|
||||||
| 1 | `lookupTicket` | `GET /getticket?jiraKey=SS-12345` | Returns an **AI-summarized** view (Grok-generated) of a specific ticket: status, assignee, and a natural-language recap of the description + last 5 comments. Best for "tell me about my ticket" moments. |
|
| 1 | `lookupTicket` | `GET /getticket?jiraKey=SS-12345` | Returns an **AI-summarized** view (Grok-generated) of a specific ticket: status, assignee, and a natural-language recap of the description + last 5 comments. Best for "tell me about my ticket" moments. |
|
||||||
| 2 | `findMyTickets` | `GET /open-tickets-by-reporter?email=user@ae.com` | Returns the caller's currently open tickets across CS/SS/SUPPORT with short summaries. Best for the opening beats of a call ("Do I already have a ticket for this?"). |
|
| 2 | `findMyStoreTickets` | `GET /open-tickets-by-store?storeNumber=00782` | Returns every currently open SS ticket filed **for a given store**, regardless of who reported it. This is the right lookup for store callers because associates typically share accounts / iPads — a ticket opened by a coworker would not show up in `findMyTickets` (which is keyed by reporter email). |
|
||||||
| 3 | `getTicketStatus` | `GET /ticket/:key/status` | Raw status fields (no Grok). Faster/cheaper than `lookupTicket`. Use when the AI just needs the current status, resolution, assignee — not a narrative. |
|
| 3 | `findMyTickets` | `GET /open-tickets-by-reporter?email=user@ae.com` | Returns the caller's currently open tickets across CS/SS/SUPPORT with short summaries. Best for **corporate** callers (developers, ops, HQ staff) whose email uniquely identifies them. For **store** callers, prefer `findMyStoreTickets`. |
|
||||||
|
| 4 | `getTicketStatus` | `GET /ticket/:key/status` | Raw status fields (no Grok). Faster/cheaper than `lookupTicket`. Use when the AI just needs the current status, resolution, assignee — not a narrative. |
|
||||||
|
|
||||||
### Create (opens a new ticket)
|
### Create (opens a new ticket)
|
||||||
|
|
||||||
| # | Tool | Endpoint | What it does |
|
| # | Tool | Endpoint | What it does |
|
||||||
| - | ------------------- | ---------------------------- | ------------ |
|
| - | ------------------- | ---------------------------- | ------------ |
|
||||||
| 4 | `createStoreTicket` | `POST /createSSRequest` | Files a new Store Support (SS) ticket with the right subType, links it to a store via Assets, and returns the new key. Uses the 14 supported subTypes below. |
|
| 5 | `createStoreTicket` | `POST /createSSRequest` | Files a new Store Support (SS) ticket with the right subType, links it to a store via Assets, and returns the new key. Uses the 14 supported subTypes below. |
|
||||||
|
|
||||||
### Update (mid-conversation)
|
### Update (mid-conversation)
|
||||||
|
|
||||||
| # | Tool | Endpoint | What it does |
|
| # | Tool | Endpoint | What it does |
|
||||||
| - | --------------------- | ---------------------------------------- | ------------ |
|
| - | --------------------- | ---------------------------------------- | ------------ |
|
||||||
| 5 | `addTicketComment` | `POST /ticket/:key/comment` | Appends a comment (public or `internal:true`). Internal is the safe default for AI-authored notes. |
|
| 6 | `addTicketComment` | `POST /ticket/:key/comment` | Appends a comment (public or `internal:true`). Internal is the safe default for AI-authored notes. |
|
||||||
| 6 | `updateTicketFields` | `PATCH /ticket/:key` | Update summary / description / priority / labels / assignee / custom fields. Rarely needed by the AI. |
|
| 7 | `updateTicketFields` | `PATCH /ticket/:key` | Update summary / description / priority / labels / assignee / custom fields. Rarely needed by the AI. |
|
||||||
|
|
||||||
### Close (three intent-specific + one full-control)
|
### Close (three intent-specific + one full-control)
|
||||||
|
|
||||||
| # | Tool | Endpoint | What it does |
|
| # | Tool | Endpoint | What it does |
|
||||||
| - | ----------------------- | --------------------------------------------- | ------------ |
|
| - | ------------------------ | --------------------------------------------- | ------------ |
|
||||||
| 7 | `confirmTicketFixed` | `POST /ticket/:key/confirmFixed` | Caller says the issue is resolved → close with `resolution=Done` + audit comment. |
|
| 8 | `confirmTicketFixed` | `POST /ticket/:key/confirmFixed` | Caller says the issue is resolved → close with `resolution=Done` + audit comment. |
|
||||||
| 8 | `cancelTicket` | `POST /ticket/:key/customerCancelled` | Caller wants to abandon the request → close with `resolution=Won't Do`. |
|
| 9 | `cancelTicket` | `POST /ticket/:key/customerCancelled` | Caller wants to abandon the request → close with `resolution=Won't Do`. |
|
||||||
| 9 | `markTicketDuplicate` | `POST /ticket/:key/duplicate` | Caller already has another ticket for the same issue → creates a formal `Duplicate` issueLink to the primary + closes with `resolution=Duplicate`. |
|
| 10 | `markTicketDuplicate` | `POST /ticket/:key/duplicate` | Caller already has another ticket for the same issue → creates a formal `Duplicate` issueLink to the primary + closes with `resolution=Duplicate`. |
|
||||||
| — | `closeTicket` | `POST /ticket/:key/close` | Full-control close. Do NOT expose to the AI — use the intent-specific tools instead. |
|
| — | `closeTicket` | `POST /ticket/:key/close` | Full-control close. Do NOT expose to the AI — use the intent-specific tools instead. |
|
||||||
|
|
||||||
All three close tools auto-populate the four workflow-validator fields
|
All three close tools auto-populate the four workflow-validator fields
|
||||||
|
|
@ -97,7 +98,8 @@ CORE PRINCIPLES
|
||||||
1. Never invent ticket keys, store numbers, or subTypes. If the caller
|
1. Never invent ticket keys, store numbers, or subTypes. If the caller
|
||||||
doesn't volunteer one, ask.
|
doesn't volunteer one, ask.
|
||||||
2. Store numbers are always 5 digits (pad with leading zeros: "305" →
|
2. Store numbers are always 5 digits (pad with leading zeros: "305" →
|
||||||
"00305").
|
"00305"). The service pads unpadded input for you but always spell
|
||||||
|
the padded form back to the caller ("store zero-zero-three-zero-five").
|
||||||
3. Never call a close tool (confirmTicketFixed, cancelTicket,
|
3. Never call a close tool (confirmTicketFixed, cancelTicket,
|
||||||
markTicketDuplicate) based on inference alone. The caller must
|
markTicketDuplicate) based on inference alone. The caller must
|
||||||
explicitly state the intent in the current turn.
|
explicitly state the intent in the current turn.
|
||||||
|
|
@ -105,14 +107,25 @@ CORE PRINCIPLES
|
||||||
internal=true) and escalate to a human. Never close a ticket you're
|
internal=true) and escalate to a human. Never close a ticket you're
|
||||||
unsure about — an open ticket costs the business less than a wrongly
|
unsure about — an open ticket costs the business less than a wrongly
|
||||||
closed one.
|
closed one.
|
||||||
5. For every ticket action, the caller's email is required to identify
|
5. Identify callers correctly: STORE callers (associates, managers, from
|
||||||
them as the reporter.
|
a store location) → use findMyStoreTickets keyed by store number,
|
||||||
|
because store accounts are frequently shared. CORPORATE callers
|
||||||
|
(developers, ops, HQ staff) → use findMyTickets keyed by their email,
|
||||||
|
because email uniquely identifies them.
|
||||||
|
|
||||||
TYPICAL CALL FLOW
|
TYPICAL CALL FLOW
|
||||||
|
|
||||||
Step 1: Identify the caller.
|
Step 1: Identify the caller and find existing tickets.
|
||||||
- Ask for or confirm their email + store number.
|
- Ask "Are you calling from a store, or are you calling from
|
||||||
- Call findMyTickets with their email to see what's already open.
|
corporate?" if you can't tell from context.
|
||||||
|
- If STORE: get the store number, then call findMyStoreTickets with
|
||||||
|
the padded store number. This returns EVERY open SS ticket for that
|
||||||
|
store — including ones opened by their coworkers.
|
||||||
|
- If CORPORATE: get their email, then call findMyTickets with the
|
||||||
|
email. This returns tickets they personally reported across CS/SS/
|
||||||
|
SUPPORT.
|
||||||
|
- You may call BOTH for a store caller who also has a personal work
|
||||||
|
email — the union catches everything.
|
||||||
|
|
||||||
Step 2: Decide what they need.
|
Step 2: Decide what they need.
|
||||||
- If they reference a specific existing ticket → lookupTicket for a
|
- If they reference a specific existing ticket → lookupTicket for a
|
||||||
|
|
@ -160,9 +173,21 @@ standard and portable across most LLM tool-calling frameworks.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
[
|
[
|
||||||
|
{
|
||||||
|
"name": "findMyStoreTickets",
|
||||||
|
"description": "Return every currently open SS ticket for a given store, regardless of who reported it. This is the RIGHT tool for store callers (associates, managers) because store accounts and iPads are typically shared — a ticket opened by their coworker earlier in the shift will NOT show up in findMyTickets (which is keyed by reporter email). Call this near the start of every store call once you have the store number.",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"storeNumber": { "type": "string", "description": "Store number, numeric. May be unpadded ('782') or padded ('00782') — the service normalizes to 5 digits." }
|
||||||
|
},
|
||||||
|
"required": ["storeNumber"]
|
||||||
|
},
|
||||||
|
"http": { "method": "GET", "path": "/api/wxccai/open-tickets-by-store", "queryParams": ["storeNumber"] }
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "findMyTickets",
|
"name": "findMyTickets",
|
||||||
"description": "Look up the caller's currently open tickets across CS/SS/SUPPORT projects. Call this near the start of every conversation once you have the caller's email, so you know what's already in flight before opening a new ticket. Returns a list of {key, summary, status, updated}.",
|
"description": "Look up the caller's currently open tickets across CS/SS/SUPPORT projects, keyed by their email. Best for CORPORATE callers (developers, ops, HQ staff) whose email uniquely identifies them. For STORE callers, prefer findMyStoreTickets — store accounts are typically shared and reporter-email search will miss tickets a coworker opened.",
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
@ -295,11 +320,16 @@ standard and portable across most LLM tool-calling frameworks.
|
||||||
|
|
||||||
## 5. Worked call examples
|
## 5. Worked call examples
|
||||||
|
|
||||||
### Example 1 — new issue, straightforward create
|
### Example 1 — store caller, checks store history first, then creates
|
||||||
|
|
||||||
```
|
```
|
||||||
Caller: "Hi, register 3 at store 782 froze during checkout."
|
Caller: "Hi, register 3 at store 782 froze during checkout."
|
||||||
AI: [findMyTickets email="jane@ae.com"] → no open tickets
|
AI: [findMyStoreTickets storeNumber="782"]
|
||||||
|
→ returns [SS-20380 (wireless phone), SS-11943 (Zipline training)]
|
||||||
|
(Both open, but neither about the register. Nothing to reuse.)
|
||||||
|
AI: "I don't see any existing tickets for this issue at your store.
|
||||||
|
Let me open a new one. Can I get your email for the ticket?"
|
||||||
|
Caller: "jane@ae.com"
|
||||||
AI: [createStoreTicket
|
AI: [createStoreTicket
|
||||||
subType="Register Not functioning properly",
|
subType="Register Not functioning properly",
|
||||||
onBehalfOf="jane@ae.com",
|
onBehalfOf="jane@ae.com",
|
||||||
|
|
@ -313,6 +343,10 @@ AI: "I've opened ticket SS-20955 for you. The store technology team
|
||||||
will pick it up shortly."
|
will pick it up shortly."
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Note how `findMyStoreTickets` surfaces tickets opened by *other* people at
|
||||||
|
the store (SS-20380, SS-11943). If Jane had called `findMyTickets` with
|
||||||
|
just her email, she'd have missed both.
|
||||||
|
|
||||||
### Example 2 — caller wants to close an existing ticket
|
### Example 2 — caller wants to close an existing ticket
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,35 @@ router.get('/wxccai/open-tickets-by-reporter', async (req, res) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ========================
|
||||||
|
// Open Tickets by Store
|
||||||
|
// ========================
|
||||||
|
// Store associates share accounts / iPads, so reporter-email search misses
|
||||||
|
// tickets opened by a coworker. This endpoint finds every open SS ticket
|
||||||
|
// filed for the given store, regardless of who reported it.
|
||||||
|
router.get('/wxccai/open-tickets-by-store', async (req, res) => {
|
||||||
|
const raw = req.query.storeNumber?.trim();
|
||||||
|
|
||||||
|
if (!raw || !/^\d+$/.test(raw)) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: "Invalid or missing storeNumber",
|
||||||
|
message: "Please provide a numeric storeNumber (will be padded to 5 digits)"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const issues = await jiraService.searchOpenTicketsByStoreNumber(raw);
|
||||||
|
const ticketArray = await grokService.generateOpenTicketsSummary(issues);
|
||||||
|
|
||||||
|
res.status(200).json(ticketArray);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Open tickets by store failed for ${raw}:`, error);
|
||||||
|
// Same degrade-gracefully pattern as the reporter route.
|
||||||
|
res.status(200).json([]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// ========================
|
// ========================
|
||||||
// Store Support (SS) Ticket Creation
|
// Store Support (SS) Ticket Creation
|
||||||
// ========================
|
// ========================
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import logger from '../../utilities/logger.js';
|
||||||
import config from '../../config/index.js';
|
import config from '../../config/index.js';
|
||||||
import { jiraClient, plainTextToAdf } from './client.js';
|
import { jiraClient, plainTextToAdf } from './client.js';
|
||||||
import { fetchPublicComments } from './comments.js';
|
import { fetchPublicComments } from './comments.js';
|
||||||
import { businessServicesCache, systemsCache, causesCache } from './caches.js';
|
import { businessServicesCache, systemsCache, causesCache, storesCache } from './caches.js';
|
||||||
import { getDefaultsForSubType } from '../../config/ssCloseDefaults.js';
|
import { getDefaultsForSubType } from '../../config/ssCloseDefaults.js';
|
||||||
|
|
||||||
const KEY_RE = /^[A-Z]+-\d+$/;
|
const KEY_RE = /^[A-Z]+-\d+$/;
|
||||||
|
|
@ -126,6 +126,90 @@ export async function searchOpenTicketsByReporterEmail(email) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search open SS tickets for a given store. Useful for store-based callers
|
||||||
|
* (associates who don't have their own reporter email — the store shares
|
||||||
|
* accounts). Enriches each result with plain-text description + last 6
|
||||||
|
* public comments, same shape as searchOpenTicketsByReporterEmail so the
|
||||||
|
* downstream Grok summarizer works on either result set.
|
||||||
|
*
|
||||||
|
* We probed the tenant's JQL behavior for the CMDB "Store Number" field:
|
||||||
|
* only the padded human-readable form works (`"Store Number" = "00782"`).
|
||||||
|
* Neither the Assets objectId, the ASSET-<id> objectKey, the workspace-
|
||||||
|
* qualified id, nor the raw digit form matches — the field resolves
|
||||||
|
* against the object's *label*. So we normalize the caller's storeNumber
|
||||||
|
* to 5 digits and search by that string.
|
||||||
|
*
|
||||||
|
* The storesCache is consulted first to fail-fast with a clear error when
|
||||||
|
* the store doesn't exist (better UX than "no tickets found for a
|
||||||
|
* nonexistent store").
|
||||||
|
*/
|
||||||
|
export async function searchOpenTicketsByStoreNumber(rawStoreNumber) {
|
||||||
|
if (rawStoreNumber === null || rawStoreNumber === undefined || String(rawStoreNumber).trim() === '') {
|
||||||
|
throw new Error('storeNumber is required');
|
||||||
|
}
|
||||||
|
const s = String(rawStoreNumber).trim();
|
||||||
|
if (!/^\d+$/.test(s)) {
|
||||||
|
throw new Error(`Invalid storeNumber "${rawStoreNumber}"; must be numeric`);
|
||||||
|
}
|
||||||
|
const padded = s.padStart(5, '0');
|
||||||
|
|
||||||
|
// Validate against the cache — nicer error than an empty result set for
|
||||||
|
// a typo'd store number. Miss doesn't necessarily mean invalid (the
|
||||||
|
// cache may not have been synced yet), so we only warn, not error.
|
||||||
|
const cachedStore = storesCache.get(padded);
|
||||||
|
if (!cachedStore) {
|
||||||
|
logger.warn('searchOpenTicketsByStoreNumber: store not in cache; proceeding anyway', {
|
||||||
|
storeNumber: padded,
|
||||||
|
cacheStatus: storesCache.status(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const jql = `project = SS
|
||||||
|
AND "Store Number" = "${padded}"
|
||||||
|
AND statusCategory != Done
|
||||||
|
ORDER BY updated DESC`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await jiraClient.post('/rest/api/3/search/jql', {
|
||||||
|
jql: jql,
|
||||||
|
maxResults: 8,
|
||||||
|
fields: ["key", "summary", "status", "updated", "reporter", "description"],
|
||||||
|
expand: "comments"
|
||||||
|
}, {
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
|
||||||
|
const issues = response.data.issues || [];
|
||||||
|
|
||||||
|
const enrichedIssues = await Promise.all(
|
||||||
|
issues.map(async (issue) => {
|
||||||
|
const key = issue.key;
|
||||||
|
try {
|
||||||
|
const [plainDesc, publicComments] = await Promise.all([
|
||||||
|
fetchPlainDescription(key).catch(() => "No description available."),
|
||||||
|
fetchPublicComments(key).catch(() => [])
|
||||||
|
]);
|
||||||
|
|
||||||
|
issue.enrichedNotes = {
|
||||||
|
description: plainDesc,
|
||||||
|
publicComments: publicComments.slice(-6)
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn(`Failed to enrich notes for ${key}:`, err.message);
|
||||||
|
issue.enrichedNotes = { description: "Notes unavailable.", publicComments: [] };
|
||||||
|
}
|
||||||
|
return issue;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return enrichedIssues;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Jira store search failed:', error.response?.data || error.message);
|
||||||
|
throw new Error(`Failed to search tickets for store ${padded}: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compact, purpose-built status view — no Grok, no comment enrichment.
|
* Compact, purpose-built status view — no Grok, no comment enrichment.
|
||||||
* Use this when a caller just wants "where is this ticket right now?".
|
* Use this when a caller just wants "where is this ticket right now?".
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ export {
|
||||||
fetchJiraIssue,
|
fetchJiraIssue,
|
||||||
fetchPlainDescription,
|
fetchPlainDescription,
|
||||||
searchOpenTicketsByReporterEmail,
|
searchOpenTicketsByReporterEmail,
|
||||||
|
searchOpenTicketsByStoreNumber,
|
||||||
getTicketStatus,
|
getTicketStatus,
|
||||||
updateTicket,
|
updateTicket,
|
||||||
getTransitions,
|
getTransitions,
|
||||||
|
|
@ -83,6 +84,7 @@ import {
|
||||||
fetchJiraIssue,
|
fetchJiraIssue,
|
||||||
fetchPlainDescription,
|
fetchPlainDescription,
|
||||||
searchOpenTicketsByReporterEmail,
|
searchOpenTicketsByReporterEmail,
|
||||||
|
searchOpenTicketsByStoreNumber,
|
||||||
getTicketStatus,
|
getTicketStatus,
|
||||||
updateTicket,
|
updateTicket,
|
||||||
getTransitions,
|
getTransitions,
|
||||||
|
|
@ -117,6 +119,7 @@ export default {
|
||||||
fetchPlainDescription,
|
fetchPlainDescription,
|
||||||
fetchPublicComments,
|
fetchPublicComments,
|
||||||
searchOpenTicketsByReporterEmail,
|
searchOpenTicketsByReporterEmail,
|
||||||
|
searchOpenTicketsByStoreNumber,
|
||||||
attachFileToJira,
|
attachFileToJira,
|
||||||
attachReadableTranscript,
|
attachReadableTranscript,
|
||||||
postWebexSummaryComment,
|
postWebexSummaryComment,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue