UNPKG

@dispatch9/client-sdk

Version:

Official Node.js SDK for Dispatch9 API - Complete solution with email/phone client creation, order management, client management, and dual-method authentication

1,033 lines (942 loc) 44.8 kB
const axios = require('axios'); /** * Dispatch9 API Client * Official Node.js SDK for Dispatch9 API * * Core Operations: * - Create Orders * - Update Orders * - Create Clients * - Update Clients */ class Dispatch9Client { /** * Create a new Dispatch9 client instance * @param {Object} config - Configuration object * @param {string} config.apiKey - Your API key (required) * @param {string} [config.baseURL='https://api.dispatch9.com'] - Base API URL * @param {number} [config.timeout=30000] - Request timeout in milliseconds * @param {Object} [config.headers] - Additional headers to send with requests * @param {boolean} [config.debug=false] - Enable debug logging */ constructor(config) { if (!config || !config.apiKey) { throw new Error('API key is required. Get one from your Dispatch9 dashboard.'); } this.config = { baseURL: 'https://api.dispatch9.com', timeout: 30000, debug: false, ...config }; // Create axios instance this.http = axios.create({ baseURL: this.config.baseURL, timeout: this.config.timeout, headers: { 'Content-Type': 'application/json', 'X-API-Key': this.config.apiKey, 'User-Agent': 'Dispatch9-NodeSDK/1.0.0', ...this.config.headers } }); // Add request interceptor for logging this.http.interceptors.request.use( (config) => { if (this.config.debug) { console.log(`[D9 SDK] ${config.method.toUpperCase()} ${config.url}`); } return config; }, (error) => Promise.reject(error) ); // Add response interceptor for error handling this.http.interceptors.response.use( (response) => response, (error) => { if (error.response) { // Server responded with error status const { status, data } = error.response; const message = data?.message || `HTTP ${status} Error`; switch (status) { case 401: throw new Error(`Authentication failed: ${message}. Check your API key.`); case 403: throw new Error(`Access forbidden: ${message}. Check your permissions.`); case 404: throw new Error(`Resource not found: ${message}`); case 429: throw new Error(`Rate limit exceeded: ${message}. Please slow down your requests.`); default: throw new Error(`API Error (${status}): ${message}`); } } else if (error.request) { // Request was made but no response received throw new Error('No response from server. Check your internet connection.'); } else { // Something else happened throw new Error(`Request failed: ${error.message}`); } } ); } /** * Make a GET request * @private */ async _get(endpoint, params = {}) { const response = await this.http.get(endpoint, { params }); return response.data; } /** * Make a POST request * @private */ async _post(endpoint, data = {}) { const response = await this.http.post(endpoint, data); return response.data; } /** * Make a PATCH request * @private */ async _patch(endpoint, data = {}) { const response = await this.http.patch(endpoint, data); return response.data; } // ========================================== // ORDER MANAGEMENT // ========================================== /** * Create a new order * @param {Object} orderData - Order data * * REQUIRED FIELDS: * @param {number} orderData.orderTotal - Total order amount (required, min: 0) * @param {string} orderData.client - Client ID (required, must be valid ObjectId) * * OPTIONAL FIELDS: * @param {string} [orderData.orderNumber] - Custom order number * @param {string} [orderData.orderCurrency='USD'] - Order currency * @param {boolean} [orderData.isPaid=false] - Payment status * @param {boolean} [orderData.hasGoods=false] - Order contains goods/items * @param {boolean} [orderData.hasServices=false] - Order contains services * @param {boolean} [orderData.hasWorkers=false] - Order requires worker transport * @param {number} [orderData.priority=0] - Order priority (0-10) * @param {boolean} [orderData.autoAssign=false] - Auto-assign to workers * @param {string} [orderData.manualAssignWorker] - Worker ID for manual assignment (ObjectId) * @param {string} [orderData.status='created'] - Order status (created, assigned, in_progress, completed, failed, partially_completed) * @param {number} [orderData.completeAfter=0] - Earliest completion timestamp * @param {number} [orderData.completeBefore=0] - Latest completion timestamp * @param {string} [orderData.pickupLocation] - Pickup address ID (ObjectId - must be created first via address API) * @param {string} [orderData.deliveryLocation] - Delivery address ID (ObjectId - must be created first via address API) * @param {string} [orderData.serviceLocation] - Service address ID (ObjectId - must be created first via address API, required if hasServices=true) * @param {string} [orderData.specialInstructions] - Special delivery instructions * @param {string} [orderData.customerNotes] - Customer notes * @param {Object} [orderData.metadata] - Additional metadata * @param {boolean} [orderData.isRecurring=false] - Is this a recurring order * @param {Object} [orderData.recurringSettings] - Recurring settings (required if isRecurring=true) * @param {Object} [orderData.requiredProof] - Proof of delivery requirements * @param {boolean} [orderData.requiredProof.signature=false] - Require signature * @param {boolean} [orderData.requiredProof.photo=false] - Require photo * * CONDITIONAL REQUIRED FIELDS: * @param {Array} [orderData.items] - Items array (required if hasGoods=true) * @param {Array} [orderData.services] - Services array (required if hasServices=true) * @param {Array} [orderData.workers] - Workers array (required if hasWorkers=true) * * ITEM SCHEMA (when hasGoods=true): * @param {string} orderData.items[].SKU - Item SKU (required) * @param {string} orderData.items[].itemName - Item name (required) * @param {number} orderData.items[].price - Item price (required, min: 0) * @param {number} orderData.items[].quantity - Quantity (required, min: 1) * @param {string} [orderData.items[].category] - Item category * @param {string} [orderData.items[].description] - Item description * @param {string} [orderData.items[].currency='USD'] - Item currency * @param {number} [orderData.items[].weight] - Item weight * @param {string} [orderData.items[].weightUnit='kg'] - Weight unit (kg, lb, g, oz) * @param {number} [orderData.items[].dimensionH] - Height * @param {number} [orderData.items[].dimensionW] - Width * @param {number} [orderData.items[].dimensionL] - Length * @param {string} [orderData.items[].dimensionUnit='cm'] - Dimension unit (cm, in) * @param {string} [orderData.items[].packaging] - Packaging requirements * @param {string} [orderData.items[].handling] - Handling instructions * @param {string} [orderData.items[].notes] - Item notes * * SERVICE SCHEMA (when hasServices=true): * @param {string} orderData.services[].serviceCode - Service code (required) * @param {string} orderData.services[].serviceName - Service name (required) * @param {string} orderData.services[].category - Service category (required): cleaning, maintenance, repair, installation, inspection, other * @param {string} [orderData.services[].description] - Service description * @param {number} [orderData.services[].estimatedDuration=60] - Estimated duration in minutes * @param {number} orderData.services[].workersRequired - Number of workers required (required, min: 1, max: 10, default: 1) * @param {number} orderData.services[].price - Service price (required, min: 0) * @param {string} [orderData.services[].currency='USD'] - Service currency * @param {Array} [orderData.services[].requirements] - Special equipment, materials, or skills required * @param {string} [orderData.services[].notes] - Service notes or special instructions * * @returns {Promise<Object>} Created order */ async createOrder(orderData) { // Validate required fields if (!orderData) throw new Error('Order data is required'); if (typeof orderData.orderTotal === 'undefined' || orderData.orderTotal < 0) { throw new Error('orderTotal is required and must be >= 0'); } if (!orderData.client) throw new Error('client ID is required'); // Validate conditional requirements if (orderData.hasGoods && (!orderData.items || !Array.isArray(orderData.items) || orderData.items.length === 0)) { throw new Error('items array is required when hasGoods=true'); } if (orderData.hasServices && (!orderData.services || !Array.isArray(orderData.services) || orderData.services.length === 0)) { throw new Error('services array is required when hasServices=true'); } if (orderData.hasWorkers && (!orderData.workers || !Array.isArray(orderData.workers) || orderData.workers.length === 0)) { throw new Error('workers array is required when hasWorkers=true'); } if (orderData.hasServices && !orderData.serviceLocation) { throw new Error('serviceLocation is required when hasServices=true'); } if (orderData.isRecurring && !orderData.recurringSettings) { throw new Error('recurringSettings is required when isRecurring=true'); } // Validate items if provided if (orderData.items && Array.isArray(orderData.items)) { orderData.items.forEach((item, index) => { if (!item.SKU) throw new Error(`items[${index}].SKU is required`); if (!item.itemName) throw new Error(`items[${index}].itemName is required`); if (typeof item.price === 'undefined' || item.price < 0) { throw new Error(`items[${index}].price is required and must be >= 0`); } if (!item.quantity || item.quantity < 1) { throw new Error(`items[${index}].quantity is required and must be >= 1`); } }); } // Validate services if provided if (orderData.services && Array.isArray(orderData.services)) { orderData.services.forEach((service, index) => { if (!service.serviceCode) throw new Error(`services[${index}].serviceCode is required`); if (!service.serviceName) throw new Error(`services[${index}].serviceName is required`); if (!service.category) throw new Error(`services[${index}].category is required`); if (typeof service.category !== 'string') { throw new Error(`services[${index}].category must be a string`); } if (typeof service.price === 'undefined' || service.price < 0) { throw new Error(`services[${index}].price is required and must be >= 0`); } // Validate requirements array if provided if (service.requirements !== undefined) { if (typeof service.requirements === 'string') { throw new Error(`services[${index}].requirements must be an array, not a string. If you're passing JSON, parse it first with JSON.parse()`); } if (!Array.isArray(service.requirements)) { throw new Error(`services[${index}].requirements must be an array`); } service.requirements.forEach((req, reqIndex) => { if (typeof req === 'object' && req !== null) { if (!req.type) { throw new Error(`services[${index}].requirements[${reqIndex}].type is required for object requirements`); } if (!['workers', 'tools', 'qualifications', 'equipment'].includes(req.type)) { throw new Error(`services[${index}].requirements[${reqIndex}].type must be one of: workers, tools, qualifications, equipment`); } if (req.type === 'workers') { const workerCount = req.count || req.workers || req.numberOfWorkers; if (!workerCount || workerCount < 1 || workerCount > 10) { throw new Error(`services[${index}].requirements[${reqIndex}] worker count must be between 1 and 10`); } } } }); } }); } return this._post('/v1/orders', orderData); } /** * Update an existing order * @param {string} orderId - Order ID (required, must be valid ObjectId) * @param {Object} updateData - Data to update (at least one field required) * * ALL FIELDS ARE OPTIONAL (but at least one must be provided): * @param {string} [updateData.orderNumber] - Custom order number * @param {boolean} [updateData.hasGoods] - Order contains goods/items * @param {boolean} [updateData.hasServices] - Order contains services * @param {boolean} [updateData.hasWorkers] - Order requires worker transport * @param {number} [updateData.priority] - Order priority (0-10) * @param {boolean} [updateData.autoAssign] - Auto-assign to workers * @param {string} [updateData.status] - Order status (created, confirmed, in_progress, completed, cancelled, partially_completed) * @param {Array} [updateData.items] - Items array (follows same schema as createOrder) * @param {Array} [updateData.services] - Services array * @param {Array} [updateData.workers] - Workers array * @param {string} [updateData.pickupLocation] - Pickup address ID (ObjectId - must be created first via address API) * @param {string} [updateData.deliveryLocation] - Delivery address ID (ObjectId - must be created first via address API) * @param {string} [updateData.serviceLocation] - Service address ID (ObjectId - must be created first via address API) * @param {string} [updateData.specialInstructions] - Special delivery instructions * @param {string} [updateData.customerNotes] - Customer notes * @param {string} [updateData.statusNotes] - Status notes * @param {Object} [updateData.metadata] - Additional metadata * @param {boolean} [updateData.isRecurring] - Is this a recurring order * @param {Object} [updateData.recurringSettings] - Recurring settings * * ITEM SCHEMA (when updating items): * @param {string} updateData.items[].SKU - Item SKU (required) * @param {string} updateData.items[].itemName - Item name (required) * @param {number} updateData.items[].price - Item price (required, min: 0) * @param {number} updateData.items[].quantity - Quantity (required, min: 1) * @param {string} [updateData.items[].category] - Item category * @param {string} [updateData.items[].description] - Item description * @param {string} [updateData.items[].currency='USD'] - Item currency * @param {number} [updateData.items[].weight] - Item weight * @param {string} [updateData.items[].weightUnit='kg'] - Weight unit (kg, lb, g, oz) * @param {number} [updateData.items[].dimensionH] - Height * @param {number} [updateData.items[].dimensionW] - Width * @param {number} [updateData.items[].dimensionL] - Length * @param {string} [updateData.items[].dimensionUnit='cm'] - Dimension unit (cm, in) * @param {string} [updateData.items[].packaging] - Packaging requirements * @param {string} [updateData.items[].handling] - Handling instructions * @param {string} [updateData.items[].notes] - Item notes * * SERVICE SCHEMA (when updating services): * @param {string} updateData.services[].serviceCode - Service code (required) * @param {string} updateData.services[].serviceName - Service name (required) * @param {string} updateData.services[].category - Service category (required): cleaning, maintenance, repair, installation, inspection, other * @param {string} [updateData.services[].description] - Service description * @param {number} [updateData.services[].estimatedDuration=60] - Estimated duration in minutes * @param {number} updateData.services[].workersRequired - Number of workers required (required, min: 1, max: 10, default: 1) * @param {number} updateData.services[].price - Service price (required, min: 0) * @param {string} [updateData.services[].currency='USD'] - Service currency * @param {Array} [updateData.services[].requirements] - Special equipment, materials, or skills required * @param {string} [updateData.services[].notes] - Service notes or special instructions * * @returns {Promise<Object>} Updated order */ async updateOrder(orderId, updateData) { // Validate required parameters if (!orderId) throw new Error('orderId is required'); if (!updateData || Object.keys(updateData).length === 0) { throw new Error('updateData is required and must contain at least one field to update'); } // Validate priority if provided if (typeof updateData.priority !== 'undefined' && (updateData.priority < 0 || updateData.priority > 10)) { throw new Error('priority must be between 0 and 10'); } // Validate status if provided if (updateData.status && !['created', 'confirmed', 'in_progress', 'completed', 'cancelled', 'partially_completed'].includes(updateData.status)) { throw new Error('status must be one of: created, confirmed, in_progress, completed, cancelled, partially_completed'); } // Validate items if provided if (updateData.items && Array.isArray(updateData.items)) { updateData.items.forEach((item, index) => { if (!item.SKU) throw new Error(`items[${index}].SKU is required`); if (!item.itemName) throw new Error(`items[${index}].itemName is required`); if (typeof item.price === 'undefined' || item.price < 0) { throw new Error(`items[${index}].price is required and must be >= 0`); } if (!item.quantity || item.quantity < 1) { throw new Error(`items[${index}].quantity is required and must be >= 1`); } }); } // Validate services if provided if (updateData.services && Array.isArray(updateData.services)) { updateData.services.forEach((service, index) => { if (!service.serviceCode) throw new Error(`services[${index}].serviceCode is required`); if (!service.serviceName) throw new Error(`services[${index}].serviceName is required`); if (!service.category) throw new Error(`services[${index}].category is required`); if (typeof service.category !== 'string') { throw new Error(`services[${index}].category must be a string`); } if (typeof service.price === 'undefined' || service.price < 0) { throw new Error(`services[${index}].price is required and must be >= 0`); } // Validate requirements array if provided if (service.requirements !== undefined) { if (typeof service.requirements === 'string') { throw new Error(`services[${index}].requirements must be an array, not a string. If you're passing JSON, parse it first with JSON.parse()`); } if (!Array.isArray(service.requirements)) { throw new Error(`services[${index}].requirements must be an array`); } service.requirements.forEach((req, reqIndex) => { if (typeof req === 'object' && req !== null) { if (!req.type) { throw new Error(`services[${index}].requirements[${reqIndex}].type is required for object requirements`); } if (!['workers', 'tools', 'qualifications', 'equipment'].includes(req.type)) { throw new Error(`services[${index}].requirements[${reqIndex}].type must be one of: workers, tools, qualifications, equipment`); } if (req.type === 'workers') { const workerCount = req.count || req.workers || req.numberOfWorkers; if (!workerCount || workerCount < 1 || workerCount > 10) { throw new Error(`services[${index}].requirements[${reqIndex}] worker count must be between 1 and 10`); } } } }); } }); } return this._patch(`/v1/orders/${orderId}`, updateData); } /** * Get a specific order by ID * @param {string} orderId - Order ID (required, must be valid ObjectId) * @param {Object} [options={}] - Query options (all optional) * @param {string} [options.populate] - Comma-separated list of fields to populate * Available fields: client, pickupLocation, deliveryLocation, serviceLocation, workers.worker * @param {boolean} [options.includeJobs] - Include associated jobs in response * * @returns {Promise<Object>} Order details * @throws {Error} If orderId is invalid or order not found * * @example * // Get basic order details * const order = await dispatch9.getOrderById('507f1f77bcf86cd799439011'); * * @example * // Get order with populated client and location details * const orderWithDetails = await dispatch9.getOrderById('507f1f77bcf86cd799439011', { * populate: 'client,pickupLocation,deliveryLocation,serviceLocation' * }); * * @example * // Get order with associated jobs * const orderWithJobs = await dispatch9.getOrderById('507f1f77bcf86cd799439011', { * includeJobs: true * }); */ async getOrderById(orderId, options = {}) { // Validate required fields if (!orderId) { throw new Error('orderId is required'); } if (typeof orderId !== 'string') { throw new Error('orderId must be a string'); } // Validate ObjectId format (24 character hex string) if (!/^[0-9a-fA-F]{24}$/.test(orderId)) { throw new Error('orderId must be a valid ObjectId (24 character hex string)'); } // Build query parameters const params = new URLSearchParams(); if (options.populate) { if (typeof options.populate !== 'string') { throw new Error('populate must be a string'); } params.append('populate', options.populate); } if (options.includeJobs) { if (typeof options.includeJobs !== 'boolean') { throw new Error('includeJobs must be a boolean'); } params.append('includeJobs', options.includeJobs.toString()); } const queryString = params.toString(); const url = `/v1/orders/${orderId}${queryString ? `?${queryString}` : ''}`; return this._get(url); } // ========================================== // CLIENT MANAGEMENT // ========================================== /** * Get clients with optional filtering and pagination * @param {Object} [options={}] - Query options (all optional) * @param {string} [options.name] - Filter by client name * @param {string} [options.status] - Filter by status (active, inactive, suspended) * @param {string} [options.businessType] - Filter by business type (restaurant, retail, grocery, pharmacy, other) * @param {string} [options.sortBy] - Sort field * @param {number} [options.limit] - Items per page * @param {number} [options.page] - Page number * @returns {Promise<Object>} Clients response with pagination */ async getClients(options = {}) { return this._get('/v1/clients', options); } /** * Create a new client * @param {Object} clientData - Client data * * REQUIRED FIELDS: * @param {string} clientData.name - Client/business name (required, min: 1 character) * @param {string} [clientData.email] - Client email (optional, must be valid email if provided) * @param {string} [clientData.phone] - Client phone number (optional, must be valid phone if provided) * * NOTE: At least one of email OR phone is required for client creation * * OPTIONAL FIELDS: * @param {string} [clientData.websiteURL] - Website URL (must be valid URI) * @param {string} [clientData.logoURL] - Logo URL (must be valid URI) * @param {string} [clientData.businessType] - Business type (restaurant, retail, grocery, pharmacy, other) * @param {string} [clientData.taxId] - Tax identification number * @param {string} [clientData.address] - Address ID (ObjectId) * @param {string} [clientData.webhookURL] - Webhook URL for notifications (must be valid URI) * * OPTIONAL OBJECTS: * @param {Object} [clientData.apiConfig] - API configuration * @param {boolean} [clientData.apiConfig.enabled=true] - Enable API access * @param {number} [clientData.apiConfig.rateLimit=1000] - API rate limit (min: 1) * * @param {Array} [clientData.integrations] - Third-party integrations * @param {string} clientData.integrations[].platform - Platform name (required if integrations provided) * @param {boolean} [clientData.integrations[].enabled=false] - Integration enabled * @param {Object} [clientData.integrations[].config={}] - Integration configuration * @param {string} [clientData.integrations[].webhookSecret] - Webhook secret * @param {boolean} [clientData.integrations[].syncOrders=true] - Sync orders * * @param {Object} [clientData.orderSettings] - Order settings * @param {boolean} [clientData.orderSettings.autoAccept=false] - Auto-accept orders * @param {boolean} [clientData.orderSettings.autoAssign=false] - Auto-assign workers * @param {number} [clientData.orderSettings.maxOrdersPerHour=50] - Max orders per hour (min: 1) * @param {number} [clientData.orderSettings.preparationTime=15] - Preparation time in minutes (min: 1) * @param {number} [clientData.orderSettings.deliveryRadius=10] - Delivery radius (min: 0) * * @param {Object} [clientData.permissions] - Client permissions * @param {boolean} [clientData.permissions.modify=false] - Can modify settings * @param {boolean} [clientData.permissions.delete=false] - Can delete data * @param {boolean} [clientData.permissions.createOrders=true] - Can create orders * @param {boolean} [clientData.permissions.viewOrders=true] - Can view orders * * @param {Object} [clientData.authentication] - Authentication settings * @param {boolean} clientData.authentication.enablePortalAccess - Enable portal access (required if authentication provided) * @param {string} [clientData.authentication.phone] - Phone number for authentication * @param {string} [clientData.authentication.password] - Password for authentication * @param {string} [clientData.authentication.firstName] - First name * @param {string} [clientData.authentication.lastName] - Last name * @param {string} [clientData.authentication.businessName] - Business name * * @returns {Promise<Object>} Created client * * @example * // Create client with email * const clientWithEmail = await dispatch9.createClient({ * name: 'Acme Corporation', * email: 'contact@acmecorp.com', * businessType: 'retail', * websiteURL: 'https://www.acmecorp.com' * }); * * @example * // Create client with phone number only * const clientWithPhone = await dispatch9.createClient({ * name: 'Mobile Business', * phone: '+1234567890', * businessType: 'restaurant' * }); * * @example * // Create client with both email and phone * const clientWithBoth = await dispatch9.createClient({ * name: 'Full Contact Business', * email: 'info@business.com', * phone: '+1234567890', * businessType: 'grocery', * authentication: { * enablePortalAccess: true, * password: 'securePassword123', * firstName: 'John', * lastName: 'Doe' * } * }); */ async createClient(clientData) { // Validate required fields if (!clientData) throw new Error('Client data is required'); if (!clientData.name || clientData.name.trim().length === 0) { throw new Error('name is required and must be at least 1 character'); } // Validate that at least one of email or phone is provided if (!clientData.email && !clientData.phone) { throw new Error('Either email or phone number is required'); } // Validate email format if provided if (clientData.email) { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(clientData.email)) { throw new Error('email must be a valid email address'); } } // Validate phone format if provided if (clientData.phone) { const phoneRegex = /^\+?[1-9]\d{1,14}$/; const cleanPhone = clientData.phone.replace(/[\s\-\(\)]/g, ''); if (!phoneRegex.test(cleanPhone)) { throw new Error('phone must be a valid phone number (e.g., +1234567890)'); } } // Validate business type if provided if (clientData.businessType && !['restaurant', 'retail', 'grocery', 'pharmacy', 'other'].includes(clientData.businessType)) { throw new Error('businessType must be one of: restaurant, retail, grocery, pharmacy, other'); } // Validate URLs if provided if (clientData.websiteURL && clientData.websiteURL.trim() && !this._isValidUrl(clientData.websiteURL)) { throw new Error('websiteURL must be a valid URI'); } if (clientData.logoURL && clientData.logoURL.trim() && !this._isValidUrl(clientData.logoURL)) { throw new Error('logoURL must be a valid URI'); } if (clientData.webhookURL && clientData.webhookURL.trim() && !this._isValidUrl(clientData.webhookURL)) { throw new Error('webhookURL must be a valid URI'); } // Validate integrations if provided if (clientData.integrations && Array.isArray(clientData.integrations)) { clientData.integrations.forEach((integration, index) => { if (!integration.platform) { throw new Error(`integrations[${index}].platform is required`); } }); } // Validate authentication if provided if (clientData.authentication && typeof clientData.authentication.enablePortalAccess === 'undefined') { throw new Error('authentication.enablePortalAccess is required when authentication is provided'); } return this._post('/v1/clients', clientData); } /** * Update an existing client * @param {string} clientId - Client ID (required, must be valid ObjectId) * @param {Object} updateData - Data to update (at least one field required) * * ALL FIELDS ARE OPTIONAL (but at least one must be provided): * @param {string} [updateData.name] - Client name (min: 1 character) * @param {string} [updateData.email] - Client email (must be valid email) * @param {string} [updateData.websiteURL] - Website URL (must be valid URI) * @param {string} [updateData.logoURL] - Logo URL (must be valid URI) * @param {string} [updateData.status] - Status (active, inactive, suspended) * @param {string} [updateData.businessType] - Business type (restaurant, retail, grocery, pharmacy, other) * @param {string} [updateData.taxId] - Tax ID * @param {string} [updateData.address] - Address ID (ObjectId) * @param {string} [updateData.contactName] - Contact name (min: 1 character) * @param {string} [updateData.contactPhone] - Contact phone (min: 10 characters) * @param {Object} [updateData.timeWindow] - Time window * @param {Date} [updateData.timeWindow.start] - Start time * @param {Date} [updateData.timeWindow.end] - End time * @param {string} [updateData.webhookURL] - Webhook URL (must be valid URI) * * @param {Object} [updateData.apiConfig] - API configuration * @param {boolean} [updateData.apiConfig.enabled] - Enable API access * @param {number} [updateData.apiConfig.rateLimit] - API rate limit (min: 1) * * @param {Object} [updateData.permissions] - Client permissions * @param {boolean} [updateData.permissions.modify] - Can modify settings * @param {boolean} [updateData.permissions.delete] - Can delete data * @param {boolean} [updateData.permissions.createOrders] - Can create orders * @param {boolean} [updateData.permissions.viewOrders] - Can view orders * * @param {Object} [updateData.orderSettings] - Order settings * @param {boolean} [updateData.orderSettings.autoAccept] - Auto-accept orders * @param {boolean} [updateData.orderSettings.autoAssign] - Auto-assign workers * @param {number} [updateData.orderSettings.maxOrdersPerHour] - Max orders per hour (min: 1) * @param {number} [updateData.orderSettings.preparationTime] - Preparation time (min: 1) * @param {number} [updateData.orderSettings.deliveryRadius] - Delivery radius (min: 0) * * @returns {Promise<Object>} Updated client */ async updateClient(clientId, updateData) { // Validate required parameters if (!clientId) throw new Error('clientId is required'); if (!updateData || Object.keys(updateData).length === 0) { throw new Error('updateData is required and must contain at least one field to update'); } // Validate email format if provided if (updateData.email && !this._isValidEmail(updateData.email)) { throw new Error('email must be a valid email address'); } // Validate business type if provided if (updateData.businessType && !['restaurant', 'retail', 'grocery', 'pharmacy', 'other'].includes(updateData.businessType)) { throw new Error('businessType must be one of: restaurant, retail, grocery, pharmacy, other'); } // Validate status if provided if (updateData.status && !['active', 'inactive', 'suspended'].includes(updateData.status)) { throw new Error('status must be one of: active, inactive, suspended'); } // Validate URLs if provided if (updateData.websiteURL && updateData.websiteURL.trim() && !this._isValidUrl(updateData.websiteURL)) { throw new Error('websiteURL must be a valid URI'); } if (updateData.logoURL && updateData.logoURL.trim() && !this._isValidUrl(updateData.logoURL)) { throw new Error('logoURL must be a valid URI'); } if (updateData.webhookURL && updateData.webhookURL.trim() && !this._isValidUrl(updateData.webhookURL)) { throw new Error('webhookURL must be a valid URI'); } // Validate string length requirements if (updateData.name !== undefined && updateData.name.trim().length === 0) { throw new Error('name must be at least 1 character'); } if (updateData.contactName !== undefined && updateData.contactName.trim().length === 0) { throw new Error('contactName must be at least 1 character'); } if (updateData.contactPhone !== undefined && updateData.contactPhone.trim().length < 10) { throw new Error('contactPhone must be at least 10 characters'); } // Validate numeric constraints if (updateData.apiConfig?.rateLimit !== undefined && updateData.apiConfig.rateLimit < 1) { throw new Error('apiConfig.rateLimit must be at least 1'); } if (updateData.orderSettings?.maxOrdersPerHour !== undefined && updateData.orderSettings.maxOrdersPerHour < 1) { throw new Error('orderSettings.maxOrdersPerHour must be at least 1'); } if (updateData.orderSettings?.preparationTime !== undefined && updateData.orderSettings.preparationTime < 1) { throw new Error('orderSettings.preparationTime must be at least 1'); } if (updateData.orderSettings?.deliveryRadius !== undefined && updateData.orderSettings.deliveryRadius < 0) { throw new Error('orderSettings.deliveryRadius must be at least 0'); } return this._patch(`/v1/clients/${clientId}`, updateData); } /** * Get a specific client by ID * @param {string} clientId - Client ID (required, must be valid ObjectId) * @param {Object} [options={}] - Query options (all optional) * @param {string} [options.populate] - Comma-separated list of fields to populate * Available fields: addresses, primaryAddress, billingAddress * @param {boolean} [options.includeStats] - Include client statistics (order count, total revenue, etc.) * * @returns {Promise<Object>} Client details * @throws {Error} If clientId is invalid or client not found * * @example * // Get basic client details * const client = await dispatch9.getClientById('507f1f77bcf86cd799439011'); * * @example * // Get client with populated address details * const clientWithAddresses = await dispatch9.getClientById('507f1f77bcf86cd799439011', { * populate: 'addresses,primaryAddress,billingAddress' * }); * * @example * // Get client with statistics * const clientWithStats = await dispatch9.getClientById('507f1f77bcf86cd799439011', { * includeStats: true * }); */ async getClientById(clientId, options = {}) { // Validate required fields if (!clientId) { throw new Error('clientId is required'); } if (typeof clientId !== 'string') { throw new Error('clientId must be a string'); } // Validate ObjectId format (24 character hex string) if (!/^[0-9a-fA-F]{24}$/.test(clientId)) { throw new Error('clientId must be a valid ObjectId (24 character hex string)'); } // Build query parameters const params = new URLSearchParams(); if (options.populate) { if (typeof options.populate !== 'string') { throw new Error('populate must be a string'); } params.append('populate', options.populate); } if (options.includeStats) { if (typeof options.includeStats !== 'boolean') { throw new Error('includeStats must be a boolean'); } params.append('includeStats', options.includeStats.toString()); } const queryString = params.toString(); const url = `/v1/clients/${clientId}${queryString ? `?${queryString}` : ''}`; return this._get(url); } // ========================================== // CLIENT AUTHENTICATION // ========================================== /** * Login client with email or phone number * @param {Object} credentials - Login credentials (required) * @param {string} credentials.identifier - Email address or phone number (required) * @param {string} credentials.password - Client password (required) * @param {string} [credentials.providerId] - Provider ID for multi-provider clients (optional) * * @returns {Promise<Object>} Login result with tokens and client data * @throws {Error} If credentials are invalid or login fails * * @example * // Login with email * const result = await dispatch9.loginClient({ * identifier: 'client@example.com', * password: 'securePassword123' * }); * * @example * // Login with phone number * const result = await dispatch9.loginClient({ * identifier: '+1234567890', * password: 'securePassword123' * }); * * @example * // Login with provider selection * const result = await dispatch9.loginClient({ * identifier: 'client@example.com', * password: 'securePassword123', * providerId: '507f1f77bcf86cd799439011' * }); */ async loginClient(credentials) { // Validate required fields if (!credentials) { throw new Error('credentials object is required'); } if (!credentials.identifier) { throw new Error('identifier is required'); } if (typeof credentials.identifier !== 'string') { throw new Error('identifier must be a string'); } if (!credentials.password) { throw new Error('password is required'); } if (typeof credentials.password !== 'string') { throw new Error('password must be a string'); } if (credentials.providerId && typeof credentials.providerId !== 'string') { throw new Error('providerId must be a string'); } // Validate identifier format (email or phone) const isEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(credentials.identifier); const isPhone = /^\+?[1-9]\d{1,14}$/.test(credentials.identifier.replace(/[\s\-\(\)]/g, '')); if (!isEmail && !isPhone) { throw new Error('identifier must be a valid email address or phone number'); } return this._post('/v1/client-auth/login', { identifier: credentials.identifier, password: credentials.password, ...(credentials.providerId && { providerId: credentials.providerId }) }); } /** * Select provider for multi-provider clients * @param {Object} selection - Provider selection data (required) * @param {string} selection.clientId - Client ID (required) * @param {string} selection.providerId - Provider ID to select (required) * * @returns {Promise<Object>} Authentication result with tokens * @throws {Error} If selection data is invalid * * @example * // Select provider after login * const result = await dispatch9.selectClientProvider({ * clientId: '507f1f77bcf86cd799439011', * providerId: '507f1f77bcf86cd799439012' * }); */ async selectClientProvider(selection) { // Validate required fields if (!selection) { throw new Error('selection object is required'); } if (!selection.clientId) { throw new Error('clientId is required'); } if (typeof selection.clientId !== 'string') { throw new Error('clientId must be a string'); } if (!selection.providerId) { throw new Error('providerId is required'); } if (typeof selection.providerId !== 'string') { throw new Error('providerId must be a string'); } // Validate ObjectId format for both IDs const objectIdRegex = /^[0-9a-fA-F]{24}$/; if (!objectIdRegex.test(selection.clientId)) { throw new Error('clientId must be a valid ObjectId (24 character hex string)'); } if (!objectIdRegex.test(selection.providerId)) { throw new Error('providerId must be a valid ObjectId (24 character hex string)'); } return this._post('/v1/client-auth/select-provider', { clientId: selection.clientId, providerId: selection.providerId }); } /** * Verify client login OTP for first-time login * @param {Object} verification - OTP verification data (required) * @param {string} verification.clientId - Client ID (required) * @param {string} verification.otp - OTP code (required, 6 digits) * * @returns {Promise<Object>} Verification result with tokens * @throws {Error} If verification data is invalid * * @example * // Verify OTP for first-time login * const result = await dispatch9.verifyClientLoginOTP({ * clientId: '507f1f77bcf86cd799439011', * otp: '123456' * }); */ async verifyClientLoginOTP(verification) { // Validate required fields if (!verification) { throw new Error('verification object is required'); } if (!verification.clientId) { throw new Error('clientId is required'); } if (typeof verification.clientId !== 'string') { throw new Error('clientId must be a string'); } if (!verification.otp) { throw new Error('otp is required'); } if (typeof verification.otp !== 'string') { throw new Error('otp must be a string'); } // Validate ObjectId format if (!/^[0-9a-fA-F]{24}$/.test(verification.clientId)) { throw new Error('clientId must be a valid ObjectId (24 character hex string)'); } // Validate OTP format (6 digits) if (!/^\d{6}$/.test(verification.otp)) { throw new Error('otp must be a 6-digit number'); } return this._post('/v1/client-auth/verify-login-otp', { clientId: verification.clientId, otp: verification.otp }); } /** * Get current client profile (requires authentication) * @returns {Promise<Object>} Current client profile data * @throws {Error} If not authenticated or request fails * * @example * // Get current client profile * const profile = await dispatch9.getClientProfile(); * console.log(`Welcome ${profile.name}`); */ async getClientProfile() { return this._get('/v1/client-auth/profile'); } // ========================================== // HELPER METHODS // ========================================== /** * Validate email format * @private */ _isValidEmail(email) { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(email); } /** * Validate URL format * @private */ _isValidUrl(url) { try { new URL(url); return true; } catch { return false; } } } module.exports = Dispatch9Client;