@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,155 @@
1
+ import { getRecipientUserIdsForFeature } from "../../notifications/lib/notificationRecipients.js";
2
+ function asContainer(resolver) {
3
+ return resolver;
4
+ }
5
+ const SYSTEM_USER_ID = "00000000-0000-0000-0000-000000000000";
6
+ async function resolveMessageSenderUserId(em, forwardedByEmail, recipientUserIds, scope) {
7
+ try {
8
+ const knex = em.getKnex();
9
+ const normalizedEmail = forwardedByEmail.trim().toLowerCase();
10
+ if (normalizedEmail) {
11
+ const row = await knex("users").select("id").where("email", normalizedEmail).whereNull("deleted_at").first();
12
+ if (row?.id) return row.id;
13
+ }
14
+ } catch {
15
+ }
16
+ if (recipientUserIds.length > 0) return recipientUserIds[0];
17
+ return SYSTEM_USER_ID;
18
+ }
19
+ function resolveCommandBus(container) {
20
+ try {
21
+ const bus = container.resolve("commandBus");
22
+ return bus && typeof bus.execute === "function" ? bus : null;
23
+ } catch {
24
+ return null;
25
+ }
26
+ }
27
+ async function createMessageRecordForEmail(email, ctx) {
28
+ try {
29
+ const commandBus = resolveCommandBus(ctx.container);
30
+ if (!commandBus) return null;
31
+ const em = ctx.container.resolve("em");
32
+ const knex = em.getKnex();
33
+ const recipientUserIds = await getRecipientUserIdsForFeature(
34
+ knex,
35
+ ctx.scope.tenantId,
36
+ "inbox_ops.proposals.view"
37
+ );
38
+ const recipients = recipientUserIds.map((userId) => ({ userId, type: "to" }));
39
+ const bodyText = email.rawText || email.cleanedText || "";
40
+ const senderUserId = await resolveMessageSenderUserId(
41
+ em,
42
+ email.forwardedByAddress,
43
+ recipientUserIds,
44
+ ctx.scope
45
+ );
46
+ const { result } = await commandBus.execute("messages.messages.compose", {
47
+ input: {
48
+ type: "inbox_ops.email",
49
+ visibility: recipients.length > 0 ? "internal" : "public",
50
+ sourceEntityType: "inbox_ops:inbox_email",
51
+ sourceEntityId: email.id,
52
+ externalEmail: email.forwardedByAddress,
53
+ externalName: email.forwardedByName || void 0,
54
+ recipients,
55
+ subject: email.subject,
56
+ body: bodyText.slice(0, 5e4),
57
+ bodyFormat: "text",
58
+ priority: "normal",
59
+ isDraft: false,
60
+ sendViaEmail: false,
61
+ objects: [
62
+ {
63
+ entityModule: "inbox_ops",
64
+ entityType: "inbox_email",
65
+ entityId: email.id,
66
+ actionRequired: false
67
+ }
68
+ ],
69
+ tenantId: ctx.scope.tenantId,
70
+ organizationId: ctx.scope.organizationId,
71
+ userId: senderUserId
72
+ },
73
+ ctx: {
74
+ container: asContainer(ctx.container),
75
+ auth: null,
76
+ organizationScope: null,
77
+ selectedOrganizationId: ctx.scope.organizationId,
78
+ organizationIds: [ctx.scope.organizationId]
79
+ }
80
+ });
81
+ const messageId = result?.id ?? null;
82
+ return messageId;
83
+ } catch (err) {
84
+ console.error(
85
+ "[inbox_ops:messages] Failed to create message record for email:",
86
+ err instanceof Error ? err.message : String(err)
87
+ );
88
+ return null;
89
+ }
90
+ }
91
+ async function createMessageRecordForReply(reply, inboxEmailId, ctx) {
92
+ try {
93
+ const commandBus = resolveCommandBus(ctx.container);
94
+ if (!commandBus) return null;
95
+ const em = ctx.container.resolve("em");
96
+ const knex = em.getKnex();
97
+ const recipientUserIds = await getRecipientUserIdsForFeature(
98
+ knex,
99
+ ctx.scope.tenantId,
100
+ "inbox_ops.proposals.view"
101
+ );
102
+ if (recipientUserIds.length === 0) return null;
103
+ const recipients = recipientUserIds.map((userId) => ({ userId, type: "to" }));
104
+ const { result } = await commandBus.execute("messages.messages.compose", {
105
+ input: {
106
+ type: "inbox_ops.reply",
107
+ visibility: "internal",
108
+ sourceEntityType: "inbox_ops:inbox_email",
109
+ sourceEntityId: inboxEmailId,
110
+ externalEmail: reply.to,
111
+ externalName: reply.toName ?? void 0,
112
+ recipients,
113
+ subject: reply.subject,
114
+ body: reply.body,
115
+ bodyFormat: "text",
116
+ priority: "normal",
117
+ isDraft: false,
118
+ sendViaEmail: false,
119
+ objects: [
120
+ {
121
+ entityModule: "inbox_ops",
122
+ entityType: "inbox_email",
123
+ entityId: inboxEmailId,
124
+ actionRequired: false
125
+ }
126
+ ],
127
+ tenantId: ctx.scope.tenantId,
128
+ organizationId: ctx.scope.organizationId,
129
+ userId: ctx.scope.userId
130
+ },
131
+ ctx: {
132
+ container: asContainer(ctx.container),
133
+ auth: null,
134
+ organizationScope: null,
135
+ selectedOrganizationId: ctx.scope.organizationId,
136
+ organizationIds: [ctx.scope.organizationId]
137
+ }
138
+ });
139
+ const messageId = result?.id ?? null;
140
+ if (!messageId) return null;
141
+ return { messageId };
142
+ } catch (err) {
143
+ console.error(
144
+ "[inbox_ops:messages] Failed to create reply message record:",
145
+ err instanceof Error ? err.message : String(err)
146
+ );
147
+ return null;
148
+ }
149
+ }
150
+ export {
151
+ createMessageRecordForEmail,
152
+ createMessageRecordForReply,
153
+ resolveMessageSenderUserId
154
+ };
155
+ //# sourceMappingURL=messagesIntegration.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/inbox_ops/lib/messagesIntegration.ts"],
4
+ "sourcesContent": ["import type { AwilixContainer } from 'awilix'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands'\nimport { getRecipientUserIdsForFeature } from '../../notifications/lib/notificationRecipients'\n\ninterface ResolverLike {\n resolve: <T = unknown>(name: string) => T\n}\n\nfunction asContainer(resolver: ResolverLike): AwilixContainer {\n return resolver as unknown as AwilixContainer\n}\n\ninterface MessagesIntegrationScope {\n tenantId: string\n organizationId: string\n userId: string\n}\n\ninterface MessagesIntegrationContext {\n container: ResolverLike\n scope: MessagesIntegrationScope\n}\n\ninterface InboxEmailData {\n id: string\n subject: string\n cleanedText?: string | null\n rawText?: string | null\n forwardedByAddress: string\n forwardedByName?: string | null\n status: string\n}\n\ninterface DraftReplyData {\n to: string\n toName?: string | null\n subject: string\n body: string\n}\n\nconst SYSTEM_USER_ID = '00000000-0000-0000-0000-000000000000'\n\n/**\n * Resolve a real user ID to use as the message sender.\n * Tries to find the forwarding user by email in the auth users table,\n * then falls back to the first recipient (admin with proposals.view).\n * Last resort: SYSTEM_USER_ID (zero UUID).\n */\nexport async function resolveMessageSenderUserId(\n em: EntityManager,\n forwardedByEmail: string,\n recipientUserIds: string[],\n scope: { tenantId: string; organizationId: string },\n): Promise<string> {\n try {\n // Direct knex: users.email is a plaintext login field, not encrypted at\n // field level, so findOneWithDecryption is unnecessary here.\n const knex = em.getKnex()\n const normalizedEmail = forwardedByEmail.trim().toLowerCase()\n if (normalizedEmail) {\n const row = await knex('users')\n .select('id')\n .where('email', normalizedEmail)\n .whereNull('deleted_at')\n .first()\n if (row?.id) return row.id\n }\n } catch {\n // User lookup failed \u2014 fall through\n }\n if (recipientUserIds.length > 0) return recipientUserIds[0]\n return SYSTEM_USER_ID\n}\n\nfunction resolveCommandBus(container: ResolverLike): CommandBus | null {\n try {\n const bus = container.resolve('commandBus') as CommandBus\n return bus && typeof bus.execute === 'function' ? bus : null\n } catch {\n return null\n }\n}\n\n/**\n * Creates an internal message record for an incoming inbox email.\n *\n * The message is delivered to all users with the `inbox_ops.proposals.view`\n * feature in the tenant \u2014 mirroring the same audience that sees proposals\n * in the inbox_ops module. This follows the shared-queue pattern used by\n * all major ERP/CRM systems (Salesforce queues, Dynamics 365 queues,\n * HubSpot shared inboxes, Odoo team followers).\n */\nexport async function createMessageRecordForEmail(\n email: InboxEmailData,\n ctx: MessagesIntegrationContext,\n): Promise<string | null> {\n try {\n const commandBus = resolveCommandBus(ctx.container)\n if (!commandBus) return null\n\n const em = ctx.container.resolve('em') as EntityManager\n const knex = em.getKnex()\n const recipientUserIds = await getRecipientUserIdsForFeature(\n knex, ctx.scope.tenantId, 'inbox_ops.proposals.view',\n )\n\n const recipients = recipientUserIds.map((userId) => ({ userId, type: 'to' as const }))\n // Use rawText (full thread) instead of cleanedText (stripped quotes) so\n // the Messages module preserves the complete email conversation.\n const bodyText = email.rawText || email.cleanedText || ''\n\n // Resolve a real user as sender \u2014 prefer the forwarding user, fall back\n // to the first recipient (admin with proposals.view feature) so the\n // Messages UI can display an actual user name instead of the zero UUID.\n const senderUserId = await resolveMessageSenderUserId(\n em, email.forwardedByAddress, recipientUserIds, ctx.scope,\n )\n\n const { result } = await commandBus.execute('messages.messages.compose', {\n input: {\n type: 'inbox_ops.email',\n visibility: recipients.length > 0 ? 'internal' as const : 'public' as const,\n sourceEntityType: 'inbox_ops:inbox_email',\n sourceEntityId: email.id,\n externalEmail: email.forwardedByAddress,\n externalName: email.forwardedByName || undefined,\n recipients,\n subject: email.subject,\n body: bodyText.slice(0, 50000),\n bodyFormat: 'text' as const,\n priority: 'normal' as const,\n isDraft: false,\n sendViaEmail: false,\n objects: [\n {\n entityModule: 'inbox_ops',\n entityType: 'inbox_email',\n entityId: email.id,\n actionRequired: false,\n },\n ],\n tenantId: ctx.scope.tenantId,\n organizationId: ctx.scope.organizationId,\n userId: senderUserId,\n },\n ctx: {\n container: asContainer(ctx.container),\n auth: null,\n organizationScope: null,\n selectedOrganizationId: ctx.scope.organizationId,\n organizationIds: [ctx.scope.organizationId],\n },\n })\n\n const messageId = (result as { id: string })?.id ?? null\n return messageId\n } catch (err) {\n console.error(\n '[inbox_ops:messages] Failed to create message record for email:',\n err instanceof Error ? err.message : String(err),\n )\n return null\n }\n}\n\nexport async function createMessageRecordForReply(\n reply: DraftReplyData,\n inboxEmailId: string,\n ctx: MessagesIntegrationContext,\n): Promise<{ messageId: string } | null> {\n try {\n const commandBus = resolveCommandBus(ctx.container)\n if (!commandBus) return null\n\n const em = ctx.container.resolve('em') as EntityManager\n const knex = em.getKnex()\n const recipientUserIds = await getRecipientUserIdsForFeature(\n knex, ctx.scope.tenantId, 'inbox_ops.proposals.view',\n )\n if (recipientUserIds.length === 0) return null\n\n const recipients = recipientUserIds.map((userId) => ({ userId, type: 'to' as const }))\n\n const { result } = await commandBus.execute('messages.messages.compose', {\n input: {\n type: 'inbox_ops.reply',\n visibility: 'internal' as const,\n sourceEntityType: 'inbox_ops:inbox_email',\n sourceEntityId: inboxEmailId,\n externalEmail: reply.to,\n externalName: reply.toName ?? undefined,\n recipients,\n subject: reply.subject,\n body: reply.body,\n bodyFormat: 'text' as const,\n priority: 'normal' as const,\n isDraft: false,\n sendViaEmail: false,\n objects: [\n {\n entityModule: 'inbox_ops',\n entityType: 'inbox_email',\n entityId: inboxEmailId,\n actionRequired: false,\n },\n ],\n tenantId: ctx.scope.tenantId,\n organizationId: ctx.scope.organizationId,\n userId: ctx.scope.userId,\n },\n ctx: {\n container: asContainer(ctx.container),\n auth: null,\n organizationScope: null,\n selectedOrganizationId: ctx.scope.organizationId,\n organizationIds: [ctx.scope.organizationId],\n },\n })\n\n const messageId = (result as { id: string })?.id ?? null\n if (!messageId) return null\n return { messageId }\n } catch (err) {\n console.error(\n '[inbox_ops:messages] Failed to create reply message record:',\n err instanceof Error ? err.message : String(err),\n )\n return null\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,36 @@
1
+ import { InboxEmailPreview } from "./components/messages/InboxEmailPreview.js";
2
+ const messageObjectTypes = [
3
+ {
4
+ module: "inbox_ops",
5
+ entityType: "inbox_email",
6
+ messageTypes: ["inbox_ops.email", "inbox_ops.reply"],
7
+ labelKey: "inbox_ops.title",
8
+ icon: "mail-open",
9
+ PreviewComponent: InboxEmailPreview,
10
+ actions: [
11
+ {
12
+ id: "view",
13
+ labelKey: "inbox_ops.view_in_messages",
14
+ variant: "outline",
15
+ href: "/backend/inbox-ops"
16
+ }
17
+ ],
18
+ loadPreview: async (entityId, ctx) => {
19
+ try {
20
+ if (typeof window !== "undefined") {
21
+ return { title: "Inbox Email", subtitle: entityId };
22
+ }
23
+ const { loadInboxEmailPreview } = await import("./lib/messageObjectPreviews.js");
24
+ return loadInboxEmailPreview(entityId, ctx);
25
+ } catch {
26
+ return { title: "Inbox Email", subtitle: entityId };
27
+ }
28
+ }
29
+ }
30
+ ];
31
+ var message_objects_default = messageObjectTypes;
32
+ export {
33
+ message_objects_default as default,
34
+ messageObjectTypes
35
+ };
36
+ //# sourceMappingURL=message-objects.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/modules/inbox_ops/message-objects.ts"],
4
+ "sourcesContent": ["import type { MessageObjectTypeDefinition } from '@open-mercato/shared/modules/messages/types'\nimport { InboxEmailPreview } from './components/messages/InboxEmailPreview'\n\nexport const messageObjectTypes: MessageObjectTypeDefinition[] = [\n {\n module: 'inbox_ops',\n entityType: 'inbox_email',\n messageTypes: ['inbox_ops.email', 'inbox_ops.reply'],\n labelKey: 'inbox_ops.title',\n icon: 'mail-open',\n PreviewComponent: InboxEmailPreview,\n actions: [\n {\n id: 'view',\n labelKey: 'inbox_ops.view_in_messages',\n variant: 'outline',\n href: '/backend/inbox-ops',\n },\n ],\n loadPreview: async (entityId, ctx) => {\n try {\n if (typeof window !== 'undefined') {\n return { title: 'Inbox Email', subtitle: entityId }\n }\n const { loadInboxEmailPreview } = await import('./lib/messageObjectPreviews')\n return loadInboxEmailPreview(entityId, ctx)\n } catch {\n return { title: 'Inbox Email', subtitle: entityId }\n }\n },\n },\n]\n\nexport default messageObjectTypes\n"],
5
+ "mappings": "AACA,SAAS,yBAAyB;AAE3B,MAAM,qBAAoD;AAAA,EAC/D;AAAA,IACE,QAAQ;AAAA,IACR,YAAY;AAAA,IACZ,cAAc,CAAC,mBAAmB,iBAAiB;AAAA,IACnD,UAAU;AAAA,IACV,MAAM;AAAA,IACN,kBAAkB;AAAA,IAClB,SAAS;AAAA,MACP;AAAA,QACE,IAAI;AAAA,QACJ,UAAU;AAAA,QACV,SAAS;AAAA,QACT,MAAM;AAAA,MACR;AAAA,IACF;AAAA,IACA,aAAa,OAAO,UAAU,QAAQ;AACpC,UAAI;AACF,YAAI,OAAO,WAAW,aAAa;AACjC,iBAAO,EAAE,OAAO,eAAe,UAAU,SAAS;AAAA,QACpD;AACA,cAAM,EAAE,sBAAsB,IAAI,MAAM,OAAO,6BAA6B;AAC5E,eAAO,sBAAsB,UAAU,GAAG;AAAA,MAC5C,QAAQ;AACN,eAAO,EAAE,OAAO,eAAe,UAAU,SAAS;AAAA,MACpD;AAAA,IACF;AAAA,EACF;AACF;AAEA,IAAO,0BAAQ;",
6
+ "names": []
7
+ }
@@ -0,0 +1,38 @@
1
+ import { InboxEmailContent } from "./components/messages/InboxEmailContent.js";
2
+ const messageTypes = [
3
+ {
4
+ type: "inbox_ops.email",
5
+ module: "inbox_ops",
6
+ labelKey: "inbox_ops.title",
7
+ icon: "mail-open",
8
+ color: "blue",
9
+ ui: {
10
+ listItemComponent: "messages.default.listItem",
11
+ contentComponent: "inbox_ops.email.content",
12
+ actionsComponent: "messages.default.actions"
13
+ },
14
+ ContentComponent: InboxEmailContent,
15
+ allowReply: false,
16
+ allowForward: true
17
+ },
18
+ {
19
+ type: "inbox_ops.reply",
20
+ module: "inbox_ops",
21
+ labelKey: "inbox_ops.action_type.draft_reply",
22
+ icon: "reply",
23
+ color: "green",
24
+ ui: {
25
+ listItemComponent: "messages.default.listItem",
26
+ contentComponent: "messages.default.content",
27
+ actionsComponent: "messages.default.actions"
28
+ },
29
+ allowReply: true,
30
+ allowForward: true
31
+ }
32
+ ];
33
+ var message_types_default = messageTypes;
34
+ export {
35
+ message_types_default as default,
36
+ messageTypes
37
+ };
38
+ //# sourceMappingURL=message-types.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/modules/inbox_ops/message-types.ts"],
4
+ "sourcesContent": ["import type { MessageTypeDefinition } from '@open-mercato/shared/modules/messages/types'\nimport { InboxEmailContent } from './components/messages/InboxEmailContent'\n\nexport const messageTypes: MessageTypeDefinition[] = [\n {\n type: 'inbox_ops.email',\n module: 'inbox_ops',\n labelKey: 'inbox_ops.title',\n icon: 'mail-open',\n color: 'blue',\n ui: {\n listItemComponent: 'messages.default.listItem',\n contentComponent: 'inbox_ops.email.content',\n actionsComponent: 'messages.default.actions',\n },\n ContentComponent: InboxEmailContent,\n allowReply: false,\n allowForward: true,\n },\n {\n type: 'inbox_ops.reply',\n module: 'inbox_ops',\n labelKey: 'inbox_ops.action_type.draft_reply',\n icon: 'reply',\n color: 'green',\n ui: {\n listItemComponent: 'messages.default.listItem',\n contentComponent: 'messages.default.content',\n actionsComponent: 'messages.default.actions',\n },\n allowReply: true,\n allowForward: true,\n },\n]\n\nexport default messageTypes\n"],
5
+ "mappings": "AACA,SAAS,yBAAyB;AAE3B,MAAM,eAAwC;AAAA,EACnD;AAAA,IACE,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,MAAM;AAAA,IACN,OAAO;AAAA,IACP,IAAI;AAAA,MACF,mBAAmB;AAAA,MACnB,kBAAkB;AAAA,MAClB,kBAAkB;AAAA,IACpB;AAAA,IACA,kBAAkB;AAAA,IAClB,YAAY;AAAA,IACZ,cAAc;AAAA,EAChB;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,MAAM;AAAA,IACN,OAAO;AAAA,IACP,IAAI;AAAA,MACF,mBAAmB;AAAA,MACnB,kBAAkB;AAAA,MAClB,kBAAkB;AAAA,IACpB;AAAA,IACA,YAAY;AAAA,IACZ,cAAc;AAAA,EAChB;AACF;AAEA,IAAO,wBAAQ;",
6
+ "names": []
7
+ }
@@ -0,0 +1,13 @@
1
+ import { Migration } from "@mikro-orm/migrations";
2
+ class Migration20260303173020 extends Migration {
3
+ async up() {
4
+ this.addSql(`create index "inbox_discrepancies_organization_id_tenant_id_index" on "inbox_discrepancies" ("organization_id", "tenant_id");`);
5
+ }
6
+ async down() {
7
+ this.addSql(`drop index "inbox_discrepancies_organization_id_tenant_id_index";`);
8
+ }
9
+ }
10
+ export {
11
+ Migration20260303173020
12
+ };
13
+ //# sourceMappingURL=Migration20260303173020.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/inbox_ops/migrations/Migration20260303173020.ts"],
4
+ "sourcesContent": ["import { Migration } from '@mikro-orm/migrations';\n\nexport class Migration20260303173020 extends Migration {\n\n override async up(): Promise<void> {\n this.addSql(`create index \"inbox_discrepancies_organization_id_tenant_id_index\" on \"inbox_discrepancies\" (\"organization_id\", \"tenant_id\");`);\n }\n\n override async down(): Promise<void> {\n this.addSql(`drop index \"inbox_discrepancies_organization_id_tenant_id_index\";`);\n }\n\n}\n"],
5
+ "mappings": "AAAA,SAAS,iBAAiB;AAEnB,MAAM,gCAAgC,UAAU;AAAA,EAErD,MAAe,KAAoB;AACjC,SAAK,OAAO,+HAA+H;AAAA,EAC7I;AAAA,EAEA,MAAe,OAAsB;AACnC,SAAK,OAAO,mEAAmE;AAAA,EACjF;AAEF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,15 @@
1
+ import { Migration } from "@mikro-orm/migrations";
2
+ class Migration20260303173215 extends Migration {
3
+ async up() {
4
+ this.addSql(`alter table "inbox_proposals" add column "category" text null;`);
5
+ this.addSql(`create index "inbox_proposals_organization_id_tenant_id_category_index" on "inbox_proposals" ("organization_id", "tenant_id", "category");`);
6
+ }
7
+ async down() {
8
+ this.addSql(`drop index "inbox_proposals_organization_id_tenant_id_category_index";`);
9
+ this.addSql(`alter table "inbox_proposals" drop column "category";`);
10
+ }
11
+ }
12
+ export {
13
+ Migration20260303173215
14
+ };
15
+ //# sourceMappingURL=Migration20260303173215.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/inbox_ops/migrations/Migration20260303173215.ts"],
4
+ "sourcesContent": ["import { Migration } from '@mikro-orm/migrations';\n\nexport class Migration20260303173215 extends Migration {\n\n override async up(): Promise<void> {\n this.addSql(`alter table \"inbox_proposals\" add column \"category\" text null;`);\n this.addSql(`create index \"inbox_proposals_organization_id_tenant_id_category_index\" on \"inbox_proposals\" (\"organization_id\", \"tenant_id\", \"category\");`);\n }\n\n override async down(): Promise<void> {\n this.addSql(`drop index \"inbox_proposals_organization_id_tenant_id_category_index\";`);\n this.addSql(`alter table \"inbox_proposals\" drop column \"category\";`);\n }\n\n}\n"],
5
+ "mappings": "AAAA,SAAS,iBAAiB;AAEnB,MAAM,gCAAgC,UAAU;AAAA,EAErD,MAAe,KAAoB;AACjC,SAAK,OAAO,gEAAgE;AAC5E,SAAK,OAAO,4IAA4I;AAAA,EAC1J;AAAA,EAEA,MAAe,OAAsB;AACnC,SAAK,OAAO,wEAAwE;AACpF,SAAK,OAAO,uDAAuD;AAAA,EACrE;AAEF;",
6
+ "names": []
7
+ }
@@ -10,7 +10,7 @@ const searchConfig = {
10
10
  enabled: true,
11
11
  priority: 6,
12
12
  fieldPolicy: {
13
- searchable: ["summary"],
13
+ searchable: ["summary", "category"],
14
14
  excluded: ["metadata", "participants"]
15
15
  },
16
16
  buildSource: async (ctx) => {
@@ -22,17 +22,19 @@ const searchConfig = {
22
22
  fields: {
23
23
  status: record.status,
24
24
  confidence: record.confidence,
25
+ category: record.category,
25
26
  detected_language: record.detected_language
26
27
  },
27
28
  presenter: {
28
29
  title: String(record.summary || "Inbox Proposal").slice(0, 80),
29
- subtitle: `Confidence: ${record.confidence} - Status: ${record.status}`,
30
+ subtitle: `Confidence: ${record.confidence} - Status: ${record.status}${record.category ? ` - Category: ${record.category}` : ""}`,
30
31
  icon: "inbox"
31
32
  },
32
33
  checksumSource: {
33
34
  summary: record.summary,
34
35
  status: record.status,
35
36
  confidence: record.confidence,
37
+ category: record.category,
36
38
  detectedLanguage: record.detected_language
37
39
  }
38
40
  };
@@ -40,7 +42,7 @@ const searchConfig = {
40
42
  formatResult: async (ctx) => {
41
43
  return {
42
44
  title: String(ctx.record.summary || "Inbox Proposal").slice(0, 80),
43
- subtitle: `Confidence: ${ctx.record.confidence} - Status: ${ctx.record.status}`,
45
+ subtitle: `Confidence: ${ctx.record.confidence} - Status: ${ctx.record.status}${ctx.record.category ? ` - Category: ${ctx.record.category}` : ""}`,
44
46
  icon: "inbox"
45
47
  };
46
48
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/modules/inbox_ops/search.ts"],
4
- "sourcesContent": ["import type {\n SearchModuleConfig,\n SearchBuildContext,\n SearchResultPresenter,\n SearchIndexSource,\n} from '@open-mercato/shared/modules/search'\n\ntype SearchContext = SearchBuildContext & {\n tenantId: string\n}\n\nfunction assertTenantContext(ctx: SearchBuildContext): asserts ctx is SearchContext {\n if (typeof ctx.tenantId !== 'string' || ctx.tenantId.length === 0) {\n throw new Error('[search.inbox_ops] Missing tenantId in search build context')\n }\n}\n\nexport const searchConfig: SearchModuleConfig = {\n entities: [\n {\n entityId: 'inbox_ops:inbox_proposal',\n enabled: true,\n priority: 6,\n fieldPolicy: {\n searchable: ['summary'],\n excluded: ['metadata', 'participants'],\n },\n buildSource: async (ctx: SearchBuildContext): Promise<SearchIndexSource | null> => {\n assertTenantContext(ctx)\n const record = ctx.record\n if (!record.summary) return null\n\n return {\n text: String(record.summary || ''),\n fields: {\n status: record.status,\n confidence: record.confidence,\n detected_language: record.detected_language,\n },\n presenter: {\n title: String(record.summary || 'Inbox Proposal').slice(0, 80),\n subtitle: `Confidence: ${record.confidence} - Status: ${record.status}`,\n icon: 'inbox',\n },\n checksumSource: {\n summary: record.summary,\n status: record.status,\n confidence: record.confidence,\n detectedLanguage: record.detected_language,\n },\n }\n },\n formatResult: async (ctx: SearchBuildContext): Promise<SearchResultPresenter | null> => {\n return {\n title: String(ctx.record.summary || 'Inbox Proposal').slice(0, 80),\n subtitle: `Confidence: ${ctx.record.confidence} - Status: ${ctx.record.status}`,\n icon: 'inbox',\n }\n },\n resolveUrl: async (ctx: SearchBuildContext): Promise<string | null> => {\n const id = ctx.record.id\n if (!id) return null\n return `/backend/inbox-ops/proposals/${encodeURIComponent(String(id))}`\n },\n },\n ],\n}\n\nexport const config = searchConfig\nexport default searchConfig\n"],
5
- "mappings": "AAWA,SAAS,oBAAoB,KAAuD;AAClF,MAAI,OAAO,IAAI,aAAa,YAAY,IAAI,SAAS,WAAW,GAAG;AACjE,UAAM,IAAI,MAAM,6DAA6D;AAAA,EAC/E;AACF;AAEO,MAAM,eAAmC;AAAA,EAC9C,UAAU;AAAA,IACR;AAAA,MACE,UAAU;AAAA,MACV,SAAS;AAAA,MACT,UAAU;AAAA,MACV,aAAa;AAAA,QACX,YAAY,CAAC,SAAS;AAAA,QACtB,UAAU,CAAC,YAAY,cAAc;AAAA,MACvC;AAAA,MACA,aAAa,OAAO,QAA+D;AACjF,4BAAoB,GAAG;AACvB,cAAM,SAAS,IAAI;AACnB,YAAI,CAAC,OAAO,QAAS,QAAO;AAE5B,eAAO;AAAA,UACL,MAAM,OAAO,OAAO,WAAW,EAAE;AAAA,UACjC,QAAQ;AAAA,YACN,QAAQ,OAAO;AAAA,YACf,YAAY,OAAO;AAAA,YACnB,mBAAmB,OAAO;AAAA,UAC5B;AAAA,UACA,WAAW;AAAA,YACT,OAAO,OAAO,OAAO,WAAW,gBAAgB,EAAE,MAAM,GAAG,EAAE;AAAA,YAC7D,UAAU,eAAe,OAAO,UAAU,cAAc,OAAO,MAAM;AAAA,YACrE,MAAM;AAAA,UACR;AAAA,UACA,gBAAgB;AAAA,YACd,SAAS,OAAO;AAAA,YAChB,QAAQ,OAAO;AAAA,YACf,YAAY,OAAO;AAAA,YACnB,kBAAkB,OAAO;AAAA,UAC3B;AAAA,QACF;AAAA,MACF;AAAA,MACA,cAAc,OAAO,QAAmE;AACtF,eAAO;AAAA,UACL,OAAO,OAAO,IAAI,OAAO,WAAW,gBAAgB,EAAE,MAAM,GAAG,EAAE;AAAA,UACjE,UAAU,eAAe,IAAI,OAAO,UAAU,cAAc,IAAI,OAAO,MAAM;AAAA,UAC7E,MAAM;AAAA,QACR;AAAA,MACF;AAAA,MACA,YAAY,OAAO,QAAoD;AACrE,cAAM,KAAK,IAAI,OAAO;AACtB,YAAI,CAAC,GAAI,QAAO;AAChB,eAAO,gCAAgC,mBAAmB,OAAO,EAAE,CAAC,CAAC;AAAA,MACvE;AAAA,IACF;AAAA,EACF;AACF;AAEO,MAAM,SAAS;AACtB,IAAO,iBAAQ;",
4
+ "sourcesContent": ["import type {\n SearchModuleConfig,\n SearchBuildContext,\n SearchResultPresenter,\n SearchIndexSource,\n} from '@open-mercato/shared/modules/search'\n\ntype SearchContext = SearchBuildContext & {\n tenantId: string\n}\n\nfunction assertTenantContext(ctx: SearchBuildContext): asserts ctx is SearchContext {\n if (typeof ctx.tenantId !== 'string' || ctx.tenantId.length === 0) {\n throw new Error('[search.inbox_ops] Missing tenantId in search build context')\n }\n}\n\nexport const searchConfig: SearchModuleConfig = {\n entities: [\n {\n entityId: 'inbox_ops:inbox_proposal',\n enabled: true,\n priority: 6,\n fieldPolicy: {\n searchable: ['summary', 'category'],\n excluded: ['metadata', 'participants'],\n },\n buildSource: async (ctx: SearchBuildContext): Promise<SearchIndexSource | null> => {\n assertTenantContext(ctx)\n const record = ctx.record\n if (!record.summary) return null\n\n return {\n text: String(record.summary || ''),\n fields: {\n status: record.status,\n confidence: record.confidence,\n category: record.category,\n detected_language: record.detected_language,\n },\n presenter: {\n title: String(record.summary || 'Inbox Proposal').slice(0, 80),\n subtitle: `Confidence: ${record.confidence} - Status: ${record.status}${record.category ? ` - Category: ${record.category}` : ''}`,\n icon: 'inbox',\n },\n checksumSource: {\n summary: record.summary,\n status: record.status,\n confidence: record.confidence,\n category: record.category,\n detectedLanguage: record.detected_language,\n },\n }\n },\n formatResult: async (ctx: SearchBuildContext): Promise<SearchResultPresenter | null> => {\n return {\n title: String(ctx.record.summary || 'Inbox Proposal').slice(0, 80),\n subtitle: `Confidence: ${ctx.record.confidence} - Status: ${ctx.record.status}${ctx.record.category ? ` - Category: ${ctx.record.category}` : ''}`,\n icon: 'inbox',\n }\n },\n resolveUrl: async (ctx: SearchBuildContext): Promise<string | null> => {\n const id = ctx.record.id\n if (!id) return null\n return `/backend/inbox-ops/proposals/${encodeURIComponent(String(id))}`\n },\n },\n ],\n}\n\nexport const config = searchConfig\nexport default searchConfig\n"],
5
+ "mappings": "AAWA,SAAS,oBAAoB,KAAuD;AAClF,MAAI,OAAO,IAAI,aAAa,YAAY,IAAI,SAAS,WAAW,GAAG;AACjE,UAAM,IAAI,MAAM,6DAA6D;AAAA,EAC/E;AACF;AAEO,MAAM,eAAmC;AAAA,EAC9C,UAAU;AAAA,IACR;AAAA,MACE,UAAU;AAAA,MACV,SAAS;AAAA,MACT,UAAU;AAAA,MACV,aAAa;AAAA,QACX,YAAY,CAAC,WAAW,UAAU;AAAA,QAClC,UAAU,CAAC,YAAY,cAAc;AAAA,MACvC;AAAA,MACA,aAAa,OAAO,QAA+D;AACjF,4BAAoB,GAAG;AACvB,cAAM,SAAS,IAAI;AACnB,YAAI,CAAC,OAAO,QAAS,QAAO;AAE5B,eAAO;AAAA,UACL,MAAM,OAAO,OAAO,WAAW,EAAE;AAAA,UACjC,QAAQ;AAAA,YACN,QAAQ,OAAO;AAAA,YACf,YAAY,OAAO;AAAA,YACnB,UAAU,OAAO;AAAA,YACjB,mBAAmB,OAAO;AAAA,UAC5B;AAAA,UACA,WAAW;AAAA,YACT,OAAO,OAAO,OAAO,WAAW,gBAAgB,EAAE,MAAM,GAAG,EAAE;AAAA,YAC7D,UAAU,eAAe,OAAO,UAAU,cAAc,OAAO,MAAM,GAAG,OAAO,WAAW,gBAAgB,OAAO,QAAQ,KAAK,EAAE;AAAA,YAChI,MAAM;AAAA,UACR;AAAA,UACA,gBAAgB;AAAA,YACd,SAAS,OAAO;AAAA,YAChB,QAAQ,OAAO;AAAA,YACf,YAAY,OAAO;AAAA,YACnB,UAAU,OAAO;AAAA,YACjB,kBAAkB,OAAO;AAAA,UAC3B;AAAA,QACF;AAAA,MACF;AAAA,MACA,cAAc,OAAO,QAAmE;AACtF,eAAO;AAAA,UACL,OAAO,OAAO,IAAI,OAAO,WAAW,gBAAgB,EAAE,MAAM,GAAG,EAAE;AAAA,UACjE,UAAU,eAAe,IAAI,OAAO,UAAU,cAAc,IAAI,OAAO,MAAM,GAAG,IAAI,OAAO,WAAW,gBAAgB,IAAI,OAAO,QAAQ,KAAK,EAAE;AAAA,UAChJ,MAAM;AAAA,QACR;AAAA,MACF;AAAA,MACA,YAAY,OAAO,QAAoD;AACrE,cAAM,KAAK,IAAI,OAAO;AACtB,YAAI,CAAC,GAAI,QAAO;AAChB,eAAO,gCAAgC,mBAAmB,OAAO,EAAE,CAAC,CAAC;AAAA,MACvE;AAAA,IACF;AAAA,EACF;AACF;AAEO,MAAM,SAAS;AACtB,IAAO,iBAAQ;",
6
6
  "names": []
7
7
  }
@@ -11,7 +11,11 @@ import { extractParticipantsFromThread } from "../lib/emailParser.js";
11
11
  import { runExtractionWithConfiguredProvider } from "../lib/llmProvider.js";
12
12
  import { safeParsePayloadJson } from "../lib/validation.js";
13
13
  import { htmlToPlainText } from "../lib/htmlToPlainText.js";
14
+ import { runWithCacheTenant } from "@open-mercato/cache";
14
15
  import { emitInboxOpsEvent } from "../events.js";
16
+ import { createMessageRecordForEmail } from "../lib/messagesIntegration.js";
17
+ import { resolveCache, invalidateCountsCache } from "../lib/cache.js";
18
+ const SYSTEM_USER_ID = "00000000-0000-0000-0000-000000000000";
15
19
  const metadata = {
16
20
  event: "inbox_ops.email.received",
17
21
  persistent: true,
@@ -250,6 +254,7 @@ async function handle(payload, ctx) {
250
254
  id: proposalId,
251
255
  inboxEmailId: email.id,
252
256
  summary: extractionResult.summary,
257
+ category: extractionResult.category || null,
253
258
  participants: enrichedParticipants,
254
259
  confidence: String(extractionResult.confidence.toFixed(2)),
255
260
  detectedLanguage: extractionResult.detectedLanguage || email.detectedLanguage,
@@ -265,7 +270,8 @@ async function handle(payload, ctx) {
265
270
  const autoContactActions = buildContactActionsForUnmatchedParticipants(
266
271
  contactMatches,
267
272
  extractionResult.proposedActions,
268
- email.toAddress
273
+ email.toAddress,
274
+ email.forwardedByAddress
269
275
  );
270
276
  const llmContactActions = buildContactActionsForUnmatchedLlmParticipants(
271
277
  enrichedParticipants,
@@ -280,7 +286,13 @@ async function handle(payload, ctx) {
280
286
  extractionResult.proposedActions,
281
287
  email.toAddress
282
288
  );
283
- const combinedProposedActions = [...autoContactActions, ...autoLinkActions, ...autoProductActions, ...extractionResult.proposedActions];
289
+ const dedupedProposedActions = deduplicateCompanyActions([
290
+ ...autoContactActions,
291
+ ...autoLinkActions,
292
+ ...autoProductActions,
293
+ ...extractionResult.proposedActions
294
+ ]);
295
+ const combinedProposedActions = dedupedProposedActions;
284
296
  const allActions = [
285
297
  ...combinedProposedActions.map((action, index) => {
286
298
  const parsedPayload = safeParsePayloadJson(action.payloadJson);
@@ -395,6 +407,35 @@ async function handle(payload, ctx) {
395
407
  email.status = requiresReview ? "needs_review" : "processed";
396
408
  email.detectedLanguage = extractionResult.detectedLanguage || email.detectedLanguage;
397
409
  await em.flush();
410
+ try {
411
+ const cache = resolveCache(ctx);
412
+ await runWithCacheTenant(email.tenantId, () => invalidateCountsCache(cache, email.tenantId));
413
+ } catch (cacheErr) {
414
+ console.warn("[inbox_ops:extraction-worker] Cache invalidation failed (non-fatal):", cacheErr);
415
+ }
416
+ try {
417
+ await createMessageRecordForEmail(
418
+ {
419
+ id: email.id,
420
+ subject: email.subject,
421
+ cleanedText: email.cleanedText,
422
+ rawText: email.rawText,
423
+ forwardedByAddress: email.forwardedByAddress,
424
+ forwardedByName: email.forwardedByName,
425
+ status: email.status
426
+ },
427
+ {
428
+ container: ctx,
429
+ scope: {
430
+ tenantId: email.tenantId,
431
+ organizationId: email.organizationId,
432
+ userId: SYSTEM_USER_ID
433
+ }
434
+ }
435
+ );
436
+ } catch (msgErr) {
437
+ console.error("[inbox_ops:extraction-worker] Messages integration failed (non-fatal):", msgErr);
438
+ }
398
439
  try {
399
440
  await emitInboxOpsEvent("inbox_ops.email.processed", {
400
441
  emailId: email.id,
@@ -445,7 +486,7 @@ function normalizeOrderPayloadFields(payload) {
445
486
  }
446
487
  }
447
488
  }
448
- function buildContactActionsForUnmatchedParticipants(contactMatches, existingActions, inboxAddress) {
489
+ function buildContactActionsForUnmatchedParticipants(contactMatches, existingActions, inboxAddress, forwardedByAddress) {
449
490
  const alreadyProposed = new Set(
450
491
  existingActions.filter((a) => a.actionType === "create_contact").map((a) => {
451
492
  const p = safeParsePayloadJson(a.payloadJson);
@@ -453,12 +494,15 @@ function buildContactActionsForUnmatchedParticipants(contactMatches, existingAct
453
494
  }).filter(Boolean)
454
495
  );
455
496
  const inboxLower = (inboxAddress || "").toLowerCase();
497
+ const forwardedByLower = (forwardedByAddress || "").toLowerCase();
456
498
  const systemPatterns = ["noreply", "no-reply", "donotreply", "mailer-daemon", "postmaster"];
457
499
  return contactMatches.filter((m) => {
458
500
  if (m.match?.contactId) return false;
459
501
  const emailLower = m.participant.email.toLowerCase();
502
+ if (!emailLower || !emailLower.includes("@")) return false;
460
503
  if (alreadyProposed.has(emailLower)) return false;
461
504
  if (emailLower === inboxLower) return false;
505
+ if (forwardedByLower && emailLower === forwardedByLower) return false;
462
506
  return !systemPatterns.some((p) => emailLower.includes(p));
463
507
  }).map((m) => ({
464
508
  actionType: "create_contact",
@@ -631,6 +675,24 @@ function buildFullTextForExtraction(email) {
631
675
  }
632
676
  return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\t/g, " ").replace(/ {2,}/g, " ").replace(/\n{3,}/g, "\n\n").trim();
633
677
  }
678
+ function deduplicateCompanyActions(actions) {
679
+ const personCompanyNames = /* @__PURE__ */ new Set();
680
+ for (const action of actions) {
681
+ if (action.actionType !== "create_contact") continue;
682
+ const payload = safeParsePayloadJson(action.payloadJson);
683
+ if (payload.type === "person" && typeof payload.companyName === "string" && payload.companyName.trim()) {
684
+ personCompanyNames.add(payload.companyName.trim().toLowerCase());
685
+ }
686
+ }
687
+ if (personCompanyNames.size === 0) return actions;
688
+ return actions.filter((action) => {
689
+ if (action.actionType !== "create_contact") return true;
690
+ const payload = safeParsePayloadJson(action.payloadJson);
691
+ if (payload.type !== "company") return true;
692
+ const companyName = typeof payload.name === "string" ? payload.name.trim().toLowerCase() : "";
693
+ return !companyName || !personCompanyNames.has(companyName);
694
+ });
695
+ }
634
696
  function findPartialNameMatch(name, map) {
635
697
  const lower = name.toLowerCase();
636
698
  const parts = lower.split(/\s*[\/,]\s*/).map((p) => p.trim()).filter(Boolean);