From e81ccf6c2d169dd5caa5a034b3cdecf0802bb1f8 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Fri, 2 Jan 2026 17:32:23 -0500 Subject: [PATCH 1/2] Add event registrations to contact activity history MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extends the contact activity API to include event registrations alongside email activities using a UNION query. This provides a unified view of all engagement with a contact. The activity response now includes: - activity_type: 'email' or 'event_registration' - title: email subject or event title - description: email direction or registration status - metadata: includes event_id, event_slug, ticket_type, attended 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- server/src/routes/admin/domains.ts | 120 +++++++++++++++++++++-------- 1 file changed, 86 insertions(+), 34 deletions(-) diff --git a/server/src/routes/admin/domains.ts b/server/src/routes/admin/domains.ts index 873b4390ce..e62895663f 100644 --- a/server/src/routes/admin/domains.ts +++ b/server/src/routes/admin/domains.ts @@ -571,24 +571,50 @@ export function setupDomainRoutes( const contact = contactResult.rows[0]; - // Get activity history for this contact + // Get activity history for this contact (emails + event registrations) const activitiesResult = await pool.query( - `SELECT - eca.id as activity_id, - eca.email_id, - eca.message_id, - eca.subject, - eca.direction, - eca.insights, - eca.metadata, - eca.email_date, - eca.created_at, - eac.role, - eac.is_primary - FROM email_contact_activities eca - INNER JOIN email_activity_contacts eac ON eac.activity_id = eca.id - WHERE eac.contact_id = $1 - ORDER BY eca.email_date DESC`, + `SELECT * FROM ( + -- Email activities + SELECT + eca.id as activity_id, + 'email' as activity_type, + eca.subject as title, + eca.direction as description, + eca.insights, + eca.metadata, + eca.email_date as activity_date, + eca.created_at, + eac.role, + eac.is_primary + FROM email_contact_activities eca + INNER JOIN email_activity_contacts eac ON eac.activity_id = eca.id + WHERE eac.contact_id = $1 + + UNION ALL + + -- Event registrations + SELECT + er.id as activity_id, + 'event_registration' as activity_type, + e.title as title, + er.registration_status as description, + NULL as insights, + jsonb_build_object( + 'event_id', er.event_id, + 'event_slug', e.slug, + 'ticket_type', er.ticket_type, + 'attended', er.attended, + 'registration_source', er.registration_source + ) as metadata, + er.registered_at as activity_date, + er.created_at, + 'registrant' as role, + true as is_primary + FROM event_registrations er + INNER JOIN events e ON e.id = er.event_id + WHERE er.email_contact_id = $1 + ) combined + ORDER BY activity_date DESC`, [id] ); @@ -649,24 +675,50 @@ export function setupDomainRoutes( const contact = contactResult.rows[0]; - // Get activity history for this contact + // Get activity history for this contact (emails + event registrations) const activitiesResult = await pool.query( - `SELECT - eca.id as activity_id, - eca.email_id, - eca.message_id, - eca.subject, - eca.direction, - eca.insights, - eca.metadata, - eca.email_date, - eca.created_at, - eac.role, - eac.is_primary - FROM email_contact_activities eca - INNER JOIN email_activity_contacts eac ON eac.activity_id = eca.id - WHERE eac.contact_id = $1 - ORDER BY eca.email_date DESC`, + `SELECT * FROM ( + -- Email activities + SELECT + eca.id as activity_id, + 'email' as activity_type, + eca.subject as title, + eca.direction as description, + eca.insights, + eca.metadata, + eca.email_date as activity_date, + eca.created_at, + eac.role, + eac.is_primary + FROM email_contact_activities eca + INNER JOIN email_activity_contacts eac ON eac.activity_id = eca.id + WHERE eac.contact_id = $1 + + UNION ALL + + -- Event registrations + SELECT + er.id as activity_id, + 'event_registration' as activity_type, + e.title as title, + er.registration_status as description, + NULL as insights, + jsonb_build_object( + 'event_id', er.event_id, + 'event_slug', e.slug, + 'ticket_type', er.ticket_type, + 'attended', er.attended, + 'registration_source', er.registration_source + ) as metadata, + er.registered_at as activity_date, + er.created_at, + 'registrant' as role, + true as is_primary + FROM event_registrations er + INNER JOIN events e ON e.id = er.event_id + WHERE er.email_contact_id = $1 + ) combined + ORDER BY activity_date DESC`, [contact.id] ); From 5f1e6bf52b788c287a089c8e0e4af713aca2ef63 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Fri, 2 Jan 2026 19:03:20 -0500 Subject: [PATCH 2/2] Extract CONTACT_ACTIVITIES_QUERY to shared constant MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Reduces code duplication between two contact endpoints - Adds NULL::TEXT cast for explicit type in UNION - Adds NULLS LAST to ORDER BY for consistent sorting 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .changeset/slimy-meteors-grab.md | 2 + server/src/routes/admin/domains.ts | 142 +++++++++++------------------ 2 files changed, 54 insertions(+), 90 deletions(-) create mode 100644 .changeset/slimy-meteors-grab.md diff --git a/.changeset/slimy-meteors-grab.md b/.changeset/slimy-meteors-grab.md new file mode 100644 index 0000000000..a845151cc8 --- /dev/null +++ b/.changeset/slimy-meteors-grab.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/server/src/routes/admin/domains.ts b/server/src/routes/admin/domains.ts index e62895663f..bff8309da0 100644 --- a/server/src/routes/admin/domains.ts +++ b/server/src/routes/admin/domains.ts @@ -14,6 +14,56 @@ import { enrichOrganization } from "../../services/enrichment.js"; const slackDb = new SlackDatabase(); const logger = createLogger("admin-domains"); +/** + * SQL query to fetch combined activity history for a contact. + * Combines email activities with event registrations via UNION ALL. + * Parameter $1 is the contact UUID. + */ +const CONTACT_ACTIVITIES_QUERY = ` + SELECT * FROM ( + -- Email activities + SELECT + eca.id as activity_id, + 'email' as activity_type, + eca.subject as title, + eca.direction as description, + eca.insights, + eca.metadata, + eca.email_date as activity_date, + eca.created_at, + eac.role, + eac.is_primary + FROM email_contact_activities eca + INNER JOIN email_activity_contacts eac ON eac.activity_id = eca.id + WHERE eac.contact_id = $1 + + UNION ALL + + -- Event registrations + SELECT + er.id as activity_id, + 'event_registration' as activity_type, + e.title as title, + er.registration_status as description, + NULL::TEXT as insights, + jsonb_build_object( + 'event_id', er.event_id, + 'event_slug', e.slug, + 'ticket_type', er.ticket_type, + 'attended', er.attended, + 'registration_source', er.registration_source + ) as metadata, + er.registered_at as activity_date, + er.created_at, + 'registrant' as role, + true as is_primary + FROM event_registrations er + INNER JOIN events e ON e.id = er.event_id + WHERE er.email_contact_id = $1 + ) combined + ORDER BY activity_date DESC NULLS LAST +`; + interface DomainRoutesConfig { workos: WorkOS | null; } @@ -572,51 +622,7 @@ export function setupDomainRoutes( const contact = contactResult.rows[0]; // Get activity history for this contact (emails + event registrations) - const activitiesResult = await pool.query( - `SELECT * FROM ( - -- Email activities - SELECT - eca.id as activity_id, - 'email' as activity_type, - eca.subject as title, - eca.direction as description, - eca.insights, - eca.metadata, - eca.email_date as activity_date, - eca.created_at, - eac.role, - eac.is_primary - FROM email_contact_activities eca - INNER JOIN email_activity_contacts eac ON eac.activity_id = eca.id - WHERE eac.contact_id = $1 - - UNION ALL - - -- Event registrations - SELECT - er.id as activity_id, - 'event_registration' as activity_type, - e.title as title, - er.registration_status as description, - NULL as insights, - jsonb_build_object( - 'event_id', er.event_id, - 'event_slug', e.slug, - 'ticket_type', er.ticket_type, - 'attended', er.attended, - 'registration_source', er.registration_source - ) as metadata, - er.registered_at as activity_date, - er.created_at, - 'registrant' as role, - true as is_primary - FROM event_registrations er - INNER JOIN events e ON e.id = er.event_id - WHERE er.email_contact_id = $1 - ) combined - ORDER BY activity_date DESC`, - [id] - ); + const activitiesResult = await pool.query(CONTACT_ACTIVITIES_QUERY, [id]); res.json({ ...contact, @@ -676,51 +682,7 @@ export function setupDomainRoutes( const contact = contactResult.rows[0]; // Get activity history for this contact (emails + event registrations) - const activitiesResult = await pool.query( - `SELECT * FROM ( - -- Email activities - SELECT - eca.id as activity_id, - 'email' as activity_type, - eca.subject as title, - eca.direction as description, - eca.insights, - eca.metadata, - eca.email_date as activity_date, - eca.created_at, - eac.role, - eac.is_primary - FROM email_contact_activities eca - INNER JOIN email_activity_contacts eac ON eac.activity_id = eca.id - WHERE eac.contact_id = $1 - - UNION ALL - - -- Event registrations - SELECT - er.id as activity_id, - 'event_registration' as activity_type, - e.title as title, - er.registration_status as description, - NULL as insights, - jsonb_build_object( - 'event_id', er.event_id, - 'event_slug', e.slug, - 'ticket_type', er.ticket_type, - 'attended', er.attended, - 'registration_source', er.registration_source - ) as metadata, - er.registered_at as activity_date, - er.created_at, - 'registrant' as role, - true as is_primary - FROM event_registrations er - INNER JOIN events e ON e.id = er.event_id - WHERE er.email_contact_id = $1 - ) combined - ORDER BY activity_date DESC`, - [contact.id] - ); + const activitiesResult = await pool.query(CONTACT_ACTIVITIES_QUERY, [contact.id]); res.json({ ...contact,