@open-mercato/core 0.10.1-develop.7672.1.c2e4c0b5df → 0.10.1-develop.7676.1.e75439fc4f

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.
@@ -1,4 +1,4 @@
1
- [build:core] found 5993 entry points
1
+ [build:core] found 5996 entry points
2
2
  [build:core] built successfully
3
3
  [build:core:generated] found 266 entry points
4
4
  [build:core:generated] built successfully
@@ -1,12 +1,15 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { createRequire } from "node:module";
3
3
  import path from "node:path";
4
- import { drainQueueFromAppRoot } from "./queue-runner.js";
4
+ import { expect } from "@playwright/test";
5
+ import { createQueue, getQueuePendingProbe } from "@open-mercato/queue";
6
+ import { parseBooleanToken } from "@open-mercato/shared/lib/boolean";
5
7
  const DEFAULT_JOB_LIMIT = 100;
6
8
  function resolveAppRoot(input) {
7
9
  return path.resolve(input?.trim() || process.env.OM_TEST_APP_ROOT?.trim() || path.resolve(process.cwd(), "apps/mercato"));
8
10
  }
9
11
  async function drainQueueInCurrentProcess(queueName, options) {
12
+ const { drainQueueFromAppRoot } = await import("./queue-runner.js");
10
13
  return drainQueueFromAppRoot(queueName, options);
11
14
  }
12
15
  function resolveAppRunnerPath(appRoot) {
@@ -62,6 +65,22 @@ ${stderr || stdout}`));
62
65
  });
63
66
  });
64
67
  }
68
+ async function waitForManagedQueue(queueName, options) {
69
+ const baseDir = process.env.QUEUE_BASE_DIR?.trim() || path.resolve(options.appRoot, ".mercato/queue");
70
+ const queue = createQueue(queueName, "local", { baseDir });
71
+ try {
72
+ const initial = await queue.getJobCounts();
73
+ await expect.poll(async () => {
74
+ const pending = await getQueuePendingProbe(queueName, "local", { baseDir });
75
+ if (pending.error) throw new Error(`[internal] Cannot observe integration queue ${queueName}: ${pending.errorMessage}`);
76
+ return pending.ready + pending.active;
77
+ }, { message: `Integration queue ${queueName} should finish processing` }).toBe(0);
78
+ const final = await queue.getJobCounts();
79
+ return final.completed + final.failed - initial.completed - initial.failed;
80
+ } finally {
81
+ await queue.close();
82
+ }
83
+ }
65
84
  async function drainIntegrationQueue(queueName, options = {}) {
66
85
  const resolved = {
67
86
  appRoot: resolveAppRoot(options.appRoot),
@@ -70,6 +89,8 @@ async function drainIntegrationQueue(queueName, options = {}) {
70
89
  if (process.env.OM_TEST_APP_ROOT?.trim()) {
71
90
  return drainQueueInAppProcess(queueName, resolved);
72
91
  }
92
+ const managed = parseBooleanToken(process.env.AUTO_SPAWN_WORKERS) ?? parseBooleanToken(process.env.OM_AUTO_SPAWN_WORKERS) ?? true;
93
+ if (managed) return waitForManagedQueue(queueName, resolved);
73
94
  return drainQueueInCurrentProcess(queueName, resolved);
74
95
  }
75
96
  export {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/helpers/integration/queue.ts"],
4
- "sourcesContent": ["import { spawn } from 'node:child_process'\nimport { createRequire } from 'node:module'\nimport path from 'node:path'\nimport { drainQueueFromAppRoot } from './queue-runner'\n\ntype DrainQueueOptions = {\n appRoot?: string\n jobLimit?: number\n}\n\nconst DEFAULT_JOB_LIMIT = 100\n\nfunction resolveAppRoot(input?: string): string {\n return path.resolve(input?.trim() || process.env.OM_TEST_APP_ROOT?.trim() || path.resolve(process.cwd(), 'apps/mercato'))\n}\n\nasync function drainQueueInCurrentProcess(queueName: string, options: Required<DrainQueueOptions>): Promise<number> {\n return drainQueueFromAppRoot(queueName, options)\n}\n\nfunction resolveAppRunnerPath(appRoot: string): string {\n const requireFromApp = createRequire(path.join(appRoot, 'package.json'))\n return requireFromApp.resolve('@open-mercato/core/helpers/integration/queue-runner')\n}\n\nfunction drainQueueInAppProcess(queueName: string, options: Required<DrainQueueOptions>): Promise<number> {\n return new Promise((resolve, reject) => {\n const child = spawn(process.execPath, [resolveAppRunnerPath(options.appRoot)], {\n cwd: options.appRoot,\n env: {\n ...process.env,\n OM_INTEGRATION_APP_ROOT: options.appRoot,\n OM_INTEGRATION_QUEUE_NAME: queueName,\n OM_INTEGRATION_QUEUE_JOB_LIMIT: String(options.jobLimit),\n QUEUE_BASE_DIR: process.env.QUEUE_BASE_DIR?.trim() || path.resolve(options.appRoot, '.mercato/queue'),\n },\n stdio: ['ignore', 'pipe', 'pipe'],\n })\n\n let stdout = ''\n let stderr = ''\n child.stdout.on('data', (chunk) => { stdout += chunk.toString() })\n child.stderr.on('data', (chunk) => { stderr += chunk.toString() })\n child.on('error', reject)\n child.on('close', (code) => {\n // The child's diagnostics (pino job logs on stdout, cache-fallback\n // console.warn on stderr) are otherwise discarded on exit 0, which makes\n // silently-failing jobs and silent memory-cache fallbacks in the drain\n // child undebuggable from CI logs. OM_DRAIN_DEBUG=1 forwards everything;\n // a non-empty stderr is forwarded unconditionally because it only carries\n // warning/error-class output.\n if (process.env.OM_DRAIN_DEBUG === '1') {\n process.stderr.write(`[drain-child ${queueName} exit=${code}] STDOUT:\\n${stdout}\\nSTDERR:\\n${stderr}\\n`)\n } else if (stderr.trim().length > 0) {\n process.stderr.write(`[drain-child ${queueName} exit=${code}] STDERR:\\n${stderr}\\n`)\n }\n if (code !== 0) {\n reject(new Error(`Queue drain failed for \"${queueName}\" in ${options.appRoot} (exit ${code}).\\n${stderr || stdout}`))\n return\n }\n const lastLine = stdout.trim().split(/\\r?\\n/).reverse().find((line) => line.trim().startsWith('{')) ?? '{}'\n try {\n const parsed = JSON.parse(lastLine) as { processed?: unknown }\n resolve(Number(parsed.processed ?? 0))\n } catch {\n resolve(0)\n }\n })\n })\n}\n\nexport async function drainIntegrationQueue(queueName: string, options: DrainQueueOptions = {}): Promise<number> {\n const resolved = {\n appRoot: resolveAppRoot(options.appRoot),\n jobLimit: options.jobLimit ?? DEFAULT_JOB_LIMIT,\n }\n if (process.env.OM_TEST_APP_ROOT?.trim()) {\n return drainQueueInAppProcess(queueName, resolved)\n }\n return drainQueueInCurrentProcess(queueName, resolved)\n}\n"],
5
- "mappings": "AAAA,SAAS,aAAa;AACtB,SAAS,qBAAqB;AAC9B,OAAO,UAAU;AACjB,SAAS,6BAA6B;AAOtC,MAAM,oBAAoB;AAE1B,SAAS,eAAe,OAAwB;AAC9C,SAAO,KAAK,QAAQ,OAAO,KAAK,KAAK,QAAQ,IAAI,kBAAkB,KAAK,KAAK,KAAK,QAAQ,QAAQ,IAAI,GAAG,cAAc,CAAC;AAC1H;AAEA,eAAe,2BAA2B,WAAmB,SAAuD;AAClH,SAAO,sBAAsB,WAAW,OAAO;AACjD;AAEA,SAAS,qBAAqB,SAAyB;AACrD,QAAM,iBAAiB,cAAc,KAAK,KAAK,SAAS,cAAc,CAAC;AACvE,SAAO,eAAe,QAAQ,qDAAqD;AACrF;AAEA,SAAS,uBAAuB,WAAmB,SAAuD;AACxG,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,UAAM,QAAQ,MAAM,QAAQ,UAAU,CAAC,qBAAqB,QAAQ,OAAO,CAAC,GAAG;AAAA,MAC7E,KAAK,QAAQ;AAAA,MACb,KAAK;AAAA,QACH,GAAG,QAAQ;AAAA,QACX,yBAAyB,QAAQ;AAAA,QACjC,2BAA2B;AAAA,QAC3B,gCAAgC,OAAO,QAAQ,QAAQ;AAAA,QACvD,gBAAgB,QAAQ,IAAI,gBAAgB,KAAK,KAAK,KAAK,QAAQ,QAAQ,SAAS,gBAAgB;AAAA,MACtG;AAAA,MACA,OAAO,CAAC,UAAU,QAAQ,MAAM;AAAA,IAClC,CAAC;AAED,QAAI,SAAS;AACb,QAAI,SAAS;AACb,UAAM,OAAO,GAAG,QAAQ,CAAC,UAAU;AAAE,gBAAU,MAAM,SAAS;AAAA,IAAE,CAAC;AACjE,UAAM,OAAO,GAAG,QAAQ,CAAC,UAAU;AAAE,gBAAU,MAAM,SAAS;AAAA,IAAE,CAAC;AACjE,UAAM,GAAG,SAAS,MAAM;AACxB,UAAM,GAAG,SAAS,CAAC,SAAS;AAO1B,UAAI,QAAQ,IAAI,mBAAmB,KAAK;AACtC,gBAAQ,OAAO,MAAM,gBAAgB,SAAS,SAAS,IAAI;AAAA,EAAc,MAAM;AAAA;AAAA,EAAc,MAAM;AAAA,CAAI;AAAA,MACzG,WAAW,OAAO,KAAK,EAAE,SAAS,GAAG;AACnC,gBAAQ,OAAO,MAAM,gBAAgB,SAAS,SAAS,IAAI;AAAA,EAAc,MAAM;AAAA,CAAI;AAAA,MACrF;AACA,UAAI,SAAS,GAAG;AACd,eAAO,IAAI,MAAM,2BAA2B,SAAS,QAAQ,QAAQ,OAAO,UAAU,IAAI;AAAA,EAAO,UAAU,MAAM,EAAE,CAAC;AACpH;AAAA,MACF;AACA,YAAM,WAAW,OAAO,KAAK,EAAE,MAAM,OAAO,EAAE,QAAQ,EAAE,KAAK,CAAC,SAAS,KAAK,KAAK,EAAE,WAAW,GAAG,CAAC,KAAK;AACvG,UAAI;AACF,cAAM,SAAS,KAAK,MAAM,QAAQ;AAClC,gBAAQ,OAAO,OAAO,aAAa,CAAC,CAAC;AAAA,MACvC,QAAQ;AACN,gBAAQ,CAAC;AAAA,MACX;AAAA,IACF,CAAC;AAAA,EACH,CAAC;AACH;AAEA,eAAsB,sBAAsB,WAAmB,UAA6B,CAAC,GAAoB;AAC/G,QAAM,WAAW;AAAA,IACf,SAAS,eAAe,QAAQ,OAAO;AAAA,IACvC,UAAU,QAAQ,YAAY;AAAA,EAChC;AACA,MAAI,QAAQ,IAAI,kBAAkB,KAAK,GAAG;AACxC,WAAO,uBAAuB,WAAW,QAAQ;AAAA,EACnD;AACA,SAAO,2BAA2B,WAAW,QAAQ;AACvD;",
4
+ "sourcesContent": ["import { spawn } from 'node:child_process'\nimport { createRequire } from 'node:module'\nimport path from 'node:path'\nimport { expect } from '@playwright/test'\nimport { createQueue, getQueuePendingProbe } from '@open-mercato/queue'\nimport { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\n\ntype DrainQueueOptions = {\n appRoot?: string\n jobLimit?: number\n}\n\nconst DEFAULT_JOB_LIMIT = 100\n\nfunction resolveAppRoot(input?: string): string {\n return path.resolve(input?.trim() || process.env.OM_TEST_APP_ROOT?.trim() || path.resolve(process.cwd(), 'apps/mercato'))\n}\n\nasync function drainQueueInCurrentProcess(queueName: string, options: Required<DrainQueueOptions>): Promise<number> {\n const { drainQueueFromAppRoot } = await import('./queue-runner')\n return drainQueueFromAppRoot(queueName, options)\n}\n\nfunction resolveAppRunnerPath(appRoot: string): string {\n const requireFromApp = createRequire(path.join(appRoot, 'package.json'))\n return requireFromApp.resolve('@open-mercato/core/helpers/integration/queue-runner')\n}\n\nfunction drainQueueInAppProcess(queueName: string, options: Required<DrainQueueOptions>): Promise<number> {\n return new Promise((resolve, reject) => {\n const child = spawn(process.execPath, [resolveAppRunnerPath(options.appRoot)], {\n cwd: options.appRoot,\n env: {\n ...process.env,\n OM_INTEGRATION_APP_ROOT: options.appRoot,\n OM_INTEGRATION_QUEUE_NAME: queueName,\n OM_INTEGRATION_QUEUE_JOB_LIMIT: String(options.jobLimit),\n QUEUE_BASE_DIR: process.env.QUEUE_BASE_DIR?.trim() || path.resolve(options.appRoot, '.mercato/queue'),\n },\n stdio: ['ignore', 'pipe', 'pipe'],\n })\n\n let stdout = ''\n let stderr = ''\n child.stdout.on('data', (chunk) => { stdout += chunk.toString() })\n child.stderr.on('data', (chunk) => { stderr += chunk.toString() })\n child.on('error', reject)\n child.on('close', (code) => {\n // The child's diagnostics (pino job logs on stdout, cache-fallback\n // console.warn on stderr) are otherwise discarded on exit 0, which makes\n // silently-failing jobs and silent memory-cache fallbacks in the drain\n // child undebuggable from CI logs. OM_DRAIN_DEBUG=1 forwards everything;\n // a non-empty stderr is forwarded unconditionally because it only carries\n // warning/error-class output.\n if (process.env.OM_DRAIN_DEBUG === '1') {\n process.stderr.write(`[drain-child ${queueName} exit=${code}] STDOUT:\\n${stdout}\\nSTDERR:\\n${stderr}\\n`)\n } else if (stderr.trim().length > 0) {\n process.stderr.write(`[drain-child ${queueName} exit=${code}] STDERR:\\n${stderr}\\n`)\n }\n if (code !== 0) {\n reject(new Error(`Queue drain failed for \"${queueName}\" in ${options.appRoot} (exit ${code}).\\n${stderr || stdout}`))\n return\n }\n const lastLine = stdout.trim().split(/\\r?\\n/).reverse().find((line) => line.trim().startsWith('{')) ?? '{}'\n try {\n const parsed = JSON.parse(lastLine) as { processed?: unknown }\n resolve(Number(parsed.processed ?? 0))\n } catch {\n resolve(0)\n }\n })\n })\n}\n\nasync function waitForManagedQueue(queueName: string, options: Required<DrainQueueOptions>): Promise<number> {\n const baseDir = process.env.QUEUE_BASE_DIR?.trim() || path.resolve(options.appRoot, '.mercato/queue')\n const queue = createQueue(queueName, 'local', { baseDir })\n try {\n const initial = await queue.getJobCounts()\n await expect.poll(async () => {\n const pending = await getQueuePendingProbe(queueName, 'local', { baseDir })\n if (pending.error) throw new Error(`[internal] Cannot observe integration queue ${queueName}: ${pending.errorMessage}`)\n return pending.ready + pending.active\n }, { message: `Integration queue ${queueName} should finish processing` }).toBe(0)\n const final = await queue.getJobCounts()\n return final.completed + final.failed - initial.completed - initial.failed\n } finally {\n await queue.close()\n }\n}\n\nexport async function drainIntegrationQueue(queueName: string, options: DrainQueueOptions = {}): Promise<number> {\n const resolved = {\n appRoot: resolveAppRoot(options.appRoot),\n jobLimit: options.jobLimit ?? DEFAULT_JOB_LIMIT,\n }\n if (process.env.OM_TEST_APP_ROOT?.trim()) {\n return drainQueueInAppProcess(queueName, resolved)\n }\n const managed = parseBooleanToken(process.env.AUTO_SPAWN_WORKERS)\n ?? parseBooleanToken(process.env.OM_AUTO_SPAWN_WORKERS)\n ?? true\n if (managed) return waitForManagedQueue(queueName, resolved)\n return drainQueueInCurrentProcess(queueName, resolved)\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -3,9 +3,12 @@ import {
3
3
  createAiApiOperationRunner
4
4
  } from "@open-mercato/ai-assistant/modules/ai_assistant/lib/ai-api-operation-runner";
5
5
  import { findOneWithDecryption, findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
6
+ import { authorizeFeatures } from "@open-mercato/shared/security/featurePolicy";
6
7
  import { TERMINAL_INTERACTION_STATUS_LIST } from "../lib/interactionStatus.js";
8
+ import { resolveCustomerInteractionFeatureFlags } from "../lib/interactionFeatureFlags.js";
9
+ import { CUSTOMER_INTERACTION_ACTIVITY_ADAPTER_SOURCE } from "../lib/interactionCompatibility.js";
10
+ import { listActivitiesForScope, loadActivityForScope } from "./activity-read-model.js";
7
11
  import {
8
- CustomerActivity,
9
12
  CustomerComment,
10
13
  CustomerDeal,
11
14
  CustomerDealCompanyLink,
@@ -93,28 +96,49 @@ async function loadCommentForScope(em, ctx, tenantId, commentId) {
93
96
  if (ctx.organizationId && row.organizationId !== ctx.organizationId) return null;
94
97
  return row;
95
98
  }
96
- async function loadActivityForScope(em, ctx, tenantId, activityId) {
97
- const where = { id: activityId, tenantId };
98
- if (ctx.organizationId) where.organizationId = ctx.organizationId;
99
- const row = await findOneWithDecryption(
100
- em,
101
- CustomerActivity,
102
- where,
103
- void 0,
104
- buildScope(ctx, tenantId)
105
- );
106
- if (!row || row.tenantId !== tenantId) return null;
107
- if (ctx.organizationId && row.organizationId !== ctx.organizationId) return null;
108
- return row;
99
+ async function runActivityOperation(ctx, request) {
100
+ const { tenantId } = assertTenantScope(ctx);
101
+ const flags = await resolveCustomerInteractionFeatureFlags(ctx.container, tenantId);
102
+ const executionContext = ctx;
103
+ if (flags.legacyAdapters) {
104
+ return createAiApiOperationRunner(executionContext).run(request);
105
+ }
106
+ const requiredFeatures = ["customers.interactions.manage"];
107
+ if (!authorizeFeatures(requiredFeatures, {
108
+ grantedFeatures: ctx.userFeatures,
109
+ unrestricted: ctx.isSuperAdmin
110
+ })) {
111
+ throw new Error("[internal] Canonical interaction management permission is required");
112
+ }
113
+ const { activityType, subject, ...body } = request.body ?? {};
114
+ const runner = createAiApiOperationRunner({
115
+ ...executionContext,
116
+ tool: { ...executionContext.tool, requiredFeatures }
117
+ });
118
+ return runner.run({
119
+ ...request,
120
+ path: "/customers/interactions",
121
+ body: {
122
+ ...body,
123
+ ...activityType !== void 0 ? { interactionType: activityType } : {},
124
+ ...subject !== void 0 ? { title: subject } : {},
125
+ ...request.method === "POST" ? {
126
+ source: CUSTOMER_INTERACTION_ACTIVITY_ADAPTER_SOURCE,
127
+ status: body.occurredAt ? "done" : "planned"
128
+ } : {},
129
+ ...request.method === "PUT" && body.occurredAt ? { status: "done" } : {}
130
+ }
131
+ });
109
132
  }
110
- function commentEntityIdOf(row) {
111
- const ent = row.entity;
112
- if (!ent) return null;
113
- if (typeof ent === "string") return ent;
114
- if (typeof ent === "object" && typeof ent.id === "string") return ent.id;
115
- return null;
133
+ function activitySnapshot(activity) {
134
+ return activity ? {
135
+ activityType: activity.activityType,
136
+ subject: activity.subject,
137
+ body: activity.body,
138
+ occurredAt: activity.occurredAt?.toISOString() ?? null
139
+ } : null;
116
140
  }
117
- function activityEntityIdOf(row) {
141
+ function commentEntityIdOf(row) {
118
142
  const ent = row.entity;
119
143
  if (!ent) return null;
120
144
  if (typeof ent === "string") return ent;
@@ -142,33 +166,17 @@ const listActivitiesTool = {
142
166
  const em = resolveEm(ctx);
143
167
  const limit = input.limit ?? 50;
144
168
  const offset = input.offset ?? 0;
145
- const where = { tenantId };
146
- if (ctx.organizationId) where.organizationId = ctx.organizationId;
147
- const entityId = input.personId ?? input.companyId ?? null;
148
- if (entityId) where.entity = entityId;
149
- if (input.dealId) where.deal = input.dealId;
150
- if (input.activityType) where.activityType = input.activityType;
151
- const [rows, total] = await Promise.all([
152
- findWithDecryption(
153
- em,
154
- CustomerActivity,
155
- where,
156
- { limit, offset, orderBy: { occurredAt: "desc", createdAt: "desc" } },
157
- buildScope(ctx, tenantId)
158
- ),
159
- em.count(CustomerActivity, where)
160
- ]);
161
- const filtered = rows.filter((row) => row.tenantId === tenantId);
169
+ const { items: rows, total } = await listActivitiesForScope(em, ctx, tenantId, { ...input, limit, offset });
162
170
  return {
163
- items: filtered.map((row) => ({
171
+ items: rows.map((row) => ({
164
172
  id: row.id,
165
173
  activityType: row.activityType,
166
174
  subject: row.subject ?? null,
167
175
  body: row.body ?? null,
168
176
  occurredAt: row.occurredAt ? new Date(row.occurredAt).toISOString() : null,
169
177
  authorUserId: row.authorUserId ?? null,
170
- entityId: activityEntityIdOf(row),
171
- dealId: row.deal && typeof row.deal === "object" ? row.deal.id : row.deal ?? null,
178
+ entityId: row.entityId,
179
+ dealId: row.dealId,
172
180
  organizationId: row.organizationId ?? null,
173
181
  tenantId: row.tenantId ?? null,
174
182
  createdAt: row.createdAt ? new Date(row.createdAt).toISOString() : null
@@ -503,14 +511,14 @@ const manageDealActivityTool = {
503
511
  if (!existing) return null;
504
512
  return {
505
513
  recordId: existing.id,
506
- entityType: "customers.customer_activity",
514
+ entityType: existing.entityType,
507
515
  recordVersion: recordVersionFromUpdatedAt(existing.updatedAt),
508
516
  before: {
509
517
  activityType: existing.activityType,
510
518
  subject: existing.subject ?? null,
511
519
  body: existing.body ?? null,
512
520
  occurredAt: existing.occurredAt ? new Date(existing.occurredAt).toISOString() : null,
513
- dealId: existing.deal && typeof existing.deal === "object" ? existing.deal.id : existing.deal ?? null
521
+ dealId: existing.dealId
514
522
  }
515
523
  };
516
524
  },
@@ -518,7 +526,6 @@ const manageDealActivityTool = {
518
526
  const { tenantId } = assertTenantScope(ctx);
519
527
  const input = manageDealActivityInput.parse(rawInput);
520
528
  const em = resolveEm(ctx);
521
- const runner = createAiApiOperationRunner(ctx);
522
529
  if (input.operation === "create") {
523
530
  const deal = await loadDealForScope(em, ctx, tenantId, input.dealId);
524
531
  if (!deal) throw new Error(`Deal "${input.dealId}" is not accessible to the caller.`);
@@ -538,23 +545,19 @@ const manageDealActivityTool = {
538
545
  if (input.subject) body2.subject = input.subject;
539
546
  if (input.body) body2.body = input.body;
540
547
  if (input.occurredAt) body2.occurredAt = input.occurredAt;
541
- const response2 = await runner.run({ method: "POST", path: "/customers/activities", body: body2 });
548
+ const response2 = await runActivityOperation(ctx, { method: "POST", path: "/customers/activities", body: body2 });
542
549
  if (!response2.success) {
543
550
  throw new Error(response2.error ?? "Failed to create activity");
544
551
  }
545
552
  const result = response2.data ?? {};
553
+ const created = result.id ? await loadActivityForScope(em, ctx, tenantId, result.id) : null;
546
554
  return {
547
555
  operation: "create",
548
556
  activityId: result.id ?? null,
549
557
  dealId: deal.id,
550
558
  commandName: "customers.activities.create",
551
559
  before: null,
552
- after: {
553
- activityType: input.activityType ?? null,
554
- subject: input.subject ?? null,
555
- body: input.body ?? null,
556
- occurredAt: input.occurredAt ?? null
557
- }
560
+ after: activitySnapshot(created)
558
561
  };
559
562
  }
560
563
  if (input.operation === "update") {
@@ -567,7 +570,7 @@ const manageDealActivityTool = {
567
570
  if (input.subject !== void 0) body2.subject = input.subject;
568
571
  if (input.body !== void 0) body2.body = input.body;
569
572
  if (input.occurredAt !== void 0) body2.occurredAt = input.occurredAt;
570
- const response2 = await runner.run({ method: "PUT", path: "/customers/activities", body: body2 });
573
+ const response2 = await runActivityOperation(ctx, { method: "PUT", path: "/customers/activities", body: body2 });
571
574
  if (!response2.success) {
572
575
  throw new Error(response2.error ?? `Failed to update activity "${existing2.id}"`);
573
576
  }
@@ -593,7 +596,7 @@ const manageDealActivityTool = {
593
596
  const existing = await loadActivityForScope(em, ctx, tenantId, input.activityId);
594
597
  if (!existing) throw new Error(`Activity "${input.activityId}" is not accessible to the caller.`);
595
598
  const body = { id: existing.id };
596
- const response = await runner.run({ method: "DELETE", path: "/customers/activities", body });
599
+ const response = await runActivityOperation(ctx, { method: "DELETE", path: "/customers/activities", body });
597
600
  if (!response.success) {
598
601
  throw new Error(response.error ?? `Failed to delete activity "${existing.id}"`);
599
602
  }
@@ -897,15 +900,15 @@ const manageRecordActivityTool = {
897
900
  if (!existing) return null;
898
901
  return {
899
902
  recordId: existing.id,
900
- entityType: "customers.customer_activity",
903
+ entityType: existing.entityType,
901
904
  recordVersion: recordVersionFromUpdatedAt(existing.updatedAt),
902
905
  before: {
903
906
  activityType: existing.activityType,
904
907
  subject: existing.subject ?? null,
905
908
  body: existing.body ?? null,
906
909
  occurredAt: existing.occurredAt ? new Date(existing.occurredAt).toISOString() : null,
907
- entityId: activityEntityIdOf(existing),
908
- dealId: existing.deal && typeof existing.deal === "object" ? existing.deal.id : existing.deal ?? null
910
+ entityId: existing.entityId,
911
+ dealId: existing.dealId
909
912
  }
910
913
  };
911
914
  },
@@ -913,7 +916,6 @@ const manageRecordActivityTool = {
913
916
  const { tenantId } = assertTenantScope(ctx);
914
917
  const input = manageRecordActivityInput.parse(rawInput);
915
918
  const em = resolveEm(ctx);
916
- const runner = createAiApiOperationRunner(ctx);
917
919
  if (input.operation === "create") {
918
920
  const entityId = input.personId ?? input.companyId;
919
921
  let organizationId = ctx.organizationId;
@@ -937,11 +939,12 @@ const manageRecordActivityTool = {
937
939
  if (input.subject) body2.subject = input.subject;
938
940
  if (input.body) body2.body = input.body;
939
941
  if (input.occurredAt) body2.occurredAt = input.occurredAt;
940
- const response2 = await runner.run({ method: "POST", path: "/customers/activities", body: body2 });
942
+ const response2 = await runActivityOperation(ctx, { method: "POST", path: "/customers/activities", body: body2 });
941
943
  if (!response2.success) {
942
944
  throw new Error(response2.error ?? "Failed to create activity");
943
945
  }
944
946
  const result = response2.data ?? {};
947
+ const created = result.id ? await loadActivityForScope(em, ctx, tenantId, result.id) : null;
945
948
  return {
946
949
  operation: "create",
947
950
  activityId: result.id ?? null,
@@ -949,12 +952,7 @@ const manageRecordActivityTool = {
949
952
  dealId,
950
953
  commandName: "customers.activities.create",
951
954
  before: null,
952
- after: {
953
- activityType: input.activityType ?? null,
954
- subject: input.subject ?? null,
955
- body: input.body ?? null,
956
- occurredAt: input.occurredAt ?? null
957
- }
955
+ after: activitySnapshot(created)
958
956
  };
959
957
  }
960
958
  if (input.operation === "update") {
@@ -967,7 +965,7 @@ const manageRecordActivityTool = {
967
965
  if (input.subject !== void 0) body2.subject = input.subject;
968
966
  if (input.body !== void 0) body2.body = input.body;
969
967
  if (input.occurredAt !== void 0) body2.occurredAt = input.occurredAt;
970
- const response2 = await runner.run({ method: "PUT", path: "/customers/activities", body: body2 });
968
+ const response2 = await runActivityOperation(ctx, { method: "PUT", path: "/customers/activities", body: body2 });
971
969
  if (!response2.success) {
972
970
  throw new Error(response2.error ?? `Failed to update activity "${existing2.id}"`);
973
971
  }
@@ -993,7 +991,7 @@ const manageRecordActivityTool = {
993
991
  const existing = await loadActivityForScope(em, ctx, tenantId, input.activityId);
994
992
  if (!existing) throw new Error(`Activity "${input.activityId}" is not accessible to the caller.`);
995
993
  const body = { id: existing.id };
996
- const response = await runner.run({ method: "DELETE", path: "/customers/activities", body });
994
+ const response = await runActivityOperation(ctx, { method: "DELETE", path: "/customers/activities", body });
997
995
  if (!response.success) {
998
996
  throw new Error(response.error ?? `Failed to delete activity "${existing.id}"`);
999
997
  }