Fix UserController permission serialization and include current updates

This commit is contained in:
2026-02-27 14:38:22 -06:00
parent 51eb36f4a6
commit b1f6462ac3
50 changed files with 6150 additions and 30 deletions
+673
View File
@@ -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.