@open-mercato/core 0.4.5-develop-811deeb983 → 0.4.5-develop-3d8e759e45

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 (76) hide show
  1. package/dist/modules/catalog/inbox-actions.js +51 -0
  2. package/dist/modules/catalog/inbox-actions.js.map +7 -0
  3. package/dist/modules/customers/inbox-actions.js +230 -0
  4. package/dist/modules/customers/inbox-actions.js.map +7 -0
  5. package/dist/modules/inbox_ops/api/emails/[id]/route.js +40 -1
  6. package/dist/modules/inbox_ops/api/emails/[id]/route.js.map +2 -2
  7. package/dist/modules/inbox_ops/api/extract/route.js +87 -0
  8. package/dist/modules/inbox_ops/api/extract/route.js.map +7 -0
  9. package/dist/modules/inbox_ops/api/proposals/[id]/translate/route.js +6 -1
  10. package/dist/modules/inbox_ops/api/proposals/[id]/translate/route.js.map +2 -2
  11. package/dist/modules/inbox_ops/api/proposals/counts/route.js.map +2 -2
  12. package/dist/modules/inbox_ops/backend/inbox-ops/log/page.js +40 -14
  13. package/dist/modules/inbox_ops/backend/inbox-ops/log/page.js.map +2 -2
  14. package/dist/modules/inbox_ops/backend/inbox-ops/log/page.meta.js +2 -2
  15. package/dist/modules/inbox_ops/backend/inbox-ops/log/page.meta.js.map +2 -2
  16. package/dist/modules/inbox_ops/backend/inbox-ops/page.js +161 -79
  17. package/dist/modules/inbox_ops/backend/inbox-ops/page.js.map +2 -2
  18. package/dist/modules/inbox_ops/backend/inbox-ops/page.meta.js +2 -2
  19. package/dist/modules/inbox_ops/backend/inbox-ops/page.meta.js.map +2 -2
  20. package/dist/modules/inbox_ops/backend/inbox-ops/proposals/[id]/page.js +109 -62
  21. package/dist/modules/inbox_ops/backend/inbox-ops/proposals/[id]/page.js.map +3 -3
  22. package/dist/modules/inbox_ops/backend/inbox-ops/proposals/[id]/page.meta.js +2 -2
  23. package/dist/modules/inbox_ops/backend/inbox-ops/proposals/[id]/page.meta.js.map +2 -2
  24. package/dist/modules/inbox_ops/backend/inbox-ops/settings/page.js +36 -14
  25. package/dist/modules/inbox_ops/backend/inbox-ops/settings/page.js.map +2 -2
  26. package/dist/modules/inbox_ops/backend/inbox-ops/settings/page.meta.js +2 -2
  27. package/dist/modules/inbox_ops/backend/inbox-ops/settings/page.meta.js.map +2 -2
  28. package/dist/modules/inbox_ops/components/proposals/ActionCard.js +65 -10
  29. package/dist/modules/inbox_ops/components/proposals/ActionCard.js.map +2 -2
  30. package/dist/modules/inbox_ops/components/proposals/EditActionDialog.js +58 -10
  31. package/dist/modules/inbox_ops/components/proposals/EditActionDialog.js.map +2 -2
  32. package/dist/modules/inbox_ops/lib/constants.js.map +2 -2
  33. package/dist/modules/inbox_ops/lib/contactValidation.js +40 -0
  34. package/dist/modules/inbox_ops/lib/contactValidation.js.map +7 -0
  35. package/dist/modules/inbox_ops/lib/executionEngine.js +31 -826
  36. package/dist/modules/inbox_ops/lib/executionEngine.js.map +3 -3
  37. package/dist/modules/inbox_ops/lib/executionHelpers.js +368 -0
  38. package/dist/modules/inbox_ops/lib/executionHelpers.js.map +7 -0
  39. package/dist/modules/inbox_ops/lib/extractionPrompt.js +28 -35
  40. package/dist/modules/inbox_ops/lib/extractionPrompt.js.map +3 -3
  41. package/dist/modules/inbox_ops/lib/inbox-actions-generated.d.js +1 -0
  42. package/dist/modules/inbox_ops/lib/inbox-actions-generated.d.js.map +7 -0
  43. package/dist/modules/inbox_ops/lib/translationProvider.js +15 -10
  44. package/dist/modules/inbox_ops/lib/translationProvider.js.map +2 -2
  45. package/dist/modules/inbox_ops/subscribers/extractionWorker.js +16 -16
  46. package/dist/modules/inbox_ops/subscribers/extractionWorker.js.map +2 -2
  47. package/dist/modules/sales/inbox-actions.js +278 -0
  48. package/dist/modules/sales/inbox-actions.js.map +7 -0
  49. package/jest.config.cjs +1 -0
  50. package/jest.mocks/inbox-actions.generated.js +5 -0
  51. package/package.json +2 -2
  52. package/src/modules/catalog/inbox-actions.ts +60 -0
  53. package/src/modules/customers/inbox-actions.ts +285 -0
  54. package/src/modules/inbox_ops/api/emails/[id]/route.ts +44 -0
  55. package/src/modules/inbox_ops/api/extract/route.ts +94 -0
  56. package/src/modules/inbox_ops/api/proposals/[id]/translate/route.ts +6 -1
  57. package/src/modules/inbox_ops/api/proposals/counts/route.ts +2 -0
  58. package/src/modules/inbox_ops/backend/inbox-ops/log/page.meta.ts +2 -2
  59. package/src/modules/inbox_ops/backend/inbox-ops/log/page.tsx +43 -13
  60. package/src/modules/inbox_ops/backend/inbox-ops/page.meta.ts +2 -2
  61. package/src/modules/inbox_ops/backend/inbox-ops/page.tsx +176 -81
  62. package/src/modules/inbox_ops/backend/inbox-ops/proposals/[id]/page.meta.ts +2 -2
  63. package/src/modules/inbox_ops/backend/inbox-ops/proposals/[id]/page.tsx +122 -68
  64. package/src/modules/inbox_ops/backend/inbox-ops/settings/page.meta.ts +2 -2
  65. package/src/modules/inbox_ops/backend/inbox-ops/settings/page.tsx +36 -14
  66. package/src/modules/inbox_ops/components/proposals/ActionCard.tsx +91 -7
  67. package/src/modules/inbox_ops/components/proposals/EditActionDialog.tsx +64 -12
  68. package/src/modules/inbox_ops/lib/constants.ts +9 -0
  69. package/src/modules/inbox_ops/lib/contactValidation.ts +54 -0
  70. package/src/modules/inbox_ops/lib/executionEngine.ts +47 -1060
  71. package/src/modules/inbox_ops/lib/executionHelpers.ts +527 -0
  72. package/src/modules/inbox_ops/lib/extractionPrompt.ts +45 -34
  73. package/src/modules/inbox_ops/lib/inbox-actions-generated.d.ts +11 -0
  74. package/src/modules/inbox_ops/lib/translationProvider.ts +16 -10
  75. package/src/modules/inbox_ops/subscribers/extractionWorker.ts +16 -18
  76. package/src/modules/sales/inbox-actions.ts +359 -0
@@ -1,24 +1,8 @@
1
1
  import { findOneWithDecryption, findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
2
2
  import { InboxProposal, InboxProposalAction, InboxDiscrepancy } from "../data/entities.js";
3
- import {
4
- createContactPayloadSchema,
5
- createProductPayloadSchema,
6
- draftReplyPayloadSchema,
7
- linkContactPayloadSchema,
8
- logActivityPayloadSchema,
9
- orderPayloadSchema,
10
- updateOrderPayloadSchema,
11
- updateShipmentPayloadSchema
12
- } from "../data/validators.js";
13
3
  import { REQUIRED_FEATURES_MAP } from "./constants.js";
14
4
  import { formatZodErrors } from "./validation.js";
15
- class ExecutionError extends Error {
16
- constructor(message, statusCode = 400) {
17
- super(message);
18
- this.statusCode = statusCode;
19
- }
20
- }
21
- const SALES_SHIPMENT_STATUS_DICTIONARY_KEY = "sales.shipment_status";
5
+ import { ExecutionError, executeCommand } from "./executionHelpers.js";
22
6
  const ACTION_EXECUTABLE_STATUSES = ["pending", "failed"];
23
7
  async function executeAction(action, ctx) {
24
8
  const em = ctx.em.fork();
@@ -225,15 +209,14 @@ async function acceptAllActions(proposalId, ctx) {
225
209
  }
226
210
  return { results, stoppedOnFailure };
227
211
  }
228
- async function normalizePayload(action, ctx) {
229
- const payload = { ...action.payload };
212
+ function normalizeCommonPayloadFields(payload, actionType) {
230
213
  if (typeof payload.type === "string") {
231
214
  payload.type = payload.type.toLowerCase();
232
215
  }
233
216
  if (typeof payload.contactType === "string") {
234
217
  payload.contactType = payload.contactType.toLowerCase();
235
218
  }
236
- if (action.actionType === "link_contact") {
219
+ if (actionType === "link_contact") {
237
220
  if (!payload.emailAddress) {
238
221
  const alt = payload.email ?? payload.contactEmail;
239
222
  if (typeof alt === "string") payload.emailAddress = alt;
@@ -251,368 +234,38 @@ async function normalizePayload(action, ctx) {
251
234
  if (typeof alt === "string") payload.contactName = alt;
252
235
  }
253
236
  }
254
- if (action.actionType === "create_order" || action.actionType === "create_quote") {
255
- if (!payload.currencyCode) {
256
- const channelId = typeof payload.channelId === "string" ? payload.channelId : null;
257
- const resolved = await resolveChannelCurrency(ctx, channelId);
258
- if (resolved) payload.currencyCode = resolved;
259
- }
260
- }
261
237
  return payload;
262
238
  }
263
- async function executeByType(action, ctx) {
264
- const payload = await normalizePayload(action, ctx);
265
- switch (action.actionType) {
266
- case "create_order": {
267
- const parsed = orderPayloadSchema.safeParse(payload);
268
- if (!parsed.success) throw new ExecutionError(`Invalid create_order payload: ${formatZodErrors(parsed.error)}`, 400);
269
- return executeCreateDocumentAction(action, parsed.data, ctx, "order");
270
- }
271
- case "create_quote": {
272
- const parsed = orderPayloadSchema.safeParse(payload);
273
- if (!parsed.success) throw new ExecutionError(`Invalid create_quote payload: ${formatZodErrors(parsed.error)}`, 400);
274
- return executeCreateDocumentAction(action, parsed.data, ctx, "quote");
275
- }
276
- case "update_order": {
277
- const parsed = updateOrderPayloadSchema.safeParse(payload);
278
- if (!parsed.success) throw new ExecutionError(`Invalid update_order payload: ${formatZodErrors(parsed.error)}`, 400);
279
- return executeUpdateOrderAction(parsed.data, ctx);
280
- }
281
- case "update_shipment": {
282
- const parsed = updateShipmentPayloadSchema.safeParse(payload);
283
- if (!parsed.success) throw new ExecutionError(`Invalid update_shipment payload: ${formatZodErrors(parsed.error)}`, 400);
284
- return executeUpdateShipmentAction(parsed.data, ctx);
285
- }
286
- case "create_contact": {
287
- const parsed = createContactPayloadSchema.safeParse(payload);
288
- if (!parsed.success) throw new ExecutionError(`Invalid create_contact payload: ${formatZodErrors(parsed.error)}`, 400);
289
- return executeCreateContactAction(parsed.data, ctx);
290
- }
291
- case "create_product": {
292
- const parsed = createProductPayloadSchema.safeParse(payload);
293
- if (!parsed.success) throw new ExecutionError(`Invalid create_product payload: ${formatZodErrors(parsed.error)}`, 400);
294
- return executeCreateProductAction(action, parsed.data, ctx);
295
- }
296
- case "link_contact": {
297
- const parsed = linkContactPayloadSchema.safeParse(payload);
298
- if (!parsed.success) throw new ExecutionError(`Invalid link_contact payload: ${formatZodErrors(parsed.error)}`, 400);
299
- return executeLinkContactAction(parsed.data);
300
- }
301
- case "log_activity": {
302
- const parsed = logActivityPayloadSchema.safeParse(payload);
303
- if (!parsed.success) throw new ExecutionError(`Invalid log_activity payload: ${formatZodErrors(parsed.error)}`, 400);
304
- return executeLogActivityAction(parsed.data, ctx);
305
- }
306
- case "draft_reply": {
307
- const parsed = draftReplyPayloadSchema.safeParse(payload);
308
- if (!parsed.success) throw new ExecutionError(`Invalid draft_reply payload: ${formatZodErrors(parsed.error)}`, 400);
309
- return executeDraftReplyAction(action, parsed.data, ctx);
310
- }
311
- default:
312
- throw new ExecutionError(`Unknown action type: ${action.actionType}`, 400);
313
- }
314
- }
315
- async function executeCreateDocumentAction(action, payload, ctx, kind) {
316
- let resolvedChannelId = payload.channelId;
317
- if (!resolvedChannelId) {
318
- resolvedChannelId = await resolveFirstChannelId(ctx) ?? void 0;
319
- if (!resolvedChannelId) {
320
- throw new ExecutionError("No sales channel available. Create a channel first or set channelId in the payload.", 400);
321
- }
322
- }
323
- const currencyCode = payload.currencyCode.trim().toUpperCase();
324
- const lines = payload.lineItems.map((line, index) => {
325
- const quantity = parseNumberToken(line.quantity, `lineItems[${index}].quantity`);
326
- const unitPrice = line.unitPrice ? parseNumberToken(line.unitPrice, `lineItems[${index}].unitPrice`) : void 0;
327
- const mappedLine = {
328
- lineNumber: index + 1,
329
- kind: line.kind ?? (line.productId ? "product" : "service"),
330
- name: line.productName,
331
- description: line.description,
332
- quantity,
333
- currencyCode
334
- };
335
- if (line.productId) mappedLine.productId = line.productId;
336
- if (line.variantId) mappedLine.productVariantId = line.variantId;
337
- if (unitPrice !== void 0) mappedLine.unitPriceNet = unitPrice;
338
- if (line.sku || line.catalogPrice) {
339
- mappedLine.catalogSnapshot = {
340
- sku: line.sku ?? null,
341
- catalogPrice: line.catalogPrice ?? null
342
- };
343
- }
344
- return mappedLine;
345
- });
346
- const metadata = buildSourceMetadata(action.id, action.proposalId);
347
- let resolvedCustomerEntityId = payload.customerEntityId;
348
- if (!resolvedCustomerEntityId && payload.customerEmail) {
349
- resolvedCustomerEntityId = await resolveCustomerEntityIdByEmail(ctx, payload.customerEmail) ?? void 0;
350
- }
351
- const createInput = {
352
- organizationId: ctx.organizationId,
353
- tenantId: ctx.tenantId,
354
- customerEntityId: resolvedCustomerEntityId,
355
- customerReference: payload.customerReference,
356
- channelId: resolvedChannelId,
357
- currencyCode,
358
- taxRateId: payload.taxRateId,
359
- comments: payload.notes,
360
- metadata,
361
- lines
362
- };
363
- if (!resolvedCustomerEntityId) {
364
- createInput.customerSnapshot = {
365
- displayName: payload.customerName,
366
- ...payload.customerEmail && { primaryEmail: payload.customerEmail }
367
- };
368
- }
369
- const normalizedBilling = payload.billingAddress ? normalizeAddressSnapshot(payload.billingAddress) : void 0;
370
- const normalizedShipping = payload.shippingAddress ? normalizeAddressSnapshot(payload.shippingAddress) : void 0;
371
- if (normalizedShipping || normalizedBilling) {
372
- createInput.shippingAddressSnapshot = normalizedShipping ?? normalizedBilling;
373
- createInput.billingAddressSnapshot = normalizedBilling ?? normalizedShipping;
374
- } else if (payload.billingAddressId || payload.shippingAddressId) {
375
- createInput.billingAddressId = payload.billingAddressId ?? payload.shippingAddressId;
376
- createInput.shippingAddressId = payload.shippingAddressId ?? payload.billingAddressId;
377
- }
378
- const requestedDeliveryAt = parseDateToken(payload.requestedDeliveryDate ?? void 0);
379
- if (requestedDeliveryAt) {
380
- createInput.expectedDeliveryAt = requestedDeliveryAt;
381
- }
382
- const effectiveKind = kind === "order" ? await resolveEffectiveDocumentKind(ctx, resolvedChannelId) : kind;
383
- if (effectiveKind === "order") {
384
- const result2 = await executeCommand(
385
- ctx,
386
- "sales.orders.create",
387
- createInput
388
- );
389
- if (!result2.orderId) {
390
- throw new ExecutionError("Order creation did not return an order ID", 500);
391
- }
392
- return {
393
- createdEntityId: result2.orderId,
394
- createdEntityType: "sales_order"
395
- };
396
- }
397
- const result = await executeCommand(
398
- ctx,
399
- "sales.quotes.create",
400
- createInput
401
- );
402
- if (!result.quoteId) {
403
- throw new ExecutionError("Quote creation did not return a quote ID", 500);
404
- }
405
- return {
406
- createdEntityId: result.quoteId,
407
- createdEntityType: "sales_quote"
408
- };
409
- }
410
- async function executeUpdateOrderAction(payload, ctx) {
411
- const order = await resolveOrderByReference(
412
- ctx,
413
- payload.orderId,
414
- payload.orderNumber
415
- );
416
- const updateInput = {
417
- id: order.id,
418
- organizationId: ctx.organizationId,
419
- tenantId: ctx.tenantId
420
- };
421
- const newDeliveryDate = parseDateToken(payload.deliveryDateChange?.newDate);
422
- if (newDeliveryDate) {
423
- updateInput.expectedDeliveryAt = newDeliveryDate;
424
- }
425
- const noteLines = payload.noteAdditions?.map((note) => note.trim()).filter((note) => note.length > 0) ?? [];
426
- if (noteLines.length > 0) {
427
- const mergedNotes = [order.comments ?? null, ...noteLines].filter(Boolean).join("\n");
428
- updateInput.comments = mergedNotes;
429
- }
430
- if (Object.keys(updateInput).length > 3) {
431
- await executeCommand(
432
- ctx,
433
- "sales.orders.update",
434
- updateInput
435
- );
436
- }
437
- const quantityChanges = payload.quantityChanges ?? [];
438
- const orderLines = quantityChanges.length > 0 && quantityChanges.some((qc) => !qc.lineItemId) ? await loadOrderLineItems(ctx, order.id) : [];
439
- for (const quantityChange of quantityChanges) {
440
- let lineItemId = quantityChange.lineItemId;
441
- if (!lineItemId) {
442
- const matched = matchLineItemByName(orderLines, quantityChange.lineItemName);
443
- if (matched) {
444
- lineItemId = matched;
445
- } else {
446
- const availableNames = orderLines.map((l) => l.name).filter(Boolean).join(", ");
447
- throw new ExecutionError(
448
- `Cannot resolve line item "${quantityChange.lineItemName}". Available line items: ${availableNames || "none"}`,
449
- 400
450
- );
451
- }
452
- }
453
- await executeCommand(
454
- ctx,
455
- "sales.orders.lines.upsert",
456
- {
457
- body: {
458
- id: lineItemId,
459
- orderId: order.id,
460
- organizationId: ctx.organizationId,
461
- tenantId: ctx.tenantId,
462
- quantity: parseNumberToken(quantityChange.newQuantity, "quantityChanges.newQuantity"),
463
- currencyCode: order.currencyCode
464
- }
465
- }
466
- );
467
- }
468
- return {
469
- createdEntityId: order.id,
470
- createdEntityType: "sales_order"
471
- };
472
- }
473
- async function executeUpdateShipmentAction(payload, ctx) {
474
- const order = await resolveOrderByReference(
475
- ctx,
476
- payload.orderId,
477
- payload.orderNumber
478
- );
479
- const SalesShipmentClass = resolveEntityClass(ctx, "SalesShipment");
480
- if (!SalesShipmentClass) {
481
- throw new ExecutionError("Sales module entities not available", 503);
482
- }
483
- const shipment = await findOneWithDecryption(
484
- ctx.em,
485
- SalesShipmentClass,
486
- {
487
- order: order.id,
488
- tenantId: ctx.tenantId,
489
- organizationId: ctx.organizationId,
490
- deletedAt: null
491
- },
492
- { orderBy: { createdAt: "DESC" } },
493
- { tenantId: ctx.tenantId, organizationId: ctx.organizationId }
494
- );
495
- if (!shipment) {
496
- throw new ExecutionError("No shipment found for the referenced order", 404);
497
- }
498
- const statusEntryId = await resolveShipmentStatusEntryId(
499
- ctx,
500
- payload.statusLabel
501
- );
502
- if (!statusEntryId) {
503
- throw new ExecutionError(`Shipment status "${payload.statusLabel}" not found`, 400);
504
- }
505
- const updateInput = {
506
- id: shipment.id,
507
- orderId: order.id,
508
- organizationId: ctx.organizationId,
509
- tenantId: ctx.tenantId,
510
- statusEntryId
511
- };
512
- if (payload.trackingNumbers) updateInput.trackingNumbers = payload.trackingNumbers;
513
- if (payload.carrierName) updateInput.carrierName = payload.carrierName;
514
- if (payload.notes) updateInput.notes = payload.notes;
515
- const shippedAt = parseDateToken(payload.shippedAt);
516
- const deliveredAt = parseDateToken(payload.deliveredAt);
517
- if (shippedAt) updateInput.shippedAt = shippedAt;
518
- if (deliveredAt) updateInput.deliveredAt = deliveredAt;
519
- await executeCommand(
520
- ctx,
521
- "sales.shipments.update",
522
- updateInput
523
- );
239
+ function adaptContext(ctx) {
524
240
  return {
525
- createdEntityId: shipment.id,
526
- createdEntityType: "sales_shipment"
241
+ ...ctx,
242
+ executeCommand: (commandId, input) => executeCommand(ctx, commandId, input),
243
+ resolveEntityClass: (key) => resolveEntityClassInternal(ctx, key)
527
244
  };
528
245
  }
529
- async function executeCreateContactAction(payload, ctx) {
530
- const CustomerEntityClass = resolveEntityClass(ctx, "CustomerEntity");
531
- if (payload.email && CustomerEntityClass) {
532
- const emailLower = payload.email.trim().toLowerCase();
533
- let existingContact = await findOneWithDecryption(
534
- ctx.em,
535
- CustomerEntityClass,
536
- {
537
- primaryEmail: emailLower,
538
- tenantId: ctx.tenantId,
539
- organizationId: ctx.organizationId,
540
- deletedAt: null
541
- },
542
- void 0,
543
- { tenantId: ctx.tenantId, organizationId: ctx.organizationId }
544
- );
545
- if (!existingContact) {
546
- const candidates = await findWithDecryption(
547
- ctx.em,
548
- CustomerEntityClass,
549
- {
550
- tenantId: ctx.tenantId,
551
- organizationId: ctx.organizationId,
552
- deletedAt: null
553
- },
554
- { limit: 100, orderBy: { createdAt: "DESC" } },
555
- { tenantId: ctx.tenantId, organizationId: ctx.organizationId }
556
- );
557
- existingContact = candidates.find(
558
- (e) => e.primaryEmail && e.primaryEmail.toLowerCase() === emailLower
559
- ) ?? null;
560
- }
561
- if (existingContact) {
562
- const isCompany = existingContact.kind === "company";
563
- return {
564
- createdEntityId: existingContact.id,
565
- createdEntityType: isCompany ? "customer_company" : "customer_person",
566
- matchedEntityId: existingContact.id,
567
- matchedEntityType: isCompany ? "company" : "person"
568
- };
569
- }
570
- }
571
- if (payload.type === "company") {
572
- const result2 = await executeCommand(
573
- ctx,
574
- "customers.companies.create",
575
- {
576
- organizationId: ctx.organizationId,
577
- tenantId: ctx.tenantId,
578
- displayName: payload.name,
579
- legalName: payload.companyName ?? payload.name,
580
- primaryEmail: payload.email,
581
- primaryPhone: payload.phone,
582
- source: payload.source
583
- }
246
+ async function executeByType(action, ctx) {
247
+ const { getInboxAction } = await import("@/.mercato/generated/inbox-actions.generated");
248
+ const definition = getInboxAction(action.actionType);
249
+ if (!definition) {
250
+ throw new ExecutionError(`Unknown action type: ${action.actionType}`, 400);
251
+ }
252
+ let payload = { ...action.payload };
253
+ payload = normalizeCommonPayloadFields(payload, action.actionType);
254
+ const actionCtx = adaptContext(ctx);
255
+ if (definition.normalizePayload) {
256
+ payload = await definition.normalizePayload(payload, actionCtx);
257
+ }
258
+ const parsed = definition.payloadSchema.safeParse(payload);
259
+ if (!parsed.success) {
260
+ throw new ExecutionError(
261
+ `Invalid ${action.actionType} payload: ${formatZodErrors(parsed.error)}`,
262
+ 400
584
263
  );
585
- if (!result2.entityId) {
586
- throw new ExecutionError("Company creation did not return an entity ID", 500);
587
- }
588
- return {
589
- createdEntityId: result2.entityId,
590
- createdEntityType: "customer_company"
591
- };
592
264
  }
593
- const { firstName, lastName } = splitPersonName(payload.name);
594
- const result = await executeCommand(
595
- ctx,
596
- "customers.people.create",
597
- {
598
- organizationId: ctx.organizationId,
599
- tenantId: ctx.tenantId,
600
- displayName: payload.name,
601
- firstName,
602
- lastName,
603
- primaryEmail: payload.email,
604
- primaryPhone: payload.phone,
605
- jobTitle: payload.role,
606
- source: payload.source
607
- }
265
+ return definition.execute(
266
+ { id: action.id, proposalId: action.proposalId, payload: parsed.data },
267
+ actionCtx
608
268
  );
609
- if (!result.entityId) {
610
- throw new ExecutionError("Person creation did not return an entity ID", 500);
611
- }
612
- return {
613
- createdEntityId: result.entityId,
614
- createdEntityType: "customer_person"
615
- };
616
269
  }
617
270
  async function resolveUnknownContactDiscrepanciesInProposal(em, proposalId, contactEmail, scope) {
618
271
  if (!contactEmail) return;
@@ -641,173 +294,6 @@ async function resolveUnknownContactDiscrepanciesInProposal(em, proposalId, cont
641
294
  await em.flush();
642
295
  }
643
296
  }
644
- async function executeCreateProductAction(action, payload, ctx) {
645
- const createInput = {
646
- organizationId: ctx.organizationId,
647
- tenantId: ctx.tenantId,
648
- title: payload.title,
649
- productType: "simple",
650
- isActive: true
651
- };
652
- if (payload.sku) createInput.sku = payload.sku;
653
- if (payload.description) createInput.description = payload.description;
654
- if (payload.currencyCode) createInput.primaryCurrencyCode = payload.currencyCode;
655
- const result = await executeCommand(
656
- ctx,
657
- "catalog.products.create",
658
- createInput
659
- );
660
- if (!result.productId) {
661
- throw new ExecutionError("Product creation did not return a product ID", 500);
662
- }
663
- await resolveProductDiscrepanciesInProposal(ctx.em, action.proposalId, payload.title, result.productId, {
664
- tenantId: ctx.tenantId,
665
- organizationId: ctx.organizationId
666
- });
667
- return {
668
- createdEntityId: result.productId,
669
- createdEntityType: "catalog_product"
670
- };
671
- }
672
- async function resolveProductDiscrepanciesInProposal(em, proposalId, productTitle, productId, scope) {
673
- const discrepancies = await findWithDecryption(
674
- em,
675
- InboxDiscrepancy,
676
- {
677
- proposalId,
678
- type: "product_not_found",
679
- resolved: false,
680
- tenantId: scope.tenantId,
681
- organizationId: scope.organizationId
682
- },
683
- void 0,
684
- scope
685
- );
686
- const normalizedTitle = productTitle.toLowerCase().trim();
687
- const matchingDiscrepancies = discrepancies.filter((d) => {
688
- const foundValue = (d.foundValue || "").toLowerCase().trim();
689
- return foundValue === normalizedTitle;
690
- });
691
- if (matchingDiscrepancies.length === 0) return;
692
- for (const discrepancy of matchingDiscrepancies) {
693
- discrepancy.resolved = true;
694
- }
695
- await em.flush();
696
- const actionIds = matchingDiscrepancies.map((d) => d.actionId).filter((id) => !!id);
697
- for (const actionId of actionIds) {
698
- await updateLineItemProductId(em, actionId, normalizedTitle, productId, scope);
699
- }
700
- if (actionIds.length > 0) {
701
- await em.flush();
702
- }
703
- }
704
- async function updateLineItemProductId(em, actionId, productName, productId, scope) {
705
- const action = await findOneWithDecryption(
706
- em,
707
- InboxProposalAction,
708
- { id: actionId, deletedAt: null },
709
- void 0,
710
- scope
711
- );
712
- if (!action) return;
713
- const payload = action.payload;
714
- const lineItems = Array.isArray(payload?.lineItems) ? payload.lineItems : [];
715
- let updated = false;
716
- for (const item of lineItems) {
717
- if (item.productId) continue;
718
- const itemName = (typeof item.productName === "string" ? item.productName : "").toLowerCase().trim();
719
- if (itemName === productName) {
720
- item.productId = productId;
721
- updated = true;
722
- break;
723
- }
724
- }
725
- if (updated) {
726
- action.payload = { ...payload, lineItems };
727
- }
728
- }
729
- function executeLinkContactAction(payload) {
730
- return {
731
- createdEntityId: payload.contactId,
732
- createdEntityType: payload.contactType === "company" ? "customer_company" : "customer_person",
733
- matchedEntityId: payload.contactId,
734
- matchedEntityType: payload.contactType
735
- };
736
- }
737
- async function executeLogActivityAction(payload, ctx) {
738
- if (!payload.contactId) {
739
- const resolved = await resolveContactIdByNameAndType(ctx, payload.contactName, payload.contactType);
740
- if (resolved) {
741
- payload = { ...payload, contactId: resolved };
742
- } else {
743
- throw new ExecutionError(
744
- `log_activity requires contactId \u2014 could not resolve contact "${payload.contactName}" (${payload.contactType})`,
745
- 400
746
- );
747
- }
748
- }
749
- const result = await executeCommand(
750
- ctx,
751
- "customers.activities.create",
752
- {
753
- organizationId: ctx.organizationId,
754
- tenantId: ctx.tenantId,
755
- entityId: payload.contactId,
756
- activityType: payload.activityType,
757
- subject: payload.subject,
758
- body: payload.body,
759
- authorUserId: ctx.userId
760
- }
761
- );
762
- if (!result.activityId) {
763
- throw new ExecutionError("Activity creation did not return an activity ID", 500);
764
- }
765
- return {
766
- createdEntityId: result.activityId,
767
- createdEntityType: "customer_activity"
768
- };
769
- }
770
- async function executeDraftReplyAction(action, payload, ctx) {
771
- const payloadRecord = action.payload;
772
- const explicitContactId = typeof payloadRecord.contactId === "string" ? payloadRecord.contactId : null;
773
- const contactId = explicitContactId ?? await resolveCustomerEntityIdByEmail(ctx, payload.to);
774
- if (!contactId) {
775
- throw new ExecutionError(
776
- `No matching contact found for "${payload.to}". Create the contact first or link an existing one.`,
777
- 400
778
- );
779
- }
780
- const details = [
781
- payload.body.trim(),
782
- "",
783
- "---",
784
- `Draft reply target: ${payload.to}`,
785
- `Subject: ${payload.subject}`,
786
- payload.context ? `Context: ${payload.context}` : null,
787
- `InboxOps Proposal: ${action.proposalId}`,
788
- `InboxOps Action: ${action.id}`
789
- ].filter((line) => typeof line === "string" && line.length > 0).join("\n");
790
- const result = await executeCommand(
791
- ctx,
792
- "customers.activities.create",
793
- {
794
- organizationId: ctx.organizationId,
795
- tenantId: ctx.tenantId,
796
- entityId: contactId,
797
- activityType: "email",
798
- subject: payload.subject,
799
- body: details,
800
- authorUserId: ctx.userId
801
- }
802
- );
803
- if (!result.activityId) {
804
- throw new ExecutionError("Draft reply activity did not return an activity ID", 500);
805
- }
806
- return {
807
- createdEntityId: result.activityId,
808
- createdEntityType: "customer_activity"
809
- };
810
- }
811
297
  async function ensureUserCanExecuteAction(action, ctx) {
812
298
  const requiredFeature = getRequiredFeatureForAction(action);
813
299
  if (!requiredFeature) return;
@@ -824,183 +310,7 @@ async function ensureUserCanExecuteAction(action, ctx) {
824
310
  throw new ExecutionError(`Insufficient permissions: ${requiredFeature} required`, 403);
825
311
  }
826
312
  }
827
- async function executeCommand(ctx, commandId, input) {
828
- const commandBus = ctx.container.resolve("commandBus");
829
- if (!commandBus || typeof commandBus.execute !== "function") {
830
- throw new ExecutionError("Command bus is not available", 503);
831
- }
832
- const auth = ctx.auth ?? {
833
- sub: ctx.userId,
834
- userId: ctx.userId,
835
- tenantId: ctx.tenantId,
836
- orgId: ctx.organizationId,
837
- isSuperAdmin: false
838
- };
839
- const commandContext = {
840
- container: ctx.container,
841
- auth,
842
- organizationScope: null,
843
- selectedOrganizationId: ctx.organizationId,
844
- organizationIds: [ctx.organizationId]
845
- };
846
- const { result } = await commandBus.execute(commandId, {
847
- input,
848
- ctx: commandContext
849
- });
850
- return result;
851
- }
852
- function buildSourceMetadata(actionId, proposalId) {
853
- return {
854
- source: "inbox_ops",
855
- inboxOpsActionId: actionId,
856
- inboxOpsProposalId: proposalId
857
- };
858
- }
859
- async function resolveOrderByReference(ctx, orderId, orderNumber) {
860
- const SalesOrderClass = resolveEntityClass(ctx, "SalesOrder");
861
- if (!SalesOrderClass) {
862
- throw new ExecutionError("Sales module entities not available", 503);
863
- }
864
- const where = {
865
- tenantId: ctx.tenantId,
866
- organizationId: ctx.organizationId,
867
- deletedAt: null
868
- };
869
- if (orderId) {
870
- where.id = orderId;
871
- } else if (orderNumber && orderNumber.trim().length > 0) {
872
- where.orderNumber = orderNumber.trim();
873
- } else {
874
- throw new ExecutionError("Order reference is required", 400);
875
- }
876
- const order = await findOneWithDecryption(
877
- ctx.em,
878
- SalesOrderClass,
879
- where,
880
- void 0,
881
- { tenantId: ctx.tenantId, organizationId: ctx.organizationId }
882
- );
883
- if (!order) {
884
- throw new ExecutionError("Referenced order not found", 404);
885
- }
886
- return order;
887
- }
888
- async function resolveShipmentStatusEntryId(ctx, statusLabel) {
889
- const DictionaryClass = resolveEntityClass(ctx, "Dictionary");
890
- const DictionaryEntryClass = resolveEntityClass(ctx, "DictionaryEntry");
891
- if (!DictionaryClass || !DictionaryEntryClass) return null;
892
- const encryptionScope = { tenantId: ctx.tenantId, organizationId: ctx.organizationId };
893
- const dictionary = await findOneWithDecryption(
894
- ctx.em,
895
- DictionaryClass,
896
- {
897
- key: SALES_SHIPMENT_STATUS_DICTIONARY_KEY,
898
- tenantId: ctx.tenantId,
899
- organizationId: ctx.organizationId,
900
- deletedAt: null
901
- },
902
- void 0,
903
- encryptionScope
904
- );
905
- if (!dictionary) return null;
906
- const entries = await findWithDecryption(
907
- ctx.em,
908
- DictionaryEntryClass,
909
- {
910
- dictionary: dictionary.id,
911
- tenantId: ctx.tenantId,
912
- organizationId: ctx.organizationId
913
- },
914
- void 0,
915
- encryptionScope
916
- );
917
- if (!entries.length) return null;
918
- const normalizedTarget = normalizeDictionaryToken(statusLabel);
919
- const loweredTarget = statusLabel.trim().toLowerCase();
920
- const match = entries.find((entry) => {
921
- const label = entry.label.trim().toLowerCase();
922
- const value = entry.value.trim().toLowerCase();
923
- return entry.normalizedValue === normalizedTarget || label === loweredTarget || value === loweredTarget;
924
- });
925
- return match?.id ?? null;
926
- }
927
- async function resolveCustomerEntityIdByEmail(ctx, email) {
928
- const normalized = email.trim().toLowerCase();
929
- if (!normalized) return null;
930
- const CustomerEntityClass = resolveEntityClass(ctx, "CustomerEntity");
931
- if (!CustomerEntityClass) return null;
932
- const entity = await findOneWithDecryption(
933
- ctx.em,
934
- CustomerEntityClass,
935
- {
936
- primaryEmail: normalized,
937
- tenantId: ctx.tenantId,
938
- organizationId: ctx.organizationId,
939
- deletedAt: null
940
- },
941
- void 0,
942
- { tenantId: ctx.tenantId, organizationId: ctx.organizationId }
943
- );
944
- if (entity) return entity.id;
945
- const candidates = await findWithDecryption(
946
- ctx.em,
947
- CustomerEntityClass,
948
- {
949
- tenantId: ctx.tenantId,
950
- organizationId: ctx.organizationId,
951
- deletedAt: null
952
- },
953
- { limit: 100, orderBy: { createdAt: "DESC" } },
954
- { tenantId: ctx.tenantId, organizationId: ctx.organizationId }
955
- );
956
- const match = candidates.find(
957
- (e) => e.primaryEmail && e.primaryEmail.toLowerCase() === normalized
958
- );
959
- return match?.id ?? null;
960
- }
961
- async function resolveEffectiveDocumentKind(ctx, channelId) {
962
- const SalesChannelClass = resolveEntityClass(ctx, "SalesChannel");
963
- if (!SalesChannelClass) return "order";
964
- const channel = await findOneWithDecryption(
965
- ctx.em,
966
- SalesChannelClass,
967
- {
968
- id: channelId,
969
- tenantId: ctx.tenantId,
970
- organizationId: ctx.organizationId,
971
- deletedAt: null
972
- },
973
- void 0,
974
- { tenantId: ctx.tenantId, organizationId: ctx.organizationId }
975
- );
976
- if (!channel) return "order";
977
- const metadata = channel.metadata;
978
- if (metadata?.quotesRequired === true) {
979
- return "quote";
980
- }
981
- return "order";
982
- }
983
- async function resolveFirstChannelId(ctx) {
984
- const SalesChannelClass = resolveEntityClass(ctx, "SalesChannel");
985
- if (!SalesChannelClass) return null;
986
- try {
987
- const channel = await findOneWithDecryption(
988
- ctx.em,
989
- SalesChannelClass,
990
- {
991
- tenantId: ctx.tenantId,
992
- organizationId: ctx.organizationId,
993
- deletedAt: null
994
- },
995
- { orderBy: { name: "ASC" } },
996
- { tenantId: ctx.tenantId, organizationId: ctx.organizationId }
997
- );
998
- return channel?.id ?? null;
999
- } catch {
1000
- return null;
1001
- }
1002
- }
1003
- function resolveEntityClass(ctx, key) {
313
+ function resolveEntityClassInternal(ctx, key) {
1004
314
  const fromEntities = ctx.entities?.[key];
1005
315
  if (fromEntities) return fromEntities;
1006
316
  try {
@@ -1009,113 +319,7 @@ function resolveEntityClass(ctx, key) {
1009
319
  return null;
1010
320
  }
1011
321
  }
1012
- async function resolveChannelCurrency(ctx, channelId) {
1013
- const SalesChannelClass = resolveEntityClass(ctx, "SalesChannel");
1014
- if (!SalesChannelClass) return null;
1015
- try {
1016
- const where = {
1017
- tenantId: ctx.tenantId,
1018
- organizationId: ctx.organizationId,
1019
- deletedAt: null
1020
- };
1021
- if (channelId) where.id = channelId;
1022
- const channel = await findOneWithDecryption(
1023
- ctx.em,
1024
- SalesChannelClass,
1025
- where,
1026
- channelId ? void 0 : { orderBy: { name: "ASC" } },
1027
- { tenantId: ctx.tenantId, organizationId: ctx.organizationId }
1028
- );
1029
- return channel?.currencyCode ?? null;
1030
- } catch {
1031
- return null;
1032
- }
1033
- }
1034
- async function resolveContactIdByNameAndType(ctx, contactName, contactType) {
1035
- const CustomerEntityClass = resolveEntityClass(ctx, "CustomerEntity");
1036
- if (!CustomerEntityClass) return null;
1037
- const normalized = contactName.trim();
1038
- if (!normalized) return null;
1039
- const entity = await findOneWithDecryption(
1040
- ctx.em,
1041
- CustomerEntityClass,
1042
- {
1043
- displayName: normalized,
1044
- kind: contactType,
1045
- tenantId: ctx.tenantId,
1046
- organizationId: ctx.organizationId,
1047
- deletedAt: null
1048
- },
1049
- void 0,
1050
- { tenantId: ctx.tenantId, organizationId: ctx.organizationId }
1051
- );
1052
- return entity?.id ?? null;
1053
- }
1054
- async function loadOrderLineItems(ctx, orderId) {
1055
- try {
1056
- const result = await executeCommand(
1057
- ctx,
1058
- "sales.orders.lines.list",
1059
- { orderId, organizationId: ctx.organizationId, tenantId: ctx.tenantId }
1060
- );
1061
- return result.lines ?? [];
1062
- } catch {
1063
- return [];
1064
- }
1065
- }
1066
- function matchLineItemByName(orderLines, lineItemName) {
1067
- const target = lineItemName.trim().toLowerCase();
1068
- if (!target) return null;
1069
- const exact = orderLines.find((l) => (l.name || "").trim().toLowerCase() === target);
1070
- if (exact) return exact.id;
1071
- const partial = orderLines.find((l) => {
1072
- const name = (l.name || "").trim().toLowerCase();
1073
- return name.includes(target) || target.includes(name);
1074
- });
1075
- return partial?.id ?? null;
1076
- }
1077
- function normalizeDictionaryToken(value) {
1078
- return value.trim().toLowerCase().replace(/[\s-]+/g, "_");
1079
- }
1080
- function splitPersonName(name) {
1081
- const trimmed = name.trim();
1082
- const parts = trimmed.split(/\s+/).filter((item) => item.length > 0);
1083
- if (parts.length <= 1) {
1084
- return {
1085
- firstName: parts[0] || trimmed,
1086
- lastName: ""
1087
- };
1088
- }
1089
- return {
1090
- firstName: parts[0],
1091
- lastName: parts.slice(1).join(" ")
1092
- };
1093
- }
1094
- function parseNumberToken(value, fieldName) {
1095
- const parsed = Number(value);
1096
- if (!Number.isFinite(parsed)) {
1097
- throw new ExecutionError(`Invalid numeric value for ${fieldName}`, 400);
1098
- }
1099
- return parsed;
1100
- }
1101
- function normalizeAddressSnapshot(address) {
1102
- return {
1103
- addressLine1: address.line1 ?? address.addressLine1 ?? "",
1104
- addressLine2: address.line2 ?? address.addressLine2 ?? null,
1105
- companyName: address.company ?? address.companyName ?? null,
1106
- name: address.contactName ?? address.name ?? null,
1107
- city: address.city ?? null,
1108
- region: address.state ?? address.region ?? null,
1109
- postalCode: address.postalCode ?? null,
1110
- country: address.country ?? null
1111
- };
1112
- }
1113
- function parseDateToken(value) {
1114
- if (!value) return void 0;
1115
- const parsed = new Date(value);
1116
- if (Number.isNaN(parsed.getTime())) return void 0;
1117
- return parsed;
1118
- }
322
+ import { splitPersonName } from "./contactValidation.js";
1119
323
  async function resolveActionDiscrepancies(em, actionId, scope) {
1120
324
  const discrepancies = await findWithDecryption(
1121
325
  em,
@@ -1189,6 +393,7 @@ export {
1189
393
  getRequiredFeature,
1190
394
  recalculateProposalStatus,
1191
395
  rejectAction,
1192
- rejectProposal
396
+ rejectProposal,
397
+ splitPersonName
1193
398
  };
1194
399
  //# sourceMappingURL=executionEngine.js.map