@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.
- package/dist/generated/entities/inbox_proposal/index.js +2 -0
- package/dist/generated/entities/inbox_proposal/index.js.map +2 -2
- package/dist/modules/catalog/inbox-actions.js +49 -0
- package/dist/modules/catalog/inbox-actions.js.map +2 -2
- package/dist/modules/customers/inbox-actions.js +69 -27
- package/dist/modules/customers/inbox-actions.js.map +3 -3
- package/dist/modules/inbox_ops/ai-tools.js +346 -0
- package/dist/modules/inbox_ops/ai-tools.js.map +7 -0
- package/dist/modules/inbox_ops/api/extract/route.js +3 -2
- package/dist/modules/inbox_ops/api/extract/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/proposals/[id]/accept-all/route.js +4 -0
- package/dist/modules/inbox_ops/api/proposals/[id]/accept-all/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/accept/route.js +4 -0
- package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/accept/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/complete/route.js +4 -0
- package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/complete/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/reject/route.js +4 -0
- package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/reject/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/route.js +4 -0
- package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/proposals/[id]/categorize/route.js +59 -0
- package/dist/modules/inbox_ops/api/proposals/[id]/categorize/route.js.map +7 -0
- package/dist/modules/inbox_ops/api/proposals/[id]/reject/route.js +4 -0
- package/dist/modules/inbox_ops/api/proposals/[id]/reject/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/proposals/[id]/replies/[replyId]/send/route.js +34 -14
- package/dist/modules/inbox_ops/api/proposals/[id]/replies/[replyId]/send/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/proposals/counts/route.js +49 -4
- package/dist/modules/inbox_ops/api/proposals/counts/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/proposals/route.js +13 -0
- package/dist/modules/inbox_ops/api/proposals/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/settings/route.js +33 -2
- package/dist/modules/inbox_ops/api/settings/route.js.map +2 -2
- package/dist/modules/inbox_ops/backend/inbox-ops/page.js +28 -3
- package/dist/modules/inbox_ops/backend/inbox-ops/page.js.map +2 -2
- package/dist/modules/inbox_ops/backend/inbox-ops/proposals/[id]/page.js +103 -5
- package/dist/modules/inbox_ops/backend/inbox-ops/proposals/[id]/page.js.map +2 -2
- package/dist/modules/inbox_ops/components/messages/InboxEmailContent.js +24 -0
- package/dist/modules/inbox_ops/components/messages/InboxEmailContent.js.map +7 -0
- package/dist/modules/inbox_ops/components/messages/InboxEmailPreview.js +29 -0
- package/dist/modules/inbox_ops/components/messages/InboxEmailPreview.js.map +7 -0
- package/dist/modules/inbox_ops/components/proposals/CategoryBadge.js +59 -0
- package/dist/modules/inbox_ops/components/proposals/CategoryBadge.js.map +7 -0
- package/dist/modules/inbox_ops/components/proposals/EditActionDialog.js +3 -1
- package/dist/modules/inbox_ops/components/proposals/EditActionDialog.js.map +2 -2
- package/dist/modules/inbox_ops/data/entities.js +4 -0
- package/dist/modules/inbox_ops/data/entities.js.map +2 -2
- package/dist/modules/inbox_ops/data/validators.js +30 -5
- package/dist/modules/inbox_ops/data/validators.js.map +2 -2
- package/dist/modules/inbox_ops/lib/cache.js +53 -0
- package/dist/modules/inbox_ops/lib/cache.js.map +7 -0
- package/dist/modules/inbox_ops/lib/contactValidation.js +38 -3
- package/dist/modules/inbox_ops/lib/contactValidation.js.map +2 -2
- package/dist/modules/inbox_ops/lib/executionHelpers.js +28 -1
- package/dist/modules/inbox_ops/lib/executionHelpers.js.map +2 -2
- package/dist/modules/inbox_ops/lib/extractionPrompt.js +2 -1
- package/dist/modules/inbox_ops/lib/extractionPrompt.js.map +2 -2
- package/dist/modules/inbox_ops/lib/messageObjectPreviews.js +52 -0
- package/dist/modules/inbox_ops/lib/messageObjectPreviews.js.map +7 -0
- package/dist/modules/inbox_ops/lib/messagesIntegration.js +155 -0
- package/dist/modules/inbox_ops/lib/messagesIntegration.js.map +7 -0
- package/dist/modules/inbox_ops/message-objects.js +36 -0
- package/dist/modules/inbox_ops/message-objects.js.map +7 -0
- package/dist/modules/inbox_ops/message-types.js +38 -0
- package/dist/modules/inbox_ops/message-types.js.map +7 -0
- package/dist/modules/inbox_ops/migrations/Migration20260303173020.js +13 -0
- package/dist/modules/inbox_ops/migrations/Migration20260303173020.js.map +7 -0
- package/dist/modules/inbox_ops/migrations/Migration20260303173215.js +15 -0
- package/dist/modules/inbox_ops/migrations/Migration20260303173215.js.map +7 -0
- package/dist/modules/inbox_ops/search.js +5 -3
- package/dist/modules/inbox_ops/search.js.map +2 -2
- package/dist/modules/inbox_ops/subscribers/extractionWorker.js +65 -3
- package/dist/modules/inbox_ops/subscribers/extractionWorker.js.map +2 -2
- package/generated/entities/inbox_proposal/index.ts +1 -0
- package/package.json +3 -3
- package/src/modules/catalog/inbox-actions.ts +55 -0
- package/src/modules/customers/inbox-actions.ts +86 -27
- package/src/modules/inbox_ops/ai-tools.ts +451 -0
- package/src/modules/inbox_ops/api/extract/route.ts +3 -2
- package/src/modules/inbox_ops/api/proposals/[id]/accept-all/route.ts +5 -0
- package/src/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/accept/route.ts +5 -0
- package/src/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/complete/route.ts +5 -0
- package/src/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/reject/route.ts +5 -0
- package/src/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/route.ts +5 -0
- package/src/modules/inbox_ops/api/proposals/[id]/categorize/route.ts +61 -0
- package/src/modules/inbox_ops/api/proposals/[id]/reject/route.ts +5 -0
- package/src/modules/inbox_ops/api/proposals/[id]/replies/[replyId]/send/route.ts +36 -16
- package/src/modules/inbox_ops/api/proposals/counts/route.ts +60 -5
- package/src/modules/inbox_ops/api/proposals/route.ts +14 -1
- package/src/modules/inbox_ops/api/settings/route.ts +36 -2
- package/src/modules/inbox_ops/backend/inbox-ops/page.tsx +31 -3
- package/src/modules/inbox_ops/backend/inbox-ops/proposals/[id]/page.tsx +103 -1
- package/src/modules/inbox_ops/components/messages/InboxEmailContent.tsx +45 -0
- package/src/modules/inbox_ops/components/messages/InboxEmailPreview.tsx +40 -0
- package/src/modules/inbox_ops/components/proposals/CategoryBadge.tsx +59 -0
- package/src/modules/inbox_ops/components/proposals/EditActionDialog.tsx +3 -1
- package/src/modules/inbox_ops/components/proposals/types.ts +1 -0
- package/src/modules/inbox_ops/data/entities.ts +14 -1
- package/src/modules/inbox_ops/data/validators.ts +41 -5
- package/src/modules/inbox_ops/lib/cache.ts +60 -0
- package/src/modules/inbox_ops/lib/contactValidation.ts +31 -2
- package/src/modules/inbox_ops/lib/executionHelpers.ts +40 -0
- package/src/modules/inbox_ops/lib/extractionPrompt.ts +2 -1
- package/src/modules/inbox_ops/lib/messageObjectPreviews.ts +61 -0
- package/src/modules/inbox_ops/lib/messagesIntegration.ts +231 -0
- package/src/modules/inbox_ops/message-objects.ts +34 -0
- package/src/modules/inbox_ops/message-types.ts +36 -0
- package/src/modules/inbox_ops/migrations/Migration20260303173020.ts +13 -0
- package/src/modules/inbox_ops/migrations/Migration20260303173215.ts +15 -0
- package/src/modules/inbox_ops/search.ts +5 -3
- 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": "AAAA,SAAS,kBAAkB;AAG3B,SAAS,6BAA6B;AACtC,SAAS,YAAY,eAAe,qBAAqB,kBAAkB,qBAAqB;AAGhG,SAAS,qBAAqB;AAC9B,SAAS,6BAA6B,iCAAiC;AACvE,SAAS,6BAA6B;AACtC,SAAS,yCAAyC;AAClD,SAAS,0BAA0B;AACnC,SAAS,sBAAsB;AAC/B,SAAS,qCAAqC;AAC9C,SAAS,2CAA2C;AACpD,SAAS,4BAA4B;AACrC,SAAS,uBAAuB;AAChC,SAAS,yBAAyB;AAE3B,MAAM,WAAW;AAAA,EACtB,OAAO;AAAA,EACP,YAAY;AAAA,EACZ,IAAI;AACN;AAgCA,SAAS,WAAc,KAAsB,MAA6B;AACxE,MAAI;AACF,WAAO,IAAI,QAAW,IAAI;AAAA,EAC5B,QAAQ;AACN,YAAQ,MAAM,+CAA+C,IAAI,iBAAiB;AAClF,WAAO;AAAA,EACT;AACF;AAEA,SAAS,qBAAqB,KAA+C;AAC3E,SAAO;AAAA,IACL,gBAAgB,WAAW,KAAK,gBAAgB;AAAA,IAChD,gBAAgB,WAAW,KAAK,gBAAgB;AAAA,IAChD,qBAAqB,WAAW,KAAK,qBAAqB;AAAA,IAC1D,YAAY,WAAW,KAAK,YAAY;AAAA,IACxC,cAAc,WAAW,KAAK,cAAc;AAAA,IAC5C,iBAAiB,WAAW,KAAK,iBAAiB;AAAA,EACpD;AACF;AAEA,SAAS,kBACP,IACA,YACA,YACA,OACA,OACA;AACA,SAAO,GAAG,OAAO,kBAAkB;AAAA,IACjC;AAAA,IACA,UAAU,MAAM,gBAAgB,UAAa,WAAW,MAAM,WAAW,IACrE,WAAW,MAAM,WAAW,EAAE,KAC9B;AAAA,IACJ,MAAM,MAAM;AAAA,IACZ,UAAU,MAAM;AAAA,IAChB,aAAa,MAAM;AAAA,IACnB,eAAe,MAAM,iBAAiB;AAAA,IACtC,YAAY,MAAM,cAAc;AAAA,IAChC,UAAU;AAAA,IACV,gBAAgB,MAAM;AAAA,IACtB,UAAU,MAAM;AAAA,EAClB,CAAC;AACH;AAEA,eAAO,OAA8B,SAA+B,KAAsB;AACxF,QAAM,KAAM,IAAI,QAAQ,IAAI,EAAoB,KAAK;AACrD,QAAM,gBAAgB,qBAAqB,GAAG;AAI9C,QAAM,UAAU,MAAM,GAAG;AAAA,IACvB;AAAA,IACA,EAAE,IAAI,QAAQ,SAAS,QAAQ,WAAW;AAAA,IAC1C,EAAE,QAAQ,aAAa;AAAA,EACzB;AACA,MAAI,YAAY,EAAG;AAEnB,QAAM,QAAQ,MAAM;AAAA,IAClB;AAAA,IACA;AAAA,IACA,EAAE,IAAI,QAAQ,QAAQ;AAAA,IACtB;AAAA,IACA,EAAE,UAAU,QAAQ,UAAU,gBAAgB,QAAQ,eAAe;AAAA,EACvE;AACA,MAAI,CAAC,OAAO;AACV,YAAQ,MAAM,kDAAkD,QAAQ,OAAO,EAAE;AACjF;AAAA,EACF;AAEA,MAAI;AACF,UAAM,QAAQ;AAAA,MACZ,UAAU,MAAM;AAAA,MAChB,gBAAgB,MAAM;AAAA,IACxB;AAGA,UAAM,WAAW,MAAM,sBAAsB,IAAI,eAAe,EAAE,gBAAgB,MAAM,gBAAgB,UAAU,MAAM,UAAU,WAAW,KAAK,GAAG,QAAW,KAAK;AACrK,UAAM,kBAAkB,UAAU,mBAAmB;AAMrD,UAAM,WAAW,2BAA2B,KAAK;AACjD,QAAI,CAAC,SAAS,KAAK,GAAG;AACpB,YAAM,SAAS;AACf,YAAM,kBAAkB;AACxB,YAAM,GAAG,MAAM;AACf;AAAA,IACF;AAGA,UAAM,qBAAqB,8BAA8B,KAAK;AAC9D,UAAM,iBAAiB,MAAM;AAAA,MAAc;AAAA,MAAI;AAAA,MAAoB;AAAA,MACjE,cAAc,iBAAiB,EAAE,qBAAqB,cAAc,eAAe,IAAI;AAAA,IACzF;AAGA,UAAM,kBAAkB,MAAM;AAAA,MAAkC;AAAA,MAAI;AAAA,MAClE,cAAc,kBAAkB,cAAc,sBAC1C,EAAE,qBAAqB,cAAc,gBAAgB,0BAA0B,cAAc,oBAAoB,IACjH;AAAA,IACN;AAGA,UAAM,cAAc,SAAS,QAAQ,IAAI,2BAA2B,UAAU,EAAE;AAChF,UAAM,gBAAgB,SAAS,MAAM,GAAG,WAAW;AAEnD,UAAM,eAAe,MAAM,4BAA4B,gBAAgB,iBAAiB,QAAW,eAAe;AAClH,UAAM,aAAa,0BAA0B,aAAa;AAE1D,QAAI;AACJ,QAAI,aAAa;AACjB,QAAI,YAAY;AAEhB,QAAI;AACF,YAAM,eAAe,OAAO,SAAS,QAAQ,IAAI,4BAA4B,SAAS,EAAE;AACxF,YAAM,YAAY,OAAO,SAAS,YAAY,KAAK,eAAe,IAAI,eAAe;AACrF,YAAM,aAAa,MAAM,oCAAoC;AAAA,QAC3D;AAAA,QACA;AAAA,QACA,eAAe,QAAQ,IAAI;AAAA,QAC3B;AAAA,MACF,CAAC;AACD,yBAAmB,WAAW;AAC9B,mBAAa,WAAW;AACxB,kBAAY,WAAW;AAAA,IACzB,SAAS,UAAU;AACjB,YAAM,SAAS;AACf,YAAM,kBAAkB,0BAA0B,oBAAoB,QAAQ,SAAS,UAAU,OAAO,QAAQ,CAAC;AACjH,YAAM,GAAG,MAAM;AAEf,UAAI;AACF,cAAM,kBAAkB,0BAA0B;AAAA,UAChD,SAAS,MAAM;AAAA,UACf,UAAU,MAAM;AAAA,UAChB,gBAAgB,MAAM;AAAA,UACtB,OAAO,MAAM;AAAA,QACf,CAAC;AAAA,MACH,SAAS,YAAY;AACnB,gBAAQ,MAAM,oEAAoE,UAAU;AAAA,MAC9F;AAEA;AAAA,IACF;AAEA,UAAM,yBAAyB,OAAO,WAAW,QAAQ,IAAI,kCAAkC,KAAK;AACpG,UAAM,sBAAsB,OAAO,SAAS,sBAAsB,IAC9D,KAAK,IAAI,KAAK,IAAI,wBAAwB,CAAC,GAAG,CAAC,IAC/C;AACJ,UAAM,iBAAiB,iBAAiB,aAAa;AAGrD,UAAM,eAAe,iBAAiB,gBACnC,IAAI,CAAC,QAAQ,WAAW;AAAA,MACvB,GAAG;AAAA,MAAQ,SAAS,qBAAqB,OAAO,WAAW;AAAA,MAAG;AAAA,IAChE,EAAE,EACD,OAAO,CAAC,MAAM,EAAE,eAAe,kBAAkB,EAAE,eAAe,cAAc;AAEnF,UAAM,qBAAqB,MAAM;AAAA,MAAe;AAAA,MAAI;AAAA,MAAc;AAAA,MAChE,cAAc,sBAAsB,EAAE,0BAA0B,cAAc,oBAAoB,IAAI;AAAA,IACxG;AAGA,UAAM,8BAA8B,MAAM,sBAAsB,IAAI,cAAc,OAAO,cAAc,UAAU;AAKjH,UAAM,eAAe,IAAI,IAAI,eAAe,IAAI,CAAC,MAAM,EAAE,YAAY,MAAM,YAAY,CAAC,CAAC;AACzF,UAAM,sBAAsB,iBAAiB,aAC1C,OAAO,CAAC,MAAM,EAAE,SAAS,CAAC,aAAa,IAAI,EAAE,MAAM,YAAY,CAAC,CAAC,EACjE,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,OAAO,EAAE,OAAO,MAAM,EAAE,QAAQ,UAAU,EAAE;AAE3E,QAAI,oBAAoB,SAAS,GAAG;AAClC,YAAM,oBAAoB,MAAM;AAAA,QAAc;AAAA,QAAI;AAAA,QAAqB;AAAA,QACrE,cAAc,iBAAiB,EAAE,qBAAqB,cAAc,eAAe,IAAI;AAAA,MACzF;AACA,qBAAe,KAAK,GAAG,iBAAiB;AAAA,IAC1C;AAGA,UAAM,uBAA+C,iBAAiB,aAAa,IAAI,CAAC,MAAM;AAC5F,YAAM,QAAQ,eAAe;AAAA,QAC3B,CAAC,MAAM,EAAE,YAAY,MAAM,YAAY,MAAM,EAAE,MAAM,YAAY;AAAA,MACnE;AACA,aAAO;AAAA,QACL,GAAG;AAAA,QACH,kBAAkB,OAAO,OAAO,aAAa;AAAA,QAC7C,oBAAoB,OAAO,OAAO,eAAe;AAAA,QACjD,iBAAiB,OAAO,OAAO;AAAA,MACjC;AAAA,IACF,CAAC;AAGD,UAAM,qBAAqB,iBAAiB,sBAAsB,qBAAqB,KAAK;AAG5F,UAAM,0BAA8C,CAAC;AACrD,eAAW,CAAC,aAAa,MAAM,KAAK,iBAAiB,gBAAgB,QAAQ,GAAG;AAC9E,UAAI,OAAO,eAAe,kBAAkB,OAAO,eAAe,gBAAgB;AAChF,cAAM,gBAAgB,qBAAqB,OAAO,WAAW;AAE7D,oCAA4B,aAAa;AAEzC,cAAM,EAAE,SAAS,UAAU,SAAS,IAAI,MAAM,mBAAmB,eAAe;AAAA,UAC9E;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA,aAAa,MAAM;AAAA,UACnB,mBAAmB,cAAc;AAAA,UACjC,sBAAsB,cAAc;AAAA,QACtC,CAAC;AAED,eAAO,cAAc,KAAK,UAAU,QAAQ;AAE5C,mBAAW,WAAW,UAAU;AAC9B,cAAI,YAAY,uBAAuB;AACrC,oCAAwB,KAAK;AAAA,cAC3B;AAAA,cACA,MAAM;AAAA,cACN,UAAU;AAAA,cACV,aAAa;AAAA,YACf,CAAC;AAAA,UACH,WAAW,YAAY,wBAAwB;AAC7C,oCAAwB,KAAK;AAAA,cAC3B;AAAA,cACA,MAAM;AAAA,cACN,UAAU;AAAA,cACV,aAAa;AAAA,YACf,CAAC;AAAA,UACH;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAIA,UAAM,sBAAsB,yBAAyB,gBAAgB,iBAAiB,YAAY;AAClG,8BAA0B,iBAAiB,iBAAiB,mBAAmB;AAC/E,4BAAwB,iBAAiB,cAAc,mBAAmB;AAG1E,UAAM,+BAAmD,CAAC;AAC1D,UAAM,qBAAgJ,CAAC;AACvJ,UAAM,mBAAmB,oBAAI,IAAY;AAEzC,eAAW,CAAC,aAAa,MAAM,KAAK,iBAAiB,gBAAgB,QAAQ,GAAG;AAC9E,UAAI,OAAO,eAAe,kBAAkB,OAAO,eAAe,eAAgB;AAClF,YAAM,gBAAgB,qBAAqB,OAAO,WAAW;AAC7D,YAAM,YAAY,MAAM,QAAQ,cAAc,SAAS,IAClD,cAAc,YACf,CAAC;AACL,iBAAW,QAAQ,WAAW;AAC5B,YAAI,CAAC,KAAK,WAAW;AACnB,gBAAM,cAAc,OAAO,KAAK,gBAAgB,WAC5C,KAAK,cACJ,OAAO,KAAK,gBAAgB,WAAW,KAAK,cAAc;AAC/D,uCAA6B,KAAK;AAAA,YAChC;AAAA,YACA,MAAM;AAAA,YACN,UAAU;AAAA,YACV,aAAa;AAAA,YACb,YAAY;AAAA,UACd,CAAC;AACD,gBAAM,UAAU,YAAY,YAAY,EAAE,KAAK;AAC/C,cAAI,WAAW,YAAY,aAAa,CAAC,iBAAiB,IAAI,OAAO,GAAG;AACtE,6BAAiB,IAAI,OAAO;AAC5B,kBAAM,MAAM,OAAO,KAAK,QAAQ,WAAW,KAAK,MAAM;AACtD,kBAAM,YAAY,OAAO,KAAK,cAAc,WAAW,KAAK,YAAY;AACxE,kBAAM,eAAe,OAAO,cAAc,iBAAiB,WAAW,cAAc,eAAe;AACnG,+BAAmB,KAAK;AAAA,cACtB,YAAY;AAAA,cACZ,aAAa;AAAA,cACb,YAAY;AAAA,cACZ,iBAAiB,sBAAsB;AAAA,cACvC,aAAa,KAAK,UAAU;AAAA,gBAC1B,OAAO;AAAA,gBACP,GAAI,OAAO,EAAE,IAAI;AAAA,gBACjB,GAAI,aAAa,EAAE,UAAU;AAAA,gBAC7B,GAAI,gBAAgB,EAAE,aAAa;AAAA,gBACnC,MAAM;AAAA,cACR,CAAC;AAAA,YACH,CAAC;AAAA,UACH;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAGA,UAAM,aAAa,WAAW;AAC9B,UAAM,WAAW,GAAG,OAAO,eAAe;AAAA,MACxC,IAAI;AAAA,MACJ,cAAc,MAAM;AAAA,MACpB,SAAS,iBAAiB;AAAA,MAC1B,cAAc;AAAA,MACd,YAAY,OAAO,iBAAiB,WAAW,QAAQ,CAAC,CAAC;AAAA,MACzD,kBAAkB,iBAAiB,oBAAoB,MAAM;AAAA,MAC7D,QAAQ;AAAA,MACR;AAAA,MACA,UAAU;AAAA,MACV,eAAe;AAAA,MACf;AAAA,MACA,gBAAgB,MAAM;AAAA,MACtB,UAAU,MAAM;AAAA,IAClB,CAAC;AACD,OAAG,QAAQ,QAAQ;AAGnB,UAAM,qBAAqB;AAAA,MACzB;AAAA,MACA,iBAAiB;AAAA,MACjB,MAAM;AAAA,IACR;AAGA,UAAM,oBAAoB;AAAA,MACxB;AAAA,MACA;AAAA,MACA,iBAAiB;AAAA,MACjB;AAAA,MACA,MAAM;AAAA,IACR;AACA,uBAAmB,KAAK,GAAG,iBAAiB;AAG5C,UAAM,kBAAkB;AAAA,MACtB;AAAA,MACA,iBAAiB;AAAA,MACjB,MAAM;AAAA,IACR;AAGA,UAAM,0BAA0B,CAAC,GAAG,oBAAoB,GAAG,iBAAiB,GAAG,oBAAoB,GAAG,iBAAiB,eAAe;AACtI,UAAM,aAAa;AAAA,MACjB,GAAG,wBAAwB,IAAI,CAAC,QAAQ,UAAU;AAChD,cAAM,gBAAgB,qBAAqB,OAAO,WAAW;AAC7D,eAAO,GAAG,OAAO,qBAAqB;AAAA,UACpC,IAAI,WAAW;AAAA,UACf;AAAA,UACA,WAAW;AAAA,UACX,YAAY,OAAO;AAAA,UACnB,aAAa,OAAO;AAAA,UACpB,SAAS;AAAA,UACT,QAAQ;AAAA,UACR,YAAY,OAAO,OAAO,WAAW,QAAQ,CAAC,CAAC;AAAA,UAC/C,iBAAiB,OAAO,mBAAmB,sBAAsB,OAAO,UAAU,KAAK;AAAA,UACvF,gBAAgB,MAAM;AAAA,UACtB,UAAU,MAAM;AAAA,QAClB,CAAC;AAAA,MACH,CAAC;AAAA,MACD,GAAG,iBAAiB,aAAa;AAAA,QAAI,CAAC,OAAO,UAC3C,GAAG,OAAO,qBAAqB;AAAA,UAC7B,IAAI,WAAW;AAAA,UACf;AAAA,UACA,WAAW,wBAAwB,SAAS;AAAA,UAC5C,YAAY;AAAA,UACZ,aAAa;AAAA,UACb,SAAS;AAAA,YACP,IAAI,MAAM;AAAA,YACV,QAAQ,MAAM;AAAA,YACd,SAAS,MAAM;AAAA,YACf,MAAM,MAAM;AAAA,YACZ,SAAS,MAAM;AAAA,YACf,SAAS,MAAM;AAAA,YACf,oBAAoB,MAAM;AAAA,YAC1B,YAAY,MAAM;AAAA,UACpB;AAAA,UACA,QAAQ;AAAA,UACR,YAAY,OAAO,iBAAiB,WAAW,QAAQ,CAAC,CAAC;AAAA,UACzD,iBAAiB;AAAA,UACjB,gBAAgB,MAAM;AAAA,UACtB,UAAU,MAAM;AAAA,QAClB,CAAC;AAAA,MACH;AAAA,IACF;AACA,eAAW,QAAQ,CAAC,MAAM,GAAG,QAAQ,CAAC,CAAC;AAIvC,UAAM,oBAAoB,mBAAmB,SAAS,gBAAgB,SAAS,mBAAmB;AAClG,UAAM,cAAc,CAAC,MACnB,EAAE,gBAAgB,SAAY,EAAE,GAAG,GAAG,aAAa,EAAE,cAAc,kBAAkB,IAAI;AAG3F,UAAM,mBAAmB;AAAA,MACvB,GAAG,iBAAiB,cAAc;AAAA,QAAI,CAAC,MACrC,kBAAkB,IAAI,YAAY,YAAY,YAAY,CAAC,GAAG,KAAK;AAAA,MACrE;AAAA,MACA,GAAG,mBAAmB;AAAA,QAAI,CAAC,MACzB,kBAAkB,IAAI,YAAY,YAAY,YAAY,CAAC,GAAG,KAAK;AAAA,MACrE;AAAA,MACA,GAAG,4BAA4B;AAAA,QAAI,CAAC,MAClC,kBAAkB,IAAI,YAAY,YAAY,YAAY,CAAC,GAAG,KAAK;AAAA,MACrE;AAAA,MACA,GAAG,6BAA6B;AAAA,QAAI,CAAC,MACnC,kBAAkB,IAAI,YAAY,YAAY,YAAY,CAAC,GAAG,KAAK;AAAA,MACrE;AAAA,MACA,GAAG,wBAAwB;AAAA,QAAI,CAAC,MAC9B,kBAAkB,IAAI,YAAY,YAAY,YAAY,CAAC,GAAG,KAAK;AAAA,MACrE;AAAA,IACF;AAGA,UAAM,2BAA2B,oBAAI,IAAY;AACjD,eAAW,SAAS,gBAAgB;AAClC,UAAI,CAAC,MAAM,SAAS,MAAM,YAAY,OAAO;AAC3C,cAAM,aAAa,MAAM,YAAY,MAAM,YAAY;AACvD,iCAAyB,IAAI,UAAU;AACvC,yBAAiB;AAAA,UACf,kBAAkB,IAAI,YAAY,YAAY;AAAA,YAC5C,MAAM;AAAA,YACN,UAAU;AAAA,YACV,aAAa;AAAA,YACb,YAAY,GAAG,MAAM,YAAY,IAAI,KAAK,MAAM,YAAY,KAAK;AAAA,UACnE,GAAG,KAAK;AAAA,QACV;AAAA,MACF;AAAA,IACF;AACA,eAAW,eAAe,sBAAsB;AAC9C,UAAI,YAAY,iBAAkB;AAClC,YAAM,cAAc,YAAY,SAAS,IAAI,YAAY;AACzD,UAAI,CAAC,cAAc,yBAAyB,IAAI,UAAU,EAAG;AAC7D,+BAAyB,IAAI,UAAU;AACvC,uBAAiB;AAAA,QACf,kBAAkB,IAAI,YAAY,YAAY;AAAA,UAC5C,MAAM;AAAA,UACN,UAAU;AAAA,UACV,aAAa;AAAA,UACb,YAAY,GAAG,YAAY,IAAI,KAAK,YAAY,KAAK;AAAA,QACvD,GAAG,KAAK;AAAA,MACV;AAAA,IACF;AAGA,UAAM,gBAAgB,IAAI;AAAA,MACxB,eACG,OAAO,CAAC,MAAM,EAAE,OAAO,SAAS,EAChC,IAAI,CAAC,MAAM,EAAE,YAAY,MAAM,YAAY,CAAC;AAAA,IACjD;AACA,eAAW,CAAC,aAAa,MAAM,KAAK,WAAW,QAAQ,GAAG;AACxD,UAAI,OAAO,eAAe,cAAe;AACzC,YAAMA,WAAU,OAAO;AACvB,YAAM,UAAU,OAAOA,UAAS,OAAO,WAAWA,SAAQ,GAAG,KAAK,EAAE,YAAY,IAAI;AACpF,UAAI,WAAW,CAAC,cAAc,IAAI,OAAO,GAAG;AAC1C,yBAAiB;AAAA,UACf,kBAAkB,IAAI,YAAY,YAAY;AAAA,YAC5C;AAAA,YACA,MAAM;AAAA,YACN,UAAU;AAAA,YACV,aAAa;AAAA,YACb,YAAY;AAAA,UACd,GAAG,KAAK;AAAA,QACV;AAAA,MACF;AAAA,IACF;AAEA,qBAAiB,QAAQ,CAAC,MAAM,GAAG,QAAQ,CAAC,CAAC;AAG7C,UAAM,SAAS,iBAAiB,iBAAiB;AACjD,UAAM,mBAAmB,iBAAiB,oBAAoB,MAAM;AAEpE,UAAM,GAAG,MAAM;AAGf,QAAI;AACF,YAAM,kBAAkB,6BAA6B;AAAA,QACnD,SAAS,MAAM;AAAA,QACf,UAAU,MAAM;AAAA,QAChB,gBAAgB,MAAM;AAAA,MACxB,CAAC;AAED,YAAM,kBAAkB,8BAA8B;AAAA,QACpD,YAAY,SAAS;AAAA,QACrB,SAAS,MAAM;AAAA,QACf,UAAU,MAAM;AAAA,QAChB,gBAAgB,MAAM;AAAA,QACtB,aAAa,WAAW;AAAA,QACxB,kBAAkB,iBAAiB;AAAA,QACnC,YAAY,SAAS;AAAA,QACrB,SAAS,SAAS;AAAA,MACpB,CAAC;AAAA,IACH,SAAS,YAAY;AACnB,cAAQ,MAAM,wDAAwD,UAAU;AAAA,IAClF;AAAA,EACF,SAAS,KAAK;AACZ,UAAM,SAAS;AACf,UAAM,kBAAkB,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AACvE,UAAM,GAAG,MAAM;AAEf,QAAI;AACF,YAAM,kBAAkB,0BAA0B;AAAA,QAChD,SAAS,MAAM;AAAA,QACf,UAAU,MAAM;AAAA,QAChB,gBAAgB,MAAM;AAAA,QACtB,OAAO,MAAM;AAAA,MACf,CAAC;AAAA,IACH,SAAS,YAAY;AACnB,cAAQ,MAAM,oEAAoE,UAAU;AAAA,IAC9F;AAEA,YAAQ,MAAM,oDAAoD,GAAG;AAAA,EACvE;AACF;AAEA,SAAS,4BAA4B,SAAwC;AAC3E,QAAM,YAAY,MAAM,QAAQ,QAAQ,SAAS,IAC5C,QAAQ,YACT,CAAC;AACL,aAAW,QAAQ,WAAW;AAC5B,QAAI,CAAC,KAAK,eAAe,OAAO,KAAK,gBAAgB,UAAU;AAC7D,WAAK,cAAc,KAAK;AAAA,IAC1B;AACA,QAAI,OAAO,KAAK,aAAa,UAAU;AACrC,WAAK,WAAW,OAAO,KAAK,QAAQ;AAAA,IACtC;AACA,QAAI,OAAO,KAAK,cAAc,UAAU;AACtC,WAAK,YAAY,OAAO,KAAK,SAAS;AAAA,IACxC;AAAA,EACF;AACF;AAEA,SAAS,4CACP,gBACA,iBACA,cAC2H;AAC3H,QAAM,kBAAkB,IAAI;AAAA,IAC1B,gBACG,OAAO,CAAC,MAAM,EAAE,eAAe,gBAAgB,EAC/C,IAAI,CAAC,MAAM;AACV,YAAM,IAAI,qBAAqB,EAAE,WAAW;AAC5C,aAAO,OAAO,EAAE,UAAU,WAAW,EAAE,MAAM,YAAY,IAAI;AAAA,IAC/D,CAAC,EACA,OAAO,OAAO;AAAA,EACnB;AAEA,QAAM,cAAc,gBAAgB,IAAI,YAAY;AACpD,QAAM,iBAAiB,CAAC,WAAW,YAAY,cAAc,iBAAiB,YAAY;AAE1F,SAAO,eACJ,OAAO,CAAC,MAAM;AACb,QAAI,EAAE,OAAO,UAAW,QAAO;AAC/B,UAAM,aAAa,EAAE,YAAY,MAAM,YAAY;AACnD,QAAI,gBAAgB,IAAI,UAAU,EAAG,QAAO;AAC5C,QAAI,eAAe,WAAY,QAAO;AACtC,WAAO,CAAC,eAAe,KAAK,CAAC,MAAM,WAAW,SAAS,CAAC,CAAC;AAAA,EAC3D,CAAC,EACA,IAAI,CAAC,OAAO;AAAA,IACX,YAAY;AAAA,IACZ,aAAa;AAAA,IACb,YAAY;AAAA,IACZ,iBAAiB,sBAAsB;AAAA,IACvC,aAAa,KAAK,UAAU;AAAA,MAC1B,MAAM;AAAA,MACN,MAAM,EAAE,YAAY;AAAA,MACpB,OAAO,EAAE,YAAY;AAAA,MACrB,QAAQ;AAAA,IACV,CAAC;AAAA,EACH,EAAE;AACN;AAEA,SAAS,8CACP,gBACA,iBACA,cACyH;AACzH,QAAM,kBAAkB,IAAI;AAAA,IAC1B,gBACG,OAAO,CAAC,MAAM,EAAE,eAAe,cAAc,EAC7C,IAAI,CAAC,MAAM;AACV,YAAM,IAAI,qBAAqB,EAAE,WAAW;AAC5C,YAAM,QAAQ,OAAO,EAAE,iBAAiB,WAAW,EAAE,eAAgB,OAAO,EAAE,UAAU,WAAW,EAAE,QAAQ;AAC7G,aAAO,MAAM,YAAY;AAAA,IAC3B,CAAC,EACA,OAAO,OAAO;AAAA,EACnB;AAEA,QAAM,cAAc,gBAAgB,IAAI,YAAY;AACpD,QAAM,iBAAiB,CAAC,WAAW,YAAY,cAAc,iBAAiB,YAAY;AAE1F,SAAO,eACJ,OAAO,CAAC,MAAM;AACb,QAAI,CAAC,EAAE,OAAO,UAAW,QAAO;AAChC,UAAM,aAAa,EAAE,YAAY,MAAM,YAAY;AACnD,QAAI,gBAAgB,IAAI,UAAU,EAAG,QAAO;AAC5C,QAAI,eAAe,WAAY,QAAO;AACtC,WAAO,CAAC,eAAe,KAAK,CAAC,MAAM,WAAW,SAAS,CAAC,CAAC;AAAA,EAC3D,CAAC,EACA,IAAI,CAAC,OAAO;AAAA,IACX,YAAY;AAAA,IACZ,aAAa;AAAA,IACb,YAAY;AAAA,IACZ,iBAAiB,sBAAsB;AAAA,IACvC,aAAa,KAAK,UAAU;AAAA,MAC1B,cAAc,EAAE,YAAY;AAAA,MAC5B,WAAW,EAAE,MAAO;AAAA,MACpB,aAAa,EAAE,MAAO,eAAe;AAAA,MACrC,aAAa,EAAE,YAAY;AAAA,IAC7B,CAAC;AAAA,EACH,EAAE;AACN;AAEA,SAAS,+CACP,sBACA,gBACA,iBACA,oBACA,cAC2H;AAC3H,QAAM,eAAe,IAAI;AAAA,IACvB,eAAe,IAAI,CAAC,MAAM,EAAE,YAAY,MAAM,YAAY,CAAC;AAAA,EAC7D;AAEA,QAAM,kBAAkB,oBAAI,IAAI;AAAA,IAC9B,GAAG,gBACA,OAAO,CAAC,MAAM,EAAE,eAAe,gBAAgB,EAC/C,IAAI,CAAC,MAAM;AACV,YAAM,IAAI,qBAAqB,EAAE,WAAW;AAC5C,aAAO,OAAO,EAAE,UAAU,WAAW,EAAE,MAAM,YAAY,IAAI;AAAA,IAC/D,CAAC,EACA,OAAO,OAAO;AAAA,IACjB,GAAG,mBACA,IAAI,CAAC,MAAM;AACV,YAAM,IAAI,qBAAqB,EAAE,WAAW;AAC5C,aAAO,OAAO,EAAE,UAAU,WAAW,EAAE,MAAM,YAAY,IAAI;AAAA,IAC/D,CAAC,EACA,OAAO,OAAO;AAAA,EACnB,CAAC;AAED,QAAM,cAAc,gBAAgB,IAAI,YAAY;AACpD,QAAM,iBAAiB,CAAC,WAAW,YAAY,cAAc,iBAAiB,YAAY;AAE1F,SAAO,qBACJ,OAAO,CAAC,MAAM;AACb,QAAI,EAAE,iBAAkB,QAAO;AAC/B,UAAM,cAAc,EAAE,SAAS,IAAI,YAAY;AAC/C,QAAI,CAAC,WAAY,QAAO;AACxB,QAAI,aAAa,IAAI,UAAU,EAAG,QAAO;AACzC,QAAI,gBAAgB,IAAI,UAAU,EAAG,QAAO;AAC5C,QAAI,eAAe,WAAY,QAAO;AACtC,WAAO,CAAC,eAAe,KAAK,CAAC,QAAQ,WAAW,SAAS,GAAG,CAAC;AAAA,EAC/D,CAAC,EACA,IAAI,CAAC,OAAO;AAAA,IACX,YAAY;AAAA,IACZ,aAAa;AAAA,IACb,YAAY;AAAA,IACZ,iBAAiB,sBAAsB;AAAA,IACvC,aAAa,KAAK,UAAU;AAAA,MAC1B,MAAM;AAAA,MACN,MAAM,EAAE;AAAA,MACR,OAAO,EAAE;AAAA,MACT,QAAQ;AAAA,IACV,CAAC;AAAA,EACH,EAAE;AACN;AAEA,eAAe,sBACb,IACA,cACA,OACA,iBAC8J;AAC9J,MAAI,CAAC,gBAAiB,QAAO,CAAC;AAC9B,QAAM,gBAAqK,CAAC;AAE5K,aAAW,UAAU,cAAc;AACjC,QAAI,OAAO,eAAe,eAAgB;AAE1C,UAAM,oBAAoB,OAAO,OAAO,QAAQ,sBAAsB,WAClE,OAAO,QAAQ,kBAAkB,KAAK,IACtC;AAEJ,QAAI,CAAC,kBAAmB;AAExB,QAAI;AACF,YAAM,WAAW,MAAM;AAAA,QACrB;AAAA,QACA;AAAA,QACA;AAAA,UACE;AAAA,UACA,UAAU,MAAM;AAAA,UAChB,gBAAgB,MAAM;AAAA,UACtB,WAAW;AAAA,QACb;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAEA,UAAI,UAAU;AACZ,sBAAc,KAAK;AAAA,UACjB,MAAM;AAAA,UACN,UAAU;AAAA,UACV,aAAa;AAAA,UACb,eAAe,SAAS,eAAe,SAAS;AAAA,UAChD,YAAY;AAAA,UACZ,aAAa,OAAO;AAAA,QACtB,CAAC;AAAA,MACH;AAAA,IACF,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,SAAO;AACT;AAEA,SAAS,qBAAqB,OAA4B;AACxD,QAAM,UAAU,MAAM,WAAW;AACjC,QAAM,YAAY,iBAAiB,KAAK,OAAO;AAC/C,QAAM,eAAe,MAAM,gBAAgB,UAAU;AACrD,SAAO,aAAa,eAAe;AACrC;AAEA,SAAS,yBACP,gBACA,iBACqB;AACrB,QAAM,cAAc,oBAAI,IAAoB;AAE5C,aAAW,KAAK,gBAAgB;AAC9B,QAAI,EAAE,YAAY,QAAQ,EAAE,YAAY,OAAO;AAC7C,kBAAY,IAAI,EAAE,YAAY,KAAK,KAAK,EAAE,YAAY,GAAG,EAAE,YAAY,MAAM,KAAK,EAAE,YAAY,CAAC;AAAA,IACnG;AAAA,EACF;AAEA,aAAW,KAAK,iBAAiB;AAC/B,QAAI,EAAE,QAAQ,EAAE,OAAO;AACrB,YAAM,MAAM,EAAE,KAAK,KAAK,EAAE,YAAY;AACtC,UAAI,CAAC,YAAY,IAAI,GAAG,GAAG;AACzB,oBAAY,IAAI,KAAK,EAAE,MAAM,KAAK,EAAE,YAAY,CAAC;AAAA,MACnD;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,0BACP,SACA,qBACM;AACN,aAAW,UAAU,SAAS;AAC5B,QAAI,OAAO,eAAe,iBAAkB;AAC5C,UAAM,UAAU,qBAAqB,OAAO,WAAW;AACvD,QAAI,QAAQ,MAAO;AACnB,UAAM,OAAO,OAAO,QAAQ,SAAS,WAAW,QAAQ,KAAK,KAAK,IAAI;AACtE,QAAI,CAAC,KAAM;AAEX,UAAM,QAAQ,oBAAoB,IAAI,KAAK,YAAY,CAAC,KACnD,qBAAqB,MAAM,mBAAmB;AACnD,QAAI,OAAO;AACT,cAAQ,QAAQ;AAChB,aAAO,cAAc,KAAK,UAAU,OAAO;AAAA,IAC7C;AAAA,EACF;AACF;AAEA,SAAS,wBACP,cACA,qBACM;AACN,QAAM,cAAc,IAAI,IAAI,oBAAoB,OAAO,CAAC;AACxD,aAAW,SAAS,cAAc;AAChC,UAAM,UAAU,MAAM,GAAG,KAAK,EAAE,YAAY;AAC5C,QAAI,YAAY,IAAI,OAAO,EAAG;AAE9B,UAAM,UAAU,MAAM,UAAU,IAAI,KAAK;AACzC,QAAI,CAAC,OAAQ;AACb,UAAM,iBAAiB,oBAAoB,IAAI,OAAO,YAAY,CAAC,KAC9D,qBAAqB,QAAQ,mBAAmB;AACrD,QAAI,gBAAgB;AAClB,YAAM,KAAK;AAAA,IACb;AAAA,EACF;AACF;AAEA,SAAS,2BAA2B,OAA2B;AAC7D,MAAI,OAAO,MAAM,WAAW;AAC5B,MAAI,CAAC,QAAQ,MAAM,SAAS;AAC1B,WAAO,gBAAgB,MAAM,OAAO;AAAA,EACtC;AACA,SAAO,KACJ,QAAQ,SAAS,IAAI,EACrB,QAAQ,OAAO,IAAI,EACnB,QAAQ,OAAO,GAAG,EAClB,QAAQ,UAAU,GAAG,EACrB,QAAQ,WAAW,MAAM,EACzB,KAAK;AACV;AAEA,SAAS,qBAAqB,MAAc,KAA8C;AACxF,QAAM,QAAQ,KAAK,YAAY;AAE/B,QAAM,QAAQ,MAAM,MAAM,aAAa,EAAE,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,OAAO;AAC5E,aAAW,QAAQ,OAAO;AACxB,UAAM,QAAQ,IAAI,IAAI,IAAI;AAC1B,QAAI,MAAO,QAAO;AAAA,EACpB;AAEA,aAAW,CAAC,SAAS,QAAQ,KAAK,KAAK;AACrC,QAAI,MAAM,SAAS,OAAO,KAAK,QAAQ,SAAS,KAAK,GAAG;AACtD,aAAO;AAAA,IACT;AACA,eAAW,QAAQ,OAAO;AACxB,UAAI,KAAK,SAAS,OAAO,KAAK,QAAQ,SAAS,IAAI,GAAG;AACpD,eAAO;AAAA,MACT;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;",
|
|
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-
|
|
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-
|
|
220
|
+
"@open-mercato/shared": "0.4.8-develop-280c02b529"
|
|
221
221
|
},
|
|
222
222
|
"devDependencies": {
|
|
223
|
-
"@open-mercato/shared": "0.4.8-develop-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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:
|
|
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) {
|