feat: add CW members, opportunity create/update, and integrator interceptor
This commit is contained in:
+276
-1
@@ -137,7 +137,46 @@ See [PERMISSIONS.md](PERMISSIONS.md) for the full list of field-level permission
|
||||
|
||||
## Authentication Routes
|
||||
|
||||
## ConnectWise Callback Routes
|
||||
## ConnectWise Routes
|
||||
|
||||
### Fetch CW Members
|
||||
|
||||
**GET** `/cw/members`
|
||||
|
||||
Returns all ConnectWise members from the server-side member cache, sorted alphabetically by name. By default only active members are returned.
|
||||
|
||||
**Authentication Required:** Yes (any authenticated user)
|
||||
|
||||
**Permissions Required:** None
|
||||
|
||||
**Query Parameters:**
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
| --------- | ------ | ------- | ------------------------------------------ |
|
||||
| `active` | string | `true` | Set to `false` to include inactive members |
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": 200,
|
||||
"message": "CW members fetched successfully!",
|
||||
"data": [
|
||||
{
|
||||
"id": 250,
|
||||
"identifier": "jroberts",
|
||||
"firstName": "John",
|
||||
"lastName": "Roberts",
|
||||
"name": "John Roberts",
|
||||
"officeEmail": "jroberts@totaltech.net",
|
||||
"inactive": false
|
||||
}
|
||||
],
|
||||
"successful": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Receive ConnectWise Callback
|
||||
|
||||
@@ -3344,6 +3383,242 @@ Refresh an opportunity's local data by fetching the latest from ConnectWise. The
|
||||
|
||||
---
|
||||
|
||||
### Create Opportunity
|
||||
|
||||
**POST** `/sales/opportunities`
|
||||
|
||||
Create a new opportunity in ConnectWise. The created opportunity is synced to the local database and returned in the response. `name` and `expectedCloseDate` are required; all other fields are optional.
|
||||
|
||||
**Authentication Required:** Yes
|
||||
|
||||
**Required Permissions:** `sales.opportunity.create`
|
||||
|
||||
**Request Body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Acme Corp Network Refresh",
|
||||
"expectedCloseDate": "2026-06-01",
|
||||
"notes": "Initial scoping phase",
|
||||
"rating": { "id": 1 },
|
||||
"type": { "id": 2 },
|
||||
"stage": { "id": 1 },
|
||||
"status": { "id": 1 },
|
||||
"priority": { "id": 2 },
|
||||
"campaign": { "id": 5 },
|
||||
"primarySalesRep": { "id": 10 },
|
||||
"secondarySalesRep": { "id": 12 },
|
||||
"company": { "id": 100 },
|
||||
"contact": { "id": 200 },
|
||||
"site": { "id": 50 },
|
||||
"source": "Referral",
|
||||
"customerPO": "PO-12345",
|
||||
"locationId": 1,
|
||||
"businessUnitId": 5
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
| ------------------- | ------------------------ | -------- | --------------------------------------------------------- |
|
||||
| `name` | `string` | Yes | Opportunity name |
|
||||
| `expectedCloseDate` | `string` | Yes | Expected close date (date string, e.g. `2026-06-01`) |
|
||||
| `primarySalesRep` | `{ id: number }` | Yes | CW member reference for primary sales rep |
|
||||
| `company` | `{ id: number }` | Yes | CW company reference |
|
||||
| `contact` | `{ id: number }` | Yes | CW contact reference |
|
||||
| `notes` | `string` | No | Opportunity description / notes |
|
||||
| `rating` | `{ id: number }` | No | CW rating reference |
|
||||
| `type` | `{ id: number }` | No | CW opportunity type reference |
|
||||
| `stage` | `{ id: number }` | No | CW pipeline stage reference |
|
||||
| `status` | `{ id: number }` | No | CW status reference |
|
||||
| `priority` | `{ id: number }` | No | CW priority reference |
|
||||
| `campaign` | `{ id: number }` | No | CW campaign reference |
|
||||
| `secondarySalesRep` | `{ id: number } \| null` | No | CW member reference for secondary sales rep (null clears) |
|
||||
| `site` | `{ id: number } \| null` | No | CW site reference (null clears) |
|
||||
| `source` | `string \| null` | No | Opportunity source (null clears) |
|
||||
| `customerPO` | `string \| null` | No | Customer PO number (null clears) |
|
||||
| `locationId` | `number` | No | CW location ID |
|
||||
| `businessUnitId` | `number` | No | CW business unit ID |
|
||||
|
||||
**Response (201):**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": 201,
|
||||
"message": "Opportunity created successfully!",
|
||||
"data": {
|
||||
"id": "clx...",
|
||||
"cwOpportunityId": 789,
|
||||
"name": "Acme Corp Network Refresh",
|
||||
"notes": "Initial scoping phase",
|
||||
"type": { "id": 2, "name": "Existing" },
|
||||
"stage": { "id": 1, "name": "Prospect" },
|
||||
"status": { "id": 1, "name": "Open" },
|
||||
"priority": { "id": 2, "name": "High" },
|
||||
"rating": { "id": 1, "name": "Hot" },
|
||||
"source": "Referral",
|
||||
"campaign": { "id": 5, "name": "Q2 Push" },
|
||||
"primarySalesRep": {
|
||||
"id": 10,
|
||||
"identifier": "JDoe",
|
||||
"name": "John Doe"
|
||||
},
|
||||
"secondarySalesRep": {
|
||||
"id": 12,
|
||||
"identifier": "ASmith",
|
||||
"name": "Alice Smith"
|
||||
},
|
||||
"company": { "id": 100, "name": "Acme Corp" },
|
||||
"contact": { "id": 200, "name": "Jane Smith" },
|
||||
"site": { "id": 50, "name": "Main Office" },
|
||||
"customerPO": "PO-12345",
|
||||
"totalSalesTax": 0,
|
||||
"probability": 0,
|
||||
"location": { "id": 1, "name": "Murray" },
|
||||
"department": null,
|
||||
"expectedCloseDate": "2026-06-01T00:00:00.000Z",
|
||||
"pipelineChangeDate": null,
|
||||
"dateBecameLead": null,
|
||||
"closedDate": null,
|
||||
"closedFlag": false,
|
||||
"closedBy": null,
|
||||
"companyId": "clx...",
|
||||
"cwLastUpdated": "2026-03-07T10:00:00.000Z",
|
||||
"createdAt": "2026-03-07T10:00:00.000Z",
|
||||
"updatedAt": "2026-03-07T10:00:00.000Z",
|
||||
"customFields": [],
|
||||
"activities": []
|
||||
},
|
||||
"successful": true
|
||||
}
|
||||
```
|
||||
|
||||
**Error Responses:**
|
||||
|
||||
| Status | Scenario |
|
||||
| ------- | ------------------------------------------------------- |
|
||||
| 400 | Zod validation failure (missing name/expectedCloseDate) |
|
||||
| 401 | Missing or invalid auth token |
|
||||
| 403 | User lacks `sales.opportunity.create` permission |
|
||||
| 4xx/5xx | ConnectWise API error (forwarded status + message) |
|
||||
| 500 | Unexpected server error |
|
||||
|
||||
---
|
||||
|
||||
### Update Opportunity
|
||||
|
||||
**PATCH** `/sales/opportunities/:identifier`
|
||||
|
||||
Update an opportunity's fields in ConnectWise (e.g. rating, sales rep, company, contact, site, description). Only the provided fields are patched; omitted fields remain unchanged. The updated opportunity is synced back to the local database and returned in the response.
|
||||
|
||||
**Authentication Required:** Yes
|
||||
|
||||
**Required Permissions:** `sales.opportunity.update`
|
||||
|
||||
**Path Parameters:**
|
||||
|
||||
- `identifier` — Internal ID (cuid) or ConnectWise opportunity ID (numeric)
|
||||
|
||||
**Request Body (all fields optional, at least one required):**
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Acme Corp Network Refresh — Phase 2",
|
||||
"notes": "Updated project scope to include wireless",
|
||||
"rating": { "id": 1 },
|
||||
"type": { "id": 2 },
|
||||
"stage": { "id": 4 },
|
||||
"status": { "id": 1 },
|
||||
"priority": { "id": 2 },
|
||||
"campaign": { "id": 5 },
|
||||
"primarySalesRep": { "id": 10 },
|
||||
"secondarySalesRep": { "id": 12 },
|
||||
"company": { "id": 100 },
|
||||
"contact": { "id": 200 },
|
||||
"site": { "id": 50 },
|
||||
"expectedCloseDate": "2026-05-01",
|
||||
"customerPO": "PO-12345",
|
||||
"source": "Referral",
|
||||
"locationId": 1,
|
||||
"businessUnitId": 5
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
| ------------------- | ------------------------ | --------------------------------------------------------- |
|
||||
| `name` | `string` | Opportunity name |
|
||||
| `notes` | `string` | Opportunity description / notes |
|
||||
| `rating` | `{ id: number }` | CW rating reference |
|
||||
| `type` | `{ id: number }` | CW opportunity type reference |
|
||||
| `stage` | `{ id: number }` | CW pipeline stage reference |
|
||||
| `status` | `{ id: number }` | CW status reference |
|
||||
| `priority` | `{ id: number }` | CW priority reference |
|
||||
| `campaign` | `{ id: number }` | CW campaign reference |
|
||||
| `primarySalesRep` | `{ id: number }` | CW member reference for primary sales rep |
|
||||
| `secondarySalesRep` | `{ id: number } \| null` | CW member reference for secondary sales rep (null clears) |
|
||||
| `company` | `{ id: number }` | CW company reference |
|
||||
| `contact` | `{ id: number } \| null` | CW contact reference (null clears) |
|
||||
| `site` | `{ id: number } \| null` | CW site reference (null clears) |
|
||||
| `expectedCloseDate` | `string` | Expected close date (ISO date string) |
|
||||
| `customerPO` | `string \| null` | Customer PO number (null clears) |
|
||||
| `source` | `string \| null` | Opportunity source (null clears) |
|
||||
| `locationId` | `number` | CW location ID |
|
||||
| `businessUnitId` | `number` | CW business unit ID |
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": 200,
|
||||
"message": "Opportunity updated successfully!",
|
||||
"data": {
|
||||
"id": "clx...",
|
||||
"cwOpportunityId": 456,
|
||||
"name": "Acme Corp Network Refresh — Phase 2",
|
||||
"description": "Updated project scope to include wireless",
|
||||
"type": { "id": 2, "name": "Existing" },
|
||||
"stage": { "id": 4, "name": "Negotiation" },
|
||||
"status": { "id": 1, "name": "Open" },
|
||||
"priority": { "id": 2, "name": "High" },
|
||||
"rating": { "id": 1, "name": "Hot" },
|
||||
"source": "Referral",
|
||||
"campaign": { "id": 5, "name": "Q2 Push" },
|
||||
"primarySalesRep": {
|
||||
"id": 10,
|
||||
"identifier": "JDoe",
|
||||
"name": "John Doe"
|
||||
},
|
||||
"secondarySalesRep": {
|
||||
"id": 12,
|
||||
"identifier": "ASmith",
|
||||
"name": "Alice Smith"
|
||||
},
|
||||
"company": { "id": 100, "name": "Acme Corp" },
|
||||
"contact": { "id": 200, "name": "Jane Smith" },
|
||||
"site": { "id": 50, "name": "Main Office" },
|
||||
"customerPO": "PO-12345",
|
||||
"totalSalesTax": 0,
|
||||
"probability": 50,
|
||||
"location": { "id": 1, "name": "Murray" },
|
||||
"department": { "id": 5, "name": "Sales" },
|
||||
"expectedCloseDate": "2026-05-01T00:00:00.000Z",
|
||||
"pipelineChangeDate": "2026-02-25T00:00:00.000Z",
|
||||
"dateBecameLead": "2026-01-10T00:00:00.000Z",
|
||||
"closedDate": null,
|
||||
"closedFlag": false,
|
||||
"closedBy": null,
|
||||
"companyId": "clx...",
|
||||
"cwLastUpdated": "2026-03-07T10:00:00.000Z",
|
||||
"createdAt": "2026-02-01T00:00:00.000Z",
|
||||
"updatedAt": "2026-03-07T10:00:00.000Z",
|
||||
"customFields": [],
|
||||
"activities": []
|
||||
},
|
||||
"successful": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Get Opportunity Products
|
||||
|
||||
**GET** `/sales/opportunities/:identifier/products`
|
||||
|
||||
Reference in New Issue
Block a user