69 lines
4.3 KiB
Markdown
69 lines
4.3 KiB
Markdown
# ConnectWise Ticket Fetching Implementation
|
||
Implementation details for fetching tickets from the VistaNet ConnectWise instance.
|
||
|
||
## API Configuration
|
||
- **Base URL**: `https://cw.vistanetinc.com/v4_6_release/apis/3.0`
|
||
- **Authentication**: Basic Auth using `companyid+publickey:privatekey`.
|
||
- **Required Header**: `clientId` (Crucial: 401 Unauthorized is returned if this is missing or incorrect).
|
||
|
||
## Implementation Pattern
|
||
Since server-side filtering is unreliable, the reliable pattern is:
|
||
1. Fetch open tickets using `conditions=id >= [RecentID] AND closedFlag=false` with a large `pageSize`.
|
||
2. Filter JSON **locally in Python** for the target company by `company.id`.
|
||
3. (Optional) Filter for specific owner, board, or other criteria locally.
|
||
4. Filter out statuses starting with `>` (e.g., `>Completed`) to identify truly open tickets.
|
||
|
||
### Why local filtering is required
|
||
The `companyId` query parameter on `/service/tickets` is **silently ignored** by the API and returns tickets from all companies. Server-side `conditions` on identifiers also often return empty lists or incorrect results. Always fetch broad and filter client-side.
|
||
|
||
## Example: All open tickets for a specific company
|
||
```python
|
||
import base64, requests, json
|
||
|
||
BASE_URL = 'https://cw.vistanetinc.com/v4_6_release/apis/3.0'
|
||
CLIENT_ID = '<CLIENT_ID>'
|
||
COMPANY_ID = 'VNI'
|
||
PUBLIC_KEY = '<PUBLIC_KEY>'
|
||
PRIVATE_KEY = '<PRIVATE_KEY>'
|
||
|
||
auth_b64 = base64.b64encode(f'{COMPANY_ID}+{PUBLIC_KEY}:{PRIVATE_KEY}'.encode()).decode()
|
||
headers = {
|
||
'Authorization': f'Basic {auth_b64}',
|
||
'clientId': CLIENT_ID,
|
||
'Content-Type': 'application/json'
|
||
}
|
||
|
||
# 1. Fetch broad — companyId is ignored, so we use id range to avoid legacy mirroring
|
||
url = f'{BASE_URL}/service/tickets?pageSize=1000&conditions=id%20%3E%3D%2080000%20AND%20closedFlag%3Dfalse'
|
||
resp = requests.get(url, headers=headers, timeout=45)
|
||
resp.raise_for_status()
|
||
tickets = resp.json()
|
||
|
||
# 2. Filter client-side for Miller Glass (company.id == 19502)
|
||
miller_tickets = [
|
||
t for t in tickets
|
||
if t.get('company', {}).get('id') == 19502
|
||
and not t.get('status', {}).get('name', '').startswith('>')
|
||
and t.get('dateResolved') is None
|
||
]
|
||
```
|
||
|
||
## "Open" ticket definition
|
||
A ticket is considered open when ALL of the following are true:
|
||
- `closedFlag` is `false`
|
||
- Status name does NOT start with `>` (e.g., `>Closed`, `>Completed`)
|
||
- `dateResolved` is `None` or absent
|
||
|
||
## Searching ticket NOTES for text across a company's tickets
|
||
Note text is NOT searchable server-side on this instance: `POST /service/tickets/search` accepts `childconditions` in its FilterValues body but **silently ignores it** — verified that six different `childconditions` values (`notes/text contains '...'`) all returned identical result sets, with no error to warn you. Never conclude a note doesn't exist based on a childconditions query.
|
||
|
||
Working pattern (validated end-to-end, ~0.25 s/ticket):
|
||
1. Resolve the company: `GET /company/companies?conditions=name contains 'Partial'` → cache `company.id`.
|
||
2. Size the corpus: `GET /service/tickets/count?conditions=company/id={id}` (the count endpoint honors the condition, unlike the list endpoint's bare `companyId` param). Bound it with `AND dateEntered >= [YYYY-01-01T00:00:00Z]` when the ask is recency-scoped — the date condition is also honored and shrank a 4,301-ticket corpus to 1,086.
|
||
3. Page-fetch ticket ids: `GET /service/tickets?conditions=company/id={id} AND dateEntered >= [...]&pageSize=200&page={n}&orderBy=id asc&fields=id,summary,status/name,closedFlag,dateResolved` until a short page. Persist the array to a scratch JSON file between calls — kernel state does not survive a timeout.
|
||
4. Per ticket: `GET /service/tickets/{id}/notes?pageSize=1000` and regex the `text` client-side. pageSize=1000 on a single ticket's notes is safe; the corruption concern applies to large LIST fetches only.
|
||
5. Triage matches by snippet before reporting: generic keywords ('office', 'field') drown in "out of the office" autoresponder noise from email-logged notes — the signal is usually in `internalFlag=true` notes written by members. Email/contact notes carry `member: null`; read `contact` or the text for attribution.
|
||
|
||
Budget runtime: 1,000+ tickets of note fetches takes ~4–5 minutes — chunk the scan (~300 tickets per execute_code call) and append matches to a scratch JSON so a timeout loses nothing.
|
||
|