// services/enrichment/redMatcher.js // // Unified RED player matching for AV enrichment. // This is the single source of truth for finding which RED player (if any) // corresponds to a given MDM/AV device (mainly the MSC* ones). // // Consolidates logic previously duplicated in: // - deviceService.js (enrichMdmDevices, with special ae/offline/aerie cases) // - avDeviceBuilder.js (enrichDomainData, with translate + includes) // - avEnricher.js (findRedMatch, which was weaker for dotted DeviceIDs) // // Uses translateREDDeviceID (and normalizePlayerName via barrel) from normalizers. // Extracted as part of Option A incremental consolidation. import { normalizePlayerName, translateREDDeviceID } from './normalizers.js'; /** * Find the best matching full RED player object for a source identifier. * Identifier can be a friendlyName, device.identifier, etc. (e.g. "US002477MSCOFF" * or "US002477MSCAE"). * * Handles RED DeviceIDs in formats like: * - "US.OFFLINE.2477" -> matches US002477MSCOFF devices * - "US.AE.1234" -> matches US001234MSCAE * - "US.AERIE.999" * - Also falls back to Name field contains, and direct matches. * * @param {string} identifier * @param {Array} redList - raw players from getREDStatusForStore (full objects) * @returns {Object|null} matching RED player (full data, including AvailabilityStatus etc.) or null */ export function findBestRedMatch(identifier, redList) { if (!identifier || !Array.isArray(redList) || redList.length === 0) { return null; } // Canonical form for the AV/MDM side (e.g. US002477MSCOFF) const idUpper = normalizePlayerName(identifier).toUpperCase().trim(); if (!idUpper) return null; // Helper: does hay contain id as a "whole" token (not as prefix of longer brand like MSCAE inside MSCAERIE) function containsWhole(id, hay) { if (!hay || !id) return false; let idx = -1; while ((idx = hay.indexOf(id, idx + 1)) !== -1) { const after = hay[idx + id.length]; if (after === undefined || !/[A-Z0-9]/.test(after)) { return true; } } return false; } // Strategy 1 (preferred for translated cases): Translate RED DeviceID using the canonical translator and exact match. // This is the reliable path for OFFLINE/AE/AERIE branded players (US.OFFLINE.2477 etc.) // and covers all the previous ad-hoc special cases in deviceService. for (const redPlayer of redList) { const translated = translateREDDeviceID(redPlayer.DeviceID); if (translated && translated.toUpperCase() === idUpper) { return redPlayer; } } // Strategy 2: Direct "whole token" match against RED's DeviceID or Name (preserves original builder heuristic, // but avoids false positives on overlapping suffixes like MSCAE vs MSCAERIE). let match = redList.find(p => { const did = (p.DeviceID || '').toUpperCase(); const nm = (p.Name || '').toUpperCase(); return containsWhole(idUpper, did) || containsWhole(idUpper, nm); }); if (match) { return match; } // Strategy 3: Fallback using normalizePlayerName on the RED side too (exact after norm). // This preserves (and may slightly improve) the previous avEnricher findRedMatch behavior // for RED records that are already in a "clean" form. For dotted DeviceIDs this alone // would have missed before (normalize on "US.OFFLINE.2477" gives "US002477"), but // Strategy 2 will have caught it. const targetLower = idUpper.toLowerCase(); match = redList.find(r => { const fromRed = normalizePlayerName(r.DeviceID || r.Name || '').toLowerCase().trim(); return fromRed === targetLower; }); if (match) { return match; } return null; } /** * Backward-compat alias (used internally by avEnricher etc. during migration). * New code should prefer findBestRedMatch. */ export function findRedMatch(name, redList) { return findBestRedMatch(name, redList); }