From 120cb5fadd5165317c20e66741174681d561a4f5 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Fri, 23 Jan 2026 13:46:06 +0100 Subject: [PATCH] Refactor and reintroduce character name enrichment logic in FalukantService - Moved the enrichNotificationsWithCharacterNames function back into the FalukantService class, ensuring character names are correctly parsed and attached to notifications. - Implemented a comprehensive ID collection and resolution mechanism for character references, enhancing the accuracy of character name assignments. - Improved error handling during JSON parsing and ID resolution to maintain robustness in notification processing. --- backend/services/falukantService.js | 376 ++++++++++++++-------------- 1 file changed, 188 insertions(+), 188 deletions(-) diff --git a/backend/services/falukantService.js b/backend/services/falukantService.js index 7fbeb17..2bf5968 100644 --- a/backend/services/falukantService.js +++ b/backend/services/falukantService.js @@ -6093,194 +6093,6 @@ class FalukantService extends BaseService { all: mapped }; } -} - -export default new FalukantService(); - -// Helper: parse notifications for character references and attach characterName -async function enrichNotificationsWithCharacterNames(notifications) { - if (!Array.isArray(notifications) || notifications.length === 0) return; - - const charIds = new Set(); - const firstNameIds = new Set(); - const lastNameIds = new Set(); - - // recursive collector that extracts any character id fields and name IDs - function collectIds(obj) { - if (!obj) return; - if (Array.isArray(obj)) { - for (const it of obj) collectIds(it); - return; - } - if (typeof obj !== 'object') return; - for (const [k, v] of Object.entries(obj)) { - if (!v) continue; - if (k === 'character_id' || k === 'characterId') { - charIds.add(Number(v)); - continue; - } - // Sammle first_name und last_name IDs - if (k === 'character_first_name' && !isNaN(Number(v))) { - firstNameIds.add(Number(v)); - continue; - } - if (k === 'character_last_name' && !isNaN(Number(v))) { - lastNameIds.add(Number(v)); - continue; - } - if (k === 'character' && typeof v === 'object') { - if (v.id) charIds.add(Number(v.id)); - if (v.character_id) charIds.add(Number(v.character_id)); - if (v.characterId) charIds.add(Number(v.characterId)); - collectIds(v); - continue; - } - collectIds(v); - } - } - - // First pass: collect all referenced character ids from notifications - for (const n of notifications) { - // parse n.tr if it's JSON - try { - if (typeof n.tr === 'string' && n.tr.trim().startsWith('{')) { - const parsed = JSON.parse(n.tr); - collectIds(parsed); - } - } catch (err) { /* ignore */ } - - // parse n.effects if present - try { - if (n.effects) { - let eff = n.effects; - if (typeof eff === 'string') { - eff = JSON.parse(eff); - } - collectIds(eff); - } - } catch (err) { /* ignore */ } - - // top-level fields - if (n.character_id) charIds.add(Number(n.character_id)); - if (n.characterId) charIds.add(Number(n.characterId)); - } - - // Batch load first names and last names - const [firstNames, lastNames] = await Promise.all([ - firstNameIds.size > 0 - ? FalukantPredefineFirstname.findAll({ where: { id: { [Op.in]: Array.from(firstNameIds) } }, attributes: ['id', 'name'] }) - : [], - lastNameIds.size > 0 - ? FalukantPredefineLastname.findAll({ where: { id: { [Op.in]: Array.from(lastNameIds) } }, attributes: ['id', 'name'] }) - : [] - ]); - const firstNameMap = new Map(firstNames.map(fn => [fn.id, fn.name])); - const lastNameMap = new Map(lastNames.map(ln => [ln.id, ln.name])); - - const ids = Array.from(charIds).filter(Boolean); - - // Batch load characters and their display names - const characters = ids.length > 0 ? await FalukantCharacter.findAll({ - where: { id: { [Op.in]: ids } }, - include: [ - { model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] }, - { model: FalukantPredefineLastname, as: 'definedLastName', attributes: ['name'] } - ], - attributes: ['id'] - }) : []; - - const nameMap = new Map(); - for (const c of characters) { - const first = c.definedFirstName?.name || ''; - const last = c.definedLastName?.name || ''; - const display = `${first} ${last}`.trim() || null; - nameMap.set(Number(c.id), display || `#${c.id}`); - } - - // helper to find first character id in an object - function findFirstId(obj) { - if (!obj) return null; - if (Array.isArray(obj)) { - for (const it of obj) { - const r = findFirstId(it); - if (r) return r; - } - return null; - } - if (typeof obj !== 'object') return null; - for (const [k, v] of Object.entries(obj)) { - if (!v) continue; - if (k === 'character_id' || k === 'characterId') return Number(v); - if (k === 'character' && typeof v === 'object') { - if (v.id) return Number(v.id); - const r = findFirstId(v); - if (r) return r; - } - const r = findFirstId(v); - if (r) return r; - } - return null; - } - - // Helper: resolve name IDs in an object and build characterName - function resolveNameFromObject(obj) { - if (!obj || typeof obj !== 'object') return null; - const firstNameId = obj.character_first_name; - const lastNameId = obj.character_last_name; - if (firstNameId || lastNameId) { - const first = firstNameMap.get(Number(firstNameId)) || ''; - const last = lastNameMap.get(Number(lastNameId)) || ''; - const name = `${first} ${last}`.trim(); - if (name) return name; - } - return null; - } - - // Attach resolved name to notifications (set both characterName and character_name) - for (const n of notifications) { - let foundId = null; - let resolvedName = null; - - try { - if (typeof n.tr === 'string' && n.tr.trim().startsWith('{')) { - const parsed = JSON.parse(n.tr); - foundId = findFirstId(parsed) || foundId; - // Versuche Namen aus first_name/last_name IDs aufzulösen - resolvedName = resolveNameFromObject(parsed.value || parsed) || resolvedName; - } - } catch (err) { /* ignore */ } - - try { - if (n.effects) { - let eff = n.effects; - if (typeof eff === 'string') { - eff = JSON.parse(eff); - } - foundId = findFirstId(eff) || foundId; - // Auch in effects nach Namen suchen - if (Array.isArray(eff)) { - for (const e of eff) { - resolvedName = resolveNameFromObject(e) || resolvedName; - } - } else { - resolvedName = resolveNameFromObject(eff) || resolvedName; - } - } - } catch (err) { /* ignore */ } - - if (!foundId && n.character_id) foundId = Number(n.character_id); - if (!foundId && n.characterId) foundId = Number(n.characterId); - - // Priorität: aufgelöster Name aus IDs > Name aus character_id > Fallback - if (resolvedName) { - n.characterName = resolvedName; - n.character_name = resolvedName; - } else if (foundId && nameMap.has(Number(foundId))) { - const resolved = nameMap.get(Number(foundId)); - n.characterName = resolved; - n.character_name = resolved; - } - } // ==================== Church Career Methods ==================== @@ -6836,3 +6648,191 @@ async function enrichNotificationsWithCharacterNames(notifications) { return !!churchOffice; } } + +export default new FalukantService(); + +// Helper: parse notifications for character references and attach characterName +async function enrichNotificationsWithCharacterNames(notifications) { + if (!Array.isArray(notifications) || notifications.length === 0) return; + + const charIds = new Set(); + const firstNameIds = new Set(); + const lastNameIds = new Set(); + + // recursive collector that extracts any character id fields and name IDs + function collectIds(obj) { + if (!obj) return; + if (Array.isArray(obj)) { + for (const it of obj) collectIds(it); + return; + } + if (typeof obj !== 'object') return; + for (const [k, v] of Object.entries(obj)) { + if (!v) continue; + if (k === 'character_id' || k === 'characterId') { + charIds.add(Number(v)); + continue; + } + // Sammle first_name und last_name IDs + if (k === 'character_first_name' && !isNaN(Number(v))) { + firstNameIds.add(Number(v)); + continue; + } + if (k === 'character_last_name' && !isNaN(Number(v))) { + lastNameIds.add(Number(v)); + continue; + } + if (k === 'character' && typeof v === 'object') { + if (v.id) charIds.add(Number(v.id)); + if (v.character_id) charIds.add(Number(v.character_id)); + if (v.characterId) charIds.add(Number(v.characterId)); + collectIds(v); + continue; + } + collectIds(v); + } + } + + // First pass: collect all referenced character ids from notifications + for (const n of notifications) { + // parse n.tr if it's JSON + try { + if (typeof n.tr === 'string' && n.tr.trim().startsWith('{')) { + const parsed = JSON.parse(n.tr); + collectIds(parsed); + } + } catch (err) { /* ignore */ } + + // parse n.effects if present + try { + if (n.effects) { + let eff = n.effects; + if (typeof eff === 'string') { + eff = JSON.parse(eff); + } + collectIds(eff); + } + } catch (err) { /* ignore */ } + + // top-level fields + if (n.character_id) charIds.add(Number(n.character_id)); + if (n.characterId) charIds.add(Number(n.characterId)); + } + + // Batch load first names and last names + const [firstNames, lastNames] = await Promise.all([ + firstNameIds.size > 0 + ? FalukantPredefineFirstname.findAll({ where: { id: { [Op.in]: Array.from(firstNameIds) } }, attributes: ['id', 'name'] }) + : [], + lastNameIds.size > 0 + ? FalukantPredefineLastname.findAll({ where: { id: { [Op.in]: Array.from(lastNameIds) } }, attributes: ['id', 'name'] }) + : [] + ]); + const firstNameMap = new Map(firstNames.map(fn => [fn.id, fn.name])); + const lastNameMap = new Map(lastNames.map(ln => [ln.id, ln.name])); + + const ids = Array.from(charIds).filter(Boolean); + + // Batch load characters and their display names + const characters = ids.length > 0 ? await FalukantCharacter.findAll({ + where: { id: { [Op.in]: ids } }, + include: [ + { model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] }, + { model: FalukantPredefineLastname, as: 'definedLastName', attributes: ['name'] } + ], + attributes: ['id'] + }) : []; + + const nameMap = new Map(); + for (const c of characters) { + const first = c.definedFirstName?.name || ''; + const last = c.definedLastName?.name || ''; + const display = `${first} ${last}`.trim() || null; + nameMap.set(Number(c.id), display || `#${c.id}`); + } + + // helper to find first character id in an object + function findFirstId(obj) { + if (!obj) return null; + if (Array.isArray(obj)) { + for (const it of obj) { + const r = findFirstId(it); + if (r) return r; + } + return null; + } + if (typeof obj !== 'object') return null; + for (const [k, v] of Object.entries(obj)) { + if (!v) continue; + if (k === 'character_id' || k === 'characterId') return Number(v); + if (k === 'character' && typeof v === 'object') { + if (v.id) return Number(v.id); + const r = findFirstId(v); + if (r) return r; + } + const r = findFirstId(v); + if (r) return r; + } + return null; + } + + // Helper: resolve name IDs in an object and build characterName + function resolveNameFromObject(obj) { + if (!obj || typeof obj !== 'object') return null; + const firstNameId = obj.character_first_name; + const lastNameId = obj.character_last_name; + if (firstNameId || lastNameId) { + const first = firstNameMap.get(Number(firstNameId)) || ''; + const last = lastNameMap.get(Number(lastNameId)) || ''; + const name = `${first} ${last}`.trim(); + if (name) return name; + } + return null; + } + + // Attach resolved name to notifications (set both characterName and character_name) + for (const n of notifications) { + let foundId = null; + let resolvedName = null; + + try { + if (typeof n.tr === 'string' && n.tr.trim().startsWith('{')) { + const parsed = JSON.parse(n.tr); + foundId = findFirstId(parsed) || foundId; + // Versuche Namen aus first_name/last_name IDs aufzulösen + resolvedName = resolveNameFromObject(parsed.value || parsed) || resolvedName; + } + } catch (err) { /* ignore */ } + + try { + if (n.effects) { + let eff = n.effects; + if (typeof eff === 'string') { + eff = JSON.parse(eff); + } + foundId = findFirstId(eff) || foundId; + // Auch in effects nach Namen suchen + if (Array.isArray(eff)) { + for (const e of eff) { + resolvedName = resolveNameFromObject(e) || resolvedName; + } + } else { + resolvedName = resolveNameFromObject(eff) || resolvedName; + } + } + } catch (err) { /* ignore */ } + + if (!foundId && n.character_id) foundId = Number(n.character_id); + if (!foundId && n.characterId) foundId = Number(n.characterId); + + // Priorität: aufgelöster Name aus IDs > Name aus character_id > Fallback + if (resolvedName) { + n.characterName = resolvedName; + n.character_name = resolvedName; + } else if (foundId && nameMap.has(Number(foundId))) { + const resolved = nameMap.get(Number(foundId)); + n.characterName = resolved; + n.character_name = resolved; + } + } +}