@open-mercato/core 0.10.1-develop.7769.1.0e6458bb99 → 0.10.1-develop.7771.1.615a6c9ba9
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.
- package/dist/modules/customer_accounts/search.js +5 -9
- package/dist/modules/customer_accounts/search.js.map +2 -2
- package/dist/modules/customers/search.js +71 -50
- package/dist/modules/customers/search.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/customer_accounts/search.ts +4 -10
- package/src/modules/customers/search.ts +82 -52
|
@@ -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(
|
|
28
|
+
function buildIndexSource(presenter, lines) {
|
|
29
29
|
if (!lines.length) return null;
|
|
30
30
|
return {
|
|
31
31
|
text: lines,
|
|
32
32
|
presenter,
|
|
33
|
-
checksumSource: {
|
|
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, "
|
|
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"
|
|
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
|
|
5
|
-
"mappings": "
|
|
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": "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",
|
|
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
|
|
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
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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, "
|
|
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
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
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, "
|
|
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) => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/customers/search.ts"],
|
|
4
|
-
"sourcesContent": ["import type { QueryCustomFieldSource, QueryEngine } from '@open-mercato/shared/lib/query/types'\nimport type {\n SearchModuleConfig,\n SearchBuildContext,\n SearchResultPresenter,\n SearchResultLink,\n SearchIndexSource,\n} from '@open-mercato/shared/modules/search'\nimport type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { CUSTOMER_INTERACTION_TASK_SOURCE, EXAMPLE_TODO_SOURCE } from './lib/interactionCompatibility'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('customers')\n\n// =============================================================================\n// Context Types\n// =============================================================================\n\ntype SearchContext = SearchBuildContext & {\n tenantId: string\n queryEngine?: QueryEngine\n}\n\nfunction assertTenantContext(ctx: SearchBuildContext): asserts ctx is SearchContext {\n if (typeof ctx.tenantId !== 'string' || ctx.tenantId.length === 0) {\n throw new Error('[search.customers] Missing tenantId in search build context')\n }\n}\n\ntype CustomerProfileKind = 'person' | 'company'\n\ntype LoadedCustomerEntity = {\n entity: Record<string, unknown> | null\n customFields: Record<string, unknown>\n}\n\n// =============================================================================\n// Caching\n// =============================================================================\n\nconst entityIdCache = new Map<string, LoadedCustomerEntity | null>()\nconst profileEntityCache = new WeakMap<Record<string, unknown>, Partial<Record<CustomerProfileKind, LoadedCustomerEntity | null>>>()\nconst todoCache = new WeakMap<Record<string, unknown>, unknown>()\n\n// =============================================================================\n// Query Configuration\n// =============================================================================\n\nconst CUSTOMER_ENTITY_FIELDS = [\n 'id',\n 'kind',\n 'display_name',\n 'description',\n 'primary_email',\n 'primary_phone',\n 'status',\n 'lifecycle_stage',\n 'owner_user_id',\n 'source',\n 'next_interaction_at',\n 'next_interaction_name',\n 'next_interaction_ref_id',\n 'next_interaction_icon',\n 'next_interaction_color',\n 'organization_id',\n 'tenant_id',\n 'created_at',\n 'updated_at',\n 'deleted_at',\n] satisfies string[]\n\nconst CUSTOMER_CUSTOM_FIELD_SOURCES: QueryCustomFieldSource[] = [\n {\n entityId: 'customers:customer_person_profile',\n table: 'customer_people',\n alias: 'person_profile',\n recordIdColumn: 'id',\n join: { fromField: 'id', toField: 'entity_id' },\n },\n {\n entityId: 'customers:customer_company_profile',\n table: 'customer_companies',\n alias: 'company_profile',\n recordIdColumn: 'id',\n join: { fromField: 'id', toField: 'entity_id' },\n },\n]\n\n// =============================================================================\n// Helper Functions\n// =============================================================================\n\nfunction extractCustomFieldMap(source: Record<string, unknown> | null | undefined): Record<string, unknown> {\n if (!source) return {}\n const result: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(source)) {\n if (value === undefined) continue\n if (key.startsWith('cf:')) {\n result[key.slice(3)] = value\n } else if (key.startsWith('cf_')) {\n result[key.slice(3)] = value\n }\n }\n return result\n}\n\nfunction normalizeCustomerEntity(row: Record<string, unknown>): Record<string, unknown> {\n const normalized: Record<string, unknown> = {\n id: row.id ?? row.entity_id ?? row.entityId ?? null,\n kind: row.kind ?? null,\n }\n const assign = (snake: string, camel?: string) => {\n const value = row[snake] ?? (camel ? row[camel] : undefined)\n if (value !== undefined) {\n normalized[snake] = value\n if (camel) normalized[camel] = value\n }\n }\n assign('display_name', 'displayName')\n assign('description')\n assign('primary_email', 'primaryEmail')\n assign('primary_phone', 'primaryPhone')\n assign('status')\n assign('lifecycle_stage', 'lifecycleStage')\n assign('owner_user_id', 'ownerUserId')\n assign('source')\n assign('next_interaction_at', 'nextInteractionAt')\n assign('next_interaction_name', 'nextInteractionName')\n assign('next_interaction_ref_id', 'nextInteractionRefId')\n assign('next_interaction_icon', 'nextInteractionIcon')\n assign('next_interaction_color', 'nextInteractionColor')\n assign('organization_id', 'organizationId')\n assign('tenant_id', 'tenantId')\n assign('created_at', 'createdAt')\n assign('updated_at', 'updatedAt')\n assign('deleted_at', 'deletedAt')\n return normalized\n}\n\nfunction getProfileCache(record: Record<string, unknown>): Partial<Record<CustomerProfileKind, LoadedCustomerEntity | null>> {\n let cache = profileEntityCache.get(record)\n if (!cache) {\n cache = {}\n profileEntityCache.set(record, cache)\n }\n return cache\n}\n\nfunction subtractCustomFields(\n primary: Record<string, unknown>,\n secondary: Record<string, unknown>,\n): Record<string, unknown> {\n if (!secondary || Object.keys(secondary).length === 0) return {}\n const primaryKeys = new Set(Object.keys(primary))\n const result: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(secondary)) {\n if (!primaryKeys.has(key)) {\n result[key] = value\n }\n }\n return result\n}\n\n// =============================================================================\n// Entity Loading Functions\n// =============================================================================\n\ntype CustomerEntityQueryOptions = {\n entityId?: string | null\n profileKind?: CustomerProfileKind\n profileId?: string | null\n}\n\nasync function loadCustomerEntityBundle(ctx: SearchContext, opts: CustomerEntityQueryOptions): Promise<LoadedCustomerEntity | null> {\n if (!ctx.queryEngine) return null\n const filters: Record<string, unknown> = {}\n const resolvedEntityId = typeof opts.entityId === 'string' && opts.entityId.length ? opts.entityId : null\n const resolvedProfileId =\n opts.profileId != null && String(opts.profileId).trim().length > 0 ? String(opts.profileId).trim() : null\n const shouldJoinProfileSource = Boolean(opts.profileKind && resolvedProfileId && !resolvedEntityId)\n if (resolvedEntityId) {\n filters.id = { $eq: resolvedEntityId }\n }\n if (shouldJoinProfileSource) {\n const alias = opts.profileKind === 'person' ? 'person_profile' : 'company_profile'\n filters[`${alias}.id`] = { $eq: resolvedProfileId }\n }\n if (!Object.keys(filters).length) return null\n try {\n const result = await ctx.queryEngine.query('customers:customer_entity', {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? undefined,\n filters,\n includeCustomFields: true,\n ...(shouldJoinProfileSource ? { customFieldSources: CUSTOMER_CUSTOM_FIELD_SOURCES } : {}),\n fields: CUSTOMER_ENTITY_FIELDS,\n page: { page: 1, pageSize: 1 },\n })\n const row = result.items[0] as Record<string, unknown> | undefined\n if (!row) return null\n const entity = normalizeCustomerEntity(row)\n const customFields = extractCustomFieldMap(row)\n return { entity, customFields }\n } catch (error) {\n logger.warn('Failed to load customer entity via QueryEngine', {\n component: 'search',\n entityId: resolvedEntityId ?? null,\n profileKind: opts.profileKind ?? null,\n profileId: resolvedProfileId ?? null,\n err: error,\n })\n return null\n }\n}\n\nasync function loadCustomerEntityForProfile(ctx: SearchContext, kind: CustomerProfileKind): Promise<LoadedCustomerEntity | null> {\n const cache = getProfileCache(ctx.record)\n if (cache[kind] !== undefined) return cache[kind] ?? null\n const entityIdHint = resolveCustomerEntityId(ctx.record)\n const profileIdRaw = ctx.record.id ?? null\n const profileId = profileIdRaw != null ? String(profileIdRaw) : null\n if (!entityIdHint && !profileId) {\n cache[kind] = null\n return null\n }\n const loaded = await loadCustomerEntityBundle(ctx, {\n entityId: entityIdHint,\n profileKind: kind,\n profileId,\n })\n cache[kind] = loaded ?? null\n const resolvedId = loaded?.entity?.id ?? entityIdHint\n if (resolvedId && typeof resolvedId === 'string') {\n ctx.record.entity_id ??= resolvedId\n ctx.record.entityId ??= resolvedId\n entityIdCache.set(resolvedId, loaded ?? null)\n }\n if (loaded?.entity) {\n if (!ctx.record.entity) ctx.record.entity = loaded.entity\n if (!ctx.record.customer_entity) ctx.record.customer_entity = loaded.entity\n }\n return loaded ?? null\n}\n\nasync function loadCustomerEntityById(ctx: SearchContext, entityId: string | null | undefined): Promise<LoadedCustomerEntity | null> {\n const resolvedId = typeof entityId === 'string' && entityId.length ? entityId : null\n if (!resolvedId) return null\n if (entityIdCache.has(resolvedId)) {\n return entityIdCache.get(resolvedId) ?? null\n }\n const loaded = await loadCustomerEntityBundle(ctx, { entityId: resolvedId })\n entityIdCache.set(resolvedId, loaded ?? null)\n return loaded ?? null\n}\n\nasync function getCustomerEntity(ctx: SearchContext, entityId?: string | null): Promise<Record<string, unknown> | null> {\n const profileCache = profileEntityCache.get(ctx.record)\n if (profileCache) {\n const cached = Object.values(profileCache).find((entry) => {\n if (!entry?.entity) return false\n if (!entityId) return true\n return entry.entity.id === entityId\n })\n if (cached?.entity) return cached.entity\n }\n const inline = getInlineCustomerEntity(ctx.record)\n if (inline && (!entityId || inline.id === entityId)) {\n if (inline.id && typeof inline.id === 'string') {\n entityIdCache.set(inline.id, { entity: inline, customFields: {} })\n }\n return inline\n }\n const resolvedId = entityId ?? resolveCustomerEntityId(ctx.record)\n const loaded = await loadCustomerEntityById(ctx, resolvedId)\n return loaded?.entity ?? null\n}\n\ntype HydratedProfileContext = {\n entity: Record<string, unknown> | null\n entityId: string | null\n profileCustomFields: Record<string, unknown>\n entityCustomFields: Record<string, unknown>\n entityOnlyCustomFields: Record<string, unknown>\n}\n\nasync function hydrateProfileContext(ctx: SearchContext, kind: CustomerProfileKind): Promise<HydratedProfileContext> {\n const profileCustomFields = ctx.customFields ?? {}\n const loaded = await loadCustomerEntityForProfile(ctx, kind)\n let entity = loaded?.entity ?? getInlineCustomerEntity(ctx.record)\n let entityCustomFields = loaded?.customFields ?? {}\n let entityId = (entity?.id as string | undefined) ?? resolveCustomerEntityId(ctx.record)\n if (!entity && entityId) {\n const fetched = await loadCustomerEntityById(ctx, entityId)\n entity = fetched?.entity ?? null\n if (fetched?.customFields) {\n entityCustomFields = Object.keys(entityCustomFields).length ? entityCustomFields : fetched.customFields\n }\n }\n if (!entity && !entityId) {\n entityId = resolveCustomerEntityId(ctx.record)\n }\n if (entity?.id && typeof entity.id === 'string') {\n entityId = entity.id\n ctx.record.entity_id ??= entity.id\n ctx.record.entityId ??= entity.id\n if (!ctx.record.entity) ctx.record.entity = entity\n if (!ctx.record.customer_entity) ctx.record.customer_entity = entity\n }\n const entityOnlyCustomFields = subtractCustomFields(profileCustomFields, entityCustomFields)\n return {\n entity: entity ?? null,\n entityId: entityId ?? null,\n profileCustomFields,\n entityCustomFields,\n entityOnlyCustomFields,\n }\n}\n\nasync function loadRecord(ctx: SearchContext, entityId: string, recordId?: string | null) {\n if (!recordId || !ctx.queryEngine) return null\n const res = await ctx.queryEngine.query(entityId, {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? undefined,\n filters: { id: recordId },\n includeCustomFields: true,\n page: { page: 1, pageSize: 1 },\n })\n return res.items[0] as Record<string, unknown> | undefined\n}\n\nfunction resolveCustomerEntityId(record: Record<string, unknown>): string | null {\n const direct =\n record.customer_entity_id ??\n record.entityId ??\n record.entity_id ??\n record.customerEntityId ??\n record.customerEntityID ??\n (typeof record.entity === 'object' && record.entity ? (record.entity as Record<string, unknown>).id : undefined) ??\n (typeof record.customer_entity === 'object' && record.customer_entity ? (record.customer_entity as Record<string, unknown>).id : undefined)\n const value = typeof direct === 'string' && direct.length ? direct : null\n return value\n}\n\nfunction getInlineCustomerEntity(record: Record<string, unknown>): Record<string, unknown> | null {\n const inline =\n (typeof record.entity === 'object' && record.entity) ||\n (typeof record.customer_entity === 'object' && record.customer_entity) ||\n null\n return inline as Record<string, unknown> | null\n}\n\nasync function getLinkedTodo(ctx: SearchContext) {\n if (todoCache.has(ctx.record)) {\n return todoCache.get(ctx.record)\n }\n const sourceRaw = typeof ctx.record.todo_source === 'string' ? ctx.record.todo_source : EXAMPLE_TODO_SOURCE\n const [moduleId, entityName] = sourceRaw.split(':')\n const entityId = moduleId && entityName ? `${moduleId}:${entityName}` : CUSTOMER_INTERACTION_TASK_SOURCE\n const todo = await loadRecord(ctx, entityId, ctx.record.todo_id as string ?? ctx.record.todoId as string)\n todoCache.set(ctx.record, todo ?? null)\n return todo ?? null\n}\n\n// =============================================================================\n// URL and Formatting Helpers\n// =============================================================================\n\nfunction buildCustomerUrl(kind: string | null | undefined, id?: string | null): string | null {\n if (!id) return null\n const encoded = encodeURIComponent(id)\n if (kind === 'person') return `/backend/customers/people-v2/${encoded}`\n if (kind === 'company') return `/backend/customers/companies-v2/${encoded}`\n return `/backend/customers/companies-v2/${encoded}`\n}\n\nfunction formatDealValue(record: Record<string, unknown>): string | undefined {\n const amount = record.value_amount ?? record.valueAmount\n if (!amount) return undefined\n const currency = record.value_currency ?? record.valueCurrency ?? ''\n return currency ? `${amount} ${currency}` : String(amount)\n}\n\nfunction snippet(text: unknown, max = 140): string | undefined {\n if (typeof text !== 'string') return undefined\n const trimmed = text.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 = Array.isArray(value)\n ? value.map((item) => (item === null || item === undefined ? '' : String(item))).filter(Boolean).join(', ')\n : (typeof value === 'object' ? JSON.stringify(value) : String(value))\n if (!text.trim()) return\n lines.push(`${label}: ${text}`)\n}\n\nfunction friendlyLabel(input: string): string {\n return input\n .replace(/^cf:/, '')\n .replace(/_/g, ' ')\n .replace(/([a-z])([A-Z])/g, (_, a, b) => `${a} ${b}`)\n .replace(/\\b\\w/g, (char) => char.toUpperCase())\n}\n\nfunction appendCustomFieldLines(lines: string[], customFields: Record<string, unknown>, prefix: string) {\n for (const [key, value] of Object.entries(customFields)) {\n if (value === null || value === undefined) continue\n const label = prefix ? `${prefix} ${friendlyLabel(key)}` : friendlyLabel(key)\n appendLine(lines, label, value)\n }\n}\n\nfunction pickValue(source: Record<string, unknown> | null | undefined, ...keys: string[]): unknown {\n if (!source) return undefined\n for (const key of keys) {\n if (key in source && source[key] != null) return source[key]\n }\n return undefined\n}\n\nfunction pickString(...candidates: unknown[]): string | null {\n for (const candidate of candidates) {\n if (typeof candidate === 'string' && candidate.trim().length) {\n return candidate.trim()\n }\n }\n return null\n}\n\nfunction pickLabel(...candidates: Array<unknown>): string | null {\n for (const candidate of candidates) {\n if (candidate === null || candidate === undefined) continue\n const value = typeof candidate === 'string' ? candidate : String(candidate)\n const trimmed = value.trim()\n if (trimmed.length) return trimmed\n }\n return null\n}\n\nfunction appendCustomerEntityLines(\n lines: string[],\n entity: Record<string, unknown> | null,\n contactLabel: 'Customer' | 'Primary' = 'Customer',\n) {\n if (!entity) return\n appendLine(lines, 'Customer', pickValue(entity, 'display_name', 'displayName') ?? entity.id)\n appendLine(lines, `${contactLabel} email`, pickValue(entity, 'primary_email', 'primaryEmail'))\n appendLine(lines, `${contactLabel} phone`, pickValue(entity, 'primary_phone', 'primaryPhone'))\n appendLine(lines, 'Lifecycle stage', pickValue(entity, 'lifecycle_stage', 'lifecycleStage'))\n appendLine(lines, 'Status', pickValue(entity, 'status'))\n}\n\nfunction ensureFallbackLines(lines: string[], record: Record<string, unknown>, options: { includeId?: boolean } = {}) {\n if (lines.length) return\n const excluded = new Set(['tenant_id', 'organization_id', 'created_at', 'updated_at', 'deleted_at'])\n for (const [key, value] of Object.entries(record)) {\n if (value === null || value === undefined) continue\n if (excluded.has(key)) continue\n if (key === 'id') continue\n appendLine(lines, friendlyLabel(key), value)\n }\n if (!lines.length && options.includeId !== false) {\n const fallbackId =\n record.id ??\n record.entity_id ??\n record.customer_entity_id ??\n record.entityId ??\n record.customerEntityId ??\n null\n if (fallbackId) {\n appendLine(lines, 'Record ID', fallbackId)\n }\n }\n}\n\n// =============================================================================\n// Presenter Functions\n// =============================================================================\n\nfunction resolvePersonPresenter(\n translate: TranslateFn,\n record: Record<string, unknown>,\n entity: Record<string, unknown> | null,\n customFields: Record<string, unknown>,\n): SearchResultPresenter {\n const fallbackEntityId = resolveCustomerEntityId(record)\n const firstName = record.first_name ?? record.firstName ?? customFields.first_name ?? customFields.firstName ?? ''\n const lastName = record.last_name ?? record.lastName ?? customFields.last_name ?? customFields.lastName ?? ''\n const nameParts = [firstName, lastName].filter(Boolean).join(' ')\n const title =\n (pickValue(entity, 'display_name', 'displayName') as string | undefined) ??\n (record.preferred_name as string | undefined) ??\n (record.preferredName as string | undefined) ??\n (nameParts.length ? nameParts : undefined) ??\n fallbackEntityId ??\n (record.id as string | undefined) ??\n translate('customers.search.fallback.person', 'Person')\n const subtitlePieces: string[] = []\n const jobTitle = record.job_title ?? record.jobTitle ?? customFields.job_title ?? customFields.jobTitle\n if (jobTitle) subtitlePieces.push(String(jobTitle))\n const department = record.department ?? customFields.department\n if (department) subtitlePieces.push(String(department))\n const primaryEmail = pickValue(entity, 'primary_email', 'primaryEmail')\n if (primaryEmail) subtitlePieces.push(String(primaryEmail))\n const primaryPhone = pickValue(entity, 'primary_phone', 'primaryPhone')\n if (primaryPhone) subtitlePieces.push(String(primaryPhone))\n const summary = snippet(\n (pickValue(entity, 'description') as string | undefined) ??\n (customFields.summary as string | undefined) ??\n (customFields.description as string | undefined),\n )\n if (summary) subtitlePieces.push(summary)\n return {\n title: String(title),\n subtitle: subtitlePieces.length ? subtitlePieces.join(' \u00B7 ') : undefined,\n icon: 'user',\n badge: pickValue(entity, 'display_name', 'displayName')\n ? translate('customers.search.badge.person', 'Person')\n : undefined,\n }\n}\n\nfunction resolveCompanyPresenter(\n translate: TranslateFn,\n record: Record<string, unknown>,\n entity: Record<string, unknown> | null,\n customFields: Record<string, unknown>,\n): SearchResultPresenter {\n const fallbackEntityId = resolveCustomerEntityId(record)\n const title =\n (pickValue(entity, 'display_name', 'displayName') as string | undefined) ??\n (customFields.display_name as string | undefined) ??\n (customFields.displayName as string | undefined) ??\n (record.brand_name as string | undefined) ??\n (record.legal_name as string | undefined) ??\n (record.domain as string | undefined) ??\n (record.brandName as string | undefined) ??\n (record.legalName as string | undefined) ??\n (entity?.id && entity?.display_name ? entity.display_name as string : undefined) ??\n fallbackEntityId ??\n (record.id as string | undefined) ??\n translate('customers.search.fallback.company', 'Company')\n const subtitlePieces: string[] = []\n const industry = record.industry\n if (industry) subtitlePieces.push(String(industry))\n const sizeBucket = record.size_bucket ?? record.sizeBucket\n if (sizeBucket) subtitlePieces.push(String(sizeBucket))\n if (entity) {\n const primaryEmail = pickValue(entity, 'primary_email', 'primaryEmail')\n if (primaryEmail) subtitlePieces.push(String(primaryEmail))\n }\n const summary = snippet(\n (pickValue(entity, 'description') as string | undefined) ??\n (customFields.summary as string | undefined) ??\n (customFields.description as string | undefined) ??\n (record.summary as string | undefined) ??\n (record.description as string | undefined),\n )\n if (summary) subtitlePieces.push(summary)\n if (!entity && (!title || title === fallbackEntityId)) {\n logger.warn('Missing customer entity during company presenter build', {\n component: 'search',\n recordId: record.id ?? null,\n entityId: fallbackEntityId,\n recordKeys: Object.keys(record),\n })\n }\n return {\n title: String(title),\n subtitle: subtitlePieces.length ? subtitlePieces.join(' \u00B7 ') : undefined,\n icon: 'building',\n badge: pickValue(entity, 'display_name', 'displayName')\n ? translate('customers.search.badge.company', 'Company')\n : undefined,\n }\n}\n\nfunction logMissingPresenterTitle(\n kind: 'person' | 'company',\n record: Record<string, unknown>,\n entity: Record<string, unknown> | null,\n presenter: SearchResultPresenter,\n) {\n const fallbackId = record.id ?? record.entity_id ?? resolveCustomerEntityId(record)\n if (!fallbackId) return\n if (presenter.title && presenter.title !== String(fallbackId)) return\n logger.warn('Presenter fell back to record id', {\n component: 'search',\n kind,\n recordId: fallbackId,\n entityId: resolveCustomerEntityId(record),\n hasEntityDisplayName: entity?.display_name != null,\n })\n}\n\n// =============================================================================\n// Search Module Configuration\n// =============================================================================\n\nexport const searchConfig: SearchModuleConfig = {\n entities: [\n // =========================================================================\n // Person Profile\n // =========================================================================\n {\n entityId: 'customers:customer_person_profile',\n enabled: true,\n priority: 10,\n\n buildSource: async (ctx: SearchBuildContext): Promise<SearchIndexSource | null> => {\n assertTenantContext(ctx)\n const { t } = await resolveTranslations()\n const lines: string[] = []\n const record = ctx.record\n appendLine(lines, 'Preferred name', record.preferred_name ?? record.preferredName ?? ctx.customFields.preferred_name)\n appendLine(lines, 'First name', record.first_name ?? record.firstName ?? ctx.customFields.first_name)\n appendLine(lines, 'Last name', record.last_name ?? record.lastName ?? ctx.customFields.last_name)\n appendLine(lines, 'Job title', record.job_title ?? record.jobTitle ?? ctx.customFields.job_title)\n appendLine(lines, 'Department', record.department ?? record.department_name ?? record.departmentName ?? ctx.customFields.department)\n appendLine(lines, 'Seniority', record.seniority ?? record.seniority_level ?? record.seniorityLevel ?? ctx.customFields.seniority)\n appendLine(lines, 'Timezone', record.timezone ?? record.time_zone ?? record.timeZone ?? ctx.customFields.timezone)\n appendLine(lines, 'LinkedIn', record.linked_in_url ?? record.linkedInUrl ?? ctx.customFields.linked_in_url)\n appendLine(lines, 'Twitter', record.twitter_url ?? record.twitterUrl ?? ctx.customFields.twitter_url)\n\n const { entity, entityId, profileCustomFields, entityCustomFields, entityOnlyCustomFields } =\n await hydrateProfileContext(ctx, 'person')\n appendCustomFieldLines(lines, profileCustomFields, 'Person custom')\n if (Object.keys(entityOnlyCustomFields).length) {\n appendCustomFieldLines(lines, entityOnlyCustomFields, 'Customer custom')\n }\n if (!entity) {\n logger.warn('Failed to load customer entity for person profile', {\n component: 'search',\n recordId: record.id,\n entityId,\n recordKeys: Object.keys(record),\n })\n }\n appendCustomerEntityLines(lines, entity, 'Customer')\n ensureFallbackLines(lines, record)\n if (!lines.length) return null\n\n if (!entityId) {\n logger.warn('Person profile missing entity id', {\n component: 'search',\n recordId: record.id,\n recordKeys: Object.keys(record),\n })\n }\n\n const presenter = resolvePersonPresenter(t, record, entity, ctx.customFields)\n logMissingPresenterTitle('person', record, entity, presenter)\n const presenterLabel = pickLabel(presenter.title) ?? t('customers.search.link.openPerson', 'Open person')\n const links: SearchResultLink[] = []\n if (entityId) {\n const href = buildCustomerUrl('person', entityId)\n if (href) {\n links.push({ href, label: presenterLabel, kind: 'primary' })\n }\n }\n\n return {\n text: lines,\n presenter,\n links,\n checksumSource: {\n record: ctx.record,\n customFields: profileCustomFields,\n entity,\n entityCustomFields,\n },\n }\n },\n\n formatResult: async (ctx: SearchBuildContext): Promise<SearchResultPresenter | null> => {\n assertTenantContext(ctx)\n const { t } = await resolveTranslations()\n const entity = await getCustomerEntity(ctx, resolveCustomerEntityId(ctx.record))\n return resolvePersonPresenter(t, ctx.record, entity, ctx.customFields)\n },\n\n resolveUrl: async (ctx: SearchBuildContext): Promise<string | null> => {\n const entityId = resolveCustomerEntityId(ctx.record)\n return buildCustomerUrl('person', entityId)\n },\n\n resolveLinks: async (ctx: SearchBuildContext): Promise<SearchResultLink[] | null> => {\n const { t } = await resolveTranslations()\n const entityId = resolveCustomerEntityId(ctx.record)\n if (!entityId) return null\n const href = buildCustomerUrl('person', entityId)\n if (!href) return null\n return [{ href: `${href}/edit`, label: t('customers.search.link.edit', 'Edit'), kind: 'secondary' }]\n },\n\n fieldPolicy: {\n searchable: [\n 'preferred_name',\n 'first_name',\n 'last_name',\n 'job_title',\n 'department',\n 'seniority',\n 'timezone',\n 'linked_in_url',\n 'twitter_url',\n ],\n hashOnly: ['primary_email', 'primary_phone', 'personal_email'],\n excluded: ['date_of_birth', 'government_id', 'ssn', 'tax_id'],\n },\n aclFeatures: ['customers.people.view'],\n },\n\n // =========================================================================\n // Company Profile\n // =========================================================================\n {\n entityId: 'customers:customer_company_profile',\n enabled: true,\n priority: 10,\n\n buildSource: async (ctx: SearchBuildContext): Promise<SearchIndexSource | null> => {\n assertTenantContext(ctx)\n const { t } = await resolveTranslations()\n const lines: string[] = []\n const record = ctx.record\n appendLine(lines, 'Legal name', record.legal_name ?? record.legalName ?? ctx.customFields.legal_name)\n appendLine(lines, 'Brand name', record.brand_name ?? record.brandName ?? ctx.customFields.brand_name)\n appendLine(lines, 'Domain', record.domain ?? record.website_domain ?? record.websiteDomain ?? ctx.customFields.domain)\n appendLine(lines, 'Website', record.website_url ?? record.websiteUrl ?? ctx.customFields.website_url)\n appendLine(lines, 'Industry', record.industry ?? ctx.customFields.industry)\n appendLine(lines, 'Company size', record.size_bucket ?? record.sizeBucket ?? ctx.customFields.size_bucket)\n appendLine(lines, 'Annual revenue', record.annual_revenue ?? record.annualRevenue ?? ctx.customFields.annual_revenue)\n\n const { entity, entityId, profileCustomFields, entityCustomFields, entityOnlyCustomFields } =\n await hydrateProfileContext(ctx, 'company')\n appendCustomFieldLines(lines, profileCustomFields, 'Company custom')\n if (Object.keys(entityOnlyCustomFields).length) {\n appendCustomFieldLines(lines, entityOnlyCustomFields, 'Customer custom')\n }\n appendCustomerEntityLines(lines, entity, 'Primary')\n ensureFallbackLines(lines, record)\n if (!lines.length) return null\n\n const presenter = resolveCompanyPresenter(t, record, entity, ctx.customFields)\n logMissingPresenterTitle('company', record, entity, presenter)\n const primaryLabel = pickLabel(presenter.title) ?? t('customers.search.link.openCompany', 'Open company')\n const links: SearchResultLink[] = []\n if (entityId) {\n const href = buildCustomerUrl('company', entityId)\n if (href) {\n links.push({ href, label: primaryLabel, kind: 'primary' })\n }\n }\n\n return {\n text: lines,\n presenter,\n links,\n checksumSource: {\n record: ctx.record,\n customFields: profileCustomFields,\n entity,\n entityCustomFields,\n },\n }\n },\n\n formatResult: async (ctx: SearchBuildContext): Promise<SearchResultPresenter | null> => {\n assertTenantContext(ctx)\n const { t } = await resolveTranslations()\n const entity = await getCustomerEntity(ctx, resolveCustomerEntityId(ctx.record))\n return resolveCompanyPresenter(t, ctx.record, entity, ctx.customFields)\n },\n\n resolveUrl: async (ctx: SearchBuildContext): Promise<string | null> => {\n const entityId = resolveCustomerEntityId(ctx.record)\n return buildCustomerUrl('company', entityId)\n },\n\n resolveLinks: async (ctx: SearchBuildContext): Promise<SearchResultLink[] | null> => {\n const { t } = await resolveTranslations()\n const entityId = resolveCustomerEntityId(ctx.record)\n if (!entityId) return null\n const href = buildCustomerUrl('company', entityId)\n if (!href) return null\n return [{ href: `${href}/edit`, label: t('customers.search.link.edit', 'Edit'), kind: 'secondary' }]\n },\n\n fieldPolicy: {\n searchable: [\n 'legal_name',\n 'brand_name',\n 'display_name',\n 'domain',\n 'website_url',\n 'industry',\n 'size_bucket',\n 'description',\n ],\n hashOnly: ['tax_id', 'registration_number'],\n excluded: ['bank_account', 'billing_info', 'credit_info'],\n },\n aclFeatures: ['customers.companies.view'],\n },\n\n // =========================================================================\n // Customer Comment\n // =========================================================================\n {\n entityId: 'customers:customer_comment',\n enabled: true,\n priority: 6,\n\n buildSource: async (ctx: SearchBuildContext): Promise<SearchIndexSource | null> => {\n assertTenantContext(ctx)\n const parent = await getCustomerEntity(ctx, ctx.record.entity_id as string ?? ctx.record.entityId as string)\n const lines: string[] = []\n if (parent?.display_name) lines.push(`Customer: ${parent.display_name}`)\n lines.push(`Note: ${ctx.record.body ?? ''}`)\n if (ctx.record.appearance_icon) lines.push(`Icon: ${ctx.record.appearance_icon}`)\n if (ctx.record.appearance_color) lines.push(`Color: ${ctx.record.appearance_color}`)\n\n const presenter: SearchResultPresenter | undefined = parent?.display_name\n ? {\n title: parent.display_name as string,\n subtitle: snippet(ctx.record.body),\n icon: parent.kind === 'person' ? 'user' : 'building',\n }\n : undefined\n\n return {\n text: lines,\n presenter,\n checksumSource: {\n body: ctx.record.body,\n entityId: ctx.record.entity_id ?? null,\n updatedAt: ctx.record.updated_at ?? ctx.record.updatedAt ?? null,\n },\n }\n },\n\n formatResult: async (ctx: SearchBuildContext): Promise<SearchResultPresenter | null> => {\n assertTenantContext(ctx)\n const { t } = await resolveTranslations()\n const parent = await getCustomerEntity(ctx, ctx.record.entity_id as string ?? ctx.record.entityId as string)\n const title = (parent?.display_name as string | undefined) ?? t('customers.search.fallback.customerNote', 'Customer note')\n return {\n title,\n subtitle: snippet(ctx.record.body),\n icon: 'sticky-note',\n }\n },\n\n resolveUrl: async (ctx: SearchBuildContext): Promise<string | null> => {\n assertTenantContext(ctx)\n const parent = await getCustomerEntity(ctx, ctx.record.entity_id as string ?? ctx.record.entityId as string)\n const base = buildCustomerUrl(parent?.kind as string ?? null, (parent?.id ?? ctx.record.entity_id ?? ctx.record.entityId) as string)\n return base ? `${base}#notes` : null\n },\n\n resolveLinks: async (ctx: SearchBuildContext): Promise<SearchResultLink[] | null> => {\n assertTenantContext(ctx)\n const { t } = await resolveTranslations()\n const links: SearchResultLink[] = []\n const parent = await getCustomerEntity(ctx, ctx.record.entity_id as string ?? ctx.record.entityId as string)\n const parentUrl = buildCustomerUrl(parent?.kind as string ?? null, (parent?.id ?? ctx.record.entity_id ?? ctx.record.entityId) as string)\n if (parentUrl) {\n links.push({ href: parentUrl, label: (parent?.display_name as string | undefined) ?? t('customers.search.link.viewCustomer', 'View customer'), kind: 'primary' })\n }\n if (ctx.record.deal_id) {\n const dealUrl = `/backend/customers/deals/${encodeURIComponent(ctx.record.deal_id as string)}`\n links.push({ href: dealUrl, label: t('customers.search.link.openDeal', 'Open deal'), kind: 'secondary' })\n }\n return links.length ? links : null\n },\n\n fieldPolicy: {\n searchable: ['body'],\n hashOnly: [],\n excluded: [],\n },\n aclFeatures: ['customers.activities.view'],\n },\n\n // =========================================================================\n // Customer Deal\n // =========================================================================\n {\n entityId: 'customers:customer_deal',\n enabled: true,\n priority: 8,\n\n buildSource: async (ctx: SearchBuildContext): Promise<SearchIndexSource | null> => {\n const { t } = await resolveTranslations()\n const lines: string[] = []\n const record = ctx.record\n appendLine(lines, 'Title', record.title)\n appendLine(lines, 'Stage', record.pipeline_stage)\n appendLine(lines, 'Status', record.status)\n appendLine(lines, 'Source', record.source)\n const value = formatDealValue(record)\n if (value) appendLine(lines, 'Value', value)\n if (!lines.length) return null\n\n const subtitleParts: string[] = []\n if (record.pipeline_stage) subtitleParts.push(String(record.pipeline_stage))\n if (record.status) subtitleParts.push(String(record.status))\n if (value) subtitleParts.push(value)\n\n return {\n text: lines,\n presenter: {\n title: String(record.title ?? t('customers.search.fallback.deal', 'Deal')),\n subtitle: subtitleParts.join(' \u00B7 ') || undefined,\n icon: 'briefcase',\n badge: t('customers.search.badge.deal', 'Deal'),\n },\n checksumSource: {\n title: record.title,\n status: record.status,\n stage: record.pipeline_stage,\n value: value,\n },\n }\n },\n\n formatResult: async (ctx: SearchBuildContext): Promise<SearchResultPresenter | null> => {\n const { t } = await resolveTranslations()\n const { record } = ctx\n const title = pickString(record.title as string, t('customers.search.fallback.deal', 'Deal'))\n const subtitleParts: string[] = []\n if (record.pipeline_stage) subtitleParts.push(String(record.pipeline_stage))\n if (record.status) subtitleParts.push(String(record.status))\n const amount = record.value_amount ?? record.valueAmount\n const currency = record.value_currency ?? record.valueCurrency\n if (amount) {\n subtitleParts.push(currency ? `${amount} ${currency}` : String(amount))\n }\n\n return {\n title: title ?? t('customers.search.fallback.deal', 'Deal'),\n subtitle: subtitleParts.length ? subtitleParts.join(' \u00B7 ') : undefined,\n icon: 'briefcase',\n badge: t('customers.search.badge.deal', 'Deal'),\n }\n },\n\n resolveUrl: async (ctx: SearchBuildContext): Promise<string | null> => {\n const id = ctx.record.id\n if (!id) return null\n return `/backend/customers/deals/${encodeURIComponent(String(id))}`\n },\n\n resolveLinks: async (ctx: SearchBuildContext): Promise<SearchResultLink[] | null> => {\n const { t } = await resolveTranslations()\n const id = ctx.record.id\n if (!id) return null\n return [\n {\n href: `/backend/customers/deals/${encodeURIComponent(String(id))}/edit`,\n label: t('customers.search.link.edit', 'Edit'),\n kind: 'secondary',\n },\n ]\n },\n\n fieldPolicy: {\n searchable: ['title', 'description', 'pipeline_stage', 'status', 'source'],\n hashOnly: [],\n excluded: ['value_amount', 'value_currency'],\n },\n aclFeatures: ['customers.deals.view'],\n },\n\n // =========================================================================\n // Customer Activity\n // =========================================================================\n {\n entityId: 'customers:customer_activity',\n enabled: true,\n priority: 5,\n\n buildSource: async (ctx: SearchBuildContext): Promise<SearchIndexSource | null> => {\n assertTenantContext(ctx)\n const { t } = await resolveTranslations()\n const parent = await getCustomerEntity(ctx, ctx.record.entity_id as string ?? ctx.record.entityId as string)\n const lines: string[] = []\n if (parent?.display_name) lines.push(`Customer: ${parent.display_name}`)\n if (ctx.record.activity_type) lines.push(`Type: ${ctx.record.activity_type}`)\n if (ctx.record.subject) lines.push(`Subject: ${ctx.record.subject}`)\n if (ctx.record.body) lines.push(`Body: ${ctx.record.body}`)\n\n const presenter: SearchResultPresenter = {\n title: ctx.record.subject\n ? String(ctx.record.subject)\n : t('customers.search.fallback.activity', 'Activity: {{type}}', {\n type: String(ctx.record.activity_type ?? t('customers.search.fallback.activityUpdate', 'update')),\n }),\n subtitle: (parent?.display_name as string | undefined) ?? snippet(ctx.record.body),\n icon: 'bolt',\n }\n\n return {\n text: lines,\n presenter,\n checksumSource: {\n subject: ctx.record.subject,\n body: ctx.record.body,\n entityId: ctx.record.entity_id ?? null,\n updatedAt: ctx.record.updated_at ?? ctx.record.updatedAt ?? null,\n },\n }\n },\n\n formatResult: async (ctx: SearchBuildContext): Promise<SearchResultPresenter | null> => {\n assertTenantContext(ctx)\n const { t } = await resolveTranslations()\n const parent = await getCustomerEntity(ctx, ctx.record.entity_id as string ?? ctx.record.entityId as string)\n return {\n title: ctx.record.subject\n ? String(ctx.record.subject)\n : t('customers.search.fallback.activity', 'Activity: {{type}}', {\n type: String(ctx.record.activity_type ?? t('customers.search.fallback.activityUpdate', 'update')),\n }),\n subtitle: (parent?.display_name as string | undefined) ?? snippet(ctx.record.body),\n icon: 'bolt',\n badge: t('customers.search.badge.activity', 'Activity'),\n }\n },\n\n resolveUrl: async (ctx: SearchBuildContext): Promise<string | null> => {\n assertTenantContext(ctx)\n const parent = await getCustomerEntity(ctx, ctx.record.entity_id as string ?? ctx.record.entityId as string)\n const base = buildCustomerUrl(parent?.kind as string ?? null, (parent?.id ?? ctx.record.entity_id ?? ctx.record.entityId) as string)\n return base ? `${base}#activity-${ctx.record.id ?? ctx.record.activity_id ?? ''}` : null\n },\n\n resolveLinks: async (ctx: SearchBuildContext): Promise<SearchResultLink[] | null> => {\n const { t } = await resolveTranslations()\n const links: SearchResultLink[] = []\n if (ctx.record.deal_id) {\n links.push({\n href: `/backend/customers/deals/${encodeURIComponent(ctx.record.deal_id as string)}`,\n label: t('customers.search.link.openDeal', 'Open deal'),\n kind: 'secondary',\n })\n }\n return links.length ? links : null\n },\n\n fieldPolicy: {\n searchable: ['subject', 'body', 'activity_type'],\n hashOnly: [],\n excluded: [],\n },\n aclFeatures: ['customers.activities.view'],\n },\n\n // =========================================================================\n // Customer Todo Link\n // =========================================================================\n {\n entityId: 'customers:customer_todo_link',\n enabled: true,\n priority: 4,\n\n buildSource: async (ctx: SearchBuildContext): Promise<SearchIndexSource | null> => {\n assertTenantContext(ctx)\n const todo = await getLinkedTodo(ctx) as Record<string, unknown> | null\n const parent = await getCustomerEntity(ctx, ctx.record.entity_id as string ?? ctx.record.entityId as string)\n const lines: string[] = []\n if (todo?.title) lines.push(`Todo: ${todo.title}`)\n if (todo?.is_done !== undefined) lines.push(`Status: ${todo.is_done ? 'Done' : 'Open'}`)\n if (parent?.display_name) lines.push(`Customer: ${parent.display_name}`)\n if (!lines.length) return null\n\n return {\n text: lines,\n presenter: todo?.title\n ? { title: todo.title as string, subtitle: parent?.display_name as string | undefined, icon: 'check-square' }\n : undefined,\n checksumSource: {\n todoId: ctx.record.todo_id ?? ctx.record.todoId,\n todoSource: ctx.record.todo_source ?? ctx.record.todoSource,\n entityId: ctx.record.entity_id ?? ctx.record.entityId,\n },\n }\n },\n\n formatResult: async (ctx: SearchBuildContext): Promise<SearchResultPresenter | null> => {\n assertTenantContext(ctx)\n const { t } = await resolveTranslations()\n const todo = await getLinkedTodo(ctx) as Record<string, unknown> | null\n const parent = await getCustomerEntity(ctx, ctx.record.entity_id as string ?? ctx.record.entityId as string)\n return {\n title: (todo?.title as string | undefined) ?? t('customers.search.fallback.customerTask', 'Customer task'),\n subtitle: parent?.display_name as string | undefined,\n icon: 'check-square',\n }\n },\n\n resolveUrl: async (ctx: SearchBuildContext): Promise<string | null> => {\n assertTenantContext(ctx)\n const parent = await getCustomerEntity(ctx, ctx.record.entity_id as string ?? ctx.record.entityId as string)\n const base = buildCustomerUrl(parent?.kind as string ?? null, (parent?.id ?? ctx.record.entity_id ?? ctx.record.entityId) as string)\n return base ? `${base}#tasks` : null\n },\n\n resolveLinks: async (ctx: SearchBuildContext): Promise<SearchResultLink[] | null> => {\n const { t } = await resolveTranslations()\n const todoId = ctx.record.todo_id ?? ctx.record.todoId\n if (!todoId) return null\n return [{\n href: `/backend/todos/${encodeURIComponent(todoId as string)}/edit`,\n label: t('customers.search.link.openTodo', 'Open todo'),\n kind: 'secondary',\n }]\n },\n\n fieldPolicy: {\n searchable: [],\n hashOnly: [],\n excluded: [],\n },\n aclFeatures: ['customers.activities.view'],\n },\n ],\n}\n\nexport default searchConfig\nexport const config = searchConfig\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import type { QueryCustomFieldSource, QueryEngine } from '@open-mercato/shared/lib/query/types'\nimport type {\n SearchModuleConfig,\n SearchBuildContext,\n SearchResultPresenter,\n SearchResultLink,\n SearchIndexSource,\n} from '@open-mercato/shared/modules/search'\nimport type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { CUSTOMER_INTERACTION_TASK_SOURCE, EXAMPLE_TODO_SOURCE } from './lib/interactionCompatibility'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { defaultEncryptionMaps } from './encryption'\nimport type { TenantDataEncryptionService } from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'\nimport { isTenantDataEncryptionEnabled } from '@open-mercato/shared/lib/encryption/toggles'\n\nconst logger = createLogger('customers')\n\n// =============================================================================\n// Context Types\n// =============================================================================\n\ntype SearchContext = SearchBuildContext & {\n tenantId: string\n queryEngine?: QueryEngine\n}\n\nfunction assertTenantContext(ctx: SearchBuildContext): asserts ctx is SearchContext {\n if (typeof ctx.tenantId !== 'string' || ctx.tenantId.length === 0) {\n throw new Error('[search.customers] Missing tenantId in search build context')\n }\n}\n\ntype CustomerProfileKind = 'person' | 'company'\n\ntype LoadedCustomerEntity = {\n entity: Record<string, unknown> | null\n customFields: Record<string, unknown>\n}\n\n// =============================================================================\n// Caching\n// =============================================================================\n\nconst entityIdCache = new Map<string, LoadedCustomerEntity | null>()\nconst profileEntityCache = new WeakMap<Record<string, unknown>, Partial<Record<CustomerProfileKind, LoadedCustomerEntity | null>>>()\nconst todoCache = new WeakMap<Record<string, unknown>, unknown>()\n\n// =============================================================================\n// Query Configuration\n// =============================================================================\n\nconst CUSTOMER_ENTITY_FIELDS = [\n 'id',\n 'kind',\n 'display_name',\n 'description',\n 'primary_email',\n 'primary_phone',\n 'status',\n 'lifecycle_stage',\n 'owner_user_id',\n 'source',\n 'next_interaction_at',\n 'next_interaction_name',\n 'next_interaction_ref_id',\n 'next_interaction_icon',\n 'next_interaction_color',\n 'organization_id',\n 'tenant_id',\n 'created_at',\n 'updated_at',\n 'deleted_at',\n] satisfies string[]\n\nconst CUSTOMER_CUSTOM_FIELD_SOURCES: QueryCustomFieldSource[] = [\n {\n entityId: 'customers:customer_person_profile',\n table: 'customer_people',\n alias: 'person_profile',\n recordIdColumn: 'id',\n join: { fromField: 'id', toField: 'entity_id' },\n },\n {\n entityId: 'customers:customer_company_profile',\n table: 'customer_companies',\n alias: 'company_profile',\n recordIdColumn: 'id',\n join: { fromField: 'id', toField: 'entity_id' },\n },\n]\n\n// =============================================================================\n// Helper Functions\n// =============================================================================\n\nfunction extractCustomFieldMap(source: Record<string, unknown> | null | undefined): Record<string, unknown> {\n if (!source) return {}\n const result: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(source)) {\n if (value === undefined) continue\n if (key.startsWith('cf:')) {\n result[key.slice(3)] = value\n } else if (key.startsWith('cf_')) {\n result[key.slice(3)] = value\n }\n }\n return result\n}\n\nfunction normalizeCustomerEntity(row: Record<string, unknown>): Record<string, unknown> {\n const normalized: Record<string, unknown> = {\n id: row.id ?? row.entity_id ?? row.entityId ?? null,\n kind: row.kind ?? null,\n }\n const assign = (snake: string, camel?: string) => {\n const value = row[snake] ?? (camel ? row[camel] : undefined)\n if (value !== undefined) {\n normalized[snake] = value\n if (camel) normalized[camel] = value\n }\n }\n assign('display_name', 'displayName')\n assign('description')\n assign('primary_email', 'primaryEmail')\n assign('primary_phone', 'primaryPhone')\n assign('status')\n assign('lifecycle_stage', 'lifecycleStage')\n assign('owner_user_id', 'ownerUserId')\n assign('source')\n assign('next_interaction_at', 'nextInteractionAt')\n assign('next_interaction_name', 'nextInteractionName')\n assign('next_interaction_ref_id', 'nextInteractionRefId')\n assign('next_interaction_icon', 'nextInteractionIcon')\n assign('next_interaction_color', 'nextInteractionColor')\n assign('organization_id', 'organizationId')\n assign('tenant_id', 'tenantId')\n assign('created_at', 'createdAt')\n assign('updated_at', 'updatedAt')\n assign('deleted_at', 'deletedAt')\n return normalized\n}\n\nfunction getProfileCache(record: Record<string, unknown>): Partial<Record<CustomerProfileKind, LoadedCustomerEntity | null>> {\n let cache = profileEntityCache.get(record)\n if (!cache) {\n cache = {}\n profileEntityCache.set(record, cache)\n }\n return cache\n}\n\nfunction subtractCustomFields(\n primary: Record<string, unknown>,\n secondary: Record<string, unknown>,\n): Record<string, unknown> {\n if (!secondary || Object.keys(secondary).length === 0) return {}\n const primaryKeys = new Set(Object.keys(primary))\n const result: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(secondary)) {\n if (!primaryKeys.has(key)) {\n result[key] = value\n }\n }\n return result\n}\n\n// =============================================================================\n// Entity Loading Functions\n// =============================================================================\n\ntype CustomerEntityQueryOptions = {\n entityId?: string | null\n profileKind?: CustomerProfileKind\n profileId?: string | null\n}\n\nasync function loadCustomerEntityBundle(ctx: SearchContext, opts: CustomerEntityQueryOptions): Promise<LoadedCustomerEntity | null> {\n if (!ctx.queryEngine) return null\n const filters: Record<string, unknown> = {}\n const resolvedEntityId = typeof opts.entityId === 'string' && opts.entityId.length ? opts.entityId : null\n const resolvedProfileId =\n opts.profileId != null && String(opts.profileId).trim().length > 0 ? String(opts.profileId).trim() : null\n const shouldJoinProfileSource = Boolean(opts.profileKind && resolvedProfileId && !resolvedEntityId)\n if (resolvedEntityId) {\n filters.id = { $eq: resolvedEntityId }\n }\n if (shouldJoinProfileSource) {\n const alias = opts.profileKind === 'person' ? 'person_profile' : 'company_profile'\n filters[`${alias}.id`] = { $eq: resolvedProfileId }\n }\n if (!Object.keys(filters).length) return null\n try {\n const result = await ctx.queryEngine.query('customers:customer_entity', {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? undefined,\n filters,\n includeCustomFields: true,\n ...(shouldJoinProfileSource ? { customFieldSources: CUSTOMER_CUSTOM_FIELD_SOURCES } : {}),\n fields: CUSTOMER_ENTITY_FIELDS,\n page: { page: 1, pageSize: 1 },\n })\n const row = result.items[0] as Record<string, unknown> | undefined\n if (!row) return null\n const entity = normalizeCustomerEntity(row)\n const customFields = extractCustomFieldMap(row)\n return { entity, customFields }\n } catch (error) {\n logger.warn('Failed to load customer entity via QueryEngine', {\n component: 'search',\n entityId: resolvedEntityId ?? null,\n profileKind: opts.profileKind ?? null,\n profileId: resolvedProfileId ?? null,\n err: error,\n })\n return null\n }\n}\n\nasync function loadCustomerEntityForProfile(ctx: SearchContext, kind: CustomerProfileKind): Promise<LoadedCustomerEntity | null> {\n const cache = getProfileCache(ctx.record)\n if (cache[kind] !== undefined) return cache[kind] ?? null\n const entityIdHint = resolveCustomerEntityId(ctx.record)\n const profileIdRaw = ctx.record.id ?? null\n const profileId = profileIdRaw != null ? String(profileIdRaw) : null\n if (!entityIdHint && !profileId) {\n cache[kind] = null\n return null\n }\n const loaded = await loadCustomerEntityBundle(ctx, {\n entityId: entityIdHint,\n profileKind: kind,\n profileId,\n })\n cache[kind] = loaded ?? null\n const resolvedId = loaded?.entity?.id ?? entityIdHint\n if (resolvedId && typeof resolvedId === 'string') {\n ctx.record.entity_id ??= resolvedId\n ctx.record.entityId ??= resolvedId\n entityIdCache.set(resolvedId, loaded ?? null)\n }\n if (loaded?.entity) {\n if (!ctx.record.entity) ctx.record.entity = loaded.entity\n if (!ctx.record.customer_entity) ctx.record.customer_entity = loaded.entity\n }\n return loaded ?? null\n}\n\nasync function loadCustomerEntityById(ctx: SearchContext, entityId: string | null | undefined): Promise<LoadedCustomerEntity | null> {\n const resolvedId = typeof entityId === 'string' && entityId.length ? entityId : null\n if (!resolvedId) return null\n if (entityIdCache.has(resolvedId)) {\n return entityIdCache.get(resolvedId) ?? null\n }\n const loaded = await loadCustomerEntityBundle(ctx, { entityId: resolvedId })\n entityIdCache.set(resolvedId, loaded ?? null)\n return loaded ?? null\n}\n\nasync function getCustomerEntity(ctx: SearchContext, entityId?: string | null): Promise<Record<string, unknown> | null> {\n const profileCache = profileEntityCache.get(ctx.record)\n if (profileCache) {\n const cached = Object.values(profileCache).find((entry) => {\n if (!entry?.entity) return false\n if (!entityId) return true\n return entry.entity.id === entityId\n })\n if (cached?.entity) return cached.entity\n }\n const inline = getInlineCustomerEntity(ctx.record)\n if (inline && (!entityId || inline.id === entityId)) {\n if (inline.id && typeof inline.id === 'string') {\n entityIdCache.set(inline.id, { entity: inline, customFields: {} })\n }\n return inline\n }\n const resolvedId = entityId ?? resolveCustomerEntityId(ctx.record)\n const loaded = await loadCustomerEntityById(ctx, resolvedId)\n return loaded?.entity ?? null\n}\n\ntype HydratedProfileContext = {\n entity: Record<string, unknown> | null\n entityId: string | null\n profileCustomFields: Record<string, unknown>\n entityCustomFields: Record<string, unknown>\n entityOnlyCustomFields: Record<string, unknown>\n}\n\nasync function hydrateProfileContext(ctx: SearchContext, kind: CustomerProfileKind): Promise<HydratedProfileContext> {\n const profileCustomFields = ctx.customFields ?? {}\n const loaded = await loadCustomerEntityForProfile(ctx, kind)\n let entity = loaded?.entity ?? getInlineCustomerEntity(ctx.record)\n let entityCustomFields = loaded?.customFields ?? {}\n let entityId = (entity?.id as string | undefined) ?? resolveCustomerEntityId(ctx.record)\n if (!entity && entityId) {\n const fetched = await loadCustomerEntityById(ctx, entityId)\n entity = fetched?.entity ?? null\n if (fetched?.customFields) {\n entityCustomFields = Object.keys(entityCustomFields).length ? entityCustomFields : fetched.customFields\n }\n }\n if (!entity && !entityId) {\n entityId = resolveCustomerEntityId(ctx.record)\n }\n if (entity?.id && typeof entity.id === 'string') {\n entityId = entity.id\n ctx.record.entity_id ??= entity.id\n ctx.record.entityId ??= entity.id\n if (!ctx.record.entity) ctx.record.entity = entity\n if (!ctx.record.customer_entity) ctx.record.customer_entity = entity\n }\n const entityOnlyCustomFields = subtractCustomFields(profileCustomFields, entityCustomFields)\n return {\n entity: entity ?? null,\n entityId: entityId ?? null,\n profileCustomFields,\n entityCustomFields,\n entityOnlyCustomFields,\n }\n}\n\nasync function loadRecord(ctx: SearchContext, entityId: string, recordId?: string | null) {\n if (!recordId || !ctx.queryEngine) return null\n const res = await ctx.queryEngine.query(entityId, {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? undefined,\n filters: { id: recordId },\n includeCustomFields: true,\n page: { page: 1, pageSize: 1 },\n })\n return res.items[0] as Record<string, unknown> | undefined\n}\n\nfunction resolveCustomerEntityId(record: Record<string, unknown>): string | null {\n const direct =\n record.customer_entity_id ??\n record.entityId ??\n record.entity_id ??\n record.customerEntityId ??\n record.customerEntityID ??\n (typeof record.entity === 'object' && record.entity ? (record.entity as Record<string, unknown>).id : undefined) ??\n (typeof record.customer_entity === 'object' && record.customer_entity ? (record.customer_entity as Record<string, unknown>).id : undefined)\n const value = typeof direct === 'string' && direct.length ? direct : null\n return value\n}\n\nfunction getInlineCustomerEntity(record: Record<string, unknown>): Record<string, unknown> | null {\n const inline =\n (typeof record.entity === 'object' && record.entity) ||\n (typeof record.customer_entity === 'object' && record.customer_entity) ||\n null\n return inline as Record<string, unknown> | null\n}\n\nasync function getLinkedTodo(ctx: SearchContext) {\n if (todoCache.has(ctx.record)) {\n return todoCache.get(ctx.record)\n }\n const sourceRaw = typeof ctx.record.todo_source === 'string' ? ctx.record.todo_source : EXAMPLE_TODO_SOURCE\n const [moduleId, entityName] = sourceRaw.split(':')\n const entityId = moduleId && entityName ? `${moduleId}:${entityName}` : CUSTOMER_INTERACTION_TASK_SOURCE\n const todo = await loadRecord(ctx, entityId, ctx.record.todo_id as string ?? ctx.record.todoId as string)\n todoCache.set(ctx.record, todo ?? null)\n return todo ?? null\n}\n\n// =============================================================================\n// URL and Formatting Helpers\n// =============================================================================\n\nfunction buildCustomerUrl(kind: string | null | undefined, id?: string | null): string | null {\n if (!id) return null\n const encoded = encodeURIComponent(id)\n if (kind === 'person') return `/backend/customers/people-v2/${encoded}`\n if (kind === 'company') return `/backend/customers/companies-v2/${encoded}`\n return `/backend/customers/companies-v2/${encoded}`\n}\n\nfunction formatDealValue(record: Record<string, unknown>): string | undefined {\n const amount = record.value_amount ?? record.valueAmount\n if (!amount) return undefined\n const currency = record.value_currency ?? record.valueCurrency ?? ''\n return currency ? `${amount} ${currency}` : String(amount)\n}\n\nfunction snippet(text: unknown, max = 140): string | undefined {\n if (typeof text !== 'string') return undefined\n const trimmed = text.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 = Array.isArray(value)\n ? value.map((item) => (item === null || item === undefined ? '' : String(item))).filter(Boolean).join(', ')\n : (typeof value === 'object' ? JSON.stringify(value) : String(value))\n if (!text.trim()) return\n lines.push(`${label}: ${text}`)\n}\n\nfunction friendlyLabel(input: string): string {\n return input\n .replace(/^cf:/, '')\n .replace(/_/g, ' ')\n .replace(/([a-z])([A-Z])/g, (_, a, b) => `${a} ${b}`)\n .replace(/\\b\\w/g, (char) => char.toUpperCase())\n}\n\nfunction appendCustomFieldLines(lines: string[], customFields: Record<string, unknown>, prefix: string) {\n for (const [key, value] of Object.entries(customFields)) {\n if (value === null || value === undefined) continue\n const label = prefix ? `${prefix} ${friendlyLabel(key)}` : friendlyLabel(key)\n appendLine(lines, label, value)\n }\n}\n\nfunction normalizeSearchFieldKey(key: string): string {\n return key.replace(/^cf[:_]/, '').replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase()\n}\n\nasync function resolveProfileEncryptedFields(ctx: SearchContext, kind: CustomerProfileKind, entity: Record<string, unknown> | null): Promise<string[]> {\n if (!isTenantDataEncryptionEnabled()) {\n throw new Error('[internal] Customer vector indexing requires available encryption declarations')\n }\n const container = ctx.container as { resolve?: (name: string) => unknown } | undefined\n if (typeof container?.resolve !== 'function') {\n throw new Error('[internal] Customer search encryption policy container is unavailable')\n }\n const service = container.resolve('tenantEncryptionService') as Pick<TenantDataEncryptionService, 'getEncryptedFieldNames'> | undefined\n if (typeof service?.getEncryptedFieldNames !== 'function') {\n throw new Error('[internal] Customer search encryption policy service is unavailable')\n }\n const organizationIds = new Set([ctx.organizationId])\n for (const record of [ctx.record, entity]) {\n const organizationId = record?.organization_id ?? record?.organizationId\n if (typeof organizationId === 'string' && organizationId) organizationIds.add(organizationId)\n }\n const fields = await Promise.all(Array.from(organizationIds).flatMap((organizationId) => [\n `customers:customer_${kind}_profile`,\n 'customers:customer_entity',\n ].map((entityId) => service.getEncryptedFieldNames(entityId, ctx.tenantId, organizationId, { ignoreRuntimeHealth: true }))))\n return fields.flat()\n}\n\nfunction selectProfileSearchFields(\n source: Record<string, unknown> | null,\n kind: CustomerProfileKind,\n options: { forEmbedding?: boolean; customerEntity?: boolean; encryptedFields?: string[] } = {},\n): Record<string, unknown> {\n const entityId = `customers:customer_${kind}_profile`\n const policy = searchConfig.entities.find((entry) => entry.entityId === entityId)?.fieldPolicy\n const allowed = new Set((options.customerEntity\n ? ['display_name', 'description', 'status', 'lifecycle_stage']\n : policy?.searchable ?? []).map(normalizeSearchFieldKey))\n const excluded = new Set([...(policy?.hashOnly ?? []), ...(policy?.excluded ?? [])].map(normalizeSearchFieldKey))\n if (options.forEmbedding) {\n for (const field of options.encryptedFields ?? []) excluded.add(normalizeSearchFieldKey(field))\n for (const map of defaultEncryptionMaps) {\n if (map.entityId !== entityId && map.entityId !== 'customers:customer_entity') continue\n for (const entry of map.fields) excluded.add(normalizeSearchFieldKey(entry.field))\n }\n }\n const fields: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(source ?? {})) {\n if (value !== null && typeof value === 'object') continue\n const field = normalizeSearchFieldKey(key)\n if (allowed.has(field) && !excluded.has(field)) fields[field] = value\n }\n return fields\n}\n\nfunction pickValue(source: Record<string, unknown> | null | undefined, ...keys: string[]): unknown {\n if (!source) return undefined\n for (const key of keys) {\n if (key in source && source[key] != null) return source[key]\n }\n return undefined\n}\n\nfunction pickString(...candidates: unknown[]): string | null {\n for (const candidate of candidates) {\n if (typeof candidate === 'string' && candidate.trim().length) {\n return candidate.trim()\n }\n }\n return null\n}\n\nfunction pickLabel(...candidates: Array<unknown>): string | null {\n for (const candidate of candidates) {\n if (candidate === null || candidate === undefined) continue\n const value = typeof candidate === 'string' ? candidate : String(candidate)\n const trimmed = value.trim()\n if (trimmed.length) return trimmed\n }\n return null\n}\n\nfunction appendCustomerEntityLines(\n lines: string[],\n entity: Record<string, unknown> | null,\n) {\n if (!entity) return\n appendLine(lines, 'Lifecycle stage', pickValue(entity, 'lifecycle_stage', 'lifecycleStage'))\n appendLine(lines, 'Status', pickValue(entity, 'status'))\n}\n\nfunction ensureFallbackLines(lines: string[], record: Record<string, unknown>, options: { includeId?: boolean } = {}) {\n if (lines.length) return\n const excluded = new Set(['tenant_id', 'organization_id', 'created_at', 'updated_at', 'deleted_at'])\n for (const [key, value] of Object.entries(record)) {\n if (value === null || value === undefined) continue\n if (excluded.has(key)) continue\n if (key === 'id') continue\n appendLine(lines, friendlyLabel(key), value)\n }\n if (!lines.length && options.includeId !== false) {\n const fallbackId =\n record.id ??\n record.entity_id ??\n record.customer_entity_id ??\n record.entityId ??\n record.customerEntityId ??\n null\n if (fallbackId) {\n appendLine(lines, 'Record ID', fallbackId)\n }\n }\n}\n\n// =============================================================================\n// Presenter Functions\n// =============================================================================\n\nfunction resolvePersonPresenter(\n translate: TranslateFn,\n record: Record<string, unknown>,\n entity: Record<string, unknown> | null,\n customFields: Record<string, unknown>,\n): SearchResultPresenter {\n const fallbackEntityId = resolveCustomerEntityId(record)\n record = { id: record.id, ...selectProfileSearchFields(record, 'person') }\n entity = entity ? { id: entity.id, ...selectProfileSearchFields(entity, 'person', { customerEntity: true }) } : null\n customFields = selectProfileSearchFields(customFields, 'person')\n const firstName = record.first_name ?? record.firstName ?? customFields.first_name ?? customFields.firstName ?? ''\n const lastName = record.last_name ?? record.lastName ?? customFields.last_name ?? customFields.lastName ?? ''\n const nameParts = [firstName, lastName].filter(Boolean).join(' ')\n const title =\n (pickValue(entity, 'display_name', 'displayName') as string | undefined) ??\n (record.preferred_name as string | undefined) ??\n (record.preferredName as string | undefined) ??\n (nameParts.length ? nameParts : undefined) ??\n fallbackEntityId ??\n (record.id as string | undefined) ??\n translate('customers.search.fallback.person', 'Person')\n const subtitlePieces: string[] = []\n const jobTitle = record.job_title ?? record.jobTitle ?? customFields.job_title ?? customFields.jobTitle\n if (jobTitle) subtitlePieces.push(String(jobTitle))\n const department = record.department ?? customFields.department\n if (department) subtitlePieces.push(String(department))\n const summary = snippet(\n (pickValue(entity, 'description') as string | undefined) ??\n (customFields.summary as string | undefined) ??\n (customFields.description as string | undefined),\n )\n if (summary) subtitlePieces.push(summary)\n return {\n title: String(title),\n subtitle: subtitlePieces.length ? subtitlePieces.join(' \u00B7 ') : undefined,\n icon: 'user',\n badge: pickValue(entity, 'display_name', 'displayName')\n ? translate('customers.search.badge.person', 'Person')\n : undefined,\n }\n}\n\nfunction resolveCompanyPresenter(\n translate: TranslateFn,\n record: Record<string, unknown>,\n entity: Record<string, unknown> | null,\n customFields: Record<string, unknown>,\n): SearchResultPresenter {\n const fallbackEntityId = resolveCustomerEntityId(record)\n record = { id: record.id, ...selectProfileSearchFields(record, 'company') }\n entity = entity ? { id: entity.id, ...selectProfileSearchFields(entity, 'company', { customerEntity: true }) } : null\n customFields = selectProfileSearchFields(customFields, 'company')\n const title =\n (pickValue(entity, 'display_name', 'displayName') as string | undefined) ??\n (customFields.display_name as string | undefined) ??\n (customFields.displayName as string | undefined) ??\n (record.brand_name as string | undefined) ??\n (record.legal_name as string | undefined) ??\n (record.domain as string | undefined) ??\n (record.brandName as string | undefined) ??\n (record.legalName as string | undefined) ??\n (entity?.id && entity?.display_name ? entity.display_name as string : undefined) ??\n fallbackEntityId ??\n (record.id as string | undefined) ??\n translate('customers.search.fallback.company', 'Company')\n const subtitlePieces: string[] = []\n const industry = record.industry\n if (industry) subtitlePieces.push(String(industry))\n const sizeBucket = record.size_bucket ?? record.sizeBucket\n if (sizeBucket) subtitlePieces.push(String(sizeBucket))\n const summary = snippet(\n (pickValue(entity, 'description') as string | undefined) ??\n (customFields.summary as string | undefined) ??\n (customFields.description as string | undefined) ??\n (record.summary as string | undefined) ??\n (record.description as string | undefined),\n )\n if (summary) subtitlePieces.push(summary)\n if (!entity && (!title || title === fallbackEntityId)) {\n logger.warn('Missing customer entity during company presenter build', {\n component: 'search',\n recordId: record.id ?? null,\n entityId: fallbackEntityId,\n recordKeys: Object.keys(record),\n })\n }\n return {\n title: String(title),\n subtitle: subtitlePieces.length ? subtitlePieces.join(' \u00B7 ') : undefined,\n icon: 'building',\n badge: pickValue(entity, 'display_name', 'displayName')\n ? translate('customers.search.badge.company', 'Company')\n : undefined,\n }\n}\n\nfunction logMissingPresenterTitle(\n kind: 'person' | 'company',\n record: Record<string, unknown>,\n entity: Record<string, unknown> | null,\n presenter: SearchResultPresenter,\n) {\n const fallbackId = record.id ?? record.entity_id ?? resolveCustomerEntityId(record)\n if (!fallbackId) return\n if (presenter.title && presenter.title !== String(fallbackId)) return\n logger.warn('Presenter fell back to record id', {\n component: 'search',\n kind,\n recordId: fallbackId,\n entityId: resolveCustomerEntityId(record),\n hasEntityDisplayName: entity?.display_name != null,\n })\n}\n\n// =============================================================================\n// Search Module Configuration\n// =============================================================================\n\nexport const searchConfig: SearchModuleConfig = {\n entities: [\n // =========================================================================\n // Person Profile\n // =========================================================================\n {\n entityId: 'customers:customer_person_profile',\n enabled: true,\n priority: 10,\n\n buildSource: async (ctx: SearchBuildContext): Promise<SearchIndexSource | null> => {\n assertTenantContext(ctx)\n const { t } = await resolveTranslations()\n const lines: string[] = []\n const record = ctx.record\n\n const { entity, entityId, profileCustomFields, entityOnlyCustomFields } =\n await hydrateProfileContext(ctx, 'person')\n const embeddingOptions = { forEmbedding: true, encryptedFields: await resolveProfileEncryptedFields(ctx, 'person', entity) }\n appendCustomFieldLines(lines, selectProfileSearchFields(record, 'person', embeddingOptions), '')\n appendCustomFieldLines(lines, selectProfileSearchFields(profileCustomFields, 'person', embeddingOptions), 'Person custom')\n if (Object.keys(entityOnlyCustomFields).length) {\n appendCustomFieldLines(lines, selectProfileSearchFields(entityOnlyCustomFields, 'person', embeddingOptions), 'Customer custom')\n }\n if (!entity) {\n logger.warn('Failed to load customer entity for person profile', {\n component: 'search',\n recordId: record.id,\n entityId,\n recordKeys: Object.keys(record),\n })\n }\n appendCustomerEntityLines(lines, selectProfileSearchFields(entity, 'person', { ...embeddingOptions, customerEntity: true }))\n ensureFallbackLines(lines, { id: record.id ?? entityId ?? 'customers:customer_person_profile' })\n if (!lines.length) return null\n\n if (!entityId) {\n logger.warn('Person profile missing entity id', {\n component: 'search',\n recordId: record.id,\n recordKeys: Object.keys(record),\n })\n }\n\n const presenter = resolvePersonPresenter(t, record, entity, ctx.customFields)\n logMissingPresenterTitle('person', record, entity, presenter)\n const presenterLabel = pickLabel(presenter.title) ?? t('customers.search.link.openPerson', 'Open person')\n const links: SearchResultLink[] = []\n if (entityId) {\n const href = buildCustomerUrl('person', entityId)\n if (href) {\n links.push({ href, label: presenterLabel, kind: 'primary' })\n }\n }\n\n return {\n text: lines,\n presenter,\n links,\n checksumSource: { text: lines, presenter, links },\n }\n },\n\n formatResult: async (ctx: SearchBuildContext): Promise<SearchResultPresenter | null> => {\n assertTenantContext(ctx)\n const { t } = await resolveTranslations()\n const entity = await getCustomerEntity(ctx, resolveCustomerEntityId(ctx.record))\n return resolvePersonPresenter(t, ctx.record, entity, ctx.customFields)\n },\n\n resolveUrl: async (ctx: SearchBuildContext): Promise<string | null> => {\n const entityId = resolveCustomerEntityId(ctx.record)\n return buildCustomerUrl('person', entityId)\n },\n\n resolveLinks: async (ctx: SearchBuildContext): Promise<SearchResultLink[] | null> => {\n const { t } = await resolveTranslations()\n const entityId = resolveCustomerEntityId(ctx.record)\n if (!entityId) return null\n const href = buildCustomerUrl('person', entityId)\n if (!href) return null\n return [{ href: `${href}/edit`, label: t('customers.search.link.edit', 'Edit'), kind: 'secondary' }]\n },\n\n fieldPolicy: {\n searchable: [\n 'preferred_name',\n 'first_name',\n 'last_name',\n 'job_title',\n 'department',\n 'seniority',\n 'timezone',\n 'linked_in_url',\n 'twitter_url',\n ],\n hashOnly: ['primary_email', 'primary_phone', 'personal_email'],\n excluded: ['date_of_birth', 'government_id', 'ssn', 'tax_id'],\n },\n aclFeatures: ['customers.people.view'],\n },\n\n // =========================================================================\n // Company Profile\n // =========================================================================\n {\n entityId: 'customers:customer_company_profile',\n enabled: true,\n priority: 10,\n\n buildSource: async (ctx: SearchBuildContext): Promise<SearchIndexSource | null> => {\n assertTenantContext(ctx)\n const { t } = await resolveTranslations()\n const lines: string[] = []\n const record = ctx.record\n\n const { entity, entityId, profileCustomFields, entityOnlyCustomFields } =\n await hydrateProfileContext(ctx, 'company')\n const embeddingOptions = { forEmbedding: true, encryptedFields: await resolveProfileEncryptedFields(ctx, 'company', entity) }\n appendCustomFieldLines(lines, selectProfileSearchFields(record, 'company', embeddingOptions), '')\n appendCustomFieldLines(lines, selectProfileSearchFields(profileCustomFields, 'company', embeddingOptions), 'Company custom')\n if (Object.keys(entityOnlyCustomFields).length) {\n appendCustomFieldLines(lines, selectProfileSearchFields(entityOnlyCustomFields, 'company', embeddingOptions), 'Customer custom')\n }\n appendCustomerEntityLines(lines, selectProfileSearchFields(entity, 'company', { ...embeddingOptions, customerEntity: true }))\n ensureFallbackLines(lines, { id: record.id ?? entityId ?? 'customers:customer_company_profile' })\n if (!lines.length) return null\n\n const presenter = resolveCompanyPresenter(t, record, entity, ctx.customFields)\n logMissingPresenterTitle('company', record, entity, presenter)\n const primaryLabel = pickLabel(presenter.title) ?? t('customers.search.link.openCompany', 'Open company')\n const links: SearchResultLink[] = []\n if (entityId) {\n const href = buildCustomerUrl('company', entityId)\n if (href) {\n links.push({ href, label: primaryLabel, kind: 'primary' })\n }\n }\n\n return {\n text: lines,\n presenter,\n links,\n checksumSource: { text: lines, presenter, links },\n }\n },\n\n formatResult: async (ctx: SearchBuildContext): Promise<SearchResultPresenter | null> => {\n assertTenantContext(ctx)\n const { t } = await resolveTranslations()\n const entity = await getCustomerEntity(ctx, resolveCustomerEntityId(ctx.record))\n return resolveCompanyPresenter(t, ctx.record, entity, ctx.customFields)\n },\n\n resolveUrl: async (ctx: SearchBuildContext): Promise<string | null> => {\n const entityId = resolveCustomerEntityId(ctx.record)\n return buildCustomerUrl('company', entityId)\n },\n\n resolveLinks: async (ctx: SearchBuildContext): Promise<SearchResultLink[] | null> => {\n const { t } = await resolveTranslations()\n const entityId = resolveCustomerEntityId(ctx.record)\n if (!entityId) return null\n const href = buildCustomerUrl('company', entityId)\n if (!href) return null\n return [{ href: `${href}/edit`, label: t('customers.search.link.edit', 'Edit'), kind: 'secondary' }]\n },\n\n fieldPolicy: {\n searchable: [\n 'legal_name',\n 'brand_name',\n 'display_name',\n 'domain',\n 'website_url',\n 'industry',\n 'size_bucket',\n 'description',\n ],\n hashOnly: ['tax_id', 'registration_number'],\n excluded: ['bank_account', 'billing_info', 'credit_info'],\n },\n aclFeatures: ['customers.companies.view'],\n },\n\n // =========================================================================\n // Customer Comment\n // =========================================================================\n {\n entityId: 'customers:customer_comment',\n enabled: true,\n priority: 6,\n\n buildSource: async (ctx: SearchBuildContext): Promise<SearchIndexSource | null> => {\n assertTenantContext(ctx)\n const parent = await getCustomerEntity(ctx, ctx.record.entity_id as string ?? ctx.record.entityId as string)\n const lines: string[] = []\n if (parent?.display_name) lines.push(`Customer: ${parent.display_name}`)\n lines.push(`Note: ${ctx.record.body ?? ''}`)\n if (ctx.record.appearance_icon) lines.push(`Icon: ${ctx.record.appearance_icon}`)\n if (ctx.record.appearance_color) lines.push(`Color: ${ctx.record.appearance_color}`)\n\n const presenter: SearchResultPresenter | undefined = parent?.display_name\n ? {\n title: parent.display_name as string,\n subtitle: snippet(ctx.record.body),\n icon: parent.kind === 'person' ? 'user' : 'building',\n }\n : undefined\n\n return {\n text: lines,\n presenter,\n checksumSource: {\n body: ctx.record.body,\n entityId: ctx.record.entity_id ?? null,\n updatedAt: ctx.record.updated_at ?? ctx.record.updatedAt ?? null,\n },\n }\n },\n\n formatResult: async (ctx: SearchBuildContext): Promise<SearchResultPresenter | null> => {\n assertTenantContext(ctx)\n const { t } = await resolveTranslations()\n const parent = await getCustomerEntity(ctx, ctx.record.entity_id as string ?? ctx.record.entityId as string)\n const title = (parent?.display_name as string | undefined) ?? t('customers.search.fallback.customerNote', 'Customer note')\n return {\n title,\n subtitle: snippet(ctx.record.body),\n icon: 'sticky-note',\n }\n },\n\n resolveUrl: async (ctx: SearchBuildContext): Promise<string | null> => {\n assertTenantContext(ctx)\n const parent = await getCustomerEntity(ctx, ctx.record.entity_id as string ?? ctx.record.entityId as string)\n const base = buildCustomerUrl(parent?.kind as string ?? null, (parent?.id ?? ctx.record.entity_id ?? ctx.record.entityId) as string)\n return base ? `${base}#notes` : null\n },\n\n resolveLinks: async (ctx: SearchBuildContext): Promise<SearchResultLink[] | null> => {\n assertTenantContext(ctx)\n const { t } = await resolveTranslations()\n const links: SearchResultLink[] = []\n const parent = await getCustomerEntity(ctx, ctx.record.entity_id as string ?? ctx.record.entityId as string)\n const parentUrl = buildCustomerUrl(parent?.kind as string ?? null, (parent?.id ?? ctx.record.entity_id ?? ctx.record.entityId) as string)\n if (parentUrl) {\n links.push({ href: parentUrl, label: (parent?.display_name as string | undefined) ?? t('customers.search.link.viewCustomer', 'View customer'), kind: 'primary' })\n }\n if (ctx.record.deal_id) {\n const dealUrl = `/backend/customers/deals/${encodeURIComponent(ctx.record.deal_id as string)}`\n links.push({ href: dealUrl, label: t('customers.search.link.openDeal', 'Open deal'), kind: 'secondary' })\n }\n return links.length ? links : null\n },\n\n fieldPolicy: {\n searchable: ['body'],\n hashOnly: [],\n excluded: [],\n },\n aclFeatures: ['customers.activities.view'],\n },\n\n // =========================================================================\n // Customer Deal\n // =========================================================================\n {\n entityId: 'customers:customer_deal',\n enabled: true,\n priority: 8,\n\n buildSource: async (ctx: SearchBuildContext): Promise<SearchIndexSource | null> => {\n const { t } = await resolveTranslations()\n const lines: string[] = []\n const record = ctx.record\n appendLine(lines, 'Title', record.title)\n appendLine(lines, 'Stage', record.pipeline_stage)\n appendLine(lines, 'Status', record.status)\n appendLine(lines, 'Source', record.source)\n const value = formatDealValue(record)\n if (value) appendLine(lines, 'Value', value)\n if (!lines.length) return null\n\n const subtitleParts: string[] = []\n if (record.pipeline_stage) subtitleParts.push(String(record.pipeline_stage))\n if (record.status) subtitleParts.push(String(record.status))\n if (value) subtitleParts.push(value)\n\n return {\n text: lines,\n presenter: {\n title: String(record.title ?? t('customers.search.fallback.deal', 'Deal')),\n subtitle: subtitleParts.join(' \u00B7 ') || undefined,\n icon: 'briefcase',\n badge: t('customers.search.badge.deal', 'Deal'),\n },\n checksumSource: {\n title: record.title,\n status: record.status,\n stage: record.pipeline_stage,\n value: value,\n },\n }\n },\n\n formatResult: async (ctx: SearchBuildContext): Promise<SearchResultPresenter | null> => {\n const { t } = await resolveTranslations()\n const { record } = ctx\n const title = pickString(record.title as string, t('customers.search.fallback.deal', 'Deal'))\n const subtitleParts: string[] = []\n if (record.pipeline_stage) subtitleParts.push(String(record.pipeline_stage))\n if (record.status) subtitleParts.push(String(record.status))\n const amount = record.value_amount ?? record.valueAmount\n const currency = record.value_currency ?? record.valueCurrency\n if (amount) {\n subtitleParts.push(currency ? `${amount} ${currency}` : String(amount))\n }\n\n return {\n title: title ?? t('customers.search.fallback.deal', 'Deal'),\n subtitle: subtitleParts.length ? subtitleParts.join(' \u00B7 ') : undefined,\n icon: 'briefcase',\n badge: t('customers.search.badge.deal', 'Deal'),\n }\n },\n\n resolveUrl: async (ctx: SearchBuildContext): Promise<string | null> => {\n const id = ctx.record.id\n if (!id) return null\n return `/backend/customers/deals/${encodeURIComponent(String(id))}`\n },\n\n resolveLinks: async (ctx: SearchBuildContext): Promise<SearchResultLink[] | null> => {\n const { t } = await resolveTranslations()\n const id = ctx.record.id\n if (!id) return null\n return [\n {\n href: `/backend/customers/deals/${encodeURIComponent(String(id))}/edit`,\n label: t('customers.search.link.edit', 'Edit'),\n kind: 'secondary',\n },\n ]\n },\n\n fieldPolicy: {\n searchable: ['title', 'description', 'pipeline_stage', 'status', 'source'],\n hashOnly: [],\n excluded: ['value_amount', 'value_currency'],\n },\n aclFeatures: ['customers.deals.view'],\n },\n\n // =========================================================================\n // Customer Activity\n // =========================================================================\n {\n entityId: 'customers:customer_activity',\n enabled: true,\n priority: 5,\n\n buildSource: async (ctx: SearchBuildContext): Promise<SearchIndexSource | null> => {\n assertTenantContext(ctx)\n const { t } = await resolveTranslations()\n const parent = await getCustomerEntity(ctx, ctx.record.entity_id as string ?? ctx.record.entityId as string)\n const lines: string[] = []\n if (parent?.display_name) lines.push(`Customer: ${parent.display_name}`)\n if (ctx.record.activity_type) lines.push(`Type: ${ctx.record.activity_type}`)\n if (ctx.record.subject) lines.push(`Subject: ${ctx.record.subject}`)\n if (ctx.record.body) lines.push(`Body: ${ctx.record.body}`)\n\n const presenter: SearchResultPresenter = {\n title: ctx.record.subject\n ? String(ctx.record.subject)\n : t('customers.search.fallback.activity', 'Activity: {{type}}', {\n type: String(ctx.record.activity_type ?? t('customers.search.fallback.activityUpdate', 'update')),\n }),\n subtitle: (parent?.display_name as string | undefined) ?? snippet(ctx.record.body),\n icon: 'bolt',\n }\n\n return {\n text: lines,\n presenter,\n checksumSource: {\n subject: ctx.record.subject,\n body: ctx.record.body,\n entityId: ctx.record.entity_id ?? null,\n updatedAt: ctx.record.updated_at ?? ctx.record.updatedAt ?? null,\n },\n }\n },\n\n formatResult: async (ctx: SearchBuildContext): Promise<SearchResultPresenter | null> => {\n assertTenantContext(ctx)\n const { t } = await resolveTranslations()\n const parent = await getCustomerEntity(ctx, ctx.record.entity_id as string ?? ctx.record.entityId as string)\n return {\n title: ctx.record.subject\n ? String(ctx.record.subject)\n : t('customers.search.fallback.activity', 'Activity: {{type}}', {\n type: String(ctx.record.activity_type ?? t('customers.search.fallback.activityUpdate', 'update')),\n }),\n subtitle: (parent?.display_name as string | undefined) ?? snippet(ctx.record.body),\n icon: 'bolt',\n badge: t('customers.search.badge.activity', 'Activity'),\n }\n },\n\n resolveUrl: async (ctx: SearchBuildContext): Promise<string | null> => {\n assertTenantContext(ctx)\n const parent = await getCustomerEntity(ctx, ctx.record.entity_id as string ?? ctx.record.entityId as string)\n const base = buildCustomerUrl(parent?.kind as string ?? null, (parent?.id ?? ctx.record.entity_id ?? ctx.record.entityId) as string)\n return base ? `${base}#activity-${ctx.record.id ?? ctx.record.activity_id ?? ''}` : null\n },\n\n resolveLinks: async (ctx: SearchBuildContext): Promise<SearchResultLink[] | null> => {\n const { t } = await resolveTranslations()\n const links: SearchResultLink[] = []\n if (ctx.record.deal_id) {\n links.push({\n href: `/backend/customers/deals/${encodeURIComponent(ctx.record.deal_id as string)}`,\n label: t('customers.search.link.openDeal', 'Open deal'),\n kind: 'secondary',\n })\n }\n return links.length ? links : null\n },\n\n fieldPolicy: {\n searchable: ['subject', 'body', 'activity_type'],\n hashOnly: [],\n excluded: [],\n },\n aclFeatures: ['customers.activities.view'],\n },\n\n // =========================================================================\n // Customer Todo Link\n // =========================================================================\n {\n entityId: 'customers:customer_todo_link',\n enabled: true,\n priority: 4,\n\n buildSource: async (ctx: SearchBuildContext): Promise<SearchIndexSource | null> => {\n assertTenantContext(ctx)\n const todo = await getLinkedTodo(ctx) as Record<string, unknown> | null\n const parent = await getCustomerEntity(ctx, ctx.record.entity_id as string ?? ctx.record.entityId as string)\n const lines: string[] = []\n if (todo?.title) lines.push(`Todo: ${todo.title}`)\n if (todo?.is_done !== undefined) lines.push(`Status: ${todo.is_done ? 'Done' : 'Open'}`)\n if (parent?.display_name) lines.push(`Customer: ${parent.display_name}`)\n if (!lines.length) return null\n\n return {\n text: lines,\n presenter: todo?.title\n ? { title: todo.title as string, subtitle: parent?.display_name as string | undefined, icon: 'check-square' }\n : undefined,\n checksumSource: {\n todoId: ctx.record.todo_id ?? ctx.record.todoId,\n todoSource: ctx.record.todo_source ?? ctx.record.todoSource,\n entityId: ctx.record.entity_id ?? ctx.record.entityId,\n },\n }\n },\n\n formatResult: async (ctx: SearchBuildContext): Promise<SearchResultPresenter | null> => {\n assertTenantContext(ctx)\n const { t } = await resolveTranslations()\n const todo = await getLinkedTodo(ctx) as Record<string, unknown> | null\n const parent = await getCustomerEntity(ctx, ctx.record.entity_id as string ?? ctx.record.entityId as string)\n return {\n title: (todo?.title as string | undefined) ?? t('customers.search.fallback.customerTask', 'Customer task'),\n subtitle: parent?.display_name as string | undefined,\n icon: 'check-square',\n }\n },\n\n resolveUrl: async (ctx: SearchBuildContext): Promise<string | null> => {\n assertTenantContext(ctx)\n const parent = await getCustomerEntity(ctx, ctx.record.entity_id as string ?? ctx.record.entityId as string)\n const base = buildCustomerUrl(parent?.kind as string ?? null, (parent?.id ?? ctx.record.entity_id ?? ctx.record.entityId) as string)\n return base ? `${base}#tasks` : null\n },\n\n resolveLinks: async (ctx: SearchBuildContext): Promise<SearchResultLink[] | null> => {\n const { t } = await resolveTranslations()\n const todoId = ctx.record.todo_id ?? ctx.record.todoId\n if (!todoId) return null\n return [{\n href: `/backend/todos/${encodeURIComponent(todoId as string)}/edit`,\n label: t('customers.search.link.openTodo', 'Open todo'),\n kind: 'secondary',\n }]\n },\n\n fieldPolicy: {\n searchable: [],\n hashOnly: [],\n excluded: [],\n },\n aclFeatures: ['customers.activities.view'],\n },\n ],\n}\n\nexport default searchConfig\nexport const config = searchConfig\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.10.1-develop.
|
|
3
|
+
"version": "0.10.1-develop.7771.1.615a6c9ba9",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -266,16 +266,16 @@
|
|
|
266
266
|
"zod": "^4.4.3"
|
|
267
267
|
},
|
|
268
268
|
"peerDependencies": {
|
|
269
|
-
"@open-mercato/ai-assistant": "0.10.1-develop.
|
|
270
|
-
"@open-mercato/shared": "0.10.1-develop.
|
|
271
|
-
"@open-mercato/ui": "0.10.1-develop.
|
|
269
|
+
"@open-mercato/ai-assistant": "0.10.1-develop.7771.1.615a6c9ba9",
|
|
270
|
+
"@open-mercato/shared": "0.10.1-develop.7771.1.615a6c9ba9",
|
|
271
|
+
"@open-mercato/ui": "0.10.1-develop.7771.1.615a6c9ba9",
|
|
272
272
|
"react": "^19.0.0",
|
|
273
273
|
"react-dom": "^19.0.0"
|
|
274
274
|
},
|
|
275
275
|
"devDependencies": {
|
|
276
|
-
"@open-mercato/ai-assistant": "0.10.1-develop.
|
|
277
|
-
"@open-mercato/shared": "0.10.1-develop.
|
|
278
|
-
"@open-mercato/ui": "0.10.1-develop.
|
|
276
|
+
"@open-mercato/ai-assistant": "0.10.1-develop.7771.1.615a6c9ba9",
|
|
277
|
+
"@open-mercato/shared": "0.10.1-develop.7771.1.615a6c9ba9",
|
|
278
|
+
"@open-mercato/ui": "0.10.1-develop.7771.1.615a6c9ba9",
|
|
279
279
|
"@testing-library/dom": "^10.4.1",
|
|
280
280
|
"@testing-library/jest-dom": "^7.0.1",
|
|
281
281
|
"@testing-library/react": "^16.3.3",
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
SearchModuleConfig,
|
|
3
|
-
SearchBuildContext,
|
|
4
3
|
SearchResultPresenter,
|
|
5
4
|
SearchIndexSource,
|
|
6
5
|
} from '@open-mercato/shared/modules/search'
|
|
@@ -40,7 +39,6 @@ function formatSubtitle(...parts: Array<unknown>): string | undefined {
|
|
|
40
39
|
}
|
|
41
40
|
|
|
42
41
|
function buildIndexSource(
|
|
43
|
-
ctx: SearchBuildContext,
|
|
44
42
|
presenter: SearchResultPresenter,
|
|
45
43
|
lines: string[],
|
|
46
44
|
): SearchIndexSource | null {
|
|
@@ -48,7 +46,7 @@ function buildIndexSource(
|
|
|
48
46
|
return {
|
|
49
47
|
text: lines,
|
|
50
48
|
presenter,
|
|
51
|
-
checksumSource: {
|
|
49
|
+
checksumSource: { text: lines, presenter },
|
|
52
50
|
}
|
|
53
51
|
}
|
|
54
52
|
|
|
@@ -63,13 +61,10 @@ export const searchConfig: SearchModuleConfig = {
|
|
|
63
61
|
const { t } = await resolveTranslations()
|
|
64
62
|
const record = ctx.record
|
|
65
63
|
const lines: string[] = []
|
|
66
|
-
appendLine(lines, '
|
|
67
|
-
appendLine(lines, 'Email', record.email)
|
|
64
|
+
appendLine(lines, 'Customer user ID', record.id ?? 'customer_accounts:customer_user')
|
|
68
65
|
return buildIndexSource(
|
|
69
|
-
ctx,
|
|
70
66
|
{
|
|
71
67
|
title: pickString(record.display_name, record.displayName) ?? String(record.id),
|
|
72
|
-
subtitle: formatSubtitle(record.email),
|
|
73
68
|
icon: 'user',
|
|
74
69
|
badge: t('customer_accounts.search.badge.customerUser', 'Customer User'),
|
|
75
70
|
},
|
|
@@ -81,14 +76,14 @@ export const searchConfig: SearchModuleConfig = {
|
|
|
81
76
|
const record = ctx.record
|
|
82
77
|
return {
|
|
83
78
|
title: pickString(record.display_name, record.displayName) ?? String(record.id),
|
|
84
|
-
subtitle: formatSubtitle(record.email),
|
|
85
79
|
icon: 'user',
|
|
86
80
|
badge: t('customer_accounts.search.badge.customerUser', 'Customer User'),
|
|
87
81
|
}
|
|
88
82
|
},
|
|
89
83
|
resolveUrl: async (ctx) => `/backend/customer-accounts/users/${encodeURIComponent(String(ctx.record.id))}`,
|
|
90
84
|
fieldPolicy: {
|
|
91
|
-
searchable: ['display_name'
|
|
85
|
+
searchable: ['display_name'],
|
|
86
|
+
hashOnly: ['email'],
|
|
92
87
|
excluded: ['password_hash', 'email_hash'],
|
|
93
88
|
},
|
|
94
89
|
},
|
|
@@ -104,7 +99,6 @@ export const searchConfig: SearchModuleConfig = {
|
|
|
104
99
|
appendLine(lines, 'Name', record.name)
|
|
105
100
|
appendLine(lines, 'Description', record.description)
|
|
106
101
|
return buildIndexSource(
|
|
107
|
-
ctx,
|
|
108
102
|
{
|
|
109
103
|
title: pickString(record.name) ?? String(record.id),
|
|
110
104
|
subtitle: formatSubtitle(snippet(record.description)),
|
|
@@ -10,6 +10,9 @@ import type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'
|
|
|
10
10
|
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
11
11
|
import { CUSTOMER_INTERACTION_TASK_SOURCE, EXAMPLE_TODO_SOURCE } from './lib/interactionCompatibility'
|
|
12
12
|
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
13
|
+
import { defaultEncryptionMaps } from './encryption'
|
|
14
|
+
import type { TenantDataEncryptionService } from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'
|
|
15
|
+
import { isTenantDataEncryptionEnabled } from '@open-mercato/shared/lib/encryption/toggles'
|
|
13
16
|
|
|
14
17
|
const logger = createLogger('customers')
|
|
15
18
|
|
|
@@ -414,6 +417,61 @@ function appendCustomFieldLines(lines: string[], customFields: Record<string, un
|
|
|
414
417
|
}
|
|
415
418
|
}
|
|
416
419
|
|
|
420
|
+
function normalizeSearchFieldKey(key: string): string {
|
|
421
|
+
return key.replace(/^cf[:_]/, '').replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase()
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
async function resolveProfileEncryptedFields(ctx: SearchContext, kind: CustomerProfileKind, entity: Record<string, unknown> | null): Promise<string[]> {
|
|
425
|
+
if (!isTenantDataEncryptionEnabled()) {
|
|
426
|
+
throw new Error('[internal] Customer vector indexing requires available encryption declarations')
|
|
427
|
+
}
|
|
428
|
+
const container = ctx.container as { resolve?: (name: string) => unknown } | undefined
|
|
429
|
+
if (typeof container?.resolve !== 'function') {
|
|
430
|
+
throw new Error('[internal] Customer search encryption policy container is unavailable')
|
|
431
|
+
}
|
|
432
|
+
const service = container.resolve('tenantEncryptionService') as Pick<TenantDataEncryptionService, 'getEncryptedFieldNames'> | undefined
|
|
433
|
+
if (typeof service?.getEncryptedFieldNames !== 'function') {
|
|
434
|
+
throw new Error('[internal] Customer search encryption policy service is unavailable')
|
|
435
|
+
}
|
|
436
|
+
const organizationIds = new Set([ctx.organizationId])
|
|
437
|
+
for (const record of [ctx.record, entity]) {
|
|
438
|
+
const organizationId = record?.organization_id ?? record?.organizationId
|
|
439
|
+
if (typeof organizationId === 'string' && organizationId) organizationIds.add(organizationId)
|
|
440
|
+
}
|
|
441
|
+
const fields = await Promise.all(Array.from(organizationIds).flatMap((organizationId) => [
|
|
442
|
+
`customers:customer_${kind}_profile`,
|
|
443
|
+
'customers:customer_entity',
|
|
444
|
+
].map((entityId) => service.getEncryptedFieldNames(entityId, ctx.tenantId, organizationId, { ignoreRuntimeHealth: true }))))
|
|
445
|
+
return fields.flat()
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function selectProfileSearchFields(
|
|
449
|
+
source: Record<string, unknown> | null,
|
|
450
|
+
kind: CustomerProfileKind,
|
|
451
|
+
options: { forEmbedding?: boolean; customerEntity?: boolean; encryptedFields?: string[] } = {},
|
|
452
|
+
): Record<string, unknown> {
|
|
453
|
+
const entityId = `customers:customer_${kind}_profile`
|
|
454
|
+
const policy = searchConfig.entities.find((entry) => entry.entityId === entityId)?.fieldPolicy
|
|
455
|
+
const allowed = new Set((options.customerEntity
|
|
456
|
+
? ['display_name', 'description', 'status', 'lifecycle_stage']
|
|
457
|
+
: policy?.searchable ?? []).map(normalizeSearchFieldKey))
|
|
458
|
+
const excluded = new Set([...(policy?.hashOnly ?? []), ...(policy?.excluded ?? [])].map(normalizeSearchFieldKey))
|
|
459
|
+
if (options.forEmbedding) {
|
|
460
|
+
for (const field of options.encryptedFields ?? []) excluded.add(normalizeSearchFieldKey(field))
|
|
461
|
+
for (const map of defaultEncryptionMaps) {
|
|
462
|
+
if (map.entityId !== entityId && map.entityId !== 'customers:customer_entity') continue
|
|
463
|
+
for (const entry of map.fields) excluded.add(normalizeSearchFieldKey(entry.field))
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
const fields: Record<string, unknown> = {}
|
|
467
|
+
for (const [key, value] of Object.entries(source ?? {})) {
|
|
468
|
+
if (value !== null && typeof value === 'object') continue
|
|
469
|
+
const field = normalizeSearchFieldKey(key)
|
|
470
|
+
if (allowed.has(field) && !excluded.has(field)) fields[field] = value
|
|
471
|
+
}
|
|
472
|
+
return fields
|
|
473
|
+
}
|
|
474
|
+
|
|
417
475
|
function pickValue(source: Record<string, unknown> | null | undefined, ...keys: string[]): unknown {
|
|
418
476
|
if (!source) return undefined
|
|
419
477
|
for (const key of keys) {
|
|
@@ -444,12 +502,8 @@ function pickLabel(...candidates: Array<unknown>): string | null {
|
|
|
444
502
|
function appendCustomerEntityLines(
|
|
445
503
|
lines: string[],
|
|
446
504
|
entity: Record<string, unknown> | null,
|
|
447
|
-
contactLabel: 'Customer' | 'Primary' = 'Customer',
|
|
448
505
|
) {
|
|
449
506
|
if (!entity) return
|
|
450
|
-
appendLine(lines, 'Customer', pickValue(entity, 'display_name', 'displayName') ?? entity.id)
|
|
451
|
-
appendLine(lines, `${contactLabel} email`, pickValue(entity, 'primary_email', 'primaryEmail'))
|
|
452
|
-
appendLine(lines, `${contactLabel} phone`, pickValue(entity, 'primary_phone', 'primaryPhone'))
|
|
453
507
|
appendLine(lines, 'Lifecycle stage', pickValue(entity, 'lifecycle_stage', 'lifecycleStage'))
|
|
454
508
|
appendLine(lines, 'Status', pickValue(entity, 'status'))
|
|
455
509
|
}
|
|
@@ -488,6 +542,9 @@ function resolvePersonPresenter(
|
|
|
488
542
|
customFields: Record<string, unknown>,
|
|
489
543
|
): SearchResultPresenter {
|
|
490
544
|
const fallbackEntityId = resolveCustomerEntityId(record)
|
|
545
|
+
record = { id: record.id, ...selectProfileSearchFields(record, 'person') }
|
|
546
|
+
entity = entity ? { id: entity.id, ...selectProfileSearchFields(entity, 'person', { customerEntity: true }) } : null
|
|
547
|
+
customFields = selectProfileSearchFields(customFields, 'person')
|
|
491
548
|
const firstName = record.first_name ?? record.firstName ?? customFields.first_name ?? customFields.firstName ?? ''
|
|
492
549
|
const lastName = record.last_name ?? record.lastName ?? customFields.last_name ?? customFields.lastName ?? ''
|
|
493
550
|
const nameParts = [firstName, lastName].filter(Boolean).join(' ')
|
|
@@ -504,10 +561,6 @@ function resolvePersonPresenter(
|
|
|
504
561
|
if (jobTitle) subtitlePieces.push(String(jobTitle))
|
|
505
562
|
const department = record.department ?? customFields.department
|
|
506
563
|
if (department) subtitlePieces.push(String(department))
|
|
507
|
-
const primaryEmail = pickValue(entity, 'primary_email', 'primaryEmail')
|
|
508
|
-
if (primaryEmail) subtitlePieces.push(String(primaryEmail))
|
|
509
|
-
const primaryPhone = pickValue(entity, 'primary_phone', 'primaryPhone')
|
|
510
|
-
if (primaryPhone) subtitlePieces.push(String(primaryPhone))
|
|
511
564
|
const summary = snippet(
|
|
512
565
|
(pickValue(entity, 'description') as string | undefined) ??
|
|
513
566
|
(customFields.summary as string | undefined) ??
|
|
@@ -531,6 +584,9 @@ function resolveCompanyPresenter(
|
|
|
531
584
|
customFields: Record<string, unknown>,
|
|
532
585
|
): SearchResultPresenter {
|
|
533
586
|
const fallbackEntityId = resolveCustomerEntityId(record)
|
|
587
|
+
record = { id: record.id, ...selectProfileSearchFields(record, 'company') }
|
|
588
|
+
entity = entity ? { id: entity.id, ...selectProfileSearchFields(entity, 'company', { customerEntity: true }) } : null
|
|
589
|
+
customFields = selectProfileSearchFields(customFields, 'company')
|
|
534
590
|
const title =
|
|
535
591
|
(pickValue(entity, 'display_name', 'displayName') as string | undefined) ??
|
|
536
592
|
(customFields.display_name as string | undefined) ??
|
|
@@ -549,10 +605,6 @@ function resolveCompanyPresenter(
|
|
|
549
605
|
if (industry) subtitlePieces.push(String(industry))
|
|
550
606
|
const sizeBucket = record.size_bucket ?? record.sizeBucket
|
|
551
607
|
if (sizeBucket) subtitlePieces.push(String(sizeBucket))
|
|
552
|
-
if (entity) {
|
|
553
|
-
const primaryEmail = pickValue(entity, 'primary_email', 'primaryEmail')
|
|
554
|
-
if (primaryEmail) subtitlePieces.push(String(primaryEmail))
|
|
555
|
-
}
|
|
556
608
|
const summary = snippet(
|
|
557
609
|
(pickValue(entity, 'description') as string | undefined) ??
|
|
558
610
|
(customFields.summary as string | undefined) ??
|
|
@@ -616,21 +668,14 @@ export const searchConfig: SearchModuleConfig = {
|
|
|
616
668
|
const { t } = await resolveTranslations()
|
|
617
669
|
const lines: string[] = []
|
|
618
670
|
const record = ctx.record
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
appendLine(lines, 'Last name', record.last_name ?? record.lastName ?? ctx.customFields.last_name)
|
|
622
|
-
appendLine(lines, 'Job title', record.job_title ?? record.jobTitle ?? ctx.customFields.job_title)
|
|
623
|
-
appendLine(lines, 'Department', record.department ?? record.department_name ?? record.departmentName ?? ctx.customFields.department)
|
|
624
|
-
appendLine(lines, 'Seniority', record.seniority ?? record.seniority_level ?? record.seniorityLevel ?? ctx.customFields.seniority)
|
|
625
|
-
appendLine(lines, 'Timezone', record.timezone ?? record.time_zone ?? record.timeZone ?? ctx.customFields.timezone)
|
|
626
|
-
appendLine(lines, 'LinkedIn', record.linked_in_url ?? record.linkedInUrl ?? ctx.customFields.linked_in_url)
|
|
627
|
-
appendLine(lines, 'Twitter', record.twitter_url ?? record.twitterUrl ?? ctx.customFields.twitter_url)
|
|
628
|
-
|
|
629
|
-
const { entity, entityId, profileCustomFields, entityCustomFields, entityOnlyCustomFields } =
|
|
671
|
+
|
|
672
|
+
const { entity, entityId, profileCustomFields, entityOnlyCustomFields } =
|
|
630
673
|
await hydrateProfileContext(ctx, 'person')
|
|
631
|
-
|
|
674
|
+
const embeddingOptions = { forEmbedding: true, encryptedFields: await resolveProfileEncryptedFields(ctx, 'person', entity) }
|
|
675
|
+
appendCustomFieldLines(lines, selectProfileSearchFields(record, 'person', embeddingOptions), '')
|
|
676
|
+
appendCustomFieldLines(lines, selectProfileSearchFields(profileCustomFields, 'person', embeddingOptions), 'Person custom')
|
|
632
677
|
if (Object.keys(entityOnlyCustomFields).length) {
|
|
633
|
-
appendCustomFieldLines(lines, entityOnlyCustomFields, 'Customer custom')
|
|
678
|
+
appendCustomFieldLines(lines, selectProfileSearchFields(entityOnlyCustomFields, 'person', embeddingOptions), 'Customer custom')
|
|
634
679
|
}
|
|
635
680
|
if (!entity) {
|
|
636
681
|
logger.warn('Failed to load customer entity for person profile', {
|
|
@@ -640,8 +685,8 @@ export const searchConfig: SearchModuleConfig = {
|
|
|
640
685
|
recordKeys: Object.keys(record),
|
|
641
686
|
})
|
|
642
687
|
}
|
|
643
|
-
appendCustomerEntityLines(lines, entity, '
|
|
644
|
-
ensureFallbackLines(lines, record)
|
|
688
|
+
appendCustomerEntityLines(lines, selectProfileSearchFields(entity, 'person', { ...embeddingOptions, customerEntity: true }))
|
|
689
|
+
ensureFallbackLines(lines, { id: record.id ?? entityId ?? 'customers:customer_person_profile' })
|
|
645
690
|
if (!lines.length) return null
|
|
646
691
|
|
|
647
692
|
if (!entityId) {
|
|
@@ -667,12 +712,7 @@ export const searchConfig: SearchModuleConfig = {
|
|
|
667
712
|
text: lines,
|
|
668
713
|
presenter,
|
|
669
714
|
links,
|
|
670
|
-
checksumSource: {
|
|
671
|
-
record: ctx.record,
|
|
672
|
-
customFields: profileCustomFields,
|
|
673
|
-
entity,
|
|
674
|
-
entityCustomFields,
|
|
675
|
-
},
|
|
715
|
+
checksumSource: { text: lines, presenter, links },
|
|
676
716
|
}
|
|
677
717
|
},
|
|
678
718
|
|
|
@@ -728,22 +768,17 @@ export const searchConfig: SearchModuleConfig = {
|
|
|
728
768
|
const { t } = await resolveTranslations()
|
|
729
769
|
const lines: string[] = []
|
|
730
770
|
const record = ctx.record
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
appendLine(lines, 'Domain', record.domain ?? record.website_domain ?? record.websiteDomain ?? ctx.customFields.domain)
|
|
734
|
-
appendLine(lines, 'Website', record.website_url ?? record.websiteUrl ?? ctx.customFields.website_url)
|
|
735
|
-
appendLine(lines, 'Industry', record.industry ?? ctx.customFields.industry)
|
|
736
|
-
appendLine(lines, 'Company size', record.size_bucket ?? record.sizeBucket ?? ctx.customFields.size_bucket)
|
|
737
|
-
appendLine(lines, 'Annual revenue', record.annual_revenue ?? record.annualRevenue ?? ctx.customFields.annual_revenue)
|
|
738
|
-
|
|
739
|
-
const { entity, entityId, profileCustomFields, entityCustomFields, entityOnlyCustomFields } =
|
|
771
|
+
|
|
772
|
+
const { entity, entityId, profileCustomFields, entityOnlyCustomFields } =
|
|
740
773
|
await hydrateProfileContext(ctx, 'company')
|
|
741
|
-
|
|
774
|
+
const embeddingOptions = { forEmbedding: true, encryptedFields: await resolveProfileEncryptedFields(ctx, 'company', entity) }
|
|
775
|
+
appendCustomFieldLines(lines, selectProfileSearchFields(record, 'company', embeddingOptions), '')
|
|
776
|
+
appendCustomFieldLines(lines, selectProfileSearchFields(profileCustomFields, 'company', embeddingOptions), 'Company custom')
|
|
742
777
|
if (Object.keys(entityOnlyCustomFields).length) {
|
|
743
|
-
appendCustomFieldLines(lines, entityOnlyCustomFields, 'Customer custom')
|
|
778
|
+
appendCustomFieldLines(lines, selectProfileSearchFields(entityOnlyCustomFields, 'company', embeddingOptions), 'Customer custom')
|
|
744
779
|
}
|
|
745
|
-
appendCustomerEntityLines(lines, entity, '
|
|
746
|
-
ensureFallbackLines(lines, record)
|
|
780
|
+
appendCustomerEntityLines(lines, selectProfileSearchFields(entity, 'company', { ...embeddingOptions, customerEntity: true }))
|
|
781
|
+
ensureFallbackLines(lines, { id: record.id ?? entityId ?? 'customers:customer_company_profile' })
|
|
747
782
|
if (!lines.length) return null
|
|
748
783
|
|
|
749
784
|
const presenter = resolveCompanyPresenter(t, record, entity, ctx.customFields)
|
|
@@ -761,12 +796,7 @@ export const searchConfig: SearchModuleConfig = {
|
|
|
761
796
|
text: lines,
|
|
762
797
|
presenter,
|
|
763
798
|
links,
|
|
764
|
-
checksumSource: {
|
|
765
|
-
record: ctx.record,
|
|
766
|
-
customFields: profileCustomFields,
|
|
767
|
-
entity,
|
|
768
|
-
entityCustomFields,
|
|
769
|
-
},
|
|
799
|
+
checksumSource: { text: lines, presenter, links },
|
|
770
800
|
}
|
|
771
801
|
},
|
|
772
802
|
|