@open-mercato/core 0.4.5-develop-636d33c995 → 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.
- package/dist/modules/catalog/backend/catalog/categories/[id]/edit/page.js +17 -2
- package/dist/modules/catalog/backend/catalog/categories/[id]/edit/page.js.map +2 -2
- package/dist/modules/catalog/backend/catalog/products/[id]/page.js +15 -0
- package/dist/modules/catalog/backend/catalog/products/[id]/page.js.map +2 -2
- package/dist/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.js +30 -0
- package/dist/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.js.map +2 -2
- package/dist/modules/catalog/inbox-actions.js +51 -0
- package/dist/modules/catalog/inbox-actions.js.map +7 -0
- package/dist/modules/catalog/lib/messageObjectPreviews.js +146 -0
- package/dist/modules/catalog/lib/messageObjectPreviews.js.map +7 -0
- package/dist/modules/catalog/message-objects.js +95 -0
- package/dist/modules/catalog/message-objects.js.map +7 -0
- package/dist/modules/currencies/backend/currencies/[id]/page.js +21 -0
- package/dist/modules/currencies/backend/currencies/[id]/page.js.map +2 -2
- package/dist/modules/currencies/lib/messageObjectPreviews.js +51 -0
- package/dist/modules/currencies/lib/messageObjectPreviews.js.map +7 -0
- package/dist/modules/currencies/message-objects.js +41 -0
- package/dist/modules/currencies/message-objects.js.map +7 -0
- package/dist/modules/customers/backend/customers/companies/[id]/page.js +20 -0
- package/dist/modules/customers/backend/customers/companies/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/page.js +12 -1
- package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/people/[id]/page.js +20 -0
- package/dist/modules/customers/backend/customers/people/[id]/page.js.map +2 -2
- package/dist/modules/customers/components/detail/CompanyHighlights.js +18 -14
- package/dist/modules/customers/components/detail/CompanyHighlights.js.map +2 -2
- package/dist/modules/customers/components/detail/PersonHighlights.js +18 -14
- package/dist/modules/customers/components/detail/PersonHighlights.js.map +2 -2
- package/dist/modules/customers/inbox-actions.js +230 -0
- package/dist/modules/customers/inbox-actions.js.map +7 -0
- package/dist/modules/customers/lib/messageObjectPreviews.js +41 -5
- package/dist/modules/customers/lib/messageObjectPreviews.js.map +2 -2
- package/dist/modules/customers/message-objects.js +31 -11
- package/dist/modules/customers/message-objects.js.map +2 -2
- package/dist/modules/inbox_ops/api/emails/[id]/route.js +40 -1
- package/dist/modules/inbox_ops/api/emails/[id]/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/extract/route.js +87 -0
- package/dist/modules/inbox_ops/api/extract/route.js.map +7 -0
- package/dist/modules/inbox_ops/api/proposals/[id]/translate/route.js +6 -1
- package/dist/modules/inbox_ops/api/proposals/[id]/translate/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/proposals/counts/route.js.map +2 -2
- package/dist/modules/inbox_ops/backend/inbox-ops/log/page.js +40 -14
- package/dist/modules/inbox_ops/backend/inbox-ops/log/page.js.map +2 -2
- package/dist/modules/inbox_ops/backend/inbox-ops/log/page.meta.js +2 -2
- package/dist/modules/inbox_ops/backend/inbox-ops/log/page.meta.js.map +2 -2
- package/dist/modules/inbox_ops/backend/inbox-ops/page.js +161 -79
- package/dist/modules/inbox_ops/backend/inbox-ops/page.js.map +2 -2
- package/dist/modules/inbox_ops/backend/inbox-ops/page.meta.js +2 -2
- package/dist/modules/inbox_ops/backend/inbox-ops/page.meta.js.map +2 -2
- package/dist/modules/inbox_ops/backend/inbox-ops/proposals/[id]/page.js +109 -62
- package/dist/modules/inbox_ops/backend/inbox-ops/proposals/[id]/page.js.map +3 -3
- package/dist/modules/inbox_ops/backend/inbox-ops/proposals/[id]/page.meta.js +2 -2
- package/dist/modules/inbox_ops/backend/inbox-ops/proposals/[id]/page.meta.js.map +2 -2
- package/dist/modules/inbox_ops/backend/inbox-ops/settings/page.js +36 -14
- package/dist/modules/inbox_ops/backend/inbox-ops/settings/page.js.map +2 -2
- package/dist/modules/inbox_ops/backend/inbox-ops/settings/page.meta.js +2 -2
- package/dist/modules/inbox_ops/backend/inbox-ops/settings/page.meta.js.map +2 -2
- package/dist/modules/inbox_ops/components/proposals/ActionCard.js +65 -10
- package/dist/modules/inbox_ops/components/proposals/ActionCard.js.map +2 -2
- package/dist/modules/inbox_ops/components/proposals/EditActionDialog.js +58 -10
- package/dist/modules/inbox_ops/components/proposals/EditActionDialog.js.map +2 -2
- package/dist/modules/inbox_ops/lib/constants.js.map +2 -2
- package/dist/modules/inbox_ops/lib/contactValidation.js +40 -0
- package/dist/modules/inbox_ops/lib/contactValidation.js.map +7 -0
- package/dist/modules/inbox_ops/lib/executionEngine.js +31 -826
- package/dist/modules/inbox_ops/lib/executionEngine.js.map +3 -3
- package/dist/modules/inbox_ops/lib/executionHelpers.js +368 -0
- package/dist/modules/inbox_ops/lib/executionHelpers.js.map +7 -0
- package/dist/modules/inbox_ops/lib/extractionPrompt.js +28 -35
- package/dist/modules/inbox_ops/lib/extractionPrompt.js.map +3 -3
- package/dist/modules/inbox_ops/lib/inbox-actions-generated.d.js +1 -0
- package/dist/modules/inbox_ops/lib/inbox-actions-generated.d.js.map +7 -0
- package/dist/modules/inbox_ops/lib/translationProvider.js +15 -10
- package/dist/modules/inbox_ops/lib/translationProvider.js.map +2 -2
- package/dist/modules/inbox_ops/subscribers/extractionWorker.js +16 -16
- package/dist/modules/inbox_ops/subscribers/extractionWorker.js.map +2 -2
- package/dist/modules/messages/commands/messages.js +3 -0
- package/dist/modules/messages/commands/messages.js.map +2 -2
- package/dist/modules/messages/components/message-detail/panels/objects-panel.js +6 -1
- package/dist/modules/messages/components/message-detail/panels/objects-panel.js.map +2 -2
- package/dist/modules/messages/components/message-detail/panels/thread-panel.js +4 -1
- package/dist/modules/messages/components/message-detail/panels/thread-panel.js.map +2 -2
- package/dist/modules/messages/frontend/messages/view/[token]/page.js +1 -0
- package/dist/modules/messages/frontend/messages/view/[token]/page.js.map +2 -2
- package/dist/modules/resources/backend/resources/resources/[id]/page.js +24 -7
- package/dist/modules/resources/backend/resources/resources/[id]/page.js.map +2 -2
- package/dist/modules/resources/lib/messageObjectPreviews.js +43 -0
- package/dist/modules/resources/lib/messageObjectPreviews.js.map +7 -0
- package/dist/modules/resources/message-objects.js +37 -0
- package/dist/modules/resources/message-objects.js.map +7 -0
- package/dist/modules/sales/backend/sales/channels/[channelId]/edit/page.js +19 -0
- package/dist/modules/sales/backend/sales/channels/[channelId]/edit/page.js.map +2 -2
- package/dist/modules/sales/backend/sales/documents/[id]/page.js +23 -2
- package/dist/modules/sales/backend/sales/documents/[id]/page.js.map +2 -2
- package/dist/modules/sales/backend/sales/quotes/[id]/page.js +1 -1
- package/dist/modules/sales/backend/sales/quotes/[id]/page.js.map +2 -2
- package/dist/modules/sales/inbox-actions.js +278 -0
- package/dist/modules/sales/inbox-actions.js.map +7 -0
- package/dist/modules/sales/lib/messageObjectPreviews.js +49 -4
- package/dist/modules/sales/lib/messageObjectPreviews.js.map +2 -2
- package/dist/modules/sales/message-objects.js +44 -2
- package/dist/modules/sales/message-objects.js.map +2 -2
- package/dist/modules/sales/widgets/messages/SalesDocumentMessageDetail.js +59 -30
- package/dist/modules/sales/widgets/messages/SalesDocumentMessageDetail.js.map +2 -2
- package/dist/modules/sales/widgets/messages/SalesDocumentMessagePreview.js +1 -1
- package/dist/modules/sales/widgets/messages/SalesDocumentMessagePreview.js.map +1 -1
- package/dist/modules/staff/backend/staff/leave-requests/[id]/page.js +8 -30
- package/dist/modules/staff/backend/staff/leave-requests/[id]/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/my-availability/page.js +13 -0
- package/dist/modules/staff/backend/staff/my-availability/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/my-leave-requests/[id]/page.js +8 -31
- package/dist/modules/staff/backend/staff/my-leave-requests/[id]/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/team-members/[id]/page.js +32 -10
- package/dist/modules/staff/backend/staff/team-members/[id]/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/team-roles/[id]/edit/page.js +14 -1
- package/dist/modules/staff/backend/staff/team-roles/[id]/edit/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/teams/[id]/edit/page.js +14 -1
- package/dist/modules/staff/backend/staff/teams/[id]/edit/page.js.map +2 -2
- package/dist/modules/staff/components/TeamForm.js +4 -2
- package/dist/modules/staff/components/TeamForm.js.map +2 -2
- package/dist/modules/staff/components/TeamRoleForm.js +4 -2
- package/dist/modules/staff/components/TeamRoleForm.js.map +2 -2
- package/dist/modules/staff/lib/messageObjectPreviews.js +111 -2
- package/dist/modules/staff/lib/messageObjectPreviews.js.map +2 -2
- package/dist/modules/staff/message-objects.js +79 -8
- package/dist/modules/staff/message-objects.js.map +2 -2
- package/jest.config.cjs +1 -0
- package/jest.mocks/inbox-actions.generated.js +5 -0
- package/package.json +2 -2
- package/src/modules/catalog/backend/catalog/categories/[id]/edit/page.tsx +19 -5
- package/src/modules/catalog/backend/catalog/products/[id]/page.tsx +14 -0
- package/src/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.tsx +40 -0
- package/src/modules/catalog/inbox-actions.ts +60 -0
- package/src/modules/catalog/lib/messageObjectPreviews.ts +176 -0
- package/src/modules/catalog/message-objects.ts +102 -0
- package/src/modules/currencies/backend/currencies/[id]/page.tsx +20 -0
- package/src/modules/currencies/lib/messageObjectPreviews.ts +65 -0
- package/src/modules/currencies/message-objects.ts +40 -0
- package/src/modules/customers/backend/customers/companies/[id]/page.tsx +19 -0
- package/src/modules/customers/backend/customers/deals/[id]/page.tsx +13 -0
- package/src/modules/customers/backend/customers/people/[id]/page.tsx +19 -0
- package/src/modules/customers/components/detail/CompanyHighlights.tsx +14 -9
- package/src/modules/customers/components/detail/PersonHighlights.tsx +14 -9
- package/src/modules/customers/inbox-actions.ts +285 -0
- package/src/modules/customers/lib/messageObjectPreviews.ts +43 -3
- package/src/modules/customers/message-objects.ts +31 -11
- package/src/modules/inbox_ops/api/emails/[id]/route.ts +44 -0
- package/src/modules/inbox_ops/api/extract/route.ts +94 -0
- package/src/modules/inbox_ops/api/proposals/[id]/translate/route.ts +6 -1
- package/src/modules/inbox_ops/api/proposals/counts/route.ts +2 -0
- package/src/modules/inbox_ops/backend/inbox-ops/log/page.meta.ts +2 -2
- package/src/modules/inbox_ops/backend/inbox-ops/log/page.tsx +43 -13
- package/src/modules/inbox_ops/backend/inbox-ops/page.meta.ts +2 -2
- package/src/modules/inbox_ops/backend/inbox-ops/page.tsx +176 -81
- package/src/modules/inbox_ops/backend/inbox-ops/proposals/[id]/page.meta.ts +2 -2
- package/src/modules/inbox_ops/backend/inbox-ops/proposals/[id]/page.tsx +122 -68
- package/src/modules/inbox_ops/backend/inbox-ops/settings/page.meta.ts +2 -2
- package/src/modules/inbox_ops/backend/inbox-ops/settings/page.tsx +36 -14
- package/src/modules/inbox_ops/components/proposals/ActionCard.tsx +91 -7
- package/src/modules/inbox_ops/components/proposals/EditActionDialog.tsx +64 -12
- package/src/modules/inbox_ops/lib/constants.ts +9 -0
- package/src/modules/inbox_ops/lib/contactValidation.ts +54 -0
- package/src/modules/inbox_ops/lib/executionEngine.ts +47 -1060
- package/src/modules/inbox_ops/lib/executionHelpers.ts +527 -0
- package/src/modules/inbox_ops/lib/extractionPrompt.ts +45 -34
- package/src/modules/inbox_ops/lib/inbox-actions-generated.d.ts +11 -0
- package/src/modules/inbox_ops/lib/translationProvider.ts +16 -10
- package/src/modules/inbox_ops/subscribers/extractionWorker.ts +16 -18
- package/src/modules/messages/commands/messages.ts +4 -0
- package/src/modules/messages/components/message-detail/panels/objects-panel.tsx +8 -1
- package/src/modules/messages/components/message-detail/panels/thread-panel.tsx +3 -0
- package/src/modules/messages/frontend/messages/view/[token]/page.tsx +1 -0
- package/src/modules/resources/backend/resources/resources/[id]/page.tsx +20 -4
- package/src/modules/resources/lib/messageObjectPreviews.ts +55 -0
- package/src/modules/resources/message-objects.ts +36 -0
- package/src/modules/sales/backend/sales/channels/[channelId]/edit/page.tsx +18 -0
- package/src/modules/sales/backend/sales/documents/[id]/page.tsx +23 -0
- package/src/modules/sales/backend/sales/quotes/[id]/page.tsx +1 -1
- package/src/modules/sales/inbox-actions.ts +359 -0
- package/src/modules/sales/lib/messageObjectPreviews.ts +54 -4
- package/src/modules/sales/message-objects.ts +44 -2
- package/src/modules/sales/widgets/messages/SalesDocumentMessageDetail.tsx +72 -34
- package/src/modules/sales/widgets/messages/SalesDocumentMessagePreview.tsx +1 -1
- package/src/modules/staff/backend/staff/leave-requests/[id]/page.tsx +7 -29
- package/src/modules/staff/backend/staff/my-availability/page.tsx +14 -0
- package/src/modules/staff/backend/staff/my-leave-requests/[id]/page.tsx +8 -30
- package/src/modules/staff/backend/staff/team-members/[id]/page.tsx +28 -7
- package/src/modules/staff/backend/staff/team-roles/[id]/edit/page.tsx +12 -0
- package/src/modules/staff/backend/staff/teams/[id]/edit/page.tsx +12 -0
- package/src/modules/staff/components/TeamForm.tsx +3 -0
- package/src/modules/staff/components/TeamRoleForm.tsx +3 -0
- package/src/modules/staff/lib/messageObjectPreviews.ts +133 -2
- package/src/modules/staff/message-objects.ts +79 -8
- package/dist/modules/customers/widgets/messages/CustomerMessageObjectDetail.js +0 -51
- package/dist/modules/customers/widgets/messages/CustomerMessageObjectDetail.js.map +0 -7
- package/dist/modules/customers/widgets/messages/CustomerMessageObjectPreview.js +0 -35
- package/dist/modules/customers/widgets/messages/CustomerMessageObjectPreview.js.map +0 -7
- package/dist/modules/customers/widgets/messages/index.js +0 -7
- package/dist/modules/customers/widgets/messages/index.js.map +0 -7
- package/dist/modules/staff/widgets/messages/StaffMessageObjectDetail.js +0 -51
- package/dist/modules/staff/widgets/messages/StaffMessageObjectDetail.js.map +0 -7
- package/dist/modules/staff/widgets/messages/StaffMessageObjectPreview.js +0 -34
- package/dist/modules/staff/widgets/messages/StaffMessageObjectPreview.js.map +0 -7
- package/dist/modules/staff/widgets/messages/index.js +0 -7
- package/dist/modules/staff/widgets/messages/index.js.map +0 -7
- package/src/modules/customers/widgets/messages/CustomerMessageObjectDetail.tsx +0 -57
- package/src/modules/customers/widgets/messages/CustomerMessageObjectPreview.tsx +0 -49
- package/src/modules/customers/widgets/messages/index.ts +0 -2
- package/src/modules/staff/widgets/messages/StaffMessageObjectDetail.tsx +0 -57
- package/src/modules/staff/widgets/messages/StaffMessageObjectPreview.tsx +0 -44
- package/src/modules/staff/widgets/messages/index.ts +0 -2
|
@@ -3,30 +3,13 @@ import type { EntityClass } from '@mikro-orm/core'
|
|
|
3
3
|
import type { AwilixContainer } from 'awilix'
|
|
4
4
|
import type { EventBus } from '@open-mercato/events/types'
|
|
5
5
|
import type { AuthContext } from '@open-mercato/shared/lib/auth/server'
|
|
6
|
-
import type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
|
|
7
6
|
import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
7
|
+
import type { InboxActionExecutionContext } from '@open-mercato/shared/modules/inbox-actions'
|
|
8
8
|
import { InboxProposal, InboxProposalAction, InboxDiscrepancy } from '../data/entities'
|
|
9
9
|
import type { InboxActionStatus, InboxActionType, InboxProposalStatus } from '../data/entities'
|
|
10
|
-
import {
|
|
11
|
-
createContactPayloadSchema,
|
|
12
|
-
createProductPayloadSchema,
|
|
13
|
-
draftReplyPayloadSchema,
|
|
14
|
-
linkContactPayloadSchema,
|
|
15
|
-
logActivityPayloadSchema,
|
|
16
|
-
orderPayloadSchema,
|
|
17
|
-
updateOrderPayloadSchema,
|
|
18
|
-
updateShipmentPayloadSchema,
|
|
19
|
-
type CreateContactPayload,
|
|
20
|
-
type CreateProductPayload,
|
|
21
|
-
type DraftReplyPayload,
|
|
22
|
-
type LinkContactPayload,
|
|
23
|
-
type LogActivityPayload,
|
|
24
|
-
type OrderPayload,
|
|
25
|
-
type UpdateOrderPayload,
|
|
26
|
-
type UpdateShipmentPayload,
|
|
27
|
-
} from '../data/validators'
|
|
28
10
|
import { REQUIRED_FEATURES_MAP } from './constants'
|
|
29
11
|
import { formatZodErrors } from './validation'
|
|
12
|
+
import { ExecutionError, executeCommand } from './executionHelpers'
|
|
30
13
|
|
|
31
14
|
interface CommonEntityFields {
|
|
32
15
|
tenantId?: string
|
|
@@ -70,16 +53,6 @@ interface TypeExecutionResult {
|
|
|
70
53
|
matchedEntityType?: string | null
|
|
71
54
|
}
|
|
72
55
|
|
|
73
|
-
class ExecutionError extends Error {
|
|
74
|
-
statusCode: number
|
|
75
|
-
|
|
76
|
-
constructor(message: string, statusCode = 400) {
|
|
77
|
-
super(message)
|
|
78
|
-
this.statusCode = statusCode
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const SALES_SHIPMENT_STATUS_DICTIONARY_KEY = 'sales.shipment_status'
|
|
83
56
|
const ACTION_EXECUTABLE_STATUSES: InboxActionStatus[] = ['pending', 'failed']
|
|
84
57
|
|
|
85
58
|
export async function executeAction(
|
|
@@ -332,16 +305,12 @@ export async function acceptAllActions(
|
|
|
332
305
|
}
|
|
333
306
|
|
|
334
307
|
/**
|
|
335
|
-
* Normalize LLM
|
|
336
|
-
* Fixes common issues: case-sensitive enums, missing fields that can be resolved,
|
|
337
|
-
* and alternate field names the LLM might use.
|
|
308
|
+
* Normalize common LLM payload issues before action-specific normalization.
|
|
338
309
|
*/
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
):
|
|
343
|
-
const payload = { ...(action.payload as Record<string, unknown>) }
|
|
344
|
-
|
|
310
|
+
function normalizeCommonPayloadFields(
|
|
311
|
+
payload: Record<string, unknown>,
|
|
312
|
+
actionType: string,
|
|
313
|
+
): Record<string, unknown> {
|
|
345
314
|
// Lowercase contact type fields (LLM often outputs "Person" / "Company")
|
|
346
315
|
if (typeof payload.type === 'string') {
|
|
347
316
|
payload.type = payload.type.toLowerCase()
|
|
@@ -350,9 +319,8 @@ async function normalizePayload(
|
|
|
350
319
|
payload.contactType = payload.contactType.toLowerCase()
|
|
351
320
|
}
|
|
352
321
|
|
|
353
|
-
// Normalize link_contact field names (LLM may use various alternatives
|
|
354
|
-
|
|
355
|
-
if (action.actionType === 'link_contact') {
|
|
322
|
+
// Normalize link_contact field names (LLM may use various alternatives)
|
|
323
|
+
if (actionType === 'link_contact') {
|
|
356
324
|
if (!payload.emailAddress) {
|
|
357
325
|
const alt = payload.email ?? payload.contactEmail
|
|
358
326
|
if (typeof alt === 'string') payload.emailAddress = alt
|
|
@@ -371,447 +339,57 @@ async function normalizePayload(
|
|
|
371
339
|
}
|
|
372
340
|
}
|
|
373
341
|
|
|
374
|
-
// Resolve missing currencyCode for order/quote payloads from channel
|
|
375
|
-
if (action.actionType === 'create_order' || action.actionType === 'create_quote') {
|
|
376
|
-
if (!payload.currencyCode) {
|
|
377
|
-
const channelId = typeof payload.channelId === 'string' ? payload.channelId : null
|
|
378
|
-
const resolved = await resolveChannelCurrency(ctx, channelId)
|
|
379
|
-
if (resolved) payload.currencyCode = resolved
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
|
|
383
342
|
return payload
|
|
384
343
|
}
|
|
385
344
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
switch (action.actionType) {
|
|
393
|
-
case 'create_order': {
|
|
394
|
-
const parsed = orderPayloadSchema.safeParse(payload)
|
|
395
|
-
if (!parsed.success) throw new ExecutionError(`Invalid create_order payload: ${formatZodErrors(parsed.error)}`, 400)
|
|
396
|
-
return executeCreateDocumentAction(action, parsed.data, ctx, 'order')
|
|
397
|
-
}
|
|
398
|
-
case 'create_quote': {
|
|
399
|
-
const parsed = orderPayloadSchema.safeParse(payload)
|
|
400
|
-
if (!parsed.success) throw new ExecutionError(`Invalid create_quote payload: ${formatZodErrors(parsed.error)}`, 400)
|
|
401
|
-
return executeCreateDocumentAction(action, parsed.data, ctx, 'quote')
|
|
402
|
-
}
|
|
403
|
-
case 'update_order': {
|
|
404
|
-
const parsed = updateOrderPayloadSchema.safeParse(payload)
|
|
405
|
-
if (!parsed.success) throw new ExecutionError(`Invalid update_order payload: ${formatZodErrors(parsed.error)}`, 400)
|
|
406
|
-
return executeUpdateOrderAction(parsed.data, ctx)
|
|
407
|
-
}
|
|
408
|
-
case 'update_shipment': {
|
|
409
|
-
const parsed = updateShipmentPayloadSchema.safeParse(payload)
|
|
410
|
-
if (!parsed.success) throw new ExecutionError(`Invalid update_shipment payload: ${formatZodErrors(parsed.error)}`, 400)
|
|
411
|
-
return executeUpdateShipmentAction(parsed.data, ctx)
|
|
412
|
-
}
|
|
413
|
-
case 'create_contact': {
|
|
414
|
-
const parsed = createContactPayloadSchema.safeParse(payload)
|
|
415
|
-
if (!parsed.success) throw new ExecutionError(`Invalid create_contact payload: ${formatZodErrors(parsed.error)}`, 400)
|
|
416
|
-
return executeCreateContactAction(parsed.data, ctx)
|
|
417
|
-
}
|
|
418
|
-
case 'create_product': {
|
|
419
|
-
const parsed = createProductPayloadSchema.safeParse(payload)
|
|
420
|
-
if (!parsed.success) throw new ExecutionError(`Invalid create_product payload: ${formatZodErrors(parsed.error)}`, 400)
|
|
421
|
-
return executeCreateProductAction(action, parsed.data, ctx)
|
|
422
|
-
}
|
|
423
|
-
case 'link_contact': {
|
|
424
|
-
const parsed = linkContactPayloadSchema.safeParse(payload)
|
|
425
|
-
if (!parsed.success) throw new ExecutionError(`Invalid link_contact payload: ${formatZodErrors(parsed.error)}`, 400)
|
|
426
|
-
return executeLinkContactAction(parsed.data)
|
|
427
|
-
}
|
|
428
|
-
case 'log_activity': {
|
|
429
|
-
const parsed = logActivityPayloadSchema.safeParse(payload)
|
|
430
|
-
if (!parsed.success) throw new ExecutionError(`Invalid log_activity payload: ${formatZodErrors(parsed.error)}`, 400)
|
|
431
|
-
return executeLogActivityAction(parsed.data, ctx)
|
|
432
|
-
}
|
|
433
|
-
case 'draft_reply': {
|
|
434
|
-
const parsed = draftReplyPayloadSchema.safeParse(payload)
|
|
435
|
-
if (!parsed.success) throw new ExecutionError(`Invalid draft_reply payload: ${formatZodErrors(parsed.error)}`, 400)
|
|
436
|
-
return executeDraftReplyAction(action, parsed.data, ctx)
|
|
437
|
-
}
|
|
438
|
-
default:
|
|
439
|
-
throw new ExecutionError(`Unknown action type: ${action.actionType}`, 400)
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
async function executeCreateDocumentAction(
|
|
444
|
-
action: InboxProposalAction,
|
|
445
|
-
payload: OrderPayload,
|
|
446
|
-
ctx: ExecutionContext,
|
|
447
|
-
kind: 'order' | 'quote',
|
|
448
|
-
): Promise<TypeExecutionResult> {
|
|
449
|
-
// Resolve channelId if not provided
|
|
450
|
-
let resolvedChannelId: string | undefined = payload.channelId
|
|
451
|
-
if (!resolvedChannelId) {
|
|
452
|
-
resolvedChannelId = (await resolveFirstChannelId(ctx)) ?? undefined
|
|
453
|
-
if (!resolvedChannelId) {
|
|
454
|
-
throw new ExecutionError('No sales channel available. Create a channel first or set channelId in the payload.', 400)
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
const currencyCode = payload.currencyCode.trim().toUpperCase()
|
|
459
|
-
const lines = payload.lineItems.map((line, index) => {
|
|
460
|
-
const quantity = parseNumberToken(line.quantity, `lineItems[${index}].quantity`)
|
|
461
|
-
const unitPrice = line.unitPrice
|
|
462
|
-
? parseNumberToken(line.unitPrice, `lineItems[${index}].unitPrice`)
|
|
463
|
-
: undefined
|
|
464
|
-
|
|
465
|
-
const mappedLine: Record<string, unknown> = {
|
|
466
|
-
lineNumber: index + 1,
|
|
467
|
-
kind: line.kind ?? (line.productId ? 'product' : 'service'),
|
|
468
|
-
name: line.productName,
|
|
469
|
-
description: line.description,
|
|
470
|
-
quantity,
|
|
471
|
-
currencyCode,
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
if (line.productId) mappedLine.productId = line.productId
|
|
475
|
-
if (line.variantId) mappedLine.productVariantId = line.variantId
|
|
476
|
-
if (unitPrice !== undefined) mappedLine.unitPriceNet = unitPrice
|
|
477
|
-
if (line.sku || line.catalogPrice) {
|
|
478
|
-
mappedLine.catalogSnapshot = {
|
|
479
|
-
sku: line.sku ?? null,
|
|
480
|
-
catalogPrice: line.catalogPrice ?? null,
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
return mappedLine
|
|
485
|
-
})
|
|
486
|
-
|
|
487
|
-
const metadata = buildSourceMetadata(action.id, action.proposalId)
|
|
488
|
-
|
|
489
|
-
// Resolve customerEntityId: use explicit ID, or look up by email (contact may
|
|
490
|
-
// have been created by a prior action in the same proposal batch)
|
|
491
|
-
let resolvedCustomerEntityId = payload.customerEntityId
|
|
492
|
-
if (!resolvedCustomerEntityId && payload.customerEmail) {
|
|
493
|
-
resolvedCustomerEntityId = (await resolveCustomerEntityIdByEmail(ctx, payload.customerEmail)) ?? undefined
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
const createInput: Record<string, unknown> = {
|
|
497
|
-
organizationId: ctx.organizationId,
|
|
498
|
-
tenantId: ctx.tenantId,
|
|
499
|
-
customerEntityId: resolvedCustomerEntityId,
|
|
500
|
-
customerReference: payload.customerReference,
|
|
501
|
-
channelId: resolvedChannelId,
|
|
502
|
-
currencyCode,
|
|
503
|
-
taxRateId: payload.taxRateId,
|
|
504
|
-
comments: payload.notes,
|
|
505
|
-
metadata,
|
|
506
|
-
lines,
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
// Only provide a manual customerSnapshot when no entity could be resolved.
|
|
510
|
-
// When customerEntityId is set, the sales command builds the proper nested
|
|
511
|
-
// snapshot ({ customer: {...}, contact: {...} }) from the entity itself.
|
|
512
|
-
if (!resolvedCustomerEntityId) {
|
|
513
|
-
createInput.customerSnapshot = {
|
|
514
|
-
displayName: payload.customerName,
|
|
515
|
-
...(payload.customerEmail && { primaryEmail: payload.customerEmail }),
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
// Address resolution: explicit address from email > addressId from CRM enrichment
|
|
520
|
-
const normalizedBilling = payload.billingAddress
|
|
521
|
-
? normalizeAddressSnapshot(payload.billingAddress)
|
|
522
|
-
: undefined
|
|
523
|
-
const normalizedShipping = payload.shippingAddress
|
|
524
|
-
? normalizeAddressSnapshot(payload.shippingAddress)
|
|
525
|
-
: undefined
|
|
526
|
-
|
|
527
|
-
if (normalizedShipping || normalizedBilling) {
|
|
528
|
-
createInput.shippingAddressSnapshot = normalizedShipping ?? normalizedBilling
|
|
529
|
-
createInput.billingAddressSnapshot = normalizedBilling ?? normalizedShipping
|
|
530
|
-
} else if (payload.billingAddressId || payload.shippingAddressId) {
|
|
531
|
-
createInput.billingAddressId = payload.billingAddressId ?? payload.shippingAddressId
|
|
532
|
-
createInput.shippingAddressId = payload.shippingAddressId ?? payload.billingAddressId
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
const requestedDeliveryAt = parseDateToken(payload.requestedDeliveryDate ?? undefined)
|
|
536
|
-
if (requestedDeliveryAt) {
|
|
537
|
-
createInput.expectedDeliveryAt = requestedDeliveryAt
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
const effectiveKind = kind === 'order'
|
|
541
|
-
? await resolveEffectiveDocumentKind(ctx, resolvedChannelId)
|
|
542
|
-
: kind
|
|
543
|
-
|
|
544
|
-
if (effectiveKind === 'order') {
|
|
545
|
-
const result = await executeCommand<Record<string, unknown>, { orderId?: string }>(
|
|
546
|
-
ctx,
|
|
547
|
-
'sales.orders.create',
|
|
548
|
-
createInput,
|
|
549
|
-
)
|
|
550
|
-
if (!result.orderId) {
|
|
551
|
-
throw new ExecutionError('Order creation did not return an order ID', 500)
|
|
552
|
-
}
|
|
553
|
-
return {
|
|
554
|
-
createdEntityId: result.orderId,
|
|
555
|
-
createdEntityType: 'sales_order',
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
const result = await executeCommand<Record<string, unknown>, { quoteId?: string }>(
|
|
560
|
-
ctx,
|
|
561
|
-
'sales.quotes.create',
|
|
562
|
-
createInput,
|
|
563
|
-
)
|
|
564
|
-
if (!result.quoteId) {
|
|
565
|
-
throw new ExecutionError('Quote creation did not return a quote ID', 500)
|
|
566
|
-
}
|
|
345
|
+
/**
|
|
346
|
+
* Adapt the internal ExecutionContext to the shared InboxActionExecutionContext
|
|
347
|
+
* for use by registered action handlers.
|
|
348
|
+
*/
|
|
349
|
+
function adaptContext(ctx: ExecutionContext): InboxActionExecutionContext {
|
|
567
350
|
return {
|
|
568
|
-
|
|
569
|
-
|
|
351
|
+
...ctx,
|
|
352
|
+
executeCommand: <TInput, TResult>(commandId: string, input: TInput) =>
|
|
353
|
+
executeCommand<TInput, TResult>(ctx, commandId, input),
|
|
354
|
+
resolveEntityClass: <T>(key: string) =>
|
|
355
|
+
resolveEntityClassInternal(ctx, key) as (new (...args: unknown[]) => T) | null,
|
|
570
356
|
}
|
|
571
357
|
}
|
|
572
358
|
|
|
573
|
-
async function
|
|
574
|
-
|
|
359
|
+
async function executeByType(
|
|
360
|
+
action: InboxProposalAction,
|
|
575
361
|
ctx: ExecutionContext,
|
|
576
362
|
): Promise<TypeExecutionResult> {
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
const updateInput: Record<string, unknown> = {
|
|
584
|
-
id: order.id,
|
|
585
|
-
organizationId: ctx.organizationId,
|
|
586
|
-
tenantId: ctx.tenantId,
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
const newDeliveryDate = parseDateToken(payload.deliveryDateChange?.newDate)
|
|
590
|
-
if (newDeliveryDate) {
|
|
591
|
-
updateInput.expectedDeliveryAt = newDeliveryDate
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
const noteLines = payload.noteAdditions?.map((note) => note.trim()).filter((note) => note.length > 0) ?? []
|
|
595
|
-
if (noteLines.length > 0) {
|
|
596
|
-
const mergedNotes = [order.comments ?? null, ...noteLines].filter(Boolean).join('\n')
|
|
597
|
-
updateInput.comments = mergedNotes
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
if (Object.keys(updateInput).length > 3) {
|
|
601
|
-
await executeCommand<Record<string, unknown>, { orderId?: string }>(
|
|
602
|
-
ctx,
|
|
603
|
-
'sales.orders.update',
|
|
604
|
-
updateInput,
|
|
605
|
-
)
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
const quantityChanges = payload.quantityChanges ?? []
|
|
609
|
-
const orderLines = quantityChanges.length > 0 && quantityChanges.some((qc) => !qc.lineItemId)
|
|
610
|
-
? await loadOrderLineItems(ctx, order.id)
|
|
611
|
-
: []
|
|
612
|
-
|
|
613
|
-
for (const quantityChange of quantityChanges) {
|
|
614
|
-
let lineItemId = quantityChange.lineItemId
|
|
615
|
-
if (!lineItemId) {
|
|
616
|
-
const matched = matchLineItemByName(orderLines, quantityChange.lineItemName)
|
|
617
|
-
if (matched) {
|
|
618
|
-
lineItemId = matched
|
|
619
|
-
} else {
|
|
620
|
-
const availableNames = orderLines.map((l) => l.name).filter(Boolean).join(', ')
|
|
621
|
-
throw new ExecutionError(
|
|
622
|
-
`Cannot resolve line item "${quantityChange.lineItemName}". Available line items: ${availableNames || 'none'}`,
|
|
623
|
-
400,
|
|
624
|
-
)
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
await executeCommand<{ body: Record<string, unknown> }, { orderId?: string; lineId?: string }>(
|
|
629
|
-
ctx,
|
|
630
|
-
'sales.orders.lines.upsert',
|
|
631
|
-
{
|
|
632
|
-
body: {
|
|
633
|
-
id: lineItemId,
|
|
634
|
-
orderId: order.id,
|
|
635
|
-
organizationId: ctx.organizationId,
|
|
636
|
-
tenantId: ctx.tenantId,
|
|
637
|
-
quantity: parseNumberToken(quantityChange.newQuantity, 'quantityChanges.newQuantity'),
|
|
638
|
-
currencyCode: order.currencyCode,
|
|
639
|
-
},
|
|
640
|
-
},
|
|
641
|
-
)
|
|
363
|
+
// Lazy-load the generated registry to avoid circular imports at module load time
|
|
364
|
+
const { getInboxAction } = await import('@/.mercato/generated/inbox-actions.generated')
|
|
365
|
+
const definition = getInboxAction(action.actionType)
|
|
366
|
+
if (!definition) {
|
|
367
|
+
throw new ExecutionError(`Unknown action type: ${action.actionType}`, 400)
|
|
642
368
|
}
|
|
643
369
|
|
|
644
|
-
|
|
645
|
-
createdEntityId: order.id,
|
|
646
|
-
createdEntityType: 'sales_order',
|
|
647
|
-
}
|
|
648
|
-
}
|
|
370
|
+
let payload = { ...(action.payload as Record<string, unknown>) }
|
|
649
371
|
|
|
650
|
-
|
|
651
|
-
payload
|
|
652
|
-
ctx: ExecutionContext,
|
|
653
|
-
): Promise<TypeExecutionResult> {
|
|
654
|
-
const order = await resolveOrderByReference(
|
|
655
|
-
ctx,
|
|
656
|
-
payload.orderId,
|
|
657
|
-
payload.orderNumber,
|
|
658
|
-
)
|
|
372
|
+
// Common normalization (lowercase enums, field aliases)
|
|
373
|
+
payload = normalizeCommonPayloadFields(payload, action.actionType)
|
|
659
374
|
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
375
|
+
// Action-specific normalization from the registered handler
|
|
376
|
+
const actionCtx = adaptContext(ctx)
|
|
377
|
+
if (definition.normalizePayload) {
|
|
378
|
+
payload = await definition.normalizePayload(payload, actionCtx)
|
|
663
379
|
}
|
|
664
380
|
|
|
665
|
-
const
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
tenantId: ctx.tenantId,
|
|
671
|
-
organizationId: ctx.organizationId,
|
|
672
|
-
deletedAt: null,
|
|
673
|
-
},
|
|
674
|
-
{ orderBy: { createdAt: 'DESC' } },
|
|
675
|
-
{ tenantId: ctx.tenantId, organizationId: ctx.organizationId },
|
|
676
|
-
)
|
|
677
|
-
|
|
678
|
-
if (!shipment) {
|
|
679
|
-
throw new ExecutionError('No shipment found for the referenced order', 404)
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
const statusEntryId = await resolveShipmentStatusEntryId(
|
|
683
|
-
ctx,
|
|
684
|
-
payload.statusLabel,
|
|
685
|
-
)
|
|
686
|
-
if (!statusEntryId) {
|
|
687
|
-
throw new ExecutionError(`Shipment status "${payload.statusLabel}" not found`, 400)
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
const updateInput: Record<string, unknown> = {
|
|
691
|
-
id: shipment.id,
|
|
692
|
-
orderId: order.id,
|
|
693
|
-
organizationId: ctx.organizationId,
|
|
694
|
-
tenantId: ctx.tenantId,
|
|
695
|
-
statusEntryId,
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
if (payload.trackingNumbers) updateInput.trackingNumbers = payload.trackingNumbers
|
|
699
|
-
if (payload.carrierName) updateInput.carrierName = payload.carrierName
|
|
700
|
-
if (payload.notes) updateInput.notes = payload.notes
|
|
701
|
-
|
|
702
|
-
const shippedAt = parseDateToken(payload.shippedAt)
|
|
703
|
-
const deliveredAt = parseDateToken(payload.deliveredAt)
|
|
704
|
-
if (shippedAt) updateInput.shippedAt = shippedAt
|
|
705
|
-
if (deliveredAt) updateInput.deliveredAt = deliveredAt
|
|
706
|
-
|
|
707
|
-
await executeCommand<Record<string, unknown>, { shipmentId?: string }>(
|
|
708
|
-
ctx,
|
|
709
|
-
'sales.shipments.update',
|
|
710
|
-
updateInput,
|
|
711
|
-
)
|
|
712
|
-
|
|
713
|
-
return {
|
|
714
|
-
createdEntityId: shipment.id,
|
|
715
|
-
createdEntityType: 'sales_shipment',
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
async function executeCreateContactAction(
|
|
720
|
-
payload: CreateContactPayload,
|
|
721
|
-
ctx: ExecutionContext,
|
|
722
|
-
): Promise<TypeExecutionResult> {
|
|
723
|
-
const CustomerEntityClass = resolveEntityClass(ctx, 'CustomerEntity')
|
|
724
|
-
if (payload.email && CustomerEntityClass) {
|
|
725
|
-
const emailLower = payload.email.trim().toLowerCase()
|
|
726
|
-
// Try direct DB lookup first (works when primaryEmail is not encrypted)
|
|
727
|
-
let existingContact = await findOneWithDecryption(
|
|
728
|
-
ctx.em,
|
|
729
|
-
CustomerEntityClass,
|
|
730
|
-
{
|
|
731
|
-
primaryEmail: emailLower,
|
|
732
|
-
tenantId: ctx.tenantId,
|
|
733
|
-
organizationId: ctx.organizationId,
|
|
734
|
-
deletedAt: null,
|
|
735
|
-
},
|
|
736
|
-
undefined,
|
|
737
|
-
{ tenantId: ctx.tenantId, organizationId: ctx.organizationId },
|
|
738
|
-
)
|
|
739
|
-
// Fallback: in-memory email check for encrypted primaryEmail fields
|
|
740
|
-
if (!existingContact) {
|
|
741
|
-
const candidates = await findWithDecryption(
|
|
742
|
-
ctx.em,
|
|
743
|
-
CustomerEntityClass,
|
|
744
|
-
{
|
|
745
|
-
tenantId: ctx.tenantId,
|
|
746
|
-
organizationId: ctx.organizationId,
|
|
747
|
-
deletedAt: null,
|
|
748
|
-
},
|
|
749
|
-
{ limit: 100, orderBy: { createdAt: 'DESC' } },
|
|
750
|
-
{ tenantId: ctx.tenantId, organizationId: ctx.organizationId },
|
|
751
|
-
)
|
|
752
|
-
existingContact = candidates.find(
|
|
753
|
-
(e) => e.primaryEmail && e.primaryEmail.toLowerCase() === emailLower,
|
|
754
|
-
) ?? null
|
|
755
|
-
}
|
|
756
|
-
if (existingContact) {
|
|
757
|
-
const isCompany = existingContact.kind === 'company'
|
|
758
|
-
return {
|
|
759
|
-
createdEntityId: existingContact.id,
|
|
760
|
-
createdEntityType: isCompany ? 'customer_company' : 'customer_person',
|
|
761
|
-
matchedEntityId: existingContact.id,
|
|
762
|
-
matchedEntityType: isCompany ? 'company' : 'person',
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
if (payload.type === 'company') {
|
|
768
|
-
const result = await executeCommand<Record<string, unknown>, { entityId?: string }>(
|
|
769
|
-
ctx,
|
|
770
|
-
'customers.companies.create',
|
|
771
|
-
{
|
|
772
|
-
organizationId: ctx.organizationId,
|
|
773
|
-
tenantId: ctx.tenantId,
|
|
774
|
-
displayName: payload.name,
|
|
775
|
-
legalName: payload.companyName ?? payload.name,
|
|
776
|
-
primaryEmail: payload.email,
|
|
777
|
-
primaryPhone: payload.phone,
|
|
778
|
-
source: payload.source,
|
|
779
|
-
},
|
|
381
|
+
const parsed = definition.payloadSchema.safeParse(payload)
|
|
382
|
+
if (!parsed.success) {
|
|
383
|
+
throw new ExecutionError(
|
|
384
|
+
`Invalid ${action.actionType} payload: ${formatZodErrors(parsed.error)}`,
|
|
385
|
+
400,
|
|
780
386
|
)
|
|
781
|
-
if (!result.entityId) {
|
|
782
|
-
throw new ExecutionError('Company creation did not return an entity ID', 500)
|
|
783
|
-
}
|
|
784
|
-
return {
|
|
785
|
-
createdEntityId: result.entityId,
|
|
786
|
-
createdEntityType: 'customer_company',
|
|
787
|
-
}
|
|
788
387
|
}
|
|
789
388
|
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
'customers.people.create',
|
|
794
|
-
{
|
|
795
|
-
organizationId: ctx.organizationId,
|
|
796
|
-
tenantId: ctx.tenantId,
|
|
797
|
-
displayName: payload.name,
|
|
798
|
-
firstName,
|
|
799
|
-
lastName,
|
|
800
|
-
primaryEmail: payload.email,
|
|
801
|
-
primaryPhone: payload.phone,
|
|
802
|
-
jobTitle: payload.role,
|
|
803
|
-
source: payload.source,
|
|
804
|
-
},
|
|
389
|
+
return definition.execute(
|
|
390
|
+
{ id: action.id, proposalId: action.proposalId, payload: parsed.data },
|
|
391
|
+
actionCtx,
|
|
805
392
|
)
|
|
806
|
-
|
|
807
|
-
if (!result.entityId) {
|
|
808
|
-
throw new ExecutionError('Person creation did not return an entity ID', 500)
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
return {
|
|
812
|
-
createdEntityId: result.entityId,
|
|
813
|
-
createdEntityType: 'customer_person',
|
|
814
|
-
}
|
|
815
393
|
}
|
|
816
394
|
|
|
817
395
|
async function resolveUnknownContactDiscrepanciesInProposal(
|
|
@@ -851,232 +429,6 @@ async function resolveUnknownContactDiscrepanciesInProposal(
|
|
|
851
429
|
}
|
|
852
430
|
}
|
|
853
431
|
|
|
854
|
-
async function executeCreateProductAction(
|
|
855
|
-
action: InboxProposalAction,
|
|
856
|
-
payload: CreateProductPayload,
|
|
857
|
-
ctx: ExecutionContext,
|
|
858
|
-
): Promise<TypeExecutionResult> {
|
|
859
|
-
const createInput: Record<string, unknown> = {
|
|
860
|
-
organizationId: ctx.organizationId,
|
|
861
|
-
tenantId: ctx.tenantId,
|
|
862
|
-
title: payload.title,
|
|
863
|
-
productType: 'simple',
|
|
864
|
-
isActive: true,
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
if (payload.sku) createInput.sku = payload.sku
|
|
868
|
-
if (payload.description) createInput.description = payload.description
|
|
869
|
-
if (payload.currencyCode) createInput.primaryCurrencyCode = payload.currencyCode
|
|
870
|
-
|
|
871
|
-
const result = await executeCommand<Record<string, unknown>, { productId?: string }>(
|
|
872
|
-
ctx,
|
|
873
|
-
'catalog.products.create',
|
|
874
|
-
createInput,
|
|
875
|
-
)
|
|
876
|
-
|
|
877
|
-
if (!result.productId) {
|
|
878
|
-
throw new ExecutionError('Product creation did not return a product ID', 500)
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
await resolveProductDiscrepanciesInProposal(ctx.em, action.proposalId, payload.title, result.productId, {
|
|
882
|
-
tenantId: ctx.tenantId,
|
|
883
|
-
organizationId: ctx.organizationId,
|
|
884
|
-
})
|
|
885
|
-
|
|
886
|
-
return {
|
|
887
|
-
createdEntityId: result.productId,
|
|
888
|
-
createdEntityType: 'catalog_product',
|
|
889
|
-
}
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
async function resolveProductDiscrepanciesInProposal(
|
|
893
|
-
em: EntityManager,
|
|
894
|
-
proposalId: string,
|
|
895
|
-
productTitle: string,
|
|
896
|
-
productId: string,
|
|
897
|
-
scope: { tenantId: string; organizationId: string },
|
|
898
|
-
): Promise<void> {
|
|
899
|
-
const discrepancies = await findWithDecryption(
|
|
900
|
-
em,
|
|
901
|
-
InboxDiscrepancy,
|
|
902
|
-
{
|
|
903
|
-
proposalId,
|
|
904
|
-
type: 'product_not_found',
|
|
905
|
-
resolved: false,
|
|
906
|
-
tenantId: scope.tenantId,
|
|
907
|
-
organizationId: scope.organizationId,
|
|
908
|
-
},
|
|
909
|
-
undefined,
|
|
910
|
-
scope,
|
|
911
|
-
)
|
|
912
|
-
|
|
913
|
-
const normalizedTitle = productTitle.toLowerCase().trim()
|
|
914
|
-
const matchingDiscrepancies = discrepancies.filter((d) => {
|
|
915
|
-
const foundValue = (d.foundValue || '').toLowerCase().trim()
|
|
916
|
-
return foundValue === normalizedTitle
|
|
917
|
-
})
|
|
918
|
-
|
|
919
|
-
if (matchingDiscrepancies.length === 0) return
|
|
920
|
-
|
|
921
|
-
// Phase 1: flush scalar mutations before any queries to avoid UoW tracking loss (SPEC-018)
|
|
922
|
-
for (const discrepancy of matchingDiscrepancies) {
|
|
923
|
-
discrepancy.resolved = true
|
|
924
|
-
}
|
|
925
|
-
await em.flush()
|
|
926
|
-
|
|
927
|
-
// Phase 2: update line item product IDs (involves findOneWithDecryption queries)
|
|
928
|
-
const actionIds = matchingDiscrepancies
|
|
929
|
-
.map((d) => d.actionId)
|
|
930
|
-
.filter((id): id is string => !!id)
|
|
931
|
-
|
|
932
|
-
for (const actionId of actionIds) {
|
|
933
|
-
await updateLineItemProductId(em, actionId, normalizedTitle, productId, scope)
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
if (actionIds.length > 0) {
|
|
937
|
-
await em.flush()
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
async function updateLineItemProductId(
|
|
942
|
-
em: EntityManager,
|
|
943
|
-
actionId: string,
|
|
944
|
-
productName: string,
|
|
945
|
-
productId: string,
|
|
946
|
-
scope: { tenantId: string; organizationId: string },
|
|
947
|
-
): Promise<void> {
|
|
948
|
-
const action = await findOneWithDecryption(
|
|
949
|
-
em,
|
|
950
|
-
InboxProposalAction,
|
|
951
|
-
{ id: actionId, deletedAt: null },
|
|
952
|
-
undefined,
|
|
953
|
-
scope,
|
|
954
|
-
)
|
|
955
|
-
if (!action) return
|
|
956
|
-
|
|
957
|
-
const payload = action.payload as Record<string, unknown>
|
|
958
|
-
const lineItems = Array.isArray(payload?.lineItems)
|
|
959
|
-
? (payload.lineItems as Record<string, unknown>[])
|
|
960
|
-
: []
|
|
961
|
-
|
|
962
|
-
let updated = false
|
|
963
|
-
for (const item of lineItems) {
|
|
964
|
-
if (item.productId) continue
|
|
965
|
-
const itemName = (typeof item.productName === 'string' ? item.productName : '').toLowerCase().trim()
|
|
966
|
-
if (itemName === productName) {
|
|
967
|
-
item.productId = productId
|
|
968
|
-
updated = true
|
|
969
|
-
break
|
|
970
|
-
}
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
if (updated) {
|
|
974
|
-
action.payload = { ...payload, lineItems }
|
|
975
|
-
}
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
function executeLinkContactAction(payload: LinkContactPayload): TypeExecutionResult {
|
|
979
|
-
return {
|
|
980
|
-
createdEntityId: payload.contactId,
|
|
981
|
-
createdEntityType: payload.contactType === 'company' ? 'customer_company' : 'customer_person',
|
|
982
|
-
matchedEntityId: payload.contactId,
|
|
983
|
-
matchedEntityType: payload.contactType,
|
|
984
|
-
}
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
async function executeLogActivityAction(
|
|
988
|
-
payload: LogActivityPayload,
|
|
989
|
-
ctx: ExecutionContext,
|
|
990
|
-
): Promise<TypeExecutionResult> {
|
|
991
|
-
if (!payload.contactId) {
|
|
992
|
-
const resolved = await resolveContactIdByNameAndType(ctx, payload.contactName, payload.contactType)
|
|
993
|
-
if (resolved) {
|
|
994
|
-
payload = { ...payload, contactId: resolved }
|
|
995
|
-
} else {
|
|
996
|
-
throw new ExecutionError(
|
|
997
|
-
`log_activity requires contactId — could not resolve contact "${payload.contactName}" (${payload.contactType})`,
|
|
998
|
-
400,
|
|
999
|
-
)
|
|
1000
|
-
}
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
const result = await executeCommand<Record<string, unknown>, { activityId?: string }>(
|
|
1004
|
-
ctx,
|
|
1005
|
-
'customers.activities.create',
|
|
1006
|
-
{
|
|
1007
|
-
organizationId: ctx.organizationId,
|
|
1008
|
-
tenantId: ctx.tenantId,
|
|
1009
|
-
entityId: payload.contactId,
|
|
1010
|
-
activityType: payload.activityType,
|
|
1011
|
-
subject: payload.subject,
|
|
1012
|
-
body: payload.body,
|
|
1013
|
-
authorUserId: ctx.userId,
|
|
1014
|
-
},
|
|
1015
|
-
)
|
|
1016
|
-
|
|
1017
|
-
if (!result.activityId) {
|
|
1018
|
-
throw new ExecutionError('Activity creation did not return an activity ID', 500)
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
return {
|
|
1022
|
-
createdEntityId: result.activityId,
|
|
1023
|
-
createdEntityType: 'customer_activity',
|
|
1024
|
-
}
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
async function executeDraftReplyAction(
|
|
1028
|
-
action: InboxProposalAction,
|
|
1029
|
-
payload: DraftReplyPayload,
|
|
1030
|
-
ctx: ExecutionContext,
|
|
1031
|
-
): Promise<TypeExecutionResult> {
|
|
1032
|
-
const payloadRecord = action.payload as Record<string, unknown>
|
|
1033
|
-
const explicitContactId = typeof payloadRecord.contactId === 'string' ? payloadRecord.contactId : null
|
|
1034
|
-
const contactId = explicitContactId ?? (await resolveCustomerEntityIdByEmail(ctx, payload.to))
|
|
1035
|
-
|
|
1036
|
-
if (!contactId) {
|
|
1037
|
-
throw new ExecutionError(
|
|
1038
|
-
`No matching contact found for "${payload.to}". Create the contact first or link an existing one.`,
|
|
1039
|
-
400,
|
|
1040
|
-
)
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
const details = [
|
|
1044
|
-
payload.body.trim(),
|
|
1045
|
-
'',
|
|
1046
|
-
'---',
|
|
1047
|
-
`Draft reply target: ${payload.to}`,
|
|
1048
|
-
`Subject: ${payload.subject}`,
|
|
1049
|
-
payload.context ? `Context: ${payload.context}` : null,
|
|
1050
|
-
`InboxOps Proposal: ${action.proposalId}`,
|
|
1051
|
-
`InboxOps Action: ${action.id}`,
|
|
1052
|
-
]
|
|
1053
|
-
.filter((line) => typeof line === 'string' && line.length > 0)
|
|
1054
|
-
.join('\n')
|
|
1055
|
-
|
|
1056
|
-
const result = await executeCommand<Record<string, unknown>, { activityId?: string }>(
|
|
1057
|
-
ctx,
|
|
1058
|
-
'customers.activities.create',
|
|
1059
|
-
{
|
|
1060
|
-
organizationId: ctx.organizationId,
|
|
1061
|
-
tenantId: ctx.tenantId,
|
|
1062
|
-
entityId: contactId,
|
|
1063
|
-
activityType: 'email',
|
|
1064
|
-
subject: payload.subject,
|
|
1065
|
-
body: details,
|
|
1066
|
-
authorUserId: ctx.userId,
|
|
1067
|
-
},
|
|
1068
|
-
)
|
|
1069
|
-
|
|
1070
|
-
if (!result.activityId) {
|
|
1071
|
-
throw new ExecutionError('Draft reply activity did not return an activity ID', 500)
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
|
-
return {
|
|
1075
|
-
createdEntityId: result.activityId,
|
|
1076
|
-
createdEntityType: 'customer_activity',
|
|
1077
|
-
}
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
432
|
async function ensureUserCanExecuteAction(action: InboxProposalAction, ctx: ExecutionContext): Promise<void> {
|
|
1081
433
|
const requiredFeature = getRequiredFeatureForAction(action)
|
|
1082
434
|
if (!requiredFeature) return
|
|
@@ -1104,382 +456,17 @@ async function ensureUserCanExecuteAction(action: InboxProposalAction, ctx: Exec
|
|
|
1104
456
|
}
|
|
1105
457
|
}
|
|
1106
458
|
|
|
1107
|
-
|
|
459
|
+
function resolveEntityClassInternal(
|
|
1108
460
|
ctx: ExecutionContext,
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
const commandBus = ctx.container.resolve('commandBus') as CommandBus
|
|
1113
|
-
if (!commandBus || typeof commandBus.execute !== 'function') {
|
|
1114
|
-
throw new ExecutionError('Command bus is not available', 503)
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
|
-
const auth =
|
|
1118
|
-
ctx.auth ??
|
|
1119
|
-
({
|
|
1120
|
-
sub: ctx.userId,
|
|
1121
|
-
userId: ctx.userId,
|
|
1122
|
-
tenantId: ctx.tenantId,
|
|
1123
|
-
orgId: ctx.organizationId,
|
|
1124
|
-
isSuperAdmin: false,
|
|
1125
|
-
} satisfies Exclude<AuthContext, null>)
|
|
1126
|
-
|
|
1127
|
-
const commandContext: CommandRuntimeContext = {
|
|
1128
|
-
container: ctx.container,
|
|
1129
|
-
auth,
|
|
1130
|
-
organizationScope: null,
|
|
1131
|
-
selectedOrganizationId: ctx.organizationId,
|
|
1132
|
-
organizationIds: [ctx.organizationId],
|
|
1133
|
-
}
|
|
1134
|
-
|
|
1135
|
-
const { result } = await commandBus.execute<TInput, TResult>(commandId, {
|
|
1136
|
-
input,
|
|
1137
|
-
ctx: commandContext,
|
|
1138
|
-
})
|
|
1139
|
-
|
|
1140
|
-
return result
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
|
-
function buildSourceMetadata(actionId: string, proposalId: string): Record<string, unknown> {
|
|
1144
|
-
return {
|
|
1145
|
-
source: 'inbox_ops',
|
|
1146
|
-
inboxOpsActionId: actionId,
|
|
1147
|
-
inboxOpsProposalId: proposalId,
|
|
1148
|
-
}
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1151
|
-
async function resolveOrderByReference(
|
|
1152
|
-
ctx: ExecutionContext,
|
|
1153
|
-
orderId?: string,
|
|
1154
|
-
orderNumber?: string,
|
|
1155
|
-
): Promise<{ id: string; orderNumber: string; currencyCode: string; comments?: string | null }> {
|
|
1156
|
-
const SalesOrderClass = resolveEntityClass(ctx, 'SalesOrder')
|
|
1157
|
-
if (!SalesOrderClass) {
|
|
1158
|
-
throw new ExecutionError('Sales module entities not available', 503)
|
|
1159
|
-
}
|
|
1160
|
-
|
|
1161
|
-
const where: Record<string, unknown> = {
|
|
1162
|
-
tenantId: ctx.tenantId,
|
|
1163
|
-
organizationId: ctx.organizationId,
|
|
1164
|
-
deletedAt: null,
|
|
1165
|
-
}
|
|
1166
|
-
if (orderId) {
|
|
1167
|
-
where.id = orderId
|
|
1168
|
-
} else if (orderNumber && orderNumber.trim().length > 0) {
|
|
1169
|
-
where.orderNumber = orderNumber.trim()
|
|
1170
|
-
} else {
|
|
1171
|
-
throw new ExecutionError('Order reference is required', 400)
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
const order = await findOneWithDecryption(
|
|
1175
|
-
ctx.em,
|
|
1176
|
-
SalesOrderClass,
|
|
1177
|
-
where,
|
|
1178
|
-
undefined,
|
|
1179
|
-
{ tenantId: ctx.tenantId, organizationId: ctx.organizationId },
|
|
1180
|
-
)
|
|
1181
|
-
if (!order) {
|
|
1182
|
-
throw new ExecutionError('Referenced order not found', 404)
|
|
1183
|
-
}
|
|
1184
|
-
return order
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
|
-
async function resolveShipmentStatusEntryId(
|
|
1188
|
-
ctx: ExecutionContext,
|
|
1189
|
-
statusLabel: string,
|
|
1190
|
-
): Promise<string | null> {
|
|
1191
|
-
const DictionaryClass = resolveEntityClass(ctx, 'Dictionary')
|
|
1192
|
-
const DictionaryEntryClass = resolveEntityClass(ctx, 'DictionaryEntry')
|
|
1193
|
-
if (!DictionaryClass || !DictionaryEntryClass) return null
|
|
1194
|
-
|
|
1195
|
-
const encryptionScope = { tenantId: ctx.tenantId, organizationId: ctx.organizationId }
|
|
1196
|
-
|
|
1197
|
-
const dictionary = await findOneWithDecryption(
|
|
1198
|
-
ctx.em,
|
|
1199
|
-
DictionaryClass,
|
|
1200
|
-
{
|
|
1201
|
-
key: SALES_SHIPMENT_STATUS_DICTIONARY_KEY,
|
|
1202
|
-
tenantId: ctx.tenantId,
|
|
1203
|
-
organizationId: ctx.organizationId,
|
|
1204
|
-
deletedAt: null,
|
|
1205
|
-
},
|
|
1206
|
-
undefined,
|
|
1207
|
-
encryptionScope,
|
|
1208
|
-
)
|
|
1209
|
-
if (!dictionary) return null
|
|
1210
|
-
|
|
1211
|
-
const entries = await findWithDecryption(
|
|
1212
|
-
ctx.em,
|
|
1213
|
-
DictionaryEntryClass,
|
|
1214
|
-
{
|
|
1215
|
-
dictionary: dictionary.id,
|
|
1216
|
-
tenantId: ctx.tenantId,
|
|
1217
|
-
organizationId: ctx.organizationId,
|
|
1218
|
-
},
|
|
1219
|
-
undefined,
|
|
1220
|
-
encryptionScope,
|
|
1221
|
-
)
|
|
1222
|
-
if (!entries.length) return null
|
|
1223
|
-
|
|
1224
|
-
const normalizedTarget = normalizeDictionaryToken(statusLabel)
|
|
1225
|
-
const loweredTarget = statusLabel.trim().toLowerCase()
|
|
1226
|
-
|
|
1227
|
-
const match = entries.find((entry) => {
|
|
1228
|
-
const label = entry.label.trim().toLowerCase()
|
|
1229
|
-
const value = entry.value.trim().toLowerCase()
|
|
1230
|
-
return (
|
|
1231
|
-
entry.normalizedValue === normalizedTarget ||
|
|
1232
|
-
label === loweredTarget ||
|
|
1233
|
-
value === loweredTarget
|
|
1234
|
-
)
|
|
1235
|
-
})
|
|
1236
|
-
|
|
1237
|
-
return match?.id ?? null
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1240
|
-
async function resolveCustomerEntityIdByEmail(
|
|
1241
|
-
ctx: ExecutionContext,
|
|
1242
|
-
email: string,
|
|
1243
|
-
): Promise<string | null> {
|
|
1244
|
-
const normalized = email.trim().toLowerCase()
|
|
1245
|
-
if (!normalized) return null
|
|
1246
|
-
|
|
1247
|
-
const CustomerEntityClass = resolveEntityClass(ctx, 'CustomerEntity')
|
|
1248
|
-
if (!CustomerEntityClass) return null
|
|
1249
|
-
|
|
1250
|
-
// Try direct DB lookup first (works when primaryEmail is not encrypted)
|
|
1251
|
-
const entity = await findOneWithDecryption(
|
|
1252
|
-
ctx.em,
|
|
1253
|
-
CustomerEntityClass,
|
|
1254
|
-
{
|
|
1255
|
-
primaryEmail: normalized,
|
|
1256
|
-
tenantId: ctx.tenantId,
|
|
1257
|
-
organizationId: ctx.organizationId,
|
|
1258
|
-
deletedAt: null,
|
|
1259
|
-
},
|
|
1260
|
-
undefined,
|
|
1261
|
-
{ tenantId: ctx.tenantId, organizationId: ctx.organizationId },
|
|
1262
|
-
)
|
|
1263
|
-
if (entity) return entity.id
|
|
1264
|
-
|
|
1265
|
-
// Fallback: in-memory email check for encrypted primaryEmail fields
|
|
1266
|
-
const candidates = await findWithDecryption(
|
|
1267
|
-
ctx.em,
|
|
1268
|
-
CustomerEntityClass,
|
|
1269
|
-
{
|
|
1270
|
-
tenantId: ctx.tenantId,
|
|
1271
|
-
organizationId: ctx.organizationId,
|
|
1272
|
-
deletedAt: null,
|
|
1273
|
-
},
|
|
1274
|
-
{ limit: 100, orderBy: { createdAt: 'DESC' } },
|
|
1275
|
-
{ tenantId: ctx.tenantId, organizationId: ctx.organizationId },
|
|
1276
|
-
)
|
|
1277
|
-
const match = candidates.find(
|
|
1278
|
-
(e) => e.primaryEmail && e.primaryEmail.toLowerCase() === normalized,
|
|
1279
|
-
)
|
|
1280
|
-
return match?.id ?? null
|
|
1281
|
-
}
|
|
1282
|
-
|
|
1283
|
-
async function resolveEffectiveDocumentKind(
|
|
1284
|
-
ctx: ExecutionContext,
|
|
1285
|
-
channelId: string,
|
|
1286
|
-
): Promise<'order' | 'quote'> {
|
|
1287
|
-
const SalesChannelClass = resolveEntityClass(ctx, 'SalesChannel')
|
|
1288
|
-
if (!SalesChannelClass) return 'order'
|
|
1289
|
-
|
|
1290
|
-
const channel = await findOneWithDecryption(
|
|
1291
|
-
ctx.em,
|
|
1292
|
-
SalesChannelClass,
|
|
1293
|
-
{
|
|
1294
|
-
id: channelId,
|
|
1295
|
-
tenantId: ctx.tenantId,
|
|
1296
|
-
organizationId: ctx.organizationId,
|
|
1297
|
-
deletedAt: null,
|
|
1298
|
-
},
|
|
1299
|
-
undefined,
|
|
1300
|
-
{ tenantId: ctx.tenantId, organizationId: ctx.organizationId },
|
|
1301
|
-
)
|
|
1302
|
-
if (!channel) return 'order'
|
|
1303
|
-
|
|
1304
|
-
const metadata = channel.metadata as Record<string, unknown> | null
|
|
1305
|
-
if (metadata?.quotesRequired === true) {
|
|
1306
|
-
return 'quote'
|
|
1307
|
-
}
|
|
1308
|
-
return 'order'
|
|
1309
|
-
}
|
|
1310
|
-
|
|
1311
|
-
async function resolveFirstChannelId(ctx: ExecutionContext): Promise<string | null> {
|
|
1312
|
-
const SalesChannelClass = resolveEntityClass(ctx, 'SalesChannel')
|
|
1313
|
-
if (!SalesChannelClass) return null
|
|
1314
|
-
|
|
1315
|
-
try {
|
|
1316
|
-
const channel = await findOneWithDecryption(
|
|
1317
|
-
ctx.em,
|
|
1318
|
-
SalesChannelClass,
|
|
1319
|
-
{
|
|
1320
|
-
tenantId: ctx.tenantId,
|
|
1321
|
-
organizationId: ctx.organizationId,
|
|
1322
|
-
deletedAt: null,
|
|
1323
|
-
},
|
|
1324
|
-
{ orderBy: { name: 'ASC' } },
|
|
1325
|
-
{ tenantId: ctx.tenantId, organizationId: ctx.organizationId },
|
|
1326
|
-
)
|
|
1327
|
-
return channel?.id ?? null
|
|
1328
|
-
} catch {
|
|
1329
|
-
return null
|
|
1330
|
-
}
|
|
1331
|
-
}
|
|
1332
|
-
|
|
1333
|
-
function resolveEntityClass<K extends keyof CrossModuleEntities>(
|
|
1334
|
-
ctx: ExecutionContext,
|
|
1335
|
-
key: K,
|
|
1336
|
-
): CrossModuleEntities[K] | null {
|
|
1337
|
-
const fromEntities = ctx.entities?.[key]
|
|
461
|
+
key: string,
|
|
462
|
+
): unknown {
|
|
463
|
+
const fromEntities = (ctx.entities as Record<string, unknown> | undefined)?.[key]
|
|
1338
464
|
if (fromEntities) return fromEntities
|
|
1339
465
|
try { return ctx.container.resolve(key) } catch { return null }
|
|
1340
466
|
}
|
|
1341
467
|
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
channelId: string | null,
|
|
1345
|
-
): Promise<string | null> {
|
|
1346
|
-
const SalesChannelClass = resolveEntityClass(ctx, 'SalesChannel')
|
|
1347
|
-
if (!SalesChannelClass) return null
|
|
1348
|
-
|
|
1349
|
-
try {
|
|
1350
|
-
const where: Record<string, unknown> = {
|
|
1351
|
-
tenantId: ctx.tenantId,
|
|
1352
|
-
organizationId: ctx.organizationId,
|
|
1353
|
-
deletedAt: null,
|
|
1354
|
-
}
|
|
1355
|
-
if (channelId) where.id = channelId
|
|
1356
|
-
const channel = await findOneWithDecryption(
|
|
1357
|
-
ctx.em,
|
|
1358
|
-
SalesChannelClass,
|
|
1359
|
-
where,
|
|
1360
|
-
channelId ? undefined : { orderBy: { name: 'ASC' } },
|
|
1361
|
-
{ tenantId: ctx.tenantId, organizationId: ctx.organizationId },
|
|
1362
|
-
)
|
|
1363
|
-
return channel?.currencyCode ?? null
|
|
1364
|
-
} catch {
|
|
1365
|
-
return null
|
|
1366
|
-
}
|
|
1367
|
-
}
|
|
1368
|
-
|
|
1369
|
-
async function resolveContactIdByNameAndType(
|
|
1370
|
-
ctx: ExecutionContext,
|
|
1371
|
-
contactName: string,
|
|
1372
|
-
contactType: string,
|
|
1373
|
-
): Promise<string | null> {
|
|
1374
|
-
const CustomerEntityClass = resolveEntityClass(ctx, 'CustomerEntity')
|
|
1375
|
-
if (!CustomerEntityClass) return null
|
|
1376
|
-
|
|
1377
|
-
const normalized = contactName.trim()
|
|
1378
|
-
if (!normalized) return null
|
|
1379
|
-
|
|
1380
|
-
const entity = await findOneWithDecryption(
|
|
1381
|
-
ctx.em,
|
|
1382
|
-
CustomerEntityClass,
|
|
1383
|
-
{
|
|
1384
|
-
displayName: normalized,
|
|
1385
|
-
kind: contactType,
|
|
1386
|
-
tenantId: ctx.tenantId,
|
|
1387
|
-
organizationId: ctx.organizationId,
|
|
1388
|
-
deletedAt: null,
|
|
1389
|
-
},
|
|
1390
|
-
undefined,
|
|
1391
|
-
{ tenantId: ctx.tenantId, organizationId: ctx.organizationId },
|
|
1392
|
-
)
|
|
1393
|
-
|
|
1394
|
-
return entity?.id ?? null
|
|
1395
|
-
}
|
|
1396
|
-
|
|
1397
|
-
interface OrderLineItem {
|
|
1398
|
-
id: string
|
|
1399
|
-
name?: string | null
|
|
1400
|
-
}
|
|
1401
|
-
|
|
1402
|
-
async function loadOrderLineItems(
|
|
1403
|
-
ctx: ExecutionContext,
|
|
1404
|
-
orderId: string,
|
|
1405
|
-
): Promise<OrderLineItem[]> {
|
|
1406
|
-
try {
|
|
1407
|
-
const result = await executeCommand<Record<string, unknown>, { lines?: OrderLineItem[] }>(
|
|
1408
|
-
ctx,
|
|
1409
|
-
'sales.orders.lines.list',
|
|
1410
|
-
{ orderId, organizationId: ctx.organizationId, tenantId: ctx.tenantId },
|
|
1411
|
-
)
|
|
1412
|
-
return result.lines ?? []
|
|
1413
|
-
} catch {
|
|
1414
|
-
return []
|
|
1415
|
-
}
|
|
1416
|
-
}
|
|
1417
|
-
|
|
1418
|
-
function matchLineItemByName(
|
|
1419
|
-
orderLines: OrderLineItem[],
|
|
1420
|
-
lineItemName: string,
|
|
1421
|
-
): string | null {
|
|
1422
|
-
const target = lineItemName.trim().toLowerCase()
|
|
1423
|
-
if (!target) return null
|
|
1424
|
-
|
|
1425
|
-
const exact = orderLines.find((l) => (l.name || '').trim().toLowerCase() === target)
|
|
1426
|
-
if (exact) return exact.id
|
|
1427
|
-
|
|
1428
|
-
const partial = orderLines.find((l) => {
|
|
1429
|
-
const name = (l.name || '').trim().toLowerCase()
|
|
1430
|
-
return name.includes(target) || target.includes(name)
|
|
1431
|
-
})
|
|
1432
|
-
return partial?.id ?? null
|
|
1433
|
-
}
|
|
1434
|
-
|
|
1435
|
-
function normalizeDictionaryToken(value: string): string {
|
|
1436
|
-
return value.trim().toLowerCase().replace(/[\s-]+/g, '_')
|
|
1437
|
-
}
|
|
1438
|
-
|
|
1439
|
-
function splitPersonName(name: string): { firstName: string; lastName: string } {
|
|
1440
|
-
const trimmed = name.trim()
|
|
1441
|
-
const parts = trimmed.split(/\s+/).filter((item) => item.length > 0)
|
|
1442
|
-
if (parts.length <= 1) {
|
|
1443
|
-
return {
|
|
1444
|
-
firstName: parts[0] || trimmed,
|
|
1445
|
-
lastName: '',
|
|
1446
|
-
}
|
|
1447
|
-
}
|
|
1448
|
-
return {
|
|
1449
|
-
firstName: parts[0],
|
|
1450
|
-
lastName: parts.slice(1).join(' '),
|
|
1451
|
-
}
|
|
1452
|
-
}
|
|
1453
|
-
|
|
1454
|
-
function parseNumberToken(value: string, fieldName: string): number {
|
|
1455
|
-
const parsed = Number(value)
|
|
1456
|
-
if (!Number.isFinite(parsed)) {
|
|
1457
|
-
throw new ExecutionError(`Invalid numeric value for ${fieldName}`, 400)
|
|
1458
|
-
}
|
|
1459
|
-
return parsed
|
|
1460
|
-
}
|
|
1461
|
-
|
|
1462
|
-
function normalizeAddressSnapshot(
|
|
1463
|
-
address: Record<string, unknown>,
|
|
1464
|
-
): Record<string, unknown> {
|
|
1465
|
-
return {
|
|
1466
|
-
addressLine1: address.line1 ?? address.addressLine1 ?? '',
|
|
1467
|
-
addressLine2: address.line2 ?? address.addressLine2 ?? null,
|
|
1468
|
-
companyName: address.company ?? address.companyName ?? null,
|
|
1469
|
-
name: address.contactName ?? address.name ?? null,
|
|
1470
|
-
city: address.city ?? null,
|
|
1471
|
-
region: address.state ?? address.region ?? null,
|
|
1472
|
-
postalCode: address.postalCode ?? null,
|
|
1473
|
-
country: address.country ?? null,
|
|
1474
|
-
}
|
|
1475
|
-
}
|
|
1476
|
-
|
|
1477
|
-
function parseDateToken(value?: string | null): Date | undefined {
|
|
1478
|
-
if (!value) return undefined
|
|
1479
|
-
const parsed = new Date(value)
|
|
1480
|
-
if (Number.isNaN(parsed.getTime())) return undefined
|
|
1481
|
-
return parsed
|
|
1482
|
-
}
|
|
468
|
+
// Re-export splitPersonName for backward compat
|
|
469
|
+
export { splitPersonName } from './contactValidation'
|
|
1483
470
|
|
|
1484
471
|
async function resolveActionDiscrepancies(
|
|
1485
472
|
em: EntityManager,
|