@open-mercato/core 0.4.8-develop-15259be22b → 0.4.8-develop-280c02b529

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 (110) hide show
  1. package/dist/generated/entities/inbox_proposal/index.js +2 -0
  2. package/dist/generated/entities/inbox_proposal/index.js.map +2 -2
  3. package/dist/modules/catalog/inbox-actions.js +49 -0
  4. package/dist/modules/catalog/inbox-actions.js.map +2 -2
  5. package/dist/modules/customers/inbox-actions.js +69 -27
  6. package/dist/modules/customers/inbox-actions.js.map +3 -3
  7. package/dist/modules/inbox_ops/ai-tools.js +346 -0
  8. package/dist/modules/inbox_ops/ai-tools.js.map +7 -0
  9. package/dist/modules/inbox_ops/api/extract/route.js +3 -2
  10. package/dist/modules/inbox_ops/api/extract/route.js.map +2 -2
  11. package/dist/modules/inbox_ops/api/proposals/[id]/accept-all/route.js +4 -0
  12. package/dist/modules/inbox_ops/api/proposals/[id]/accept-all/route.js.map +2 -2
  13. package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/accept/route.js +4 -0
  14. package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/accept/route.js.map +2 -2
  15. package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/complete/route.js +4 -0
  16. package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/complete/route.js.map +2 -2
  17. package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/reject/route.js +4 -0
  18. package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/reject/route.js.map +2 -2
  19. package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/route.js +4 -0
  20. package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/route.js.map +2 -2
  21. package/dist/modules/inbox_ops/api/proposals/[id]/categorize/route.js +59 -0
  22. package/dist/modules/inbox_ops/api/proposals/[id]/categorize/route.js.map +7 -0
  23. package/dist/modules/inbox_ops/api/proposals/[id]/reject/route.js +4 -0
  24. package/dist/modules/inbox_ops/api/proposals/[id]/reject/route.js.map +2 -2
  25. package/dist/modules/inbox_ops/api/proposals/[id]/replies/[replyId]/send/route.js +34 -14
  26. package/dist/modules/inbox_ops/api/proposals/[id]/replies/[replyId]/send/route.js.map +2 -2
  27. package/dist/modules/inbox_ops/api/proposals/counts/route.js +49 -4
  28. package/dist/modules/inbox_ops/api/proposals/counts/route.js.map +2 -2
  29. package/dist/modules/inbox_ops/api/proposals/route.js +13 -0
  30. package/dist/modules/inbox_ops/api/proposals/route.js.map +2 -2
  31. package/dist/modules/inbox_ops/api/settings/route.js +33 -2
  32. package/dist/modules/inbox_ops/api/settings/route.js.map +2 -2
  33. package/dist/modules/inbox_ops/backend/inbox-ops/page.js +28 -3
  34. package/dist/modules/inbox_ops/backend/inbox-ops/page.js.map +2 -2
  35. package/dist/modules/inbox_ops/backend/inbox-ops/proposals/[id]/page.js +103 -5
  36. package/dist/modules/inbox_ops/backend/inbox-ops/proposals/[id]/page.js.map +2 -2
  37. package/dist/modules/inbox_ops/components/messages/InboxEmailContent.js +24 -0
  38. package/dist/modules/inbox_ops/components/messages/InboxEmailContent.js.map +7 -0
  39. package/dist/modules/inbox_ops/components/messages/InboxEmailPreview.js +29 -0
  40. package/dist/modules/inbox_ops/components/messages/InboxEmailPreview.js.map +7 -0
  41. package/dist/modules/inbox_ops/components/proposals/CategoryBadge.js +59 -0
  42. package/dist/modules/inbox_ops/components/proposals/CategoryBadge.js.map +7 -0
  43. package/dist/modules/inbox_ops/components/proposals/EditActionDialog.js +3 -1
  44. package/dist/modules/inbox_ops/components/proposals/EditActionDialog.js.map +2 -2
  45. package/dist/modules/inbox_ops/data/entities.js +4 -0
  46. package/dist/modules/inbox_ops/data/entities.js.map +2 -2
  47. package/dist/modules/inbox_ops/data/validators.js +30 -5
  48. package/dist/modules/inbox_ops/data/validators.js.map +2 -2
  49. package/dist/modules/inbox_ops/lib/cache.js +53 -0
  50. package/dist/modules/inbox_ops/lib/cache.js.map +7 -0
  51. package/dist/modules/inbox_ops/lib/contactValidation.js +38 -3
  52. package/dist/modules/inbox_ops/lib/contactValidation.js.map +2 -2
  53. package/dist/modules/inbox_ops/lib/executionHelpers.js +28 -1
  54. package/dist/modules/inbox_ops/lib/executionHelpers.js.map +2 -2
  55. package/dist/modules/inbox_ops/lib/extractionPrompt.js +2 -1
  56. package/dist/modules/inbox_ops/lib/extractionPrompt.js.map +2 -2
  57. package/dist/modules/inbox_ops/lib/messageObjectPreviews.js +52 -0
  58. package/dist/modules/inbox_ops/lib/messageObjectPreviews.js.map +7 -0
  59. package/dist/modules/inbox_ops/lib/messagesIntegration.js +155 -0
  60. package/dist/modules/inbox_ops/lib/messagesIntegration.js.map +7 -0
  61. package/dist/modules/inbox_ops/message-objects.js +36 -0
  62. package/dist/modules/inbox_ops/message-objects.js.map +7 -0
  63. package/dist/modules/inbox_ops/message-types.js +38 -0
  64. package/dist/modules/inbox_ops/message-types.js.map +7 -0
  65. package/dist/modules/inbox_ops/migrations/Migration20260303173020.js +13 -0
  66. package/dist/modules/inbox_ops/migrations/Migration20260303173020.js.map +7 -0
  67. package/dist/modules/inbox_ops/migrations/Migration20260303173215.js +15 -0
  68. package/dist/modules/inbox_ops/migrations/Migration20260303173215.js.map +7 -0
  69. package/dist/modules/inbox_ops/search.js +5 -3
  70. package/dist/modules/inbox_ops/search.js.map +2 -2
  71. package/dist/modules/inbox_ops/subscribers/extractionWorker.js +65 -3
  72. package/dist/modules/inbox_ops/subscribers/extractionWorker.js.map +2 -2
  73. package/generated/entities/inbox_proposal/index.ts +1 -0
  74. package/package.json +3 -3
  75. package/src/modules/catalog/inbox-actions.ts +55 -0
  76. package/src/modules/customers/inbox-actions.ts +86 -27
  77. package/src/modules/inbox_ops/ai-tools.ts +451 -0
  78. package/src/modules/inbox_ops/api/extract/route.ts +3 -2
  79. package/src/modules/inbox_ops/api/proposals/[id]/accept-all/route.ts +5 -0
  80. package/src/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/accept/route.ts +5 -0
  81. package/src/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/complete/route.ts +5 -0
  82. package/src/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/reject/route.ts +5 -0
  83. package/src/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/route.ts +5 -0
  84. package/src/modules/inbox_ops/api/proposals/[id]/categorize/route.ts +61 -0
  85. package/src/modules/inbox_ops/api/proposals/[id]/reject/route.ts +5 -0
  86. package/src/modules/inbox_ops/api/proposals/[id]/replies/[replyId]/send/route.ts +36 -16
  87. package/src/modules/inbox_ops/api/proposals/counts/route.ts +60 -5
  88. package/src/modules/inbox_ops/api/proposals/route.ts +14 -1
  89. package/src/modules/inbox_ops/api/settings/route.ts +36 -2
  90. package/src/modules/inbox_ops/backend/inbox-ops/page.tsx +31 -3
  91. package/src/modules/inbox_ops/backend/inbox-ops/proposals/[id]/page.tsx +103 -1
  92. package/src/modules/inbox_ops/components/messages/InboxEmailContent.tsx +45 -0
  93. package/src/modules/inbox_ops/components/messages/InboxEmailPreview.tsx +40 -0
  94. package/src/modules/inbox_ops/components/proposals/CategoryBadge.tsx +59 -0
  95. package/src/modules/inbox_ops/components/proposals/EditActionDialog.tsx +3 -1
  96. package/src/modules/inbox_ops/components/proposals/types.ts +1 -0
  97. package/src/modules/inbox_ops/data/entities.ts +14 -1
  98. package/src/modules/inbox_ops/data/validators.ts +41 -5
  99. package/src/modules/inbox_ops/lib/cache.ts +60 -0
  100. package/src/modules/inbox_ops/lib/contactValidation.ts +31 -2
  101. package/src/modules/inbox_ops/lib/executionHelpers.ts +40 -0
  102. package/src/modules/inbox_ops/lib/extractionPrompt.ts +2 -1
  103. package/src/modules/inbox_ops/lib/messageObjectPreviews.ts +61 -0
  104. package/src/modules/inbox_ops/lib/messagesIntegration.ts +231 -0
  105. package/src/modules/inbox_ops/message-objects.ts +34 -0
  106. package/src/modules/inbox_ops/message-types.ts +36 -0
  107. package/src/modules/inbox_ops/migrations/Migration20260303173020.ts +13 -0
  108. package/src/modules/inbox_ops/migrations/Migration20260303173215.ts +15 -0
  109. package/src/modules/inbox_ops/search.ts +5 -3
  110. package/src/modules/inbox_ops/subscribers/extractionWorker.ts +75 -1
@@ -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 = 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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",
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 { runWithCacheTenant } from '@open-mercato/cache'\nimport { emitInboxOpsEvent } from '../events'\nimport { createMessageRecordForEmail } from '../lib/messagesIntegration'\nimport { resolveCache, invalidateCountsCache } from '../lib/cache'\n\nconst SYSTEM_USER_ID = '00000000-0000-0000-0000-000000000000'\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 category: extractionResult.category || null,\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 email.forwardedByAddress,\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 // Step 6f: Deduplicate \u2014 remove company create_contact actions when a person\n // action with the same companyName already exists (person creation auto-creates\n // the company, so the separate company action would be redundant).\n const dedupedProposedActions = deduplicateCompanyActions([\n ...autoContactActions, ...autoLinkActions, ...autoProductActions, ...extractionResult.proposedActions,\n ])\n\n // Create actions \u2014 contact & product creation actions go first so they're executed before orders\n const combinedProposedActions = dedupedProposedActions\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 8b: Invalidate counts cache (new proposal affects counts)\n try {\n const cache = resolveCache(ctx)\n await runWithCacheTenant(email.tenantId, () => invalidateCountsCache(cache, email.tenantId))\n } catch (cacheErr) {\n console.warn('[inbox_ops:extraction-worker] Cache invalidation failed (non-fatal):', cacheErr)\n }\n\n // Step 8c: Register email as a message record (graceful degradation)\n try {\n await createMessageRecordForEmail(\n {\n id: email.id,\n subject: email.subject,\n cleanedText: email.cleanedText,\n rawText: email.rawText,\n forwardedByAddress: email.forwardedByAddress,\n forwardedByName: email.forwardedByName,\n status: email.status,\n },\n {\n container: ctx,\n scope: {\n tenantId: email.tenantId,\n organizationId: email.organizationId,\n userId: SYSTEM_USER_ID,\n },\n },\n )\n } catch (msgErr) {\n console.error('[inbox_ops:extraction-worker] Messages integration failed (non-fatal):', msgErr)\n }\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 forwardedByAddress?: 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 forwardedByLower = (forwardedByAddress || '').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 (!emailLower || !emailLower.includes('@')) return false\n if (alreadyProposed.has(emailLower)) return false\n if (emailLower === inboxLower) return false\n if (forwardedByLower && emailLower === forwardedByLower) 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 deduplicateCompanyActions<T extends { actionType: string; payloadJson: string }>(\n actions: T[],\n): T[] {\n // Collect company names that will be auto-created by person actions via companyName field\n const personCompanyNames = new Set<string>()\n for (const action of actions) {\n if (action.actionType !== 'create_contact') continue\n const payload = safeParsePayloadJson(action.payloadJson)\n if (payload.type === 'person' && typeof payload.companyName === 'string' && payload.companyName.trim()) {\n personCompanyNames.add(payload.companyName.trim().toLowerCase())\n }\n }\n if (personCompanyNames.size === 0) return actions\n\n return actions.filter((action) => {\n if (action.actionType !== 'create_contact') return true\n const payload = safeParsePayloadJson(action.payloadJson)\n if (payload.type !== 'company') return true\n const companyName = typeof payload.name === 'string' ? payload.name.trim().toLowerCase() : ''\n return !companyName || !personCompanyNames.has(companyName)\n })\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
  }
@@ -4,6 +4,7 @@ export const summary = 'summary'
4
4
  export const participants = 'participants'
5
5
  export const confidence = 'confidence'
6
6
  export const detected_language = 'detected_language'
7
+ export const category = 'category'
7
8
  export const status = 'status'
8
9
  export const possibly_incomplete = 'possibly_incomplete'
9
10
  export const reviewed_by_user_id = 'reviewed_by_user_id'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.4.8-develop-15259be22b",
3
+ "version": "0.4.8-develop-280c02b529",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -217,10 +217,10 @@
217
217
  "semver": "^7.6.3"
218
218
  },
219
219
  "peerDependencies": {
220
- "@open-mercato/shared": "0.4.8-develop-15259be22b"
220
+ "@open-mercato/shared": "0.4.8-develop-280c02b529"
221
221
  },
222
222
  "devDependencies": {
223
- "@open-mercato/shared": "0.4.8-develop-15259be22b",
223
+ "@open-mercato/shared": "0.4.8-develop-280c02b529",
224
224
  "@testing-library/dom": "^10.4.1",
225
225
  "@testing-library/jest-dom": "^6.9.1",
226
226
  "@testing-library/react": "^16.3.1",
@@ -1,4 +1,5 @@
1
1
  import type { InboxActionDefinition, InboxActionExecutionContext } from '@open-mercato/shared/modules/inbox-actions'
2
+ import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
2
3
  import { createProductPayloadSchema } from '../inbox_ops/data/validators'
3
4
  import type { CreateProductPayload } from '../inbox_ops/data/validators'
4
5
  import {
@@ -7,6 +8,7 @@ import {
7
8
  executeCommand,
8
9
  resolveProductDiscrepanciesInProposal,
9
10
  } from '../inbox_ops/lib/executionHelpers'
11
+ import { CatalogPriceKind } from './data/entities'
10
12
 
11
13
  async function executeCreateProductAction(
12
14
  action: { id: string; proposalId: string; payload: unknown },
@@ -37,6 +39,59 @@ async function executeCreateProductAction(
37
39
  throw new ExecutionError('Product creation did not return a product ID', 500)
38
40
  }
39
41
 
42
+ // Create default variant so the product works with quotes/orders (issue #891)
43
+ // No separate permission check — the user already passed catalog.products.manage
44
+ // in the execution engine, and variant/price creation is an integral part of
45
+ // product setup, not a separate user action.
46
+ let variantId: string | null = null
47
+ try {
48
+ const variantResult = await executeCommand<Record<string, unknown>, { variantId?: string }>(
49
+ hCtx,
50
+ 'catalog.variants.create',
51
+ {
52
+ productId: result.productId,
53
+ organizationId: hCtx.organizationId,
54
+ tenantId: hCtx.tenantId,
55
+ name: 'Default',
56
+ isDefault: true,
57
+ isActive: true,
58
+ sku: payload.sku || undefined,
59
+ },
60
+ )
61
+ variantId = variantResult.variantId ?? null
62
+ } catch (variantErr) {
63
+ console.warn('[catalog:inbox-action] Failed to create default variant (non-fatal):', variantErr instanceof Error ? variantErr.message : variantErr)
64
+ }
65
+
66
+ if (variantId && payload.unitPrice && payload.currencyCode) {
67
+ try {
68
+ const priceKind = await findOneWithDecryption(
69
+ hCtx.em,
70
+ CatalogPriceKind,
71
+ {
72
+ code: 'regular',
73
+ tenantId: hCtx.tenantId,
74
+ deletedAt: null,
75
+ },
76
+ undefined,
77
+ { tenantId: hCtx.tenantId, organizationId: hCtx.organizationId },
78
+ )
79
+ if (priceKind) {
80
+ await executeCommand(hCtx, 'catalog.prices.create', {
81
+ variantId,
82
+ productId: result.productId,
83
+ organizationId: hCtx.organizationId,
84
+ tenantId: hCtx.tenantId,
85
+ priceKindId: priceKind.id,
86
+ currencyCode: payload.currencyCode,
87
+ unitPriceNet: Number(payload.unitPrice),
88
+ })
89
+ }
90
+ } catch (priceErr) {
91
+ console.warn('[catalog:inbox-action] Failed to set price on default variant (non-fatal):', priceErr)
92
+ }
93
+ }
94
+
40
95
  await resolveProductDiscrepanciesInProposal(hCtx.em, action.proposalId, payload.title, result.productId, {
41
96
  tenantId: hCtx.tenantId,
42
97
  organizationId: hCtx.organizationId,
@@ -19,9 +19,58 @@ import {
19
19
  resolveCustomerEntityIdByEmail,
20
20
  resolveContactIdByNameAndType,
21
21
  } from '../inbox_ops/lib/executionHelpers'
22
- import { splitPersonName } from '../inbox_ops/lib/contactValidation'
22
+ import { splitPersonName, stripTitleFromName } from '../inbox_ops/lib/contactValidation'
23
23
  import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
24
24
 
25
+ // ---------------------------------------------------------------------------
26
+ // Helpers
27
+ // ---------------------------------------------------------------------------
28
+
29
+ async function resolveOrCreateCompany(
30
+ hCtx: ReturnType<typeof asHelperContext>,
31
+ companyName: string,
32
+ ): Promise<string | null> {
33
+ const CustomerEntityClass = resolveEntityClass(hCtx, 'CustomerEntity')
34
+ if (CustomerEntityClass) {
35
+ const allCompanies = await findWithDecryption(
36
+ hCtx.em,
37
+ CustomerEntityClass,
38
+ {
39
+ kind: 'company',
40
+ tenantId: hCtx.tenantId,
41
+ organizationId: hCtx.organizationId,
42
+ deletedAt: null,
43
+ },
44
+ { limit: 500 },
45
+ { tenantId: hCtx.tenantId, organizationId: hCtx.organizationId },
46
+ )
47
+ const trimmedLower = companyName.trim().toLowerCase()
48
+ const existing = allCompanies.find(
49
+ (c) => c.displayName && c.displayName.toLowerCase() === trimmedLower,
50
+ )
51
+ if (existing) return existing.id
52
+ }
53
+
54
+ try {
55
+ const result = await executeCommand<Record<string, unknown>, { entityId?: string }>(
56
+ hCtx,
57
+ 'customers.companies.create',
58
+ {
59
+ organizationId: hCtx.organizationId,
60
+ tenantId: hCtx.tenantId,
61
+ displayName: companyName.trim(),
62
+ legalName: companyName.trim(),
63
+ source: 'ai_inbox',
64
+ status: 'active',
65
+ },
66
+ )
67
+ return result.entityId ?? null
68
+ } catch (err) {
69
+ console.warn('[customers:inbox-action] Failed to create company (non-fatal):', err)
70
+ return null
71
+ }
72
+ }
73
+
25
74
  // ---------------------------------------------------------------------------
26
75
  // create_contact
27
76
  // ---------------------------------------------------------------------------
@@ -36,11 +85,13 @@ async function executeCreateContactAction(
36
85
  const CustomerEntityClass = resolveEntityClass(hCtx, 'CustomerEntity')
37
86
  if (payload.email && CustomerEntityClass) {
38
87
  const emailLower = payload.email.trim().toLowerCase()
88
+ const targetKind = payload.type === 'company' ? 'company' : 'person'
39
89
  let existingContact = await findOneWithDecryption(
40
90
  hCtx.em,
41
91
  CustomerEntityClass,
42
92
  {
43
93
  primaryEmail: emailLower,
94
+ kind: targetKind,
44
95
  tenantId: hCtx.tenantId,
45
96
  organizationId: hCtx.organizationId,
46
97
  deletedAt: null,
@@ -53,6 +104,7 @@ async function executeCreateContactAction(
53
104
  hCtx.em,
54
105
  CustomerEntityClass,
55
106
  {
107
+ kind: targetKind,
56
108
  tenantId: hCtx.tenantId,
57
109
  organizationId: hCtx.organizationId,
58
110
  deletedAt: null,
@@ -76,40 +128,47 @@ async function executeCreateContactAction(
76
128
  }
77
129
 
78
130
  if (payload.type === 'company') {
79
- const result = await executeCommand<Record<string, unknown>, { entityId?: string }>(
80
- hCtx,
81
- 'customers.companies.create',
82
- {
83
- organizationId: hCtx.organizationId,
84
- tenantId: hCtx.tenantId,
85
- displayName: payload.name,
86
- legalName: payload.companyName ?? payload.name,
87
- primaryEmail: payload.email,
88
- primaryPhone: payload.phone,
89
- source: payload.source,
90
- },
91
- )
92
- if (!result.entityId) {
131
+ // Use find-or-create to prevent duplicates when a person action with the
132
+ // same companyName already created this company (or vice versa).
133
+ const companyId = await resolveOrCreateCompany(hCtx, payload.name)
134
+ if (!companyId) {
93
135
  throw new ExecutionError('Company creation did not return an entity ID', 500)
94
136
  }
95
- return { createdEntityId: result.entityId, createdEntityType: 'customer_company' }
137
+ return { createdEntityId: companyId, createdEntityType: 'customer_company' }
96
138
  }
97
139
 
140
+ const { cleanedName } = stripTitleFromName(payload.name)
98
141
  const { firstName, lastName } = splitPersonName(payload.name, payload.email)
142
+
143
+ // If company name is provided, find or create the company and link it to the person.
144
+ // No separate permission check — the user already passed customers.people.manage
145
+ // in the execution engine, and company creation is an integral part of contact setup.
146
+ let companyEntityId: string | null = null
147
+ if (payload.companyName) {
148
+ companyEntityId = await resolveOrCreateCompany(hCtx, payload.companyName)
149
+ }
150
+
151
+ const personInput: Record<string, unknown> = {
152
+ organizationId: hCtx.organizationId,
153
+ tenantId: hCtx.tenantId,
154
+ displayName: cleanedName,
155
+ firstName,
156
+ lastName,
157
+ primaryEmail: payload.email,
158
+ primaryPhone: payload.phone,
159
+ jobTitle: payload.role || undefined,
160
+ source: 'ai_inbox',
161
+ status: 'active',
162
+ lifecycleStage: 'lead',
163
+ }
164
+ if (companyEntityId) {
165
+ personInput.companyEntityId = companyEntityId
166
+ }
167
+
99
168
  const result = await executeCommand<Record<string, unknown>, { entityId?: string }>(
100
169
  hCtx,
101
170
  'customers.people.create',
102
- {
103
- organizationId: hCtx.organizationId,
104
- tenantId: hCtx.tenantId,
105
- displayName: payload.name,
106
- firstName,
107
- lastName,
108
- primaryEmail: payload.email,
109
- primaryPhone: payload.phone,
110
- jobTitle: payload.role,
111
- source: payload.source,
112
- },
171
+ personInput,
113
172
  )
114
173
 
115
174
  if (!result.entityId) {