@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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/customers/api/activities/route.js +20 -26
- package/dist/modules/customers/api/activities/route.js.map +2 -2
- package/dist/modules/customers/api/interactions/route.js +9 -19
- package/dist/modules/customers/api/interactions/route.js.map +2 -2
- package/dist/modules/customers/api/people/[id]/route.js +4 -1
- package/dist/modules/customers/api/people/[id]/route.js.map +2 -2
- package/dist/modules/customers/commands/interactions.js +26 -3
- package/dist/modules/customers/commands/interactions.js.map +2 -2
- package/dist/modules/customers/lib/interactionReadModel.js +5 -19
- package/dist/modules/customers/lib/interactionReadModel.js.map +2 -2
- package/dist/modules/customers/lib/interactionUsers.js +56 -0
- package/dist/modules/customers/lib/interactionUsers.js.map +7 -0
- package/package.json +7 -7
- package/src/modules/customers/api/activities/route.ts +23 -26
- package/src/modules/customers/api/interactions/route.ts +9 -21
- package/src/modules/customers/api/people/[id]/route.ts +4 -1
- package/src/modules/customers/commands/interactions.ts +33 -3
- package/src/modules/customers/lib/interactionReadModel.ts +5 -22
- package/src/modules/customers/lib/interactionUsers.ts +79 -0
|
@@ -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 {
|
|
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 [
|
|
85
|
-
|
|
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 {
|
|
5
|
-
"mappings": "AACA,SAAS,8BAA8B;AACvC,SAAS,6BAA6B;AACtC,SAAS,oCAAoC;AAE7C,SAAS,0BAA0B;AACnC,SAAS,cAAc,sBAA2C;AAClE,SAAS,
|
|
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.
|
|
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.
|
|
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.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.
|
|
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.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 {
|
|
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
|
|
219
|
-
|
|
220
|
-
|
|
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 [
|
|
241
|
-
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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 {
|
|
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 [
|
|
648
|
-
|
|
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, {
|
|
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
|
|
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 {
|
|
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 [
|
|
150
|
-
|
|
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
|
+
}
|