const fetch = require('node-fetch'); class APIClient { constructor(baseUrl = process.env.API_URL || 'https://api.thinklink.ai') { this.baseUrl = baseUrl; this._refreshInFlight = null; } async request(endpoint, options = {}) { const url = `${this.baseUrl}${endpoint}`; try { const response = await fetch(url, { method: options.method || 'POST', headers: { 'Content-Type': 'application/json', ...(options.headers || {}) }, body: options.body ? JSON.stringify(options.body) : undefined }); const data = await response.json(); return data; } catch (error) { console.error(`API request failed: ${endpoint}`, error.message); return { error: true, message: `Network error: ${error.message}` }; } } async login(email, password, recaptchaToken) { return this.request('/user/login', { body: { login: email, password: password, 'g-recaptcha-response': recaptchaToken } }); } async getBots(token) { return this.request('/bot/list', { body: { token: token } }); } async getOrders(token, botId, afterId = 0, options = {}) { const body = { token: token, botId: parseInt(botId, 10), afterId: afterId || 0, limit: options.limit || 50, includeCanceled: options.includeCanceled || false }; if (options.orderStatus) { body.orderStatus = options.orderStatus; } if (options.sinceTs) { body.sinceTs = options.sinceTs; } return this.request('/food-order/orders', { body }); } async modifyOrder(token, botId, orderId, action, cancellationReason = '') { const body = { token: token, botId: parseInt(botId, 10), orderId: parseInt(orderId, 10), action: action }; if (action === 'cancel' && cancellationReason) { body.cancellationReason = cancellationReason; } return this.request('/food-order/modify', { body }); } async refreshToken(token) { return this.request('/user/refresh-token', { body: { token } }); } async getAbandonedCalls(token, botId, options = {}) { const body = { token, botId: parseInt(botId, 10) || 0, limit: options.limit || 50, offset: options.offset || 0 }; if (options.stage) body.stage = options.stage; if (options.priority) body.priority = options.priority; if (options.startDate) body.startDate = options.startDate; if (options.endDate) body.endDate = options.endDate; return this.request('/abandoned-calls/list', { body }); } async getAbandonedCallbackQueue(token, botId, limit = 20, offset = 0) { return this.request('/abandoned-calls/callback-queue', { body: { token, botId: parseInt(botId, 10) || 0, limit, offset } }); } async updateAbandonedCallback(token, abandonedCallId, action, notes = '') { return this.request('/abandoned-calls/update-callback', { body: { token, abandonedCallId: parseInt(abandonedCallId, 10), action, notes } }); } async getAbandonedCallMetrics(token, botId, startDate = 0, endDate = 0) { return this.request('/abandoned-calls/metrics', { body: { token, botId: parseInt(botId, 10) || 0, startDate, endDate } }); } isTokenExpired(expirationDate) { if (!expirationDate) return true; const expiry = new Date(expirationDate); if (Number.isNaN(expiry.getTime())) return true; const now = new Date(); return now >= expiry; } isTokenNearExpiry(expirationDate, thresholdDays = 7) { if (!expirationDate) return true; const expiry = new Date(expirationDate); if (Number.isNaN(expiry.getTime())) return true; const now = new Date(); const msRemaining = expiry.getTime() - now.getTime(); const thresholdMs = thresholdDays * 24 * 60 * 60 * 1000; return msRemaining < thresholdMs; } async ensureValidToken() { const config = require('./config'); const token = config.get('authToken'); const expiry = config.get('tokenExpiry'); if (!token || !expiry) return null; if (!this.isTokenNearExpiry(expiry, 7)) return token; if (this._refreshInFlight) { return this._refreshInFlight; } this._refreshInFlight = (async () => { // Re-read in case another caller refreshed between checks. const currentToken = config.get('authToken'); const currentExpiry = config.get('tokenExpiry'); if (!currentToken || !currentExpiry) return null; if (!this.isTokenNearExpiry(currentExpiry, 7)) return currentToken; console.log('Token near expiry or expired, attempting refresh...'); try { const result = await this.refreshToken(currentToken); if (result.error) { // If another concurrent refresh already updated config, don't treat this as fatal. const latestToken = config.get('authToken'); const latestExpiry = config.get('tokenExpiry'); if (latestToken && latestToken !== currentToken && !this.isTokenExpired(latestExpiry)) { return latestToken; } console.error('Token refresh failed:', result.message); return null; } config.set('previousAuthToken', currentToken); config.set('authToken', result.token); config.set('tokenExpiry', result.expirationDate); console.log('Token refreshed successfully, new expiry:', result.expirationDate); return result.token; } catch (error) { console.error('Token refresh error:', error.message); return null; } })(); try { return await this._refreshInFlight; } finally { this._refreshInFlight = null; } } } module.exports = new APIClient();