Add special-order product flow for sales opportunities

This commit is contained in:
2026-03-04 00:11:40 -06:00
parent a048e1e824
commit d5c22c8eff
12 changed files with 457 additions and 114 deletions
+85 -6
View File
@@ -2326,8 +2326,8 @@ Fetch a paginated list of catalog items. Supports search.
- `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
- `category` (optional) — Filter by CW category name (e.g. `Technology`, `Field`, `General`)
- `subcategory` (optional) — Filter by CW subcategory name (e.g. `Network-Switch`, `AlarmBurg-Panels`)
- `category` (optional) — Filter by CW category **name or CW ID** (e.g. `Technology` or `18`)
- `subcategory` (optional) — Filter by CW subcategory **name or CW ID** (e.g. `Network-Switch` or `112`)
- `group` (optional) — Filter by umbrella group name (e.g. `Network`, `AlarmBurg`, `Cables`). When used with `category`, returns items whose subcategory belongs to that group within the category.
- `manufacturer` (optional) — Filter by manufacturer name (case-insensitive contains match)
- `ecosystem` (optional) — Filter by ecosystem name (e.g. `Networking`, `Video Surveillance`, `Burg/Alarm`). Applies manufacturer + category + subcategory-prefix matching rules.
@@ -2726,8 +2726,8 @@ Fetch the distinct values available for filter dropdowns (categories, subcategor
**Query Parameters:**
- `category` (optional) — Scope subcategories and manufacturers to items in this category
- `subcategory` (optional) — Scope manufacturers to items in this subcategory
- `category` (optional) — Scope subcategories and manufacturers to items in this category (accepts CW category name or CW ID)
- `subcategory` (optional) — Scope manufacturers to items in this subcategory (accepts CW subcategory name or CW ID)
- `includeInactive` (optional, default `false`) — Include inactive catalog items
**Response:**
@@ -2988,7 +2988,7 @@ Fetch a single opportunity by its internal ID or ConnectWise opportunity ID. The
**Query Parameters:**
- `include` _(optional)_ — Comma-separated list of sub-resources to embed in the response. Supported values: `notes`, `contacts`, `products`. Example: `?include=notes,contacts,products`. Sub-resources are fetched in parallel and added as top-level keys on the response object.
- `include` _(optional)_ — Comma-separated list of sub-resources to embed in the response. Supported values: `notes`, `contacts`, `products`. Example: `?include=notes,contacts,products`. Sub-resources are fetched in parallel and added as top-level keys on the response object. When `notes` is included, `data.notes` is returned as an array of note objects and the original opportunity text note is preserved under `data.opportunityNoteText`.
**Response:**
@@ -3238,7 +3238,7 @@ Refresh an opportunity's local data by fetching the latest from ConnectWise. The
**GET** `/sales/opportunities/:identifier/products`
Fetch products (forecast/revenue line items) for an opportunity. Data is served from the Redis cache when available; on cache miss, data is fetched live from ConnectWise and cached. Hot opportunities (updated within 3 days) have a 15-second TTL; others use a 30-minute lazy TTL. Products are returned sorted by the opportunity's local `productSequence` array when set; otherwise, items are sorted by their ConnectWise `sequenceNumber`.
Fetch products for an opportunity, scoped to items that have a matching ConnectWise procurement product (`forecastDetailId` link). Data is served from the Redis cache when available; on cache miss, data is fetched live from ConnectWise and cached. Hot opportunities (updated within 3 days) have a 15-second TTL; others use a 30-minute lazy TTL. Products are returned sorted by the opportunity's local `productSequence` array when set; otherwise, items are sorted by their ConnectWise `sequenceNumber`.
**Authentication Required:** Yes
@@ -3490,6 +3490,85 @@ All fields are optional. Only fields the user has the corresponding `sales.oppor
---
### Add SPECIAL ORDER Product
**POST** `/sales/opportunities/:identifier/products/special-order`
Add one or more products as **SPECIAL ORDER** procurement line items. This route creates ConnectWise procurement products (not forecast items) and enforces stable defaults for quick-entry workflows.
**Authentication Required:** Yes
**Required Permissions:** `sales.opportunity.product.add.specialOrder`
**Path Parameters:**
- `identifier` — Internal ID (cuid) or ConnectWise opportunity ID (numeric)
**Request Body:**
Accepts either a single object or an array of objects.
```json
{
"desc": "SPECIAL ORDER - Lead time confirmed",
"customerDesc": "Customer-facing special order description",
"qty": 1,
"price": 750,
"cost": 500,
"taxable": true,
"procurementNotes": "Vendor ETA pending confirmation",
"productNarrative": "Install with existing rack accessories"
}
```
| Field | Type | Required | Description |
| ------------------ | ------- | -------- | --------------------------------------------------------- |
| `desc` | string | Yes | Internal/sales line description |
| `customerDesc` | string | No | Customer-facing line description |
| `qty` | number | No | Quantity (defaults to `1` when omitted) |
| `price` | number | Yes | Revenue amount |
| `cost` | number | No | Cost amount |
| `taxable` | boolean | No | Taxable flag (defaults to the catalog item's tax setting) |
| `procurementNotes` | string | No | Maps to custom field `id: 29` |
| `productNarrative` | string | No | Maps to custom field `id: 46` |
**Route-Enforced Defaults:**
- `catalogItem` is always set to the canonical catalog item with identifier `SPECIAL ORDER`
- `description` is always set from `desc`
- `customerDescription` is always set from `customerDesc` when provided
- `quantity` is always set from `qty` (default `1`)
- `price` is always set from `price`
- `cost` is always set from `cost` when provided
- `dropshipFlag` is always set to `false`
- `billableOption` is always set to `Billable`
- `taxableFlag` is set from `taxable` (or `taxableFlag`), defaulting to the catalog item's `salesTaxable` value
- `customFields` are auto-built when notes are provided:
- `procurementNotes` → `Procurement Notes` (`id: 29`)
- `productNarrative` → `Product Narrative` (`id: 46`)
**Response:**
```json
{
"status": 201,
"message": "Special-order product added successfully!",
"data": {
"id": 88340,
"forecastDetailId": 32280,
"description": "SPECIAL ORDER - Lead time confirmed",
"customerDescription": "Customer-facing special order description",
"quantity": 1,
"price": 750,
"cost": 500,
"taxableFlag": true
},
"successful": true
}
```
---
### Get Opportunity Notes
**GET** `/sales/opportunities/:identifier/notes`