/** * Client groups (CM-1) - first-class mailing/segment groups for clients. * * A `client_groups` row is a named, per-port group (e.g. a mailing list). * `client_group_members` is the M2M join to `clients`. Membership carries its * own `port_id` for defense-in-depth tenant isolation (same doctrine as the * document-folders aggregated projection - port_id at every join). * * Optional Mailchimp mapping lives on the group row: `mailchimpTag` is the * tag/segment name pushed to the port's single Mailchimp audience. Null until * an admin wires Mailchimp up (the integration is inert without creds). */ import { sql } from 'drizzle-orm'; import { index, pgTable, primaryKey, text, timestamp, uniqueIndex } from 'drizzle-orm/pg-core'; import { clients } from './clients'; import { ports } from './ports'; export const clientGroups = pgTable( 'client_groups', { id: text('id') .primaryKey() .$defaultFn(() => crypto.randomUUID()), portId: text('port_id') .notNull() .references(() => ports.id, { onDelete: 'cascade' }), name: text('name').notNull(), description: text('description'), /** Chip color in the CRM UI. */ color: text('color').notNull().default('#6B7280'), /** CM-1 Mailchimp: the tag/segment name this group maps to in the port's * single Mailchimp audience. Null = not synced. */ mailchimpTag: text('mailchimp_tag'), archivedAt: timestamp('archived_at', { withTimezone: true }), createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), }, (table) => [ index('idx_client_groups_port').on(table.portId), // Per-port, case-insensitive name uniqueness among non-archived groups. uniqueIndex('idx_client_groups_port_name') .on(table.portId, sql`lower(${table.name})`) .where(sql`${table.archivedAt} IS NULL`), ], ); export const clientGroupMembers = pgTable( 'client_group_members', { groupId: text('group_id') .notNull() .references(() => clientGroups.id, { onDelete: 'cascade' }), clientId: text('client_id') .notNull() .references(() => clients.id, { onDelete: 'cascade' }), portId: text('port_id') .notNull() .references(() => ports.id, { onDelete: 'cascade' }), createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), }, (table) => [ primaryKey({ columns: [table.groupId, table.clientId] }), index('idx_cgm_client').on(table.clientId), index('idx_cgm_port').on(table.portId), ], ); export type ClientGroup = typeof clientGroups.$inferSelect; export type NewClientGroup = typeof clientGroups.$inferInsert; export type ClientGroupMember = typeof clientGroupMembers.$inferSelect; export type NewClientGroupMember = typeof clientGroupMembers.$inferInsert;