// src/integrations/atlas/devices.js import { atlasGet } from './client.js'; import { logger } from '../../utils/logger.js'; // In-memory cache (→ replace with node-cache/redis later if scale needed) let cachedDeviceList = []; // full list from /organization/devices let lastCacheTime = 0; const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour – adjust based on how often devices change /** * Refresh full organization device list (called by cron) * Handles pagination (API defaults to 100 items/page, max per_page=100; uses ?page=N and next_page in body) */ export async function refreshAtlasDevicesCache() { const start = Date.now(); logger('atlas:devices', 'Refreshing full device cache', 'debug'); try { let allItems = []; let page = 1; let hasMore = true; const PAGE_SIZE = 100; while (hasMore) { const data = await atlasGet('/organization/devices', { page, per_page: PAGE_SIZE }); const items = Array.isArray(data.items) ? data.items : []; allItems = allItems.concat(items); const nextPage = data.next_page; hasMore = !!nextPage && items.length > 0; logger('atlas:devices', `Page ${page}: ${items.length} items (running total ${allItems.length}) next_page=${nextPage}`, 'debug'); if (items.length < PAGE_SIZE) { hasMore = false; } if (hasMore) { page = Number(nextPage) || (page + 1); await new Promise(r => setTimeout(r, 80)); // be nice between pages } if (page > 100) { // hard safety logger('atlas:devices', 'Safety stop: exceeded 100 pages'); break; } } cachedDeviceList = allItems; lastCacheTime = Date.now(); logger('atlas:devices', `Cached ${cachedDeviceList.length} devices (${Date.now() - start} ms)`, 'debug'); } catch (err) { logger('atlas:devices', `Cache refresh failed: ${err.message}`); // Keep old cache if possible } } /** * Get cached device list (auto-refresh if stale/empty) */ export async function getAtlasDeviceList(forceRefresh = false) { const now = Date.now(); if (forceRefresh || !cachedDeviceList.length || (now - lastCacheTime > CACHE_TTL_MS)) { await refreshAtlasDevicesCache(); } return cachedDeviceList; } /** * Find devices matching a store number (name contains 6-digit padded storeNum, e.g. "002477" in "US002477AMP") * @param {string|number} storeNumber e.g. "2477", 305 or "000305" * @returns {Promise} matching device summaries (name, id, status, etc.) */ export async function findAtlasDevicesForStore(storeNumber) { // Always use 6-digit zero-padded form for name matching in Atlas (e.g. 2477 → 002477, 305 → 000305). // Raw/short numbers like "305" can substring-match unrelated devices (e.g. "00305x"), causing // multiple/incorrect Atlas AMP results. Names follow US00NNNNAMP pattern. const padded = String(storeNumber).trim().padStart(6, '0'); logger('atlas:devices', `Looking up Atlas devices for store ${storeNumber} (padded ${padded})`, 'debug'); const devices = await getAtlasDeviceList(); const matches = devices.filter(dev => { const name = String(dev.name || '').toUpperCase(); return name.includes(padded); }); logger('atlas:findDevices', `Searched for store ${padded} → Found ${matches.length} Atlas AMP device(s)`, 'debug'); if (matches.length > 0) { logger('atlas:findDevices', `Matched: ${matches.map(m => m.name).join(', ')}`, 'debug'); } return matches; } /** * Get detailed info for a single Atlas device by its ID * @param {string} deviceId * @returns {Promise} */ export async function getAtlasDeviceDetail(deviceId) { if (!deviceId) return null; const start = Date.now(); logger('atlas:detail', `Fetching detail for device ${deviceId}`, 'debug'); try { const data = await atlasGet(`/organization/devices/${deviceId}`); logger('atlas:detail', `Detail fetched (${Date.now() - start} ms)`, 'debug'); return data; } catch (err) { logger('atlas:detail', `Failed for ${deviceId}: ${err.message}`); return null; } } /** * Convenience: Get detailed device(s) for a store (find → fetch detail) * Returns array (usually 0–1 items in practice) */ export async function getAtlasDeviceForStore(storeNumber) { const candidates = await findAtlasDevicesForStore(storeNumber); if (candidates.length === 0) { return []; } // Fetch details for all matches (parallel) const details = await Promise.allSettled( candidates.map(c => getAtlasDeviceDetail(c.id)) ); const successful = details .filter(r => r.status === 'fulfilled') .map(r => r.value) .filter(Boolean); if (successful.length === 0) { logger('atlas:getForStore', `No successful detail fetches for store ${storeNumber}`); } return successful; }