@open-mercato/core 0.5.1-develop.3036.f02c281f23 → 0.5.1-develop.3045.b4b3320cc2

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.
Files changed (163) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +13 -1
  3. package/dist/helpers/integration/api.js +29 -16
  4. package/dist/helpers/integration/api.js.map +2 -2
  5. package/dist/helpers/integration/auth.js +11 -6
  6. package/dist/helpers/integration/auth.js.map +3 -3
  7. package/dist/modules/auth/commands/roles.js +9 -12
  8. package/dist/modules/auth/commands/roles.js.map +2 -2
  9. package/dist/modules/catalog/ai-agents-context.js +147 -0
  10. package/dist/modules/catalog/ai-agents-context.js.map +7 -0
  11. package/dist/modules/catalog/ai-agents.js +383 -0
  12. package/dist/modules/catalog/ai-agents.js.map +7 -0
  13. package/dist/modules/catalog/ai-tools/_shared.js +318 -0
  14. package/dist/modules/catalog/ai-tools/_shared.js.map +7 -0
  15. package/dist/modules/catalog/ai-tools/authoring-pack.js +391 -0
  16. package/dist/modules/catalog/ai-tools/authoring-pack.js.map +7 -0
  17. package/dist/modules/catalog/ai-tools/categories-pack.js +167 -0
  18. package/dist/modules/catalog/ai-tools/categories-pack.js.map +7 -0
  19. package/dist/modules/catalog/ai-tools/configuration-pack.js +120 -0
  20. package/dist/modules/catalog/ai-tools/configuration-pack.js.map +7 -0
  21. package/dist/modules/catalog/ai-tools/media-tags-pack.js +107 -0
  22. package/dist/modules/catalog/ai-tools/media-tags-pack.js.map +7 -0
  23. package/dist/modules/catalog/ai-tools/merchandising-pack.js +429 -0
  24. package/dist/modules/catalog/ai-tools/merchandising-pack.js.map +7 -0
  25. package/dist/modules/catalog/ai-tools/mutation-pack.js +576 -0
  26. package/dist/modules/catalog/ai-tools/mutation-pack.js.map +7 -0
  27. package/dist/modules/catalog/ai-tools/prices-offers-pack.js +208 -0
  28. package/dist/modules/catalog/ai-tools/prices-offers-pack.js.map +7 -0
  29. package/dist/modules/catalog/ai-tools/products-pack.js +298 -0
  30. package/dist/modules/catalog/ai-tools/products-pack.js.map +7 -0
  31. package/dist/modules/catalog/ai-tools/stats-pack.js +57 -0
  32. package/dist/modules/catalog/ai-tools/stats-pack.js.map +7 -0
  33. package/dist/modules/catalog/ai-tools/types.js +10 -0
  34. package/dist/modules/catalog/ai-tools/types.js.map +7 -0
  35. package/dist/modules/catalog/ai-tools/variants-pack.js +75 -0
  36. package/dist/modules/catalog/ai-tools/variants-pack.js.map +7 -0
  37. package/dist/modules/catalog/ai-tools.js +28 -0
  38. package/dist/modules/catalog/ai-tools.js.map +7 -0
  39. package/dist/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.js +466 -0
  40. package/dist/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.js.map +7 -0
  41. package/dist/modules/catalog/backend/catalog/products/page.js +7 -1
  42. package/dist/modules/catalog/backend/catalog/products/page.js.map +2 -2
  43. package/dist/modules/catalog/components/CatalogStatsCard.js +91 -0
  44. package/dist/modules/catalog/components/CatalogStatsCard.js.map +7 -0
  45. package/dist/modules/catalog/components/products/ProductsDataTable.js +23 -3
  46. package/dist/modules/catalog/components/products/ProductsDataTable.js.map +2 -2
  47. package/dist/modules/catalog/events.js +7 -4
  48. package/dist/modules/catalog/events.js.map +2 -2
  49. package/dist/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.client.js +59 -0
  50. package/dist/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.client.js.map +7 -0
  51. package/dist/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.js +17 -0
  52. package/dist/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.js.map +7 -0
  53. package/dist/modules/catalog/widgets/injection/product-seo/widget.client.js +1 -1
  54. package/dist/modules/catalog/widgets/injection/product-seo/widget.client.js.map +2 -2
  55. package/dist/modules/catalog/widgets/injection-table.js +13 -1
  56. package/dist/modules/catalog/widgets/injection-table.js.map +2 -2
  57. package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.client.js +94 -0
  58. package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.client.js.map +7 -0
  59. package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.js +17 -0
  60. package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.js.map +7 -0
  61. package/dist/modules/customer_accounts/widgets/injection-table.js +9 -0
  62. package/dist/modules/customer_accounts/widgets/injection-table.js.map +2 -2
  63. package/dist/modules/customers/ai-agents-context.js +96 -0
  64. package/dist/modules/customers/ai-agents-context.js.map +7 -0
  65. package/dist/modules/customers/ai-agents.js +244 -0
  66. package/dist/modules/customers/ai-agents.js.map +7 -0
  67. package/dist/modules/customers/ai-tools/activities-tasks-pack.js +1015 -0
  68. package/dist/modules/customers/ai-tools/activities-tasks-pack.js.map +7 -0
  69. package/dist/modules/customers/ai-tools/addresses-tags-pack.js +134 -0
  70. package/dist/modules/customers/ai-tools/addresses-tags-pack.js.map +7 -0
  71. package/dist/modules/customers/ai-tools/companies-pack.js +249 -0
  72. package/dist/modules/customers/ai-tools/companies-pack.js.map +7 -0
  73. package/dist/modules/customers/ai-tools/deals-pack.js +348 -0
  74. package/dist/modules/customers/ai-tools/deals-pack.js.map +7 -0
  75. package/dist/modules/customers/ai-tools/people-pack.js +261 -0
  76. package/dist/modules/customers/ai-tools/people-pack.js.map +7 -0
  77. package/dist/modules/customers/ai-tools/settings-pack.js +102 -0
  78. package/dist/modules/customers/ai-tools/settings-pack.js.map +7 -0
  79. package/dist/modules/customers/ai-tools/types.js +10 -0
  80. package/dist/modules/customers/ai-tools/types.js.map +7 -0
  81. package/dist/modules/customers/ai-tools.js +20 -0
  82. package/dist/modules/customers/ai-tools.js.map +7 -0
  83. package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.js +469 -0
  84. package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.js.map +7 -0
  85. package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.js +17 -0
  86. package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.js.map +7 -0
  87. package/dist/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.client.js +117 -0
  88. package/dist/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.client.js.map +7 -0
  89. package/dist/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.js +17 -0
  90. package/dist/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.js.map +7 -0
  91. package/dist/modules/customers/widgets/injection-table.js +26 -0
  92. package/dist/modules/customers/widgets/injection-table.js.map +7 -0
  93. package/dist/modules/inbox_ops/ai-tools.js +4 -0
  94. package/dist/modules/inbox_ops/ai-tools.js.map +2 -2
  95. package/dist/modules/inbox_ops/lib/llmProvider.js +52 -7
  96. package/dist/modules/inbox_ops/lib/llmProvider.js.map +2 -2
  97. package/dist/modules/notifications/setup.js +13 -0
  98. package/dist/modules/notifications/setup.js.map +7 -0
  99. package/jest.config.cjs +1 -0
  100. package/jest.setup.ts +18 -0
  101. package/package.json +5 -3
  102. package/src/helpers/integration/api.ts +38 -16
  103. package/src/helpers/integration/auth.ts +13 -6
  104. package/src/modules/auth/commands/roles.ts +10 -12
  105. package/src/modules/catalog/AGENTS.md +11 -0
  106. package/src/modules/catalog/ai-agents-context.ts +239 -0
  107. package/src/modules/catalog/ai-agents.ts +525 -0
  108. package/src/modules/catalog/ai-tools/_shared.ts +487 -0
  109. package/src/modules/catalog/ai-tools/authoring-pack.ts +600 -0
  110. package/src/modules/catalog/ai-tools/categories-pack.ts +192 -0
  111. package/src/modules/catalog/ai-tools/configuration-pack.ts +218 -0
  112. package/src/modules/catalog/ai-tools/media-tags-pack.ts +127 -0
  113. package/src/modules/catalog/ai-tools/merchandising-pack.ts +608 -0
  114. package/src/modules/catalog/ai-tools/mutation-pack.ts +761 -0
  115. package/src/modules/catalog/ai-tools/prices-offers-pack.ts +376 -0
  116. package/src/modules/catalog/ai-tools/products-pack.ts +387 -0
  117. package/src/modules/catalog/ai-tools/stats-pack.ts +84 -0
  118. package/src/modules/catalog/ai-tools/types.ts +81 -0
  119. package/src/modules/catalog/ai-tools/variants-pack.ts +147 -0
  120. package/src/modules/catalog/ai-tools.ts +78 -0
  121. package/src/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.tsx +597 -0
  122. package/src/modules/catalog/backend/catalog/products/page.tsx +23 -2
  123. package/src/modules/catalog/components/CatalogStatsCard.tsx +118 -0
  124. package/src/modules/catalog/components/products/ProductsDataTable.tsx +54 -6
  125. package/src/modules/catalog/events.ts +7 -4
  126. package/src/modules/catalog/i18n/de.json +17 -0
  127. package/src/modules/catalog/i18n/en.json +17 -0
  128. package/src/modules/catalog/i18n/es.json +17 -0
  129. package/src/modules/catalog/i18n/pl.json +17 -0
  130. package/src/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.client.tsx +109 -0
  131. package/src/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.ts +29 -0
  132. package/src/modules/catalog/widgets/injection/product-seo/widget.client.tsx +1 -1
  133. package/src/modules/catalog/widgets/injection-table.ts +12 -0
  134. package/src/modules/customer_accounts/i18n/de.json +5 -0
  135. package/src/modules/customer_accounts/i18n/en.json +5 -0
  136. package/src/modules/customer_accounts/i18n/es.json +5 -0
  137. package/src/modules/customer_accounts/i18n/pl.json +5 -0
  138. package/src/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.client.tsx +136 -0
  139. package/src/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.ts +43 -0
  140. package/src/modules/customer_accounts/widgets/injection-table.ts +9 -0
  141. package/src/modules/customers/AGENTS.md +13 -0
  142. package/src/modules/customers/ai-agents-context.ts +150 -0
  143. package/src/modules/customers/ai-agents.ts +355 -0
  144. package/src/modules/customers/ai-tools/activities-tasks-pack.ts +1248 -0
  145. package/src/modules/customers/ai-tools/addresses-tags-pack.ts +145 -0
  146. package/src/modules/customers/ai-tools/companies-pack.ts +362 -0
  147. package/src/modules/customers/ai-tools/deals-pack.ts +505 -0
  148. package/src/modules/customers/ai-tools/people-pack.ts +369 -0
  149. package/src/modules/customers/ai-tools/settings-pack.ts +121 -0
  150. package/src/modules/customers/ai-tools/types.ts +76 -0
  151. package/src/modules/customers/ai-tools.ts +34 -0
  152. package/src/modules/customers/i18n/de.json +25 -0
  153. package/src/modules/customers/i18n/en.json +25 -0
  154. package/src/modules/customers/i18n/es.json +25 -0
  155. package/src/modules/customers/i18n/pl.json +25 -0
  156. package/src/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.tsx +580 -0
  157. package/src/modules/customers/widgets/injection/ai-assistant-trigger/widget.ts +36 -0
  158. package/src/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.client.tsx +191 -0
  159. package/src/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.ts +37 -0
  160. package/src/modules/customers/widgets/injection-table.ts +41 -0
  161. package/src/modules/inbox_ops/ai-tools.ts +4 -0
  162. package/src/modules/inbox_ops/lib/llmProvider.ts +83 -7
  163. package/src/modules/notifications/setup.ts +11 -0
@@ -0,0 +1,1015 @@
1
+ import { z } from "zod";
2
+ import {
3
+ createAiApiOperationRunner
4
+ } from "@open-mercato/ai-assistant/modules/ai_assistant/lib/ai-api-operation-runner";
5
+ import { findOneWithDecryption, findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
6
+ import {
7
+ CustomerActivity,
8
+ CustomerComment,
9
+ CustomerDeal,
10
+ CustomerDealCompanyLink,
11
+ CustomerDealPersonLink,
12
+ CustomerInteraction,
13
+ CustomerTodoLink
14
+ } from "../data/entities.js";
15
+ import {
16
+ assertTenantScope
17
+ } from "./types.js";
18
+ function resolveEm(ctx) {
19
+ return ctx.container.resolve("em");
20
+ }
21
+ function buildScope(ctx, tenantId) {
22
+ return { tenantId, organizationId: ctx.organizationId };
23
+ }
24
+ function recordVersionFromUpdatedAt(updatedAt) {
25
+ if (!updatedAt) return null;
26
+ const value = updatedAt instanceof Date ? updatedAt : new Date(updatedAt);
27
+ if (Number.isNaN(value.getTime())) return null;
28
+ return value.toISOString();
29
+ }
30
+ const blankToUndefined = (value) => {
31
+ if (typeof value !== "string") return value;
32
+ const trimmed = value.trim();
33
+ return trimmed.length === 0 ? void 0 : trimmed;
34
+ };
35
+ async function loadDealForScope(em, ctx, tenantId, dealId) {
36
+ const where = { id: dealId, tenantId, deletedAt: null };
37
+ if (ctx.organizationId) where.organizationId = ctx.organizationId;
38
+ const deal = await findOneWithDecryption(
39
+ em,
40
+ CustomerDeal,
41
+ where,
42
+ void 0,
43
+ buildScope(ctx, tenantId)
44
+ );
45
+ if (!deal || deal.tenantId !== tenantId) return null;
46
+ if (ctx.organizationId && deal.organizationId !== ctx.organizationId) return null;
47
+ return deal;
48
+ }
49
+ async function resolveDealCommentEntityId(em, ctx, tenantId, dealId) {
50
+ const personLink = await em.findOne(
51
+ CustomerDealPersonLink,
52
+ { deal: dealId, tenantId },
53
+ { populate: ["personEntity"] }
54
+ );
55
+ if (personLink) {
56
+ const linked = personLink.personEntity;
57
+ if (linked && typeof linked === "object" && typeof linked.id === "string") return linked.id;
58
+ const raw = personLink.personEntity;
59
+ if (typeof raw === "string") return raw;
60
+ }
61
+ const companyLink = await em.findOne(
62
+ CustomerDealCompanyLink,
63
+ { deal: dealId, tenantId },
64
+ { populate: ["companyEntity"] }
65
+ );
66
+ if (companyLink) {
67
+ const linked = companyLink.companyEntity;
68
+ if (linked && typeof linked === "object" && typeof linked.id === "string") return linked.id;
69
+ const raw = companyLink.companyEntity;
70
+ if (typeof raw === "string") return raw;
71
+ }
72
+ return null;
73
+ }
74
+ async function loadCommentForScope(em, ctx, tenantId, commentId) {
75
+ const where = { id: commentId, tenantId };
76
+ if (ctx.organizationId) where.organizationId = ctx.organizationId;
77
+ const row = await findOneWithDecryption(
78
+ em,
79
+ CustomerComment,
80
+ where,
81
+ void 0,
82
+ buildScope(ctx, tenantId)
83
+ );
84
+ if (!row || row.tenantId !== tenantId) return null;
85
+ if (ctx.organizationId && row.organizationId !== ctx.organizationId) return null;
86
+ return row;
87
+ }
88
+ async function loadActivityForScope(em, ctx, tenantId, activityId) {
89
+ const where = { id: activityId, tenantId };
90
+ if (ctx.organizationId) where.organizationId = ctx.organizationId;
91
+ const row = await findOneWithDecryption(
92
+ em,
93
+ CustomerActivity,
94
+ where,
95
+ void 0,
96
+ buildScope(ctx, tenantId)
97
+ );
98
+ if (!row || row.tenantId !== tenantId) return null;
99
+ if (ctx.organizationId && row.organizationId !== ctx.organizationId) return null;
100
+ return row;
101
+ }
102
+ function commentEntityIdOf(row) {
103
+ const ent = row.entity;
104
+ if (!ent) return null;
105
+ if (typeof ent === "string") return ent;
106
+ if (typeof ent === "object" && typeof ent.id === "string") return ent.id;
107
+ return null;
108
+ }
109
+ function activityEntityIdOf(row) {
110
+ const ent = row.entity;
111
+ if (!ent) return null;
112
+ if (typeof ent === "string") return ent;
113
+ if (typeof ent === "object" && typeof ent.id === "string") return ent.id;
114
+ return null;
115
+ }
116
+ const listActivitiesInput = z.object({
117
+ personId: z.string().uuid().optional().describe("Restrict to activities attached to this person entity id."),
118
+ companyId: z.string().uuid().optional().describe("Restrict to activities attached to this company entity id."),
119
+ dealId: z.string().uuid().optional().describe("Restrict to activities attached to this deal id."),
120
+ activityType: z.string().optional().describe('Filter by activity type (e.g. "call", "email").'),
121
+ limit: z.number().int().min(1).max(100).optional().describe("Max rows (default 50, max 100)."),
122
+ offset: z.number().int().min(0).optional().describe("Rows to skip (default 0).")
123
+ }).passthrough();
124
+ const listActivitiesTool = {
125
+ name: "customers.list_activities",
126
+ displayName: "List activities",
127
+ description: "List logged customer activities (calls, emails, meetings, notes, etc.) scoped to tenant + organization. Supply `personId` / `companyId` / `dealId` to narrow; otherwise returns the most recent activities across the tenant.",
128
+ inputSchema: listActivitiesInput,
129
+ requiredFeatures: ["customers.activities.view"],
130
+ tags: ["read", "customers"],
131
+ handler: async (rawInput, ctx) => {
132
+ const { tenantId } = assertTenantScope(ctx);
133
+ const input = listActivitiesInput.parse(rawInput);
134
+ const em = resolveEm(ctx);
135
+ const limit = input.limit ?? 50;
136
+ const offset = input.offset ?? 0;
137
+ const where = { tenantId };
138
+ if (ctx.organizationId) where.organizationId = ctx.organizationId;
139
+ const entityId = input.personId ?? input.companyId ?? null;
140
+ if (entityId) where.entity = entityId;
141
+ if (input.dealId) where.deal = input.dealId;
142
+ if (input.activityType) where.activityType = input.activityType;
143
+ const [rows, total] = await Promise.all([
144
+ findWithDecryption(
145
+ em,
146
+ CustomerActivity,
147
+ where,
148
+ { limit, offset, orderBy: { occurredAt: "desc", createdAt: "desc" } },
149
+ buildScope(ctx, tenantId)
150
+ ),
151
+ em.count(CustomerActivity, where)
152
+ ]);
153
+ const filtered = rows.filter((row) => row.tenantId === tenantId);
154
+ return {
155
+ items: filtered.map((row) => ({
156
+ id: row.id,
157
+ activityType: row.activityType,
158
+ subject: row.subject ?? null,
159
+ body: row.body ?? null,
160
+ occurredAt: row.occurredAt ? new Date(row.occurredAt).toISOString() : null,
161
+ authorUserId: row.authorUserId ?? null,
162
+ entityId: activityEntityIdOf(row),
163
+ dealId: row.deal && typeof row.deal === "object" ? row.deal.id : row.deal ?? null,
164
+ organizationId: row.organizationId ?? null,
165
+ tenantId: row.tenantId ?? null,
166
+ createdAt: row.createdAt ? new Date(row.createdAt).toISOString() : null
167
+ })),
168
+ total,
169
+ limit,
170
+ offset
171
+ };
172
+ }
173
+ };
174
+ const listTasksInput = z.object({
175
+ personId: z.string().uuid().optional().describe("Restrict to tasks linked to this person entity id."),
176
+ companyId: z.string().uuid().optional().describe("Restrict to tasks linked to this company entity id."),
177
+ dealId: z.string().uuid().optional().describe("Restrict to tasks connected to this deal id."),
178
+ status: z.enum(["open", "done", "cancelled"]).optional().describe("Filter canonical interaction tasks by status. Ignored when listing legacy todo links."),
179
+ limit: z.number().int().min(1).max(100).optional().describe("Max rows (default 50, max 100)."),
180
+ offset: z.number().int().min(0).optional().describe("Rows to skip (default 0).")
181
+ }).passthrough();
182
+ const listTasksTool = {
183
+ name: "customers.list_tasks",
184
+ displayName: "List tasks",
185
+ description: 'List customer tasks scoped to tenant + organization. Returns canonical interaction tasks (interactionType="task") merged with legacy todo links for compatibility.',
186
+ inputSchema: listTasksInput,
187
+ requiredFeatures: ["customers.activities.view"],
188
+ tags: ["read", "customers"],
189
+ handler: async (rawInput, ctx) => {
190
+ const { tenantId } = assertTenantScope(ctx);
191
+ const input = listTasksInput.parse(rawInput);
192
+ const em = resolveEm(ctx);
193
+ const limit = input.limit ?? 50;
194
+ const offset = input.offset ?? 0;
195
+ const entityId = input.personId ?? input.companyId ?? null;
196
+ const interactionWhere = {
197
+ tenantId,
198
+ interactionType: "task",
199
+ deletedAt: null
200
+ };
201
+ if (ctx.organizationId) interactionWhere.organizationId = ctx.organizationId;
202
+ if (entityId) interactionWhere.entity = entityId;
203
+ if (input.dealId) interactionWhere.dealId = input.dealId;
204
+ if (input.status) interactionWhere.status = input.status === "open" ? "planned" : input.status === "done" ? "completed" : "cancelled";
205
+ const interactionRows = await findWithDecryption(
206
+ em,
207
+ CustomerInteraction,
208
+ interactionWhere,
209
+ { limit, offset, orderBy: { scheduledAt: "desc", createdAt: "desc" } },
210
+ buildScope(ctx, tenantId)
211
+ );
212
+ const legacyWhere = { tenantId };
213
+ if (ctx.organizationId) legacyWhere.organizationId = ctx.organizationId;
214
+ if (entityId) legacyWhere.entity = entityId;
215
+ const legacyRows = input.status || input.dealId ? [] : await findWithDecryption(
216
+ em,
217
+ CustomerTodoLink,
218
+ legacyWhere,
219
+ { limit, offset, orderBy: { createdAt: "desc" } },
220
+ buildScope(ctx, tenantId)
221
+ );
222
+ const filteredInteractions = interactionRows.filter((row) => row.tenantId === tenantId);
223
+ const filteredLegacy = legacyRows.filter((row) => row.tenantId === tenantId);
224
+ const items = [
225
+ ...filteredInteractions.map((row) => ({
226
+ kind: "interaction",
227
+ id: row.id,
228
+ title: row.title ?? null,
229
+ body: row.body ?? null,
230
+ status: row.status,
231
+ scheduledAt: row.scheduledAt ? new Date(row.scheduledAt).toISOString() : null,
232
+ occurredAt: row.occurredAt ? new Date(row.occurredAt).toISOString() : null,
233
+ dealId: row.dealId ?? null,
234
+ ownerUserId: row.ownerUserId ?? null,
235
+ priority: row.priority ?? null,
236
+ organizationId: row.organizationId ?? null,
237
+ tenantId: row.tenantId ?? null,
238
+ createdAt: row.createdAt ? new Date(row.createdAt).toISOString() : null
239
+ })),
240
+ ...filteredLegacy.map((row) => ({
241
+ kind: "todo_link",
242
+ id: row.id,
243
+ todoId: row.todoId,
244
+ todoSource: row.todoSource,
245
+ entityId: row.entity && typeof row.entity === "object" ? row.entity.id : row.entity ?? null,
246
+ organizationId: row.organizationId ?? null,
247
+ tenantId: row.tenantId ?? null,
248
+ createdAt: row.createdAt ? new Date(row.createdAt).toISOString() : null
249
+ }))
250
+ ];
251
+ return {
252
+ items,
253
+ total: items.length,
254
+ limit,
255
+ offset
256
+ };
257
+ }
258
+ };
259
+ const listDealCommentsInput = z.object({
260
+ dealId: z.string().uuid().describe("Deal id whose comments should be listed."),
261
+ limit: z.number().int().min(1).max(100).optional().describe("Max rows (default 50, max 100)."),
262
+ offset: z.number().int().min(0).optional().describe("Rows to skip (default 0).")
263
+ }).passthrough();
264
+ const listDealCommentsTool = {
265
+ name: "customers.list_deal_comments",
266
+ displayName: "List deal comments",
267
+ description: "List comments left on a specific deal, ordered by most recent first. Read-only; tenant + organization scoped.",
268
+ inputSchema: listDealCommentsInput,
269
+ requiredFeatures: ["customers.activities.view"],
270
+ tags: ["read", "customers"],
271
+ handler: async (rawInput, ctx) => {
272
+ const { tenantId } = assertTenantScope(ctx);
273
+ const input = listDealCommentsInput.parse(rawInput);
274
+ const em = resolveEm(ctx);
275
+ const limit = input.limit ?? 50;
276
+ const offset = input.offset ?? 0;
277
+ const where = { tenantId, deal: input.dealId };
278
+ if (ctx.organizationId) where.organizationId = ctx.organizationId;
279
+ const [rows, total] = await Promise.all([
280
+ findWithDecryption(
281
+ em,
282
+ CustomerComment,
283
+ where,
284
+ { limit, offset, orderBy: { createdAt: "desc" } },
285
+ buildScope(ctx, tenantId)
286
+ ),
287
+ em.count(CustomerComment, where)
288
+ ]);
289
+ const filtered = rows.filter((row) => row.tenantId === tenantId);
290
+ return {
291
+ items: filtered.map((row) => ({
292
+ id: row.id,
293
+ body: row.body ?? null,
294
+ entityId: commentEntityIdOf(row),
295
+ dealId: row.deal && typeof row.deal === "object" ? row.deal.id : row.deal ?? null,
296
+ authorUserId: row.authorUserId ?? null,
297
+ organizationId: row.organizationId ?? null,
298
+ tenantId: row.tenantId ?? null,
299
+ createdAt: row.createdAt ? new Date(row.createdAt).toISOString() : null,
300
+ updatedAt: row.updatedAt ? new Date(row.updatedAt).toISOString() : null
301
+ })),
302
+ total,
303
+ limit,
304
+ offset
305
+ };
306
+ }
307
+ };
308
+ const manageDealCommentInput = z.object({
309
+ operation: z.enum(["create", "update", "delete"]).describe("Which write to perform: create a new comment, update an existing one, or delete it."),
310
+ dealId: z.preprocess(blankToUndefined, z.string().uuid().optional()).describe("Required for `create` \u2014 the deal the comment is attached to."),
311
+ commentId: z.preprocess(blankToUndefined, z.string().uuid().optional()).describe("Required for `update` and `delete` \u2014 id of the existing comment row."),
312
+ body: z.preprocess(blankToUndefined, z.string().min(1).max(8e3).optional()).describe("Comment text. Required for `create`; optional on `update`.")
313
+ }).superRefine((value, ctx) => {
314
+ if (value.operation === "create") {
315
+ if (!value.dealId) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "dealId is required for create.", path: ["dealId"] });
316
+ if (!value.body) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "body is required for create.", path: ["body"] });
317
+ }
318
+ if (value.operation === "update") {
319
+ if (!value.commentId) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "commentId is required for update.", path: ["commentId"] });
320
+ if (!value.body) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "body is required for update.", path: ["body"] });
321
+ }
322
+ if (value.operation === "delete") {
323
+ if (!value.commentId) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "commentId is required for delete.", path: ["commentId"] });
324
+ }
325
+ });
326
+ const manageDealCommentTool = {
327
+ name: "customers.manage_deal_comment",
328
+ displayName: "Manage deal comment",
329
+ description: "Create, update, or delete a comment on a deal. Use `operation` to pick the action. Under `destructive-confirm-required` policy, only the `delete` branch routes through the approval card; `create` and `update` execute directly.",
330
+ inputSchema: manageDealCommentInput,
331
+ requiredFeatures: ["customers.activities.manage"],
332
+ tags: ["write", "customers"],
333
+ isMutation: true,
334
+ // Predicate `isDestructive`: only the `delete` branch counts as
335
+ // destructive. Under `confirm-required` policy every branch still
336
+ // gates (the framework ignores this flag); under
337
+ // `destructive-confirm-required` only deletes go through the approval
338
+ // card while creates/updates run directly.
339
+ isDestructive: (input) => {
340
+ if (!input || typeof input !== "object") return false;
341
+ return input.operation === "delete";
342
+ },
343
+ loadBeforeRecord: async (rawInput, ctx) => {
344
+ const { tenantId } = assertTenantScope(ctx);
345
+ const input = manageDealCommentInput.parse(rawInput);
346
+ const em = resolveEm(ctx);
347
+ if (input.operation === "create") {
348
+ const deal = await loadDealForScope(em, ctx, tenantId, input.dealId);
349
+ if (!deal) return null;
350
+ return {
351
+ recordId: deal.id,
352
+ entityType: "customers.deal",
353
+ recordVersion: recordVersionFromUpdatedAt(deal.updatedAt),
354
+ before: { commentId: null, body: null, dealId: deal.id }
355
+ };
356
+ }
357
+ const existing = await loadCommentForScope(em, ctx, tenantId, input.commentId);
358
+ if (!existing) return null;
359
+ return {
360
+ recordId: existing.id,
361
+ entityType: "customers.customer_comment",
362
+ recordVersion: recordVersionFromUpdatedAt(existing.updatedAt),
363
+ before: {
364
+ body: existing.body ?? null,
365
+ dealId: existing.deal && typeof existing.deal === "object" ? existing.deal.id : existing.deal ?? null,
366
+ entityId: commentEntityIdOf(existing),
367
+ authorUserId: existing.authorUserId ?? null
368
+ }
369
+ };
370
+ },
371
+ handler: async (rawInput, ctx) => {
372
+ const { tenantId } = assertTenantScope(ctx);
373
+ const input = manageDealCommentInput.parse(rawInput);
374
+ const em = resolveEm(ctx);
375
+ const runner = createAiApiOperationRunner(ctx);
376
+ if (input.operation === "create") {
377
+ const deal = await loadDealForScope(em, ctx, tenantId, input.dealId);
378
+ if (!deal) throw new Error(`Deal "${input.dealId}" is not accessible to the caller.`);
379
+ const organizationId = deal.organizationId;
380
+ if (!organizationId) throw new Error(`Deal "${deal.id}" has no organization scope.`);
381
+ const dealEntityId = await resolveDealCommentEntityId(em, ctx, tenantId, deal.id);
382
+ if (!dealEntityId) {
383
+ throw new Error(
384
+ `Deal "${deal.id}" has no linked person or company. Link a contact to the deal in the backoffice before adding a comment, or post the comment directly on the person/company record instead.`
385
+ );
386
+ }
387
+ const body2 = {
388
+ tenantId,
389
+ organizationId,
390
+ dealId: deal.id,
391
+ // Comments require an `entityId` (the person/company on the timeline).
392
+ entityId: dealEntityId,
393
+ body: input.body
394
+ };
395
+ const response2 = await runner.run({ method: "POST", path: "/customers/comments", body: body2 });
396
+ if (!response2.success) {
397
+ throw new Error(response2.error ?? "Failed to create comment");
398
+ }
399
+ const result = response2.data ?? {};
400
+ return {
401
+ operation: "create",
402
+ commentId: result.id ?? null,
403
+ dealId: deal.id,
404
+ commandName: "customers.comments.create",
405
+ before: null,
406
+ after: { body: input.body ?? null, dealId: deal.id }
407
+ };
408
+ }
409
+ if (input.operation === "update") {
410
+ const existing2 = await loadCommentForScope(em, ctx, tenantId, input.commentId);
411
+ if (!existing2) throw new Error(`Comment "${input.commentId}" is not accessible to the caller.`);
412
+ const organizationId = existing2.organizationId;
413
+ if (!organizationId) throw new Error(`Comment "${existing2.id}" has no organization scope.`);
414
+ const body2 = {
415
+ id: existing2.id,
416
+ tenantId,
417
+ organizationId,
418
+ body: input.body
419
+ };
420
+ const response2 = await runner.run({ method: "PUT", path: "/customers/comments", body: body2 });
421
+ if (!response2.success) {
422
+ throw new Error(response2.error ?? `Failed to update comment "${existing2.id}"`);
423
+ }
424
+ const after = await loadCommentForScope(em, ctx, tenantId, existing2.id);
425
+ return {
426
+ operation: "update",
427
+ commentId: existing2.id,
428
+ commandName: "customers.comments.update",
429
+ before: { body: existing2.body ?? null },
430
+ after: after ? { body: after.body ?? null } : null
431
+ };
432
+ }
433
+ const existing = await loadCommentForScope(em, ctx, tenantId, input.commentId);
434
+ if (!existing) throw new Error(`Comment "${input.commentId}" is not accessible to the caller.`);
435
+ const body = { id: existing.id };
436
+ const response = await runner.run({ method: "DELETE", path: "/customers/comments", body });
437
+ if (!response.success) {
438
+ throw new Error(response.error ?? `Failed to delete comment "${existing.id}"`);
439
+ }
440
+ return {
441
+ operation: "delete",
442
+ commentId: existing.id,
443
+ commandName: "customers.comments.delete",
444
+ before: { body: existing.body ?? null },
445
+ after: null
446
+ };
447
+ }
448
+ };
449
+ const manageDealActivityInput = z.object({
450
+ operation: z.enum(["create", "update", "delete"]).describe("Which write to perform: create a new activity, update an existing one, or delete it."),
451
+ activityId: z.preprocess(blankToUndefined, z.string().uuid().optional()).describe("Required for `update` and `delete`."),
452
+ dealId: z.preprocess(blankToUndefined, z.string().uuid().optional()).describe("Required for `create` \u2014 the deal the activity is logged against."),
453
+ activityType: z.preprocess(blankToUndefined, z.string().min(1).max(100).optional()).describe('Required for `create` \u2014 e.g. "call", "email", "meeting", "note".'),
454
+ subject: z.preprocess(blankToUndefined, z.string().max(200).optional()).describe("Optional short subject line."),
455
+ body: z.preprocess(blankToUndefined, z.string().max(8e3).optional()).describe("Optional free-text body."),
456
+ occurredAt: z.preprocess(blankToUndefined, z.string().datetime().optional()).describe('ISO-8601 timestamp when the activity occurred. Omit for "now" (server-side default applies).')
457
+ }).superRefine((value, ctx) => {
458
+ if (value.operation === "create") {
459
+ if (!value.dealId) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "dealId is required for create.", path: ["dealId"] });
460
+ if (!value.activityType) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "activityType is required for create.", path: ["activityType"] });
461
+ }
462
+ if (value.operation === "update" || value.operation === "delete") {
463
+ if (!value.activityId) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "activityId is required.", path: ["activityId"] });
464
+ }
465
+ });
466
+ const manageDealActivityTool = {
467
+ name: "customers.manage_deal_activity",
468
+ displayName: "Manage deal activity",
469
+ description: "Create, update, or delete a deal activity (call, email, meeting, note, etc.). Mutation tool \u2014 every call routes through the AI pending-action approval gate. Use `operation` to pick the action.",
470
+ inputSchema: manageDealActivityInput,
471
+ requiredFeatures: ["customers.activities.manage"],
472
+ tags: ["write", "customers"],
473
+ isMutation: true,
474
+ loadBeforeRecord: async (rawInput, ctx) => {
475
+ const { tenantId } = assertTenantScope(ctx);
476
+ const input = manageDealActivityInput.parse(rawInput);
477
+ const em = resolveEm(ctx);
478
+ if (input.operation === "create") {
479
+ const deal = await loadDealForScope(em, ctx, tenantId, input.dealId);
480
+ if (!deal) return null;
481
+ return {
482
+ recordId: deal.id,
483
+ entityType: "customers.deal",
484
+ recordVersion: recordVersionFromUpdatedAt(deal.updatedAt),
485
+ before: { activityId: null, dealId: deal.id }
486
+ };
487
+ }
488
+ const existing = await loadActivityForScope(em, ctx, tenantId, input.activityId);
489
+ if (!existing) return null;
490
+ return {
491
+ recordId: existing.id,
492
+ entityType: "customers.customer_activity",
493
+ recordVersion: recordVersionFromUpdatedAt(existing.updatedAt),
494
+ before: {
495
+ activityType: existing.activityType,
496
+ subject: existing.subject ?? null,
497
+ body: existing.body ?? null,
498
+ occurredAt: existing.occurredAt ? new Date(existing.occurredAt).toISOString() : null,
499
+ dealId: existing.deal && typeof existing.deal === "object" ? existing.deal.id : existing.deal ?? null
500
+ }
501
+ };
502
+ },
503
+ handler: async (rawInput, ctx) => {
504
+ const { tenantId } = assertTenantScope(ctx);
505
+ const input = manageDealActivityInput.parse(rawInput);
506
+ const em = resolveEm(ctx);
507
+ const runner = createAiApiOperationRunner(ctx);
508
+ if (input.operation === "create") {
509
+ const deal = await loadDealForScope(em, ctx, tenantId, input.dealId);
510
+ if (!deal) throw new Error(`Deal "${input.dealId}" is not accessible to the caller.`);
511
+ const organizationId = deal.organizationId;
512
+ if (!organizationId) throw new Error(`Deal "${deal.id}" has no organization scope.`);
513
+ const dealEntity = deal.entity;
514
+ const entityId = dealEntity && typeof dealEntity === "object" ? dealEntity.id ?? null : typeof dealEntity === "string" ? dealEntity : null;
515
+ if (!entityId) {
516
+ throw new Error(`Deal "${deal.id}" has no associated person/company; cannot attach an activity.`);
517
+ }
518
+ const body2 = {
519
+ tenantId,
520
+ organizationId,
521
+ entityId,
522
+ dealId: deal.id,
523
+ activityType: input.activityType
524
+ };
525
+ if (input.subject) body2.subject = input.subject;
526
+ if (input.body) body2.body = input.body;
527
+ if (input.occurredAt) body2.occurredAt = input.occurredAt;
528
+ const response2 = await runner.run({ method: "POST", path: "/customers/activities", body: body2 });
529
+ if (!response2.success) {
530
+ throw new Error(response2.error ?? "Failed to create activity");
531
+ }
532
+ const result = response2.data ?? {};
533
+ return {
534
+ operation: "create",
535
+ activityId: result.id ?? null,
536
+ dealId: deal.id,
537
+ commandName: "customers.activities.create",
538
+ before: null,
539
+ after: {
540
+ activityType: input.activityType ?? null,
541
+ subject: input.subject ?? null,
542
+ body: input.body ?? null,
543
+ occurredAt: input.occurredAt ?? null
544
+ }
545
+ };
546
+ }
547
+ if (input.operation === "update") {
548
+ const existing2 = await loadActivityForScope(em, ctx, tenantId, input.activityId);
549
+ if (!existing2) throw new Error(`Activity "${input.activityId}" is not accessible to the caller.`);
550
+ const organizationId = existing2.organizationId;
551
+ if (!organizationId) throw new Error(`Activity "${existing2.id}" has no organization scope.`);
552
+ const body2 = { id: existing2.id, tenantId, organizationId };
553
+ if (input.activityType) body2.activityType = input.activityType;
554
+ if (input.subject !== void 0) body2.subject = input.subject;
555
+ if (input.body !== void 0) body2.body = input.body;
556
+ if (input.occurredAt !== void 0) body2.occurredAt = input.occurredAt;
557
+ const response2 = await runner.run({ method: "PUT", path: "/customers/activities", body: body2 });
558
+ if (!response2.success) {
559
+ throw new Error(response2.error ?? `Failed to update activity "${existing2.id}"`);
560
+ }
561
+ const after = await loadActivityForScope(em, ctx, tenantId, existing2.id);
562
+ return {
563
+ operation: "update",
564
+ activityId: existing2.id,
565
+ commandName: "customers.activities.update",
566
+ before: {
567
+ activityType: existing2.activityType,
568
+ subject: existing2.subject ?? null,
569
+ body: existing2.body ?? null,
570
+ occurredAt: existing2.occurredAt ? new Date(existing2.occurredAt).toISOString() : null
571
+ },
572
+ after: after ? {
573
+ activityType: after.activityType,
574
+ subject: after.subject ?? null,
575
+ body: after.body ?? null,
576
+ occurredAt: after.occurredAt ? new Date(after.occurredAt).toISOString() : null
577
+ } : null
578
+ };
579
+ }
580
+ const existing = await loadActivityForScope(em, ctx, tenantId, input.activityId);
581
+ if (!existing) throw new Error(`Activity "${input.activityId}" is not accessible to the caller.`);
582
+ const body = { id: existing.id };
583
+ const response = await runner.run({ method: "DELETE", path: "/customers/activities", body });
584
+ if (!response.success) {
585
+ throw new Error(response.error ?? `Failed to delete activity "${existing.id}"`);
586
+ }
587
+ return {
588
+ operation: "delete",
589
+ activityId: existing.id,
590
+ commandName: "customers.activities.delete",
591
+ before: {
592
+ activityType: existing.activityType,
593
+ subject: existing.subject ?? null,
594
+ body: existing.body ?? null
595
+ },
596
+ after: null
597
+ };
598
+ }
599
+ };
600
+ const listRecordCommentsInput = z.object({
601
+ personId: z.preprocess(blankToUndefined, z.string().uuid().optional()).describe("Restrict to comments on this person entity id."),
602
+ companyId: z.preprocess(blankToUndefined, z.string().uuid().optional()).describe("Restrict to comments on this company entity id."),
603
+ dealId: z.preprocess(blankToUndefined, z.string().uuid().optional()).describe("Restrict to comments attached to this deal id."),
604
+ limit: z.number().int().min(1).max(100).optional().describe("Max rows (default 50, max 100)."),
605
+ offset: z.number().int().min(0).optional().describe("Rows to skip (default 0).")
606
+ }).superRefine((value, ctx) => {
607
+ if (!value.personId && !value.companyId && !value.dealId) {
608
+ ctx.addIssue({
609
+ code: z.ZodIssueCode.custom,
610
+ message: "Provide at least one of personId, companyId, or dealId.",
611
+ path: ["personId"]
612
+ });
613
+ }
614
+ });
615
+ const listRecordCommentsTool = {
616
+ name: "customers.list_record_comments",
617
+ displayName: "List record comments",
618
+ description: "List comments left on a person, company, or deal record. Read-only; tenant + organization scoped. Provide at least one of `personId`, `companyId`, or `dealId`.",
619
+ inputSchema: listRecordCommentsInput,
620
+ requiredFeatures: ["customers.activities.view"],
621
+ tags: ["read", "customers"],
622
+ handler: async (rawInput, ctx) => {
623
+ const { tenantId } = assertTenantScope(ctx);
624
+ const input = listRecordCommentsInput.parse(rawInput);
625
+ const em = resolveEm(ctx);
626
+ const limit = input.limit ?? 50;
627
+ const offset = input.offset ?? 0;
628
+ const where = { tenantId };
629
+ if (ctx.organizationId) where.organizationId = ctx.organizationId;
630
+ const entityId = input.personId ?? input.companyId ?? null;
631
+ if (entityId) where.entity = entityId;
632
+ if (input.dealId) where.deal = input.dealId;
633
+ const [rows, total] = await Promise.all([
634
+ findWithDecryption(
635
+ em,
636
+ CustomerComment,
637
+ where,
638
+ { limit, offset, orderBy: { createdAt: "desc" } },
639
+ buildScope(ctx, tenantId)
640
+ ),
641
+ em.count(CustomerComment, where)
642
+ ]);
643
+ const filtered = rows.filter((row) => row.tenantId === tenantId);
644
+ return {
645
+ items: filtered.map((row) => ({
646
+ id: row.id,
647
+ body: row.body ?? null,
648
+ entityId: commentEntityIdOf(row),
649
+ dealId: row.deal && typeof row.deal === "object" ? row.deal.id : row.deal ?? null,
650
+ authorUserId: row.authorUserId ?? null,
651
+ organizationId: row.organizationId ?? null,
652
+ tenantId: row.tenantId ?? null,
653
+ createdAt: row.createdAt ? new Date(row.createdAt).toISOString() : null,
654
+ updatedAt: row.updatedAt ? new Date(row.updatedAt).toISOString() : null
655
+ })),
656
+ total,
657
+ limit,
658
+ offset
659
+ };
660
+ }
661
+ };
662
+ const manageRecordCommentInput = z.object({
663
+ operation: z.enum(["create", "update", "delete"]).describe("Which write to perform: create a new comment, update an existing one, or delete it."),
664
+ personId: z.preprocess(blankToUndefined, z.string().uuid().optional()).describe("Required for `create` (or supply `companyId`) \u2014 the person entity the comment is attached to."),
665
+ companyId: z.preprocess(blankToUndefined, z.string().uuid().optional()).describe("Required for `create` (or supply `personId`) \u2014 the company entity the comment is attached to."),
666
+ dealId: z.preprocess(blankToUndefined, z.string().uuid().optional()).describe("Optional on `create` \u2014 when set, the comment also shows up under that deal."),
667
+ commentId: z.preprocess(blankToUndefined, z.string().uuid().optional()).describe("Required for `update` and `delete` \u2014 id of the existing comment row."),
668
+ body: z.preprocess(blankToUndefined, z.string().min(1).max(8e3).optional()).describe("Comment text. Required for `create`; optional on `update`.")
669
+ }).superRefine((value, ctx) => {
670
+ if (value.operation === "create") {
671
+ if (!value.personId && !value.companyId) {
672
+ ctx.addIssue({
673
+ code: z.ZodIssueCode.custom,
674
+ message: "Provide personId or companyId for create.",
675
+ path: ["personId"]
676
+ });
677
+ }
678
+ if (value.personId && value.companyId) {
679
+ ctx.addIssue({
680
+ code: z.ZodIssueCode.custom,
681
+ message: "Provide only one of personId or companyId.",
682
+ path: ["personId"]
683
+ });
684
+ }
685
+ if (!value.body) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "body is required for create.", path: ["body"] });
686
+ }
687
+ if (value.operation === "update") {
688
+ if (!value.commentId) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "commentId is required for update.", path: ["commentId"] });
689
+ if (!value.body) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "body is required for update.", path: ["body"] });
690
+ }
691
+ if (value.operation === "delete") {
692
+ if (!value.commentId) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "commentId is required for delete.", path: ["commentId"] });
693
+ }
694
+ });
695
+ const manageRecordCommentTool = {
696
+ name: "customers.manage_record_comment",
697
+ displayName: "Manage record comment",
698
+ description: "Create, update, or delete a comment on a person, company, or deal record. Mutation tool \u2014 every call routes through the AI pending-action approval gate. Use `operation` to pick the action; for `create` provide `personId` OR `companyId` (and optionally `dealId`).",
699
+ inputSchema: manageRecordCommentInput,
700
+ requiredFeatures: ["customers.activities.manage"],
701
+ tags: ["write", "customers"],
702
+ isMutation: true,
703
+ loadBeforeRecord: async (rawInput, ctx) => {
704
+ const { tenantId } = assertTenantScope(ctx);
705
+ const input = manageRecordCommentInput.parse(rawInput);
706
+ const em = resolveEm(ctx);
707
+ if (input.operation === "create") {
708
+ const entityId = input.personId ?? input.companyId;
709
+ if (input.dealId) {
710
+ const deal = await loadDealForScope(em, ctx, tenantId, input.dealId);
711
+ if (!deal) return null;
712
+ return {
713
+ recordId: deal.id,
714
+ entityType: "customers.deal",
715
+ recordVersion: recordVersionFromUpdatedAt(deal.updatedAt),
716
+ before: { commentId: null, body: null, entityId, dealId: deal.id }
717
+ };
718
+ }
719
+ return {
720
+ recordId: entityId,
721
+ entityType: input.personId ? "customers.person" : "customers.company",
722
+ recordVersion: null,
723
+ before: { commentId: null, body: null, entityId, dealId: null }
724
+ };
725
+ }
726
+ const existing = await loadCommentForScope(em, ctx, tenantId, input.commentId);
727
+ if (!existing) return null;
728
+ return {
729
+ recordId: existing.id,
730
+ entityType: "customers.customer_comment",
731
+ recordVersion: recordVersionFromUpdatedAt(existing.updatedAt),
732
+ before: {
733
+ body: existing.body ?? null,
734
+ dealId: existing.deal && typeof existing.deal === "object" ? existing.deal.id : existing.deal ?? null,
735
+ entityId: commentEntityIdOf(existing),
736
+ authorUserId: existing.authorUserId ?? null
737
+ }
738
+ };
739
+ },
740
+ handler: async (rawInput, ctx) => {
741
+ const { tenantId } = assertTenantScope(ctx);
742
+ const input = manageRecordCommentInput.parse(rawInput);
743
+ const em = resolveEm(ctx);
744
+ const runner = createAiApiOperationRunner(ctx);
745
+ if (input.operation === "create") {
746
+ const entityId = input.personId ?? input.companyId;
747
+ let organizationId = ctx.organizationId;
748
+ let dealId = null;
749
+ if (input.dealId) {
750
+ const deal = await loadDealForScope(em, ctx, tenantId, input.dealId);
751
+ if (!deal) throw new Error(`Deal "${input.dealId}" is not accessible to the caller.`);
752
+ organizationId = deal.organizationId ?? organizationId;
753
+ dealId = deal.id;
754
+ }
755
+ if (!organizationId) {
756
+ throw new Error("Organization scope is required to create a comment.");
757
+ }
758
+ const body2 = {
759
+ tenantId,
760
+ organizationId,
761
+ entityId,
762
+ body: input.body
763
+ };
764
+ if (dealId) body2.dealId = dealId;
765
+ const response2 = await runner.run({ method: "POST", path: "/customers/comments", body: body2 });
766
+ if (!response2.success) {
767
+ throw new Error(response2.error ?? "Failed to create comment");
768
+ }
769
+ const result = response2.data ?? {};
770
+ return {
771
+ operation: "create",
772
+ commentId: result.id ?? null,
773
+ entityId,
774
+ dealId,
775
+ commandName: "customers.comments.create",
776
+ before: null,
777
+ after: { body: input.body ?? null, entityId, dealId }
778
+ };
779
+ }
780
+ if (input.operation === "update") {
781
+ const existing2 = await loadCommentForScope(em, ctx, tenantId, input.commentId);
782
+ if (!existing2) throw new Error(`Comment "${input.commentId}" is not accessible to the caller.`);
783
+ const organizationId = existing2.organizationId;
784
+ if (!organizationId) throw new Error(`Comment "${existing2.id}" has no organization scope.`);
785
+ const body2 = {
786
+ id: existing2.id,
787
+ tenantId,
788
+ organizationId,
789
+ body: input.body
790
+ };
791
+ const response2 = await runner.run({ method: "PUT", path: "/customers/comments", body: body2 });
792
+ if (!response2.success) {
793
+ throw new Error(response2.error ?? `Failed to update comment "${existing2.id}"`);
794
+ }
795
+ const after = await loadCommentForScope(em, ctx, tenantId, existing2.id);
796
+ return {
797
+ operation: "update",
798
+ commentId: existing2.id,
799
+ commandName: "customers.comments.update",
800
+ before: { body: existing2.body ?? null },
801
+ after: after ? { body: after.body ?? null } : null
802
+ };
803
+ }
804
+ const existing = await loadCommentForScope(em, ctx, tenantId, input.commentId);
805
+ if (!existing) throw new Error(`Comment "${input.commentId}" is not accessible to the caller.`);
806
+ const body = { id: existing.id };
807
+ const response = await runner.run({ method: "DELETE", path: "/customers/comments", body });
808
+ if (!response.success) {
809
+ throw new Error(response.error ?? `Failed to delete comment "${existing.id}"`);
810
+ }
811
+ return {
812
+ operation: "delete",
813
+ commentId: existing.id,
814
+ commandName: "customers.comments.delete",
815
+ before: { body: existing.body ?? null },
816
+ after: null
817
+ };
818
+ }
819
+ };
820
+ const manageRecordActivityInput = z.object({
821
+ operation: z.enum(["create", "update", "delete"]).describe("Which write to perform: create a new activity, update an existing one, or delete it."),
822
+ activityId: z.preprocess(blankToUndefined, z.string().uuid().optional()).describe("Required for `update` and `delete`."),
823
+ personId: z.preprocess(blankToUndefined, z.string().uuid().optional()).describe("Required for `create` (or supply `companyId`) \u2014 the person entity the activity is logged on."),
824
+ companyId: z.preprocess(blankToUndefined, z.string().uuid().optional()).describe("Required for `create` (or supply `personId`) \u2014 the company entity the activity is logged on."),
825
+ dealId: z.preprocess(blankToUndefined, z.string().uuid().optional()).describe("Optional on `create` \u2014 when set, the activity is also linked to that deal."),
826
+ activityType: z.preprocess(blankToUndefined, z.string().min(1).max(100).optional()).describe('Required for `create` \u2014 e.g. "call", "email", "meeting", "note".'),
827
+ subject: z.preprocess(blankToUndefined, z.string().max(200).optional()).describe("Optional short subject line."),
828
+ body: z.preprocess(blankToUndefined, z.string().max(8e3).optional()).describe("Optional free-text body."),
829
+ occurredAt: z.preprocess(blankToUndefined, z.string().datetime().optional()).describe('ISO-8601 timestamp when the activity occurred. Omit for "now" (server-side default applies).')
830
+ }).superRefine((value, ctx) => {
831
+ if (value.operation === "create") {
832
+ if (!value.personId && !value.companyId) {
833
+ ctx.addIssue({
834
+ code: z.ZodIssueCode.custom,
835
+ message: "Provide personId or companyId for create.",
836
+ path: ["personId"]
837
+ });
838
+ }
839
+ if (value.personId && value.companyId) {
840
+ ctx.addIssue({
841
+ code: z.ZodIssueCode.custom,
842
+ message: "Provide only one of personId or companyId.",
843
+ path: ["personId"]
844
+ });
845
+ }
846
+ if (!value.activityType) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "activityType is required for create.", path: ["activityType"] });
847
+ }
848
+ if (value.operation === "update" || value.operation === "delete") {
849
+ if (!value.activityId) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "activityId is required.", path: ["activityId"] });
850
+ }
851
+ });
852
+ const manageRecordActivityTool = {
853
+ name: "customers.manage_record_activity",
854
+ displayName: "Manage record activity",
855
+ description: "Create, update, or delete an activity (call, email, meeting, note) on a person, company, or deal record. Mutation tool \u2014 every call routes through the AI pending-action approval gate. For `create` provide `personId` OR `companyId` (and optionally `dealId`).",
856
+ inputSchema: manageRecordActivityInput,
857
+ requiredFeatures: ["customers.activities.manage"],
858
+ tags: ["write", "customers"],
859
+ isMutation: true,
860
+ loadBeforeRecord: async (rawInput, ctx) => {
861
+ const { tenantId } = assertTenantScope(ctx);
862
+ const input = manageRecordActivityInput.parse(rawInput);
863
+ const em = resolveEm(ctx);
864
+ if (input.operation === "create") {
865
+ const entityId = input.personId ?? input.companyId;
866
+ if (input.dealId) {
867
+ const deal = await loadDealForScope(em, ctx, tenantId, input.dealId);
868
+ if (!deal) return null;
869
+ return {
870
+ recordId: deal.id,
871
+ entityType: "customers.deal",
872
+ recordVersion: recordVersionFromUpdatedAt(deal.updatedAt),
873
+ before: { activityId: null, dealId: deal.id, entityId }
874
+ };
875
+ }
876
+ return {
877
+ recordId: entityId,
878
+ entityType: input.personId ? "customers.person" : "customers.company",
879
+ recordVersion: null,
880
+ before: { activityId: null, entityId, dealId: null }
881
+ };
882
+ }
883
+ const existing = await loadActivityForScope(em, ctx, tenantId, input.activityId);
884
+ if (!existing) return null;
885
+ return {
886
+ recordId: existing.id,
887
+ entityType: "customers.customer_activity",
888
+ recordVersion: recordVersionFromUpdatedAt(existing.updatedAt),
889
+ before: {
890
+ activityType: existing.activityType,
891
+ subject: existing.subject ?? null,
892
+ body: existing.body ?? null,
893
+ occurredAt: existing.occurredAt ? new Date(existing.occurredAt).toISOString() : null,
894
+ entityId: activityEntityIdOf(existing),
895
+ dealId: existing.deal && typeof existing.deal === "object" ? existing.deal.id : existing.deal ?? null
896
+ }
897
+ };
898
+ },
899
+ handler: async (rawInput, ctx) => {
900
+ const { tenantId } = assertTenantScope(ctx);
901
+ const input = manageRecordActivityInput.parse(rawInput);
902
+ const em = resolveEm(ctx);
903
+ const runner = createAiApiOperationRunner(ctx);
904
+ if (input.operation === "create") {
905
+ const entityId = input.personId ?? input.companyId;
906
+ let organizationId = ctx.organizationId;
907
+ let dealId = null;
908
+ if (input.dealId) {
909
+ const deal = await loadDealForScope(em, ctx, tenantId, input.dealId);
910
+ if (!deal) throw new Error(`Deal "${input.dealId}" is not accessible to the caller.`);
911
+ organizationId = deal.organizationId ?? organizationId;
912
+ dealId = deal.id;
913
+ }
914
+ if (!organizationId) {
915
+ throw new Error("Organization scope is required to create an activity.");
916
+ }
917
+ const body2 = {
918
+ tenantId,
919
+ organizationId,
920
+ entityId,
921
+ activityType: input.activityType
922
+ };
923
+ if (dealId) body2.dealId = dealId;
924
+ if (input.subject) body2.subject = input.subject;
925
+ if (input.body) body2.body = input.body;
926
+ if (input.occurredAt) body2.occurredAt = input.occurredAt;
927
+ const response2 = await runner.run({ method: "POST", path: "/customers/activities", body: body2 });
928
+ if (!response2.success) {
929
+ throw new Error(response2.error ?? "Failed to create activity");
930
+ }
931
+ const result = response2.data ?? {};
932
+ return {
933
+ operation: "create",
934
+ activityId: result.id ?? null,
935
+ entityId,
936
+ dealId,
937
+ commandName: "customers.activities.create",
938
+ before: null,
939
+ after: {
940
+ activityType: input.activityType ?? null,
941
+ subject: input.subject ?? null,
942
+ body: input.body ?? null,
943
+ occurredAt: input.occurredAt ?? null
944
+ }
945
+ };
946
+ }
947
+ if (input.operation === "update") {
948
+ const existing2 = await loadActivityForScope(em, ctx, tenantId, input.activityId);
949
+ if (!existing2) throw new Error(`Activity "${input.activityId}" is not accessible to the caller.`);
950
+ const organizationId = existing2.organizationId;
951
+ if (!organizationId) throw new Error(`Activity "${existing2.id}" has no organization scope.`);
952
+ const body2 = { id: existing2.id, tenantId, organizationId };
953
+ if (input.activityType) body2.activityType = input.activityType;
954
+ if (input.subject !== void 0) body2.subject = input.subject;
955
+ if (input.body !== void 0) body2.body = input.body;
956
+ if (input.occurredAt !== void 0) body2.occurredAt = input.occurredAt;
957
+ const response2 = await runner.run({ method: "PUT", path: "/customers/activities", body: body2 });
958
+ if (!response2.success) {
959
+ throw new Error(response2.error ?? `Failed to update activity "${existing2.id}"`);
960
+ }
961
+ const after = await loadActivityForScope(em, ctx, tenantId, existing2.id);
962
+ return {
963
+ operation: "update",
964
+ activityId: existing2.id,
965
+ commandName: "customers.activities.update",
966
+ before: {
967
+ activityType: existing2.activityType,
968
+ subject: existing2.subject ?? null,
969
+ body: existing2.body ?? null,
970
+ occurredAt: existing2.occurredAt ? new Date(existing2.occurredAt).toISOString() : null
971
+ },
972
+ after: after ? {
973
+ activityType: after.activityType,
974
+ subject: after.subject ?? null,
975
+ body: after.body ?? null,
976
+ occurredAt: after.occurredAt ? new Date(after.occurredAt).toISOString() : null
977
+ } : null
978
+ };
979
+ }
980
+ const existing = await loadActivityForScope(em, ctx, tenantId, input.activityId);
981
+ if (!existing) throw new Error(`Activity "${input.activityId}" is not accessible to the caller.`);
982
+ const body = { id: existing.id };
983
+ const response = await runner.run({ method: "DELETE", path: "/customers/activities", body });
984
+ if (!response.success) {
985
+ throw new Error(response.error ?? `Failed to delete activity "${existing.id}"`);
986
+ }
987
+ return {
988
+ operation: "delete",
989
+ activityId: existing.id,
990
+ commandName: "customers.activities.delete",
991
+ before: {
992
+ activityType: existing.activityType,
993
+ subject: existing.subject ?? null,
994
+ body: existing.body ?? null
995
+ },
996
+ after: null
997
+ };
998
+ }
999
+ };
1000
+ const activitiesTasksAiTools = [
1001
+ listActivitiesTool,
1002
+ listTasksTool,
1003
+ listDealCommentsTool,
1004
+ manageDealCommentTool,
1005
+ manageDealActivityTool,
1006
+ listRecordCommentsTool,
1007
+ manageRecordCommentTool,
1008
+ manageRecordActivityTool
1009
+ ];
1010
+ var activities_tasks_pack_default = activitiesTasksAiTools;
1011
+ export {
1012
+ activitiesTasksAiTools,
1013
+ activities_tasks_pack_default as default
1014
+ };
1015
+ //# sourceMappingURL=activities-tasks-pack.js.map