/** * One-time migration: legacy Participant (single `event` ref) → tenantOrganization + eventMemberships[]. * Merges legacy rows that share the same (organization + email) into one document. * * Usage (from backend/): node scripts/migrate-participants-multi-event.js */ require('dotenv').config(); const mongoose = require('mongoose'); const Event = require('../src/models/Event'); const LEGACY_COLLECTION = 'participants'; function pickIdentity(doc) { const keys = [ 'email', 'firstName', 'lastName', 'displayName', 'phone', 'organization', 'designation', 'customFields', 'payment', 'notes', 'emailVerified', ]; const out = {}; for (const k of keys) { if (doc[k] !== undefined) out[k] = doc[k]; } return out; } function mergeIdentity(base, next) { const out = { ...base }; for (const [k, v] of Object.entries(next)) { if (v === undefined || v === null) continue; if (typeof v === 'string' && !v.trim()) continue; if (out[k] === undefined || out[k] === null || out[k] === '') { out[k] = v; } } return out; } async function main() { const uri = process.env.MONGODB_URI || process.env.MONGO_URI; if (!uri) { console.error('Set MONGODB_URI or MONGO_URI'); process.exit(1); } await mongoose.connect(uri); const coll = mongoose.connection.db.collection(LEGACY_COLLECTION); const legacy = await coll .find({ event: { $exists: true, $ne: null }, $or: [{ eventMemberships: { $exists: false } }, { eventMemberships: { $size: 0 } }], }) .toArray(); console.log(`Found ${legacy.length} legacy participant documents`); /** @type {Map} */ const emailGroups = new Map(); const singles = []; for (const doc of legacy) { const ev = await Event.findById(doc.event).lean(); if (!ev?.organization) { console.warn('Skipping participant (no event org)', String(doc._id)); continue; } const membership = { event: doc.event, status: doc.status || 'pending', checkedIn: Boolean(doc.checkedIn), checkedInAt: doc.checkedInAt || undefined, guestToken: doc.guestToken || undefined, registrationDate: doc.registrationDate || doc.createdAt || new Date(), tags: Array.isArray(doc.tags) ? [...doc.tags] : [], }; const email = String(doc.email || '').trim().toLowerCase(); if (!email) { singles.push({ docId: doc._id, tenantOrganization: ev.organization, membership, identity: pickIdentity(doc), }); continue; } const key = `${String(ev.organization)}|${email}`; if (!emailGroups.has(key)) { emailGroups.set(key, { keepId: doc._id, docIds: [], memberships: [], identity: pickIdentity(doc), }); } const g = emailGroups.get(key); g.docIds.push(doc._id); g.memberships.push(membership); g.identity = mergeIdentity(g.identity, pickIdentity(doc)); if (String(doc._id) < String(g.keepId)) g.keepId = doc._id; } for (const [, g] of emailGroups) { const ev0 = await Event.findById(g.memberships[0].event).lean(); const tenantOrganization = ev0?.organization; if (!tenantOrganization) continue; const byEvent = new Map(); for (const m of g.memberships) { byEvent.set(String(m.event), m); } const eventMemberships = [...byEvent.values()]; await coll.updateOne( { _id: g.keepId }, { $set: { ...g.identity, email: String(g.identity.email || '').trim().toLowerCase(), tenantOrganization, eventMemberships, }, $unset: { event: '', status: '', checkedIn: '', checkedInAt: '', guestToken: '', registrationDate: '', tags: '', }, } ); const others = g.docIds.filter((id) => String(id) !== String(g.keepId)); if (others.length) { await coll.deleteMany({ _id: { $in: others } }); console.log('Merged duplicates:', others.length, 'removed; kept', String(g.keepId)); } } for (const s of singles) { await coll.updateOne( { _id: s.docId }, { $set: { ...s.identity, tenantOrganization: s.tenantOrganization, eventMemberships: [s.membership], }, $unset: { event: '', status: '', checkedIn: '', checkedInAt: '', guestToken: '', registrationDate: '', tags: '', }, } ); } console.log('Done. Email groups:', emailGroups.size, 'guest rows:', singles.length); await mongoose.disconnect(); } main().catch((e) => { console.error(e); process.exit(1); });