@open-mercato/core 0.10.1-develop.7800.1.54566eb974 → 0.10.1-develop.7801.1.52db3dc0dd

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.
@@ -3,7 +3,7 @@ import { loadCustomFieldValues } from "@open-mercato/shared/lib/crud/custom-fiel
3
3
  import { normalizeCustomFieldResponse } from "@open-mercato/shared/lib/custom-fields/normalize";
4
4
  import { findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
5
5
  import { CustomerDeal, CustomerEntity } from "../data/entities.js";
6
- import { User } from "@open-mercato/core/modules/auth/data/entities";
6
+ import { interactionUserKey, loadInteractionAuthors } from "./interactionUsers.js";
7
7
  import {
8
8
  CUSTOMER_INTERACTION_ENTITY_ID
9
9
  } from "./interactionCompatibility.js";
@@ -69,11 +69,6 @@ async function hydrateCanonicalInteractions({
69
69
  enrich = false
70
70
  }) {
71
71
  if (interactions.length === 0) return [];
72
- const authorIds = Array.from(
73
- new Set(
74
- interactions.map((interaction) => typeof interaction.authorUserId === "string" ? interaction.authorUserId : null).filter((value) => !!value)
75
- )
76
- );
77
72
  const dealIds = Array.from(
78
73
  new Set(
79
74
  interactions.map((interaction) => typeof interaction.dealId === "string" ? interaction.dealId : null).filter((value) => !!value)
@@ -81,8 +76,8 @@ async function hydrateCanonicalInteractions({
81
76
  );
82
77
  const tenantId = auth.tenantId ?? null;
83
78
  const organizationId = selectedOrganizationId ?? null;
84
- const [users, deals, customFieldValues] = await Promise.all([
85
- authorIds.length > 0 ? findWithDecryption(em, User, { id: { $in: authorIds } }, void 0, { tenantId, organizationId }) : Promise.resolve([]),
79
+ const [userMap, deals, customFieldValues] = await Promise.all([
80
+ loadInteractionAuthors(em, interactions),
86
81
  dealIds.length > 0 ? findWithDecryption(em, CustomerDeal, { id: { $in: dealIds } }, void 0, { tenantId, organizationId }) : Promise.resolve([]),
87
82
  loadCustomFieldValues({
88
83
  em,
@@ -93,15 +88,6 @@ async function hydrateCanonicalInteractions({
93
88
  tenantFallbacks: [auth.tenantId].filter((value) => !!value)
94
89
  })
95
90
  ]);
96
- const userMap = new Map(
97
- users.map((user) => [
98
- user.id,
99
- {
100
- name: user.name ?? null,
101
- email: user.email ?? null
102
- }
103
- ])
104
- );
105
91
  const dealMap = new Map(deals.map((deal) => [deal.id, deal.title]));
106
92
  const baseItems = interactions.map((interaction) => {
107
93
  const entityId = typeof interaction.entity === "string" ? interaction.entity : interaction.entity.id;
@@ -136,8 +122,8 @@ async function hydrateCanonicalInteractions({
136
122
  tenantId: interaction.tenantId,
137
123
  createdAt: interaction.createdAt.toISOString(),
138
124
  updatedAt: interaction.updatedAt.toISOString(),
139
- authorName: interaction.authorUserId ? userMap.get(interaction.authorUserId)?.name ?? null : null,
140
- authorEmail: interaction.authorUserId ? userMap.get(interaction.authorUserId)?.email ?? null : null,
125
+ authorName: interaction.authorUserId ? userMap.get(interactionUserKey(interaction, interaction.authorUserId))?.name ?? null : null,
126
+ authorEmail: interaction.authorUserId ? userMap.get(interactionUserKey(interaction, interaction.authorUserId))?.email ?? null : null,
141
127
  dealTitle: interaction.dealId ? dealMap.get(interaction.dealId) ?? null : null,
142
128
  customValues: normalizeInteractionCustomValues(customFieldValues[interaction.id])
143
129
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/customers/lib/interactionReadModel.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { applyResponseEnrichers } from '@open-mercato/shared/lib/crud/enricher-runner'\nimport { loadCustomFieldValues } from '@open-mercato/shared/lib/crud/custom-fields'\nimport { normalizeCustomFieldResponse } from '@open-mercato/shared/lib/custom-fields/normalize'\nimport type { EnricherContext } from '@open-mercato/shared/lib/crud/response-enricher'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { CustomerDeal, CustomerEntity, CustomerInteraction } from '../data/entities'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport {\n CUSTOMER_INTERACTION_ENTITY_ID,\n type InteractionRecord,\n} from './interactionCompatibility'\n\ntype ContainerLike = {\n resolve: (name: string) => unknown\n}\n\ntype AuthLike = {\n tenantId: string | null\n orgId: string | null\n sub?: string | null\n userId?: string | null\n keyId?: string | null\n}\n\ntype RbacServiceLike = {\n getGrantedFeatures?: (\n userId: string,\n input: { tenantId: string | null; organizationId: string | null },\n ) => Promise<string[]>\n}\n\ntype HydrateCanonicalInteractionsInput = {\n em: EntityManager\n container: ContainerLike\n auth: AuthLike\n selectedOrganizationId: string | null\n interactions: CustomerInteraction[]\n enrich?: boolean\n}\n\ntype CustomerSummary = {\n id: string\n displayName: string | null\n kind: string | null\n}\n\nfunction resolveActorId(auth: AuthLike): string {\n if (typeof auth.sub === 'string' && auth.sub.trim().length > 0) return auth.sub\n if (typeof auth.userId === 'string' && auth.userId.trim().length > 0) return auth.userId\n if (typeof auth.keyId === 'string' && auth.keyId.trim().length > 0) return auth.keyId\n return 'system'\n}\n\nfunction mergeAdditiveRecord<T extends Record<string, unknown>>(base: T, candidate: T | undefined): T {\n if (!candidate) return base\n const additions = Object.fromEntries(\n Object.entries(candidate).filter(([key]) => !(key in base)),\n ) as Partial<T>\n return {\n ...base,\n ...additions,\n }\n}\n\nfunction normalizeInteractionCustomValues(values: Record<string, unknown> | null | undefined): Record<string, unknown> | null {\n return normalizeCustomFieldResponse(values) ?? null\n}\n\nasync function resolveUserFeatures(\n container: ContainerLike,\n userId: string,\n tenantId: string | null,\n organizationId: string | null,\n): Promise<string[] | undefined> {\n try {\n const rbac = container.resolve('rbacService') as RbacServiceLike | undefined\n if (!rbac?.getGrantedFeatures) return undefined\n return await rbac.getGrantedFeatures(userId, { tenantId, organizationId })\n } catch {\n return undefined\n }\n}\n\nexport async function buildCustomersInteractionEnricherContext(\n container: ContainerLike,\n auth: AuthLike,\n organizationId: string | null,\n): Promise<EnricherContext> {\n const userId = resolveActorId(auth)\n return {\n organizationId: organizationId ?? '',\n tenantId: auth.tenantId ?? '',\n userId,\n em: container.resolve('em'),\n container,\n userFeatures: await resolveUserFeatures(container, userId, auth.tenantId, organizationId),\n }\n}\n\nexport async function loadCustomerSummaries(\n em: EntityManager,\n entityIds: string[],\n tenantId?: string | null,\n organizationId?: string | null,\n): Promise<Map<string, CustomerSummary>> {\n if (!entityIds.length) return new Map()\n const entities = await findWithDecryption(em, CustomerEntity, { id: { $in: entityIds } }, undefined, { tenantId, organizationId })\n return new Map(\n entities.map((entity) => [\n entity.id,\n {\n id: entity.id,\n displayName: entity.displayName ?? null,\n kind: entity.kind ?? null,\n },\n ]),\n )\n}\n\nexport async function hydrateCanonicalInteractions({\n em,\n container,\n auth,\n selectedOrganizationId,\n interactions,\n enrich = false,\n}: HydrateCanonicalInteractionsInput): Promise<InteractionRecord[]> {\n if (interactions.length === 0) return []\n\n const authorIds = Array.from(\n new Set(\n interactions\n .map((interaction) =>\n typeof interaction.authorUserId === 'string' ? interaction.authorUserId : null)\n .filter((value): value is string => !!value),\n ),\n )\n const dealIds = Array.from(\n new Set(\n interactions\n .map((interaction) => (typeof interaction.dealId === 'string' ? interaction.dealId : null))\n .filter((value): value is string => !!value),\n ),\n )\n\n const tenantId = auth.tenantId ?? null\n const organizationId = selectedOrganizationId ?? null\n const [users, deals, customFieldValues] = await Promise.all([\n authorIds.length > 0 ? findWithDecryption(em, User, { id: { $in: authorIds } }, undefined, { tenantId, organizationId }) : Promise.resolve([]),\n dealIds.length > 0 ? findWithDecryption(em, CustomerDeal, { id: { $in: dealIds } }, undefined, { tenantId, organizationId }) : Promise.resolve([]),\n loadCustomFieldValues({\n em,\n entityId: CUSTOMER_INTERACTION_ENTITY_ID,\n recordIds: interactions.map((interaction) => interaction.id),\n tenantIdByRecord: Object.fromEntries(interactions.map((interaction) => [interaction.id, interaction.tenantId])),\n organizationIdByRecord: Object.fromEntries(interactions.map((interaction) => [interaction.id, interaction.organizationId])),\n tenantFallbacks: [auth.tenantId].filter((value): value is string => !!value),\n }),\n ])\n\n const userMap = new Map(\n users.map((user) => [\n user.id,\n {\n name: user.name ?? null,\n email: user.email ?? null,\n },\n ]),\n )\n const dealMap = new Map(deals.map((deal) => [deal.id, deal.title]))\n\n const baseItems: InteractionRecord[] = interactions.map((interaction) => {\n const entityId = typeof interaction.entity === 'string' ? interaction.entity : interaction.entity.id\n return {\n id: interaction.id,\n entityId,\n dealId: interaction.dealId ?? null,\n interactionType: interaction.interactionType,\n title: interaction.title ?? null,\n body: interaction.body ?? null,\n status: interaction.status,\n scheduledAt: interaction.scheduledAt ? interaction.scheduledAt.toISOString() : null,\n occurredAt: interaction.occurredAt ? interaction.occurredAt.toISOString() : null,\n priority: interaction.priority ?? null,\n authorUserId: interaction.authorUserId ?? null,\n ownerUserId: interaction.ownerUserId ?? null,\n appearanceIcon: interaction.appearanceIcon ?? null,\n appearanceColor: interaction.appearanceColor ?? null,\n source: interaction.source ?? null,\n duration: interaction.durationMinutes ?? null,\n timezone: interaction.timezone ?? null,\n location: interaction.location ?? null,\n allDay: interaction.allDay ?? null,\n recurrenceRule: interaction.recurrenceRule ?? null,\n recurrenceEnd: interaction.recurrenceEnd ? interaction.recurrenceEnd.toISOString() : null,\n participants: interaction.participants ?? null,\n reminderMinutes: interaction.reminderMinutes ?? null,\n visibility: interaction.visibility ?? null,\n linkedEntities: interaction.linkedEntities ?? null,\n guestPermissions: interaction.guestPermissions ?? null,\n organizationId: interaction.organizationId,\n tenantId: interaction.tenantId,\n createdAt: interaction.createdAt.toISOString(),\n updatedAt: interaction.updatedAt.toISOString(),\n authorName: interaction.authorUserId ? userMap.get(interaction.authorUserId)?.name ?? null : null,\n authorEmail: interaction.authorUserId ? userMap.get(interaction.authorUserId)?.email ?? null : null,\n dealTitle: interaction.dealId ? dealMap.get(interaction.dealId) ?? null : null,\n customValues: normalizeInteractionCustomValues(customFieldValues[interaction.id]),\n }\n })\n\n if (!enrich) return baseItems\n\n const enricherContext = await buildCustomersInteractionEnricherContext(\n container,\n auth,\n selectedOrganizationId,\n )\n const enriched = await applyResponseEnrichers(baseItems, 'customers.interaction', enricherContext)\n return baseItems.map((item, index) => mergeAdditiveRecord(item, enriched.items[index]))\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { applyResponseEnrichers } from '@open-mercato/shared/lib/crud/enricher-runner'\nimport { loadCustomFieldValues } from '@open-mercato/shared/lib/crud/custom-fields'\nimport { normalizeCustomFieldResponse } from '@open-mercato/shared/lib/custom-fields/normalize'\nimport type { EnricherContext } from '@open-mercato/shared/lib/crud/response-enricher'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { CustomerDeal, CustomerEntity, CustomerInteraction } from '../data/entities'\nimport { interactionUserKey, loadInteractionAuthors } from './interactionUsers'\nimport {\n CUSTOMER_INTERACTION_ENTITY_ID,\n type InteractionRecord,\n} from './interactionCompatibility'\n\ntype ContainerLike = {\n resolve: (name: string) => unknown\n}\n\ntype AuthLike = {\n tenantId: string | null\n orgId: string | null\n sub?: string | null\n userId?: string | null\n keyId?: string | null\n}\n\ntype RbacServiceLike = {\n getGrantedFeatures?: (\n userId: string,\n input: { tenantId: string | null; organizationId: string | null },\n ) => Promise<string[]>\n}\n\ntype HydrateCanonicalInteractionsInput = {\n em: EntityManager\n container: ContainerLike\n auth: AuthLike\n selectedOrganizationId: string | null\n interactions: CustomerInteraction[]\n enrich?: boolean\n}\n\ntype CustomerSummary = {\n id: string\n displayName: string | null\n kind: string | null\n}\n\nfunction resolveActorId(auth: AuthLike): string {\n if (typeof auth.sub === 'string' && auth.sub.trim().length > 0) return auth.sub\n if (typeof auth.userId === 'string' && auth.userId.trim().length > 0) return auth.userId\n if (typeof auth.keyId === 'string' && auth.keyId.trim().length > 0) return auth.keyId\n return 'system'\n}\n\nfunction mergeAdditiveRecord<T extends Record<string, unknown>>(base: T, candidate: T | undefined): T {\n if (!candidate) return base\n const additions = Object.fromEntries(\n Object.entries(candidate).filter(([key]) => !(key in base)),\n ) as Partial<T>\n return {\n ...base,\n ...additions,\n }\n}\n\nfunction normalizeInteractionCustomValues(values: Record<string, unknown> | null | undefined): Record<string, unknown> | null {\n return normalizeCustomFieldResponse(values) ?? null\n}\n\nasync function resolveUserFeatures(\n container: ContainerLike,\n userId: string,\n tenantId: string | null,\n organizationId: string | null,\n): Promise<string[] | undefined> {\n try {\n const rbac = container.resolve('rbacService') as RbacServiceLike | undefined\n if (!rbac?.getGrantedFeatures) return undefined\n return await rbac.getGrantedFeatures(userId, { tenantId, organizationId })\n } catch {\n return undefined\n }\n}\n\nexport async function buildCustomersInteractionEnricherContext(\n container: ContainerLike,\n auth: AuthLike,\n organizationId: string | null,\n): Promise<EnricherContext> {\n const userId = resolveActorId(auth)\n return {\n organizationId: organizationId ?? '',\n tenantId: auth.tenantId ?? '',\n userId,\n em: container.resolve('em'),\n container,\n userFeatures: await resolveUserFeatures(container, userId, auth.tenantId, organizationId),\n }\n}\n\nexport async function loadCustomerSummaries(\n em: EntityManager,\n entityIds: string[],\n tenantId?: string | null,\n organizationId?: string | null,\n): Promise<Map<string, CustomerSummary>> {\n if (!entityIds.length) return new Map()\n const entities = await findWithDecryption(em, CustomerEntity, { id: { $in: entityIds } }, undefined, { tenantId, organizationId })\n return new Map(\n entities.map((entity) => [\n entity.id,\n {\n id: entity.id,\n displayName: entity.displayName ?? null,\n kind: entity.kind ?? null,\n },\n ]),\n )\n}\n\nexport async function hydrateCanonicalInteractions({\n em,\n container,\n auth,\n selectedOrganizationId,\n interactions,\n enrich = false,\n}: HydrateCanonicalInteractionsInput): Promise<InteractionRecord[]> {\n if (interactions.length === 0) return []\n\n const dealIds = Array.from(\n new Set(\n interactions\n .map((interaction) => (typeof interaction.dealId === 'string' ? interaction.dealId : null))\n .filter((value): value is string => !!value),\n ),\n )\n\n const tenantId = auth.tenantId ?? null\n const organizationId = selectedOrganizationId ?? null\n const [userMap, deals, customFieldValues] = await Promise.all([\n loadInteractionAuthors(em, interactions),\n dealIds.length > 0 ? findWithDecryption(em, CustomerDeal, { id: { $in: dealIds } }, undefined, { tenantId, organizationId }) : Promise.resolve([]),\n loadCustomFieldValues({\n em,\n entityId: CUSTOMER_INTERACTION_ENTITY_ID,\n recordIds: interactions.map((interaction) => interaction.id),\n tenantIdByRecord: Object.fromEntries(interactions.map((interaction) => [interaction.id, interaction.tenantId])),\n organizationIdByRecord: Object.fromEntries(interactions.map((interaction) => [interaction.id, interaction.organizationId])),\n tenantFallbacks: [auth.tenantId].filter((value): value is string => !!value),\n }),\n ])\n\n const dealMap = new Map(deals.map((deal) => [deal.id, deal.title]))\n\n const baseItems: InteractionRecord[] = interactions.map((interaction) => {\n const entityId = typeof interaction.entity === 'string' ? interaction.entity : interaction.entity.id\n return {\n id: interaction.id,\n entityId,\n dealId: interaction.dealId ?? null,\n interactionType: interaction.interactionType,\n title: interaction.title ?? null,\n body: interaction.body ?? null,\n status: interaction.status,\n scheduledAt: interaction.scheduledAt ? interaction.scheduledAt.toISOString() : null,\n occurredAt: interaction.occurredAt ? interaction.occurredAt.toISOString() : null,\n priority: interaction.priority ?? null,\n authorUserId: interaction.authorUserId ?? null,\n ownerUserId: interaction.ownerUserId ?? null,\n appearanceIcon: interaction.appearanceIcon ?? null,\n appearanceColor: interaction.appearanceColor ?? null,\n source: interaction.source ?? null,\n duration: interaction.durationMinutes ?? null,\n timezone: interaction.timezone ?? null,\n location: interaction.location ?? null,\n allDay: interaction.allDay ?? null,\n recurrenceRule: interaction.recurrenceRule ?? null,\n recurrenceEnd: interaction.recurrenceEnd ? interaction.recurrenceEnd.toISOString() : null,\n participants: interaction.participants ?? null,\n reminderMinutes: interaction.reminderMinutes ?? null,\n visibility: interaction.visibility ?? null,\n linkedEntities: interaction.linkedEntities ?? null,\n guestPermissions: interaction.guestPermissions ?? null,\n organizationId: interaction.organizationId,\n tenantId: interaction.tenantId,\n createdAt: interaction.createdAt.toISOString(),\n updatedAt: interaction.updatedAt.toISOString(),\n authorName: interaction.authorUserId ? userMap.get(interactionUserKey(interaction, interaction.authorUserId))?.name ?? null : null,\n authorEmail: interaction.authorUserId ? userMap.get(interactionUserKey(interaction, interaction.authorUserId))?.email ?? null : null,\n dealTitle: interaction.dealId ? dealMap.get(interaction.dealId) ?? null : null,\n customValues: normalizeInteractionCustomValues(customFieldValues[interaction.id]),\n }\n })\n\n if (!enrich) return baseItems\n\n const enricherContext = await buildCustomersInteractionEnricherContext(\n container,\n auth,\n selectedOrganizationId,\n )\n const enriched = await applyResponseEnrichers(baseItems, 'customers.interaction', enricherContext)\n return baseItems.map((item, index) => mergeAdditiveRecord(item, enriched.items[index]))\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,56 @@
1
+ import { User } from "@open-mercato/core/modules/auth/data/entities";
2
+ import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
3
+ import { findOneWithDecryption, findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
4
+ import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
5
+ function interactionUserKey(scope, userId) {
6
+ return `${scope.tenantId ?? ""}:${scope.organizationId ?? ""}:${userId}`;
7
+ }
8
+ async function findInteractionUserInScope(em, userId, scope, options = {}) {
9
+ if (!scope.tenantId || !scope.organizationId) return null;
10
+ return findOneWithDecryption(em, User, {
11
+ id: userId,
12
+ tenantId: scope.tenantId,
13
+ ...options.includeDeleted ? {} : { deletedAt: null }
14
+ }, void 0, scope);
15
+ }
16
+ async function requireInteractionUsersInScope(em, scope, references, previousReferences = {}) {
17
+ for (const field of ["authorUserId", "ownerUserId"]) {
18
+ const userId = references[field];
19
+ if (!userId) continue;
20
+ if (await findInteractionUserInScope(em, userId, scope, { includeDeleted: userId === previousReferences[field] })) continue;
21
+ const { translate } = await resolveTranslations();
22
+ throw new CrudHttpError(400, { error: translate("auth.users.form.errors.notFound", "User not found") });
23
+ }
24
+ }
25
+ async function loadInteractionAuthors(em, references) {
26
+ const groups = /* @__PURE__ */ new Map();
27
+ for (const reference of references) {
28
+ if (!reference.tenantId || !reference.organizationId || !reference.authorUserId) continue;
29
+ const key = interactionUserKey(reference, "");
30
+ let group = groups.get(key);
31
+ if (!group) {
32
+ group = { tenantId: reference.tenantId, organizationId: reference.organizationId, userIds: /* @__PURE__ */ new Set() };
33
+ groups.set(key, group);
34
+ }
35
+ group.userIds.add(reference.authorUserId);
36
+ }
37
+ const profiles = /* @__PURE__ */ new Map();
38
+ await Promise.all(Array.from(groups.values(), async ({ tenantId, organizationId, userIds }) => {
39
+ const scope = { tenantId, organizationId };
40
+ const users = await findWithDecryption(em, User, {
41
+ id: { $in: Array.from(userIds) },
42
+ tenantId
43
+ }, void 0, scope);
44
+ for (const user of users) {
45
+ profiles.set(interactionUserKey(scope, user.id), { name: user.name ?? null, email: user.email ?? null });
46
+ }
47
+ }));
48
+ return profiles;
49
+ }
50
+ export {
51
+ findInteractionUserInScope,
52
+ interactionUserKey,
53
+ loadInteractionAuthors,
54
+ requireInteractionUsersInScope
55
+ };
56
+ //# sourceMappingURL=interactionUsers.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/customers/lib/interactionUsers.ts"],
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\n\ntype InteractionUserScope = {\n tenantId?: string | null\n organizationId?: string | null\n}\n\ntype InteractionUserReferences = {\n authorUserId?: string | null\n ownerUserId?: string | null\n}\n\ntype AuthorProfile = { name: string | null; email: string | null }\n\nexport function interactionUserKey(scope: InteractionUserScope, userId: string): string {\n return `${scope.tenantId ?? ''}:${scope.organizationId ?? ''}:${userId}`\n}\n\nexport async function findInteractionUserInScope(\n em: EntityManager,\n userId: string,\n scope: InteractionUserScope,\n options: { includeDeleted?: boolean } = {},\n): Promise<User | null> {\n if (!scope.tenantId || !scope.organizationId) return null\n return findOneWithDecryption(em, User, {\n id: userId,\n tenantId: scope.tenantId,\n ...(options.includeDeleted ? {} : { deletedAt: null }),\n }, undefined, scope)\n}\n\nexport async function requireInteractionUsersInScope(\n em: EntityManager,\n scope: InteractionUserScope,\n references: InteractionUserReferences,\n previousReferences: InteractionUserReferences = {},\n): Promise<void> {\n for (const field of ['authorUserId', 'ownerUserId'] as const) {\n const userId = references[field]\n if (!userId) continue\n if (await findInteractionUserInScope(em, userId, scope, { includeDeleted: userId === previousReferences[field] })) continue\n const { translate } = await resolveTranslations()\n throw new CrudHttpError(400, { error: translate('auth.users.form.errors.notFound', 'User not found') })\n }\n}\n\nexport async function loadInteractionAuthors(\n em: EntityManager,\n references: Array<InteractionUserScope & { authorUserId?: string | null }>,\n): Promise<Map<string, AuthorProfile>> {\n const groups = new Map<string, { tenantId: string; organizationId: string; userIds: Set<string> }>()\n for (const reference of references) {\n if (!reference.tenantId || !reference.organizationId || !reference.authorUserId) continue\n const key = interactionUserKey(reference, '')\n let group = groups.get(key)\n if (!group) {\n group = { tenantId: reference.tenantId, organizationId: reference.organizationId, userIds: new Set() }\n groups.set(key, group)\n }\n group.userIds.add(reference.authorUserId)\n }\n const profiles = new Map<string, AuthorProfile>()\n await Promise.all(Array.from(groups.values(), async ({ tenantId, organizationId, userIds }) => {\n const scope = { tenantId, organizationId }\n const users = await findWithDecryption(em, User, {\n id: { $in: Array.from(userIds) },\n tenantId,\n }, undefined, scope)\n for (const user of users) {\n profiles.set(interactionUserKey(scope, user.id), { name: user.name ?? null, email: user.email ?? null })\n }\n }))\n return profiles\n}\n"],
5
+ "mappings": "AACA,SAAS,YAAY;AACrB,SAAS,qBAAqB;AAC9B,SAAS,uBAAuB,0BAA0B;AAC1D,SAAS,2BAA2B;AAc7B,SAAS,mBAAmB,OAA6B,QAAwB;AACtF,SAAO,GAAG,MAAM,YAAY,EAAE,IAAI,MAAM,kBAAkB,EAAE,IAAI,MAAM;AACxE;AAEA,eAAsB,2BACpB,IACA,QACA,OACA,UAAwC,CAAC,GACnB;AACtB,MAAI,CAAC,MAAM,YAAY,CAAC,MAAM,eAAgB,QAAO;AACrD,SAAO,sBAAsB,IAAI,MAAM;AAAA,IACrC,IAAI;AAAA,IACJ,UAAU,MAAM;AAAA,IAChB,GAAI,QAAQ,iBAAiB,CAAC,IAAI,EAAE,WAAW,KAAK;AAAA,EACtD,GAAG,QAAW,KAAK;AACrB;AAEA,eAAsB,+BACpB,IACA,OACA,YACA,qBAAgD,CAAC,GAClC;AACf,aAAW,SAAS,CAAC,gBAAgB,aAAa,GAAY;AAC5D,UAAM,SAAS,WAAW,KAAK;AAC/B,QAAI,CAAC,OAAQ;AACb,QAAI,MAAM,2BAA2B,IAAI,QAAQ,OAAO,EAAE,gBAAgB,WAAW,mBAAmB,KAAK,EAAE,CAAC,EAAG;AACnH,UAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,UAAM,IAAI,cAAc,KAAK,EAAE,OAAO,UAAU,mCAAmC,gBAAgB,EAAE,CAAC;AAAA,EACxG;AACF;AAEA,eAAsB,uBACpB,IACA,YACqC;AACrC,QAAM,SAAS,oBAAI,IAAgF;AACnG,aAAW,aAAa,YAAY;AAClC,QAAI,CAAC,UAAU,YAAY,CAAC,UAAU,kBAAkB,CAAC,UAAU,aAAc;AACjF,UAAM,MAAM,mBAAmB,WAAW,EAAE;AAC5C,QAAI,QAAQ,OAAO,IAAI,GAAG;AAC1B,QAAI,CAAC,OAAO;AACV,cAAQ,EAAE,UAAU,UAAU,UAAU,gBAAgB,UAAU,gBAAgB,SAAS,oBAAI,IAAI,EAAE;AACrG,aAAO,IAAI,KAAK,KAAK;AAAA,IACvB;AACA,UAAM,QAAQ,IAAI,UAAU,YAAY;AAAA,EAC1C;AACA,QAAM,WAAW,oBAAI,IAA2B;AAChD,QAAM,QAAQ,IAAI,MAAM,KAAK,OAAO,OAAO,GAAG,OAAO,EAAE,UAAU,gBAAgB,QAAQ,MAAM;AAC7F,UAAM,QAAQ,EAAE,UAAU,eAAe;AACzC,UAAM,QAAQ,MAAM,mBAAmB,IAAI,MAAM;AAAA,MAC/C,IAAI,EAAE,KAAK,MAAM,KAAK,OAAO,EAAE;AAAA,MAC/B;AAAA,IACF,GAAG,QAAW,KAAK;AACnB,eAAW,QAAQ,OAAO;AACxB,eAAS,IAAI,mBAAmB,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,KAAK,QAAQ,MAAM,OAAO,KAAK,SAAS,KAAK,CAAC;AAAA,IACzG;AAAA,EACF,CAAC,CAAC;AACF,SAAO;AACT;",
6
+ "names": []
7
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.10.1-develop.7800.1.54566eb974",
3
+ "version": "0.10.1-develop.7801.1.52db3dc0dd",
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.7800.1.54566eb974",
270
- "@open-mercato/shared": "0.10.1-develop.7800.1.54566eb974",
271
- "@open-mercato/ui": "0.10.1-develop.7800.1.54566eb974",
269
+ "@open-mercato/ai-assistant": "0.10.1-develop.7801.1.52db3dc0dd",
270
+ "@open-mercato/shared": "0.10.1-develop.7801.1.52db3dc0dd",
271
+ "@open-mercato/ui": "0.10.1-develop.7801.1.52db3dc0dd",
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.7800.1.54566eb974",
277
- "@open-mercato/shared": "0.10.1-develop.7800.1.54566eb974",
278
- "@open-mercato/ui": "0.10.1-develop.7800.1.54566eb974",
276
+ "@open-mercato/ai-assistant": "0.10.1-develop.7801.1.52db3dc0dd",
277
+ "@open-mercato/shared": "0.10.1-develop.7801.1.52db3dc0dd",
278
+ "@open-mercato/ui": "0.10.1-develop.7801.1.52db3dc0dd",
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",
@@ -17,7 +17,7 @@ import {
17
17
  import { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'
18
18
  import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
19
19
  import { CustomerActivity, CustomerDeal, CustomerInteraction } from '../../data/entities'
20
- import { User } from '@open-mercato/core/modules/auth/data/entities'
20
+ import { interactionUserKey, loadInteractionAuthors } from '../../lib/interactionUsers'
21
21
  import { activityCreateSchema, activityUpdateSchema } from '../../data/validators'
22
22
  import { createCustomersCrudOpenApi, createPagedListResponseSchema, defaultOkResponseSchema } from '../openapi'
23
23
  import {
@@ -212,16 +212,13 @@ export async function decorateActivityItems(
212
212
  em: EntityManager,
213
213
  items: ActivityItem[],
214
214
  decryptionScope?: { tenantId: string; organizationId: string },
215
+ authorScopesById?: ReadonlyMap<string, { tenantId: string; organizationId: string }>,
215
216
  ): Promise<ActivityItem[]> {
216
217
  if (items.length === 0) return items
217
218
 
218
- const authorIds = Array.from(
219
- new Set(
220
- items
221
- .map((item) => (typeof item.authorUserId === 'string' ? item.authorUserId : null))
222
- .filter((value): value is string => !!value),
223
- ),
224
- )
219
+ const resolveAuthorScope = (itemId: string) => authorScopesById
220
+ ? authorScopesById.get(itemId) ?? {}
221
+ : decryptionScope ?? {}
225
222
  const dealIds = Array.from(
226
223
  new Set(
227
224
  items
@@ -237,8 +234,11 @@ export async function decorateActivityItems(
237
234
  })
238
235
  }
239
236
 
240
- const [users, deals] = await Promise.all([
241
- authorIds.length > 0 ? em.find(User, { id: { $in: authorIds } }) : Promise.resolve([]),
237
+ const [userMap, deals] = await Promise.all([
238
+ loadInteractionAuthors(em, items.map((item) => ({
239
+ ...resolveAuthorScope(item.id),
240
+ authorUserId: item.authorUserId,
241
+ }))),
242
242
  dealIds.length > 0 && decryptionScope
243
243
  ? findWithDecryption(
244
244
  em,
@@ -250,24 +250,20 @@ export async function decorateActivityItems(
250
250
  : Promise.resolve([]),
251
251
  ])
252
252
 
253
- const userMap = new Map(
254
- users.map((user) => [
255
- user.id,
256
- {
257
- name: user.name ?? null,
258
- email: user.email ?? null,
259
- },
260
- ]),
261
- )
262
253
  const dealMap = new Map(deals.map((deal) => [deal.id, deal.title]))
263
254
 
264
- return items.map((item) => ({
265
- ...item,
266
- activityTypeLabel: item.activityType,
267
- authorName: item.authorUserId ? userMap.get(item.authorUserId)?.name ?? null : null,
268
- authorEmail: item.authorUserId ? userMap.get(item.authorUserId)?.email ?? null : null,
269
- dealTitle: item.dealId ? dealMap.get(item.dealId) ?? null : null,
270
- }))
255
+ return items.map((item) => {
256
+ const author = item.authorUserId
257
+ ? userMap.get(interactionUserKey(resolveAuthorScope(item.id), item.authorUserId))
258
+ : null
259
+ return {
260
+ ...item,
261
+ activityTypeLabel: item.activityType,
262
+ authorName: author?.name ?? null,
263
+ authorEmail: author?.email ?? null,
264
+ dealTitle: item.dealId ? dealMap.get(item.dealId) ?? null : null,
265
+ }
266
+ })
271
267
  }
272
268
 
273
269
  function mapLegacyActivity(activity: CustomerActivity): ActivityItem {
@@ -438,6 +434,7 @@ async function listLegacyActivities(
438
434
  em,
439
435
  rows.map(mapLegacyActivity),
440
436
  selectedOrganizationId ? { tenantId, organizationId: selectedOrganizationId } : undefined,
437
+ new Map(rows.map((row) => [row.id, { tenantId: row.tenantId, organizationId: row.organizationId }])),
441
438
  ),
442
439
  total,
443
440
  }
@@ -18,7 +18,7 @@ import { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/d
18
18
  import { resolveOrganizationScopeFilter } from '@open-mercato/core/modules/directory/utils/organizationScopeFilter'
19
19
  import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
20
20
  import { CustomerDeal, CustomerInteraction } from '../../data/entities'
21
- import { User } from '@open-mercato/core/modules/auth/data/entities'
21
+ import { interactionUserKey, loadInteractionAuthors } from '../../lib/interactionUsers'
22
22
  import { interactionCreateSchema, interactionUpdateSchema } from '../../data/validators'
23
23
  import { parseScopedCommandInput } from '../utils'
24
24
  import {
@@ -614,13 +614,6 @@ export async function GET(req: Request) {
614
614
  hasMore = rows.length > query.limit
615
615
  }
616
616
 
617
- const authorIds = Array.from(
618
- new Set(
619
- pageRows
620
- .map((row) => (typeof row.author_user_id === 'string' ? row.author_user_id : null))
621
- .filter((value): value is string => !!value),
622
- ),
623
- )
624
617
  const dealIds = Array.from(
625
618
  new Set(
626
619
  pageRows
@@ -644,8 +637,12 @@ export async function GET(req: Request) {
644
637
  new Set(pageRows.map((row) => row.organization_id).filter((value): value is string => !!value)),
645
638
  )
646
639
 
647
- const [users, deals, customFieldValues, interactionRecords, encryptedFieldsByOrganization] = await Promise.all([
648
- authorIds.length > 0 ? findWithDecryption(em, User, { id: { $in: authorIds } }, undefined, { tenantId: auth.tenantId, organizationId: selectedOrganizationId }) : Promise.resolve([]),
640
+ const [userMap, deals, customFieldValues, interactionRecords, encryptedFieldsByOrganization] = await Promise.all([
641
+ loadInteractionAuthors(em, pageRows.map((row) => ({
642
+ tenantId: row.tenant_id,
643
+ organizationId: row.organization_id,
644
+ authorUserId: row.author_user_id,
645
+ }))),
649
646
  dealIds.length > 0 ? findWithDecryption(em, CustomerDeal, { id: { $in: dealIds } }, undefined, { tenantId: auth.tenantId, organizationId: selectedOrganizationId }) : Promise.resolve([]),
650
647
  interactionIds.length > 0
651
648
  ? loadCustomFieldValues({
@@ -673,15 +670,6 @@ export async function GET(req: Request) {
673
670
  })(),
674
671
  ])
675
672
 
676
- const userMap = new Map(
677
- users.map((user) => [
678
- user.id,
679
- {
680
- name: user.name ?? null,
681
- email: user.email ?? null,
682
- },
683
- ]),
684
- )
685
673
  const dealMap = new Map(
686
674
  deals.map((deal) => [deal.id, deal.title]),
687
675
  )
@@ -731,8 +719,8 @@ export async function GET(req: Request) {
731
719
  tenantId: row.tenant_id,
732
720
  createdAt: toIsoString(row.created_at) ?? new Date().toISOString(),
733
721
  updatedAt: toIsoString(row.updated_at) ?? new Date().toISOString(),
734
- authorName: row.author_user_id ? userMap.get(row.author_user_id)?.name ?? null : null,
735
- authorEmail: row.author_user_id ? userMap.get(row.author_user_id)?.email ?? null : null,
722
+ authorName: row.author_user_id ? userMap.get(interactionUserKey({ tenantId: row.tenant_id, organizationId: row.organization_id }, row.author_user_id))?.name ?? null : null,
723
+ authorEmail: row.author_user_id ? userMap.get(interactionUserKey({ tenantId: row.tenant_id, organizationId: row.organization_id }, row.author_user_id))?.email ?? null : null,
736
724
  dealTitle: row.deal_id ? dealMap.get(row.deal_id) ?? null : null,
737
725
  customValues: normalizeCustomFieldResponse(customFieldValues[row.id]) ?? null,
738
726
  }, interactionRecordMap.get(row.id), encryptedFieldsByOrganization.get(row.organization_id) ?? []))
@@ -771,7 +771,10 @@ export async function GET(_req: Request, ctx: { params?: { id?: string } }) {
771
771
  if (viewerUserId) authorIds.add(viewerUserId)
772
772
 
773
773
  if (authorIds.size) {
774
- const users = await findWithDecryption(em, User, { id: { $in: Array.from(authorIds) } }, {}, { tenantId: person.tenantId ?? auth.tenantId ?? null, organizationId: person.organizationId ?? auth.orgId ?? null })
774
+ const users = await findWithDecryption(em, User, {
775
+ id: { $in: Array.from(authorIds) },
776
+ tenantId: person.tenantId,
777
+ }, {}, { tenantId: person.tenantId, organizationId: person.organizationId })
775
778
  userMap = new Map(
776
779
  users.map((user) => [
777
780
  user.id,
@@ -12,7 +12,8 @@ import {
12
12
  import { DefaultDataEngine, type DataEngine } from '@open-mercato/shared/lib/data/engine'
13
13
  import type { EntityManager } from '@mikro-orm/postgresql'
14
14
  import type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
15
- import { CustomerInteraction, CustomerEntity } from '../data/entities'
15
+ import { CustomerActivity, CustomerInteraction, CustomerEntity } from '../data/entities'
16
+ import { User } from '@open-mercato/core/modules/auth/data/entities'
16
17
  import {
17
18
  interactionCreateSchema,
18
19
  interactionUpdateSchema,
@@ -38,7 +39,7 @@ import {
38
39
  loadCustomFieldSnapshot,
39
40
  buildCustomFieldResetMap,
40
41
  } from '@open-mercato/shared/lib/commands/customFieldSnapshots'
41
- import { CrudHttpError, notFound } from '@open-mercato/shared/lib/crud/errors'
42
+ import { badRequest, CrudHttpError, notFound } from '@open-mercato/shared/lib/crud/errors'
42
43
  import { enforceRecordGoneIsConflict, enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
43
44
  import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
44
45
  import type { CrudIndexerConfig, CrudEventsConfig } from '@open-mercato/shared/lib/crud/types'
@@ -57,6 +58,8 @@ import { resolveCatalogEventType, resolveScopedCalendarEventTypes } from '../lib
57
58
  import { clearInapplicableCoreFields, findInapplicableCoreFields, omitHiddenCoreClears, preserveHiddenCoreValuesOnSameTypeEdit } from '../lib/calendar/interactionApplicability'
58
59
  import { calendarEventTypes, type CalendarEventTypeBehavior } from '../calendar-event-types'
59
60
  import { canonicalCalendarTimezone } from '../lib/calendar/timezone'
61
+ import { findInteractionUserInScope, requireInteractionUsersInScope } from '../lib/interactionUsers'
62
+ import { CUSTOMER_INTERACTION_ACTIVITY_ADAPTER_SOURCE } from '../lib/interactionCompatibility'
60
63
 
61
64
  const logger = createLogger('customers')
62
65
 
@@ -205,6 +208,16 @@ async function ensureInteractionRestoreAccess(
205
208
  await ensureInteractionPrivacyChangeAccess(ctx, current, { ...restored, visibility: restored.visibility ?? null })
206
209
  }
207
210
  await ensureInteractionMutationAccess(ctx, em, { ...restored, channelId: current?.channelId })
211
+ const userIds = new Set([restored.authorUserId, restored.ownerUserId].filter((value): value is string => !!value))
212
+ for (const userId of userIds) {
213
+ const user = await findOneWithDecryption(em, User, {
214
+ id: userId,
215
+ tenantId: restored.tenantId,
216
+ }, undefined, { tenantId: restored.tenantId, organizationId: restored.organizationId })
217
+ if (user) continue
218
+ const { translate } = await resolveTranslations()
219
+ throw badRequest(translate('auth.users.form.errors.notFound', 'User not found'))
220
+ }
208
221
  }
209
222
 
210
223
  async function loadInteractionSnapshot(em: EntityManager, id: string, ctx?: CommandRuntimeContext): Promise<InteractionSnapshot | null> {
@@ -559,12 +572,28 @@ const createInteractionCommand: CommandHandler<InteractionCreateInput, { interac
559
572
  const { parsed, custom } = parseWithCustomFields(interactionCreateSchema, rawInput)
560
573
 
561
574
  const em = ctx.transactionalEm ?? (ctx.container.resolve('em') as EntityManager).fork()
562
- const normalizedAuthor = normalizeAuthorUserId(parsed.authorUserId ?? null, ctx.auth)
575
+ const defaultAuthor = normalizeAuthorUserId(parsed.authorUserId ?? null, ctx.auth)
563
576
  const { interaction, entityId, nextInteractionId } = await runInTransaction(em, async (trx) => {
564
577
  const entity = await requireTimelineParentEntity(trx, parsed.entityId, { tenantId: parsed.tenantId, organizationId: parsed.organizationId })
565
578
  ensureTenantScope(ctx, entity.tenantId)
566
579
  ensureOrganizationScope(ctx, entity.organizationId)
567
580
 
581
+ const legacyActivity = parsed.id && parsed.source === CUSTOMER_INTERACTION_ACTIVITY_ADAPTER_SOURCE
582
+ ? await findOneWithDecryption(trx, CustomerActivity, {
583
+ id: parsed.id,
584
+ entity: entity.id,
585
+ tenantId: entity.tenantId,
586
+ organizationId: entity.organizationId,
587
+ }, undefined, entity)
588
+ : null
589
+ const preservesHistoricalAuthor = !!legacyActivity && parsed.authorUserId === legacyActivity.authorUserId
590
+ await requireInteractionUsersInScope(trx, entity, parsed, legacyActivity ?? {})
591
+ const normalizedAuthor = preservesHistoricalAuthor
592
+ ? legacyActivity.authorUserId ?? null
593
+ : defaultAuthor && await findInteractionUserInScope(trx, defaultAuthor, entity)
594
+ ? defaultAuthor
595
+ : null
596
+
568
597
  if (parsed.dealId) {
569
598
  await requireDealInScope(trx, parsed.dealId, entity.tenantId, entity.organizationId)
570
599
  }
@@ -865,6 +894,7 @@ const updateInteractionCommand: CommandHandler<InteractionUpdateInput, { interac
865
894
  })
866
895
 
867
896
  await ensureInteractionPrivacyChangeAccess(ctx, interaction, parsed)
897
+ await requireInteractionUsersInScope(trx, interaction, parsed, interaction)
868
898
 
869
899
  const nextType = parsed.interactionType ?? interaction.interactionType
870
900
  const changingType = nextType !== interaction.interactionType
@@ -5,7 +5,7 @@ import { normalizeCustomFieldResponse } from '@open-mercato/shared/lib/custom-fi
5
5
  import type { EnricherContext } from '@open-mercato/shared/lib/crud/response-enricher'
6
6
  import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
7
7
  import { CustomerDeal, CustomerEntity, CustomerInteraction } from '../data/entities'
8
- import { User } from '@open-mercato/core/modules/auth/data/entities'
8
+ import { interactionUserKey, loadInteractionAuthors } from './interactionUsers'
9
9
  import {
10
10
  CUSTOMER_INTERACTION_ENTITY_ID,
11
11
  type InteractionRecord,
@@ -128,14 +128,6 @@ export async function hydrateCanonicalInteractions({
128
128
  }: HydrateCanonicalInteractionsInput): Promise<InteractionRecord[]> {
129
129
  if (interactions.length === 0) return []
130
130
 
131
- const authorIds = Array.from(
132
- new Set(
133
- interactions
134
- .map((interaction) =>
135
- typeof interaction.authorUserId === 'string' ? interaction.authorUserId : null)
136
- .filter((value): value is string => !!value),
137
- ),
138
- )
139
131
  const dealIds = Array.from(
140
132
  new Set(
141
133
  interactions
@@ -146,8 +138,8 @@ export async function hydrateCanonicalInteractions({
146
138
 
147
139
  const tenantId = auth.tenantId ?? null
148
140
  const organizationId = selectedOrganizationId ?? null
149
- const [users, deals, customFieldValues] = await Promise.all([
150
- authorIds.length > 0 ? findWithDecryption(em, User, { id: { $in: authorIds } }, undefined, { tenantId, organizationId }) : Promise.resolve([]),
141
+ const [userMap, deals, customFieldValues] = await Promise.all([
142
+ loadInteractionAuthors(em, interactions),
151
143
  dealIds.length > 0 ? findWithDecryption(em, CustomerDeal, { id: { $in: dealIds } }, undefined, { tenantId, organizationId }) : Promise.resolve([]),
152
144
  loadCustomFieldValues({
153
145
  em,
@@ -159,15 +151,6 @@ export async function hydrateCanonicalInteractions({
159
151
  }),
160
152
  ])
161
153
 
162
- const userMap = new Map(
163
- users.map((user) => [
164
- user.id,
165
- {
166
- name: user.name ?? null,
167
- email: user.email ?? null,
168
- },
169
- ]),
170
- )
171
154
  const dealMap = new Map(deals.map((deal) => [deal.id, deal.title]))
172
155
 
173
156
  const baseItems: InteractionRecord[] = interactions.map((interaction) => {
@@ -203,8 +186,8 @@ export async function hydrateCanonicalInteractions({
203
186
  tenantId: interaction.tenantId,
204
187
  createdAt: interaction.createdAt.toISOString(),
205
188
  updatedAt: interaction.updatedAt.toISOString(),
206
- authorName: interaction.authorUserId ? userMap.get(interaction.authorUserId)?.name ?? null : null,
207
- authorEmail: interaction.authorUserId ? userMap.get(interaction.authorUserId)?.email ?? null : null,
189
+ authorName: interaction.authorUserId ? userMap.get(interactionUserKey(interaction, interaction.authorUserId))?.name ?? null : null,
190
+ authorEmail: interaction.authorUserId ? userMap.get(interactionUserKey(interaction, interaction.authorUserId))?.email ?? null : null,
208
191
  dealTitle: interaction.dealId ? dealMap.get(interaction.dealId) ?? null : null,
209
192
  customValues: normalizeInteractionCustomValues(customFieldValues[interaction.id]),
210
193
  }
@@ -0,0 +1,79 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql'
2
+ import { User } from '@open-mercato/core/modules/auth/data/entities'
3
+ import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
4
+ import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
5
+ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
6
+
7
+ type InteractionUserScope = {
8
+ tenantId?: string | null
9
+ organizationId?: string | null
10
+ }
11
+
12
+ type InteractionUserReferences = {
13
+ authorUserId?: string | null
14
+ ownerUserId?: string | null
15
+ }
16
+
17
+ type AuthorProfile = { name: string | null; email: string | null }
18
+
19
+ export function interactionUserKey(scope: InteractionUserScope, userId: string): string {
20
+ return `${scope.tenantId ?? ''}:${scope.organizationId ?? ''}:${userId}`
21
+ }
22
+
23
+ export async function findInteractionUserInScope(
24
+ em: EntityManager,
25
+ userId: string,
26
+ scope: InteractionUserScope,
27
+ options: { includeDeleted?: boolean } = {},
28
+ ): Promise<User | null> {
29
+ if (!scope.tenantId || !scope.organizationId) return null
30
+ return findOneWithDecryption(em, User, {
31
+ id: userId,
32
+ tenantId: scope.tenantId,
33
+ ...(options.includeDeleted ? {} : { deletedAt: null }),
34
+ }, undefined, scope)
35
+ }
36
+
37
+ export async function requireInteractionUsersInScope(
38
+ em: EntityManager,
39
+ scope: InteractionUserScope,
40
+ references: InteractionUserReferences,
41
+ previousReferences: InteractionUserReferences = {},
42
+ ): Promise<void> {
43
+ for (const field of ['authorUserId', 'ownerUserId'] as const) {
44
+ const userId = references[field]
45
+ if (!userId) continue
46
+ if (await findInteractionUserInScope(em, userId, scope, { includeDeleted: userId === previousReferences[field] })) continue
47
+ const { translate } = await resolveTranslations()
48
+ throw new CrudHttpError(400, { error: translate('auth.users.form.errors.notFound', 'User not found') })
49
+ }
50
+ }
51
+
52
+ export async function loadInteractionAuthors(
53
+ em: EntityManager,
54
+ references: Array<InteractionUserScope & { authorUserId?: string | null }>,
55
+ ): Promise<Map<string, AuthorProfile>> {
56
+ const groups = new Map<string, { tenantId: string; organizationId: string; userIds: Set<string> }>()
57
+ for (const reference of references) {
58
+ if (!reference.tenantId || !reference.organizationId || !reference.authorUserId) continue
59
+ const key = interactionUserKey(reference, '')
60
+ let group = groups.get(key)
61
+ if (!group) {
62
+ group = { tenantId: reference.tenantId, organizationId: reference.organizationId, userIds: new Set() }
63
+ groups.set(key, group)
64
+ }
65
+ group.userIds.add(reference.authorUserId)
66
+ }
67
+ const profiles = new Map<string, AuthorProfile>()
68
+ await Promise.all(Array.from(groups.values(), async ({ tenantId, organizationId, userIds }) => {
69
+ const scope = { tenantId, organizationId }
70
+ const users = await findWithDecryption(em, User, {
71
+ id: { $in: Array.from(userIds) },
72
+ tenantId,
73
+ }, undefined, scope)
74
+ for (const user of users) {
75
+ profiles.set(interactionUserKey(scope, user.id), { name: user.name ?? null, email: user.email ?? null })
76
+ }
77
+ }))
78
+ return profiles
79
+ }