Fix UserController permission serialization and include current updates
This commit is contained in:
+673
@@ -2204,6 +2204,679 @@ A fun Easter egg endpoint that returns HTTP 418 (I'm a teapot).
|
||||
|
||||
---
|
||||
|
||||
## Procurement Routes
|
||||
|
||||
### Get All Catalog Items
|
||||
|
||||
**GET** `/procurement/items`
|
||||
|
||||
Fetch a paginated list of catalog items. Supports search.
|
||||
|
||||
**Authentication Required:** Yes
|
||||
|
||||
**Required Permissions:** `procurement.catalog.fetch.many`
|
||||
|
||||
**Query Parameters:**
|
||||
|
||||
- `page` (optional, default `1`) — Page number
|
||||
- `rpp` (optional, default `30`) — Records per page
|
||||
- `search` (optional) — Search by name, description, part number, vendor SKU, or manufacturer
|
||||
- `includeInactive` (optional, default `false`) — Include inactive catalog items in results
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": 200,
|
||||
"message": "Catalog items fetched successfully!",
|
||||
"data": [
|
||||
{
|
||||
"id": "clx...",
|
||||
"cwCatalogId": 123,
|
||||
"name": "Dell OptiPlex 7020",
|
||||
"description": "Dell OptiPlex 7020 SFF Desktop",
|
||||
"customerDescription": "Business Desktop Computer",
|
||||
"internalNotes": null,
|
||||
"manufacturer": "Dell",
|
||||
"manufactureCwId": 45,
|
||||
"partNumber": "OPT7020-SFF",
|
||||
"vendorName": "Dell Direct",
|
||||
"vendorSku": "DELL-OPT7020",
|
||||
"vendorCwId": 12,
|
||||
"price": 899.99,
|
||||
"cost": 650.0,
|
||||
"inactive": false,
|
||||
"salesTaxable": true,
|
||||
"onHand": 5,
|
||||
"cwLastUpdated": "2026-02-25T10:00:00.000Z",
|
||||
"createdAt": "2026-01-15T00:00:00.000Z",
|
||||
"updatedAt": "2026-02-25T10:00:00.000Z"
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"pagination": {
|
||||
"previousPage": null,
|
||||
"currentPage": 1,
|
||||
"nextPage": 2,
|
||||
"totalPages": 10,
|
||||
"totalRecords": 300,
|
||||
"listedRecords": 30
|
||||
}
|
||||
},
|
||||
"successful": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Get Catalog Item
|
||||
|
||||
**GET** `/procurement/items/:identifier`
|
||||
|
||||
Fetch a single catalog item by its internal ID or ConnectWise catalog ID.
|
||||
|
||||
**Authentication Required:** Yes
|
||||
|
||||
**Required Permissions:** `procurement.catalog.fetch`
|
||||
|
||||
**Path Parameters:**
|
||||
|
||||
- `identifier` — Internal ID (cuid) or ConnectWise catalog ID (numeric)
|
||||
|
||||
**Query Parameters:**
|
||||
|
||||
- `includeLinkedItems` (optional, default `false`) — Include linked catalog items in the response
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": 200,
|
||||
"message": "Catalog item fetched successfully!",
|
||||
"data": {
|
||||
"id": "clx...",
|
||||
"cwCatalogId": 123,
|
||||
"name": "Dell OptiPlex 7020",
|
||||
"description": "Dell OptiPlex 7020 SFF Desktop",
|
||||
"customerDescription": "Business Desktop Computer",
|
||||
"internalNotes": null,
|
||||
"manufacturer": "Dell",
|
||||
"manufactureCwId": 45,
|
||||
"partNumber": "OPT7020-SFF",
|
||||
"vendorName": "Dell Direct",
|
||||
"vendorSku": "DELL-OPT7020",
|
||||
"vendorCwId": 12,
|
||||
"price": 899.99,
|
||||
"cost": 650.0,
|
||||
"inactive": false,
|
||||
"salesTaxable": true,
|
||||
"onHand": 5,
|
||||
"cwLastUpdated": "2026-02-25T10:00:00.000Z",
|
||||
"linkedItems": [
|
||||
{
|
||||
"id": "clx...",
|
||||
"cwCatalogId": 456,
|
||||
"name": "Dell Warranty - 3 Year"
|
||||
}
|
||||
],
|
||||
"createdAt": "2026-01-15T00:00:00.000Z",
|
||||
"updatedAt": "2026-02-25T10:00:00.000Z"
|
||||
},
|
||||
"successful": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Get Catalog Item Count
|
||||
|
||||
**GET** `/procurement/count`
|
||||
|
||||
Get the total number of catalog items.
|
||||
|
||||
**Authentication Required:** Yes
|
||||
|
||||
**Required Permissions:** `procurement.catalog.fetch.many`
|
||||
|
||||
**Query Parameters:**
|
||||
|
||||
- `activeOnly` (optional, default `false`) — Only count active (non-inactive) items
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": 200,
|
||||
"message": "Catalog item count fetched successfully!",
|
||||
"data": {
|
||||
"count": 300
|
||||
},
|
||||
"successful": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Refresh Catalog Item Inventory
|
||||
|
||||
**POST** `/procurement/items/:identifier/refresh-inventory`
|
||||
|
||||
Refresh the on-hand inventory count for a catalog item by fetching the latest data from ConnectWise.
|
||||
|
||||
**Authentication Required:** Yes
|
||||
|
||||
**Required Permissions:** `procurement.catalog.inventory.refresh`
|
||||
|
||||
**Path Parameters:**
|
||||
|
||||
- `identifier` — Internal ID (cuid) or ConnectWise catalog ID (numeric)
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": 200,
|
||||
"message": "Inventory refreshed successfully!",
|
||||
"data": {
|
||||
"id": "clx...",
|
||||
"cwCatalogId": 123,
|
||||
"name": "Dell OptiPlex 7020",
|
||||
"onHand": 7,
|
||||
"price": 899.99,
|
||||
"cost": 650.0,
|
||||
"inactive": false,
|
||||
"createdAt": "2026-01-15T00:00:00.000Z",
|
||||
"updatedAt": "2026-02-26T12:00:00.000Z"
|
||||
},
|
||||
"successful": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Get Linked Catalog Items
|
||||
|
||||
**GET** `/procurement/items/:identifier/linked`
|
||||
|
||||
Fetch all catalog items linked to a specific item.
|
||||
|
||||
**Authentication Required:** Yes
|
||||
|
||||
**Required Permissions:** `procurement.catalog.fetch`
|
||||
|
||||
**Path Parameters:**
|
||||
|
||||
- `identifier` — Internal ID (cuid), CW identifier string, or CW catalog ID (numeric)
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": 200,
|
||||
"message": "Linked catalog items fetched successfully!",
|
||||
"data": [
|
||||
{
|
||||
"id": "clx...",
|
||||
"cwCatalogId": 456,
|
||||
"identifier": "DELL-WAR-3YR",
|
||||
"name": "Dell Warranty - 3 Year",
|
||||
"description": "Dell 3 Year ProSupport Warranty",
|
||||
"price": 199.99,
|
||||
"cost": 120.0,
|
||||
"inactive": false,
|
||||
"onHand": 0,
|
||||
"createdAt": "2026-01-15T00:00:00.000Z",
|
||||
"updatedAt": "2026-02-25T10:00:00.000Z"
|
||||
}
|
||||
],
|
||||
"successful": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Link Catalog Items
|
||||
|
||||
**POST** `/procurement/items/:identifier/link`
|
||||
|
||||
Link a target catalog item to the specified source item. The source item is identified by the URL parameter and the target by the request body.
|
||||
|
||||
**Authentication Required:** Yes
|
||||
|
||||
**Required Permissions:** `procurement.catalog.link`
|
||||
|
||||
**Path Parameters:**
|
||||
|
||||
- `identifier` — Internal ID (cuid), CW identifier string, or CW catalog ID (numeric) of the source item
|
||||
|
||||
**Request Body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"targetId": "clx..."
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": 200,
|
||||
"message": "Catalog item linked successfully!",
|
||||
"data": {
|
||||
"id": "clx...",
|
||||
"cwCatalogId": 123,
|
||||
"identifier": "OPT7020-SFF",
|
||||
"name": "Dell OptiPlex 7020",
|
||||
"linkedItems": [
|
||||
{
|
||||
"id": "clx...",
|
||||
"cwCatalogId": 456,
|
||||
"identifier": "DELL-WAR-3YR",
|
||||
"name": "Dell Warranty - 3 Year"
|
||||
}
|
||||
],
|
||||
"createdAt": "2026-01-15T00:00:00.000Z",
|
||||
"updatedAt": "2026-02-26T12:00:00.000Z"
|
||||
},
|
||||
"successful": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Unlink Catalog Items
|
||||
|
||||
**POST** `/procurement/items/:identifier/unlink`
|
||||
|
||||
Remove the link between a source catalog item and a target catalog item.
|
||||
|
||||
**Authentication Required:** Yes
|
||||
|
||||
**Required Permissions:** `procurement.catalog.link`
|
||||
|
||||
**Path Parameters:**
|
||||
|
||||
- `identifier` — Internal ID (cuid), CW identifier string, or CW catalog ID (numeric) of the source item
|
||||
|
||||
**Request Body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"targetId": "clx..."
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": 200,
|
||||
"message": "Catalog item unlinked successfully!",
|
||||
"data": {
|
||||
"id": "clx...",
|
||||
"cwCatalogId": 123,
|
||||
"identifier": "OPT7020-SFF",
|
||||
"name": "Dell OptiPlex 7020",
|
||||
"linkedItems": [],
|
||||
"createdAt": "2026-01-15T00:00:00.000Z",
|
||||
"updatedAt": "2026-02-26T12:00:00.000Z"
|
||||
},
|
||||
"successful": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Sales Routes
|
||||
|
||||
Sales routes serve opportunity data stored locally and synced from ConnectWise. List, search, and count operations read from the local database. Sub-resource routes (forecasts, notes, contacts) fetch live data from ConnectWise using the opportunity's CW ID.
|
||||
|
||||
### Get All Opportunities
|
||||
|
||||
**GET** `/sales/opportunities`
|
||||
|
||||
Fetch a paginated list of opportunities. Supports search.
|
||||
|
||||
**Authentication Required:** Yes
|
||||
|
||||
**Required Permissions:** `sales.opportunity.fetch.many`
|
||||
|
||||
**Query Parameters:**
|
||||
|
||||
- `page` (optional, default `1`) — Page number
|
||||
- `rpp` (optional, default `30`) — Records per page
|
||||
- `search` (optional) — Search by opportunity name
|
||||
- `includeClosed` (optional, default `false`) — Include closed opportunities in results
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": 200,
|
||||
"message": "Opportunities fetched successfully!",
|
||||
"data": [
|
||||
{
|
||||
"id": "clx...",
|
||||
"cwOpportunityId": 456,
|
||||
"name": "Acme Corp Network Refresh",
|
||||
"notes": "Full network redesign and hardware refresh",
|
||||
"type": { "id": 1, "name": "New" },
|
||||
"stage": { "id": 3, "name": "Proposal" },
|
||||
"status": { "id": 1, "name": "Open" },
|
||||
"priority": { "id": 2, "name": "High" },
|
||||
"rating": { "id": 1, "name": "Hot" },
|
||||
"source": "Referral",
|
||||
"campaign": null,
|
||||
"primarySalesRep": {
|
||||
"id": 10,
|
||||
"identifier": "JDoe",
|
||||
"name": "John Doe"
|
||||
},
|
||||
"secondarySalesRep": null,
|
||||
"company": { "id": 100, "name": "Acme Corp" },
|
||||
"contact": { "id": 200, "name": "Jane Smith" },
|
||||
"site": { "id": 50, "name": "Main Office" },
|
||||
"customerPO": null,
|
||||
"totalSalesTax": 0,
|
||||
"location": { "id": 1, "name": "Murray" },
|
||||
"department": { "id": 5, "name": "Sales" },
|
||||
"expectedCloseDate": "2026-04-15T00:00:00.000Z",
|
||||
"pipelineChangeDate": "2026-02-20T00:00:00.000Z",
|
||||
"dateBecameLead": "2026-01-10T00:00:00.000Z",
|
||||
"closedDate": null,
|
||||
"closedFlag": false,
|
||||
"closedBy": null,
|
||||
"companyId": "clx...",
|
||||
"cwLastUpdated": "2026-02-26T10:00:00.000Z",
|
||||
"createdAt": "2026-02-01T00:00:00.000Z",
|
||||
"updatedAt": "2026-02-26T10:00:00.000Z"
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"pagination": {
|
||||
"previousPage": null,
|
||||
"currentPage": 1,
|
||||
"nextPage": 2,
|
||||
"totalPages": 5,
|
||||
"totalRecords": 150,
|
||||
"listedRecords": 30
|
||||
}
|
||||
},
|
||||
"successful": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Get Opportunity Count
|
||||
|
||||
**GET** `/sales/opportunities/count`
|
||||
|
||||
Get the total number of opportunities.
|
||||
|
||||
**Authentication Required:** Yes
|
||||
|
||||
**Required Permissions:** `sales.opportunity.fetch.many`
|
||||
|
||||
**Query Parameters:**
|
||||
|
||||
- `openOnly` (optional, default `false`) — Only count open (non-closed) opportunities
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": 200,
|
||||
"message": "Opportunity count fetched successfully!",
|
||||
"data": {
|
||||
"count": 150
|
||||
},
|
||||
"successful": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Get Opportunity
|
||||
|
||||
**GET** `/sales/opportunities/:identifier`
|
||||
|
||||
Fetch a single opportunity by its internal ID or ConnectWise opportunity ID.
|
||||
|
||||
**Authentication Required:** Yes
|
||||
|
||||
**Required Permissions:** `sales.opportunity.fetch`
|
||||
|
||||
**Path Parameters:**
|
||||
|
||||
- `identifier` — Internal ID (cuid) or ConnectWise opportunity ID (numeric)
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": 200,
|
||||
"message": "Opportunity fetched successfully!",
|
||||
"data": {
|
||||
"id": "clx...",
|
||||
"cwOpportunityId": 456,
|
||||
"name": "Acme Corp Network Refresh",
|
||||
"notes": "Full network redesign and hardware refresh",
|
||||
"type": { "id": 1, "name": "New" },
|
||||
"stage": { "id": 3, "name": "Proposal" },
|
||||
"status": { "id": 1, "name": "Open" },
|
||||
"priority": { "id": 2, "name": "High" },
|
||||
"rating": { "id": 1, "name": "Hot" },
|
||||
"source": "Referral",
|
||||
"campaign": null,
|
||||
"primarySalesRep": {
|
||||
"id": 10,
|
||||
"identifier": "JDoe",
|
||||
"name": "John Doe"
|
||||
},
|
||||
"secondarySalesRep": null,
|
||||
"company": { "id": 100, "name": "Acme Corp" },
|
||||
"contact": { "id": 200, "name": "Jane Smith" },
|
||||
"site": { "id": 50, "name": "Main Office" },
|
||||
"customerPO": null,
|
||||
"totalSalesTax": 0,
|
||||
"location": { "id": 1, "name": "Murray" },
|
||||
"department": { "id": 5, "name": "Sales" },
|
||||
"expectedCloseDate": "2026-04-15T00:00:00.000Z",
|
||||
"pipelineChangeDate": "2026-02-20T00:00:00.000Z",
|
||||
"dateBecameLead": "2026-01-10T00:00:00.000Z",
|
||||
"closedDate": null,
|
||||
"closedFlag": false,
|
||||
"closedBy": null,
|
||||
"companyId": "clx...",
|
||||
"cwLastUpdated": "2026-02-26T10:00:00.000Z",
|
||||
"createdAt": "2026-02-01T00:00:00.000Z",
|
||||
"updatedAt": "2026-02-26T10:00:00.000Z"
|
||||
},
|
||||
"successful": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Refresh Opportunity
|
||||
|
||||
**POST** `/sales/opportunities/:identifier/refresh`
|
||||
|
||||
Refresh an opportunity's local data by fetching the latest from ConnectWise.
|
||||
|
||||
**Authentication Required:** Yes
|
||||
|
||||
**Required Permissions:** `sales.opportunity.refresh`
|
||||
|
||||
**Path Parameters:**
|
||||
|
||||
- `identifier` — Internal ID (cuid) or ConnectWise opportunity ID (numeric)
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": 200,
|
||||
"message": "Opportunity refreshed from ConnectWise successfully!",
|
||||
"data": {
|
||||
"id": "clx...",
|
||||
"cwOpportunityId": 456,
|
||||
"name": "Acme Corp Network Refresh",
|
||||
"notes": "Updated notes from CW",
|
||||
"type": { "id": 1, "name": "New" },
|
||||
"stage": { "id": 4, "name": "Negotiation" },
|
||||
"status": { "id": 1, "name": "Open" },
|
||||
"priority": { "id": 2, "name": "High" },
|
||||
"rating": { "id": 1, "name": "Hot" },
|
||||
"source": "Referral",
|
||||
"campaign": null,
|
||||
"primarySalesRep": {
|
||||
"id": 10,
|
||||
"identifier": "JDoe",
|
||||
"name": "John Doe"
|
||||
},
|
||||
"secondarySalesRep": null,
|
||||
"company": { "id": 100, "name": "Acme Corp" },
|
||||
"contact": { "id": 200, "name": "Jane Smith" },
|
||||
"site": { "id": 50, "name": "Main Office" },
|
||||
"customerPO": null,
|
||||
"totalSalesTax": 0,
|
||||
"location": { "id": 1, "name": "Murray" },
|
||||
"department": { "id": 5, "name": "Sales" },
|
||||
"expectedCloseDate": "2026-04-15T00: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-02-26T14:00:00.000Z",
|
||||
"createdAt": "2026-02-01T00:00:00.000Z",
|
||||
"updatedAt": "2026-02-26T14:00:00.000Z"
|
||||
},
|
||||
"successful": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Get Opportunity Forecasts
|
||||
|
||||
**GET** `/sales/opportunities/:identifier/forecasts`
|
||||
|
||||
Fetch forecast/revenue items for an opportunity. Data is fetched live from ConnectWise using the opportunity's CW ID.
|
||||
|
||||
**Authentication Required:** Yes
|
||||
|
||||
**Required Permissions:** `sales.opportunity.fetch`
|
||||
|
||||
**Path Parameters:**
|
||||
|
||||
- `identifier` — Internal ID (cuid) or ConnectWise opportunity ID (numeric)
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": 200,
|
||||
"message": "Opportunity forecasts fetched successfully!",
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"forecastType": "Revenue",
|
||||
"forecastMonth": "2026-03-01T00:00:00Z",
|
||||
"revenue": 50000.0,
|
||||
"cost": 30000.0,
|
||||
"forecastPercentage": 75,
|
||||
"status": { "id": 1, "name": "Open" },
|
||||
"includedFlag": true,
|
||||
"linkedFlag": false,
|
||||
"recurringFlag": false
|
||||
}
|
||||
],
|
||||
"successful": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Get Opportunity Notes
|
||||
|
||||
**GET** `/sales/opportunities/:identifier/notes`
|
||||
|
||||
Fetch notes for an opportunity. Data is fetched live from ConnectWise using the opportunity's CW ID.
|
||||
|
||||
**Authentication Required:** Yes
|
||||
|
||||
**Required Permissions:** `sales.opportunity.fetch`
|
||||
|
||||
**Path Parameters:**
|
||||
|
||||
- `identifier` — Internal ID (cuid) or ConnectWise opportunity ID (numeric)
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": 200,
|
||||
"message": "Opportunity notes fetched successfully!",
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"text": "Client expressed interest in a full network refresh.",
|
||||
"type": { "id": 2, "name": "Discussion" },
|
||||
"flagged": false,
|
||||
"enteredBy": "JDoe"
|
||||
}
|
||||
],
|
||||
"successful": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Get Opportunity Contacts
|
||||
|
||||
**GET** `/sales/opportunities/:identifier/contacts`
|
||||
|
||||
Fetch contacts associated with an opportunity. Data is fetched live from ConnectWise using the opportunity's CW ID.
|
||||
|
||||
**Authentication Required:** Yes
|
||||
|
||||
**Required Permissions:** `sales.opportunity.fetch`
|
||||
|
||||
**Path Parameters:**
|
||||
|
||||
- `identifier` — Internal ID (cuid) or ConnectWise opportunity ID (numeric)
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": 200,
|
||||
"message": "Opportunity contacts fetched successfully!",
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"contact": { "id": 200, "name": "Jane Smith" },
|
||||
"company": {
|
||||
"id": 100,
|
||||
"identifier": "AcmeCorp",
|
||||
"name": "Acme Corp"
|
||||
},
|
||||
"role": { "id": 1, "name": "Decision Maker" },
|
||||
"notes": "Primary point of contact for this deal",
|
||||
"referralFlag": false
|
||||
}
|
||||
],
|
||||
"successful": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## UniFi Routes
|
||||
|
||||
All UniFi routes require the `unifi.access` permission in addition to their route-specific permission. This acts as a gate for the entire UniFi API.
|
||||
|
||||
Reference in New Issue
Block a user