@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
|
@@ -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": "AAGA,SAAS,qCAAqC;AAM9C,SAAS,YAAY,UAAyC;AAC5D,SAAO;AACT;AA8BA,MAAM,iBAAiB;AAQvB,eAAsB,2BACpB,IACA,kBACA,kBACA,OACiB;AACjB,MAAI;AAGF,UAAM,OAAO,GAAG,QAAQ;AACxB,UAAM,kBAAkB,iBAAiB,KAAK,EAAE,YAAY;AAC5D,QAAI,iBAAiB;AACnB,YAAM,MAAM,MAAM,KAAK,OAAO,EAC3B,OAAO,IAAI,EACX,MAAM,SAAS,eAAe,EAC9B,UAAU,YAAY,EACtB,MAAM;AACT,UAAI,KAAK,GAAI,QAAO,IAAI;AAAA,IAC1B;AAAA,EACF,QAAQ;AAAA,EAER;AACA,MAAI,iBAAiB,SAAS,EAAG,QAAO,iBAAiB,CAAC;AAC1D,SAAO;AACT;AAEA,SAAS,kBAAkB,WAA4C;AACrE,MAAI;AACF,UAAM,MAAM,UAAU,QAAQ,YAAY;AAC1C,WAAO,OAAO,OAAO,IAAI,YAAY,aAAa,MAAM;AAAA,EAC1D,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAWA,eAAsB,4BACpB,OACA,KACwB;AACxB,MAAI;AACF,UAAM,aAAa,kBAAkB,IAAI,SAAS;AAClD,QAAI,CAAC,WAAY,QAAO;AAExB,UAAM,KAAK,IAAI,UAAU,QAAQ,IAAI;AACrC,UAAM,OAAO,GAAG,QAAQ;AACxB,UAAM,mBAAmB,MAAM;AAAA,MAC7B;AAAA,MAAM,IAAI,MAAM;AAAA,MAAU;AAAA,IAC5B;AAEA,UAAM,aAAa,iBAAiB,IAAI,CAAC,YAAY,EAAE,QAAQ,MAAM,KAAc,EAAE;AAGrF,UAAM,WAAW,MAAM,WAAW,MAAM,eAAe;AAKvD,UAAM,eAAe,MAAM;AAAA,MACzB;AAAA,MAAI,MAAM;AAAA,MAAoB;AAAA,MAAkB,IAAI;AAAA,IACtD;AAEA,UAAM,EAAE,OAAO,IAAI,MAAM,WAAW,QAAQ,6BAA6B;AAAA,MACvE,OAAO;AAAA,QACL,MAAM;AAAA,QACN,YAAY,WAAW,SAAS,IAAI,aAAsB;AAAA,QAC1D,kBAAkB;AAAA,QAClB,gBAAgB,MAAM;AAAA,QACtB,eAAe,MAAM;AAAA,QACrB,cAAc,MAAM,mBAAmB;AAAA,QACvC;AAAA,QACA,SAAS,MAAM;AAAA,QACf,MAAM,SAAS,MAAM,GAAG,GAAK;AAAA,QAC7B,YAAY;AAAA,QACZ,UAAU;AAAA,QACV,SAAS;AAAA,QACT,cAAc;AAAA,QACd,SAAS;AAAA,UACP;AAAA,YACE,cAAc;AAAA,YACd,YAAY;AAAA,YACZ,UAAU,MAAM;AAAA,YAChB,gBAAgB;AAAA,UAClB;AAAA,QACF;AAAA,QACA,UAAU,IAAI,MAAM;AAAA,QACpB,gBAAgB,IAAI,MAAM;AAAA,QAC1B,QAAQ;AAAA,MACV;AAAA,MACA,KAAK;AAAA,QACH,WAAW,YAAY,IAAI,SAAS;AAAA,QACpC,MAAM;AAAA,QACN,mBAAmB;AAAA,QACnB,wBAAwB,IAAI,MAAM;AAAA,QAClC,iBAAiB,CAAC,IAAI,MAAM,cAAc;AAAA,MAC5C;AAAA,IACF,CAAC;AAED,UAAM,YAAa,QAA2B,MAAM;AACpD,WAAO;AAAA,EACT,SAAS,KAAK;AACZ,YAAQ;AAAA,MACN;AAAA,MACA,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAAA,IACjD;AACA,WAAO;AAAA,EACT;AACF;AAEA,eAAsB,4BACpB,OACA,cACA,KACuC;AACvC,MAAI;AACF,UAAM,aAAa,kBAAkB,IAAI,SAAS;AAClD,QAAI,CAAC,WAAY,QAAO;AAExB,UAAM,KAAK,IAAI,UAAU,QAAQ,IAAI;AACrC,UAAM,OAAO,GAAG,QAAQ;AACxB,UAAM,mBAAmB,MAAM;AAAA,MAC7B;AAAA,MAAM,IAAI,MAAM;AAAA,MAAU;AAAA,IAC5B;AACA,QAAI,iBAAiB,WAAW,EAAG,QAAO;AAE1C,UAAM,aAAa,iBAAiB,IAAI,CAAC,YAAY,EAAE,QAAQ,MAAM,KAAc,EAAE;AAErF,UAAM,EAAE,OAAO,IAAI,MAAM,WAAW,QAAQ,6BAA6B;AAAA,MACvE,OAAO;AAAA,QACL,MAAM;AAAA,QACN,YAAY;AAAA,QACZ,kBAAkB;AAAA,QAClB,gBAAgB;AAAA,QAChB,eAAe,MAAM;AAAA,QACrB,cAAc,MAAM,UAAU;AAAA,QAC9B;AAAA,QACA,SAAS,MAAM;AAAA,QACf,MAAM,MAAM;AAAA,QACZ,YAAY;AAAA,QACZ,UAAU;AAAA,QACV,SAAS;AAAA,QACT,cAAc;AAAA,QACd,SAAS;AAAA,UACP;AAAA,YACE,cAAc;AAAA,YACd,YAAY;AAAA,YACZ,UAAU;AAAA,YACV,gBAAgB;AAAA,UAClB;AAAA,QACF;AAAA,QACA,UAAU,IAAI,MAAM;AAAA,QACpB,gBAAgB,IAAI,MAAM;AAAA,QAC1B,QAAQ,IAAI,MAAM;AAAA,MACpB;AAAA,MACA,KAAK;AAAA,QACH,WAAW,YAAY,IAAI,SAAS;AAAA,QACpC,MAAM;AAAA,QACN,mBAAmB;AAAA,QACnB,wBAAwB,IAAI,MAAM;AAAA,QAClC,iBAAiB,CAAC,IAAI,MAAM,cAAc;AAAA,MAC5C;AAAA,IACF,CAAC;AAED,UAAM,YAAa,QAA2B,MAAM;AACpD,QAAI,CAAC,UAAW,QAAO;AACvB,WAAO,EAAE,UAAU;AAAA,EACrB,SAAS,KAAK;AACZ,YAAQ;AAAA,MACN;AAAA,MACA,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAAA,IACjD;AACA,WAAO;AAAA,EACT;AACF;",
|
|
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,
|
|
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
|
|
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);
|