feat: add contact-book variable registry for campaign personalization (#359)

* feat: add contact-book variable registry for campaign personalization

* test: include contact-book variables default in service expectation

* fix: address personalization review issues

* fix text

* fix: normalize contact variable access across contact flows

* stuff

* fix
This commit is contained in:
KM Koushik
2026-03-08 00:03:58 +11:00
committed by GitHub
parent d97e445ea0
commit 62e0a1db88
29 changed files with 1564 additions and 406 deletions
@@ -1,13 +1,17 @@
import { CampaignStatus } from "@prisma/client";
import { db } from "../db";
import { LimitService } from "./limit-service";
import { UnsendApiError } from "../public-api/api-error";
import {
DEFAULT_DOUBLE_OPT_IN_CONTENT,
DEFAULT_DOUBLE_OPT_IN_SUBJECT,
hasDoubleOptInUrlPlaceholder,
} from "~/lib/constants/double-opt-in";
import { db } from "../db";
import { UnsendApiError } from "../public-api/api-error";
import { validateDomainFromEmail } from "./domain-service";
import { LimitService } from "./limit-service";
import {
normalizeContactBookVariables,
validateContactBookVariables,
} from "./contact-variable-service";
type ContactBookDbClient = Pick<typeof db, "contactBook">;
@@ -22,6 +26,7 @@ export async function getContactBooks(teamId: number, search?: string) {
name: true,
teamId: true,
properties: true,
variables: true,
emoji: true,
createdAt: true,
updatedAt: true,
@@ -39,6 +44,7 @@ export async function getContactBooks(teamId: number, search?: string) {
export async function createContactBook(
teamId: number,
name: string,
variables?: string[],
client: ContactBookDbClient = db,
) {
const { isLimitReached, reason } =
@@ -51,11 +57,23 @@ export async function createContactBook(
});
}
const normalizedVariables = normalizeContactBookVariables(variables);
try {
validateContactBookVariables(normalizedVariables);
} catch (error) {
throw new UnsendApiError({
code: "BAD_REQUEST",
message: error instanceof Error ? error.message : "Invalid variables",
});
}
const created = await client.contactBook.create({
data: {
name,
teamId,
properties: {},
variables: normalizedVariables,
doubleOptInEnabled: true,
doubleOptInSubject: DEFAULT_DOUBLE_OPT_IN_SUBJECT,
doubleOptInContent: DEFAULT_DOUBLE_OPT_IN_CONTENT,
@@ -98,6 +116,7 @@ export async function updateContactBook(
name?: string;
properties?: Record<string, string>;
emoji?: string;
variables?: string[];
doubleOptInEnabled?: boolean;
doubleOptInFrom?: string | null;
doubleOptInSubject?: string;
@@ -105,7 +124,39 @@ export async function updateContactBook(
},
client: ContactBookDbClient = db,
) {
const updateData = { ...data };
const restData = { ...data };
delete restData.variables;
const normalizedVariables =
data.variables === undefined
? undefined
: normalizeContactBookVariables(data.variables);
if (normalizedVariables !== undefined) {
try {
validateContactBookVariables(normalizedVariables);
} catch (error) {
throw new UnsendApiError({
code: "BAD_REQUEST",
message: error instanceof Error ? error.message : "Invalid variables",
});
}
}
const updateData: {
name?: string;
properties?: Record<string, string>;
emoji?: string;
variables?: string[];
doubleOptInEnabled?: boolean;
doubleOptInSubject?: string;
doubleOptInContent?: string;
} = {
...restData,
...(normalizedVariables !== undefined
? { variables: normalizedVariables }
: {}),
};
if (data.doubleOptInFrom !== undefined) {
const normalizedFrom = data.doubleOptInFrom?.trim() ?? "";