feat: restructure sales, add PDF quote generation and WebSocket support

This commit is contained in:
2026-03-06 23:25:37 -06:00
parent 4efca6cc53
commit 1907bb433b
73 changed files with 8115 additions and 170 deletions
+407 -1
View File
@@ -8,6 +8,111 @@ This document provides a comprehensive overview of all API routes available in t
http://localhost:3000/v1
```
## WebSocket Base URL
```
ws://localhost:3000/socket.io/
```
---
## WebSocket Events
### Secure Namespace
**Namespace:** `/secure`
**Authentication Required:** Yes
Provide authorization during Socket.IO handshake using one of:
- `handshake.headers.authorization` as `Bearer <accessToken>` or `Key <accessToken>`
- `handshake.auth.authorization` as `Bearer <accessToken>` or `Key <accessToken>`
- `handshake.auth.token` as raw access token
When connected, server emits `secure:connected` with `{ userId }`.
If the linked session expires, is invalidated, or no longer exists, server emits `secure:session:expired` and disconnects.
### Register Live Quote Preview Channel
**Client → Server Event:** `opp:live_quote_preview`
Registers a per-opportunity live preview channel.
**Required Permissions:** `sales.opportunity.fetch`
**Payload:**
```json
{
"id": "<opportunity-id>"
}
```
**Ack Response (success):**
```json
{
"ok": true,
"event": "opp:live_quote_preview:<id>:data"
}
```
**Ack Response (error):**
```json
{
"ok": false,
"error": "Missing opportunity id"
}
```
Server may also emit `opp:live_quote_preview:ready` and `opp:live_quote_preview:error`.
### Live Quote Preview Data Channel
**Client → Server Event:** `opp:live_quote_preview:<id>:data`
After registration, clients send live quote preview options on this dynamic event.
The server will:
1. Relay the incoming `...:data` payload to other sockets in the same preview room.
2. Fetch the target opportunity.
3. Generate a preview PDF using `generateQuote` with `showPreview: true`.
4. Emit the generated preview on `opp:live_quote_preview:<id>:preview`.
**Payload (all optional):**
```json
{
"lineItemPricing": true,
"includeQuoteNarrative": true,
"includeItemNarratives": true,
"logoPath": "/absolute/or/runtime/path/to/logo.png"
}
```
**Server → Client Event:** `opp:live_quote_preview:<id>:preview`
```json
{
"id": "<opportunity-id>",
"mimeType": "application/pdf",
"contentBase64": "JVBERi0xLjQKJ..."
}
```
**Server → Client Event (error):** `opp:live_quote_preview:error`
```json
{
"id": "<opportunity-id>",
"message": "Failed to generate live quote preview"
}
```
---
## Object Type Field-Level Gating
@@ -2874,6 +2979,7 @@ Fetch a paginated list of opportunities. Supports search. Each opportunity inclu
"site": { "id": 50, "name": "Main Office" },
"customerPO": null,
"totalSalesTax": 0,
"probability": 50,
"location": { "id": 1, "name": "Murray" },
"department": { "id": 5, "name": "Sales" },
"expectedCloseDate": "2026-04-15T00:00:00.000Z",
@@ -2988,7 +3094,9 @@ 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. 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`.
- `include` _(optional)_ — Comma-separated list of sub-resources to embed in the response. Supported values: `notes`, `contacts`, `products`, `quotes`. Example: `?include=notes,contacts,products,quotes`. 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`. When `quotes` is included, `data.quotes` is returned as an array of committed quote metadata objects (PDF file data is excluded).
- `includeRegenData` _(optional)_ — When `"true"`, includes `quoteRegenData` on each quote object (applies to `?include=quotes`). Default: `false`.
- `includeRegenParams` _(optional)_ — When `"true"`, includes `quoteRegenParams` on each quote object (applies to `?include=quotes`). Default: `false`.
**Response:**
@@ -3062,6 +3170,7 @@ Fetch a single opportunity by its internal ID or ConnectWise opportunity ID. The
},
"customerPO": null,
"totalSalesTax": 0,
"probability": 50,
"location": { "id": 1, "name": "Murray" },
"department": { "id": 5, "name": "Sales" },
"expectedCloseDate": "2026-04-15T00:00:00.000Z",
@@ -3183,6 +3292,7 @@ Refresh an opportunity's local data by fetching the latest from ConnectWise. The
"site": { "id": 50, "name": "Main Office" },
"customerPO": null,
"totalSalesTax": 0,
"probability": 50,
"location": { "id": 1, "name": "Murray" },
"department": { "id": 5, "name": "Sales" },
"expectedCloseDate": "2026-04-15T00:00:00.000Z",
@@ -3835,6 +3945,302 @@ Add a labor line item to an opportunity using one of the two canonical labor cat
---
### Fetch Committed Quotes
**GET** `/sales/opportunities/:identifier/quotes`
Fetch all committed (finalized) quotes for an opportunity, ordered by most recent first.
**Authentication Required:** Yes
**Required Permissions:** `sales.opportunity.quote.fetch`
**Path Parameters:**
- `identifier` — Internal ID (cuid) or ConnectWise opportunity ID (numeric)
**Query Parameters:**
- `includeRegenData` _(optional)_ — When `"true"`, includes `quoteRegenData` on each quote object. Default: `false`.
- `includeRegenParams` _(optional)_ — When `"true"`, includes `quoteRegenParams` on each quote object. Default: `false`.
**Response:**
```json
{
"status": 200,
"message": "Committed quotes fetched successfully!",
"data": [
{
"id": "clx9abc123...",
"quoteFileName": "OPP-12345-2026-03-06T12-00-00-000Z.pdf",
"quoteRegenHash": "a1b2c3d4e5f6...",
"opportunityId": "clx9xyz789...",
"createdById": "clx9user456...",
"quoteRegenData": "(included when ?includeRegenData=true)",
"quoteRegenParams": "(included when ?includeRegenParams=true)",
"createdAt": "2026-03-06T12:00:00.000Z",
"updatedAt": "2026-03-06T12:00:00.000Z"
}
],
"successful": true
}
```
---
### Commit Quote
**POST** `/sales/opportunities/:identifier/quote/commit`
Generate a finalized (non-preview) quote PDF for an opportunity and store it in the database with regeneration metadata and creator attribution.
**Authentication Required:** Yes
**Required Permissions:** `sales.opportunity.quote.commit`
**Path Parameters:**
- `identifier` — Internal ID (cuid) or ConnectWise opportunity ID (numeric)
**Request Body (optional):**
```json
{
"lineItemPricing": true,
"includeQuoteNarrative": true,
"includeItemNarratives": true
}
```
| Field | Type | Required | Default | Description |
| ----------------------- | ------- | -------- | ------- | ---------------------------------------------- |
| `lineItemPricing` | boolean | No | `true` | Include per-line-item pricing in the quote PDF |
| `includeQuoteNarrative` | boolean | No | `true` | Include the quote-level narrative section |
| `includeItemNarratives` | boolean | No | `true` | Include per-item narrative sections |
**Response:**
```json
{
"status": 201,
"message": "Quote committed successfully!",
"data": {
"id": "clx9abc123...",
"quoteFileName": "OPP-12345-2026-03-06T12-00-00-000Z.pdf",
"quoteRegenHash": "a1b2c3d4e5f6...",
"opportunityId": "clx9xyz789...",
"createdById": "clx9user456...",
"quoteRegenData": {
"options": {
"lineItemPricing": true,
"includeQuoteNarrative": true,
"includeItemNarratives": true
},
"opportunity": {
"id": "clx9xyz789...",
"cwOpportunityId": 12345,
"name": "Network Refresh Acme Corp",
"totalSalesTax": 245.5,
"contactName": "Jane Smith",
"companyName": "Acme Corp"
},
"customer": {
"preparedFor": "Jane Smith",
"companyName": "Acme Corp",
"primaryContact": {
"firstName": "Jane",
"lastName": "Smith",
"email": "jane@acme.com",
"phone": "801-555-1234"
},
"siteAddress": ["123 Main St", "Salt Lake City UT 84101"],
"companyAddress": ["456 Corporate Blvd", "Murray UT 84107"]
},
"salesRep": {
"name": "John Roberts",
"email": "jroberts@example.com"
},
"quoteNarrative": "Full network infrastructure refresh including...",
"products": [
{
"cwForecastId": 101,
"forecastDescription": "UniFi U6-Pro AP",
"productDescription": "UniFi U6-Pro Access Point",
"customerDescription": "Wireless access point",
"productNarrative": "Install and validate onsite",
"productClass": "Product",
"forecastType": "Products",
"catalogItem": { "id": 500, "identifier": "U6-PRO" },
"quantity": 4,
"effectiveQuantity": 4,
"revenue": 800.0,
"cost": 520.0,
"margin": 280.0,
"percentage": 35.0,
"includeFlag": true,
"taxableFlag": true,
"recurringFlag": false,
"recurringRevenue": 0,
"recurringCost": 0,
"sequenceNumber": 1,
"cancelledFlag": false,
"cancellationType": null,
"quantityCancelled": 0,
"cancelledReason": null,
"cancelledDate": null
}
],
"snapshotTimestamp": "2026-03-06T12:00:00.000Z"
},
"quoteRegenParams": {
"opportunityId": "clx9xyz789...",
"cwOpportunityId": 12345
},
"createdAt": "2026-03-06T12:00:00.000Z",
"updatedAt": "2026-03-06T12:00:00.000Z"
},
"successful": true
}
```
---
### Preview Quote
**GET** `/sales/opportunities/:identifier/quote/:quoteId/preview`
Regenerate a preview-stamped version of an existing committed quote PDF using its stored generation parameters. The PDF is not persisted — it is returned as a base64-encoded string.
**Authentication Required:** Yes
**Required Permissions:** `sales.opportunity.quote.preview`
**Path Parameters:**
- `identifier` — Internal ID (cuid) or ConnectWise opportunity ID (numeric)
- `quoteId` — The generated quote's internal ID (uuid)
**Response:**
```json
{
"status": 200,
"message": "Quote preview generated successfully!",
"data": {
"mimeType": "application/pdf",
"contentBase64": "JVBERi0xLjQK... (base64-encoded PDF)"
},
"successful": true
}
```
---
### Download Quote
**GET** `/sales/opportunities/:identifier/quote/:quoteId/download`
Download a committed quote PDF by its ID. Returns the PDF file as a base64-encoded string. Each call automatically records a download entry with the timestamp, user info, and fetch action in the quote's `downloads` array. Download-time metadata (downloadedAt, downloadedBy) is also injected into the PDF's document properties.
**Authentication Required:** Yes
**Required Permissions:** `sales.opportunity.quote.download`
**Path Parameters:**
- `identifier` — Internal ID (cuid) or ConnectWise opportunity ID (numeric)
- `quoteId` — The generated quote's internal ID (uuid)
**Query Parameters:**
- `fetchAction` **(required)** — The action being performed. Must be one of: `download`, `print`. Tracked in the download record for audit purposes.
**Response:**
```json
{
"status": 200,
"message": "Quote downloaded successfully!",
"data": {
"id": "a1b2c3d4-e5f6-...",
"quoteFileName": "OPP-12345-2026-03-06T12-00-00-000Z.pdf",
"mimeType": "application/pdf",
"contentBase64": "JVBERi0xLjQK... (base64-encoded PDF)"
},
"successful": true
}
```
**Download Record Shape (stored in `downloads` JSON array):**
```json
{
"downloadedAt": "2026-03-06T15:30:00.000Z",
"fetchAction": "download",
"userId": "clx9user456...",
"userName": "John Roberts",
"userEmail": "jroberts@example.com"
}
```
**Error Responses:**
- `400` — Missing or invalid `fetchAction` query parameter.
- `404` — Generated quote not found.
---
### Fetch Quote Download History
**GET** `/sales/opportunities/:identifier/quotes/downloads`
Fetch download/print history for all committed quotes on an opportunity. Returns an array of quote summaries, each containing the full `downloads` array with every download/print record. This is an admin-level route intended for audit and tracking purposes.
**Authentication Required:** Yes
**Required Permissions:** `sales.opportunity.quote.fetch_downloads`
**Path Parameters:**
- `identifier` — Internal ID (cuid) or ConnectWise opportunity ID (numeric)
**Response:**
```json
{
"status": 200,
"message": "Download logs fetched successfully!",
"data": [
{
"quoteId": "a1b2c3d4-e5f6-...",
"quoteFileName": "OPP-12345-2026-03-06T12-00-00-000Z.pdf",
"createdById": "clx9user123...",
"createdAt": "2026-03-06T12:00:00.000Z",
"downloads": [
{
"downloadedAt": "2026-03-06T15:30:00.000Z",
"fetchAction": "download",
"userId": "clx9user456...",
"userName": "John Roberts",
"userEmail": "jroberts@example.com"
},
{
"downloadedAt": "2026-03-06T16:00:00.000Z",
"fetchAction": "print",
"userId": "clx9user789...",
"userName": "Jane Smith",
"userEmail": "jsmith@example.com"
}
]
}
],
"successful": true
}
```
---
### Get Opportunity Notes
**GET** `/sales/opportunities/:identifier/notes`