@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,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/inbox_ops/subscribers/extractionWorker.ts"],
4
- "sourcesContent": ["import { randomUUID } from 'node:crypto'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { EntityClass } from '@mikro-orm/core'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { InboxEmail, InboxProposal, InboxProposalAction, InboxDiscrepancy, InboxSettings } from '../data/entities'\nimport type { ExtractedParticipant, InboxDiscrepancyType } from '../data/entities'\nimport { extractionOutputSchema } from '../data/validators'\nimport { matchContacts } from '../lib/contactMatcher'\nimport { buildExtractionSystemPrompt, buildExtractionUserPrompt } from '../lib/extractionPrompt'\nimport { REQUIRED_FEATURES_MAP } from '../lib/constants'\nimport { fetchCatalogProductsForExtraction } from '../lib/catalogLookup'\nimport { enrichOrderPayload } from '../lib/payloadEnrichment'\nimport { validatePrices } from '../lib/priceValidator'\nimport { extractParticipantsFromThread } from '../lib/emailParser'\nimport { runExtractionWithConfiguredProvider } from '../lib/llmProvider'\nimport { safeParsePayloadJson } from '../lib/validation'\nimport { htmlToPlainText } from '../lib/htmlToPlainText'\nimport { emitInboxOpsEvent } from '../events'\n\nexport const metadata = {\n event: 'inbox_ops.email.received',\n persistent: true,\n id: 'inbox_ops:extraction-worker',\n}\n\ninterface EmailReceivedPayload {\n emailId: string\n tenantId: string\n organizationId: string\n forwardedByAddress: string\n subject: string\n}\n\ninterface ResolverContext {\n resolve: <T = unknown>(name: string) => T\n}\n\ninterface ExtractionEntityClasses {\n customerEntity?: EntityClass<{ id: string; kind: string; displayName: string; primaryEmail?: string | null }>\n catalogProduct?: EntityClass<{ id: string; name: string; sku?: string | null; tenantId?: string; organizationId?: string; deletedAt?: Date | null }>\n catalogProductPrice?: EntityClass<{ product?: unknown; unitPriceNet?: string | null; unitPriceGross?: string | null; currencyCode?: string | null; tenantId?: string; organizationId?: string; deletedAt?: Date | null; createdAt?: Date }>\n salesOrder?: EntityClass<{ id: string; orderNumber: string; customerReference?: string | null; tenantId?: string; organizationId?: string; deletedAt?: Date | null }>\n salesChannel?: EntityClass<{ id: string; name: string; currencyCode?: string; tenantId?: string; organizationId?: string; deletedAt?: Date | null }>\n customerAddress?: EntityClass<{ id: string; isPrimary: boolean; tenantId?: string; organizationId?: string; entity?: { id: string } | string; createdAt?: Date }>\n}\n\ninterface DiscrepancyInput {\n actionIndex?: number\n type: InboxDiscrepancyType\n severity: 'warning' | 'error'\n description: string\n expectedValue?: string | null\n foundValue?: string | null\n}\n\nfunction tryResolve<T>(ctx: ResolverContext, name: string): T | undefined {\n try {\n return ctx.resolve<T>(name)\n } catch {\n console.debug(`[inbox_ops:extraction] optional dependency \"${name}\" not available`)\n return undefined\n }\n}\n\nfunction resolveEntityClasses(ctx: ResolverContext): ExtractionEntityClasses {\n return {\n customerEntity: tryResolve(ctx, 'CustomerEntity'),\n catalogProduct: tryResolve(ctx, 'CatalogProduct'),\n catalogProductPrice: tryResolve(ctx, 'CatalogProductPrice'),\n salesOrder: tryResolve(ctx, 'SalesOrder'),\n salesChannel: tryResolve(ctx, 'SalesChannel'),\n customerAddress: tryResolve(ctx, 'CustomerAddress'),\n }\n}\n\nfunction createDiscrepancy(\n em: EntityManager,\n proposalId: string,\n allActions: { id: string }[],\n input: DiscrepancyInput,\n scope: { organizationId: string; tenantId: string },\n) {\n return em.create(InboxDiscrepancy, {\n proposalId,\n actionId: input.actionIndex !== undefined && allActions[input.actionIndex]\n ? allActions[input.actionIndex].id\n : null,\n type: input.type,\n severity: input.severity,\n description: input.description,\n expectedValue: input.expectedValue || null,\n foundValue: input.foundValue || null,\n resolved: false,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n })\n}\n\nexport default async function handle(payload: EmailReceivedPayload, ctx: ResolverContext) {\n const em = (ctx.resolve('em') as EntityManager).fork()\n const entityClasses = resolveEntityClasses(ctx)\n\n // Optimistic lock: atomically claim the email for processing.\n // If another worker already claimed it, nativeUpdate returns 0 rows.\n const claimed = await em.nativeUpdate(\n InboxEmail,\n { id: payload.emailId, status: 'received' },\n { status: 'processing' },\n )\n if (claimed === 0) return\n\n const email = await findOneWithDecryption(\n em,\n InboxEmail,\n { id: payload.emailId },\n undefined,\n { tenantId: payload.tenantId, organizationId: payload.organizationId },\n )\n if (!email) {\n console.error(`[inbox_ops:extraction-worker] Email not found: ${payload.emailId}`)\n return\n }\n\n try {\n const scope = {\n tenantId: email.tenantId,\n organizationId: email.organizationId,\n }\n\n // Load tenant settings for working language\n const settings = await findOneWithDecryption(em, InboxSettings, { organizationId: scope.organizationId, tenantId: scope.tenantId, deletedAt: null }, undefined, scope)\n const workingLanguage = settings?.workingLanguage || 'en'\n\n // Step 1: Build full text for LLM extraction.\n // Use rawText (or derive from rawHtml) instead of cleanedText because\n // cleanedText strips quoted replies \u2014 which contain the actual order content\n // in forwarded email threads.\n const fullText = buildFullTextForExtraction(email)\n if (!fullText.trim()) {\n email.status = 'failed'\n email.processingError = 'No text content found in email'\n await em.flush()\n return\n }\n\n // Step 2: Match contacts from thread participants\n const threadParticipants = extractParticipantsFromThread(email)\n const contactMatches = await matchContacts(em, threadParticipants, scope,\n entityClasses.customerEntity ? { customerEntityClass: entityClasses.customerEntity } : undefined,\n )\n\n // Step 2b: Fetch catalog products for LLM context\n const catalogProducts = await fetchCatalogProductsForExtraction(em, scope,\n entityClasses.catalogProduct && entityClasses.catalogProductPrice\n ? { catalogProductClass: entityClasses.catalogProduct, catalogProductPriceClass: entityClasses.catalogProductPrice }\n : undefined,\n )\n\n // Step 3: Call LLM for extraction\n const maxTextSize = parseInt(process.env.INBOX_OPS_MAX_TEXT_SIZE || '204800', 10)\n const truncatedText = fullText.slice(0, maxTextSize)\n\n const systemPrompt = buildExtractionSystemPrompt(contactMatches, catalogProducts, undefined, workingLanguage)\n const userPrompt = buildExtractionUserPrompt(truncatedText)\n\n let extractionResult: ReturnType<typeof extractionOutputSchema.parse>\n let tokensUsed = 0\n let modelUsed = ''\n\n try {\n const timeoutMsRaw = Number.parseInt(process.env.INBOX_OPS_LLM_TIMEOUT_MS || '90000', 10)\n const timeoutMs = Number.isFinite(timeoutMsRaw) && timeoutMsRaw > 0 ? timeoutMsRaw : 90000\n const extraction = await runExtractionWithConfiguredProvider({\n systemPrompt,\n userPrompt,\n modelOverride: process.env.INBOX_OPS_LLM_MODEL,\n timeoutMs,\n })\n extractionResult = extraction.object\n tokensUsed = extraction.totalTokens\n modelUsed = extraction.modelWithProvider\n } catch (llmError) {\n email.status = 'failed'\n email.processingError = `LLM extraction failed: ${llmError instanceof Error ? llmError.message : String(llmError)}`\n await em.flush()\n\n try {\n await emitInboxOpsEvent('inbox_ops.email.failed', {\n emailId: email.id,\n tenantId: email.tenantId,\n organizationId: email.organizationId,\n error: email.processingError,\n })\n } catch (eventError) {\n console.error('[inbox_ops:extraction-worker] Failed to emit email.failed event:', eventError)\n }\n\n return\n }\n\n const confidenceThresholdRaw = Number.parseFloat(process.env.INBOX_OPS_CONFIDENCE_THRESHOLD || '0.5')\n const confidenceThreshold = Number.isFinite(confidenceThresholdRaw)\n ? Math.min(Math.max(confidenceThresholdRaw, 0), 1)\n : 0.5\n const requiresReview = extractionResult.confidence < confidenceThreshold\n\n // Step 4: Validate prices for order/quote actions\n const orderActions = extractionResult.proposedActions\n .map((action, index) => ({\n ...action, payload: safeParsePayloadJson(action.payloadJson), index,\n }))\n .filter((a) => a.actionType === 'create_order' || a.actionType === 'create_quote')\n\n const priceDiscrepancies = await validatePrices(em, orderActions, scope,\n entityClasses.catalogProductPrice ? { catalogProductPriceClass: entityClasses.catalogProductPrice } : undefined,\n )\n\n // Step 4b: Check for duplicate orders by customerReference\n const duplicateOrderDiscrepancies = await detectDuplicateOrders(em, orderActions, scope, entityClasses.salesOrder)\n\n // Step 5: Match LLM-discovered participants not found in email headers.\n // Header-based matchContacts (step 2) only covers From/To/Cc addresses.\n // In forwarded threads, the original sender is in the body, not the headers.\n const headerEmails = new Set(contactMatches.map((m) => m.participant.email.toLowerCase()))\n const llmOnlyParticipants = extractionResult.participants\n .filter((p) => p.email && !headerEmails.has(p.email.toLowerCase()))\n .map((p) => ({ name: p.name, email: p.email, role: p.role || 'unknown' }))\n\n if (llmOnlyParticipants.length > 0) {\n const llmContactMatches = await matchContacts(em, llmOnlyParticipants, scope,\n entityClasses.customerEntity ? { customerEntityClass: entityClasses.customerEntity } : undefined,\n )\n contactMatches.push(...llmContactMatches)\n }\n\n // Step 5b: Merge contact match data into participants\n const enrichedParticipants: ExtractedParticipant[] = extractionResult.participants.map((p) => {\n const match = contactMatches.find(\n (m) => m.participant.email.toLowerCase() === p.email.toLowerCase(),\n )\n return {\n ...p,\n matchedContactId: match?.match?.contactId || null,\n matchedContactType: match?.match?.contactType || null,\n matchConfidence: match?.match?.confidence,\n }\n })\n\n // Step 6: Detect partial forward\n const possiblyIncomplete = extractionResult.possiblyIncomplete || detectPartialForward(email)\n\n // Step 6b: Normalize + enrich order/quote payloads\n const enrichmentDiscrepancies: DiscrepancyInput[] = []\n for (const [actionIndex, action] of extractionResult.proposedActions.entries()) {\n if (action.actionType === 'create_order' || action.actionType === 'create_quote') {\n const parsedPayload = safeParsePayloadJson(action.payloadJson)\n\n normalizeOrderPayloadFields(parsedPayload)\n\n const { payload: enriched, warnings } = await enrichOrderPayload(parsedPayload, {\n em,\n scope,\n contactMatches,\n catalogProducts,\n senderEmail: email.forwardedByAddress,\n salesChannelClass: entityClasses.salesChannel,\n customerAddressClass: entityClasses.customerAddress,\n })\n\n action.payloadJson = JSON.stringify(enriched)\n\n // Discrepancy descriptions are stored in the DB and rendered on the proposal review page.\n // Not i18n keys \u2014 the proposal UI displays them as-is for operator guidance.\n for (const warning of warnings) {\n if (warning === 'no_channel_resolved') {\n enrichmentDiscrepancies.push({\n actionIndex,\n type: 'other',\n severity: 'error',\n description: 'No sales channel available. Create a channel in Sales settings before accepting this order.',\n })\n } else if (warning === 'no_currency_resolved') {\n enrichmentDiscrepancies.push({\n actionIndex,\n type: 'currency_mismatch',\n severity: 'warning',\n description: 'No currency could be resolved for this order. Set a currency code or configure a sales channel with a default currency.',\n })\n }\n }\n }\n }\n\n // Step 6b-2: Enrich create_contact payloads with participant emails when the LLM omitted them,\n // and fix hallucinated draft_reply target emails using known participant data.\n const participantEmailMap = buildParticipantEmailMap(contactMatches, extractionResult.participants)\n enrichCreateContactEmails(extractionResult.proposedActions, participantEmailMap)\n enrichDraftReplyTargets(extractionResult.draftReplies, participantEmailMap)\n\n // Step 6c: Detect unresolved products and auto-generate create_product actions\n const productNotFoundDiscrepancies: DiscrepancyInput[] = []\n const autoProductActions: { actionType: 'create_product'; description: string; confidence: number; requiredFeature: string; payloadJson: string }[] = []\n const seenProductNames = new Set<string>()\n\n for (const [actionIndex, action] of extractionResult.proposedActions.entries()) {\n if (action.actionType !== 'create_order' && action.actionType !== 'create_quote') continue\n const parsedPayload = safeParsePayloadJson(action.payloadJson)\n const lineItems = Array.isArray(parsedPayload.lineItems)\n ? (parsedPayload.lineItems as Record<string, unknown>[])\n : []\n for (const item of lineItems) {\n if (!item.productId) {\n const productName = typeof item.productName === 'string'\n ? item.productName\n : (typeof item.description === 'string' ? item.description : 'Unknown')\n productNotFoundDiscrepancies.push({\n actionIndex,\n type: 'product_not_found',\n severity: 'error',\n description: `Product \"${productName}\" could not be matched to any catalog product`,\n foundValue: productName,\n })\n const nameKey = productName.toLowerCase().trim()\n if (nameKey && nameKey !== 'unknown' && !seenProductNames.has(nameKey)) {\n seenProductNames.add(nameKey)\n const sku = typeof item.sku === 'string' ? item.sku : undefined\n const unitPrice = typeof item.unitPrice === 'string' ? item.unitPrice : undefined\n const currencyCode = typeof parsedPayload.currencyCode === 'string' ? parsedPayload.currencyCode : undefined\n autoProductActions.push({\n actionType: 'create_product',\n description: `Create catalog product \"${productName}\"`,\n confidence: 0.9,\n requiredFeature: REQUIRED_FEATURES_MAP.create_product,\n payloadJson: JSON.stringify({\n title: productName,\n ...(sku && { sku }),\n ...(unitPrice && { unitPrice }),\n ...(currencyCode && { currencyCode }),\n kind: 'product',\n }),\n })\n }\n }\n }\n }\n\n // Step 7: Create proposal + actions + discrepancies atomically\n const proposalId = randomUUID()\n const proposal = em.create(InboxProposal, {\n id: proposalId,\n inboxEmailId: email.id,\n summary: extractionResult.summary,\n participants: enrichedParticipants,\n confidence: String(extractionResult.confidence.toFixed(2)),\n detectedLanguage: extractionResult.detectedLanguage || email.detectedLanguage,\n status: 'pending',\n possiblyIncomplete,\n llmModel: modelUsed,\n llmTokensUsed: tokensUsed,\n workingLanguage,\n organizationId: email.organizationId,\n tenantId: email.tenantId,\n })\n em.persist(proposal)\n\n // Step 6d: Auto-generate create_contact actions for unmatched participants (from headers)\n const autoContactActions = buildContactActionsForUnmatchedParticipants(\n contactMatches,\n extractionResult.proposedActions,\n email.toAddress,\n )\n\n // Step 6d-2: Also generate create_contact for LLM-discovered unmatched participants\n const llmContactActions = buildContactActionsForUnmatchedLlmParticipants(\n enrichedParticipants,\n contactMatches,\n extractionResult.proposedActions,\n autoContactActions,\n email.toAddress,\n )\n autoContactActions.push(...llmContactActions)\n\n // Step 6e: Auto-generate link_contact actions for matched participants\n const autoLinkActions = buildLinkContactActionsForMatchedParticipants(\n contactMatches,\n extractionResult.proposedActions,\n email.toAddress,\n )\n\n // Create actions \u2014 contact & product creation actions go first so they're executed before orders\n const combinedProposedActions = [...autoContactActions, ...autoLinkActions, ...autoProductActions, ...extractionResult.proposedActions]\n const allActions = [\n ...combinedProposedActions.map((action, index) => {\n const parsedPayload = safeParsePayloadJson(action.payloadJson)\n return em.create(InboxProposalAction, {\n id: randomUUID(),\n proposalId: proposalId,\n sortOrder: index,\n actionType: action.actionType,\n description: action.description,\n payload: parsedPayload,\n status: 'pending',\n confidence: String(action.confidence.toFixed(2)),\n requiredFeature: action.requiredFeature || REQUIRED_FEATURES_MAP[action.actionType] || null,\n organizationId: email.organizationId,\n tenantId: email.tenantId,\n })\n }),\n ...extractionResult.draftReplies.map((reply, index) =>\n em.create(InboxProposalAction, {\n id: randomUUID(),\n proposalId: proposalId,\n sortOrder: combinedProposedActions.length + index,\n actionType: 'draft_reply',\n description: `Draft reply to ${reply.toName || reply.to}: ${reply.subject}`,\n payload: {\n to: reply.to,\n toName: reply.toName,\n subject: reply.subject,\n body: reply.body,\n context: reply.context,\n replyTo: email.replyTo,\n inReplyToMessageId: email.messageId,\n references: email.emailReferences,\n },\n status: 'pending',\n confidence: String(extractionResult.confidence.toFixed(2)),\n requiredFeature: 'inbox_ops.replies.send',\n organizationId: email.organizationId,\n tenantId: email.tenantId,\n }),\n ),\n ]\n allActions.forEach((a) => em.persist(a))\n\n // Discrepancy actionIndex values reference extractionResult.proposedActions,\n // but allActions prepends auto-generated actions. Offset indices accordingly.\n const actionIndexOffset = autoContactActions.length + autoLinkActions.length + autoProductActions.length\n const offsetIndex = (d: DiscrepancyInput): DiscrepancyInput =>\n d.actionIndex !== undefined ? { ...d, actionIndex: d.actionIndex + actionIndexOffset } : d\n\n // Create discrepancies using factory\n const allDiscrepancies = [\n ...extractionResult.discrepancies.map((d) =>\n createDiscrepancy(em, proposalId, allActions, offsetIndex(d), scope),\n ),\n ...priceDiscrepancies.map((d) =>\n createDiscrepancy(em, proposalId, allActions, offsetIndex(d), scope),\n ),\n ...duplicateOrderDiscrepancies.map((d) =>\n createDiscrepancy(em, proposalId, allActions, offsetIndex(d), scope),\n ),\n ...productNotFoundDiscrepancies.map((d) =>\n createDiscrepancy(em, proposalId, allActions, offsetIndex(d), scope),\n ),\n ...enrichmentDiscrepancies.map((d) =>\n createDiscrepancy(em, proposalId, allActions, offsetIndex(d), scope),\n ),\n ]\n\n // Flag unmatched contacts as discrepancies (from header-based matches + LLM-discovered participants)\n const contactDiscrepancyEmails = new Set<string>()\n for (const match of contactMatches) {\n if (!match.match && match.participant.email) {\n const emailLower = match.participant.email.toLowerCase()\n contactDiscrepancyEmails.add(emailLower)\n allDiscrepancies.push(\n createDiscrepancy(em, proposalId, allActions, {\n type: 'unknown_contact',\n severity: 'warning',\n description: `No matching contact found for ${match.participant.name} (${match.participant.email})`,\n foundValue: match.participant.email,\n }, scope),\n )\n }\n }\n for (const participant of enrichedParticipants) {\n if (participant.matchedContactId) continue\n const emailLower = (participant.email || '').toLowerCase()\n if (!emailLower || contactDiscrepancyEmails.has(emailLower)) continue\n contactDiscrepancyEmails.add(emailLower)\n allDiscrepancies.push(\n createDiscrepancy(em, proposalId, allActions, {\n type: 'unknown_contact',\n severity: 'warning',\n description: `No matching contact found for ${participant.name} (${participant.email})`,\n foundValue: participant.email,\n }, scope),\n )\n }\n\n // Flag draft_reply actions that target unmatched contacts (blocks accept)\n const matchedEmails = new Set(\n contactMatches\n .filter((m) => m.match?.contactId)\n .map((m) => m.participant.email.toLowerCase()),\n )\n for (const [actionIndex, action] of allActions.entries()) {\n if (action.actionType !== 'draft_reply') continue\n const payload = action.payload as Record<string, unknown> | null\n const toEmail = typeof payload?.to === 'string' ? payload.to.trim().toLowerCase() : ''\n if (toEmail && !matchedEmails.has(toEmail)) {\n allDiscrepancies.push(\n createDiscrepancy(em, proposalId, allActions, {\n actionIndex,\n type: 'unknown_contact',\n severity: 'error',\n description: `Draft reply target \"${toEmail}\" has no matching contact. Create the contact first.`,\n foundValue: toEmail,\n }, scope),\n )\n }\n }\n\n allDiscrepancies.forEach((d) => em.persist(d))\n\n // Step 8: Update email status\n email.status = requiresReview ? 'needs_review' : 'processed'\n email.detectedLanguage = extractionResult.detectedLanguage || email.detectedLanguage\n\n await em.flush()\n\n // Step 9: Emit events\n try {\n await emitInboxOpsEvent('inbox_ops.email.processed', {\n emailId: email.id,\n tenantId: email.tenantId,\n organizationId: email.organizationId,\n })\n\n await emitInboxOpsEvent('inbox_ops.proposal.created', {\n proposalId: proposal.id,\n emailId: email.id,\n tenantId: email.tenantId,\n organizationId: email.organizationId,\n actionCount: allActions.length,\n discrepancyCount: allDiscrepancies.length,\n confidence: proposal.confidence,\n summary: proposal.summary,\n })\n } catch (eventError) {\n console.error('[inbox_ops:extraction-worker] Failed to emit events:', eventError)\n }\n } catch (err) {\n email.status = 'failed'\n email.processingError = err instanceof Error ? err.message : String(err)\n await em.flush()\n\n try {\n await emitInboxOpsEvent('inbox_ops.email.failed', {\n emailId: email.id,\n tenantId: email.tenantId,\n organizationId: email.organizationId,\n error: email.processingError,\n })\n } catch (eventError) {\n console.error('[inbox_ops:extraction-worker] Failed to emit email.failed event:', eventError)\n }\n\n console.error('[inbox_ops:extraction-worker] Extraction failed:', err)\n }\n}\n\nfunction normalizeOrderPayloadFields(payload: Record<string, unknown>): void {\n const lineItems = Array.isArray(payload.lineItems)\n ? (payload.lineItems as Record<string, unknown>[])\n : []\n for (const item of lineItems) {\n if (!item.productName && typeof item.description === 'string') {\n item.productName = item.description\n }\n if (typeof item.quantity === 'number') {\n item.quantity = String(item.quantity)\n }\n if (typeof item.unitPrice === 'number') {\n item.unitPrice = String(item.unitPrice)\n }\n }\n}\n\nfunction buildContactActionsForUnmatchedParticipants(\n contactMatches: { participant: { name: string; email: string }; match?: { contactId: string } | null }[],\n existingActions: { actionType: string; payloadJson: string }[],\n inboxAddress: string,\n): { actionType: 'create_contact'; description: string; confidence: number; requiredFeature: string; payloadJson: string }[] {\n const alreadyProposed = new Set(\n existingActions\n .filter((a) => a.actionType === 'create_contact')\n .map((a) => {\n const p = safeParsePayloadJson(a.payloadJson)\n return typeof p.email === 'string' ? p.email.toLowerCase() : ''\n })\n .filter(Boolean),\n )\n\n const inboxLower = (inboxAddress || '').toLowerCase()\n const systemPatterns = ['noreply', 'no-reply', 'donotreply', 'mailer-daemon', 'postmaster']\n\n return contactMatches\n .filter((m) => {\n if (m.match?.contactId) return false\n const emailLower = m.participant.email.toLowerCase()\n if (alreadyProposed.has(emailLower)) return false\n if (emailLower === inboxLower) return false\n return !systemPatterns.some((p) => emailLower.includes(p))\n })\n .map((m) => ({\n actionType: 'create_contact' as const,\n description: `Create contact for ${m.participant.name} (${m.participant.email})`,\n confidence: 0.9,\n requiredFeature: REQUIRED_FEATURES_MAP.create_contact,\n payloadJson: JSON.stringify({\n type: 'person',\n name: m.participant.name,\n email: m.participant.email,\n source: 'inbox_ops',\n }),\n }))\n}\n\nfunction buildLinkContactActionsForMatchedParticipants(\n contactMatches: { participant: { name: string; email: string }; match?: { contactId: string; contactType?: string; contactName?: string } | null }[],\n existingActions: { actionType: string; payloadJson: string }[],\n inboxAddress: string,\n): { actionType: 'link_contact'; description: string; confidence: number; requiredFeature: string; payloadJson: string }[] {\n const alreadyProposed = new Set(\n existingActions\n .filter((a) => a.actionType === 'link_contact')\n .map((a) => {\n const p = safeParsePayloadJson(a.payloadJson)\n const email = typeof p.emailAddress === 'string' ? p.emailAddress : (typeof p.email === 'string' ? p.email : '')\n return email.toLowerCase()\n })\n .filter(Boolean),\n )\n\n const inboxLower = (inboxAddress || '').toLowerCase()\n const systemPatterns = ['noreply', 'no-reply', 'donotreply', 'mailer-daemon', 'postmaster']\n\n return contactMatches\n .filter((m) => {\n if (!m.match?.contactId) return false\n const emailLower = m.participant.email.toLowerCase()\n if (alreadyProposed.has(emailLower)) return false\n if (emailLower === inboxLower) return false\n return !systemPatterns.some((p) => emailLower.includes(p))\n })\n .map((m) => ({\n actionType: 'link_contact' as const,\n description: `Link ${m.participant.name} (${m.participant.email}) to existing contact`,\n confidence: 0.95,\n requiredFeature: REQUIRED_FEATURES_MAP.link_contact,\n payloadJson: JSON.stringify({\n emailAddress: m.participant.email,\n contactId: m.match!.contactId,\n contactType: m.match!.contactType || 'person',\n contactName: m.participant.name,\n }),\n }))\n}\n\nfunction buildContactActionsForUnmatchedLlmParticipants(\n enrichedParticipants: { name: string; email: string; matchedContactId?: string | null }[],\n contactMatches: { participant: { email: string } }[],\n existingActions: { actionType: string; payloadJson: string }[],\n alreadyAutoCreated: { payloadJson: string }[],\n inboxAddress: string,\n): { actionType: 'create_contact'; description: string; confidence: number; requiredFeature: string; payloadJson: string }[] {\n const headerEmails = new Set(\n contactMatches.map((m) => m.participant.email.toLowerCase()),\n )\n\n const alreadyProposed = new Set([\n ...existingActions\n .filter((a) => a.actionType === 'create_contact')\n .map((a) => {\n const p = safeParsePayloadJson(a.payloadJson)\n return typeof p.email === 'string' ? p.email.toLowerCase() : ''\n })\n .filter(Boolean),\n ...alreadyAutoCreated\n .map((a) => {\n const p = safeParsePayloadJson(a.payloadJson)\n return typeof p.email === 'string' ? p.email.toLowerCase() : ''\n })\n .filter(Boolean),\n ])\n\n const inboxLower = (inboxAddress || '').toLowerCase()\n const systemPatterns = ['noreply', 'no-reply', 'donotreply', 'mailer-daemon', 'postmaster']\n\n return enrichedParticipants\n .filter((p) => {\n if (p.matchedContactId) return false\n const emailLower = (p.email || '').toLowerCase()\n if (!emailLower) return false\n if (headerEmails.has(emailLower)) return false\n if (alreadyProposed.has(emailLower)) return false\n if (emailLower === inboxLower) return false\n return !systemPatterns.some((pat) => emailLower.includes(pat))\n })\n .map((p) => ({\n actionType: 'create_contact' as const,\n description: `Create contact for ${p.name} (${p.email})`,\n confidence: 0.85,\n requiredFeature: REQUIRED_FEATURES_MAP.create_contact,\n payloadJson: JSON.stringify({\n type: 'person',\n name: p.name,\n email: p.email,\n source: 'inbox_ops',\n }),\n }))\n}\n\nasync function detectDuplicateOrders(\n em: EntityManager,\n orderActions: { actionType: string; payload: Record<string, unknown>; index: number }[],\n scope: { tenantId: string; organizationId: string },\n salesOrderClass?: EntityClass<{ id: string; orderNumber: string; customerReference?: string | null; tenantId?: string; organizationId?: string; deletedAt?: Date | null }>,\n): Promise<{ type: 'duplicate_order'; severity: 'error'; description: string; expectedValue: string | null; foundValue: string | null; actionIndex: number }[]> {\n if (!salesOrderClass) return []\n const discrepancies: { type: 'duplicate_order'; severity: 'error'; description: string; expectedValue: string | null; foundValue: string | null; actionIndex: number }[] = []\n\n for (const action of orderActions) {\n if (action.actionType !== 'create_order') continue\n\n const customerReference = typeof action.payload.customerReference === 'string'\n ? action.payload.customerReference.trim()\n : null\n\n if (!customerReference) continue\n\n try {\n const existing = await findOneWithDecryption(\n em,\n salesOrderClass,\n {\n customerReference,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n\n if (existing) {\n discrepancies.push({\n type: 'duplicate_order',\n severity: 'error',\n description: `An order with customer reference \"${customerReference}\" already exists (${existing.orderNumber || existing.id})`,\n expectedValue: null,\n foundValue: customerReference,\n actionIndex: action.index,\n })\n }\n } catch {\n // Skip duplicate detection if lookup fails\n }\n }\n\n return discrepancies\n}\n\nfunction detectPartialForward(email: InboxEmail): boolean {\n const subject = email.subject || ''\n const hasReOrFw = /^(RE|FW|Fwd):/i.test(subject)\n const messageCount = email.threadMessages?.length || 0\n return hasReOrFw && messageCount < 2\n}\n\nfunction buildParticipantEmailMap(\n contactMatches: { participant: { name: string; email: string } }[],\n llmParticipants: { name: string; email: string }[],\n): Map<string, string> {\n const nameToEmail = new Map<string, string>()\n // Header-based participants are the most reliable source\n for (const m of contactMatches) {\n if (m.participant.name && m.participant.email) {\n nameToEmail.set(m.participant.name.trim().toLowerCase(), m.participant.email.trim().toLowerCase())\n }\n }\n // LLM-extracted participants as fallback (don't overwrite header-based)\n for (const p of llmParticipants) {\n if (p.name && p.email) {\n const key = p.name.trim().toLowerCase()\n if (!nameToEmail.has(key)) {\n nameToEmail.set(key, p.email.trim().toLowerCase())\n }\n }\n }\n return nameToEmail\n}\n\nfunction enrichCreateContactEmails(\n actions: { actionType: string; payloadJson: string }[],\n participantEmailMap: Map<string, string>,\n): void {\n for (const action of actions) {\n if (action.actionType !== 'create_contact') continue\n const payload = safeParsePayloadJson(action.payloadJson)\n if (payload.email) continue\n const name = typeof payload.name === 'string' ? payload.name.trim() : ''\n if (!name) continue\n // Try exact name match first, then partial (first part before / or ,)\n const email = participantEmailMap.get(name.toLowerCase())\n ?? findPartialNameMatch(name, participantEmailMap)\n if (email) {\n payload.email = email\n action.payloadJson = JSON.stringify(payload)\n }\n }\n}\n\nfunction enrichDraftReplyTargets(\n draftReplies: { to: string; toName?: string; subject: string; body: string; context?: string }[],\n participantEmailMap: Map<string, string>,\n): void {\n const knownEmails = new Set(participantEmailMap.values())\n for (const reply of draftReplies) {\n const toEmail = reply.to.trim().toLowerCase()\n if (knownEmails.has(toEmail)) continue\n // The LLM hallucinated an email \u2014 try to resolve via toName\n const toName = (reply.toName || '').trim()\n if (!toName) continue\n const correctedEmail = participantEmailMap.get(toName.toLowerCase())\n ?? findPartialNameMatch(toName, participantEmailMap)\n if (correctedEmail) {\n reply.to = correctedEmail\n }\n }\n}\n\nfunction buildFullTextForExtraction(email: InboxEmail): string {\n let text = email.rawText || ''\n if (!text && email.rawHtml) {\n text = htmlToPlainText(email.rawHtml)\n }\n return text\n .replace(/\\r\\n/g, '\\n')\n .replace(/\\r/g, '\\n')\n .replace(/\\t/g, ' ')\n .replace(/ {2,}/g, ' ')\n .replace(/\\n{3,}/g, '\\n\\n')\n .trim()\n}\n\nfunction findPartialNameMatch(name: string, map: Map<string, string>): string | undefined {\n const lower = name.toLowerCase()\n // Split on common separators (e.g. \"Marco Rossi / Rossi Imports S.r.l.\")\n const parts = lower.split(/\\s*[\\/,]\\s*/).map((p) => p.trim()).filter(Boolean)\n for (const part of parts) {\n const match = map.get(part)\n if (match) return match\n }\n // Try matching first+last name against map keys\n for (const [mapName, mapEmail] of map) {\n if (lower.includes(mapName) || mapName.includes(lower)) {\n return mapEmail\n }\n for (const part of parts) {\n if (part.includes(mapName) || mapName.includes(part)) {\n return mapEmail\n }\n }\n }\n return undefined\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { randomUUID } from 'node:crypto'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { EntityClass } from '@mikro-orm/core'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { InboxEmail, InboxProposal, InboxProposalAction, InboxDiscrepancy, InboxSettings } from '../data/entities'\nimport type { ExtractedParticipant, InboxDiscrepancyType } from '../data/entities'\nimport { extractionOutputSchema } from '../data/validators'\nimport { matchContacts } from '../lib/contactMatcher'\nimport { buildExtractionSystemPrompt, buildExtractionUserPrompt } from '../lib/extractionPrompt'\nimport { REQUIRED_FEATURES_MAP } from '../lib/constants'\nimport { fetchCatalogProductsForExtraction } from '../lib/catalogLookup'\nimport { enrichOrderPayload } from '../lib/payloadEnrichment'\nimport { validatePrices } from '../lib/priceValidator'\nimport { extractParticipantsFromThread } from '../lib/emailParser'\nimport { runExtractionWithConfiguredProvider } from '../lib/llmProvider'\nimport { safeParsePayloadJson } from '../lib/validation'\nimport { htmlToPlainText } from '../lib/htmlToPlainText'\nimport { emitInboxOpsEvent } from '../events'\n\nexport const metadata = {\n event: 'inbox_ops.email.received',\n persistent: true,\n id: 'inbox_ops:extraction-worker',\n}\n\ninterface EmailReceivedPayload {\n emailId: string\n tenantId: string\n organizationId: string\n forwardedByAddress: string\n subject: string\n}\n\ninterface ResolverContext {\n resolve: <T = unknown>(name: string) => T\n}\n\ninterface ExtractionEntityClasses {\n customerEntity?: EntityClass<{ id: string; kind: string; displayName: string; primaryEmail?: string | null }>\n catalogProduct?: EntityClass<{ id: string; name: string; sku?: string | null; tenantId?: string; organizationId?: string; deletedAt?: Date | null }>\n catalogProductPrice?: EntityClass<{ product?: unknown; unitPriceNet?: string | null; unitPriceGross?: string | null; currencyCode?: string | null; tenantId?: string; organizationId?: string; deletedAt?: Date | null; createdAt?: Date }>\n salesOrder?: EntityClass<{ id: string; orderNumber: string; customerReference?: string | null; tenantId?: string; organizationId?: string; deletedAt?: Date | null }>\n salesChannel?: EntityClass<{ id: string; name: string; currencyCode?: string; tenantId?: string; organizationId?: string; deletedAt?: Date | null }>\n customerAddress?: EntityClass<{ id: string; isPrimary: boolean; tenantId?: string; organizationId?: string; entity?: { id: string } | string; createdAt?: Date }>\n}\n\ninterface DiscrepancyInput {\n actionIndex?: number\n type: InboxDiscrepancyType\n severity: 'warning' | 'error'\n description: string\n expectedValue?: string | null\n foundValue?: string | null\n}\n\nfunction tryResolve<T>(ctx: ResolverContext, name: string): T | undefined {\n try {\n return ctx.resolve<T>(name)\n } catch {\n console.debug(`[inbox_ops:extraction] optional dependency \"${name}\" not available`)\n return undefined\n }\n}\n\nfunction resolveEntityClasses(ctx: ResolverContext): ExtractionEntityClasses {\n return {\n customerEntity: tryResolve(ctx, 'CustomerEntity'),\n catalogProduct: tryResolve(ctx, 'CatalogProduct'),\n catalogProductPrice: tryResolve(ctx, 'CatalogProductPrice'),\n salesOrder: tryResolve(ctx, 'SalesOrder'),\n salesChannel: tryResolve(ctx, 'SalesChannel'),\n customerAddress: tryResolve(ctx, 'CustomerAddress'),\n }\n}\n\nfunction createDiscrepancy(\n em: EntityManager,\n proposalId: string,\n allActions: { id: string }[],\n input: DiscrepancyInput,\n scope: { organizationId: string; tenantId: string },\n) {\n return em.create(InboxDiscrepancy, {\n proposalId,\n actionId: input.actionIndex !== undefined && allActions[input.actionIndex]\n ? allActions[input.actionIndex].id\n : null,\n type: input.type,\n severity: input.severity,\n description: input.description,\n expectedValue: input.expectedValue || null,\n foundValue: input.foundValue || null,\n resolved: false,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n })\n}\n\nexport default async function handle(payload: EmailReceivedPayload, ctx: ResolverContext) {\n const em = (ctx.resolve('em') as EntityManager).fork()\n const entityClasses = resolveEntityClasses(ctx)\n\n // Optimistic lock: atomically claim the email for processing.\n // If another worker already claimed it, nativeUpdate returns 0 rows.\n const claimed = await em.nativeUpdate(\n InboxEmail,\n { id: payload.emailId, status: 'received' },\n { status: 'processing' },\n )\n if (claimed === 0) return\n\n const email = await findOneWithDecryption(\n em,\n InboxEmail,\n { id: payload.emailId },\n undefined,\n { tenantId: payload.tenantId, organizationId: payload.organizationId },\n )\n if (!email) {\n console.error(`[inbox_ops:extraction-worker] Email not found: ${payload.emailId}`)\n return\n }\n\n try {\n const scope = {\n tenantId: email.tenantId,\n organizationId: email.organizationId,\n }\n\n // Load tenant settings for working language\n const settings = await findOneWithDecryption(em, InboxSettings, { organizationId: scope.organizationId, tenantId: scope.tenantId, deletedAt: null }, undefined, scope)\n const workingLanguage = settings?.workingLanguage || 'en'\n\n // Step 1: Build full text for LLM extraction.\n // Use rawText (or derive from rawHtml) instead of cleanedText because\n // cleanedText strips quoted replies \u2014 which contain the actual order content\n // in forwarded email threads.\n const fullText = buildFullTextForExtraction(email)\n if (!fullText.trim()) {\n email.status = 'failed'\n email.processingError = 'No text content found in email'\n await em.flush()\n return\n }\n\n // Step 2: Match contacts from thread participants\n const threadParticipants = extractParticipantsFromThread(email)\n const contactMatches = await matchContacts(em, threadParticipants, scope,\n entityClasses.customerEntity ? { customerEntityClass: entityClasses.customerEntity } : undefined,\n )\n\n // Step 2b: Fetch catalog products for LLM context\n const catalogProducts = await fetchCatalogProductsForExtraction(em, scope,\n entityClasses.catalogProduct && entityClasses.catalogProductPrice\n ? { catalogProductClass: entityClasses.catalogProduct, catalogProductPriceClass: entityClasses.catalogProductPrice }\n : undefined,\n )\n\n // Step 3: Call LLM for extraction\n const maxTextSize = parseInt(process.env.INBOX_OPS_MAX_TEXT_SIZE || '204800', 10)\n const truncatedText = fullText.slice(0, maxTextSize)\n\n const systemPrompt = await buildExtractionSystemPrompt(contactMatches, catalogProducts, undefined, workingLanguage)\n const userPrompt = buildExtractionUserPrompt(truncatedText)\n\n let extractionResult: ReturnType<typeof extractionOutputSchema.parse>\n let tokensUsed = 0\n let modelUsed = ''\n\n try {\n const timeoutMsRaw = Number.parseInt(process.env.INBOX_OPS_LLM_TIMEOUT_MS || '90000', 10)\n const timeoutMs = Number.isFinite(timeoutMsRaw) && timeoutMsRaw > 0 ? timeoutMsRaw : 90000\n const extraction = await runExtractionWithConfiguredProvider({\n systemPrompt,\n userPrompt,\n modelOverride: process.env.INBOX_OPS_LLM_MODEL,\n timeoutMs,\n })\n extractionResult = extraction.object\n tokensUsed = extraction.totalTokens\n modelUsed = extraction.modelWithProvider\n } catch (llmError) {\n email.status = 'failed'\n email.processingError = `LLM extraction failed: ${llmError instanceof Error ? llmError.message : String(llmError)}`\n await em.flush()\n\n try {\n await emitInboxOpsEvent('inbox_ops.email.failed', {\n emailId: email.id,\n tenantId: email.tenantId,\n organizationId: email.organizationId,\n error: email.processingError,\n })\n } catch (eventError) {\n console.error('[inbox_ops:extraction-worker] Failed to emit email.failed event:', eventError)\n }\n\n return\n }\n\n const confidenceThresholdRaw = Number.parseFloat(process.env.INBOX_OPS_CONFIDENCE_THRESHOLD || '0.5')\n const confidenceThreshold = Number.isFinite(confidenceThresholdRaw)\n ? Math.min(Math.max(confidenceThresholdRaw, 0), 1)\n : 0.5\n const requiresReview = extractionResult.confidence < confidenceThreshold\n\n // Step 4: Validate prices for order/quote actions\n const orderActions = extractionResult.proposedActions\n .map((action, index) => ({\n ...action, payload: safeParsePayloadJson(action.payloadJson), index,\n }))\n .filter((a) => a.actionType === 'create_order' || a.actionType === 'create_quote')\n\n const priceDiscrepancies = await validatePrices(em, orderActions, scope,\n entityClasses.catalogProductPrice ? { catalogProductPriceClass: entityClasses.catalogProductPrice } : undefined,\n )\n\n // Step 4b: Check for duplicate orders by customerReference\n const duplicateOrderDiscrepancies = await detectDuplicateOrders(em, orderActions, scope, entityClasses.salesOrder)\n\n // Step 5: Match LLM-discovered participants not found in email headers.\n // Header-based matchContacts (step 2) only covers From/To/Cc addresses.\n // In forwarded threads, the original sender is in the body, not the headers.\n const headerEmails = new Set(contactMatches.map((m) => m.participant.email.toLowerCase()))\n const llmOnlyParticipants = extractionResult.participants\n .filter((p) => p.email && !headerEmails.has(p.email.toLowerCase()))\n .map((p) => ({ name: p.name, email: p.email, role: p.role || 'unknown' }))\n\n if (llmOnlyParticipants.length > 0) {\n const llmContactMatches = await matchContacts(em, llmOnlyParticipants, scope,\n entityClasses.customerEntity ? { customerEntityClass: entityClasses.customerEntity } : undefined,\n )\n contactMatches.push(...llmContactMatches)\n }\n\n // Step 5b: Merge contact match data into participants\n const enrichedParticipants: ExtractedParticipant[] = extractionResult.participants.map((p) => {\n const match = contactMatches.find(\n (m) => m.participant.email.toLowerCase() === p.email.toLowerCase(),\n )\n return {\n ...p,\n matchedContactId: match?.match?.contactId || null,\n matchedContactType: match?.match?.contactType || null,\n matchConfidence: match?.match?.confidence,\n }\n })\n\n // Step 6: Detect partial forward\n const possiblyIncomplete = extractionResult.possiblyIncomplete || detectPartialForward(email)\n\n // Step 6b: Normalize + enrich order/quote payloads\n const enrichmentDiscrepancies: DiscrepancyInput[] = []\n for (const [actionIndex, action] of extractionResult.proposedActions.entries()) {\n if (action.actionType === 'create_order' || action.actionType === 'create_quote') {\n const parsedPayload = safeParsePayloadJson(action.payloadJson)\n\n normalizeOrderPayloadFields(parsedPayload)\n\n const { payload: enriched, warnings } = await enrichOrderPayload(parsedPayload, {\n em,\n scope,\n contactMatches,\n catalogProducts,\n senderEmail: email.forwardedByAddress,\n salesChannelClass: entityClasses.salesChannel,\n customerAddressClass: entityClasses.customerAddress,\n })\n\n action.payloadJson = JSON.stringify(enriched)\n\n for (const warning of warnings) {\n if (warning === 'no_channel_resolved') {\n enrichmentDiscrepancies.push({\n actionIndex,\n type: 'other',\n severity: 'error',\n description: 'inbox_ops.discrepancy.desc.no_channel',\n })\n } else if (warning === 'no_currency_resolved') {\n enrichmentDiscrepancies.push({\n actionIndex,\n type: 'currency_mismatch',\n severity: 'warning',\n description: 'inbox_ops.discrepancy.desc.no_currency',\n })\n }\n }\n }\n }\n\n // Step 6b-2: Enrich create_contact payloads with participant emails when the LLM omitted them,\n // and fix hallucinated draft_reply target emails using known participant data.\n const participantEmailMap = buildParticipantEmailMap(contactMatches, extractionResult.participants)\n enrichCreateContactEmails(extractionResult.proposedActions, participantEmailMap)\n enrichDraftReplyTargets(extractionResult.draftReplies, participantEmailMap)\n\n // Step 6c: Detect unresolved products and auto-generate create_product actions\n const productNotFoundDiscrepancies: DiscrepancyInput[] = []\n const autoProductActions: { actionType: 'create_product'; description: string; confidence: number; requiredFeature: string; payloadJson: string }[] = []\n const seenProductNames = new Set<string>()\n\n for (const [actionIndex, action] of extractionResult.proposedActions.entries()) {\n if (action.actionType !== 'create_order' && action.actionType !== 'create_quote') continue\n const parsedPayload = safeParsePayloadJson(action.payloadJson)\n const lineItems = Array.isArray(parsedPayload.lineItems)\n ? (parsedPayload.lineItems as Record<string, unknown>[])\n : []\n for (const item of lineItems) {\n if (!item.productId) {\n const productName = typeof item.productName === 'string'\n ? item.productName\n : (typeof item.description === 'string' ? item.description : 'Unknown')\n productNotFoundDiscrepancies.push({\n actionIndex,\n type: 'product_not_found',\n severity: 'error',\n description: 'inbox_ops.discrepancy.desc.product_not_matched',\n foundValue: productName,\n })\n const nameKey = productName.toLowerCase().trim()\n if (nameKey && nameKey !== 'unknown' && !seenProductNames.has(nameKey)) {\n seenProductNames.add(nameKey)\n const sku = typeof item.sku === 'string' ? item.sku : undefined\n const unitPrice = typeof item.unitPrice === 'string' ? item.unitPrice : undefined\n const currencyCode = typeof parsedPayload.currencyCode === 'string' ? parsedPayload.currencyCode : undefined\n autoProductActions.push({\n actionType: 'create_product',\n description: 'inbox_ops.action.desc.create_product',\n confidence: 0.9,\n requiredFeature: REQUIRED_FEATURES_MAP.create_product,\n payloadJson: JSON.stringify({\n title: productName,\n ...(sku && { sku }),\n ...(unitPrice && { unitPrice }),\n ...(currencyCode && { currencyCode }),\n kind: 'product',\n }),\n })\n }\n }\n }\n }\n\n // Step 7: Create proposal + actions + discrepancies atomically\n const proposalId = randomUUID()\n const proposal = em.create(InboxProposal, {\n id: proposalId,\n inboxEmailId: email.id,\n summary: extractionResult.summary,\n participants: enrichedParticipants,\n confidence: String(extractionResult.confidence.toFixed(2)),\n detectedLanguage: extractionResult.detectedLanguage || email.detectedLanguage,\n status: 'pending',\n possiblyIncomplete,\n llmModel: modelUsed,\n llmTokensUsed: tokensUsed,\n workingLanguage,\n organizationId: email.organizationId,\n tenantId: email.tenantId,\n })\n em.persist(proposal)\n\n // Step 6d: Auto-generate create_contact actions for unmatched participants (from headers)\n const autoContactActions = buildContactActionsForUnmatchedParticipants(\n contactMatches,\n extractionResult.proposedActions,\n email.toAddress,\n )\n\n // Step 6d-2: Also generate create_contact for LLM-discovered unmatched participants\n const llmContactActions = buildContactActionsForUnmatchedLlmParticipants(\n enrichedParticipants,\n contactMatches,\n extractionResult.proposedActions,\n autoContactActions,\n email.toAddress,\n )\n autoContactActions.push(...llmContactActions)\n\n // Step 6e: Auto-generate link_contact actions for matched participants\n const autoLinkActions = buildLinkContactActionsForMatchedParticipants(\n contactMatches,\n extractionResult.proposedActions,\n email.toAddress,\n )\n\n // Create actions \u2014 contact & product creation actions go first so they're executed before orders\n const combinedProposedActions = [...autoContactActions, ...autoLinkActions, ...autoProductActions, ...extractionResult.proposedActions]\n const allActions = [\n ...combinedProposedActions.map((action, index) => {\n const parsedPayload = safeParsePayloadJson(action.payloadJson)\n return em.create(InboxProposalAction, {\n id: randomUUID(),\n proposalId: proposalId,\n sortOrder: index,\n actionType: action.actionType,\n description: action.description,\n payload: parsedPayload,\n status: 'pending',\n confidence: String(action.confidence.toFixed(2)),\n requiredFeature: action.requiredFeature || REQUIRED_FEATURES_MAP[action.actionType] || null,\n organizationId: email.organizationId,\n tenantId: email.tenantId,\n })\n }),\n ...extractionResult.draftReplies.map((reply, index) =>\n em.create(InboxProposalAction, {\n id: randomUUID(),\n proposalId: proposalId,\n sortOrder: combinedProposedActions.length + index,\n actionType: 'draft_reply',\n description: 'inbox_ops.action.desc.draft_reply',\n payload: {\n to: reply.to,\n toName: reply.toName,\n subject: reply.subject,\n body: reply.body,\n context: reply.context,\n replyTo: email.replyTo,\n inReplyToMessageId: email.messageId,\n references: email.emailReferences,\n },\n status: 'pending',\n confidence: String(extractionResult.confidence.toFixed(2)),\n requiredFeature: 'inbox_ops.replies.send',\n organizationId: email.organizationId,\n tenantId: email.tenantId,\n }),\n ),\n ]\n allActions.forEach((a) => em.persist(a))\n\n // Discrepancy actionIndex values reference extractionResult.proposedActions,\n // but allActions prepends auto-generated actions. Offset indices accordingly.\n const actionIndexOffset = autoContactActions.length + autoLinkActions.length + autoProductActions.length\n const offsetIndex = (d: DiscrepancyInput): DiscrepancyInput =>\n d.actionIndex !== undefined ? { ...d, actionIndex: d.actionIndex + actionIndexOffset } : d\n\n // Create discrepancies using factory\n const allDiscrepancies = [\n ...extractionResult.discrepancies.map((d) =>\n createDiscrepancy(em, proposalId, allActions, offsetIndex(d), scope),\n ),\n ...priceDiscrepancies.map((d) =>\n createDiscrepancy(em, proposalId, allActions, offsetIndex(d), scope),\n ),\n ...duplicateOrderDiscrepancies.map((d) =>\n createDiscrepancy(em, proposalId, allActions, offsetIndex(d), scope),\n ),\n ...productNotFoundDiscrepancies.map((d) =>\n createDiscrepancy(em, proposalId, allActions, offsetIndex(d), scope),\n ),\n ...enrichmentDiscrepancies.map((d) =>\n createDiscrepancy(em, proposalId, allActions, offsetIndex(d), scope),\n ),\n ]\n\n // Flag unmatched contacts as discrepancies (from header-based matches + LLM-discovered participants)\n const contactDiscrepancyEmails = new Set<string>()\n for (const match of contactMatches) {\n if (!match.match && match.participant.email) {\n const emailLower = match.participant.email.toLowerCase()\n contactDiscrepancyEmails.add(emailLower)\n allDiscrepancies.push(\n createDiscrepancy(em, proposalId, allActions, {\n type: 'unknown_contact',\n severity: 'warning',\n description: 'inbox_ops.discrepancy.desc.no_matching_contact',\n foundValue: `${match.participant.name} (${match.participant.email})`,\n }, scope),\n )\n }\n }\n for (const participant of enrichedParticipants) {\n if (participant.matchedContactId) continue\n const emailLower = (participant.email || '').toLowerCase()\n if (!emailLower || contactDiscrepancyEmails.has(emailLower)) continue\n contactDiscrepancyEmails.add(emailLower)\n allDiscrepancies.push(\n createDiscrepancy(em, proposalId, allActions, {\n type: 'unknown_contact',\n severity: 'warning',\n description: 'inbox_ops.discrepancy.desc.no_matching_contact',\n foundValue: `${participant.name} (${participant.email})`,\n }, scope),\n )\n }\n\n // Flag draft_reply actions that target unmatched contacts (blocks accept)\n const matchedEmails = new Set(\n contactMatches\n .filter((m) => m.match?.contactId)\n .map((m) => m.participant.email.toLowerCase()),\n )\n for (const [actionIndex, action] of allActions.entries()) {\n if (action.actionType !== 'draft_reply') continue\n const payload = action.payload as Record<string, unknown> | null\n const toEmail = typeof payload?.to === 'string' ? payload.to.trim().toLowerCase() : ''\n if (toEmail && !matchedEmails.has(toEmail)) {\n allDiscrepancies.push(\n createDiscrepancy(em, proposalId, allActions, {\n actionIndex,\n type: 'unknown_contact',\n severity: 'error',\n description: 'inbox_ops.discrepancy.desc.draft_reply_no_contact',\n foundValue: toEmail,\n }, scope),\n )\n }\n }\n\n allDiscrepancies.forEach((d) => em.persist(d))\n\n // Step 8: Update email status\n email.status = requiresReview ? 'needs_review' : 'processed'\n email.detectedLanguage = extractionResult.detectedLanguage || email.detectedLanguage\n\n await em.flush()\n\n // Step 9: Emit events\n try {\n await emitInboxOpsEvent('inbox_ops.email.processed', {\n emailId: email.id,\n tenantId: email.tenantId,\n organizationId: email.organizationId,\n })\n\n await emitInboxOpsEvent('inbox_ops.proposal.created', {\n proposalId: proposal.id,\n emailId: email.id,\n tenantId: email.tenantId,\n organizationId: email.organizationId,\n actionCount: allActions.length,\n discrepancyCount: allDiscrepancies.length,\n confidence: proposal.confidence,\n summary: proposal.summary,\n })\n } catch (eventError) {\n console.error('[inbox_ops:extraction-worker] Failed to emit events:', eventError)\n }\n } catch (err) {\n email.status = 'failed'\n email.processingError = err instanceof Error ? err.message : String(err)\n await em.flush()\n\n try {\n await emitInboxOpsEvent('inbox_ops.email.failed', {\n emailId: email.id,\n tenantId: email.tenantId,\n organizationId: email.organizationId,\n error: email.processingError,\n })\n } catch (eventError) {\n console.error('[inbox_ops:extraction-worker] Failed to emit email.failed event:', eventError)\n }\n\n console.error('[inbox_ops:extraction-worker] Extraction failed:', err)\n }\n}\n\nfunction normalizeOrderPayloadFields(payload: Record<string, unknown>): void {\n const lineItems = Array.isArray(payload.lineItems)\n ? (payload.lineItems as Record<string, unknown>[])\n : []\n for (const item of lineItems) {\n if (!item.productName && typeof item.description === 'string') {\n item.productName = item.description\n }\n if (typeof item.quantity === 'number') {\n item.quantity = String(item.quantity)\n }\n if (typeof item.unitPrice === 'number') {\n item.unitPrice = String(item.unitPrice)\n }\n }\n}\n\nfunction buildContactActionsForUnmatchedParticipants(\n contactMatches: { participant: { name: string; email: string }; match?: { contactId: string } | null }[],\n existingActions: { actionType: string; payloadJson: string }[],\n inboxAddress: string,\n): { actionType: 'create_contact'; description: string; confidence: number; requiredFeature: string; payloadJson: string }[] {\n const alreadyProposed = new Set(\n existingActions\n .filter((a) => a.actionType === 'create_contact')\n .map((a) => {\n const p = safeParsePayloadJson(a.payloadJson)\n return typeof p.email === 'string' ? p.email.toLowerCase() : ''\n })\n .filter(Boolean),\n )\n\n const inboxLower = (inboxAddress || '').toLowerCase()\n const systemPatterns = ['noreply', 'no-reply', 'donotreply', 'mailer-daemon', 'postmaster']\n\n return contactMatches\n .filter((m) => {\n if (m.match?.contactId) return false\n const emailLower = m.participant.email.toLowerCase()\n if (alreadyProposed.has(emailLower)) return false\n if (emailLower === inboxLower) return false\n return !systemPatterns.some((p) => emailLower.includes(p))\n })\n .map((m) => ({\n actionType: 'create_contact' as const,\n description: 'inbox_ops.action.desc.create_contact',\n confidence: 0.9,\n requiredFeature: REQUIRED_FEATURES_MAP.create_contact,\n payloadJson: JSON.stringify({\n type: 'person',\n name: m.participant.name,\n email: m.participant.email,\n source: 'inbox_ops',\n }),\n }))\n}\n\nfunction buildLinkContactActionsForMatchedParticipants(\n contactMatches: { participant: { name: string; email: string }; match?: { contactId: string; contactType?: string; contactName?: string } | null }[],\n existingActions: { actionType: string; payloadJson: string }[],\n inboxAddress: string,\n): { actionType: 'link_contact'; description: string; confidence: number; requiredFeature: string; payloadJson: string }[] {\n const alreadyProposed = new Set(\n existingActions\n .filter((a) => a.actionType === 'link_contact')\n .map((a) => {\n const p = safeParsePayloadJson(a.payloadJson)\n const email = typeof p.emailAddress === 'string' ? p.emailAddress : (typeof p.email === 'string' ? p.email : '')\n return email.toLowerCase()\n })\n .filter(Boolean),\n )\n\n const inboxLower = (inboxAddress || '').toLowerCase()\n const systemPatterns = ['noreply', 'no-reply', 'donotreply', 'mailer-daemon', 'postmaster']\n\n return contactMatches\n .filter((m) => {\n if (!m.match?.contactId) return false\n const emailLower = m.participant.email.toLowerCase()\n if (alreadyProposed.has(emailLower)) return false\n if (emailLower === inboxLower) return false\n return !systemPatterns.some((p) => emailLower.includes(p))\n })\n .map((m) => ({\n actionType: 'link_contact' as const,\n description: 'inbox_ops.action.desc.link_contact',\n confidence: 0.95,\n requiredFeature: REQUIRED_FEATURES_MAP.link_contact,\n payloadJson: JSON.stringify({\n emailAddress: m.participant.email,\n contactId: m.match!.contactId,\n contactType: m.match!.contactType || 'person',\n contactName: m.participant.name,\n }),\n }))\n}\n\nfunction buildContactActionsForUnmatchedLlmParticipants(\n enrichedParticipants: { name: string; email: string; matchedContactId?: string | null }[],\n contactMatches: { participant: { email: string } }[],\n existingActions: { actionType: string; payloadJson: string }[],\n alreadyAutoCreated: { payloadJson: string }[],\n inboxAddress: string,\n): { actionType: 'create_contact'; description: string; confidence: number; requiredFeature: string; payloadJson: string }[] {\n const headerEmails = new Set(\n contactMatches.map((m) => m.participant.email.toLowerCase()),\n )\n\n const alreadyProposed = new Set([\n ...existingActions\n .filter((a) => a.actionType === 'create_contact')\n .map((a) => {\n const p = safeParsePayloadJson(a.payloadJson)\n return typeof p.email === 'string' ? p.email.toLowerCase() : ''\n })\n .filter(Boolean),\n ...alreadyAutoCreated\n .map((a) => {\n const p = safeParsePayloadJson(a.payloadJson)\n return typeof p.email === 'string' ? p.email.toLowerCase() : ''\n })\n .filter(Boolean),\n ])\n\n const inboxLower = (inboxAddress || '').toLowerCase()\n const systemPatterns = ['noreply', 'no-reply', 'donotreply', 'mailer-daemon', 'postmaster']\n\n return enrichedParticipants\n .filter((p) => {\n if (p.matchedContactId) return false\n const emailLower = (p.email || '').toLowerCase()\n if (!emailLower) return false\n if (headerEmails.has(emailLower)) return false\n if (alreadyProposed.has(emailLower)) return false\n if (emailLower === inboxLower) return false\n return !systemPatterns.some((pat) => emailLower.includes(pat))\n })\n .map((p) => ({\n actionType: 'create_contact' as const,\n description: 'inbox_ops.action.desc.create_contact',\n confidence: 0.85,\n requiredFeature: REQUIRED_FEATURES_MAP.create_contact,\n payloadJson: JSON.stringify({\n type: 'person',\n name: p.name,\n email: p.email,\n source: 'inbox_ops',\n }),\n }))\n}\n\nasync function detectDuplicateOrders(\n em: EntityManager,\n orderActions: { actionType: string; payload: Record<string, unknown>; index: number }[],\n scope: { tenantId: string; organizationId: string },\n salesOrderClass?: EntityClass<{ id: string; orderNumber: string; customerReference?: string | null; tenantId?: string; organizationId?: string; deletedAt?: Date | null }>,\n): Promise<{ type: 'duplicate_order'; severity: 'error'; description: string; expectedValue: string | null; foundValue: string | null; actionIndex: number }[]> {\n if (!salesOrderClass) return []\n const discrepancies: { type: 'duplicate_order'; severity: 'error'; description: string; expectedValue: string | null; foundValue: string | null; actionIndex: number }[] = []\n\n for (const action of orderActions) {\n if (action.actionType !== 'create_order') continue\n\n const customerReference = typeof action.payload.customerReference === 'string'\n ? action.payload.customerReference.trim()\n : null\n\n if (!customerReference) continue\n\n try {\n const existing = await findOneWithDecryption(\n em,\n salesOrderClass,\n {\n customerReference,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n\n if (existing) {\n discrepancies.push({\n type: 'duplicate_order',\n severity: 'error',\n description: 'inbox_ops.discrepancy.desc.duplicate_order_reference',\n expectedValue: existing.orderNumber || existing.id,\n foundValue: customerReference,\n actionIndex: action.index,\n })\n }\n } catch {\n // Skip duplicate detection if lookup fails\n }\n }\n\n return discrepancies\n}\n\nfunction detectPartialForward(email: InboxEmail): boolean {\n const subject = email.subject || ''\n const hasReOrFw = /^(RE|FW|Fwd):/i.test(subject)\n const messageCount = email.threadMessages?.length || 0\n return hasReOrFw && messageCount < 2\n}\n\nfunction buildParticipantEmailMap(\n contactMatches: { participant: { name: string; email: string } }[],\n llmParticipants: { name: string; email: string }[],\n): Map<string, string> {\n const nameToEmail = new Map<string, string>()\n // Header-based participants are the most reliable source\n for (const m of contactMatches) {\n if (m.participant.name && m.participant.email) {\n nameToEmail.set(m.participant.name.trim().toLowerCase(), m.participant.email.trim().toLowerCase())\n }\n }\n // LLM-extracted participants as fallback (don't overwrite header-based)\n for (const p of llmParticipants) {\n if (p.name && p.email) {\n const key = p.name.trim().toLowerCase()\n if (!nameToEmail.has(key)) {\n nameToEmail.set(key, p.email.trim().toLowerCase())\n }\n }\n }\n return nameToEmail\n}\n\nfunction enrichCreateContactEmails(\n actions: { actionType: string; payloadJson: string }[],\n participantEmailMap: Map<string, string>,\n): void {\n for (const action of actions) {\n if (action.actionType !== 'create_contact') continue\n const payload = safeParsePayloadJson(action.payloadJson)\n if (payload.email) continue\n const name = typeof payload.name === 'string' ? payload.name.trim() : ''\n if (!name) continue\n // Try exact name match first, then partial (first part before / or ,)\n const email = participantEmailMap.get(name.toLowerCase())\n ?? findPartialNameMatch(name, participantEmailMap)\n if (email) {\n payload.email = email\n action.payloadJson = JSON.stringify(payload)\n }\n }\n}\n\nfunction enrichDraftReplyTargets(\n draftReplies: { to: string; toName?: string; subject: string; body: string; context?: string }[],\n participantEmailMap: Map<string, string>,\n): void {\n const knownEmails = new Set(participantEmailMap.values())\n for (const reply of draftReplies) {\n const toEmail = reply.to.trim().toLowerCase()\n if (knownEmails.has(toEmail)) continue\n // The LLM hallucinated an email \u2014 try to resolve via toName\n const toName = (reply.toName || '').trim()\n if (!toName) continue\n const correctedEmail = participantEmailMap.get(toName.toLowerCase())\n ?? findPartialNameMatch(toName, participantEmailMap)\n if (correctedEmail) {\n reply.to = correctedEmail\n }\n }\n}\n\nfunction buildFullTextForExtraction(email: InboxEmail): string {\n let text = email.rawText || ''\n if (!text && email.rawHtml) {\n text = htmlToPlainText(email.rawHtml)\n }\n return text\n .replace(/\\r\\n/g, '\\n')\n .replace(/\\r/g, '\\n')\n .replace(/\\t/g, ' ')\n .replace(/ {2,}/g, ' ')\n .replace(/\\n{3,}/g, '\\n\\n')\n .trim()\n}\n\nfunction findPartialNameMatch(name: string, map: Map<string, string>): string | undefined {\n const lower = name.toLowerCase()\n // Split on common separators (e.g. \"Marco Rossi / Rossi Imports S.r.l.\")\n const parts = lower.split(/\\s*[\\/,]\\s*/).map((p) => p.trim()).filter(Boolean)\n for (const part of parts) {\n const match = map.get(part)\n if (match) return match\n }\n // Try matching first+last name against map keys\n for (const [mapName, mapEmail] of map) {\n if (lower.includes(mapName) || mapName.includes(lower)) {\n return mapEmail\n }\n for (const part of parts) {\n if (part.includes(mapName) || mapName.includes(part)) {\n return mapEmail\n }\n }\n }\n return undefined\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["payload"]
7
7
  }
@@ -0,0 +1,278 @@
1
+ import { orderPayloadSchema, updateOrderPayloadSchema, updateShipmentPayloadSchema } from "../inbox_ops/data/validators.js";
2
+ import {
3
+ asHelperContext,
4
+ ExecutionError,
5
+ executeCommand,
6
+ buildSourceMetadata,
7
+ resolveOrderByReference,
8
+ resolveFirstChannelId,
9
+ resolveChannelCurrency,
10
+ resolveEffectiveDocumentKind,
11
+ resolveShipmentStatusEntryId,
12
+ resolveCustomerEntityIdByEmail,
13
+ resolveEntityClass,
14
+ normalizeAddressSnapshot,
15
+ parseDateToken,
16
+ parseNumberToken,
17
+ loadOrderLineItems,
18
+ matchLineItemByName
19
+ } from "../inbox_ops/lib/executionHelpers.js";
20
+ async function executeCreateDocumentAction(action, ctx, kind) {
21
+ const hCtx = asHelperContext(ctx);
22
+ const payload = action.payload;
23
+ let resolvedChannelId = payload.channelId;
24
+ if (!resolvedChannelId) {
25
+ resolvedChannelId = await resolveFirstChannelId(hCtx) ?? void 0;
26
+ if (!resolvedChannelId) {
27
+ throw new ExecutionError("No sales channel available. Create a channel first or set channelId in the payload.", 400);
28
+ }
29
+ }
30
+ const currencyCode = payload.currencyCode.trim().toUpperCase();
31
+ const lines = payload.lineItems.map((line, index) => {
32
+ const quantity = parseNumberToken(line.quantity, `lineItems[${index}].quantity`);
33
+ const unitPrice = line.unitPrice ? parseNumberToken(line.unitPrice, `lineItems[${index}].unitPrice`) : void 0;
34
+ const mappedLine = {
35
+ lineNumber: index + 1,
36
+ kind: line.kind ?? (line.productId ? "product" : "service"),
37
+ name: line.productName,
38
+ description: line.description,
39
+ quantity,
40
+ currencyCode
41
+ };
42
+ if (line.productId) mappedLine.productId = line.productId;
43
+ if (line.variantId) mappedLine.productVariantId = line.variantId;
44
+ if (unitPrice !== void 0) mappedLine.unitPriceNet = unitPrice;
45
+ if (line.sku || line.catalogPrice) {
46
+ mappedLine.catalogSnapshot = {
47
+ sku: line.sku ?? null,
48
+ catalogPrice: line.catalogPrice ?? null
49
+ };
50
+ }
51
+ return mappedLine;
52
+ });
53
+ const metadata = buildSourceMetadata(action.id, action.proposalId);
54
+ let resolvedCustomerEntityId = payload.customerEntityId;
55
+ if (!resolvedCustomerEntityId && payload.customerEmail) {
56
+ resolvedCustomerEntityId = await resolveCustomerEntityIdByEmail(hCtx, payload.customerEmail) ?? void 0;
57
+ }
58
+ const createInput = {
59
+ organizationId: hCtx.organizationId,
60
+ tenantId: hCtx.tenantId,
61
+ customerEntityId: resolvedCustomerEntityId,
62
+ customerReference: payload.customerReference,
63
+ channelId: resolvedChannelId,
64
+ currencyCode,
65
+ taxRateId: payload.taxRateId,
66
+ comments: payload.notes,
67
+ metadata,
68
+ lines
69
+ };
70
+ if (!resolvedCustomerEntityId) {
71
+ createInput.customerSnapshot = {
72
+ displayName: payload.customerName,
73
+ ...payload.customerEmail && { primaryEmail: payload.customerEmail }
74
+ };
75
+ }
76
+ const normalizedBilling = payload.billingAddress ? normalizeAddressSnapshot(payload.billingAddress) : void 0;
77
+ const normalizedShipping = payload.shippingAddress ? normalizeAddressSnapshot(payload.shippingAddress) : void 0;
78
+ if (normalizedShipping || normalizedBilling) {
79
+ createInput.shippingAddressSnapshot = normalizedShipping ?? normalizedBilling;
80
+ createInput.billingAddressSnapshot = normalizedBilling ?? normalizedShipping;
81
+ } else if (payload.billingAddressId || payload.shippingAddressId) {
82
+ createInput.billingAddressId = payload.billingAddressId ?? payload.shippingAddressId;
83
+ createInput.shippingAddressId = payload.shippingAddressId ?? payload.billingAddressId;
84
+ }
85
+ const requestedDeliveryAt = parseDateToken(payload.requestedDeliveryDate ?? void 0);
86
+ if (requestedDeliveryAt) {
87
+ createInput.expectedDeliveryAt = requestedDeliveryAt;
88
+ }
89
+ const effectiveKind = kind === "order" ? await resolveEffectiveDocumentKind(hCtx, resolvedChannelId) : kind;
90
+ if (effectiveKind === "order") {
91
+ const result2 = await executeCommand(
92
+ hCtx,
93
+ "sales.orders.create",
94
+ createInput
95
+ );
96
+ if (!result2.orderId) {
97
+ throw new ExecutionError("Order creation did not return an order ID", 500);
98
+ }
99
+ return { createdEntityId: result2.orderId, createdEntityType: "sales_order" };
100
+ }
101
+ const result = await executeCommand(
102
+ hCtx,
103
+ "sales.quotes.create",
104
+ createInput
105
+ );
106
+ if (!result.quoteId) {
107
+ throw new ExecutionError("Quote creation did not return a quote ID", 500);
108
+ }
109
+ return { createdEntityId: result.quoteId, createdEntityType: "sales_quote" };
110
+ }
111
+ async function executeUpdateOrderAction(action, ctx) {
112
+ const hCtx = asHelperContext(ctx);
113
+ const payload = action.payload;
114
+ const order = await resolveOrderByReference(hCtx, payload.orderId, payload.orderNumber);
115
+ const updateInput = {
116
+ id: order.id,
117
+ organizationId: hCtx.organizationId,
118
+ tenantId: hCtx.tenantId
119
+ };
120
+ const newDeliveryDate = parseDateToken(payload.deliveryDateChange?.newDate);
121
+ if (newDeliveryDate) {
122
+ updateInput.expectedDeliveryAt = newDeliveryDate;
123
+ }
124
+ const noteLines = payload.noteAdditions?.map((note) => note.trim()).filter((note) => note.length > 0) ?? [];
125
+ if (noteLines.length > 0) {
126
+ const mergedNotes = [order.comments ?? null, ...noteLines].filter(Boolean).join("\n");
127
+ updateInput.comments = mergedNotes;
128
+ }
129
+ if (Object.keys(updateInput).length > 3) {
130
+ await executeCommand(
131
+ hCtx,
132
+ "sales.orders.update",
133
+ updateInput
134
+ );
135
+ }
136
+ const quantityChanges = payload.quantityChanges ?? [];
137
+ const orderLines = quantityChanges.length > 0 && quantityChanges.some((qc) => !qc.lineItemId) ? await loadOrderLineItems(hCtx, order.id) : [];
138
+ for (const quantityChange of quantityChanges) {
139
+ let lineItemId = quantityChange.lineItemId;
140
+ if (!lineItemId) {
141
+ const matched = matchLineItemByName(orderLines, quantityChange.lineItemName);
142
+ if (matched) {
143
+ lineItemId = matched;
144
+ } else {
145
+ const availableNames = orderLines.map((l) => l.name).filter(Boolean).join(", ");
146
+ throw new ExecutionError(
147
+ `Cannot resolve line item "${quantityChange.lineItemName}". Available line items: ${availableNames || "none"}`,
148
+ 400
149
+ );
150
+ }
151
+ }
152
+ await executeCommand(
153
+ hCtx,
154
+ "sales.orders.lines.upsert",
155
+ {
156
+ body: {
157
+ id: lineItemId,
158
+ orderId: order.id,
159
+ organizationId: hCtx.organizationId,
160
+ tenantId: hCtx.tenantId,
161
+ quantity: parseNumberToken(quantityChange.newQuantity, "quantityChanges.newQuantity"),
162
+ currencyCode: order.currencyCode
163
+ }
164
+ }
165
+ );
166
+ }
167
+ return { createdEntityId: order.id, createdEntityType: "sales_order" };
168
+ }
169
+ async function executeUpdateShipmentAction(action, ctx) {
170
+ const hCtx = asHelperContext(ctx);
171
+ const payload = action.payload;
172
+ const order = await resolveOrderByReference(hCtx, payload.orderId, payload.orderNumber);
173
+ const SalesShipmentClass = resolveEntityClass(hCtx, "SalesShipment");
174
+ if (!SalesShipmentClass) {
175
+ throw new ExecutionError("Sales module entities not available", 503);
176
+ }
177
+ const { findOneWithDecryption } = await import("@open-mercato/shared/lib/encryption/find");
178
+ const shipment = await findOneWithDecryption(
179
+ hCtx.em,
180
+ SalesShipmentClass,
181
+ {
182
+ order: order.id,
183
+ tenantId: hCtx.tenantId,
184
+ organizationId: hCtx.organizationId,
185
+ deletedAt: null
186
+ },
187
+ { orderBy: { createdAt: "DESC" } },
188
+ { tenantId: hCtx.tenantId, organizationId: hCtx.organizationId }
189
+ );
190
+ if (!shipment) {
191
+ throw new ExecutionError("No shipment found for the referenced order", 404);
192
+ }
193
+ const statusEntryId = await resolveShipmentStatusEntryId(hCtx, payload.statusLabel);
194
+ if (!statusEntryId) {
195
+ throw new ExecutionError(`Shipment status "${payload.statusLabel}" not found`, 400);
196
+ }
197
+ const updateInput = {
198
+ id: shipment.id,
199
+ orderId: order.id,
200
+ organizationId: hCtx.organizationId,
201
+ tenantId: hCtx.tenantId,
202
+ statusEntryId
203
+ };
204
+ if (payload.trackingNumbers) updateInput.trackingNumbers = payload.trackingNumbers;
205
+ if (payload.carrierName) updateInput.carrierName = payload.carrierName;
206
+ if (payload.notes) updateInput.notes = payload.notes;
207
+ const shippedAt = parseDateToken(payload.shippedAt);
208
+ const deliveredAt = parseDateToken(payload.deliveredAt);
209
+ if (shippedAt) updateInput.shippedAt = shippedAt;
210
+ if (deliveredAt) updateInput.deliveredAt = deliveredAt;
211
+ await executeCommand(
212
+ hCtx,
213
+ "sales.shipments.update",
214
+ updateInput
215
+ );
216
+ return { createdEntityId: shipment.id, createdEntityType: "sales_shipment" };
217
+ }
218
+ async function normalizeOrderPayload(payload, ctx) {
219
+ if (!payload.currencyCode) {
220
+ const hCtx = asHelperContext(ctx);
221
+ const channelId = typeof payload.channelId === "string" ? payload.channelId : null;
222
+ const resolved = await resolveChannelCurrency(hCtx, channelId);
223
+ if (resolved) payload.currencyCode = resolved;
224
+ }
225
+ return payload;
226
+ }
227
+ const inboxActions = [
228
+ {
229
+ type: "create_order",
230
+ requiredFeature: "sales.orders.manage",
231
+ payloadSchema: orderPayloadSchema,
232
+ label: "Create Sales Order",
233
+ promptSchema: `create_order / create_quote payload:
234
+ { customerName: string, customerEmail?: string, customerEntityId?: uuid, channelId?: uuid, currencyCode: string (3-letter ISO), taxRateId?: uuid, lineItems: [{ productName: string (REQUIRED), productId?: uuid, variantId?: uuid, sku?: string, quantity: string, unitPrice?: string, kind?: "product"|"service", description?: string }], requestedDeliveryDate?: string, notes?: string, customerReference?: string (customer's own PO number or reference code \u2014 only set if explicitly stated in the email, do NOT use the email subject), shippingAddress?: { line1?: string, line2?: string, city?: string, state?: string, postalCode?: string, country?: string, company?: string, contactName?: string }, billingAddress?: { line1?: string, line2?: string, city?: string, state?: string, postalCode?: string, country?: string, company?: string, contactName?: string } }`,
235
+ promptRules: [
236
+ "ALWAYS propose a create_order or create_quote action when the customer expresses interest in buying, even if some product names are uncertain or not in the catalog. Use the best product name available; the system will flag unmatched products as discrepancies. Do NOT replace an order with a draft_reply asking for clarification \u2014 propose both if needed.",
237
+ `Use create_order when the customer has clearly confirmed they want to proceed (e.g., "let's go ahead", "please process", "confirmed"). Use create_quote when the customer is still inquiring, requesting pricing, asking for a proposal, or negotiating (e.g., "could you send a quote", "what would it cost", "we're interested in", "can you offer"). When in doubt, prefer create_quote.`,
238
+ 'For create_order / create_quote: each line item MUST have "productName" (the product name goes here, NOT in "description"). Include currencyCode and customerName.',
239
+ "For create_order / create_quote: extract shippingAddress and billingAddress as structured objects when addresses are mentioned. Parse street, city, postal code, country from the text. Do NOT put address data in notes."
240
+ ],
241
+ normalizePayload: normalizeOrderPayload,
242
+ execute: (action, ctx) => executeCreateDocumentAction(action, ctx, "order")
243
+ },
244
+ {
245
+ type: "create_quote",
246
+ requiredFeature: "sales.quotes.manage",
247
+ payloadSchema: orderPayloadSchema,
248
+ label: "Create Quote",
249
+ promptSchema: "(shared with create_order)",
250
+ normalizePayload: normalizeOrderPayload,
251
+ execute: (action, ctx) => executeCreateDocumentAction(action, ctx, "quote")
252
+ },
253
+ {
254
+ type: "update_order",
255
+ requiredFeature: "sales.orders.manage",
256
+ payloadSchema: updateOrderPayloadSchema,
257
+ label: "Update Order",
258
+ promptSchema: `update_order payload:
259
+ { orderId?: uuid, orderNumber?: string, quantityChanges?: [{ lineItemName: string, lineItemId?: uuid, oldQuantity?: string, newQuantity: string }], deliveryDateChange?: { oldDate?: string, newDate: string }, noteAdditions?: string[] }`,
260
+ execute: executeUpdateOrderAction
261
+ },
262
+ {
263
+ type: "update_shipment",
264
+ requiredFeature: "sales.shipments.manage",
265
+ payloadSchema: updateShipmentPayloadSchema,
266
+ label: "Update Shipment",
267
+ promptSchema: `update_shipment payload:
268
+ { orderId?: uuid, orderNumber?: string, trackingNumbers?: string[], carrierName?: string, statusLabel: string, shippedAt?: string, deliveredAt?: string, estimatedDelivery?: string, notes?: string }`,
269
+ promptRules: ["For update_shipment: use statusLabel text only."],
270
+ execute: executeUpdateShipmentAction
271
+ }
272
+ ];
273
+ var inbox_actions_default = inboxActions;
274
+ export {
275
+ inbox_actions_default as default,
276
+ inboxActions
277
+ };
278
+ //# sourceMappingURL=inbox-actions.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/modules/sales/inbox-actions.ts"],
4
+ "sourcesContent": ["import type { InboxActionDefinition, InboxActionExecutionContext } from '@open-mercato/shared/modules/inbox-actions'\nimport { orderPayloadSchema, updateOrderPayloadSchema, updateShipmentPayloadSchema } from '../inbox_ops/data/validators'\nimport type { OrderPayload, UpdateOrderPayload, UpdateShipmentPayload } from '../inbox_ops/data/validators'\nimport {\n asHelperContext,\n ExecutionError,\n executeCommand,\n buildSourceMetadata,\n resolveOrderByReference,\n resolveFirstChannelId,\n resolveChannelCurrency,\n resolveEffectiveDocumentKind,\n resolveShipmentStatusEntryId,\n resolveCustomerEntityIdByEmail,\n resolveEntityClass,\n normalizeAddressSnapshot,\n parseDateToken,\n parseNumberToken,\n loadOrderLineItems,\n matchLineItemByName,\n} from '../inbox_ops/lib/executionHelpers'\n\n// ---------------------------------------------------------------------------\n// create_order\n// ---------------------------------------------------------------------------\n\nasync function executeCreateDocumentAction(\n action: { id: string; proposalId: string; payload: unknown },\n ctx: InboxActionExecutionContext,\n kind: 'order' | 'quote',\n): Promise<{ createdEntityId?: string | null; createdEntityType?: string | null }> {\n const hCtx = asHelperContext(ctx)\n const payload = action.payload as OrderPayload\n\n let resolvedChannelId: string | undefined = payload.channelId\n if (!resolvedChannelId) {\n resolvedChannelId = (await resolveFirstChannelId(hCtx)) ?? undefined\n if (!resolvedChannelId) {\n throw new ExecutionError('No sales channel available. Create a channel first or set channelId in the payload.', 400)\n }\n }\n\n const currencyCode = payload.currencyCode.trim().toUpperCase()\n const lines = payload.lineItems.map((line, index) => {\n const quantity = parseNumberToken(line.quantity, `lineItems[${index}].quantity`)\n const unitPrice = line.unitPrice\n ? parseNumberToken(line.unitPrice, `lineItems[${index}].unitPrice`)\n : undefined\n\n const mappedLine: Record<string, unknown> = {\n lineNumber: index + 1,\n kind: line.kind ?? (line.productId ? 'product' : 'service'),\n name: line.productName,\n description: line.description,\n quantity,\n currencyCode,\n }\n\n if (line.productId) mappedLine.productId = line.productId\n if (line.variantId) mappedLine.productVariantId = line.variantId\n if (unitPrice !== undefined) mappedLine.unitPriceNet = unitPrice\n if (line.sku || line.catalogPrice) {\n mappedLine.catalogSnapshot = {\n sku: line.sku ?? null,\n catalogPrice: line.catalogPrice ?? null,\n }\n }\n\n return mappedLine\n })\n\n const metadata = buildSourceMetadata(action.id, action.proposalId)\n\n let resolvedCustomerEntityId = payload.customerEntityId\n if (!resolvedCustomerEntityId && payload.customerEmail) {\n resolvedCustomerEntityId = (await resolveCustomerEntityIdByEmail(hCtx, payload.customerEmail)) ?? undefined\n }\n\n const createInput: Record<string, unknown> = {\n organizationId: hCtx.organizationId,\n tenantId: hCtx.tenantId,\n customerEntityId: resolvedCustomerEntityId,\n customerReference: payload.customerReference,\n channelId: resolvedChannelId,\n currencyCode,\n taxRateId: payload.taxRateId,\n comments: payload.notes,\n metadata,\n lines,\n }\n\n if (!resolvedCustomerEntityId) {\n createInput.customerSnapshot = {\n displayName: payload.customerName,\n ...(payload.customerEmail && { primaryEmail: payload.customerEmail }),\n }\n }\n\n const normalizedBilling = payload.billingAddress\n ? normalizeAddressSnapshot(payload.billingAddress)\n : undefined\n const normalizedShipping = payload.shippingAddress\n ? normalizeAddressSnapshot(payload.shippingAddress)\n : undefined\n\n if (normalizedShipping || normalizedBilling) {\n createInput.shippingAddressSnapshot = normalizedShipping ?? normalizedBilling\n createInput.billingAddressSnapshot = normalizedBilling ?? normalizedShipping\n } else if (payload.billingAddressId || payload.shippingAddressId) {\n createInput.billingAddressId = payload.billingAddressId ?? payload.shippingAddressId\n createInput.shippingAddressId = payload.shippingAddressId ?? payload.billingAddressId\n }\n\n const requestedDeliveryAt = parseDateToken(payload.requestedDeliveryDate ?? undefined)\n if (requestedDeliveryAt) {\n createInput.expectedDeliveryAt = requestedDeliveryAt\n }\n\n const effectiveKind = kind === 'order'\n ? await resolveEffectiveDocumentKind(hCtx, resolvedChannelId)\n : kind\n\n if (effectiveKind === 'order') {\n const result = await executeCommand<Record<string, unknown>, { orderId?: string }>(\n hCtx,\n 'sales.orders.create',\n createInput,\n )\n if (!result.orderId) {\n throw new ExecutionError('Order creation did not return an order ID', 500)\n }\n return { createdEntityId: result.orderId, createdEntityType: 'sales_order' }\n }\n\n const result = await executeCommand<Record<string, unknown>, { quoteId?: string }>(\n hCtx,\n 'sales.quotes.create',\n createInput,\n )\n if (!result.quoteId) {\n throw new ExecutionError('Quote creation did not return a quote ID', 500)\n }\n return { createdEntityId: result.quoteId, createdEntityType: 'sales_quote' }\n}\n\n// ---------------------------------------------------------------------------\n// update_order\n// ---------------------------------------------------------------------------\n\nasync function executeUpdateOrderAction(\n action: { id: string; proposalId: string; payload: unknown },\n ctx: InboxActionExecutionContext,\n): Promise<{ createdEntityId?: string | null; createdEntityType?: string | null }> {\n const hCtx = asHelperContext(ctx)\n const payload = action.payload as UpdateOrderPayload\n\n const order = await resolveOrderByReference(hCtx, payload.orderId, payload.orderNumber)\n\n const updateInput: Record<string, unknown> = {\n id: order.id,\n organizationId: hCtx.organizationId,\n tenantId: hCtx.tenantId,\n }\n\n const newDeliveryDate = parseDateToken(payload.deliveryDateChange?.newDate)\n if (newDeliveryDate) {\n updateInput.expectedDeliveryAt = newDeliveryDate\n }\n\n const noteLines = payload.noteAdditions?.map((note) => note.trim()).filter((note) => note.length > 0) ?? []\n if (noteLines.length > 0) {\n const mergedNotes = [order.comments ?? null, ...noteLines].filter(Boolean).join('\\n')\n updateInput.comments = mergedNotes\n }\n\n if (Object.keys(updateInput).length > 3) {\n await executeCommand<Record<string, unknown>, { orderId?: string }>(\n hCtx,\n 'sales.orders.update',\n updateInput,\n )\n }\n\n const quantityChanges = payload.quantityChanges ?? []\n const orderLines = quantityChanges.length > 0 && quantityChanges.some((qc) => !qc.lineItemId)\n ? await loadOrderLineItems(hCtx, order.id)\n : []\n\n for (const quantityChange of quantityChanges) {\n let lineItemId = quantityChange.lineItemId\n if (!lineItemId) {\n const matched = matchLineItemByName(orderLines, quantityChange.lineItemName)\n if (matched) {\n lineItemId = matched\n } else {\n const availableNames = orderLines.map((l) => l.name).filter(Boolean).join(', ')\n throw new ExecutionError(\n `Cannot resolve line item \"${quantityChange.lineItemName}\". Available line items: ${availableNames || 'none'}`,\n 400,\n )\n }\n }\n\n await executeCommand<{ body: Record<string, unknown> }, { orderId?: string; lineId?: string }>(\n hCtx,\n 'sales.orders.lines.upsert',\n {\n body: {\n id: lineItemId,\n orderId: order.id,\n organizationId: hCtx.organizationId,\n tenantId: hCtx.tenantId,\n quantity: parseNumberToken(quantityChange.newQuantity, 'quantityChanges.newQuantity'),\n currencyCode: order.currencyCode,\n },\n },\n )\n }\n\n return { createdEntityId: order.id, createdEntityType: 'sales_order' }\n}\n\n// ---------------------------------------------------------------------------\n// update_shipment\n// ---------------------------------------------------------------------------\n\nasync function executeUpdateShipmentAction(\n action: { id: string; proposalId: string; payload: unknown },\n ctx: InboxActionExecutionContext,\n): Promise<{ createdEntityId?: string | null; createdEntityType?: string | null }> {\n const hCtx = asHelperContext(ctx)\n const payload = action.payload as UpdateShipmentPayload\n\n const order = await resolveOrderByReference(hCtx, payload.orderId, payload.orderNumber)\n\n const SalesShipmentClass = resolveEntityClass(hCtx, 'SalesShipment')\n if (!SalesShipmentClass) {\n throw new ExecutionError('Sales module entities not available', 503)\n }\n\n const { findOneWithDecryption } = await import('@open-mercato/shared/lib/encryption/find')\n\n const shipment = await findOneWithDecryption(\n hCtx.em,\n SalesShipmentClass,\n {\n order: order.id,\n tenantId: hCtx.tenantId,\n organizationId: hCtx.organizationId,\n deletedAt: null,\n },\n { orderBy: { createdAt: 'DESC' } },\n { tenantId: hCtx.tenantId, organizationId: hCtx.organizationId },\n )\n\n if (!shipment) {\n throw new ExecutionError('No shipment found for the referenced order', 404)\n }\n\n const statusEntryId = await resolveShipmentStatusEntryId(hCtx, payload.statusLabel)\n if (!statusEntryId) {\n throw new ExecutionError(`Shipment status \"${payload.statusLabel}\" not found`, 400)\n }\n\n const updateInput: Record<string, unknown> = {\n id: shipment.id,\n orderId: order.id,\n organizationId: hCtx.organizationId,\n tenantId: hCtx.tenantId,\n statusEntryId,\n }\n\n if (payload.trackingNumbers) updateInput.trackingNumbers = payload.trackingNumbers\n if (payload.carrierName) updateInput.carrierName = payload.carrierName\n if (payload.notes) updateInput.notes = payload.notes\n\n const shippedAt = parseDateToken(payload.shippedAt)\n const deliveredAt = parseDateToken(payload.deliveredAt)\n if (shippedAt) updateInput.shippedAt = shippedAt\n if (deliveredAt) updateInput.deliveredAt = deliveredAt\n\n await executeCommand<Record<string, unknown>, { shipmentId?: string }>(\n hCtx,\n 'sales.shipments.update',\n updateInput,\n )\n\n return { createdEntityId: shipment.id, createdEntityType: 'sales_shipment' }\n}\n\n// ---------------------------------------------------------------------------\n// Normalization helper for order/quote payloads\n// ---------------------------------------------------------------------------\n\nasync function normalizeOrderPayload(\n payload: Record<string, unknown>,\n ctx: InboxActionExecutionContext,\n): Promise<Record<string, unknown>> {\n if (!payload.currencyCode) {\n const hCtx = asHelperContext(ctx)\n const channelId = typeof payload.channelId === 'string' ? payload.channelId : null\n const resolved = await resolveChannelCurrency(hCtx, channelId)\n if (resolved) payload.currencyCode = resolved\n }\n return payload\n}\n\n// ---------------------------------------------------------------------------\n// Exported action definitions\n// ---------------------------------------------------------------------------\n\nexport const inboxActions: InboxActionDefinition[] = [\n {\n type: 'create_order',\n requiredFeature: 'sales.orders.manage',\n payloadSchema: orderPayloadSchema,\n label: 'Create Sales Order',\n promptSchema: `create_order / create_quote payload:\n{ customerName: string, customerEmail?: string, customerEntityId?: uuid, channelId?: uuid, currencyCode: string (3-letter ISO), taxRateId?: uuid, lineItems: [{ productName: string (REQUIRED), productId?: uuid, variantId?: uuid, sku?: string, quantity: string, unitPrice?: string, kind?: \"product\"|\"service\", description?: string }], requestedDeliveryDate?: string, notes?: string, customerReference?: string (customer's own PO number or reference code \u2014 only set if explicitly stated in the email, do NOT use the email subject), shippingAddress?: { line1?: string, line2?: string, city?: string, state?: string, postalCode?: string, country?: string, company?: string, contactName?: string }, billingAddress?: { line1?: string, line2?: string, city?: string, state?: string, postalCode?: string, country?: string, company?: string, contactName?: string } }`,\n promptRules: [\n 'ALWAYS propose a create_order or create_quote action when the customer expresses interest in buying, even if some product names are uncertain or not in the catalog. Use the best product name available; the system will flag unmatched products as discrepancies. Do NOT replace an order with a draft_reply asking for clarification \u2014 propose both if needed.',\n 'Use create_order when the customer has clearly confirmed they want to proceed (e.g., \"let\\'s go ahead\", \"please process\", \"confirmed\"). Use create_quote when the customer is still inquiring, requesting pricing, asking for a proposal, or negotiating (e.g., \"could you send a quote\", \"what would it cost\", \"we\\'re interested in\", \"can you offer\"). When in doubt, prefer create_quote.',\n 'For create_order / create_quote: each line item MUST have \"productName\" (the product name goes here, NOT in \"description\"). Include currencyCode and customerName.',\n 'For create_order / create_quote: extract shippingAddress and billingAddress as structured objects when addresses are mentioned. Parse street, city, postal code, country from the text. Do NOT put address data in notes.',\n ],\n normalizePayload: normalizeOrderPayload,\n execute: (action, ctx) => executeCreateDocumentAction(action, ctx, 'order'),\n },\n {\n type: 'create_quote',\n requiredFeature: 'sales.quotes.manage',\n payloadSchema: orderPayloadSchema,\n label: 'Create Quote',\n promptSchema: '(shared with create_order)',\n normalizePayload: normalizeOrderPayload,\n execute: (action, ctx) => executeCreateDocumentAction(action, ctx, 'quote'),\n },\n {\n type: 'update_order',\n requiredFeature: 'sales.orders.manage',\n payloadSchema: updateOrderPayloadSchema,\n label: 'Update Order',\n promptSchema: `update_order payload:\n{ orderId?: uuid, orderNumber?: string, quantityChanges?: [{ lineItemName: string, lineItemId?: uuid, oldQuantity?: string, newQuantity: string }], deliveryDateChange?: { oldDate?: string, newDate: string }, noteAdditions?: string[] }`,\n execute: executeUpdateOrderAction,\n },\n {\n type: 'update_shipment',\n requiredFeature: 'sales.shipments.manage',\n payloadSchema: updateShipmentPayloadSchema,\n label: 'Update Shipment',\n promptSchema: `update_shipment payload:\n{ orderId?: uuid, orderNumber?: string, trackingNumbers?: string[], carrierName?: string, statusLabel: string, shippedAt?: string, deliveredAt?: string, estimatedDelivery?: string, notes?: string }`,\n promptRules: ['For update_shipment: use statusLabel text only.'],\n execute: executeUpdateShipmentAction,\n },\n]\n\nexport default inboxActions\n"],
5
+ "mappings": "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",
6
+ "names": ["result"]
7
+ }
package/jest.config.cjs CHANGED
@@ -9,6 +9,7 @@ module.exports = {
9
9
  '^#generated/(.*)$': '<rootDir>/generated/$1',
10
10
  '^@open-mercato/core/generated/(.*)$': '<rootDir>/generated/$1',
11
11
  '^@open-mercato/core/(.*)$': '<rootDir>/src/$1',
12
+ '^@/\\.mercato/generated/inbox-actions\\.generated$': '<rootDir>/jest.mocks/inbox-actions.generated.js',
12
13
  '^react-markdown$': '<rootDir>/jest.mocks/react-markdown.js',
13
14
  '^remark-gfm$': '<rootDir>/jest.mocks/remark-gfm.js',
14
15
  },
@@ -0,0 +1,5 @@
1
+ module.exports = {
2
+ inboxActions: [],
3
+ getInboxAction: () => undefined,
4
+ getRegisteredActionTypes: () => [],
5
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.4.5-develop-811deeb983",
3
+ "version": "0.4.5-develop-3d8e759e45",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -207,7 +207,7 @@
207
207
  }
208
208
  },
209
209
  "dependencies": {
210
- "@open-mercato/shared": "0.4.5-develop-811deeb983",
210
+ "@open-mercato/shared": "0.4.5-develop-3d8e759e45",
211
211
  "@types/html-to-text": "^9.0.4",
212
212
  "@types/semver": "^7.5.8",
213
213
  "@xyflow/react": "^12.6.0",