Sales & Quotes API (Updated)
The Sales & Quotes API provides comprehensive endpoints for managing draft quotes, processing sales, and generating reports. The system uses a draft-first workflow with automatic validation and atomic transaction processing.
Added in version 2.0: Draft quote system with session-based isolation and automatic clearing after sale processing.
Overview
Key Features:
Draft Quote Management: Session-specific draft quotes that auto-save as items are added
Charge Code Validation: Real-time validation with descriptive error messages
Atomic Processing: Quote-to-sale conversion in a single database transaction
Fractional Quantities: Support for decimal quantities (0.5 units, 1.25 meters, etc.)
Stock Validation: Comprehensive stock checking before processing
Auto-Cleanup: Draft quotes automatically cleared after successful processing
Workflow:
Add items to draft quote →
POST /api/sales/quotesValidate charge code → Automatic validation on process
Process quote →
POST /api/sales/quotes/:id/processDraft automatically cleared → System handles cleanup
Draft Quote Endpoints
Get Current Draft Quote
- GET /api/sales/quotes/current-draft
Retrieves the current draft quote for the authenticated user’s session.
Query Parameters:
sessionId (string, required) – Session identifier for draft isolation
Example Request:
GET /api/sales/quotes/current-draft?sessionId=abc123 HTTP/1.1 Host: localhost:5000 Authorization: Bearer eyJhbGc...
Success Response (200 OK):
{ "id": 123, "quoteId": "Q20251205-001", "chargeCode": "PHYSICS-LAB", "subtotalAmount": "2599.98", "vatAmount": "519.99", "totalAmount": "3119.97", "vatApplied": true, "status": "draft", "items": [ { "id": 1, "itemId": 45, "itemName": "Dell Laptop XPS 13", "itemSku": "DELL-XPS13-001", "quantity": "2.00", "unitPrice": "1299.99", "vatRate": "0.2000", "vatAmount": "519.99", "subtotal": "2599.98", "totalWithVat": "3119.97" } ], "createdAt": "2025-12-05T10:30:00.000Z", "updatedAt": "2025-12-05T10:32:00.000Z" }
Not Found Response (404):
{ "message": "No draft quote found for this session" }
Status Codes:
200 – Draft quote retrieved successfully
404 – No draft quote exists for this session
401 – Unauthorized (not authenticated)
Create or Update Draft Quote
- POST /api/sales/quotes
Creates a new draft quote or updates the existing draft for the session. This endpoint is idempotent - calling it multiple times will update the same draft.
Request Body:
{ "chargeCode": "PHYSICS-LAB", "items": [ { "itemId": 45, "quantity": 2.0 }, { "itemId": 67, "quantity": 0.5 } ], "vatApplied": true, "customerNotes": "For Room 204 lab setup", "sessionId": "abc123" }
Field Descriptions:
chargeCode (string, required) – Valid charge code for billing
items (array, required) – Array of items with quantities
itemId (integer) – Database ID of the item
quantity (decimal) – Quantity to order (supports decimals like 0.5)
vatApplied (boolean, optional) – Whether to apply VAT (default: true)
customerNotes (string, optional) – Additional notes for the order
sessionId (string, required) – Session identifier
Success Response (200 OK):
{ "message": "Draft quote saved successfully", "quote": { "id": 123, "quoteId": "Q20251205-001", "chargeCode": "PHYSICS-LAB", "totalAmount": "3119.97", "status": "draft" } }
Error Response - Invalid Charge Code (400):
{ "message": "Invalid charge code: 'INVALID-CODE' does not exist" }
Status Codes:
200 – Draft quote created/updated successfully
400 – Invalid input (missing fields, invalid charge code)
401 – Unauthorized
Update Draft Quote Charge Code
- PATCH /api/sales/quotes/current-draft/charge-code
Updates only the charge code for the current draft quote.
Request Body:
{ "chargeCode": "CHEMISTRY-LAB", "sessionId": "abc123" }
Success Response (200 OK):
{ "message": "Charge code updated successfully", "chargeCode": "CHEMISTRY-LAB" }
Status Codes:
200 – Charge code updated
400 – Invalid charge code
404 – No draft quote found
Delete Draft Quote
- DELETE /api/sales/quotes/current-draft
Clears the current draft quote from the database and removes all associated items.
Query Parameters:
sessionId (string, required) – Session identifier
Example Request:
DELETE /api/sales/quotes/current-draft?sessionId=abc123 HTTP/1.1 Host: localhost:5000 Authorization: Bearer eyJhbGc...
Success Response (200 OK):
{ "message": "Draft quote deleted successfully" }
Status Codes:
200 – Draft deleted successfully
404 – No draft quote found
401 – Unauthorized
Process Quote (Convert to Sale)
Process Quote Endpoint
- POST /api/sales/quotes/:id/process
Converts a draft quote into a completed sale. This operation:
Validates the charge code (existence, expiration, category restrictions)
Checks stock availability for all items
Creates a permanent sale record
Reduces inventory stock levels atomically
Deletes the draft quote automatically
This is an atomic transaction - either all steps succeed or none do.
Path Parameters:
id (integer) – Quote ID to process
Example Request:
POST /api/sales/quotes/123/process HTTP/1.1 Host: localhost:5000 Authorization: Bearer eyJhbGc...
Success Response (200 OK):
{ "message": "Quote processed successfully", "sale": { "id": 456, "saleId": "SALE-20251205-001", "chargeCode": "PHYSICS-LAB", "subtotalAmount": "2599.98", "vatAmount": "519.99", "totalAmount": "3119.97", "status": "completed", "createdAt": "2025-12-05T10:35:00.000Z", "processedBy": { "id": "user-123", "email": "john.doe@university.edu", "firstName": "John", "lastName": "Doe" } } }
Error Responses:
Missing Charge Code (400):
{ "message": "Quote is missing a charge code. Please edit the quote and add a valid charge code before processing." }
Invalid Charge Code (400):
{ "message": "Invalid charge code: 'DEPT999' does not exist. Available codes include: PHYSICS, CHEMISTRY, BIOLOGY, ENGINEERING" }
Expired Charge Code (400):
{ "message": "Charge code 'OLD-LAB' has expired on 11/30/2024." }
Not Yet Valid (400):
{ "message": "Charge code 'FUTURE-LAB' is not yet valid until 01/15/2026." }
Category Restrictions (400):
{ "message": "Charge code 'RESTRICTED-LAB' cannot be used for some items in this quote due to category restrictions." }
Insufficient Stock (400):
{ "message": "Insufficient stock for item 'Dell Laptop XPS 13'. Current stock: 3, Required: 5" }
Quote Not Found (404):
{ "message": "Quote not found" }
Status Codes:
200 – Quote processed successfully, sale created
400 – Validation failed (charge code, stock, restrictions)
404 – Quote not found
401 – Unauthorized
500 – Server error during processing
Sales Management Endpoints
Get Sales Report
- GET /api/sales/reports
Retrieves sales data with filtering, pagination, and aggregation.
Query Parameters:
page (integer, optional) – Page number (default: 1)
limit (integer, optional) – Results per page (default: 50, max: 100)
chargeCode (string, optional) – Filter by charge code (partial match)
startDate (string, optional) – Start date (ISO 8601 format)
endDate (string, optional) – End date (ISO 8601 format)
unpaidOnly (boolean, optional) – Show only unpaid sales
format (string, optional) – Response format: ‘json’ or ‘csv’ (default: json)
Example Request:
GET /api/sales/reports?page=1&limit=20&chargeCode=PHYSICS&startDate=2025-01-01&endDate=2025-12-31 HTTP/1.1 Host: localhost:5000 Authorization: Bearer eyJhbGc...
Success Response (200 OK):
{ "success": true, "data": { "summary": { "totalSales": 156, "totalAmount": 45678.90, "uniqueChargeCodes": 12 }, "departmentSummary": [ { "department": "PHYSICS-LAB", "totalSales": 45, "totalAmount": 15234.50 }, { "department": "CHEMISTRY-LAB", "totalSales": 38, "totalAmount": 12456.78 } ], "sales": [ { "id": 456, "saleId": "SALE-20251205-001", "chargeCode": "PHYSICS-LAB", "total": 3119.97, "totalAmount": "3119.97", "isPaid": false, "status": "completed", "createdAt": "2025-12-05T10:35:00.000Z", "processedBy": { "firstName": "John", "lastName": "Doe" }, "items": [ { "itemName": "Dell Laptop XPS 13", "itemSku": "DELL-XPS13-001", "quantity": "2.00", "unitPrice": "1299.99" } ] } ], "pagination": { "page": 1, "limit": 20, "total": 156, "totalPages": 8 } } }
CSV Export:
When
format=csv, returns CSV file with headers:Sale ID,Charge Code,Total Amount,Customer Info,Notes,Status,Processed By,Date,Items Count "SALE-20251205-001","PHYSICS-LAB","3119.97","","","completed","John Doe","2025-12-05T10:35:00.000Z","2"
Status Codes:
200 – Report generated successfully
401 – Unauthorized
Mark Sale as Paid
- PATCH /api/sales/:saleId/paid
Updates a sale’s status to ‘paid’. This operation is used for financial reconciliation.
Path Parameters:
saleId (integer) – Sale ID to mark as paid
Example Request:
PATCH /api/sales/456/paid HTTP/1.1 Host: localhost:5000 Authorization: Bearer eyJhbGc...
Success Response (200 OK):
{ "id": 456, "saleId": "SALE-20251205-001", "status": "paid", "isPaid": true, "updatedAt": "2025-12-05T11:00:00.000Z" }
Status Codes:
200 – Sale marked as paid successfully
404 – Sale not found
401 – Unauthorized
500 – Server error
Charge Code Validation
The system performs comprehensive charge code validation when processing quotes:
Validation Checks:
Existence: Charge code must exist in the
chargecodestableDate Range: Must be within
validFromandvalidUntildatesCategory Exclusions: Items must not belong to excluded categories
Active Status: Code must not be marked as inactive
Example Validation Flow:
// 1. Check if charge code exists
const chargeCode = await getChargeCode("PHYSICS-LAB");
if (!chargeCode) {
throw new Error("Invalid charge code: 'PHYSICS-LAB' does not exist");
}
// 2. Check if expired
if (chargeCode.validUntil < new Date()) {
throw new Error("Charge code has expired on " + chargeCode.validUntil);
}
// 3. Check if not yet valid
if (chargeCode.validFrom > new Date()) {
throw new Error("Charge code is not yet valid until " + chargeCode.validFrom);
}
// 4. Check category exclusions
const exclusions = await getChargeCodeExclusions("PHYSICS-LAB");
// Validate items against exclusions...
Error Handling
All endpoints return structured error responses:
Standard Error Format:
{
"message": "Descriptive error message explaining what went wrong",
"error": "Error details (in development mode only)"
}
Common Error Scenarios:
400 Bad Request: Invalid input, validation failure, business logic violation
401 Unauthorized: Missing or invalid authentication token
404 Not Found: Resource doesn’t exist
409 Conflict: Referential integrity violation
500 Internal Server Error: Unexpected server error
Best Practices
Draft Quote Management:
Always use the same sessionId for all draft operations
Clear draft quotes when user logs out or abandons the cart
Validate charge codes before allowing user to process
Stock Management:
Check stock availability before adding items to quote
Support decimal quantities for appropriate item types
Show clear feedback when stock is insufficient
Error Handling:
Display full error messages to users - they contain helpful information
For “Invalid charge code” errors, show the list of suggested codes
Implement retry logic for 500 errors
Performance:
Use pagination for large sales reports
Cache draft quotes on the client side
Implement debouncing for draft quote updates
Security:
Never expose internal IDs in user-facing messages
Validate all user input on the backend
Use parameterized queries for all database operations
Implement rate limiting for API endpoints
Code Examples
TypeScript/React Example - Processing a Quote:
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@/lib/queryClient";
const processQuoteMutation = useMutation({
mutationFn: async (quoteId: number) => {
const response = await apiRequest(
'POST',
`/api/sales/quotes/${quoteId}/process`
);
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || 'Failed to process quote');
}
return response.json();
},
onSuccess: async () => {
// Clear draft quote after successful processing
await clearDraftQuoteMutation.mutateAsync();
// Invalidate related queries
queryClient.invalidateQueries({ queryKey: ["/api/sales/quotes"] });
queryClient.invalidateQueries({ queryKey: ["/api/items"] });
toast({
title: "Quote Processed",
description: "The quote has been processed and converted to a sale.",
});
},
onError: (error: Error) => {
// Display error message from backend
toast({
title: "Cannot Process Quote",
description: error.message,
variant: "destructive",
});
},
});
Node.js/Express Example - Creating a Draft Quote:
app.post('/api/sales/quotes', requireAuth, async (req, res) => {
try {
const { chargeCode, items, sessionId, vatApplied = true } = req.body;
const currentUserId = getCurrentUserId(req);
// Validate charge code
const chargeCodeRecord = await storage.getChargeCode(chargeCode);
if (!chargeCodeRecord) {
return res.status(400).json({
message: `Invalid charge code: '${chargeCode}' does not exist`
});
}
// Create or update draft quote
const quote = await storage.createOrUpdateDraftQuote({
sessionId,
chargeCode,
items,
vatApplied,
createdBy: currentUserId
});
res.json({
message: "Draft quote saved successfully",
quote
});
} catch (error) {
console.error("Error creating draft quote:", error);
res.status(500).json({ message: "Failed to create draft quote" });
}
});
See Also
Sales & Quotes User Guide - User guide for sales and quotes
API Endpoints Reference - Complete API reference
Testing and Quality Assurance - Testing strategies for sales endpoints
Monitoring - Monitoring sales and quote processing