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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/dist/generated/entities/inbox_proposal/index.js +2 -0
  2. package/dist/generated/entities/inbox_proposal/index.js.map +2 -2
  3. package/dist/modules/catalog/inbox-actions.js +49 -0
  4. package/dist/modules/catalog/inbox-actions.js.map +2 -2
  5. package/dist/modules/customers/inbox-actions.js +69 -27
  6. package/dist/modules/customers/inbox-actions.js.map +3 -3
  7. package/dist/modules/inbox_ops/ai-tools.js +346 -0
  8. package/dist/modules/inbox_ops/ai-tools.js.map +7 -0
  9. package/dist/modules/inbox_ops/api/extract/route.js +3 -2
  10. package/dist/modules/inbox_ops/api/extract/route.js.map +2 -2
  11. package/dist/modules/inbox_ops/api/proposals/[id]/accept-all/route.js +4 -0
  12. package/dist/modules/inbox_ops/api/proposals/[id]/accept-all/route.js.map +2 -2
  13. package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/accept/route.js +4 -0
  14. package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/accept/route.js.map +2 -2
  15. package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/complete/route.js +4 -0
  16. package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/complete/route.js.map +2 -2
  17. package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/reject/route.js +4 -0
  18. package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/reject/route.js.map +2 -2
  19. package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/route.js +4 -0
  20. package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/route.js.map +2 -2
  21. package/dist/modules/inbox_ops/api/proposals/[id]/categorize/route.js +59 -0
  22. package/dist/modules/inbox_ops/api/proposals/[id]/categorize/route.js.map +7 -0
  23. package/dist/modules/inbox_ops/api/proposals/[id]/reject/route.js +4 -0
  24. package/dist/modules/inbox_ops/api/proposals/[id]/reject/route.js.map +2 -2
  25. package/dist/modules/inbox_ops/api/proposals/[id]/replies/[replyId]/send/route.js +34 -14
  26. package/dist/modules/inbox_ops/api/proposals/[id]/replies/[replyId]/send/route.js.map +2 -2
  27. package/dist/modules/inbox_ops/api/proposals/counts/route.js +49 -4
  28. package/dist/modules/inbox_ops/api/proposals/counts/route.js.map +2 -2
  29. package/dist/modules/inbox_ops/api/proposals/route.js +13 -0
  30. package/dist/modules/inbox_ops/api/proposals/route.js.map +2 -2
  31. package/dist/modules/inbox_ops/api/settings/route.js +33 -2
  32. package/dist/modules/inbox_ops/api/settings/route.js.map +2 -2
  33. package/dist/modules/inbox_ops/backend/inbox-ops/page.js +28 -3
  34. package/dist/modules/inbox_ops/backend/inbox-ops/page.js.map +2 -2
  35. package/dist/modules/inbox_ops/backend/inbox-ops/proposals/[id]/page.js +103 -5
  36. package/dist/modules/inbox_ops/backend/inbox-ops/proposals/[id]/page.js.map +2 -2
  37. package/dist/modules/inbox_ops/components/messages/InboxEmailContent.js +24 -0
  38. package/dist/modules/inbox_ops/components/messages/InboxEmailContent.js.map +7 -0
  39. package/dist/modules/inbox_ops/components/messages/InboxEmailPreview.js +29 -0
  40. package/dist/modules/inbox_ops/components/messages/InboxEmailPreview.js.map +7 -0
  41. package/dist/modules/inbox_ops/components/proposals/CategoryBadge.js +59 -0
  42. package/dist/modules/inbox_ops/components/proposals/CategoryBadge.js.map +7 -0
  43. package/dist/modules/inbox_ops/components/proposals/EditActionDialog.js +3 -1
  44. package/dist/modules/inbox_ops/components/proposals/EditActionDialog.js.map +2 -2
  45. package/dist/modules/inbox_ops/data/entities.js +4 -0
  46. package/dist/modules/inbox_ops/data/entities.js.map +2 -2
  47. package/dist/modules/inbox_ops/data/validators.js +30 -5
  48. package/dist/modules/inbox_ops/data/validators.js.map +2 -2
  49. package/dist/modules/inbox_ops/lib/cache.js +53 -0
  50. package/dist/modules/inbox_ops/lib/cache.js.map +7 -0
  51. package/dist/modules/inbox_ops/lib/contactValidation.js +38 -3
  52. package/dist/modules/inbox_ops/lib/contactValidation.js.map +2 -2
  53. package/dist/modules/inbox_ops/lib/executionHelpers.js +28 -1
  54. package/dist/modules/inbox_ops/lib/executionHelpers.js.map +2 -2
  55. package/dist/modules/inbox_ops/lib/extractionPrompt.js +2 -1
  56. package/dist/modules/inbox_ops/lib/extractionPrompt.js.map +2 -2
  57. package/dist/modules/inbox_ops/lib/messageObjectPreviews.js +52 -0
  58. package/dist/modules/inbox_ops/lib/messageObjectPreviews.js.map +7 -0
  59. package/dist/modules/inbox_ops/lib/messagesIntegration.js +155 -0
  60. package/dist/modules/inbox_ops/lib/messagesIntegration.js.map +7 -0
  61. package/dist/modules/inbox_ops/message-objects.js +36 -0
  62. package/dist/modules/inbox_ops/message-objects.js.map +7 -0
  63. package/dist/modules/inbox_ops/message-types.js +38 -0
  64. package/dist/modules/inbox_ops/message-types.js.map +7 -0
  65. package/dist/modules/inbox_ops/migrations/Migration20260303173020.js +13 -0
  66. package/dist/modules/inbox_ops/migrations/Migration20260303173020.js.map +7 -0
  67. package/dist/modules/inbox_ops/migrations/Migration20260303173215.js +15 -0
  68. package/dist/modules/inbox_ops/migrations/Migration20260303173215.js.map +7 -0
  69. package/dist/modules/inbox_ops/search.js +5 -3
  70. package/dist/modules/inbox_ops/search.js.map +2 -2
  71. package/dist/modules/inbox_ops/subscribers/extractionWorker.js +65 -3
  72. package/dist/modules/inbox_ops/subscribers/extractionWorker.js.map +2 -2
  73. package/generated/entities/inbox_proposal/index.ts +1 -0
  74. package/package.json +3 -3
  75. package/src/modules/catalog/inbox-actions.ts +55 -0
  76. package/src/modules/customers/inbox-actions.ts +86 -27
  77. package/src/modules/inbox_ops/ai-tools.ts +451 -0
  78. package/src/modules/inbox_ops/api/extract/route.ts +3 -2
  79. package/src/modules/inbox_ops/api/proposals/[id]/accept-all/route.ts +5 -0
  80. package/src/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/accept/route.ts +5 -0
  81. package/src/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/complete/route.ts +5 -0
  82. package/src/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/reject/route.ts +5 -0
  83. package/src/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/route.ts +5 -0
  84. package/src/modules/inbox_ops/api/proposals/[id]/categorize/route.ts +61 -0
  85. package/src/modules/inbox_ops/api/proposals/[id]/reject/route.ts +5 -0
  86. package/src/modules/inbox_ops/api/proposals/[id]/replies/[replyId]/send/route.ts +36 -16
  87. package/src/modules/inbox_ops/api/proposals/counts/route.ts +60 -5
  88. package/src/modules/inbox_ops/api/proposals/route.ts +14 -1
  89. package/src/modules/inbox_ops/api/settings/route.ts +36 -2
  90. package/src/modules/inbox_ops/backend/inbox-ops/page.tsx +31 -3
  91. package/src/modules/inbox_ops/backend/inbox-ops/proposals/[id]/page.tsx +103 -1
  92. package/src/modules/inbox_ops/components/messages/InboxEmailContent.tsx +45 -0
  93. package/src/modules/inbox_ops/components/messages/InboxEmailPreview.tsx +40 -0
  94. package/src/modules/inbox_ops/components/proposals/CategoryBadge.tsx +59 -0
  95. package/src/modules/inbox_ops/components/proposals/EditActionDialog.tsx +3 -1
  96. package/src/modules/inbox_ops/components/proposals/types.ts +1 -0
  97. package/src/modules/inbox_ops/data/entities.ts +14 -1
  98. package/src/modules/inbox_ops/data/validators.ts +41 -5
  99. package/src/modules/inbox_ops/lib/cache.ts +60 -0
  100. package/src/modules/inbox_ops/lib/contactValidation.ts +31 -2
  101. package/src/modules/inbox_ops/lib/executionHelpers.ts +40 -0
  102. package/src/modules/inbox_ops/lib/extractionPrompt.ts +2 -1
  103. package/src/modules/inbox_ops/lib/messageObjectPreviews.ts +61 -0
  104. package/src/modules/inbox_ops/lib/messagesIntegration.ts +231 -0
  105. package/src/modules/inbox_ops/message-objects.ts +34 -0
  106. package/src/modules/inbox_ops/message-types.ts +36 -0
  107. package/src/modules/inbox_ops/migrations/Migration20260303173020.ts +13 -0
  108. package/src/modules/inbox_ops/migrations/Migration20260303173215.ts +15 -0
  109. package/src/modules/inbox_ops/search.ts +5 -3
  110. package/src/modules/inbox_ops/subscribers/extractionWorker.ts +75 -1
@@ -0,0 +1,34 @@
1
+ import type { MessageObjectTypeDefinition } from '@open-mercato/shared/modules/messages/types'
2
+ import { InboxEmailPreview } from './components/messages/InboxEmailPreview'
3
+
4
+ export const messageObjectTypes: MessageObjectTypeDefinition[] = [
5
+ {
6
+ module: 'inbox_ops',
7
+ entityType: 'inbox_email',
8
+ messageTypes: ['inbox_ops.email', 'inbox_ops.reply'],
9
+ labelKey: 'inbox_ops.title',
10
+ icon: 'mail-open',
11
+ PreviewComponent: InboxEmailPreview,
12
+ actions: [
13
+ {
14
+ id: 'view',
15
+ labelKey: 'inbox_ops.view_in_messages',
16
+ variant: 'outline',
17
+ href: '/backend/inbox-ops',
18
+ },
19
+ ],
20
+ loadPreview: async (entityId, ctx) => {
21
+ try {
22
+ if (typeof window !== 'undefined') {
23
+ return { title: 'Inbox Email', subtitle: entityId }
24
+ }
25
+ const { loadInboxEmailPreview } = await import('./lib/messageObjectPreviews')
26
+ return loadInboxEmailPreview(entityId, ctx)
27
+ } catch {
28
+ return { title: 'Inbox Email', subtitle: entityId }
29
+ }
30
+ },
31
+ },
32
+ ]
33
+
34
+ export default messageObjectTypes
@@ -0,0 +1,36 @@
1
+ import type { MessageTypeDefinition } from '@open-mercato/shared/modules/messages/types'
2
+ import { InboxEmailContent } from './components/messages/InboxEmailContent'
3
+
4
+ export const messageTypes: MessageTypeDefinition[] = [
5
+ {
6
+ type: 'inbox_ops.email',
7
+ module: 'inbox_ops',
8
+ labelKey: 'inbox_ops.title',
9
+ icon: 'mail-open',
10
+ color: 'blue',
11
+ ui: {
12
+ listItemComponent: 'messages.default.listItem',
13
+ contentComponent: 'inbox_ops.email.content',
14
+ actionsComponent: 'messages.default.actions',
15
+ },
16
+ ContentComponent: InboxEmailContent,
17
+ allowReply: false,
18
+ allowForward: true,
19
+ },
20
+ {
21
+ type: 'inbox_ops.reply',
22
+ module: 'inbox_ops',
23
+ labelKey: 'inbox_ops.action_type.draft_reply',
24
+ icon: 'reply',
25
+ color: 'green',
26
+ ui: {
27
+ listItemComponent: 'messages.default.listItem',
28
+ contentComponent: 'messages.default.content',
29
+ actionsComponent: 'messages.default.actions',
30
+ },
31
+ allowReply: true,
32
+ allowForward: true,
33
+ },
34
+ ]
35
+
36
+ export default messageTypes
@@ -0,0 +1,13 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+
3
+ export class Migration20260303173020 extends Migration {
4
+
5
+ override async up(): Promise<void> {
6
+ this.addSql(`create index "inbox_discrepancies_organization_id_tenant_id_index" on "inbox_discrepancies" ("organization_id", "tenant_id");`);
7
+ }
8
+
9
+ override async down(): Promise<void> {
10
+ this.addSql(`drop index "inbox_discrepancies_organization_id_tenant_id_index";`);
11
+ }
12
+
13
+ }
@@ -0,0 +1,15 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+
3
+ export class Migration20260303173215 extends Migration {
4
+
5
+ override async up(): Promise<void> {
6
+ this.addSql(`alter table "inbox_proposals" add column "category" text null;`);
7
+ this.addSql(`create index "inbox_proposals_organization_id_tenant_id_category_index" on "inbox_proposals" ("organization_id", "tenant_id", "category");`);
8
+ }
9
+
10
+ override async down(): Promise<void> {
11
+ this.addSql(`drop index "inbox_proposals_organization_id_tenant_id_category_index";`);
12
+ this.addSql(`alter table "inbox_proposals" drop column "category";`);
13
+ }
14
+
15
+ }
@@ -22,7 +22,7 @@ export const searchConfig: SearchModuleConfig = {
22
22
  enabled: true,
23
23
  priority: 6,
24
24
  fieldPolicy: {
25
- searchable: ['summary'],
25
+ searchable: ['summary', 'category'],
26
26
  excluded: ['metadata', 'participants'],
27
27
  },
28
28
  buildSource: async (ctx: SearchBuildContext): Promise<SearchIndexSource | null> => {
@@ -35,17 +35,19 @@ export const searchConfig: SearchModuleConfig = {
35
35
  fields: {
36
36
  status: record.status,
37
37
  confidence: record.confidence,
38
+ category: record.category,
38
39
  detected_language: record.detected_language,
39
40
  },
40
41
  presenter: {
41
42
  title: String(record.summary || 'Inbox Proposal').slice(0, 80),
42
- subtitle: `Confidence: ${record.confidence} - Status: ${record.status}`,
43
+ subtitle: `Confidence: ${record.confidence} - Status: ${record.status}${record.category ? ` - Category: ${record.category}` : ''}`,
43
44
  icon: 'inbox',
44
45
  },
45
46
  checksumSource: {
46
47
  summary: record.summary,
47
48
  status: record.status,
48
49
  confidence: record.confidence,
50
+ category: record.category,
49
51
  detectedLanguage: record.detected_language,
50
52
  },
51
53
  }
@@ -53,7 +55,7 @@ export const searchConfig: SearchModuleConfig = {
53
55
  formatResult: async (ctx: SearchBuildContext): Promise<SearchResultPresenter | null> => {
54
56
  return {
55
57
  title: String(ctx.record.summary || 'Inbox Proposal').slice(0, 80),
56
- subtitle: `Confidence: ${ctx.record.confidence} - Status: ${ctx.record.status}`,
58
+ subtitle: `Confidence: ${ctx.record.confidence} - Status: ${ctx.record.status}${ctx.record.category ? ` - Category: ${ctx.record.category}` : ''}`,
57
59
  icon: 'inbox',
58
60
  }
59
61
  },
@@ -15,7 +15,12 @@ import { extractParticipantsFromThread } from '../lib/emailParser'
15
15
  import { runExtractionWithConfiguredProvider } from '../lib/llmProvider'
16
16
  import { safeParsePayloadJson } from '../lib/validation'
17
17
  import { htmlToPlainText } from '../lib/htmlToPlainText'
18
+ import { runWithCacheTenant } from '@open-mercato/cache'
18
19
  import { emitInboxOpsEvent } from '../events'
20
+ import { createMessageRecordForEmail } from '../lib/messagesIntegration'
21
+ import { resolveCache, invalidateCountsCache } from '../lib/cache'
22
+
23
+ const SYSTEM_USER_ID = '00000000-0000-0000-0000-000000000000'
19
24
 
20
25
  export const metadata = {
21
26
  event: 'inbox_ops.email.received',
@@ -348,6 +353,7 @@ export default async function handle(payload: EmailReceivedPayload, ctx: Resolve
348
353
  id: proposalId,
349
354
  inboxEmailId: email.id,
350
355
  summary: extractionResult.summary,
356
+ category: extractionResult.category || null,
351
357
  participants: enrichedParticipants,
352
358
  confidence: String(extractionResult.confidence.toFixed(2)),
353
359
  detectedLanguage: extractionResult.detectedLanguage || email.detectedLanguage,
@@ -366,6 +372,7 @@ export default async function handle(payload: EmailReceivedPayload, ctx: Resolve
366
372
  contactMatches,
367
373
  extractionResult.proposedActions,
368
374
  email.toAddress,
375
+ email.forwardedByAddress,
369
376
  )
370
377
 
371
378
  // Step 6d-2: Also generate create_contact for LLM-discovered unmatched participants
@@ -385,8 +392,15 @@ export default async function handle(payload: EmailReceivedPayload, ctx: Resolve
385
392
  email.toAddress,
386
393
  )
387
394
 
395
+ // Step 6f: Deduplicate — remove company create_contact actions when a person
396
+ // action with the same companyName already exists (person creation auto-creates
397
+ // the company, so the separate company action would be redundant).
398
+ const dedupedProposedActions = deduplicateCompanyActions([
399
+ ...autoContactActions, ...autoLinkActions, ...autoProductActions, ...extractionResult.proposedActions,
400
+ ])
401
+
388
402
  // Create actions — contact & product creation actions go first so they're executed before orders
389
- const combinedProposedActions = [...autoContactActions, ...autoLinkActions, ...autoProductActions, ...extractionResult.proposedActions]
403
+ const combinedProposedActions = dedupedProposedActions
390
404
  const allActions = [
391
405
  ...combinedProposedActions.map((action, index) => {
392
406
  const parsedPayload = safeParsePayloadJson(action.payloadJson)
@@ -518,6 +532,39 @@ export default async function handle(payload: EmailReceivedPayload, ctx: Resolve
518
532
 
519
533
  await em.flush()
520
534
 
535
+ // Step 8b: Invalidate counts cache (new proposal affects counts)
536
+ try {
537
+ const cache = resolveCache(ctx)
538
+ await runWithCacheTenant(email.tenantId, () => invalidateCountsCache(cache, email.tenantId))
539
+ } catch (cacheErr) {
540
+ console.warn('[inbox_ops:extraction-worker] Cache invalidation failed (non-fatal):', cacheErr)
541
+ }
542
+
543
+ // Step 8c: Register email as a message record (graceful degradation)
544
+ try {
545
+ await createMessageRecordForEmail(
546
+ {
547
+ id: email.id,
548
+ subject: email.subject,
549
+ cleanedText: email.cleanedText,
550
+ rawText: email.rawText,
551
+ forwardedByAddress: email.forwardedByAddress,
552
+ forwardedByName: email.forwardedByName,
553
+ status: email.status,
554
+ },
555
+ {
556
+ container: ctx,
557
+ scope: {
558
+ tenantId: email.tenantId,
559
+ organizationId: email.organizationId,
560
+ userId: SYSTEM_USER_ID,
561
+ },
562
+ },
563
+ )
564
+ } catch (msgErr) {
565
+ console.error('[inbox_ops:extraction-worker] Messages integration failed (non-fatal):', msgErr)
566
+ }
567
+
521
568
  // Step 9: Emit events
522
569
  try {
523
570
  await emitInboxOpsEvent('inbox_ops.email.processed', {
@@ -580,6 +627,7 @@ function buildContactActionsForUnmatchedParticipants(
580
627
  contactMatches: { participant: { name: string; email: string }; match?: { contactId: string } | null }[],
581
628
  existingActions: { actionType: string; payloadJson: string }[],
582
629
  inboxAddress: string,
630
+ forwardedByAddress?: string,
583
631
  ): { actionType: 'create_contact'; description: string; confidence: number; requiredFeature: string; payloadJson: string }[] {
584
632
  const alreadyProposed = new Set(
585
633
  existingActions
@@ -592,14 +640,17 @@ function buildContactActionsForUnmatchedParticipants(
592
640
  )
593
641
 
594
642
  const inboxLower = (inboxAddress || '').toLowerCase()
643
+ const forwardedByLower = (forwardedByAddress || '').toLowerCase()
595
644
  const systemPatterns = ['noreply', 'no-reply', 'donotreply', 'mailer-daemon', 'postmaster']
596
645
 
597
646
  return contactMatches
598
647
  .filter((m) => {
599
648
  if (m.match?.contactId) return false
600
649
  const emailLower = m.participant.email.toLowerCase()
650
+ if (!emailLower || !emailLower.includes('@')) return false
601
651
  if (alreadyProposed.has(emailLower)) return false
602
652
  if (emailLower === inboxLower) return false
653
+ if (forwardedByLower && emailLower === forwardedByLower) return false
603
654
  return !systemPatterns.some((p) => emailLower.includes(p))
604
655
  })
605
656
  .map((m) => ({
@@ -844,6 +895,29 @@ function buildFullTextForExtraction(email: InboxEmail): string {
844
895
  .trim()
845
896
  }
846
897
 
898
+ function deduplicateCompanyActions<T extends { actionType: string; payloadJson: string }>(
899
+ actions: T[],
900
+ ): T[] {
901
+ // Collect company names that will be auto-created by person actions via companyName field
902
+ const personCompanyNames = new Set<string>()
903
+ for (const action of actions) {
904
+ if (action.actionType !== 'create_contact') continue
905
+ const payload = safeParsePayloadJson(action.payloadJson)
906
+ if (payload.type === 'person' && typeof payload.companyName === 'string' && payload.companyName.trim()) {
907
+ personCompanyNames.add(payload.companyName.trim().toLowerCase())
908
+ }
909
+ }
910
+ if (personCompanyNames.size === 0) return actions
911
+
912
+ return actions.filter((action) => {
913
+ if (action.actionType !== 'create_contact') return true
914
+ const payload = safeParsePayloadJson(action.payloadJson)
915
+ if (payload.type !== 'company') return true
916
+ const companyName = typeof payload.name === 'string' ? payload.name.trim().toLowerCase() : ''
917
+ return !companyName || !personCompanyNames.has(companyName)
918
+ })
919
+ }
920
+
847
921
  function findPartialNameMatch(name: string, map: Map<string, string>): string | undefined {
848
922
  const lower = name.toLowerCase()
849
923
  // Split on common separators (e.g. "Marco Rossi / Rossi Imports S.r.l.")