3 * Handles OAuth2 authentication and API interactions with Pax8
4 * Multi-tenant aware - each instance is bound to a specific tenant
7const axios = require('axios');
8const pool = require('./db');
10const PAX8_API_BASE = process.env.PAX8_API_BASE || 'https://api.pax8.com/v1';
11const PAX8_AUTH_URL = process.env.PAX8_AUTH_URL || 'https://api.pax8.com/v1/oauth/token';
21 constructor(tenantId) {
22 this.tenantId = tenantId;
24 this.accessToken = null;
25 this.tokenExpiry = null;
29 * Load Pax8 configuration for the tenant
32 const result = await pool.query(
33 'SELECT * FROM pax8_config WHERE tenant_id = $1 AND is_active = true',
37 if (result.rows.length === 0) {
38 throw new Error('Pax8 configuration not found for this tenant. Please configure Pax8 API credentials first.');
41 this.config = result.rows[0];
46 * Get OAuth2 access token (cached for performance)
47 * Automatically refreshes when expired
49 async getAccessToken() {
50 // Return cached token if still valid (with 60s buffer)
51 if (this.accessToken && this.tokenExpiry && Date.now() < this.tokenExpiry) {
52 return this.accessToken;
56 await this.loadConfig();
59 console.log(`[Pax8] Requesting new access token for tenant ${this.tenantId}`);
62 // TODO: Decrypt client_secret before using
63 const clientSecret = this.config.client_secret_encrypted; // Implement decryption
65 const response = await axios.post(PAX8_AUTH_URL,
67 grant_type: 'client_credentials',
68 client_id: this.config.client_id,
69 client_secret: clientSecret
73 'Content-Type': 'application/json'
78 this.accessToken = response.data.access_token;
79 // Set expiry with 60 second buffer
80 this.tokenExpiry = Date.now() + (response.data.expires_in * 1000) - 60000;
82 console.log(`[Pax8] Access token acquired, expires in ${response.data.expires_in}s`);
84 return this.accessToken;
86 console.error('[Pax8] OAuth2 authentication failed:', error.response?.data || error.message);
87 throw new Error(`Pax8 authentication failed: ${error.response?.data?.error_description || error.message}`);
92 * Make authenticated API request to Pax8
93 * @param {string} method - HTTP method (GET, POST, PATCH, DELETE)
94 * @param {string} endpoint - API endpoint (e.g., '/companies')
95 * @param {object} data - Request body data (for POST/PATCH)
96 * @param {object} params - Query parameters
98 async apiRequest(method, endpoint, data = null, params = null) {
99 const token = await this.getAccessToken();
103 url: `${PAX8_API_BASE}${endpoint}`,
105 'Authorization': `Bearer ${token}`,
106 'Content-Type': 'application/json'
115 config.params = params;
119 const response = await axios(config);
120 return response.data;
122 const errorDetail = error.response?.data || error.message;
123 console.error(`[Pax8] API Error (${method} ${endpoint}):`, errorDetail);
125 // Throw with more context
126 throw new Error(`Pax8 API error: ${JSON.stringify(errorDetail)}`);
130 // ========================================
131 // Company (Customer) Management
132 // ========================================
135 * List all companies (customers) in Pax8
136 * @param {object} filters - Optional filters (page, size, etc.)
138 async listCompanies(filters = {}) {
139 return this.apiRequest('GET', '/companies', null, filters);
144 * @param {string} pax8CompanyId - Pax8 company ID
146 async getCompany(pax8CompanyId) {
147 return this.apiRequest('GET', `/companies/${pax8CompanyId}`);
151 * Create a new company in Pax8
152 * @param {object} companyData - Company details
154 async createCompany(companyData) {
155 return this.apiRequest('POST', '/companies', companyData);
159 * Update company information
160 * @param {string} pax8CompanyId - Pax8 company ID
161 * @param {object} updates - Updated company data
163 async updateCompany(pax8CompanyId, updates) {
164 return this.apiRequest('PATCH', `/companies/${pax8CompanyId}`, updates);
167 // ========================================
168 // Product Management
169 // ========================================
172 * List available products (SKUs)
173 * @param {object} filters - Filters like vendor, category, etc.
175 async listProducts(filters = {}) {
176 return this.apiRequest('GET', '/products', null, filters);
180 * Get product details by ID
181 * @param {string} productId - Pax8 product ID
183 async getProduct(productId) {
184 return this.apiRequest('GET', `/products/${productId}`);
189 * @param {string} searchTerm - Search term
191 async searchProducts(searchTerm) {
192 return this.apiRequest('GET', '/products', null, { search: searchTerm });
195 // ========================================
196 // Subscription Management
197 // ========================================
201 * @param {object} filters - Optional filters (companyId, status, etc.)
203 async listSubscriptions(filters = {}) {
204 return this.apiRequest('GET', '/subscriptions', null, filters);
208 * Get subscriptions for a specific company
209 * @param {string} pax8CompanyId - Pax8 company ID
211 async getCompanySubscriptions(pax8CompanyId) {
212 return this.apiRequest('GET', '/subscriptions', null, { companyId: pax8CompanyId });
216 * Get subscription by ID
217 * @param {string} pax8SubscriptionId - Pax8 subscription ID
219 async getSubscription(pax8SubscriptionId) {
220 return this.apiRequest('GET', `/subscriptions/${pax8SubscriptionId}`);
224 * Create a new subscription (purchase licenses)
225 * @param {object} subscriptionData - Subscription details
228 * companyId: 'company-id',
229 * productId: 'product-id',
231 * billingTerm: 'monthly'
234 async createSubscription(subscriptionData) {
235 return this.apiRequest('POST', '/subscriptions', subscriptionData);
239 * Update subscription (change quantity, billing term, etc.)
240 * @param {string} pax8SubscriptionId - Pax8 subscription ID
241 * @param {object} updates - Updates to apply
243 async updateSubscription(pax8SubscriptionId, updates) {
244 return this.apiRequest('PATCH', `/subscriptions/${pax8SubscriptionId}`, updates);
248 * Cancel subscription
249 * @param {string} pax8SubscriptionId - Pax8 subscription ID
251 async cancelSubscription(pax8SubscriptionId) {
252 return this.apiRequest('DELETE', `/subscriptions/${pax8SubscriptionId}`);
255 // ========================================
257 // ========================================
260 * Test API connection
261 * @returns {boolean} True if connection successful
263 async testConnection() {
265 await this.getAccessToken();
266 // Try to list companies to verify access
267 await this.listCompanies({ size: 1 });
270 console.error('[Pax8] Connection test failed:', error.message);
276 * Update last sync status in database
277 * @param {string} status - 'success', 'failed', 'in_progress'
278 * @param {string} error - Error message (if failed)
280 async updateSyncStatus(status, error = null) {
283 SET last_sync_at = NOW(), last_sync_status = $1, sync_error = $2
284 WHERE tenant_id = $3`,
285 [status, error, this.tenantId]
290module.exports = Pax8Service;