@open-mercato/core 0.4.8-develop-641703d2a6 → 0.4.8-develop-bc5be31b5c

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,346 @@
1
+ import { z } from "zod";
2
+ import { runWithCacheTenant } from "@open-mercato/cache";
3
+ import { findOneWithDecryption, findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
4
+ import {
5
+ resolveOpenCodeModel,
6
+ resolveOpenCodeProviderApiKey
7
+ } from "@open-mercato/shared/lib/ai/opencode-provider";
8
+ import { InboxProposal, InboxProposalAction, InboxDiscrepancy } from "./data/entities.js";
9
+ import { inboxProposalCategoryEnum } from "./data/validators.js";
10
+ import { executeAction } from "./lib/executionEngine.js";
11
+ import { resolveExtractionProviderId, createStructuredModel, withTimeout } from "./lib/llmProvider.js";
12
+ import { resolveOptionalEventBus } from "./lib/eventBus.js";
13
+ function requireTenantContext(ctx) {
14
+ if (!ctx.tenantId || !ctx.organizationId) {
15
+ throw new Error("Tenant context is required");
16
+ }
17
+ return { tenantId: ctx.tenantId, organizationId: ctx.organizationId };
18
+ }
19
+ function resolveCrossModuleEntities(container) {
20
+ const entities = {};
21
+ const keys = [
22
+ "CustomerEntity",
23
+ "SalesOrder",
24
+ "SalesShipment",
25
+ "SalesChannel",
26
+ "Dictionary",
27
+ "DictionaryEntry"
28
+ ];
29
+ for (const key of keys) {
30
+ try {
31
+ entities[key] = container.resolve(key);
32
+ } catch {
33
+ }
34
+ }
35
+ return entities;
36
+ }
37
+ const listProposalsTool = {
38
+ name: "inbox_ops_list_proposals",
39
+ description: `List inbox proposals with optional filters by status, category, and date range.
40
+
41
+ Returns: total count and an array of proposals with id, summary, status, category, confidence, actionCount, and createdAt.`,
42
+ inputSchema: z.object({
43
+ status: z.enum(["pending", "partial", "accepted", "rejected"]).optional().describe("Filter by proposal status"),
44
+ category: inboxProposalCategoryEnum.optional().describe("Filter by email category"),
45
+ limit: z.number().int().min(1).max(50).optional().default(10).describe("Maximum number of proposals to return (default: 10)"),
46
+ dateFrom: z.string().optional().describe("Filter proposals created on or after this date (ISO 8601)"),
47
+ dateTo: z.string().optional().describe("Filter proposals created on or before this date (ISO 8601)")
48
+ }),
49
+ requiredFeatures: ["inbox_ops.proposals.view"],
50
+ handler: async (input, ctx) => {
51
+ const scope = requireTenantContext(ctx);
52
+ const em = ctx.container.resolve("em").fork();
53
+ const where = {
54
+ organizationId: scope.organizationId,
55
+ tenantId: scope.tenantId,
56
+ isActive: true,
57
+ deletedAt: null
58
+ };
59
+ if (input.status) {
60
+ where.status = input.status;
61
+ }
62
+ if (input.category) {
63
+ where.category = input.category;
64
+ }
65
+ if (input.dateFrom || input.dateTo) {
66
+ const createdAt = {};
67
+ if (input.dateFrom) {
68
+ createdAt.$gte = new Date(input.dateFrom);
69
+ }
70
+ if (input.dateTo) {
71
+ createdAt.$lte = new Date(input.dateTo);
72
+ }
73
+ where.createdAt = createdAt;
74
+ }
75
+ const proposals = await findWithDecryption(
76
+ em,
77
+ InboxProposal,
78
+ where,
79
+ { orderBy: { createdAt: "DESC" }, limit: input.limit },
80
+ scope
81
+ );
82
+ const proposalIds = proposals.map((p) => p.id);
83
+ const actionCountMap = /* @__PURE__ */ new Map();
84
+ if (proposalIds.length > 0) {
85
+ const actions = await findWithDecryption(
86
+ em,
87
+ InboxProposalAction,
88
+ {
89
+ proposalId: { $in: proposalIds },
90
+ deletedAt: null
91
+ },
92
+ void 0,
93
+ scope
94
+ );
95
+ for (const action of actions) {
96
+ actionCountMap.set(action.proposalId, (actionCountMap.get(action.proposalId) ?? 0) + 1);
97
+ }
98
+ }
99
+ const total = await em.count(InboxProposal, where);
100
+ return {
101
+ total,
102
+ proposals: proposals.map((p) => ({
103
+ id: p.id,
104
+ summary: p.summary,
105
+ status: p.status,
106
+ category: p.category ?? null,
107
+ confidence: Number(p.confidence),
108
+ actionCount: actionCountMap.get(p.id) ?? 0,
109
+ createdAt: p.createdAt.toISOString()
110
+ }))
111
+ };
112
+ }
113
+ };
114
+ const getProposalTool = {
115
+ name: "inbox_ops_get_proposal",
116
+ description: `Get full details of an inbox proposal including its actions and discrepancies.
117
+
118
+ Returns: proposal with id, summary, status, category, confidence, actions array, and discrepancies array.`,
119
+ inputSchema: z.object({
120
+ proposalId: z.string().uuid().describe("The UUID of the proposal to retrieve")
121
+ }),
122
+ requiredFeatures: ["inbox_ops.proposals.view"],
123
+ handler: async (input, ctx) => {
124
+ const scope = requireTenantContext(ctx);
125
+ const em = ctx.container.resolve("em").fork();
126
+ const proposal = await findOneWithDecryption(
127
+ em,
128
+ InboxProposal,
129
+ {
130
+ id: input.proposalId,
131
+ organizationId: scope.organizationId,
132
+ tenantId: scope.tenantId,
133
+ isActive: true,
134
+ deletedAt: null
135
+ },
136
+ void 0,
137
+ scope
138
+ );
139
+ if (!proposal) {
140
+ return { error: "Proposal not found" };
141
+ }
142
+ const actions = await findWithDecryption(
143
+ em,
144
+ InboxProposalAction,
145
+ {
146
+ proposalId: proposal.id,
147
+ organizationId: scope.organizationId,
148
+ tenantId: scope.tenantId,
149
+ deletedAt: null
150
+ },
151
+ { orderBy: { sortOrder: "ASC" } },
152
+ scope
153
+ );
154
+ const discrepancies = await findWithDecryption(
155
+ em,
156
+ InboxDiscrepancy,
157
+ {
158
+ proposalId: proposal.id,
159
+ organizationId: scope.organizationId,
160
+ tenantId: scope.tenantId
161
+ },
162
+ void 0,
163
+ scope
164
+ );
165
+ return {
166
+ proposal: {
167
+ id: proposal.id,
168
+ summary: proposal.summary,
169
+ status: proposal.status,
170
+ category: proposal.category ?? null,
171
+ confidence: Number(proposal.confidence),
172
+ actions: actions.map((a) => ({
173
+ id: a.id,
174
+ actionType: a.actionType,
175
+ description: a.description,
176
+ status: a.status,
177
+ confidence: Number(a.confidence),
178
+ requiredFeature: a.requiredFeature ?? null,
179
+ sortOrder: a.sortOrder,
180
+ createdEntityId: a.createdEntityId ?? null,
181
+ createdEntityType: a.createdEntityType ?? null
182
+ })),
183
+ discrepancies: discrepancies.map((d) => ({
184
+ id: d.id,
185
+ type: d.type,
186
+ severity: d.severity,
187
+ description: d.description,
188
+ expectedValue: d.expectedValue ?? null,
189
+ foundValue: d.foundValue ?? null,
190
+ resolved: d.resolved
191
+ }))
192
+ }
193
+ };
194
+ }
195
+ };
196
+ const acceptActionTool = {
197
+ name: "inbox_ops_accept_action",
198
+ description: `Accept and execute a specific action from an inbox proposal. Creates the entity in the target module (e.g., order, contact).
199
+
200
+ Returns on success: { ok: true, createdEntityId, createdEntityType }
201
+ Returns on error: error message with appropriate detail.`,
202
+ inputSchema: z.object({
203
+ proposalId: z.string().uuid().describe("The UUID of the proposal"),
204
+ actionId: z.string().uuid().describe("The UUID of the action to accept")
205
+ }),
206
+ requiredFeatures: ["inbox_ops.proposals.manage"],
207
+ handler: async (input, ctx) => {
208
+ const scope = requireTenantContext(ctx);
209
+ if (!ctx.userId) {
210
+ throw new Error("User context is required");
211
+ }
212
+ const em = ctx.container.resolve("em").fork();
213
+ const action = await findOneWithDecryption(
214
+ em,
215
+ InboxProposalAction,
216
+ {
217
+ id: input.actionId,
218
+ proposalId: input.proposalId,
219
+ organizationId: scope.organizationId,
220
+ tenantId: scope.tenantId,
221
+ deletedAt: null
222
+ },
223
+ void 0,
224
+ scope
225
+ );
226
+ if (!action) {
227
+ return { error: "Action not found" };
228
+ }
229
+ if (action.status !== "pending" && action.status !== "failed") {
230
+ return { error: "Action already processed", status: action.status };
231
+ }
232
+ if (action.requiredFeature) {
233
+ const hasFeature = ctx.isSuperAdmin || ctx.userFeatures.includes(action.requiredFeature);
234
+ if (!hasFeature) {
235
+ return {
236
+ error: "Insufficient permissions",
237
+ requiredFeature: action.requiredFeature
238
+ };
239
+ }
240
+ }
241
+ const entities = resolveCrossModuleEntities(ctx.container);
242
+ const eventBus = resolveOptionalEventBus(ctx.container);
243
+ const result = await executeAction(action, {
244
+ em,
245
+ userId: ctx.userId,
246
+ tenantId: scope.tenantId,
247
+ organizationId: scope.organizationId,
248
+ eventBus,
249
+ container: ctx.container,
250
+ entities
251
+ });
252
+ if (!result.success) {
253
+ if (result.statusCode === 409) {
254
+ return { error: "Action already processed", status: "accepted" };
255
+ }
256
+ if (result.statusCode === 403) {
257
+ return {
258
+ error: "Insufficient permissions",
259
+ requiredFeature: action.requiredFeature ?? "unknown"
260
+ };
261
+ }
262
+ return { error: "Execution failed", detail: result.error ?? "Unknown error" };
263
+ }
264
+ try {
265
+ const { resolveCache, invalidateCountsCache } = await import("./lib/cache.js");
266
+ const cache = resolveCache(ctx.container);
267
+ if (cache && scope.tenantId) {
268
+ await runWithCacheTenant(scope.tenantId, () => invalidateCountsCache(cache, scope.tenantId));
269
+ }
270
+ } catch {
271
+ }
272
+ return {
273
+ ok: true,
274
+ createdEntityId: result.createdEntityId ?? null,
275
+ createdEntityType: result.createdEntityType ?? null
276
+ };
277
+ }
278
+ };
279
+ const categorizeEmailSchema = z.object({
280
+ category: inboxProposalCategoryEnum,
281
+ confidence: z.number(),
282
+ reasoning: z.string()
283
+ });
284
+ const categorizeEmailTool = {
285
+ name: "inbox_ops_categorize_email",
286
+ description: `Categorize email or text content using AI. Classifies text into one of: rfq, order, order_update, complaint, shipping_update, inquiry, payment, other.
287
+
288
+ Returns: { category, confidence (0-1), reasoning }
289
+ Input text is limited to 10,000 characters for cost control.`,
290
+ inputSchema: z.object({
291
+ text: z.string().min(1).max(1e4).describe("Email or text content to categorize (max 10K chars)")
292
+ }),
293
+ requiredFeatures: ["inbox_ops.proposals.view"],
294
+ handler: async (input, ctx) => {
295
+ requireTenantContext(ctx);
296
+ const providerId = resolveExtractionProviderId();
297
+ const apiKey = resolveOpenCodeProviderApiKey(providerId);
298
+ if (!apiKey) {
299
+ throw new Error(`Missing API key for provider "${providerId}"`);
300
+ }
301
+ const modelConfig = resolveOpenCodeModel(providerId, {});
302
+ const model = await createStructuredModel(providerId, apiKey, modelConfig.modelId);
303
+ const { generateObject } = await import("ai");
304
+ const result = await withTimeout(
305
+ generateObject({
306
+ model,
307
+ schema: categorizeEmailSchema,
308
+ system: `You are an email classification agent. Classify the given text into exactly one category:
309
+ - rfq: Request for quotation or pricing inquiry
310
+ - order: New purchase order or order placement
311
+ - order_update: Change or update to an existing order
312
+ - complaint: Customer complaint, dispute, or dissatisfaction
313
+ - shipping_update: Shipment status, tracking, or delivery information
314
+ - inquiry: General question or information request
315
+ - payment: Payment-related (invoice, receipt, payment terms)
316
+ - other: Does not fit any category above
317
+
318
+ Return a JSON object with:
319
+ - category: one of the categories above
320
+ - confidence: a number between 0 and 1 indicating how confident you are
321
+ - reasoning: a brief explanation (1-2 sentences) of why this category was chosen`,
322
+ prompt: input.text,
323
+ temperature: 0
324
+ }),
325
+ 15e3,
326
+ "Email categorization timed out after 15s"
327
+ );
328
+ return {
329
+ category: result.object.category,
330
+ confidence: Math.round(result.object.confidence * 100) / 100,
331
+ reasoning: result.object.reasoning
332
+ };
333
+ }
334
+ };
335
+ const aiTools = [
336
+ listProposalsTool,
337
+ getProposalTool,
338
+ acceptActionTool,
339
+ categorizeEmailTool
340
+ ];
341
+ var ai_tools_default = aiTools;
342
+ export {
343
+ aiTools,
344
+ ai_tools_default as default
345
+ };
346
+ //# sourceMappingURL=ai-tools.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/modules/inbox_ops/ai-tools.ts"],
4
+ "sourcesContent": ["import { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AwilixContainer } from 'awilix'\nimport { runWithCacheTenant } from '@open-mercato/cache'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n resolveOpenCodeModel,\n resolveOpenCodeProviderApiKey,\n} from '@open-mercato/shared/lib/ai/opencode-provider'\nimport { InboxProposal, InboxProposalAction, InboxDiscrepancy } from './data/entities'\nimport { inboxProposalCategoryEnum } from './data/validators'\nimport { executeAction } from './lib/executionEngine'\nimport { resolveExtractionProviderId, createStructuredModel, withTimeout } from './lib/llmProvider'\nimport { resolveOptionalEventBus } from './lib/eventBus'\n\ntype ToolContext = {\n tenantId: string | null\n organizationId: string | null\n userId: string | null\n container: AwilixContainer\n userFeatures: string[]\n isSuperAdmin: boolean\n}\n\ninterface AiToolDefinition {\n name: string\n description: string\n inputSchema: z.ZodType\n requiredFeatures?: string[]\n handler: (input: never, ctx: ToolContext) => Promise<unknown>\n}\n\n// =============================================================================\n// Helpers\n// =============================================================================\n\nfunction requireTenantContext(ctx: ToolContext): { tenantId: string; organizationId: string } {\n if (!ctx.tenantId || !ctx.organizationId) {\n throw new Error('Tenant context is required')\n }\n return { tenantId: ctx.tenantId, organizationId: ctx.organizationId }\n}\n\nfunction resolveCrossModuleEntities(container: ToolContext['container']) {\n const entities: Record<string, unknown> = {}\n const keys = [\n 'CustomerEntity',\n 'SalesOrder',\n 'SalesShipment',\n 'SalesChannel',\n 'Dictionary',\n 'DictionaryEntry',\n ]\n for (const key of keys) {\n try {\n entities[key] = container.resolve(key)\n } catch {\n /* module not available */\n }\n }\n return entities\n}\n\n// =============================================================================\n// inbox_ops_list_proposals \u2014 Query proposals by status, category, date range\n// =============================================================================\n\nconst listProposalsTool = {\n name: 'inbox_ops_list_proposals',\n description: `List inbox proposals with optional filters by status, category, and date range.\n\nReturns: total count and an array of proposals with id, summary, status, category, confidence, actionCount, and createdAt.`,\n inputSchema: z.object({\n status: z\n .enum(['pending', 'partial', 'accepted', 'rejected'])\n .optional()\n .describe('Filter by proposal status'),\n category: inboxProposalCategoryEnum\n .optional()\n .describe('Filter by email category'),\n limit: z\n .number()\n .int()\n .min(1)\n .max(50)\n .optional()\n .default(10)\n .describe('Maximum number of proposals to return (default: 10)'),\n dateFrom: z\n .string()\n .optional()\n .describe('Filter proposals created on or after this date (ISO 8601)'),\n dateTo: z\n .string()\n .optional()\n .describe('Filter proposals created on or before this date (ISO 8601)'),\n }),\n requiredFeatures: ['inbox_ops.proposals.view'],\n handler: async (input: { status?: string; category?: string; limit?: number; dateFrom?: string; dateTo?: string }, ctx: ToolContext) => {\n const scope = requireTenantContext(ctx)\n const em = ctx.container.resolve<EntityManager>('em').fork()\n\n const where: Record<string, unknown> = {\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n isActive: true,\n deletedAt: null,\n }\n\n if (input.status) {\n where.status = input.status\n }\n if (input.category) {\n where.category = input.category\n }\n if (input.dateFrom || input.dateTo) {\n const createdAt: Record<string, unknown> = {}\n if (input.dateFrom) {\n createdAt.$gte = new Date(input.dateFrom)\n }\n if (input.dateTo) {\n createdAt.$lte = new Date(input.dateTo)\n }\n where.createdAt = createdAt\n }\n\n const proposals = await findWithDecryption(\n em,\n InboxProposal,\n where,\n { orderBy: { createdAt: 'DESC' }, limit: input.limit },\n scope,\n )\n\n // Count actions per proposal in a single query\n const proposalIds = proposals.map((p) => p.id)\n const actionCountMap = new Map<string, number>()\n\n if (proposalIds.length > 0) {\n const actions = await findWithDecryption(\n em,\n InboxProposalAction,\n {\n proposalId: { $in: proposalIds },\n deletedAt: null,\n },\n undefined,\n scope,\n )\n for (const action of actions) {\n actionCountMap.set(action.proposalId, (actionCountMap.get(action.proposalId) ?? 0) + 1)\n }\n }\n\n // Get total count for the filter\n const total = await em.count(InboxProposal, where)\n\n return {\n total,\n proposals: proposals.map((p) => ({\n id: p.id,\n summary: p.summary,\n status: p.status,\n category: p.category ?? null,\n confidence: Number(p.confidence),\n actionCount: actionCountMap.get(p.id) ?? 0,\n createdAt: p.createdAt.toISOString(),\n })),\n }\n },\n}\n\n// =============================================================================\n// inbox_ops_get_proposal \u2014 Fetch proposal detail with actions and discrepancies\n// =============================================================================\n\nconst getProposalTool = {\n name: 'inbox_ops_get_proposal',\n description: `Get full details of an inbox proposal including its actions and discrepancies.\n\nReturns: proposal with id, summary, status, category, confidence, actions array, and discrepancies array.`,\n inputSchema: z.object({\n proposalId: z.string().uuid().describe('The UUID of the proposal to retrieve'),\n }),\n requiredFeatures: ['inbox_ops.proposals.view'],\n handler: async (input: { proposalId: string }, ctx: ToolContext) => {\n const scope = requireTenantContext(ctx)\n const em = ctx.container.resolve<EntityManager>('em').fork()\n\n const proposal = await findOneWithDecryption(\n em,\n InboxProposal,\n {\n id: input.proposalId,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n isActive: true,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n\n if (!proposal) {\n return { error: 'Proposal not found' }\n }\n\n const actions = await findWithDecryption(\n em,\n InboxProposalAction,\n {\n proposalId: proposal.id,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n { orderBy: { sortOrder: 'ASC' } },\n scope,\n )\n\n const discrepancies = await findWithDecryption(\n em,\n InboxDiscrepancy,\n {\n proposalId: proposal.id,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n },\n undefined,\n scope,\n )\n\n return {\n proposal: {\n id: proposal.id,\n summary: proposal.summary,\n status: proposal.status,\n category: proposal.category ?? null,\n confidence: Number(proposal.confidence),\n actions: actions.map((a) => ({\n id: a.id,\n actionType: a.actionType,\n description: a.description,\n status: a.status,\n confidence: Number(a.confidence),\n requiredFeature: a.requiredFeature ?? null,\n sortOrder: a.sortOrder,\n createdEntityId: a.createdEntityId ?? null,\n createdEntityType: a.createdEntityType ?? null,\n })),\n discrepancies: discrepancies.map((d) => ({\n id: d.id,\n type: d.type,\n severity: d.severity,\n description: d.description,\n expectedValue: d.expectedValue ?? null,\n foundValue: d.foundValue ?? null,\n resolved: d.resolved,\n })),\n },\n }\n },\n}\n\n// =============================================================================\n// inbox_ops_accept_action \u2014 Accept and execute a specific action\n// =============================================================================\n\nconst acceptActionTool = {\n name: 'inbox_ops_accept_action',\n description: `Accept and execute a specific action from an inbox proposal. Creates the entity in the target module (e.g., order, contact).\n\nReturns on success: { ok: true, createdEntityId, createdEntityType }\nReturns on error: error message with appropriate detail.`,\n inputSchema: z.object({\n proposalId: z.string().uuid().describe('The UUID of the proposal'),\n actionId: z.string().uuid().describe('The UUID of the action to accept'),\n }),\n requiredFeatures: ['inbox_ops.proposals.manage'],\n handler: async (input: { proposalId: string; actionId: string }, ctx: ToolContext) => {\n const scope = requireTenantContext(ctx)\n if (!ctx.userId) {\n throw new Error('User context is required')\n }\n\n const em = ctx.container.resolve<EntityManager>('em').fork()\n\n const action = await findOneWithDecryption(\n em,\n InboxProposalAction,\n {\n id: input.actionId,\n proposalId: input.proposalId,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n\n if (!action) {\n return { error: 'Action not found' }\n }\n\n // Check if action was already processed\n if (action.status !== 'pending' && action.status !== 'failed') {\n return { error: 'Action already processed', status: action.status }\n }\n\n // Check target module permission\n if (action.requiredFeature) {\n const hasFeature =\n ctx.isSuperAdmin || ctx.userFeatures.includes(action.requiredFeature)\n if (!hasFeature) {\n return {\n error: 'Insufficient permissions',\n requiredFeature: action.requiredFeature,\n }\n }\n }\n\n const entities = resolveCrossModuleEntities(ctx.container)\n const eventBus = resolveOptionalEventBus(ctx.container)\n\n const result = await executeAction(action, {\n em,\n userId: ctx.userId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n eventBus,\n container: ctx.container,\n entities: entities as unknown as import('./lib/executionEngine').CrossModuleEntities,\n })\n\n if (!result.success) {\n if (result.statusCode === 409) {\n return { error: 'Action already processed', status: 'accepted' }\n }\n if (result.statusCode === 403) {\n return {\n error: 'Insufficient permissions',\n requiredFeature: action.requiredFeature ?? 'unknown',\n }\n }\n return { error: 'Execution failed', detail: result.error ?? 'Unknown error' }\n }\n\n try {\n const { resolveCache, invalidateCountsCache } = await import('./lib/cache')\n const cache = resolveCache(ctx.container)\n if (cache && scope.tenantId) {\n await runWithCacheTenant(scope.tenantId, () => invalidateCountsCache(cache, scope.tenantId))\n }\n } catch { /* cache invalidation is non-critical */ }\n\n return {\n ok: true,\n createdEntityId: result.createdEntityId ?? null,\n createdEntityType: result.createdEntityType ?? null,\n }\n },\n}\n\n// =============================================================================\n// inbox_ops_categorize_email \u2014 Standalone LLM-based text categorization\n// =============================================================================\n\nconst categorizeEmailSchema = z.object({\n category: inboxProposalCategoryEnum,\n confidence: z.number(),\n reasoning: z.string(),\n})\n\nconst categorizeEmailTool = {\n name: 'inbox_ops_categorize_email',\n description: `Categorize email or text content using AI. Classifies text into one of: rfq, order, order_update, complaint, shipping_update, inquiry, payment, other.\n\nReturns: { category, confidence (0-1), reasoning }\nInput text is limited to 10,000 characters for cost control.`,\n inputSchema: z.object({\n text: z\n .string()\n .min(1)\n .max(10000)\n .describe('Email or text content to categorize (max 10K chars)'),\n }),\n requiredFeatures: ['inbox_ops.proposals.view'],\n handler: async (input: { text: string }, ctx: ToolContext) => {\n requireTenantContext(ctx)\n\n const providerId = resolveExtractionProviderId()\n const apiKey = resolveOpenCodeProviderApiKey(providerId)\n if (!apiKey) {\n throw new Error(`Missing API key for provider \"${providerId}\"`)\n }\n\n const modelConfig = resolveOpenCodeModel(providerId, {})\n const model = await createStructuredModel(providerId, apiKey, modelConfig.modelId)\n\n const { generateObject } = await import('ai')\n\n const result = await withTimeout(\n generateObject({\n model,\n schema: categorizeEmailSchema,\n system: `You are an email classification agent. Classify the given text into exactly one category:\n- rfq: Request for quotation or pricing inquiry\n- order: New purchase order or order placement\n- order_update: Change or update to an existing order\n- complaint: Customer complaint, dispute, or dissatisfaction\n- shipping_update: Shipment status, tracking, or delivery information\n- inquiry: General question or information request\n- payment: Payment-related (invoice, receipt, payment terms)\n- other: Does not fit any category above\n\nReturn a JSON object with:\n- category: one of the categories above\n- confidence: a number between 0 and 1 indicating how confident you are\n- reasoning: a brief explanation (1-2 sentences) of why this category was chosen`,\n prompt: input.text,\n temperature: 0,\n }),\n 15000,\n 'Email categorization timed out after 15s',\n )\n\n return {\n category: result.object.category,\n confidence: Math.round(result.object.confidence * 100) / 100,\n reasoning: result.object.reasoning,\n }\n },\n}\n\n// =============================================================================\n// Export\n// =============================================================================\n\n/**\n * All AI tools exported by the inbox_ops module.\n * Discovered by ai-assistant module's generator.\n */\nexport const aiTools: AiToolDefinition[] = [\n listProposalsTool,\n getProposalTool,\n acceptActionTool,\n categorizeEmailTool,\n]\n\nexport default aiTools\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -28,8 +28,9 @@ async function POST(req) {
28
28
  const { text, title, metadata: inputMetadata } = parsed.data;
29
29
  const maxTextSize = parseInt(process.env.INBOX_OPS_MAX_TEXT_SIZE || "204800", 10);
30
30
  const truncatedText = text.slice(0, maxTextSize);
31
+ const submitterEmail = ctx.auth?.email || ctx.userId;
31
32
  const email = ctx.em.create(InboxEmail, {
32
- forwardedByAddress: ctx.userId,
33
+ forwardedByAddress: submitterEmail,
33
34
  forwardedByName: null,
34
35
  toAddress: "text-extract",
35
36
  subject: title || "Text extraction",
@@ -53,7 +54,7 @@ async function POST(req) {
53
54
  emailId: email.id,
54
55
  tenantId: ctx.tenantId,
55
56
  organizationId: ctx.organizationId,
56
- forwardedByAddress: ctx.userId,
57
+ forwardedByAddress: submitterEmail,
57
58
  subject: title || "Text extraction"
58
59
  });
59
60
  } catch (eventError) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/inbox_ops/api/extract/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { InboxEmail } from '../../data/entities'\nimport { emitInboxOpsEvent } from '../../events'\nimport { resolveRequestContext, handleRouteError } from '../routeHelpers'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['inbox_ops.proposals.manage'] },\n}\n\nconst extractRequestSchema = z.object({\n text: z.string().min(1, 'Text is required').max(100_000, 'Text exceeds maximum length'),\n title: z.string().max(500).optional(),\n metadata: z.record(z.string(), z.unknown()).optional(),\n})\n\nexport async function POST(req: Request) {\n try {\n const ctx = await resolveRequestContext(req)\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })\n }\n\n const parsed = extractRequestSchema.safeParse(body)\n if (!parsed.success) {\n const errors = parsed.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join('; ')\n return NextResponse.json({ error: errors }, { status: 400 })\n }\n\n const { text, title, metadata: inputMetadata } = parsed.data\n\n const maxTextSize = parseInt(process.env.INBOX_OPS_MAX_TEXT_SIZE || '204800', 10)\n const truncatedText = text.slice(0, maxTextSize)\n\n const email = ctx.em.create(InboxEmail, {\n forwardedByAddress: ctx.userId,\n forwardedByName: null,\n toAddress: 'text-extract',\n subject: title || 'Text extraction',\n cleanedText: truncatedText,\n rawText: truncatedText,\n receivedAt: new Date(),\n status: 'received' as const,\n isActive: true,\n organizationId: ctx.organizationId,\n tenantId: ctx.tenantId,\n metadata: {\n ...inputMetadata,\n source: 'text_extract',\n submittedByUserId: ctx.userId,\n },\n })\n\n ctx.em.persist(email)\n await ctx.em.flush()\n\n try {\n await emitInboxOpsEvent('inbox_ops.email.received', {\n emailId: email.id,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n forwardedByAddress: ctx.userId,\n subject: title || 'Text extraction',\n })\n } catch (eventError) {\n console.error('[inbox_ops:extract] Failed to emit email.received event:', eventError)\n }\n\n return NextResponse.json({ ok: true, emailId: email.id })\n } catch (err) {\n return handleRouteError(err, 'extract text')\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'InboxOps',\n summary: 'Extract actions from raw text',\n methods: {\n POST: {\n summary: 'Submit raw text for LLM extraction',\n description: 'Creates an InboxEmail record from raw text and triggers the extraction pipeline. The extraction runs asynchronously.',\n responses: [\n { status: 200, description: 'Extraction queued successfully' },\n { status: 400, description: 'Invalid request body' },\n { status: 401, description: 'Unauthorized' },\n ],\n },\n },\n}\n"],
5
- "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,kBAAkB;AAC3B,SAAS,yBAAyB;AAClC,SAAS,uBAAuB,wBAAwB;AAEjD,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,4BAA4B,EAAE;AAC7E;AAEA,MAAM,uBAAuB,EAAE,OAAO;AAAA,EACpC,MAAM,EAAE,OAAO,EAAE,IAAI,GAAG,kBAAkB,EAAE,IAAI,KAAS,6BAA6B;AAAA,EACtF,OAAO,EAAE,OAAO,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,EACpC,UAAU,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,QAAQ,CAAC,EAAE,SAAS;AACvD,CAAC;AAED,eAAsB,KAAK,KAAc;AACvC,MAAI;AACF,UAAM,MAAM,MAAM,sBAAsB,GAAG;AAE3C,QAAI;AACJ,QAAI;AACF,aAAO,MAAM,IAAI,KAAK;AAAA,IACxB,QAAQ;AACN,aAAO,aAAa,KAAK,EAAE,OAAO,oBAAoB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC1E;AAEA,UAAM,SAAS,qBAAqB,UAAU,IAAI;AAClD,QAAI,CAAC,OAAO,SAAS;AACnB,YAAM,SAAS,OAAO,MAAM,OAAO,IAAI,CAAC,MAAM,GAAG,EAAE,KAAK,KAAK,GAAG,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,KAAK,IAAI;AAC5F,aAAO,aAAa,KAAK,EAAE,OAAO,OAAO,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC7D;AAEA,UAAM,EAAE,MAAM,OAAO,UAAU,cAAc,IAAI,OAAO;AAExD,UAAM,cAAc,SAAS,QAAQ,IAAI,2BAA2B,UAAU,EAAE;AAChF,UAAM,gBAAgB,KAAK,MAAM,GAAG,WAAW;AAE/C,UAAM,QAAQ,IAAI,GAAG,OAAO,YAAY;AAAA,MACtC,oBAAoB,IAAI;AAAA,MACxB,iBAAiB;AAAA,MACjB,WAAW;AAAA,MACX,SAAS,SAAS;AAAA,MAClB,aAAa;AAAA,MACb,SAAS;AAAA,MACT,YAAY,oBAAI,KAAK;AAAA,MACrB,QAAQ;AAAA,MACR,UAAU;AAAA,MACV,gBAAgB,IAAI;AAAA,MACpB,UAAU,IAAI;AAAA,MACd,UAAU;AAAA,QACR,GAAG;AAAA,QACH,QAAQ;AAAA,QACR,mBAAmB,IAAI;AAAA,MACzB;AAAA,IACF,CAAC;AAED,QAAI,GAAG,QAAQ,KAAK;AACpB,UAAM,IAAI,GAAG,MAAM;AAEnB,QAAI;AACF,YAAM,kBAAkB,4BAA4B;AAAA,QAClD,SAAS,MAAM;AAAA,QACf,UAAU,IAAI;AAAA,QACd,gBAAgB,IAAI;AAAA,QACpB,oBAAoB,IAAI;AAAA,QACxB,SAAS,SAAS;AAAA,MACpB,CAAC;AAAA,IACH,SAAS,YAAY;AACnB,cAAQ,MAAM,4DAA4D,UAAU;AAAA,IACtF;AAEA,WAAO,aAAa,KAAK,EAAE,IAAI,MAAM,SAAS,MAAM,GAAG,CAAC;AAAA,EAC1D,SAAS,KAAK;AACZ,WAAO,iBAAiB,KAAK,cAAc;AAAA,EAC7C;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,aAAa;AAAA,MACb,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,iCAAiC;AAAA,QAC7D,EAAE,QAAQ,KAAK,aAAa,uBAAuB;AAAA,QACnD,EAAE,QAAQ,KAAK,aAAa,eAAe;AAAA,MAC7C;AAAA,IACF;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { InboxEmail } from '../../data/entities'\nimport { emitInboxOpsEvent } from '../../events'\nimport { resolveRequestContext, handleRouteError } from '../routeHelpers'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['inbox_ops.proposals.manage'] },\n}\n\nconst extractRequestSchema = z.object({\n text: z.string().min(1, 'Text is required').max(100_000, 'Text exceeds maximum length'),\n title: z.string().max(500).optional(),\n metadata: z.record(z.string(), z.unknown()).optional(),\n})\n\nexport async function POST(req: Request) {\n try {\n const ctx = await resolveRequestContext(req)\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })\n }\n\n const parsed = extractRequestSchema.safeParse(body)\n if (!parsed.success) {\n const errors = parsed.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join('; ')\n return NextResponse.json({ error: errors }, { status: 400 })\n }\n\n const { text, title, metadata: inputMetadata } = parsed.data\n\n const maxTextSize = parseInt(process.env.INBOX_OPS_MAX_TEXT_SIZE || '204800', 10)\n const truncatedText = text.slice(0, maxTextSize)\n\n const submitterEmail = ctx.auth?.email || ctx.userId\n const email = ctx.em.create(InboxEmail, {\n forwardedByAddress: submitterEmail,\n forwardedByName: null,\n toAddress: 'text-extract',\n subject: title || 'Text extraction',\n cleanedText: truncatedText,\n rawText: truncatedText,\n receivedAt: new Date(),\n status: 'received' as const,\n isActive: true,\n organizationId: ctx.organizationId,\n tenantId: ctx.tenantId,\n metadata: {\n ...inputMetadata,\n source: 'text_extract',\n submittedByUserId: ctx.userId,\n },\n })\n\n ctx.em.persist(email)\n await ctx.em.flush()\n\n try {\n await emitInboxOpsEvent('inbox_ops.email.received', {\n emailId: email.id,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n forwardedByAddress: submitterEmail,\n subject: title || 'Text extraction',\n })\n } catch (eventError) {\n console.error('[inbox_ops:extract] Failed to emit email.received event:', eventError)\n }\n\n return NextResponse.json({ ok: true, emailId: email.id })\n } catch (err) {\n return handleRouteError(err, 'extract text')\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'InboxOps',\n summary: 'Extract actions from raw text',\n methods: {\n POST: {\n summary: 'Submit raw text for LLM extraction',\n description: 'Creates an InboxEmail record from raw text and triggers the extraction pipeline. The extraction runs asynchronously.',\n responses: [\n { status: 200, description: 'Extraction queued successfully' },\n { status: 400, description: 'Invalid request body' },\n { status: 401, description: 'Unauthorized' },\n ],\n },\n },\n}\n"],
5
+ "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,kBAAkB;AAC3B,SAAS,yBAAyB;AAClC,SAAS,uBAAuB,wBAAwB;AAEjD,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,4BAA4B,EAAE;AAC7E;AAEA,MAAM,uBAAuB,EAAE,OAAO;AAAA,EACpC,MAAM,EAAE,OAAO,EAAE,IAAI,GAAG,kBAAkB,EAAE,IAAI,KAAS,6BAA6B;AAAA,EACtF,OAAO,EAAE,OAAO,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,EACpC,UAAU,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,QAAQ,CAAC,EAAE,SAAS;AACvD,CAAC;AAED,eAAsB,KAAK,KAAc;AACvC,MAAI;AACF,UAAM,MAAM,MAAM,sBAAsB,GAAG;AAE3C,QAAI;AACJ,QAAI;AACF,aAAO,MAAM,IAAI,KAAK;AAAA,IACxB,QAAQ;AACN,aAAO,aAAa,KAAK,EAAE,OAAO,oBAAoB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC1E;AAEA,UAAM,SAAS,qBAAqB,UAAU,IAAI;AAClD,QAAI,CAAC,OAAO,SAAS;AACnB,YAAM,SAAS,OAAO,MAAM,OAAO,IAAI,CAAC,MAAM,GAAG,EAAE,KAAK,KAAK,GAAG,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,KAAK,IAAI;AAC5F,aAAO,aAAa,KAAK,EAAE,OAAO,OAAO,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC7D;AAEA,UAAM,EAAE,MAAM,OAAO,UAAU,cAAc,IAAI,OAAO;AAExD,UAAM,cAAc,SAAS,QAAQ,IAAI,2BAA2B,UAAU,EAAE;AAChF,UAAM,gBAAgB,KAAK,MAAM,GAAG,WAAW;AAE/C,UAAM,iBAAiB,IAAI,MAAM,SAAS,IAAI;AAC9C,UAAM,QAAQ,IAAI,GAAG,OAAO,YAAY;AAAA,MACtC,oBAAoB;AAAA,MACpB,iBAAiB;AAAA,MACjB,WAAW;AAAA,MACX,SAAS,SAAS;AAAA,MAClB,aAAa;AAAA,MACb,SAAS;AAAA,MACT,YAAY,oBAAI,KAAK;AAAA,MACrB,QAAQ;AAAA,MACR,UAAU;AAAA,MACV,gBAAgB,IAAI;AAAA,MACpB,UAAU,IAAI;AAAA,MACd,UAAU;AAAA,QACR,GAAG;AAAA,QACH,QAAQ;AAAA,QACR,mBAAmB,IAAI;AAAA,MACzB;AAAA,IACF,CAAC;AAED,QAAI,GAAG,QAAQ,KAAK;AACpB,UAAM,IAAI,GAAG,MAAM;AAEnB,QAAI;AACF,YAAM,kBAAkB,4BAA4B;AAAA,QAClD,SAAS,MAAM;AAAA,QACf,UAAU,IAAI;AAAA,QACd,gBAAgB,IAAI;AAAA,QACpB,oBAAoB;AAAA,QACpB,SAAS,SAAS;AAAA,MACpB,CAAC;AAAA,IACH,SAAS,YAAY;AACnB,cAAQ,MAAM,4DAA4D,UAAU;AAAA,IACtF;AAEA,WAAO,aAAa,KAAK,EAAE,IAAI,MAAM,SAAS,MAAM,GAAG,CAAC;AAAA,EAC1D,SAAS,KAAK;AACZ,WAAO,iBAAiB,KAAK,cAAc;AAAA,EAC7C;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,aAAa;AAAA,MACb,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,iCAAiC;AAAA,QAC7D,EAAE,QAAQ,KAAK,aAAa,uBAAuB;AAAA,QACnD,EAAE,QAAQ,KAAK,aAAa,eAAe;AAAA,MAC7C;AAAA,IACF;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
@@ -1,5 +1,7 @@
1
1
  import { NextResponse } from "next/server";
2
+ import { runWithCacheTenant } from "@open-mercato/cache";
2
3
  import { acceptAllActions } from "../../../../lib/executionEngine.js";
4
+ import { resolveCache, invalidateCountsCache } from "../../../../lib/cache.js";
3
5
  import {
4
6
  resolveRequestContext,
5
7
  resolveProposal,
@@ -23,6 +25,8 @@ async function POST(req) {
23
25
  );
24
26
  const succeeded = results.filter((r) => r.success).length;
25
27
  const failed = results.filter((r) => !r.success).length;
28
+ const cache = resolveCache(ctx.container);
29
+ await runWithCacheTenant(ctx.tenantId, () => invalidateCountsCache(cache, ctx.tenantId));
26
30
  return NextResponse.json({ ok: !stoppedOnFailure, succeeded, failed, stoppedOnFailure, results });
27
31
  } catch (err) {
28
32
  return handleRouteError(err, "accept all actions");
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../src/modules/inbox_ops/api/proposals/%5Bid%5D/accept-all/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { acceptAllActions } from '../../../../lib/executionEngine'\nimport {\n resolveRequestContext,\n resolveProposal,\n resolveCrossModuleEntities,\n toExecutionContext,\n handleRouteError,\n isErrorResponse,\n} from '../../../routeHelpers'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['inbox_ops.proposals.manage'] },\n}\n\nexport async function POST(req: Request) {\n try {\n const ctx = await resolveRequestContext(req)\n const proposal = await resolveProposal(new URL(req.url), ctx)\n if (isErrorResponse(proposal)) return proposal\n\n const entities = resolveCrossModuleEntities(ctx.container)\n const { results, stoppedOnFailure } = await acceptAllActions(\n proposal.id,\n toExecutionContext(ctx, entities),\n )\n\n const succeeded = results.filter((r) => r.success).length\n const failed = results.filter((r) => !r.success).length\n\n return NextResponse.json({ ok: !stoppedOnFailure, succeeded, failed, stoppedOnFailure, results })\n } catch (err) {\n return handleRouteError(err, 'accept all actions')\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'InboxOps',\n summary: 'Accept all actions',\n methods: {\n POST: {\n summary: 'Accept and execute all pending actions in a proposal',\n responses: [\n { status: 200, description: 'All actions processed' },\n { status: 404, description: 'Proposal not found' },\n ],\n },\n },\n}\n"],
5
- "mappings": "AAAA,SAAS,oBAAoB;AAE7B,SAAS,wBAAwB;AACjC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEA,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,4BAA4B,EAAE;AAC7E;AAEA,eAAsB,KAAK,KAAc;AACvC,MAAI;AACF,UAAM,MAAM,MAAM,sBAAsB,GAAG;AAC3C,UAAM,WAAW,MAAM,gBAAgB,IAAI,IAAI,IAAI,GAAG,GAAG,GAAG;AAC5D,QAAI,gBAAgB,QAAQ,EAAG,QAAO;AAEtC,UAAM,WAAW,2BAA2B,IAAI,SAAS;AACzD,UAAM,EAAE,SAAS,iBAAiB,IAAI,MAAM;AAAA,MAC1C,SAAS;AAAA,MACT,mBAAmB,KAAK,QAAQ;AAAA,IAClC;AAEA,UAAM,YAAY,QAAQ,OAAO,CAAC,MAAM,EAAE,OAAO,EAAE;AACnD,UAAM,SAAS,QAAQ,OAAO,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE;AAEjD,WAAO,aAAa,KAAK,EAAE,IAAI,CAAC,kBAAkB,WAAW,QAAQ,kBAAkB,QAAQ,CAAC;AAAA,EAClG,SAAS,KAAK;AACZ,WAAO,iBAAiB,KAAK,oBAAoB;AAAA,EACnD;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,wBAAwB;AAAA,QACpD,EAAE,QAAQ,KAAK,aAAa,qBAAqB;AAAA,MACnD;AAAA,IACF;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { runWithCacheTenant } from '@open-mercato/cache'\nimport { acceptAllActions } from '../../../../lib/executionEngine'\nimport { resolveCache, invalidateCountsCache } from '../../../../lib/cache'\nimport {\n resolveRequestContext,\n resolveProposal,\n resolveCrossModuleEntities,\n toExecutionContext,\n handleRouteError,\n isErrorResponse,\n} from '../../../routeHelpers'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['inbox_ops.proposals.manage'] },\n}\n\nexport async function POST(req: Request) {\n try {\n const ctx = await resolveRequestContext(req)\n const proposal = await resolveProposal(new URL(req.url), ctx)\n if (isErrorResponse(proposal)) return proposal\n\n const entities = resolveCrossModuleEntities(ctx.container)\n const { results, stoppedOnFailure } = await acceptAllActions(\n proposal.id,\n toExecutionContext(ctx, entities),\n )\n\n const succeeded = results.filter((r) => r.success).length\n const failed = results.filter((r) => !r.success).length\n\n const cache = resolveCache(ctx.container)\n await runWithCacheTenant(ctx.tenantId, () => invalidateCountsCache(cache, ctx.tenantId))\n\n return NextResponse.json({ ok: !stoppedOnFailure, succeeded, failed, stoppedOnFailure, results })\n } catch (err) {\n return handleRouteError(err, 'accept all actions')\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'InboxOps',\n summary: 'Accept all actions',\n methods: {\n POST: {\n summary: 'Accept and execute all pending actions in a proposal',\n responses: [\n { status: 200, description: 'All actions processed' },\n { status: 404, description: 'Proposal not found' },\n ],\n },\n },\n}\n"],
5
+ "mappings": "AAAA,SAAS,oBAAoB;AAE7B,SAAS,0BAA0B;AACnC,SAAS,wBAAwB;AACjC,SAAS,cAAc,6BAA6B;AACpD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEA,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,4BAA4B,EAAE;AAC7E;AAEA,eAAsB,KAAK,KAAc;AACvC,MAAI;AACF,UAAM,MAAM,MAAM,sBAAsB,GAAG;AAC3C,UAAM,WAAW,MAAM,gBAAgB,IAAI,IAAI,IAAI,GAAG,GAAG,GAAG;AAC5D,QAAI,gBAAgB,QAAQ,EAAG,QAAO;AAEtC,UAAM,WAAW,2BAA2B,IAAI,SAAS;AACzD,UAAM,EAAE,SAAS,iBAAiB,IAAI,MAAM;AAAA,MAC1C,SAAS;AAAA,MACT,mBAAmB,KAAK,QAAQ;AAAA,IAClC;AAEA,UAAM,YAAY,QAAQ,OAAO,CAAC,MAAM,EAAE,OAAO,EAAE;AACnD,UAAM,SAAS,QAAQ,OAAO,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE;AAEjD,UAAM,QAAQ,aAAa,IAAI,SAAS;AACxC,UAAM,mBAAmB,IAAI,UAAU,MAAM,sBAAsB,OAAO,IAAI,QAAQ,CAAC;AAEvF,WAAO,aAAa,KAAK,EAAE,IAAI,CAAC,kBAAkB,WAAW,QAAQ,kBAAkB,QAAQ,CAAC;AAAA,EAClG,SAAS,KAAK;AACZ,WAAO,iBAAiB,KAAK,oBAAoB;AAAA,EACnD;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,wBAAwB;AAAA,QACpD,EAAE,QAAQ,KAAK,aAAa,qBAAqB;AAAA,MACnD;AAAA,IACF;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,9 @@
1
1
  import { NextResponse } from "next/server";
2
+ import { runWithCacheTenant } from "@open-mercato/cache";
2
3
  import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
3
4
  import { InboxProposal, InboxProposalAction } from "../../../../../../data/entities.js";
4
5
  import { executeAction } from "../../../../../../lib/executionEngine.js";
6
+ import { resolveCache, invalidateCountsCache } from "../../../../../../lib/cache.js";
5
7
  import {
6
8
  resolveRequestContext,
7
9
  resolveActionAndProposal,
@@ -40,6 +42,8 @@ async function POST(req) {
40
42
  void 0,
41
43
  ctx.scope
42
44
  );
45
+ const cache = resolveCache(ctx.container);
46
+ await runWithCacheTenant(ctx.tenantId, () => invalidateCountsCache(cache, ctx.tenantId));
43
47
  return NextResponse.json({
44
48
  ok: true,
45
49
  action: freshAction ? {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../../../src/modules/inbox_ops/api/proposals/%5Bid%5D/actions/%5BactionId%5D/accept/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { InboxProposal, InboxProposalAction } from '../../../../../../data/entities'\nimport { executeAction } from '../../../../../../lib/executionEngine'\nimport {\n resolveRequestContext,\n resolveActionAndProposal,\n resolveCrossModuleEntities,\n toExecutionContext,\n handleRouteError,\n isErrorResponse,\n} from '../../../../../routeHelpers'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['inbox_ops.proposals.manage'] },\n}\n\nexport async function POST(req: Request) {\n try {\n const ctx = await resolveRequestContext(req)\n const resolved = await resolveActionAndProposal(new URL(req.url), ctx)\n if (isErrorResponse(resolved)) return resolved\n\n const entities = resolveCrossModuleEntities(ctx.container)\n const result = await executeAction(resolved.action, toExecutionContext(ctx, entities))\n\n if (!result.success) {\n return NextResponse.json(\n { error: result.error || 'Failed to execute action' },\n { status: result.statusCode || 400 },\n )\n }\n\n const freshAction = await findOneWithDecryption(\n ctx.em,\n InboxProposalAction,\n { id: resolved.action.id, deletedAt: null },\n undefined,\n ctx.scope,\n )\n\n const freshProposal = await findOneWithDecryption(\n ctx.em,\n InboxProposal,\n { id: resolved.proposal.id, deletedAt: null },\n undefined,\n ctx.scope,\n )\n\n return NextResponse.json({\n ok: true,\n action: freshAction ? {\n id: freshAction.id,\n status: freshAction.status,\n createdEntityId: freshAction.createdEntityId,\n createdEntityType: freshAction.createdEntityType,\n executedAt: freshAction.executedAt,\n executedByUserId: freshAction.executedByUserId,\n } : null,\n proposal: freshProposal ? {\n id: freshProposal.id,\n status: freshProposal.status,\n } : null,\n })\n } catch (err) {\n return handleRouteError(err, 'execute action')\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'InboxOps',\n summary: 'Accept action',\n methods: {\n POST: {\n summary: 'Accept and execute a proposal action',\n description: 'Executes the action and creates the entity in the target module. Returns 409 if already processed.',\n responses: [\n { status: 200, description: 'Action executed successfully' },\n { status: 403, description: 'Insufficient permissions in target module' },\n { status: 404, description: 'Action not found' },\n { status: 409, description: 'Action already processed' },\n ],\n },\n },\n}\n"],
5
- "mappings": "AAAA,SAAS,oBAAoB;AAE7B,SAAS,6BAA6B;AACtC,SAAS,eAAe,2BAA2B;AACnD,SAAS,qBAAqB;AAC9B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEA,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,4BAA4B,EAAE;AAC7E;AAEA,eAAsB,KAAK,KAAc;AACvC,MAAI;AACF,UAAM,MAAM,MAAM,sBAAsB,GAAG;AAC3C,UAAM,WAAW,MAAM,yBAAyB,IAAI,IAAI,IAAI,GAAG,GAAG,GAAG;AACrE,QAAI,gBAAgB,QAAQ,EAAG,QAAO;AAEtC,UAAM,WAAW,2BAA2B,IAAI,SAAS;AACzD,UAAM,SAAS,MAAM,cAAc,SAAS,QAAQ,mBAAmB,KAAK,QAAQ,CAAC;AAErF,QAAI,CAAC,OAAO,SAAS;AACnB,aAAO,aAAa;AAAA,QAClB,EAAE,OAAO,OAAO,SAAS,2BAA2B;AAAA,QACpD,EAAE,QAAQ,OAAO,cAAc,IAAI;AAAA,MACrC;AAAA,IACF;AAEA,UAAM,cAAc,MAAM;AAAA,MACxB,IAAI;AAAA,MACJ;AAAA,MACA,EAAE,IAAI,SAAS,OAAO,IAAI,WAAW,KAAK;AAAA,MAC1C;AAAA,MACA,IAAI;AAAA,IACN;AAEA,UAAM,gBAAgB,MAAM;AAAA,MAC1B,IAAI;AAAA,MACJ;AAAA,MACA,EAAE,IAAI,SAAS,SAAS,IAAI,WAAW,KAAK;AAAA,MAC5C;AAAA,MACA,IAAI;AAAA,IACN;AAEA,WAAO,aAAa,KAAK;AAAA,MACvB,IAAI;AAAA,MACJ,QAAQ,cAAc;AAAA,QACpB,IAAI,YAAY;AAAA,QAChB,QAAQ,YAAY;AAAA,QACpB,iBAAiB,YAAY;AAAA,QAC7B,mBAAmB,YAAY;AAAA,QAC/B,YAAY,YAAY;AAAA,QACxB,kBAAkB,YAAY;AAAA,MAChC,IAAI;AAAA,MACJ,UAAU,gBAAgB;AAAA,QACxB,IAAI,cAAc;AAAA,QAClB,QAAQ,cAAc;AAAA,MACxB,IAAI;AAAA,IACN,CAAC;AAAA,EACH,SAAS,KAAK;AACZ,WAAO,iBAAiB,KAAK,gBAAgB;AAAA,EAC/C;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,aAAa;AAAA,MACb,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,+BAA+B;AAAA,QAC3D,EAAE,QAAQ,KAAK,aAAa,4CAA4C;AAAA,QACxE,EAAE,QAAQ,KAAK,aAAa,mBAAmB;AAAA,QAC/C,EAAE,QAAQ,KAAK,aAAa,2BAA2B;AAAA,MACzD;AAAA,IACF;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { runWithCacheTenant } from '@open-mercato/cache'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { InboxProposal, InboxProposalAction } from '../../../../../../data/entities'\nimport { executeAction } from '../../../../../../lib/executionEngine'\nimport { resolveCache, invalidateCountsCache } from '../../../../../../lib/cache'\nimport {\n resolveRequestContext,\n resolveActionAndProposal,\n resolveCrossModuleEntities,\n toExecutionContext,\n handleRouteError,\n isErrorResponse,\n} from '../../../../../routeHelpers'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['inbox_ops.proposals.manage'] },\n}\n\nexport async function POST(req: Request) {\n try {\n const ctx = await resolveRequestContext(req)\n const resolved = await resolveActionAndProposal(new URL(req.url), ctx)\n if (isErrorResponse(resolved)) return resolved\n\n const entities = resolveCrossModuleEntities(ctx.container)\n const result = await executeAction(resolved.action, toExecutionContext(ctx, entities))\n\n if (!result.success) {\n return NextResponse.json(\n { error: result.error || 'Failed to execute action' },\n { status: result.statusCode || 400 },\n )\n }\n\n const freshAction = await findOneWithDecryption(\n ctx.em,\n InboxProposalAction,\n { id: resolved.action.id, deletedAt: null },\n undefined,\n ctx.scope,\n )\n\n const freshProposal = await findOneWithDecryption(\n ctx.em,\n InboxProposal,\n { id: resolved.proposal.id, deletedAt: null },\n undefined,\n ctx.scope,\n )\n\n const cache = resolveCache(ctx.container)\n await runWithCacheTenant(ctx.tenantId, () => invalidateCountsCache(cache, ctx.tenantId))\n\n return NextResponse.json({\n ok: true,\n action: freshAction ? {\n id: freshAction.id,\n status: freshAction.status,\n createdEntityId: freshAction.createdEntityId,\n createdEntityType: freshAction.createdEntityType,\n executedAt: freshAction.executedAt,\n executedByUserId: freshAction.executedByUserId,\n } : null,\n proposal: freshProposal ? {\n id: freshProposal.id,\n status: freshProposal.status,\n } : null,\n })\n } catch (err) {\n return handleRouteError(err, 'execute action')\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'InboxOps',\n summary: 'Accept action',\n methods: {\n POST: {\n summary: 'Accept and execute a proposal action',\n description: 'Executes the action and creates the entity in the target module. Returns 409 if already processed.',\n responses: [\n { status: 200, description: 'Action executed successfully' },\n { status: 403, description: 'Insufficient permissions in target module' },\n { status: 404, description: 'Action not found' },\n { status: 409, description: 'Action already processed' },\n ],\n },\n },\n}\n"],
5
+ "mappings": "AAAA,SAAS,oBAAoB;AAE7B,SAAS,0BAA0B;AACnC,SAAS,6BAA6B;AACtC,SAAS,eAAe,2BAA2B;AACnD,SAAS,qBAAqB;AAC9B,SAAS,cAAc,6BAA6B;AACpD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEA,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,4BAA4B,EAAE;AAC7E;AAEA,eAAsB,KAAK,KAAc;AACvC,MAAI;AACF,UAAM,MAAM,MAAM,sBAAsB,GAAG;AAC3C,UAAM,WAAW,MAAM,yBAAyB,IAAI,IAAI,IAAI,GAAG,GAAG,GAAG;AACrE,QAAI,gBAAgB,QAAQ,EAAG,QAAO;AAEtC,UAAM,WAAW,2BAA2B,IAAI,SAAS;AACzD,UAAM,SAAS,MAAM,cAAc,SAAS,QAAQ,mBAAmB,KAAK,QAAQ,CAAC;AAErF,QAAI,CAAC,OAAO,SAAS;AACnB,aAAO,aAAa;AAAA,QAClB,EAAE,OAAO,OAAO,SAAS,2BAA2B;AAAA,QACpD,EAAE,QAAQ,OAAO,cAAc,IAAI;AAAA,MACrC;AAAA,IACF;AAEA,UAAM,cAAc,MAAM;AAAA,MACxB,IAAI;AAAA,MACJ;AAAA,MACA,EAAE,IAAI,SAAS,OAAO,IAAI,WAAW,KAAK;AAAA,MAC1C;AAAA,MACA,IAAI;AAAA,IACN;AAEA,UAAM,gBAAgB,MAAM;AAAA,MAC1B,IAAI;AAAA,MACJ;AAAA,MACA,EAAE,IAAI,SAAS,SAAS,IAAI,WAAW,KAAK;AAAA,MAC5C;AAAA,MACA,IAAI;AAAA,IACN;AAEA,UAAM,QAAQ,aAAa,IAAI,SAAS;AACxC,UAAM,mBAAmB,IAAI,UAAU,MAAM,sBAAsB,OAAO,IAAI,QAAQ,CAAC;AAEvF,WAAO,aAAa,KAAK;AAAA,MACvB,IAAI;AAAA,MACJ,QAAQ,cAAc;AAAA,QACpB,IAAI,YAAY;AAAA,QAChB,QAAQ,YAAY;AAAA,QACpB,iBAAiB,YAAY;AAAA,QAC7B,mBAAmB,YAAY;AAAA,QAC/B,YAAY,YAAY;AAAA,QACxB,kBAAkB,YAAY;AAAA,MAChC,IAAI;AAAA,MACJ,UAAU,gBAAgB;AAAA,QACxB,IAAI,cAAc;AAAA,QAClB,QAAQ,cAAc;AAAA,MACxB,IAAI;AAAA,IACN,CAAC;AAAA,EACH,SAAS,KAAK;AACZ,WAAO,iBAAiB,KAAK,gBAAgB;AAAA,EAC/C;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,aAAa;AAAA,MACb,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,+BAA+B;AAAA,QAC3D,EAAE,QAAQ,KAAK,aAAa,4CAA4C;AAAA,QACxE,EAAE,QAAQ,KAAK,aAAa,mBAAmB;AAAA,QAC/C,EAAE,QAAQ,KAAK,aAAa,2BAA2B;AAAA,MACzD;AAAA,IACF;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
@@ -1,9 +1,11 @@
1
1
  import { NextResponse } from "next/server";
2
2
  import { z } from "zod";
3
+ import { runWithCacheTenant } from "@open-mercato/cache";
3
4
  import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
4
5
  import { InboxProposal, InboxProposalAction } from "../../../../../../data/entities.js";
5
6
  import { recalculateProposalStatus } from "../../../../../../lib/executionEngine.js";
6
7
  import { formatZodErrors } from "../../../../../../lib/validation.js";
8
+ import { resolveCache, invalidateCountsCache } from "../../../../../../lib/cache.js";
7
9
  import {
8
10
  resolveRequestContext,
9
11
  resolveActionAndProposal,
@@ -65,6 +67,8 @@ async function PATCH(req) {
65
67
  void 0,
66
68
  ctx.scope
67
69
  );
70
+ const cache = resolveCache(ctx.container);
71
+ await runWithCacheTenant(ctx.tenantId, () => invalidateCountsCache(cache, ctx.tenantId));
68
72
  return NextResponse.json({
69
73
  ok: true,
70
74
  action: {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../../../src/modules/inbox_ops/api/proposals/%5Bid%5D/actions/%5BactionId%5D/complete/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { InboxProposal, InboxProposalAction } from '../../../../../../data/entities'\nimport { recalculateProposalStatus } from '../../../../../../lib/executionEngine'\nimport { formatZodErrors } from '../../../../../../lib/validation'\nimport {\n resolveRequestContext,\n resolveActionAndProposal,\n handleRouteError,\n isErrorResponse,\n} from '../../../../../routeHelpers'\n\nexport const metadata = {\n PATCH: { requireAuth: true, requireFeatures: ['inbox_ops.proposals.manage'] },\n}\n\nconst completeBodySchema = z.object({\n createdEntityId: z.string().uuid(),\n createdEntityType: z.string().trim().min(1).max(100),\n})\n\nexport async function PATCH(req: Request) {\n try {\n const ctx = await resolveRequestContext(req)\n const resolved = await resolveActionAndProposal(new URL(req.url), ctx)\n if (isErrorResponse(resolved)) return resolved\n\n const { action, proposal } = resolved\n\n if (action.status !== 'pending' && action.status !== 'failed') {\n return NextResponse.json(\n { error: 'Action already processed' },\n { status: 409 },\n )\n }\n\n let body: Record<string, unknown>\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })\n }\n\n const parsed = completeBodySchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ error: `Invalid body: ${formatZodErrors(parsed.error)}` }, { status: 400 })\n }\n\n const em = ctx.em.fork()\n const freshAction = await findOneWithDecryption(\n em,\n InboxProposalAction,\n { id: action.id, deletedAt: null },\n undefined,\n ctx.scope,\n )\n if (!freshAction) {\n return NextResponse.json({ error: 'Action not found' }, { status: 404 })\n }\n\n freshAction.status = 'executed'\n freshAction.executedAt = new Date()\n freshAction.executedByUserId = ctx.userId\n freshAction.createdEntityId = parsed.data.createdEntityId\n freshAction.createdEntityType = parsed.data.createdEntityType\n freshAction.executionError = null\n await em.flush()\n\n await recalculateProposalStatus(em, proposal.id, ctx.scope)\n\n const freshProposal = await findOneWithDecryption(\n em,\n InboxProposal,\n { id: proposal.id, deletedAt: null },\n undefined,\n ctx.scope,\n )\n\n return NextResponse.json({\n ok: true,\n action: {\n id: freshAction.id,\n status: freshAction.status,\n createdEntityId: freshAction.createdEntityId,\n createdEntityType: freshAction.createdEntityType,\n executedAt: freshAction.executedAt,\n },\n proposal: freshProposal ? {\n id: freshProposal.id,\n status: freshProposal.status,\n } : null,\n })\n } catch (err) {\n return handleRouteError(err, 'complete action')\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'InboxOps',\n summary: 'Complete action externally',\n methods: {\n PATCH: {\n summary: 'Mark an action as completed with an externally-created entity',\n description: 'Used when an action is fulfilled through the normal sales form instead of the execution engine. Updates the action status without running the execution engine.',\n responses: [\n { status: 200, description: 'Action marked as completed' },\n { status: 400, description: 'Invalid body' },\n { status: 404, description: 'Action not found' },\n { status: 409, description: 'Action already processed' },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { runWithCacheTenant } from '@open-mercato/cache'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { InboxProposal, InboxProposalAction } from '../../../../../../data/entities'\nimport { recalculateProposalStatus } from '../../../../../../lib/executionEngine'\nimport { formatZodErrors } from '../../../../../../lib/validation'\nimport { resolveCache, invalidateCountsCache } from '../../../../../../lib/cache'\nimport {\n resolveRequestContext,\n resolveActionAndProposal,\n handleRouteError,\n isErrorResponse,\n} from '../../../../../routeHelpers'\n\nexport const metadata = {\n PATCH: { requireAuth: true, requireFeatures: ['inbox_ops.proposals.manage'] },\n}\n\nconst completeBodySchema = z.object({\n createdEntityId: z.string().uuid(),\n createdEntityType: z.string().trim().min(1).max(100),\n})\n\nexport async function PATCH(req: Request) {\n try {\n const ctx = await resolveRequestContext(req)\n const resolved = await resolveActionAndProposal(new URL(req.url), ctx)\n if (isErrorResponse(resolved)) return resolved\n\n const { action, proposal } = resolved\n\n if (action.status !== 'pending' && action.status !== 'failed') {\n return NextResponse.json(\n { error: 'Action already processed' },\n { status: 409 },\n )\n }\n\n let body: Record<string, unknown>\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })\n }\n\n const parsed = completeBodySchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ error: `Invalid body: ${formatZodErrors(parsed.error)}` }, { status: 400 })\n }\n\n const em = ctx.em.fork()\n const freshAction = await findOneWithDecryption(\n em,\n InboxProposalAction,\n { id: action.id, deletedAt: null },\n undefined,\n ctx.scope,\n )\n if (!freshAction) {\n return NextResponse.json({ error: 'Action not found' }, { status: 404 })\n }\n\n freshAction.status = 'executed'\n freshAction.executedAt = new Date()\n freshAction.executedByUserId = ctx.userId\n freshAction.createdEntityId = parsed.data.createdEntityId\n freshAction.createdEntityType = parsed.data.createdEntityType\n freshAction.executionError = null\n await em.flush()\n\n await recalculateProposalStatus(em, proposal.id, ctx.scope)\n\n const freshProposal = await findOneWithDecryption(\n em,\n InboxProposal,\n { id: proposal.id, deletedAt: null },\n undefined,\n ctx.scope,\n )\n\n const cache = resolveCache(ctx.container)\n await runWithCacheTenant(ctx.tenantId, () => invalidateCountsCache(cache, ctx.tenantId))\n\n return NextResponse.json({\n ok: true,\n action: {\n id: freshAction.id,\n status: freshAction.status,\n createdEntityId: freshAction.createdEntityId,\n createdEntityType: freshAction.createdEntityType,\n executedAt: freshAction.executedAt,\n },\n proposal: freshProposal ? {\n id: freshProposal.id,\n status: freshProposal.status,\n } : null,\n })\n } catch (err) {\n return handleRouteError(err, 'complete action')\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'InboxOps',\n summary: 'Complete action externally',\n methods: {\n PATCH: {\n summary: 'Mark an action as completed with an externally-created entity',\n description: 'Used when an action is fulfilled through the normal sales form instead of the execution engine. Updates the action status without running the execution engine.',\n responses: [\n { status: 200, description: 'Action marked as completed' },\n { status: 400, description: 'Invalid body' },\n { status: 404, description: 'Action not found' },\n { status: 409, description: 'Action already processed' },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,5 +1,7 @@
1
1
  import { NextResponse } from "next/server";
2
+ import { runWithCacheTenant } from "@open-mercato/cache";
2
3
  import { rejectAction } from "../../../../../../lib/executionEngine.js";
4
+ import { resolveCache, invalidateCountsCache } from "../../../../../../lib/cache.js";
3
5
  import {
4
6
  resolveRequestContext,
5
7
  resolveActionAndProposal,
@@ -19,6 +21,8 @@ async function POST(req) {
19
21
  return NextResponse.json({ error: "Action already processed" }, { status: 409 });
20
22
  }
21
23
  await rejectAction(resolved.action, toExecutionContext(ctx));
24
+ const cache = resolveCache(ctx.container);
25
+ await runWithCacheTenant(ctx.tenantId, () => invalidateCountsCache(cache, ctx.tenantId));
22
26
  return NextResponse.json({ ok: true });
23
27
  } catch (err) {
24
28
  return handleRouteError(err, "reject action");