68 lines
2.8 KiB
TypeScript
68 lines
2.8 KiB
TypeScript
|
|
/**
|
||
|
|
* CM-1: Mailchimp Marketing API integration (one-way push, CRM → Mailchimp).
|
||
|
|
*
|
||
|
|
* SCOPE NOTE: per the locked CM-1 decision, the exact group → tag/segment
|
||
|
|
* mapping is finalised only once we have the client's actual Mailchimp account.
|
||
|
|
* So this module ships the config plumbing + an inert sync that no-ops until
|
||
|
|
* (a) an admin stores an API key + audience ID and (b) the mapping is wired.
|
||
|
|
* The members viewer + copy-emails features do NOT depend on Mailchimp.
|
||
|
|
*
|
||
|
|
* Settings keys (per-port, in system_settings):
|
||
|
|
* - `mailchimp_api_key` (AES-encrypted at rest, like SMTP/IMAP creds)
|
||
|
|
* - `mailchimp_audience_id` (the single audience all groups map into)
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { logger } from '@/lib/logger';
|
||
|
|
import { getSetting } from '@/lib/services/settings.service';
|
||
|
|
import { decrypt } from '@/lib/utils/encryption';
|
||
|
|
|
||
|
|
export interface MailchimpConfig {
|
||
|
|
apiKey: string;
|
||
|
|
audienceId: string;
|
||
|
|
/** Datacenter prefix derived from the key suffix (e.g. `us21`). */
|
||
|
|
serverPrefix: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Resolve + decrypt the per-port Mailchimp config, or null when unset. */
|
||
|
|
export async function getMailchimpConfig(portId: string): Promise<MailchimpConfig | null> {
|
||
|
|
const keyRow = await getSetting('mailchimp_api_key', portId);
|
||
|
|
const audRow = await getSetting('mailchimp_audience_id', portId);
|
||
|
|
const encKey = typeof keyRow?.value === 'string' ? keyRow.value : null;
|
||
|
|
const audienceId = typeof audRow?.value === 'string' ? audRow.value : null;
|
||
|
|
if (!encKey || !audienceId) return null;
|
||
|
|
let apiKey: string;
|
||
|
|
try {
|
||
|
|
apiKey = decrypt(encKey);
|
||
|
|
} catch {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
// Mailchimp keys are `<hex>-<dc>`; the datacenter is the API host prefix.
|
||
|
|
const serverPrefix = apiKey.split('-')[1] ?? '';
|
||
|
|
if (!serverPrefix) return null;
|
||
|
|
return { apiKey, audienceId, serverPrefix };
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function isMailchimpConfigured(portId: string): Promise<boolean> {
|
||
|
|
return (await getMailchimpConfig(portId)) !== null;
|
||
|
|
}
|
||
|
|
|
||
|
|
export type MailchimpSyncResult = { skipped: string } | { synced: true; count: number };
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Push a group's members to Mailchimp as a tag/segment on the port's audience.
|
||
|
|
* Inert until configured AND the mapping is confirmed (see SCOPE NOTE).
|
||
|
|
*/
|
||
|
|
export async function syncGroupToMailchimp(
|
||
|
|
groupId: string,
|
||
|
|
portId: string,
|
||
|
|
): Promise<MailchimpSyncResult> {
|
||
|
|
const config = await getMailchimpConfig(portId);
|
||
|
|
if (!config) return { skipped: 'not-configured' };
|
||
|
|
// TODO(CM-1): mapping pending the client's Mailchimp account. Once confirmed,
|
||
|
|
// upsert each member via
|
||
|
|
// PUT https://{serverPrefix}.api.mailchimp.com/3.0/lists/{audienceId}/members/{md5(lowercased-email)}
|
||
|
|
// then apply the group's tag. Only push subscribed/opted-in contacts (GDPR).
|
||
|
|
logger.info({ groupId, portId }, 'Mailchimp sync requested (mapping pending client account)');
|
||
|
|
return { skipped: 'mapping-pending' };
|
||
|
|
}
|