"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.listmonkSyncService = exports.SUPPORT_LEVEL_LIST_MAP = void 0; const env_1 = require("../config/env"); const database_1 = require("../config/database"); const logger_1 = require("../utils/logger"); const listmonk_client_1 = require("./listmonk.client"); // --- List definitions --- const LIST_DEFINITIONS = [ { name: 'All Contacts', tags: ['v2'] }, { name: 'Campaign Participants', tags: ['v2', 'influence'] }, { name: 'Locations - All', tags: ['v2', 'map'] }, { name: 'Support Level 1 (Strong)', tags: ['v2', 'map', 'support'] }, { name: 'Support Level 2 (Likely)', tags: ['v2', 'map', 'support'] }, { name: 'Support Level 3 (Unsure)', tags: ['v2', 'map', 'support'] }, { name: 'Support Level 4 (Opposition)', tags: ['v2', 'map', 'support'] }, { name: 'Has Campaign Sign', tags: ['v2', 'map', 'signs'] }, { name: 'Users', tags: ['v2', 'users'] }, { name: 'Volunteers', tags: ['v2', 'map', 'shifts'] }, { name: 'Canvassers', tags: ['v2', 'map', 'canvass'] }, { name: 'Subscribers', tags: ['v2', 'payments'] }, { name: 'Donors', tags: ['v2', 'payments'] }, ]; exports.SUPPORT_LEVEL_LIST_MAP = { LEVEL_1: 'Support Level 1 (Strong)', LEVEL_2: 'Support Level 2 (Likely)', LEVEL_3: 'Support Level 3 (Unsure)', LEVEL_4: 'Support Level 4 (Opposition)', }; class ListmonkSyncService { listIds = {}; initialized = false; lastSyncAt = null; lastError = null; async initializeLists() { const existingLists = await listmonk_client_1.listmonkClient.getLists(); const existingByName = new Map(existingLists.map(l => [l.name, l])); for (const def of LIST_DEFINITIONS) { const existing = existingByName.get(def.name); if (existing) { this.listIds[def.name] = existing.id; } else { const created = await listmonk_client_1.listmonkClient.createList(def.name, 'private', def.tags); this.listIds[def.name] = created.id; logger_1.logger.info(`Created Listmonk list: ${def.name} (id=${created.id})`); } } this.initialized = true; logger_1.logger.info('Listmonk lists initialized', { listIds: this.listIds }); } async ensureInitialized() { if (!this.initialized) { await this.initializeLists(); } } getListId(name) { return this.listIds[name]; } async syncCampaignParticipants() { await this.ensureInitialized(); const result = { total: 0, success: 0, failed: 0, errors: [] }; // Get distinct senders from campaign emails const emails = await database_1.prisma.campaignEmail.findMany({ where: { userEmail: { not: null } }, distinct: ['userEmail'], select: { userEmail: true, userName: true, userPostalCode: true, campaignSlug: true, recipientName: true, sentAt: true, }, orderBy: { sentAt: 'desc' }, }); result.total = emails.length; const allContactsId = this.listIds['All Contacts']; const participantsId = this.listIds['Campaign Participants']; for (const email of emails) { if (!email.userEmail) continue; try { await listmonk_client_1.listmonkClient.upsertSubscriber(email.userEmail, email.userName || '', [allContactsId, participantsId], { source: 'campaign_participant', campaign_slug: email.campaignSlug, postal_code: email.userPostalCode || null, last_sent: email.sentAt.toISOString(), recipient_name: email.recipientName || null, }); result.success++; } catch (err) { result.failed++; const msg = `Failed to sync participant ${email.userEmail}: ${err instanceof Error ? err.message : String(err)}`; result.errors.push(msg); logger_1.logger.warn(msg); } } return result; } async syncLocations() { await this.ensureInitialized(); const result = { total: 0, success: 0, failed: 0, errors: [] }; // Query addresses (unit-level) with email, include location for street address const addresses = await database_1.prisma.address.findMany({ where: { email: { not: null } }, select: { email: true, firstName: true, lastName: true, supportLevel: true, sign: true, signSize: true, phone: true, location: { select: { address: true, }, }, }, }); result.total = addresses.length; const allContactsId = this.listIds['All Contacts']; const locationsAllId = this.listIds['Locations - All']; for (const addr of addresses) { if (!addr.email) continue; try { const listIds = [allContactsId, locationsAllId]; // Add support level list if (addr.supportLevel && exports.SUPPORT_LEVEL_LIST_MAP[addr.supportLevel]) { const levelListId = this.listIds[exports.SUPPORT_LEVEL_LIST_MAP[addr.supportLevel]]; if (levelListId) listIds.push(levelListId); } // Add sign list if (addr.sign) { const signListId = this.listIds['Has Campaign Sign']; if (signListId) listIds.push(signListId); } const name = [addr.firstName, addr.lastName].filter(Boolean).join(' '); await listmonk_client_1.listmonkClient.upsertSubscriber(addr.email, name, listIds, { source: 'location', address: addr.location.address || null, support_level: addr.supportLevel || null, sign: addr.sign, sign_size: addr.signSize || null, phone: addr.phone || null, }); result.success++; } catch (err) { result.failed++; const msg = `Failed to sync address ${addr.email}: ${err instanceof Error ? err.message : String(err)}`; result.errors.push(msg); logger_1.logger.warn(msg); } } return result; } async syncUsers() { await this.ensureInitialized(); const result = { total: 0, success: 0, failed: 0, errors: [] }; const users = await database_1.prisma.user.findMany({ where: { status: 'ACTIVE', role: { not: 'TEMP' }, }, select: { email: true, name: true, role: true, createdAt: true, }, }); result.total = users.length; const allContactsId = this.listIds['All Contacts']; const usersListId = this.listIds['Users']; for (const user of users) { try { await listmonk_client_1.listmonkClient.upsertSubscriber(user.email, user.name || '', [allContactsId, usersListId], { source: 'user', role: user.role, created_at: user.createdAt.toISOString(), }); result.success++; } catch (err) { result.failed++; const msg = `Failed to sync user ${user.email}: ${err instanceof Error ? err.message : String(err)}`; result.errors.push(msg); logger_1.logger.warn(msg); } } return result; } async syncCrmTags() { await this.ensureInitialized(); const result = { total: 0, success: 0, failed: 0, errors: [] }; // Find all CRM tags that have a Listmonk list linked const crmTags = await database_1.prisma.crmTag.findMany({ where: { listmonkListId: { not: null } }, }); for (const tag of crmTags) { try { // Find all contacts with this tag using raw SQL (JSONB query) const contacts = await database_1.prisma.$queryRaw ` SELECT email, "displayName" FROM contacts WHERE "mergedIntoId" IS NULL AND email IS NOT NULL AND tags @> ${JSON.stringify([tag.name])}::jsonb `; result.total += contacts.length; for (const contact of contacts) { try { await listmonk_client_1.listmonkClient.upsertSubscriber(contact.email, contact.displayName || '', [tag.listmonkListId], { source: 'crm_tag', tag_name: tag.name, last_synced: new Date().toISOString() }); result.success++; } catch (err) { result.failed++; const msg = `Failed to sync tag "${tag.name}" for ${contact.email}: ${err instanceof Error ? err.message : String(err)}`; result.errors.push(msg); logger_1.logger.warn(msg); } } // Update denormalized count const countResult = await database_1.prisma.$queryRaw ` SELECT COUNT(*) as count FROM contacts WHERE "mergedIntoId" IS NULL AND tags @> ${JSON.stringify([tag.name])}::jsonb `; await database_1.prisma.crmTag.update({ where: { id: tag.id }, data: { contactCount: Number(countResult[0]?.count ?? 0) }, }); } catch (err) { const msg = `Failed to sync CRM tag "${tag.name}": ${err instanceof Error ? err.message : String(err)}`; result.errors.push(msg); logger_1.logger.warn(msg); } } return result; } async syncAll() { this.lastError = null; try { const participants = await this.syncCampaignParticipants(); const locations = await this.syncLocations(); const users = await this.syncUsers(); const crmTags = await this.syncCrmTags(); this.lastSyncAt = new Date(); return { participants, locations, users, crmTags }; } catch (err) { this.lastError = err instanceof Error ? err.message : String(err); throw err; } } getStatus() { return { enabled: env_1.env.LISTMONK_SYNC_ENABLED === 'true', connected: false, // Caller should use testConnection for live check initialized: this.initialized, lastSyncAt: this.lastSyncAt?.toISOString() || null, lastError: this.lastError, }; } async getStats() { await this.ensureInitialized(); const lists = await listmonk_client_1.listmonkClient.getLists(); // Only return lists matching our definitions const ourListNames = new Set(LIST_DEFINITIONS.map(d => d.name)); return { lists: lists .filter(l => ourListNames.has(l.name)) .map(l => ({ name: l.name, subscriberCount: l.subscriber_count })), }; } async reinitialize() { this.initialized = false; this.listIds = {}; await this.initializeLists(); } setLastSyncAt(date) { this.lastSyncAt = date; } setLastError(error) { this.lastError = error; } } exports.listmonkSyncService = new ListmonkSyncService(); //# sourceMappingURL=listmonk-sync.service.js.map