const { getStoreLocation, getStoreRegisters, getStorePrinters, getStorePaymentTerminals, } = require('../services/siw'); const { findMerakiNetwork, getMerakiDeviceAvailabilities, getMerakiClients, } = require('../services/meraki'); const { getMDMDevices } = require('../services/mdm'); const { createStore } = require('../models/Store'); const { findMatchingClient, getClientStatus, formatLastSeen, buildMerakiClientLink, } = require('../utils/merakiMatcher'); const { STORE_MODES, MDM_DEVICE_TYPES, filterMdmByType } = require('../constants'); const logger = require('../utils/logger'); function shouldFetchForMode(mode, category) { if (mode === STORE_MODES.IOS) { return ['mdm', 'meraki', 'merakiClients'].includes(category); } if (mode === STORE_MODES.POS) { return ['siw', 'mdm', 'meraki', 'merakiClients'].includes(category); } return true; // default fetches most things } async function getStoreDetail(storeNumber, mode = STORE_MODES.DEFAULT) { logger.info('Starting store analysis', { storeNumber, mode }); let locationData = null, merakiNetwork = null, registers = [], printers = [], paymentTerminals = [], merakiClients = [], mdmDevices = []; try { // Phase 1: basic lookups [locationData, merakiNetwork] = await Promise.all([ shouldFetchForMode(mode, 'location') ? getStoreLocation(storeNumber) : Promise.resolve(null), shouldFetchForMode(mode, 'meraki') ? findMerakiNetwork(storeNumber) : Promise.resolve(null), ]); // Phase 2: heavy data, fanned out in parallel const dataPromises = []; if (shouldFetchForMode(mode, 'siw')) { dataPromises.push( getStoreRegisters(storeNumber), getStorePrinters(storeNumber), getStorePaymentTerminals(storeNumber) ); } else { dataPromises.push(Promise.resolve([]), Promise.resolve([]), Promise.resolve([])); } dataPromises.push( merakiNetwork && shouldFetchForMode(mode, 'merakiClients') ? getMerakiClients(merakiNetwork.id) : Promise.resolve([]) ); dataPromises.push( shouldFetchForMode(mode, 'mdm') ? getMDMDevices(storeNumber) : Promise.resolve([]) ); const [reg, prn, pay, clients, mdm] = await Promise.all(dataPromises); registers = reg || []; printers = prn || []; paymentTerminals = pay || []; merakiClients = clients || []; mdmDevices = mdm || []; logger.info('Store data fetched', { mode }); } catch (err) { logger.error('Error fetching store data', { error: err.message }); } let report = ''; if (mode === STORE_MODES.DEFAULT) { report += createStore(locationData, storeNumber).toSummary(); report += await buildActiveNetworkDevices(merakiNetwork); report += buildStoreServers(mdmDevices, merakiClients, merakiNetwork); } else if (mode === STORE_MODES.POS) { report += buildStoreServers(mdmDevices, merakiClients, merakiNetwork); report += buildRegisters(registers, merakiClients, merakiNetwork); report += buildMobileRegisters(mdmDevices, merakiClients, merakiNetwork); report += buildPrinters(printers, merakiClients, merakiNetwork); report += buildPaymentTerminals(paymentTerminals, merakiClients, merakiNetwork); } else if (mode === STORE_MODES.IOS) { report += buildIOSDevices(mdmDevices, merakiClients, merakiNetwork); } return report || 'No matching data for this view.'; } // === Section Builders === /** * Render a single "device → matched Meraki client" line. * `prefixParts` is rendered before the status; `identifiers` is passed * straight through to findMatchingClient. */ function renderClientLine({ prefixParts, identifiers, merakiClients, merakiNetwork }) { const match = findMatchingClient(merakiClients, identifiers); const status = getClientStatus(match); const lastSeen = formatLastSeen(match?.lastSeen); const recentDevice = match?.recentDeviceName ? ` via ${match.recentDeviceName}` : ''; const connection = match?.recentDeviceConnection ? ` (${match.recentDeviceConnection})` : ''; const link = match ? ` → [Meraki Client](${buildMerakiClientLink(merakiNetwork, match)})` : ''; const prefix = prefixParts.filter(Boolean).join(' — '); return `${prefix} — ${status} — ${lastSeen}${recentDevice}${connection}${link}\n`; } async function buildActiveNetworkDevices(merakiNetwork) { if (!merakiNetwork) return '\n⚠️ No matching Meraki network found.\n'; let out = `**🌐 [${merakiNetwork.name}](${merakiNetwork.url})**\n\n`; let activeDevices = []; try { const availabilities = await getMerakiDeviceAvailabilities(merakiNetwork.id); activeDevices = availabilities.filter(d => d.status !== 'dormant'); } catch (e) { logger.error('Device availabilities failed', { error: e.message }); } activeDevices.sort((a, b) => { const nameA = (a.name || '').trim().toUpperCase(); const nameB = (b.name || '').trim().toUpperCase(); if (nameA.includes('SWR') || /SW.*R\d?$/.test(nameA)) return -1; if (nameB.includes('SWR') || /SW.*R\d?$/.test(nameB)) return 1; if (nameA.includes('SWF') || /SW.*F\d/.test(nameA)) return -1; if (nameB.includes('SWF') || /SW.*F\d/.test(nameB)) return 1; return nameA.localeCompare(nameB); }); out += `**🛠️ Active Network Devices (${activeDevices.length})**\n`; activeDevices.forEach(dev => { const statusEmoji = dev.status === 'online' ? '✅' : '⚠️'; const deviceName = (dev.name || dev.serial || 'Unknown').trim(); out += `**${deviceName}** — ${statusEmoji} ${dev.status}`; if (dev.serial && merakiNetwork.url) { const urlParts = merakiNetwork.url.split('/n/'); const networkCode = urlParts[1] ? urlParts[1].split('/')[0] : merakiNetwork.id; let link = `${urlParts[0]}/n/${networkCode}`; if (dev.productType === 'switch') link += `/manage/switches/${dev.serial}/summary`; else if (dev.productType === 'wireless') link += `/manage/access_points/${dev.serial}/summary`; else link += `/manage/${dev.serial}/summary`; out += ` → [Dashboard](${link})`; } out += `\n`; }); return out; } function buildMdmSection({ devices, marker, title, fallbackName, merakiClients, merakiNetwork, formatPrefix, }) { const filtered = filterMdmByType(devices, marker); if (filtered.length === 0) return ''; let out = `\n**${title} (${filtered.length})**\n`; filtered.forEach(dev => { const name = dev.UserName || dev.DeviceFriendlyName || fallbackName; out += renderClientLine({ prefixParts: formatPrefix ? formatPrefix(name, dev) : [`**${name}**`], identifiers: { UserName: dev.UserName, DeviceFriendlyName: dev.DeviceFriendlyName, name, }, merakiClients, merakiNetwork, }); }); return out; } function buildStoreServers(mdmDevices, merakiClients, merakiNetwork) { return buildMdmSection({ devices: mdmDevices, marker: MDM_DEVICE_TYPES.SERVER, title: '🖥️ Store Servers', fallbackName: 'Unknown Server', merakiClients, merakiNetwork, }); } function buildMobileRegisters(mdmDevices, merakiClients, merakiNetwork) { return buildMdmSection({ devices: mdmDevices, marker: MDM_DEVICE_TYPES.MOBILE_REGISTER, title: '📱 Mobile Registers', fallbackName: 'Unknown Mobile Register', merakiClients, merakiNetwork, }); } function buildIOSDevices(mdmDevices, merakiClients, merakiNetwork) { return buildMdmSection({ devices: mdmDevices, marker: MDM_DEVICE_TYPES.IPHONE, title: '📱 Store iPhones', fallbackName: 'Unknown iPhone', merakiClients, merakiNetwork, }); } function buildRegisters(registers, merakiClients, merakiNetwork) { if (!registers || registers.length === 0) return ''; let out = `\n**🖥️ Store Registers (${registers.length})**\n`; registers.forEach(reg => { const regNum = reg.register_number ? `Register ${reg.register_number}` : 'Register'; const brand = reg.brand_display_name || 'Unknown'; const type = reg.register_type_name || 'Unknown'; const name = (reg.register_display_name || reg.printer_name || 'Unknown').trim(); out += renderClientLine({ prefixParts: [`**${regNum}**`, brand, type], identifiers: { name, register_display_name: name }, merakiClients, merakiNetwork, }); }); return out; } function buildPrinters(printers, merakiClients, merakiNetwork) { const filtered = (printers || []).filter( p => (p.connection_type_name || '').toLowerCase() !== 'usb' ); if (filtered.length === 0) return ''; let out = `\n**🖨️ Store Printers (${filtered.length})**\n`; filtered.forEach(printer => { const name = (printer.printer_name || 'Unknown').trim(); const model = printer.printer_model_name || 'N/A'; out += renderClientLine({ prefixParts: [`**${name}**`, model], identifiers: { name, printer_name: name }, merakiClients, merakiNetwork, }); }); return out; } function buildPaymentTerminals(paymentTerminals, merakiClients, merakiNetwork) { if (!paymentTerminals || paymentTerminals.length === 0) return ''; let out = `\n**💳 Payment Terminals (${paymentTerminals.length})**\n`; paymentTerminals.forEach(term => { const deviceName = term.device_name || 'Unknown Terminal'; const adyenName = term.adyen_device_name || 'N/A'; const model = term.device_model_name || 'N/A'; const type = term.device_type_name || 'N/A'; out += renderClientLine({ prefixParts: [`**${deviceName}**`, adyenName, model, type], identifiers: { deviceName, adyenName, ip_address: term.ip_address }, merakiClients, merakiNetwork, }); }); return out; } module.exports = { getStoreDetail };