ConnectWise PSA skills and tools under ops/connectwise (hub-sanitized 2026-11-01: client intel, internal workflow/credential/member/company ids, archived-page apiToken/sessionId stripped to placeholders)
This commit is contained in:
commit
9c96092034
36 files changed
+310875
No files matched your search
@@ -0,0 +1,74 @@
|
||||
# ConnectWise Customer Aggregation Recipe
|
||||
|
||||
Reference: How to aggregate open tickets by customer and enrich with primary contact details.
|
||||
|
||||
## Use Case
|
||||
- "List customers who have had open tickets in the past N years"
|
||||
- "Show me every company with active Help Desk tickets and their primary contact email"
|
||||
|
||||
## API Traversal
|
||||
|
||||
```python
|
||||
import base64
|
||||
import requests
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# Auth
|
||||
creds = {...} # from ~/.secrets/connectwise
|
||||
auth_str = f"{company_id}{public_key}:{private_key}"
|
||||
encoded_auth = base64.b64encode(auth_str.encode()).decode()
|
||||
headers = {
|
||||
'Authorization': f'Basic {encoded_auth}',
|
||||
'clientId': client_id,
|
||||
'Company-Id': company_id,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
# 1. Fetch tickets
|
||||
base_url = 'https://cw.vistanetinc.com/v4_6_release/apis/3.0'
|
||||
two_years_ago = (datetime.now() - timedelta(days=730)).strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||
conditions = f"board/id=1 and closedFlag=false and dateEntered>'{two_years_ago}'"
|
||||
url = f"{base_url}/service/tickets?conditions={conditions}&pageSize=1000"
|
||||
tickets = requests.get(url, headers=headers).json()
|
||||
|
||||
# 2. Aggregate by company
|
||||
companies = {}
|
||||
for t in tickets:
|
||||
status_name = t.get('status', {}).get('name', '')
|
||||
if status_name.startswith('>'):
|
||||
continue
|
||||
comp_id = t.get('company', {}).get('id')
|
||||
if not comp_id:
|
||||
continue
|
||||
companies.setdefault(comp_id, {
|
||||
'name': t['company']['name'],
|
||||
'ticket_count': 0,
|
||||
'latest_date': t['_info']['dateEntered']
|
||||
})
|
||||
companies[comp_id]['ticket_count'] += 1
|
||||
if t['_info']['dateEntered'] > companies[comp_id]['latest_date']:
|
||||
companies[comp_id]['latest_date'] = t['_info']['dateEntered']
|
||||
|
||||
# 3. Enrich with primary contact email
|
||||
for comp_id, data in companies.items():
|
||||
comp = requests.get(f"{base_url}/company/companies/{comp_id}", headers=headers).json()
|
||||
contact_id = comp.get('defaultContact', {}).get('id')
|
||||
data['contact_name'] = comp.get('defaultContact', {}).get('name', 'N/A')
|
||||
data['email'] = comp.get('invoiceToEmailAddress', 'N/A')
|
||||
|
||||
if contact_id:
|
||||
contact = requests.get(f"{base_url}/company/contacts/{contact_id}", headers=headers).json()
|
||||
for item in contact.get('communicationItems', []):
|
||||
if item.get('type', {}).get('name') == 'Email' and item.get('defaultFlag'):
|
||||
data['email'] = item['value']
|
||||
break
|
||||
```
|
||||
|
||||
## Board IDs
|
||||
- VistaNet Help Desk = 1
|
||||
- VistaNet Management = 25
|
||||
- Managed Service Alerts = 6
|
||||
|
||||
## Known Gaps
|
||||
- Some companies (e.g., BJ LaRossa, Far West Rice Inc.) have no default contact email in CW. Display `N/A` gracefully.
|
||||
- `invoiceToEmailAddress` on the company record is often billing/AP, not the operational contact.
|
||||
Reference in new issue
Block a user