@open-mercato/core 0.10.1-develop.7668.1.3aa8463bd8 → 0.10.1-develop.7670.1.7b5ad74924
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/commands/interactions.js +65 -21
- package/dist/modules/customers/commands/interactions.js.map +2 -2
- package/dist/modules/customers/components/detail/ScheduleActivityDialog.js +26 -24
- package/dist/modules/customers/components/detail/ScheduleActivityDialog.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/fieldConfig.js +10 -1
- package/dist/modules/customers/components/detail/schedule/fieldConfig.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/index.js +2 -1
- package/dist/modules/customers/components/detail/schedule/index.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js +5 -4
- package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/customers/commands/interactions.ts +87 -35
- package/src/modules/customers/components/detail/ScheduleActivityDialog.tsx +31 -24
- package/src/modules/customers/components/detail/schedule/fieldConfig.ts +16 -0
- package/src/modules/customers/components/detail/schedule/index.ts +1 -1
- package/src/modules/customers/components/detail/schedule/useScheduleFormState.ts +12 -6
package/.turbo/turbo-build.log
CHANGED
|
@@ -40,7 +40,8 @@ import {
|
|
|
40
40
|
INTERACTION_STATUS_COMPLETED,
|
|
41
41
|
INTERACTION_STATUS_PLANNED
|
|
42
42
|
} from "../lib/interactionStatus.js";
|
|
43
|
-
import { canChangeEmailVisibility } from "../lib/visibilityFilter.js";
|
|
43
|
+
import { canChangeEmailVisibility, isEmailHiddenFrom } from "../lib/visibilityFilter.js";
|
|
44
|
+
import { listGrantsForViewerOnPerson, listSharedChannelIds } from "../lib/conversationShares.js";
|
|
44
45
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
45
46
|
import { loadCustomFieldDefinitionIndex } from "@open-mercato/shared/lib/crud/custom-fields";
|
|
46
47
|
import { resolveCatalogEventType, resolveScopedCalendarEventTypes } from "../lib/calendar/eventTypeResolver.js";
|
|
@@ -73,9 +74,56 @@ const interactionCrudEvents = {
|
|
|
73
74
|
const INTERACTION_UNDO_DATE_OPTIONS = {
|
|
74
75
|
datePaths: ["before.interaction.scheduledAt", "before.interaction.occurredAt", "before.interaction.recurrenceEnd"]
|
|
75
76
|
};
|
|
76
|
-
async function
|
|
77
|
+
async function ensureInteractionMutationAccess(ctx, em, interaction) {
|
|
78
|
+
ensureTenantScope(ctx, interaction.tenantId);
|
|
79
|
+
ensureOrganizationScope(ctx, interaction.organizationId);
|
|
80
|
+
const currentUserId = ctx.auth?.isApiKey ? null : ctx.auth?.sub ?? null;
|
|
81
|
+
const personEntityId = "entityId" in interaction ? interaction.entityId : typeof interaction.entity === "string" ? interaction.entity : interaction.entity.id;
|
|
82
|
+
const visibility = {
|
|
83
|
+
interactionType: interaction.interactionType,
|
|
84
|
+
visibility: interaction.visibility,
|
|
85
|
+
authorUserId: interaction.authorUserId,
|
|
86
|
+
currentUserId,
|
|
87
|
+
personEntityId,
|
|
88
|
+
channelId: interaction.channelId
|
|
89
|
+
};
|
|
90
|
+
if (!isEmailHiddenFrom(visibility)) return;
|
|
91
|
+
const scope = { tenantId: interaction.tenantId, organizationId: interaction.organizationId };
|
|
92
|
+
const sharedConversations = await listGrantsForViewerOnPerson(em, scope, currentUserId, personEntityId);
|
|
93
|
+
const sharedChannelIds = await listSharedChannelIds(em, scope, currentUserId);
|
|
94
|
+
if (isEmailHiddenFrom({ ...visibility, sharedConversations, sharedChannelIds })) {
|
|
95
|
+
const { translate } = await resolveTranslations();
|
|
96
|
+
throw notFound(translate("customers.errors.entity_not_found", "Entity not found"));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async function ensureInteractionPrivacyChangeAccess(ctx, interaction, next) {
|
|
100
|
+
if (interaction.interactionType !== "email") return;
|
|
101
|
+
const actorUserId = ctx.auth?.isApiKey ? null : ctx.auth?.sub ?? null;
|
|
102
|
+
const changesOwnership = next.authorUserId !== void 0 && (next.authorUserId ?? null) !== (interaction.authorUserId ?? null);
|
|
103
|
+
const changesType = next.interactionType !== void 0 && next.interactionType !== interaction.interactionType;
|
|
104
|
+
if ((changesOwnership || changesType) && (!actorUserId || actorUserId !== interaction.authorUserId) || next.visibility !== void 0 && !canChangeEmailVisibility({
|
|
105
|
+
interactionType: interaction.interactionType,
|
|
106
|
+
currentVisibility: interaction.visibility,
|
|
107
|
+
nextVisibility: next.visibility,
|
|
108
|
+
authorUserId: interaction.authorUserId,
|
|
109
|
+
actorUserId,
|
|
110
|
+
userFeatures: void 0
|
|
111
|
+
})) {
|
|
112
|
+
const { translate } = await resolveTranslations();
|
|
113
|
+
throw notFound(translate("customers.errors.entity_not_found", "Entity not found"));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async function ensureInteractionRestoreAccess(ctx, em, current, restored) {
|
|
117
|
+
if (current) {
|
|
118
|
+
await ensureInteractionMutationAccess(ctx, em, current);
|
|
119
|
+
await ensureInteractionPrivacyChangeAccess(ctx, current, { ...restored, visibility: restored.visibility ?? null });
|
|
120
|
+
}
|
|
121
|
+
await ensureInteractionMutationAccess(ctx, em, { ...restored, channelId: current?.channelId });
|
|
122
|
+
}
|
|
123
|
+
async function loadInteractionSnapshot(em, id, ctx) {
|
|
77
124
|
const interaction = await findOneWithDecryption(em, CustomerInteraction, { id }, { populate: ["entity"] });
|
|
78
125
|
if (!interaction) return null;
|
|
126
|
+
if (ctx) await ensureInteractionMutationAccess(ctx, em, interaction);
|
|
79
127
|
const custom = await loadCustomFieldSnapshot(em, {
|
|
80
128
|
entityId: INTERACTION_ENTITY_ID,
|
|
81
129
|
recordId: interaction.id,
|
|
@@ -422,6 +470,7 @@ const createInteractionCommand = {
|
|
|
422
470
|
const result = await runInTransaction(em, async (trx) => {
|
|
423
471
|
const record = await findOneWithDecryption(trx, CustomerInteraction, { id: interactionId });
|
|
424
472
|
if (!record) return null;
|
|
473
|
+
await ensureInteractionMutationAccess(ctx, trx, record);
|
|
425
474
|
const entityId = typeof record.entity === "string" ? record.entity : record.entity.id;
|
|
426
475
|
trx.remove(record);
|
|
427
476
|
await trx.flush();
|
|
@@ -451,6 +500,7 @@ const createInteractionCommand = {
|
|
|
451
500
|
const { interaction, nextInteractionId } = await runInTransaction(em, async (trx) => {
|
|
452
501
|
const entity = await requireTimelineParentEntity(trx, after.interaction.entityId, { tenantId: after.interaction.tenantId, organizationId: after.interaction.organizationId });
|
|
453
502
|
let interaction2 = await findOneWithDecryption(trx, CustomerInteraction, { id: after.interaction.id });
|
|
503
|
+
await ensureInteractionRestoreAccess(ctx, trx, interaction2, after.interaction);
|
|
454
504
|
if (!interaction2) {
|
|
455
505
|
interaction2 = buildInteractionGraph(trx, {
|
|
456
506
|
id: after.interaction.id,
|
|
@@ -555,7 +605,7 @@ const createInteractionCommand = {
|
|
|
555
605
|
async function prepareUpdateInteractionSnapshot(rawInput, ctx) {
|
|
556
606
|
const { parsed } = parseWithCustomFields(interactionUpdateSchema, rawInput);
|
|
557
607
|
const em = ctx.transactionalEm ?? ctx.container.resolve("em").fork();
|
|
558
|
-
const snapshot = await loadInteractionSnapshot(em, parsed.id);
|
|
608
|
+
const snapshot = await loadInteractionSnapshot(em, parsed.id, ctx);
|
|
559
609
|
return snapshot ? { before: snapshot } : {};
|
|
560
610
|
}
|
|
561
611
|
const updateInteractionCommand = {
|
|
@@ -573,12 +623,14 @@ const updateInteractionCommand = {
|
|
|
573
623
|
}
|
|
574
624
|
ensureTenantScope(ctx, interaction2.tenantId);
|
|
575
625
|
ensureOrganizationScope(ctx, interaction2.organizationId);
|
|
626
|
+
await ensureInteractionMutationAccess(ctx, trx, interaction2);
|
|
576
627
|
await enforceCommandOptimisticLockWithGuards(ctx.container, {
|
|
577
628
|
resourceKind: "customers.interaction",
|
|
578
629
|
resourceId: interaction2.id,
|
|
579
630
|
current: interaction2.updatedAt,
|
|
580
631
|
request: ctx.request ?? null
|
|
581
632
|
});
|
|
633
|
+
await ensureInteractionPrivacyChangeAccess(ctx, interaction2, parsed);
|
|
582
634
|
const nextType = parsed.interactionType ?? interaction2.interactionType;
|
|
583
635
|
const changingType = nextType !== interaction2.interactionType;
|
|
584
636
|
const enforceSelectableType = requestsSelectableTypeEnforcement(rawInput);
|
|
@@ -637,21 +689,6 @@ const updateInteractionCommand = {
|
|
|
637
689
|
ignoreHiddenClears: enforceSelectableType
|
|
638
690
|
});
|
|
639
691
|
const sameTypeCustom = changingType || !customKeys || !enforceSelectableType ? custom : Object.fromEntries(Object.entries(custom).filter(([key]) => !customKeys.defined.has(key.toLowerCase()) || customKeys.allowed.has(key.toLowerCase())));
|
|
640
|
-
if (parsed.visibility !== void 0 && interaction2.interactionType === "email" && (parsed.visibility ?? null) !== (interaction2.visibility ?? null)) {
|
|
641
|
-
const actorUserId = ctx.auth?.sub ?? null;
|
|
642
|
-
if (!canChangeEmailVisibility({
|
|
643
|
-
interactionType: interaction2.interactionType,
|
|
644
|
-
currentVisibility: interaction2.visibility,
|
|
645
|
-
nextVisibility: parsed.visibility,
|
|
646
|
-
authorUserId: interaction2.authorUserId,
|
|
647
|
-
actorUserId,
|
|
648
|
-
// v1 strict owner-only: only the author may flip visibility; no admin
|
|
649
|
-
// bypass (canChangeEmailVisibility ignores caller features in v1).
|
|
650
|
-
userFeatures: void 0
|
|
651
|
-
})) {
|
|
652
|
-
throw notFound("Email not found");
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
692
|
const previousEntityId = typeof interaction2.entity === "string" ? interaction2.entity : interaction2.entity.id;
|
|
656
693
|
const nextEntity = parsed.entityId !== void 0 && parsed.entityId !== previousEntityId ? await requireTimelineParentEntity(trx, parsed.entityId, {
|
|
657
694
|
tenantId: interaction2.tenantId,
|
|
@@ -773,6 +810,7 @@ const updateInteractionCommand = {
|
|
|
773
810
|
const em = ctx.container.resolve("em").fork();
|
|
774
811
|
const { interaction, projections } = await runInTransaction(em, async (trx) => {
|
|
775
812
|
let interaction2 = await findOneWithDecryption(trx, CustomerInteraction, { id: before.interaction.id });
|
|
813
|
+
await ensureInteractionRestoreAccess(ctx, trx, interaction2, before.interaction);
|
|
776
814
|
const entity = await requireTimelineParentEntity(trx, before.interaction.entityId, { tenantId: before.interaction.tenantId, organizationId: before.interaction.organizationId });
|
|
777
815
|
if (!interaction2) {
|
|
778
816
|
interaction2 = trx.create(CustomerInteraction, {
|
|
@@ -890,7 +928,7 @@ const completeInteractionCommand = {
|
|
|
890
928
|
async prepare(rawInput, ctx) {
|
|
891
929
|
const parsed = interactionCompleteSchema.parse(rawInput);
|
|
892
930
|
const em = ctx.container.resolve("em").fork();
|
|
893
|
-
const snapshot = await loadInteractionSnapshot(em, parsed.id);
|
|
931
|
+
const snapshot = await loadInteractionSnapshot(em, parsed.id, ctx);
|
|
894
932
|
return snapshot ? { before: snapshot } : {};
|
|
895
933
|
},
|
|
896
934
|
async execute(rawInput, ctx) {
|
|
@@ -904,6 +942,7 @@ const completeInteractionCommand = {
|
|
|
904
942
|
}
|
|
905
943
|
ensureTenantScope(ctx, interaction2.tenantId);
|
|
906
944
|
ensureOrganizationScope(ctx, interaction2.organizationId);
|
|
945
|
+
await ensureInteractionMutationAccess(ctx, trx, interaction2);
|
|
907
946
|
await enforceCommandOptimisticLockWithGuards(ctx.container, {
|
|
908
947
|
resourceKind: "customers.interaction",
|
|
909
948
|
resourceId: interaction2.id,
|
|
@@ -979,6 +1018,7 @@ const completeInteractionCommand = {
|
|
|
979
1018
|
const result = await runInTransaction(em, async (trx) => {
|
|
980
1019
|
const interaction = await findOneWithDecryption(trx, CustomerInteraction, { id: before.interaction.id });
|
|
981
1020
|
if (!interaction) return null;
|
|
1021
|
+
await ensureInteractionMutationAccess(ctx, trx, interaction);
|
|
982
1022
|
interaction.status = before.interaction.status;
|
|
983
1023
|
interaction.occurredAt = before.interaction.occurredAt;
|
|
984
1024
|
await trx.flush();
|
|
@@ -1019,7 +1059,7 @@ const cancelInteractionCommand = {
|
|
|
1019
1059
|
async prepare(rawInput, ctx) {
|
|
1020
1060
|
const parsed = interactionCancelSchema.parse(rawInput);
|
|
1021
1061
|
const em = ctx.container.resolve("em").fork();
|
|
1022
|
-
const snapshot = await loadInteractionSnapshot(em, parsed.id);
|
|
1062
|
+
const snapshot = await loadInteractionSnapshot(em, parsed.id, ctx);
|
|
1023
1063
|
return snapshot ? { before: snapshot } : {};
|
|
1024
1064
|
},
|
|
1025
1065
|
async execute(rawInput, ctx) {
|
|
@@ -1033,6 +1073,7 @@ const cancelInteractionCommand = {
|
|
|
1033
1073
|
}
|
|
1034
1074
|
ensureTenantScope(ctx, interaction2.tenantId);
|
|
1035
1075
|
ensureOrganizationScope(ctx, interaction2.organizationId);
|
|
1076
|
+
await ensureInteractionMutationAccess(ctx, trx, interaction2);
|
|
1036
1077
|
await enforceCommandOptimisticLockWithGuards(ctx.container, {
|
|
1037
1078
|
resourceKind: "customers.interaction",
|
|
1038
1079
|
resourceId: interaction2.id,
|
|
@@ -1106,6 +1147,7 @@ const cancelInteractionCommand = {
|
|
|
1106
1147
|
const result = await runInTransaction(em, async (trx) => {
|
|
1107
1148
|
const interaction = await findOneWithDecryption(trx, CustomerInteraction, { id: before.interaction.id });
|
|
1108
1149
|
if (!interaction) return null;
|
|
1150
|
+
await ensureInteractionMutationAccess(ctx, trx, interaction);
|
|
1109
1151
|
interaction.status = before.interaction.status;
|
|
1110
1152
|
await trx.flush();
|
|
1111
1153
|
const projection = await recomputeNextInteraction(trx, before.interaction.entityId);
|
|
@@ -1145,7 +1187,7 @@ const deleteInteractionCommand = {
|
|
|
1145
1187
|
async prepare(input, ctx) {
|
|
1146
1188
|
const id = requireId(input, "Interaction id required");
|
|
1147
1189
|
const em = ctx.container.resolve("em").fork();
|
|
1148
|
-
const snapshot = await loadInteractionSnapshot(em, id);
|
|
1190
|
+
const snapshot = await loadInteractionSnapshot(em, id, ctx);
|
|
1149
1191
|
return snapshot ? { before: snapshot } : {};
|
|
1150
1192
|
},
|
|
1151
1193
|
async execute(input, ctx) {
|
|
@@ -1159,6 +1201,7 @@ const deleteInteractionCommand = {
|
|
|
1159
1201
|
}
|
|
1160
1202
|
ensureTenantScope(ctx, interaction2.tenantId);
|
|
1161
1203
|
ensureOrganizationScope(ctx, interaction2.organizationId);
|
|
1204
|
+
await ensureInteractionMutationAccess(ctx, trx, interaction2);
|
|
1162
1205
|
await enforceCommandOptimisticLockWithGuards(ctx.container, {
|
|
1163
1206
|
resourceKind: "customers.interaction",
|
|
1164
1207
|
resourceId: interaction2.id,
|
|
@@ -1220,6 +1263,7 @@ const deleteInteractionCommand = {
|
|
|
1220
1263
|
const { interaction, nextInteractionId } = await runInTransaction(em, async (trx) => {
|
|
1221
1264
|
const entity = await requireTimelineParentEntity(trx, before.interaction.entityId, { tenantId: before.interaction.tenantId, organizationId: before.interaction.organizationId });
|
|
1222
1265
|
let interaction2 = await findOneWithDecryption(trx, CustomerInteraction, { id: before.interaction.id });
|
|
1266
|
+
await ensureInteractionRestoreAccess(ctx, trx, interaction2, before.interaction);
|
|
1223
1267
|
if (!interaction2) {
|
|
1224
1268
|
interaction2 = trx.create(CustomerInteraction, {
|
|
1225
1269
|
id: before.interaction.id,
|