@open-mercato/core 0.10.1-develop.7768.1.90fa052c11 → 0.10.1-develop.7770.1.970b396ebb

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -25,12 +25,12 @@ function formatSubtitle(...parts) {
25
25
  if (text.length === 0) return void 0;
26
26
  return text.join(" \xB7 ");
27
27
  }
28
- function buildIndexSource(ctx, presenter, lines) {
28
+ function buildIndexSource(presenter, lines) {
29
29
  if (!lines.length) return null;
30
30
  return {
31
31
  text: lines,
32
32
  presenter,
33
- checksumSource: { record: ctx.record, customFields: ctx.customFields }
33
+ checksumSource: { text: lines, presenter }
34
34
  };
35
35
  }
36
36
  const searchConfig = {
@@ -44,13 +44,10 @@ const searchConfig = {
44
44
  const { t } = await resolveTranslations();
45
45
  const record = ctx.record;
46
46
  const lines = [];
47
- appendLine(lines, "Name", record.display_name ?? record.displayName);
48
- appendLine(lines, "Email", record.email);
47
+ appendLine(lines, "Customer user ID", record.id ?? "customer_accounts:customer_user");
49
48
  return buildIndexSource(
50
- ctx,
51
49
  {
52
50
  title: pickString(record.display_name, record.displayName) ?? String(record.id),
53
- subtitle: formatSubtitle(record.email),
54
51
  icon: "user",
55
52
  badge: t("customer_accounts.search.badge.customerUser", "Customer User")
56
53
  },
@@ -62,14 +59,14 @@ const searchConfig = {
62
59
  const record = ctx.record;
63
60
  return {
64
61
  title: pickString(record.display_name, record.displayName) ?? String(record.id),
65
- subtitle: formatSubtitle(record.email),
66
62
  icon: "user",
67
63
  badge: t("customer_accounts.search.badge.customerUser", "Customer User")
68
64
  };
69
65
  },
70
66
  resolveUrl: async (ctx) => `/backend/customer-accounts/users/${encodeURIComponent(String(ctx.record.id))}`,
71
67
  fieldPolicy: {
72
- searchable: ["display_name", "email"],
68
+ searchable: ["display_name"],
69
+ hashOnly: ["email"],
73
70
  excluded: ["password_hash", "email_hash"]
74
71
  }
75
72
  },
@@ -85,7 +82,6 @@ const searchConfig = {
85
82
  appendLine(lines, "Name", record.name);
86
83
  appendLine(lines, "Description", record.description);
87
84
  return buildIndexSource(
88
- ctx,
89
85
  {
90
86
  title: pickString(record.name) ?? String(record.id),
91
87
  subtitle: formatSubtitle(snippet(record.description)),
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/modules/customer_accounts/search.ts"],
4
- "sourcesContent": ["import type {\n SearchModuleConfig,\n SearchBuildContext,\n SearchResultPresenter,\n SearchIndexSource,\n} from '@open-mercato/shared/modules/search'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\n\nfunction pickString(...candidates: Array<unknown>): string | null {\n for (const candidate of candidates) {\n if (typeof candidate !== 'string') continue\n const trimmed = candidate.trim()\n if (trimmed.length > 0) return trimmed\n }\n return null\n}\n\nfunction snippet(value: unknown, max = 140): string | undefined {\n if (typeof value !== 'string') return undefined\n const trimmed = value.trim()\n if (!trimmed.length) return undefined\n if (trimmed.length <= max) return trimmed\n return `${trimmed.slice(0, max - 3)}...`\n}\n\nfunction appendLine(lines: string[], label: string, value: unknown) {\n if (value === null || value === undefined) return\n const text = typeof value === 'object' ? JSON.stringify(value) : String(value)\n if (!text.trim()) return\n lines.push(`${label}: ${text}`)\n}\n\nfunction formatSubtitle(...parts: Array<unknown>): string | undefined {\n const text = parts\n .map((part) => (part === null || part === undefined ? '' : String(part)))\n .map((part) => part.trim())\n .filter(Boolean)\n if (text.length === 0) return undefined\n return text.join(' \u00B7 ')\n}\n\nfunction buildIndexSource(\n ctx: SearchBuildContext,\n presenter: SearchResultPresenter,\n lines: string[],\n): SearchIndexSource | null {\n if (!lines.length) return null\n return {\n text: lines,\n presenter,\n checksumSource: { record: ctx.record, customFields: ctx.customFields },\n }\n}\n\nexport const searchConfig: SearchModuleConfig = {\n entities: [\n {\n entityId: 'customer_accounts:customer_user',\n aclFeatures: ['customer_accounts.view'],\n enabled: true,\n priority: 6,\n buildSource: async (ctx) => {\n const { t } = await resolveTranslations()\n const record = ctx.record\n const lines: string[] = []\n appendLine(lines, 'Name', record.display_name ?? record.displayName)\n appendLine(lines, 'Email', record.email)\n return buildIndexSource(\n ctx,\n {\n title: pickString(record.display_name, record.displayName) ?? String(record.id),\n subtitle: formatSubtitle(record.email),\n icon: 'user',\n badge: t('customer_accounts.search.badge.customerUser', 'Customer User'),\n },\n lines,\n )\n },\n formatResult: async (ctx) => {\n const { t } = await resolveTranslations()\n const record = ctx.record\n return {\n title: pickString(record.display_name, record.displayName) ?? String(record.id),\n subtitle: formatSubtitle(record.email),\n icon: 'user',\n badge: t('customer_accounts.search.badge.customerUser', 'Customer User'),\n }\n },\n resolveUrl: async (ctx) => `/backend/customer-accounts/users/${encodeURIComponent(String(ctx.record.id))}`,\n fieldPolicy: {\n searchable: ['display_name', 'email'],\n excluded: ['password_hash', 'email_hash'],\n },\n },\n {\n entityId: 'customer_accounts:customer_role',\n aclFeatures: ['customer_accounts.view'],\n enabled: true,\n priority: 6,\n buildSource: async (ctx) => {\n const { t } = await resolveTranslations()\n const record = ctx.record\n const lines: string[] = []\n appendLine(lines, 'Name', record.name)\n appendLine(lines, 'Description', record.description)\n return buildIndexSource(\n ctx,\n {\n title: pickString(record.name) ?? String(record.id),\n subtitle: formatSubtitle(snippet(record.description)),\n icon: 'shield',\n badge: t('customer_accounts.search.badge.customerRole', 'Customer Role'),\n },\n lines,\n )\n },\n formatResult: async (ctx) => {\n const { t } = await resolveTranslations()\n const record = ctx.record\n return {\n title: pickString(record.name) ?? String(record.id),\n subtitle: formatSubtitle(snippet(record.description)),\n icon: 'shield',\n badge: t('customer_accounts.search.badge.customerRole', 'Customer Role'),\n }\n },\n resolveUrl: async (ctx) => `/backend/customer-accounts/roles/${encodeURIComponent(String(ctx.record.id))}/edit`,\n fieldPolicy: {\n searchable: ['name', 'description'],\n },\n },\n ],\n}\n\nexport default searchConfig\nexport const config = searchConfig\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type {\n SearchModuleConfig,\n SearchResultPresenter,\n SearchIndexSource,\n} from '@open-mercato/shared/modules/search'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\n\nfunction pickString(...candidates: Array<unknown>): string | null {\n for (const candidate of candidates) {\n if (typeof candidate !== 'string') continue\n const trimmed = candidate.trim()\n if (trimmed.length > 0) return trimmed\n }\n return null\n}\n\nfunction snippet(value: unknown, max = 140): string | undefined {\n if (typeof value !== 'string') return undefined\n const trimmed = value.trim()\n if (!trimmed.length) return undefined\n if (trimmed.length <= max) return trimmed\n return `${trimmed.slice(0, max - 3)}...`\n}\n\nfunction appendLine(lines: string[], label: string, value: unknown) {\n if (value === null || value === undefined) return\n const text = typeof value === 'object' ? JSON.stringify(value) : String(value)\n if (!text.trim()) return\n lines.push(`${label}: ${text}`)\n}\n\nfunction formatSubtitle(...parts: Array<unknown>): string | undefined {\n const text = parts\n .map((part) => (part === null || part === undefined ? '' : String(part)))\n .map((part) => part.trim())\n .filter(Boolean)\n if (text.length === 0) return undefined\n return text.join(' \u00B7 ')\n}\n\nfunction buildIndexSource(\n presenter: SearchResultPresenter,\n lines: string[],\n): SearchIndexSource | null {\n if (!lines.length) return null\n return {\n text: lines,\n presenter,\n checksumSource: { text: lines, presenter },\n }\n}\n\nexport const searchConfig: SearchModuleConfig = {\n entities: [\n {\n entityId: 'customer_accounts:customer_user',\n aclFeatures: ['customer_accounts.view'],\n enabled: true,\n priority: 6,\n buildSource: async (ctx) => {\n const { t } = await resolveTranslations()\n const record = ctx.record\n const lines: string[] = []\n appendLine(lines, 'Customer user ID', record.id ?? 'customer_accounts:customer_user')\n return buildIndexSource(\n {\n title: pickString(record.display_name, record.displayName) ?? String(record.id),\n icon: 'user',\n badge: t('customer_accounts.search.badge.customerUser', 'Customer User'),\n },\n lines,\n )\n },\n formatResult: async (ctx) => {\n const { t } = await resolveTranslations()\n const record = ctx.record\n return {\n title: pickString(record.display_name, record.displayName) ?? String(record.id),\n icon: 'user',\n badge: t('customer_accounts.search.badge.customerUser', 'Customer User'),\n }\n },\n resolveUrl: async (ctx) => `/backend/customer-accounts/users/${encodeURIComponent(String(ctx.record.id))}`,\n fieldPolicy: {\n searchable: ['display_name'],\n hashOnly: ['email'],\n excluded: ['password_hash', 'email_hash'],\n },\n },\n {\n entityId: 'customer_accounts:customer_role',\n aclFeatures: ['customer_accounts.view'],\n enabled: true,\n priority: 6,\n buildSource: async (ctx) => {\n const { t } = await resolveTranslations()\n const record = ctx.record\n const lines: string[] = []\n appendLine(lines, 'Name', record.name)\n appendLine(lines, 'Description', record.description)\n return buildIndexSource(\n {\n title: pickString(record.name) ?? String(record.id),\n subtitle: formatSubtitle(snippet(record.description)),\n icon: 'shield',\n badge: t('customer_accounts.search.badge.customerRole', 'Customer Role'),\n },\n lines,\n )\n },\n formatResult: async (ctx) => {\n const { t } = await resolveTranslations()\n const record = ctx.record\n return {\n title: pickString(record.name) ?? String(record.id),\n subtitle: formatSubtitle(snippet(record.description)),\n icon: 'shield',\n badge: t('customer_accounts.search.badge.customerRole', 'Customer Role'),\n }\n },\n resolveUrl: async (ctx) => `/backend/customer-accounts/roles/${encodeURIComponent(String(ctx.record.id))}/edit`,\n fieldPolicy: {\n searchable: ['name', 'description'],\n },\n },\n ],\n}\n\nexport default searchConfig\nexport const config = searchConfig\n"],
5
+ "mappings": "AAKA,SAAS,2BAA2B;AAEpC,SAAS,cAAc,YAA2C;AAChE,aAAW,aAAa,YAAY;AAClC,QAAI,OAAO,cAAc,SAAU;AACnC,UAAM,UAAU,UAAU,KAAK;AAC/B,QAAI,QAAQ,SAAS,EAAG,QAAO;AAAA,EACjC;AACA,SAAO;AACT;AAEA,SAAS,QAAQ,OAAgB,MAAM,KAAyB;AAC9D,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,QAAM,UAAU,MAAM,KAAK;AAC3B,MAAI,CAAC,QAAQ,OAAQ,QAAO;AAC5B,MAAI,QAAQ,UAAU,IAAK,QAAO;AAClC,SAAO,GAAG,QAAQ,MAAM,GAAG,MAAM,CAAC,CAAC;AACrC;AAEA,SAAS,WAAW,OAAiB,OAAe,OAAgB;AAClE,MAAI,UAAU,QAAQ,UAAU,OAAW;AAC3C,QAAM,OAAO,OAAO,UAAU,WAAW,KAAK,UAAU,KAAK,IAAI,OAAO,KAAK;AAC7E,MAAI,CAAC,KAAK,KAAK,EAAG;AAClB,QAAM,KAAK,GAAG,KAAK,KAAK,IAAI,EAAE;AAChC;AAEA,SAAS,kBAAkB,OAA2C;AACpE,QAAM,OAAO,MACV,IAAI,CAAC,SAAU,SAAS,QAAQ,SAAS,SAAY,KAAK,OAAO,IAAI,CAAE,EACvE,IAAI,CAAC,SAAS,KAAK,KAAK,CAAC,EACzB,OAAO,OAAO;AACjB,MAAI,KAAK,WAAW,EAAG,QAAO;AAC9B,SAAO,KAAK,KAAK,QAAK;AACxB;AAEA,SAAS,iBACP,WACA,OAC0B;AAC1B,MAAI,CAAC,MAAM,OAAQ,QAAO;AAC1B,SAAO;AAAA,IACL,MAAM;AAAA,IACN;AAAA,IACA,gBAAgB,EAAE,MAAM,OAAO,UAAU;AAAA,EAC3C;AACF;AAEO,MAAM,eAAmC;AAAA,EAC9C,UAAU;AAAA,IACR;AAAA,MACE,UAAU;AAAA,MACV,aAAa,CAAC,wBAAwB;AAAA,MACtC,SAAS;AAAA,MACT,UAAU;AAAA,MACV,aAAa,OAAO,QAAQ;AAC1B,cAAM,EAAE,EAAE,IAAI,MAAM,oBAAoB;AACxC,cAAM,SAAS,IAAI;AACnB,cAAM,QAAkB,CAAC;AACzB,mBAAW,OAAO,oBAAoB,OAAO,MAAM,iCAAiC;AACpF,eAAO;AAAA,UACL;AAAA,YACE,OAAO,WAAW,OAAO,cAAc,OAAO,WAAW,KAAK,OAAO,OAAO,EAAE;AAAA,YAC9E,MAAM;AAAA,YACN,OAAO,EAAE,+CAA+C,eAAe;AAAA,UACzE;AAAA,UACA;AAAA,QACF;AAAA,MACF;AAAA,MACA,cAAc,OAAO,QAAQ;AAC3B,cAAM,EAAE,EAAE,IAAI,MAAM,oBAAoB;AACxC,cAAM,SAAS,IAAI;AACnB,eAAO;AAAA,UACL,OAAO,WAAW,OAAO,cAAc,OAAO,WAAW,KAAK,OAAO,OAAO,EAAE;AAAA,UAC9E,MAAM;AAAA,UACN,OAAO,EAAE,+CAA+C,eAAe;AAAA,QACzE;AAAA,MACF;AAAA,MACA,YAAY,OAAO,QAAQ,oCAAoC,mBAAmB,OAAO,IAAI,OAAO,EAAE,CAAC,CAAC;AAAA,MACxG,aAAa;AAAA,QACX,YAAY,CAAC,cAAc;AAAA,QAC3B,UAAU,CAAC,OAAO;AAAA,QAClB,UAAU,CAAC,iBAAiB,YAAY;AAAA,MAC1C;AAAA,IACF;AAAA,IACA;AAAA,MACE,UAAU;AAAA,MACV,aAAa,CAAC,wBAAwB;AAAA,MACtC,SAAS;AAAA,MACT,UAAU;AAAA,MACV,aAAa,OAAO,QAAQ;AAC1B,cAAM,EAAE,EAAE,IAAI,MAAM,oBAAoB;AACxC,cAAM,SAAS,IAAI;AACnB,cAAM,QAAkB,CAAC;AACzB,mBAAW,OAAO,QAAQ,OAAO,IAAI;AACrC,mBAAW,OAAO,eAAe,OAAO,WAAW;AACnD,eAAO;AAAA,UACL;AAAA,YACE,OAAO,WAAW,OAAO,IAAI,KAAK,OAAO,OAAO,EAAE;AAAA,YAClD,UAAU,eAAe,QAAQ,OAAO,WAAW,CAAC;AAAA,YACpD,MAAM;AAAA,YACN,OAAO,EAAE,+CAA+C,eAAe;AAAA,UACzE;AAAA,UACA;AAAA,QACF;AAAA,MACF;AAAA,MACA,cAAc,OAAO,QAAQ;AAC3B,cAAM,EAAE,EAAE,IAAI,MAAM,oBAAoB;AACxC,cAAM,SAAS,IAAI;AACnB,eAAO;AAAA,UACL,OAAO,WAAW,OAAO,IAAI,KAAK,OAAO,OAAO,EAAE;AAAA,UAClD,UAAU,eAAe,QAAQ,OAAO,WAAW,CAAC;AAAA,UACpD,MAAM;AAAA,UACN,OAAO,EAAE,+CAA+C,eAAe;AAAA,QACzE;AAAA,MACF;AAAA,MACA,YAAY,OAAO,QAAQ,oCAAoC,mBAAmB,OAAO,IAAI,OAAO,EAAE,CAAC,CAAC;AAAA,MACxG,aAAa;AAAA,QACX,YAAY,CAAC,QAAQ,aAAa;AAAA,MACpC;AAAA,IACF;AAAA,EACF;AACF;AAEA,IAAO,iBAAQ;AACR,MAAM,SAAS;",
6
6
  "names": []
7
7
  }
@@ -1,6 +1,8 @@
1
1
  import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
2
2
  import { CUSTOMER_INTERACTION_TASK_SOURCE, EXAMPLE_TODO_SOURCE } from "./lib/interactionCompatibility.js";
3
3
  import { createLogger } from "@open-mercato/shared/lib/logger";
4
+ import { defaultEncryptionMaps } from "./encryption.js";
5
+ import { isTenantDataEncryptionEnabled } from "@open-mercato/shared/lib/encryption/toggles";
4
6
  const logger = createLogger("customers");
5
7
  function assertTenantContext(ctx) {
6
8
  if (typeof ctx.tenantId !== "string" || ctx.tenantId.length === 0) {
@@ -310,6 +312,52 @@ function appendCustomFieldLines(lines, customFields, prefix) {
310
312
  appendLine(lines, label, value);
311
313
  }
312
314
  }
315
+ function normalizeSearchFieldKey(key) {
316
+ return key.replace(/^cf[:_]/, "").replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
317
+ }
318
+ async function resolveProfileEncryptedFields(ctx, kind, entity) {
319
+ if (!isTenantDataEncryptionEnabled()) {
320
+ throw new Error("[internal] Customer vector indexing requires available encryption declarations");
321
+ }
322
+ const container = ctx.container;
323
+ if (typeof container?.resolve !== "function") {
324
+ throw new Error("[internal] Customer search encryption policy container is unavailable");
325
+ }
326
+ const service = container.resolve("tenantEncryptionService");
327
+ if (typeof service?.getEncryptedFieldNames !== "function") {
328
+ throw new Error("[internal] Customer search encryption policy service is unavailable");
329
+ }
330
+ const organizationIds = /* @__PURE__ */ new Set([ctx.organizationId]);
331
+ for (const record of [ctx.record, entity]) {
332
+ const organizationId = record?.organization_id ?? record?.organizationId;
333
+ if (typeof organizationId === "string" && organizationId) organizationIds.add(organizationId);
334
+ }
335
+ const fields = await Promise.all(Array.from(organizationIds).flatMap((organizationId) => [
336
+ `customers:customer_${kind}_profile`,
337
+ "customers:customer_entity"
338
+ ].map((entityId) => service.getEncryptedFieldNames(entityId, ctx.tenantId, organizationId, { ignoreRuntimeHealth: true }))));
339
+ return fields.flat();
340
+ }
341
+ function selectProfileSearchFields(source, kind, options = {}) {
342
+ const entityId = `customers:customer_${kind}_profile`;
343
+ const policy = searchConfig.entities.find((entry) => entry.entityId === entityId)?.fieldPolicy;
344
+ const allowed = new Set((options.customerEntity ? ["display_name", "description", "status", "lifecycle_stage"] : policy?.searchable ?? []).map(normalizeSearchFieldKey));
345
+ const excluded = new Set([...policy?.hashOnly ?? [], ...policy?.excluded ?? []].map(normalizeSearchFieldKey));
346
+ if (options.forEmbedding) {
347
+ for (const field of options.encryptedFields ?? []) excluded.add(normalizeSearchFieldKey(field));
348
+ for (const map of defaultEncryptionMaps) {
349
+ if (map.entityId !== entityId && map.entityId !== "customers:customer_entity") continue;
350
+ for (const entry of map.fields) excluded.add(normalizeSearchFieldKey(entry.field));
351
+ }
352
+ }
353
+ const fields = {};
354
+ for (const [key, value] of Object.entries(source ?? {})) {
355
+ if (value !== null && typeof value === "object") continue;
356
+ const field = normalizeSearchFieldKey(key);
357
+ if (allowed.has(field) && !excluded.has(field)) fields[field] = value;
358
+ }
359
+ return fields;
360
+ }
313
361
  function pickValue(source, ...keys) {
314
362
  if (!source) return void 0;
315
363
  for (const key of keys) {
@@ -334,11 +382,8 @@ function pickLabel(...candidates) {
334
382
  }
335
383
  return null;
336
384
  }
337
- function appendCustomerEntityLines(lines, entity, contactLabel = "Customer") {
385
+ function appendCustomerEntityLines(lines, entity) {
338
386
  if (!entity) return;
339
- appendLine(lines, "Customer", pickValue(entity, "display_name", "displayName") ?? entity.id);
340
- appendLine(lines, `${contactLabel} email`, pickValue(entity, "primary_email", "primaryEmail"));
341
- appendLine(lines, `${contactLabel} phone`, pickValue(entity, "primary_phone", "primaryPhone"));
342
387
  appendLine(lines, "Lifecycle stage", pickValue(entity, "lifecycle_stage", "lifecycleStage"));
343
388
  appendLine(lines, "Status", pickValue(entity, "status"));
344
389
  }
@@ -360,6 +405,9 @@ function ensureFallbackLines(lines, record, options = {}) {
360
405
  }
361
406
  function resolvePersonPresenter(translate, record, entity, customFields) {
362
407
  const fallbackEntityId = resolveCustomerEntityId(record);
408
+ record = { id: record.id, ...selectProfileSearchFields(record, "person") };
409
+ entity = entity ? { id: entity.id, ...selectProfileSearchFields(entity, "person", { customerEntity: true }) } : null;
410
+ customFields = selectProfileSearchFields(customFields, "person");
363
411
  const firstName = record.first_name ?? record.firstName ?? customFields.first_name ?? customFields.firstName ?? "";
364
412
  const lastName = record.last_name ?? record.lastName ?? customFields.last_name ?? customFields.lastName ?? "";
365
413
  const nameParts = [firstName, lastName].filter(Boolean).join(" ");
@@ -369,10 +417,6 @@ function resolvePersonPresenter(translate, record, entity, customFields) {
369
417
  if (jobTitle) subtitlePieces.push(String(jobTitle));
370
418
  const department = record.department ?? customFields.department;
371
419
  if (department) subtitlePieces.push(String(department));
372
- const primaryEmail = pickValue(entity, "primary_email", "primaryEmail");
373
- if (primaryEmail) subtitlePieces.push(String(primaryEmail));
374
- const primaryPhone = pickValue(entity, "primary_phone", "primaryPhone");
375
- if (primaryPhone) subtitlePieces.push(String(primaryPhone));
376
420
  const summary = snippet(
377
421
  pickValue(entity, "description") ?? customFields.summary ?? customFields.description
378
422
  );
@@ -386,16 +430,15 @@ function resolvePersonPresenter(translate, record, entity, customFields) {
386
430
  }
387
431
  function resolveCompanyPresenter(translate, record, entity, customFields) {
388
432
  const fallbackEntityId = resolveCustomerEntityId(record);
433
+ record = { id: record.id, ...selectProfileSearchFields(record, "company") };
434
+ entity = entity ? { id: entity.id, ...selectProfileSearchFields(entity, "company", { customerEntity: true }) } : null;
435
+ customFields = selectProfileSearchFields(customFields, "company");
389
436
  const title = pickValue(entity, "display_name", "displayName") ?? customFields.display_name ?? customFields.displayName ?? record.brand_name ?? record.legal_name ?? record.domain ?? record.brandName ?? record.legalName ?? (entity?.id && entity?.display_name ? entity.display_name : void 0) ?? fallbackEntityId ?? record.id ?? translate("customers.search.fallback.company", "Company");
390
437
  const subtitlePieces = [];
391
438
  const industry = record.industry;
392
439
  if (industry) subtitlePieces.push(String(industry));
393
440
  const sizeBucket = record.size_bucket ?? record.sizeBucket;
394
441
  if (sizeBucket) subtitlePieces.push(String(sizeBucket));
395
- if (entity) {
396
- const primaryEmail = pickValue(entity, "primary_email", "primaryEmail");
397
- if (primaryEmail) subtitlePieces.push(String(primaryEmail));
398
- }
399
442
  const summary = snippet(
400
443
  pickValue(entity, "description") ?? customFields.summary ?? customFields.description ?? record.summary ?? record.description
401
444
  );
@@ -441,19 +484,12 @@ const searchConfig = {
441
484
  const { t } = await resolveTranslations();
442
485
  const lines = [];
443
486
  const record = ctx.record;
444
- appendLine(lines, "Preferred name", record.preferred_name ?? record.preferredName ?? ctx.customFields.preferred_name);
445
- appendLine(lines, "First name", record.first_name ?? record.firstName ?? ctx.customFields.first_name);
446
- appendLine(lines, "Last name", record.last_name ?? record.lastName ?? ctx.customFields.last_name);
447
- appendLine(lines, "Job title", record.job_title ?? record.jobTitle ?? ctx.customFields.job_title);
448
- appendLine(lines, "Department", record.department ?? record.department_name ?? record.departmentName ?? ctx.customFields.department);
449
- appendLine(lines, "Seniority", record.seniority ?? record.seniority_level ?? record.seniorityLevel ?? ctx.customFields.seniority);
450
- appendLine(lines, "Timezone", record.timezone ?? record.time_zone ?? record.timeZone ?? ctx.customFields.timezone);
451
- appendLine(lines, "LinkedIn", record.linked_in_url ?? record.linkedInUrl ?? ctx.customFields.linked_in_url);
452
- appendLine(lines, "Twitter", record.twitter_url ?? record.twitterUrl ?? ctx.customFields.twitter_url);
453
- const { entity, entityId, profileCustomFields, entityCustomFields, entityOnlyCustomFields } = await hydrateProfileContext(ctx, "person");
454
- appendCustomFieldLines(lines, profileCustomFields, "Person custom");
487
+ const { entity, entityId, profileCustomFields, entityOnlyCustomFields } = await hydrateProfileContext(ctx, "person");
488
+ const embeddingOptions = { forEmbedding: true, encryptedFields: await resolveProfileEncryptedFields(ctx, "person", entity) };
489
+ appendCustomFieldLines(lines, selectProfileSearchFields(record, "person", embeddingOptions), "");
490
+ appendCustomFieldLines(lines, selectProfileSearchFields(profileCustomFields, "person", embeddingOptions), "Person custom");
455
491
  if (Object.keys(entityOnlyCustomFields).length) {
456
- appendCustomFieldLines(lines, entityOnlyCustomFields, "Customer custom");
492
+ appendCustomFieldLines(lines, selectProfileSearchFields(entityOnlyCustomFields, "person", embeddingOptions), "Customer custom");
457
493
  }
458
494
  if (!entity) {
459
495
  logger.warn("Failed to load customer entity for person profile", {
@@ -463,8 +499,8 @@ const searchConfig = {
463
499
  recordKeys: Object.keys(record)
464
500
  });
465
501
  }
466
- appendCustomerEntityLines(lines, entity, "Customer");
467
- ensureFallbackLines(lines, record);
502
+ appendCustomerEntityLines(lines, selectProfileSearchFields(entity, "person", { ...embeddingOptions, customerEntity: true }));
503
+ ensureFallbackLines(lines, { id: record.id ?? entityId ?? "customers:customer_person_profile" });
468
504
  if (!lines.length) return null;
469
505
  if (!entityId) {
470
506
  logger.warn("Person profile missing entity id", {
@@ -487,12 +523,7 @@ const searchConfig = {
487
523
  text: lines,
488
524
  presenter,
489
525
  links,
490
- checksumSource: {
491
- record: ctx.record,
492
- customFields: profileCustomFields,
493
- entity,
494
- entityCustomFields
495
- }
526
+ checksumSource: { text: lines, presenter, links }
496
527
  };
497
528
  },
498
529
  formatResult: async (ctx) => {
@@ -542,20 +573,15 @@ const searchConfig = {
542
573
  const { t } = await resolveTranslations();
543
574
  const lines = [];
544
575
  const record = ctx.record;
545
- appendLine(lines, "Legal name", record.legal_name ?? record.legalName ?? ctx.customFields.legal_name);
546
- appendLine(lines, "Brand name", record.brand_name ?? record.brandName ?? ctx.customFields.brand_name);
547
- appendLine(lines, "Domain", record.domain ?? record.website_domain ?? record.websiteDomain ?? ctx.customFields.domain);
548
- appendLine(lines, "Website", record.website_url ?? record.websiteUrl ?? ctx.customFields.website_url);
549
- appendLine(lines, "Industry", record.industry ?? ctx.customFields.industry);
550
- appendLine(lines, "Company size", record.size_bucket ?? record.sizeBucket ?? ctx.customFields.size_bucket);
551
- appendLine(lines, "Annual revenue", record.annual_revenue ?? record.annualRevenue ?? ctx.customFields.annual_revenue);
552
- const { entity, entityId, profileCustomFields, entityCustomFields, entityOnlyCustomFields } = await hydrateProfileContext(ctx, "company");
553
- appendCustomFieldLines(lines, profileCustomFields, "Company custom");
576
+ const { entity, entityId, profileCustomFields, entityOnlyCustomFields } = await hydrateProfileContext(ctx, "company");
577
+ const embeddingOptions = { forEmbedding: true, encryptedFields: await resolveProfileEncryptedFields(ctx, "company", entity) };
578
+ appendCustomFieldLines(lines, selectProfileSearchFields(record, "company", embeddingOptions), "");
579
+ appendCustomFieldLines(lines, selectProfileSearchFields(profileCustomFields, "company", embeddingOptions), "Company custom");
554
580
  if (Object.keys(entityOnlyCustomFields).length) {
555
- appendCustomFieldLines(lines, entityOnlyCustomFields, "Customer custom");
581
+ appendCustomFieldLines(lines, selectProfileSearchFields(entityOnlyCustomFields, "company", embeddingOptions), "Customer custom");
556
582
  }
557
- appendCustomerEntityLines(lines, entity, "Primary");
558
- ensureFallbackLines(lines, record);
583
+ appendCustomerEntityLines(lines, selectProfileSearchFields(entity, "company", { ...embeddingOptions, customerEntity: true }));
584
+ ensureFallbackLines(lines, { id: record.id ?? entityId ?? "customers:customer_company_profile" });
559
585
  if (!lines.length) return null;
560
586
  const presenter = resolveCompanyPresenter(t, record, entity, ctx.customFields);
561
587
  logMissingPresenterTitle("company", record, entity, presenter);
@@ -571,12 +597,7 @@ const searchConfig = {
571
597
  text: lines,
572
598
  presenter,
573
599
  links,
574
- checksumSource: {
575
- record: ctx.record,
576
- customFields: profileCustomFields,
577
- entity,
578
- entityCustomFields
579
- }
600
+ checksumSource: { text: lines, presenter, links }
580
601
  };
581
602
  },
582
603
  formatResult: async (ctx) => {