feat: add CW members, opportunity create/update, and integrator interceptor

This commit is contained in:
2026-03-07 18:15:17 -06:00
parent 0ce1eda606
commit c0a4d4f919
27 changed files with 2504 additions and 16 deletions
+276 -1
View File
@@ -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`