@open-mercato/core 0.4.5-develop-811deeb983 → 0.4.5-develop-3d8e759e45
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/modules/catalog/inbox-actions.js +51 -0
- package/dist/modules/catalog/inbox-actions.js.map +7 -0
- package/dist/modules/customers/inbox-actions.js +230 -0
- package/dist/modules/customers/inbox-actions.js.map +7 -0
- package/dist/modules/inbox_ops/api/emails/[id]/route.js +40 -1
- package/dist/modules/inbox_ops/api/emails/[id]/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/extract/route.js +87 -0
- package/dist/modules/inbox_ops/api/extract/route.js.map +7 -0
- package/dist/modules/inbox_ops/api/proposals/[id]/translate/route.js +6 -1
- package/dist/modules/inbox_ops/api/proposals/[id]/translate/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/proposals/counts/route.js.map +2 -2
- package/dist/modules/inbox_ops/backend/inbox-ops/log/page.js +40 -14
- package/dist/modules/inbox_ops/backend/inbox-ops/log/page.js.map +2 -2
- package/dist/modules/inbox_ops/backend/inbox-ops/log/page.meta.js +2 -2
- package/dist/modules/inbox_ops/backend/inbox-ops/log/page.meta.js.map +2 -2
- package/dist/modules/inbox_ops/backend/inbox-ops/page.js +161 -79
- package/dist/modules/inbox_ops/backend/inbox-ops/page.js.map +2 -2
- package/dist/modules/inbox_ops/backend/inbox-ops/page.meta.js +2 -2
- package/dist/modules/inbox_ops/backend/inbox-ops/page.meta.js.map +2 -2
- package/dist/modules/inbox_ops/backend/inbox-ops/proposals/[id]/page.js +109 -62
- package/dist/modules/inbox_ops/backend/inbox-ops/proposals/[id]/page.js.map +3 -3
- package/dist/modules/inbox_ops/backend/inbox-ops/proposals/[id]/page.meta.js +2 -2
- package/dist/modules/inbox_ops/backend/inbox-ops/proposals/[id]/page.meta.js.map +2 -2
- package/dist/modules/inbox_ops/backend/inbox-ops/settings/page.js +36 -14
- package/dist/modules/inbox_ops/backend/inbox-ops/settings/page.js.map +2 -2
- package/dist/modules/inbox_ops/backend/inbox-ops/settings/page.meta.js +2 -2
- package/dist/modules/inbox_ops/backend/inbox-ops/settings/page.meta.js.map +2 -2
- package/dist/modules/inbox_ops/components/proposals/ActionCard.js +65 -10
- package/dist/modules/inbox_ops/components/proposals/ActionCard.js.map +2 -2
- package/dist/modules/inbox_ops/components/proposals/EditActionDialog.js +58 -10
- package/dist/modules/inbox_ops/components/proposals/EditActionDialog.js.map +2 -2
- package/dist/modules/inbox_ops/lib/constants.js.map +2 -2
- package/dist/modules/inbox_ops/lib/contactValidation.js +40 -0
- package/dist/modules/inbox_ops/lib/contactValidation.js.map +7 -0
- package/dist/modules/inbox_ops/lib/executionEngine.js +31 -826
- package/dist/modules/inbox_ops/lib/executionEngine.js.map +3 -3
- package/dist/modules/inbox_ops/lib/executionHelpers.js +368 -0
- package/dist/modules/inbox_ops/lib/executionHelpers.js.map +7 -0
- package/dist/modules/inbox_ops/lib/extractionPrompt.js +28 -35
- package/dist/modules/inbox_ops/lib/extractionPrompt.js.map +3 -3
- package/dist/modules/inbox_ops/lib/inbox-actions-generated.d.js +1 -0
- package/dist/modules/inbox_ops/lib/inbox-actions-generated.d.js.map +7 -0
- package/dist/modules/inbox_ops/lib/translationProvider.js +15 -10
- package/dist/modules/inbox_ops/lib/translationProvider.js.map +2 -2
- package/dist/modules/inbox_ops/subscribers/extractionWorker.js +16 -16
- package/dist/modules/inbox_ops/subscribers/extractionWorker.js.map +2 -2
- package/dist/modules/sales/inbox-actions.js +278 -0
- package/dist/modules/sales/inbox-actions.js.map +7 -0
- package/jest.config.cjs +1 -0
- package/jest.mocks/inbox-actions.generated.js +5 -0
- package/package.json +2 -2
- package/src/modules/catalog/inbox-actions.ts +60 -0
- package/src/modules/customers/inbox-actions.ts +285 -0
- package/src/modules/inbox_ops/api/emails/[id]/route.ts +44 -0
- package/src/modules/inbox_ops/api/extract/route.ts +94 -0
- package/src/modules/inbox_ops/api/proposals/[id]/translate/route.ts +6 -1
- package/src/modules/inbox_ops/api/proposals/counts/route.ts +2 -0
- package/src/modules/inbox_ops/backend/inbox-ops/log/page.meta.ts +2 -2
- package/src/modules/inbox_ops/backend/inbox-ops/log/page.tsx +43 -13
- package/src/modules/inbox_ops/backend/inbox-ops/page.meta.ts +2 -2
- package/src/modules/inbox_ops/backend/inbox-ops/page.tsx +176 -81
- package/src/modules/inbox_ops/backend/inbox-ops/proposals/[id]/page.meta.ts +2 -2
- package/src/modules/inbox_ops/backend/inbox-ops/proposals/[id]/page.tsx +122 -68
- package/src/modules/inbox_ops/backend/inbox-ops/settings/page.meta.ts +2 -2
- package/src/modules/inbox_ops/backend/inbox-ops/settings/page.tsx +36 -14
- package/src/modules/inbox_ops/components/proposals/ActionCard.tsx +91 -7
- package/src/modules/inbox_ops/components/proposals/EditActionDialog.tsx +64 -12
- package/src/modules/inbox_ops/lib/constants.ts +9 -0
- package/src/modules/inbox_ops/lib/contactValidation.ts +54 -0
- package/src/modules/inbox_ops/lib/executionEngine.ts +47 -1060
- package/src/modules/inbox_ops/lib/executionHelpers.ts +527 -0
- package/src/modules/inbox_ops/lib/extractionPrompt.ts +45 -34
- package/src/modules/inbox_ops/lib/inbox-actions-generated.d.ts +11 -0
- package/src/modules/inbox_ops/lib/translationProvider.ts +16 -10
- package/src/modules/inbox_ops/subscribers/extractionWorker.ts +16 -18
- package/src/modules/sales/inbox-actions.ts +359 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { createProductPayloadSchema } from "../inbox_ops/data/validators.js";
|
|
2
|
+
import {
|
|
3
|
+
asHelperContext,
|
|
4
|
+
ExecutionError,
|
|
5
|
+
executeCommand,
|
|
6
|
+
resolveProductDiscrepanciesInProposal
|
|
7
|
+
} from "../inbox_ops/lib/executionHelpers.js";
|
|
8
|
+
async function executeCreateProductAction(action, ctx) {
|
|
9
|
+
const hCtx = asHelperContext(ctx);
|
|
10
|
+
const payload = action.payload;
|
|
11
|
+
const createInput = {
|
|
12
|
+
organizationId: hCtx.organizationId,
|
|
13
|
+
tenantId: hCtx.tenantId,
|
|
14
|
+
title: payload.title,
|
|
15
|
+
productType: "simple",
|
|
16
|
+
isActive: true
|
|
17
|
+
};
|
|
18
|
+
if (payload.sku) createInput.sku = payload.sku;
|
|
19
|
+
if (payload.description) createInput.description = payload.description;
|
|
20
|
+
if (payload.currencyCode) createInput.primaryCurrencyCode = payload.currencyCode;
|
|
21
|
+
const result = await executeCommand(
|
|
22
|
+
hCtx,
|
|
23
|
+
"catalog.products.create",
|
|
24
|
+
createInput
|
|
25
|
+
);
|
|
26
|
+
if (!result.productId) {
|
|
27
|
+
throw new ExecutionError("Product creation did not return a product ID", 500);
|
|
28
|
+
}
|
|
29
|
+
await resolveProductDiscrepanciesInProposal(hCtx.em, action.proposalId, payload.title, result.productId, {
|
|
30
|
+
tenantId: hCtx.tenantId,
|
|
31
|
+
organizationId: hCtx.organizationId
|
|
32
|
+
});
|
|
33
|
+
return { createdEntityId: result.productId, createdEntityType: "catalog_product" };
|
|
34
|
+
}
|
|
35
|
+
const inboxActions = [
|
|
36
|
+
{
|
|
37
|
+
type: "create_product",
|
|
38
|
+
requiredFeature: "catalog.products.manage",
|
|
39
|
+
payloadSchema: createProductPayloadSchema,
|
|
40
|
+
label: "Create Product",
|
|
41
|
+
promptSchema: `create_product payload:
|
|
42
|
+
{ title: string, sku?: string, unitPrice?: string, currencyCode?: string (3-letter ISO), kind?: "product"|"service", description?: string }`,
|
|
43
|
+
execute: executeCreateProductAction
|
|
44
|
+
}
|
|
45
|
+
];
|
|
46
|
+
var inbox_actions_default = inboxActions;
|
|
47
|
+
export {
|
|
48
|
+
inbox_actions_default as default,
|
|
49
|
+
inboxActions
|
|
50
|
+
};
|
|
51
|
+
//# sourceMappingURL=inbox-actions.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/modules/catalog/inbox-actions.ts"],
|
|
4
|
+
"sourcesContent": ["import type { InboxActionDefinition, InboxActionExecutionContext } from '@open-mercato/shared/modules/inbox-actions'\nimport { createProductPayloadSchema } from '../inbox_ops/data/validators'\nimport type { CreateProductPayload } from '../inbox_ops/data/validators'\nimport {\n asHelperContext,\n ExecutionError,\n executeCommand,\n resolveProductDiscrepanciesInProposal,\n} from '../inbox_ops/lib/executionHelpers'\n\nasync function executeCreateProductAction(\n action: { id: string; proposalId: string; payload: unknown },\n ctx: InboxActionExecutionContext,\n): Promise<{ createdEntityId?: string | null; createdEntityType?: string | null }> {\n const hCtx = asHelperContext(ctx)\n const payload = action.payload as CreateProductPayload\n\n const createInput: Record<string, unknown> = {\n organizationId: hCtx.organizationId,\n tenantId: hCtx.tenantId,\n title: payload.title,\n productType: 'simple',\n isActive: true,\n }\n\n if (payload.sku) createInput.sku = payload.sku\n if (payload.description) createInput.description = payload.description\n if (payload.currencyCode) createInput.primaryCurrencyCode = payload.currencyCode\n\n const result = await executeCommand<Record<string, unknown>, { productId?: string }>(\n hCtx,\n 'catalog.products.create',\n createInput,\n )\n\n if (!result.productId) {\n throw new ExecutionError('Product creation did not return a product ID', 500)\n }\n\n await resolveProductDiscrepanciesInProposal(hCtx.em, action.proposalId, payload.title, result.productId, {\n tenantId: hCtx.tenantId,\n organizationId: hCtx.organizationId,\n })\n\n return { createdEntityId: result.productId, createdEntityType: 'catalog_product' }\n}\n\nexport const inboxActions: InboxActionDefinition[] = [\n {\n type: 'create_product',\n requiredFeature: 'catalog.products.manage',\n payloadSchema: createProductPayloadSchema,\n label: 'Create Product',\n promptSchema: `create_product payload:\n{ title: string, sku?: string, unitPrice?: string, currencyCode?: string (3-letter ISO), kind?: \"product\"|\"service\", description?: string }`,\n execute: executeCreateProductAction,\n },\n]\n\nexport default inboxActions\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,kCAAkC;AAE3C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,eAAe,2BACb,QACA,KACiF;AACjF,QAAM,OAAO,gBAAgB,GAAG;AAChC,QAAM,UAAU,OAAO;AAEvB,QAAM,cAAuC;AAAA,IAC3C,gBAAgB,KAAK;AAAA,IACrB,UAAU,KAAK;AAAA,IACf,OAAO,QAAQ;AAAA,IACf,aAAa;AAAA,IACb,UAAU;AAAA,EACZ;AAEA,MAAI,QAAQ,IAAK,aAAY,MAAM,QAAQ;AAC3C,MAAI,QAAQ,YAAa,aAAY,cAAc,QAAQ;AAC3D,MAAI,QAAQ,aAAc,aAAY,sBAAsB,QAAQ;AAEpE,QAAM,SAAS,MAAM;AAAA,IACnB;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,MAAI,CAAC,OAAO,WAAW;AACrB,UAAM,IAAI,eAAe,gDAAgD,GAAG;AAAA,EAC9E;AAEA,QAAM,sCAAsC,KAAK,IAAI,OAAO,YAAY,QAAQ,OAAO,OAAO,WAAW;AAAA,IACvG,UAAU,KAAK;AAAA,IACf,gBAAgB,KAAK;AAAA,EACvB,CAAC;AAED,SAAO,EAAE,iBAAiB,OAAO,WAAW,mBAAmB,kBAAkB;AACnF;AAEO,MAAM,eAAwC;AAAA,EACnD;AAAA,IACE,MAAM;AAAA,IACN,iBAAiB;AAAA,IACjB,eAAe;AAAA,IACf,OAAO;AAAA,IACP,cAAc;AAAA;AAAA,IAEd,SAAS;AAAA,EACX;AACF;AAEA,IAAO,wBAAQ;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContactPayloadSchema,
|
|
3
|
+
linkContactPayloadSchema,
|
|
4
|
+
logActivityPayloadSchema,
|
|
5
|
+
draftReplyPayloadSchema
|
|
6
|
+
} from "../inbox_ops/data/validators.js";
|
|
7
|
+
import {
|
|
8
|
+
asHelperContext,
|
|
9
|
+
ExecutionError,
|
|
10
|
+
executeCommand,
|
|
11
|
+
resolveEntityClass,
|
|
12
|
+
resolveCustomerEntityIdByEmail,
|
|
13
|
+
resolveContactIdByNameAndType
|
|
14
|
+
} from "../inbox_ops/lib/executionHelpers.js";
|
|
15
|
+
import { splitPersonName } from "../inbox_ops/lib/contactValidation.js";
|
|
16
|
+
import { findOneWithDecryption, findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
17
|
+
async function executeCreateContactAction(action, ctx) {
|
|
18
|
+
const hCtx = asHelperContext(ctx);
|
|
19
|
+
const payload = action.payload;
|
|
20
|
+
const CustomerEntityClass = resolveEntityClass(hCtx, "CustomerEntity");
|
|
21
|
+
if (payload.email && CustomerEntityClass) {
|
|
22
|
+
const emailLower = payload.email.trim().toLowerCase();
|
|
23
|
+
let existingContact = await findOneWithDecryption(
|
|
24
|
+
hCtx.em,
|
|
25
|
+
CustomerEntityClass,
|
|
26
|
+
{
|
|
27
|
+
primaryEmail: emailLower,
|
|
28
|
+
tenantId: hCtx.tenantId,
|
|
29
|
+
organizationId: hCtx.organizationId,
|
|
30
|
+
deletedAt: null
|
|
31
|
+
},
|
|
32
|
+
void 0,
|
|
33
|
+
{ tenantId: hCtx.tenantId, organizationId: hCtx.organizationId }
|
|
34
|
+
);
|
|
35
|
+
if (!existingContact) {
|
|
36
|
+
const candidates = await findWithDecryption(
|
|
37
|
+
hCtx.em,
|
|
38
|
+
CustomerEntityClass,
|
|
39
|
+
{
|
|
40
|
+
tenantId: hCtx.tenantId,
|
|
41
|
+
organizationId: hCtx.organizationId,
|
|
42
|
+
deletedAt: null
|
|
43
|
+
},
|
|
44
|
+
{ limit: 100, orderBy: { createdAt: "DESC" } },
|
|
45
|
+
{ tenantId: hCtx.tenantId, organizationId: hCtx.organizationId }
|
|
46
|
+
);
|
|
47
|
+
existingContact = candidates.find(
|
|
48
|
+
(e) => e.primaryEmail && e.primaryEmail.toLowerCase() === emailLower
|
|
49
|
+
) ?? null;
|
|
50
|
+
}
|
|
51
|
+
if (existingContact) {
|
|
52
|
+
const isCompany = existingContact.kind === "company";
|
|
53
|
+
return {
|
|
54
|
+
createdEntityId: existingContact.id,
|
|
55
|
+
createdEntityType: isCompany ? "customer_company" : "customer_person",
|
|
56
|
+
matchedEntityId: existingContact.id,
|
|
57
|
+
matchedEntityType: isCompany ? "company" : "person"
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (payload.type === "company") {
|
|
62
|
+
const result2 = await executeCommand(
|
|
63
|
+
hCtx,
|
|
64
|
+
"customers.companies.create",
|
|
65
|
+
{
|
|
66
|
+
organizationId: hCtx.organizationId,
|
|
67
|
+
tenantId: hCtx.tenantId,
|
|
68
|
+
displayName: payload.name,
|
|
69
|
+
legalName: payload.companyName ?? payload.name,
|
|
70
|
+
primaryEmail: payload.email,
|
|
71
|
+
primaryPhone: payload.phone,
|
|
72
|
+
source: payload.source
|
|
73
|
+
}
|
|
74
|
+
);
|
|
75
|
+
if (!result2.entityId) {
|
|
76
|
+
throw new ExecutionError("Company creation did not return an entity ID", 500);
|
|
77
|
+
}
|
|
78
|
+
return { createdEntityId: result2.entityId, createdEntityType: "customer_company" };
|
|
79
|
+
}
|
|
80
|
+
const { firstName, lastName } = splitPersonName(payload.name, payload.email);
|
|
81
|
+
const result = await executeCommand(
|
|
82
|
+
hCtx,
|
|
83
|
+
"customers.people.create",
|
|
84
|
+
{
|
|
85
|
+
organizationId: hCtx.organizationId,
|
|
86
|
+
tenantId: hCtx.tenantId,
|
|
87
|
+
displayName: payload.name,
|
|
88
|
+
firstName,
|
|
89
|
+
lastName,
|
|
90
|
+
primaryEmail: payload.email,
|
|
91
|
+
primaryPhone: payload.phone,
|
|
92
|
+
jobTitle: payload.role,
|
|
93
|
+
source: payload.source
|
|
94
|
+
}
|
|
95
|
+
);
|
|
96
|
+
if (!result.entityId) {
|
|
97
|
+
throw new ExecutionError("Person creation did not return an entity ID", 500);
|
|
98
|
+
}
|
|
99
|
+
return { createdEntityId: result.entityId, createdEntityType: "customer_person" };
|
|
100
|
+
}
|
|
101
|
+
function executeLinkContactAction(action) {
|
|
102
|
+
const payload = action.payload;
|
|
103
|
+
return {
|
|
104
|
+
createdEntityId: payload.contactId,
|
|
105
|
+
createdEntityType: payload.contactType === "company" ? "customer_company" : "customer_person",
|
|
106
|
+
matchedEntityId: payload.contactId,
|
|
107
|
+
matchedEntityType: payload.contactType
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
async function executeLogActivityAction(action, ctx) {
|
|
111
|
+
const hCtx = asHelperContext(ctx);
|
|
112
|
+
let payload = action.payload;
|
|
113
|
+
if (!payload.contactId) {
|
|
114
|
+
const resolved = await resolveContactIdByNameAndType(hCtx, payload.contactName, payload.contactType);
|
|
115
|
+
if (resolved) {
|
|
116
|
+
payload = { ...payload, contactId: resolved };
|
|
117
|
+
} else {
|
|
118
|
+
throw new ExecutionError(
|
|
119
|
+
`log_activity requires contactId \u2014 could not resolve contact "${payload.contactName}" (${payload.contactType})`,
|
|
120
|
+
400
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const result = await executeCommand(
|
|
125
|
+
hCtx,
|
|
126
|
+
"customers.activities.create",
|
|
127
|
+
{
|
|
128
|
+
organizationId: hCtx.organizationId,
|
|
129
|
+
tenantId: hCtx.tenantId,
|
|
130
|
+
entityId: payload.contactId,
|
|
131
|
+
activityType: payload.activityType,
|
|
132
|
+
subject: payload.subject,
|
|
133
|
+
body: payload.body,
|
|
134
|
+
authorUserId: hCtx.userId
|
|
135
|
+
}
|
|
136
|
+
);
|
|
137
|
+
if (!result.activityId) {
|
|
138
|
+
throw new ExecutionError("Activity creation did not return an activity ID", 500);
|
|
139
|
+
}
|
|
140
|
+
return { createdEntityId: result.activityId, createdEntityType: "customer_activity" };
|
|
141
|
+
}
|
|
142
|
+
async function executeDraftReplyAction(action, ctx) {
|
|
143
|
+
const hCtx = asHelperContext(ctx);
|
|
144
|
+
const payload = action.payload;
|
|
145
|
+
const payloadRecord = action.payload;
|
|
146
|
+
const explicitContactId = typeof payloadRecord.contactId === "string" ? payloadRecord.contactId : null;
|
|
147
|
+
const contactId = explicitContactId ?? await resolveCustomerEntityIdByEmail(hCtx, payload.to);
|
|
148
|
+
if (!contactId) {
|
|
149
|
+
throw new ExecutionError(
|
|
150
|
+
`No matching contact found for "${payload.to}". Create the contact first or link an existing one.`,
|
|
151
|
+
400
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
const details = [
|
|
155
|
+
payload.body.trim(),
|
|
156
|
+
"",
|
|
157
|
+
"---",
|
|
158
|
+
`Draft reply target: ${payload.to}`,
|
|
159
|
+
`Subject: ${payload.subject}`,
|
|
160
|
+
payload.context ? `Context: ${payload.context}` : null,
|
|
161
|
+
`InboxOps Proposal: ${action.proposalId}`,
|
|
162
|
+
`InboxOps Action: ${action.id}`
|
|
163
|
+
].filter((line) => typeof line === "string" && line.length > 0).join("\n");
|
|
164
|
+
const result = await executeCommand(
|
|
165
|
+
hCtx,
|
|
166
|
+
"customers.activities.create",
|
|
167
|
+
{
|
|
168
|
+
organizationId: hCtx.organizationId,
|
|
169
|
+
tenantId: hCtx.tenantId,
|
|
170
|
+
entityId: contactId,
|
|
171
|
+
activityType: "email",
|
|
172
|
+
subject: payload.subject,
|
|
173
|
+
body: details,
|
|
174
|
+
authorUserId: hCtx.userId
|
|
175
|
+
}
|
|
176
|
+
);
|
|
177
|
+
if (!result.activityId) {
|
|
178
|
+
throw new ExecutionError("Draft reply activity did not return an activity ID", 500);
|
|
179
|
+
}
|
|
180
|
+
return { createdEntityId: result.activityId, createdEntityType: "customer_activity" };
|
|
181
|
+
}
|
|
182
|
+
const inboxActions = [
|
|
183
|
+
{
|
|
184
|
+
type: "create_contact",
|
|
185
|
+
requiredFeature: "customers.people.manage",
|
|
186
|
+
payloadSchema: createContactPayloadSchema,
|
|
187
|
+
label: "Create Contact",
|
|
188
|
+
promptSchema: `create_contact payload:
|
|
189
|
+
{ type: "person"|"company", name: string, email?: string, phone?: string, companyName?: string, role?: string, source: "inbox_ops" }`,
|
|
190
|
+
promptRules: [
|
|
191
|
+
'For create_contact: always include email when available from the thread. Set source to "inbox_ops", type must be lowercase "person" or "company".',
|
|
192
|
+
`For create_contact with type "person": if the sender's display name is not available in the email header or signature, attempt to derive a human-readable name from the email address (e.g., john.doe@company.com -> "John Doe", m.smith@corp.net -> "M Smith"). If the email address does not contain a derivable name (e.g., info@, noreply@), use the full email address as the name. Always aim to provide both a first and last name when possible.`
|
|
193
|
+
],
|
|
194
|
+
execute: executeCreateContactAction
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
type: "link_contact",
|
|
198
|
+
requiredFeature: "customers.people.manage",
|
|
199
|
+
payloadSchema: linkContactPayloadSchema,
|
|
200
|
+
label: "Link Contact",
|
|
201
|
+
promptSchema: `link_contact payload:
|
|
202
|
+
{ emailAddress: string (email), contactId: uuid, contactType: "person"|"company", contactName: string }`,
|
|
203
|
+
execute: (action) => Promise.resolve(executeLinkContactAction(action))
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
type: "log_activity",
|
|
207
|
+
requiredFeature: "customers.activities.manage",
|
|
208
|
+
payloadSchema: logActivityPayloadSchema,
|
|
209
|
+
label: "Log Activity",
|
|
210
|
+
promptSchema: `log_activity payload:
|
|
211
|
+
{ contactId?: uuid, contactType: "person"|"company", contactName: string, activityType: "email"|"call"|"meeting"|"note", subject: string, body: string }`,
|
|
212
|
+
execute: executeLogActivityAction
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
type: "draft_reply",
|
|
216
|
+
requiredFeature: "inbox_ops.replies.send",
|
|
217
|
+
payloadSchema: draftReplyPayloadSchema,
|
|
218
|
+
label: "Draft Reply",
|
|
219
|
+
promptSchema: `draft_reply payload:
|
|
220
|
+
{ to: string (email), toName?: string, subject: string, body: string, context?: string }`,
|
|
221
|
+
promptRules: ["For draft_reply: include ERP context when available."],
|
|
222
|
+
execute: executeDraftReplyAction
|
|
223
|
+
}
|
|
224
|
+
];
|
|
225
|
+
var inbox_actions_default = inboxActions;
|
|
226
|
+
export {
|
|
227
|
+
inbox_actions_default as default,
|
|
228
|
+
inboxActions
|
|
229
|
+
};
|
|
230
|
+
//# sourceMappingURL=inbox-actions.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/modules/customers/inbox-actions.ts"],
|
|
4
|
+
"sourcesContent": ["import type { InboxActionDefinition, InboxActionExecutionContext } from '@open-mercato/shared/modules/inbox-actions'\nimport {\n createContactPayloadSchema,\n linkContactPayloadSchema,\n logActivityPayloadSchema,\n draftReplyPayloadSchema,\n} from '../inbox_ops/data/validators'\nimport type {\n CreateContactPayload,\n LinkContactPayload,\n LogActivityPayload,\n DraftReplyPayload,\n} from '../inbox_ops/data/validators'\nimport {\n asHelperContext,\n ExecutionError,\n executeCommand,\n resolveEntityClass,\n resolveCustomerEntityIdByEmail,\n resolveContactIdByNameAndType,\n} from '../inbox_ops/lib/executionHelpers'\nimport { splitPersonName } from '../inbox_ops/lib/contactValidation'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\n\n// ---------------------------------------------------------------------------\n// create_contact\n// ---------------------------------------------------------------------------\n\nasync function executeCreateContactAction(\n action: { id: string; proposalId: string; payload: unknown },\n ctx: InboxActionExecutionContext,\n): Promise<{ createdEntityId?: string | null; createdEntityType?: string | null; matchedEntityId?: string | null; matchedEntityType?: string | null }> {\n const hCtx = asHelperContext(ctx)\n const payload = action.payload as CreateContactPayload\n\n const CustomerEntityClass = resolveEntityClass(hCtx, 'CustomerEntity')\n if (payload.email && CustomerEntityClass) {\n const emailLower = payload.email.trim().toLowerCase()\n let existingContact = await findOneWithDecryption(\n hCtx.em,\n CustomerEntityClass,\n {\n primaryEmail: emailLower,\n tenantId: hCtx.tenantId,\n organizationId: hCtx.organizationId,\n deletedAt: null,\n },\n undefined,\n { tenantId: hCtx.tenantId, organizationId: hCtx.organizationId },\n )\n if (!existingContact) {\n const candidates = await findWithDecryption(\n hCtx.em,\n CustomerEntityClass,\n {\n tenantId: hCtx.tenantId,\n organizationId: hCtx.organizationId,\n deletedAt: null,\n },\n { limit: 100, orderBy: { createdAt: 'DESC' } },\n { tenantId: hCtx.tenantId, organizationId: hCtx.organizationId },\n )\n existingContact = candidates.find(\n (e) => e.primaryEmail && e.primaryEmail.toLowerCase() === emailLower,\n ) ?? null\n }\n if (existingContact) {\n const isCompany = existingContact.kind === 'company'\n return {\n createdEntityId: existingContact.id,\n createdEntityType: isCompany ? 'customer_company' : 'customer_person',\n matchedEntityId: existingContact.id,\n matchedEntityType: isCompany ? 'company' : 'person',\n }\n }\n }\n\n if (payload.type === 'company') {\n const result = await executeCommand<Record<string, unknown>, { entityId?: string }>(\n hCtx,\n 'customers.companies.create',\n {\n organizationId: hCtx.organizationId,\n tenantId: hCtx.tenantId,\n displayName: payload.name,\n legalName: payload.companyName ?? payload.name,\n primaryEmail: payload.email,\n primaryPhone: payload.phone,\n source: payload.source,\n },\n )\n if (!result.entityId) {\n throw new ExecutionError('Company creation did not return an entity ID', 500)\n }\n return { createdEntityId: result.entityId, createdEntityType: 'customer_company' }\n }\n\n const { firstName, lastName } = splitPersonName(payload.name, payload.email)\n const result = await executeCommand<Record<string, unknown>, { entityId?: string }>(\n hCtx,\n 'customers.people.create',\n {\n organizationId: hCtx.organizationId,\n tenantId: hCtx.tenantId,\n displayName: payload.name,\n firstName,\n lastName,\n primaryEmail: payload.email,\n primaryPhone: payload.phone,\n jobTitle: payload.role,\n source: payload.source,\n },\n )\n\n if (!result.entityId) {\n throw new ExecutionError('Person creation did not return an entity ID', 500)\n }\n\n return { createdEntityId: result.entityId, createdEntityType: 'customer_person' }\n}\n\n// ---------------------------------------------------------------------------\n// link_contact\n// ---------------------------------------------------------------------------\n\nfunction executeLinkContactAction(\n action: { id: string; proposalId: string; payload: unknown },\n): { createdEntityId?: string | null; createdEntityType?: string | null; matchedEntityId?: string | null; matchedEntityType?: string | null } {\n const payload = action.payload as LinkContactPayload\n return {\n createdEntityId: payload.contactId,\n createdEntityType: payload.contactType === 'company' ? 'customer_company' : 'customer_person',\n matchedEntityId: payload.contactId,\n matchedEntityType: payload.contactType,\n }\n}\n\n// ---------------------------------------------------------------------------\n// log_activity\n// ---------------------------------------------------------------------------\n\nasync function executeLogActivityAction(\n action: { id: string; proposalId: string; payload: unknown },\n ctx: InboxActionExecutionContext,\n): Promise<{ createdEntityId?: string | null; createdEntityType?: string | null }> {\n const hCtx = asHelperContext(ctx)\n let payload = action.payload as LogActivityPayload\n\n if (!payload.contactId) {\n const resolved = await resolveContactIdByNameAndType(hCtx, payload.contactName, payload.contactType)\n if (resolved) {\n payload = { ...payload, contactId: resolved }\n } else {\n throw new ExecutionError(\n `log_activity requires contactId \u2014 could not resolve contact \"${payload.contactName}\" (${payload.contactType})`,\n 400,\n )\n }\n }\n\n const result = await executeCommand<Record<string, unknown>, { activityId?: string }>(\n hCtx,\n 'customers.activities.create',\n {\n organizationId: hCtx.organizationId,\n tenantId: hCtx.tenantId,\n entityId: payload.contactId,\n activityType: payload.activityType,\n subject: payload.subject,\n body: payload.body,\n authorUserId: hCtx.userId,\n },\n )\n\n if (!result.activityId) {\n throw new ExecutionError('Activity creation did not return an activity ID', 500)\n }\n\n return { createdEntityId: result.activityId, createdEntityType: 'customer_activity' }\n}\n\n// ---------------------------------------------------------------------------\n// draft_reply\n// ---------------------------------------------------------------------------\n\nasync function executeDraftReplyAction(\n action: { id: string; proposalId: string; payload: unknown },\n ctx: InboxActionExecutionContext,\n): Promise<{ createdEntityId?: string | null; createdEntityType?: string | null }> {\n const hCtx = asHelperContext(ctx)\n const payload = action.payload as DraftReplyPayload\n const payloadRecord = action.payload as Record<string, unknown>\n const explicitContactId = typeof payloadRecord.contactId === 'string' ? payloadRecord.contactId : null\n const contactId = explicitContactId ?? (await resolveCustomerEntityIdByEmail(hCtx, payload.to))\n\n if (!contactId) {\n throw new ExecutionError(\n `No matching contact found for \"${payload.to}\". Create the contact first or link an existing one.`,\n 400,\n )\n }\n\n const details = [\n payload.body.trim(),\n '',\n '---',\n `Draft reply target: ${payload.to}`,\n `Subject: ${payload.subject}`,\n payload.context ? `Context: ${payload.context}` : null,\n `InboxOps Proposal: ${action.proposalId}`,\n `InboxOps Action: ${action.id}`,\n ]\n .filter((line) => typeof line === 'string' && line.length > 0)\n .join('\\n')\n\n const result = await executeCommand<Record<string, unknown>, { activityId?: string }>(\n hCtx,\n 'customers.activities.create',\n {\n organizationId: hCtx.organizationId,\n tenantId: hCtx.tenantId,\n entityId: contactId,\n activityType: 'email',\n subject: payload.subject,\n body: details,\n authorUserId: hCtx.userId,\n },\n )\n\n if (!result.activityId) {\n throw new ExecutionError('Draft reply activity did not return an activity ID', 500)\n }\n\n return { createdEntityId: result.activityId, createdEntityType: 'customer_activity' }\n}\n\n// ---------------------------------------------------------------------------\n// Exported action definitions\n// ---------------------------------------------------------------------------\n\nexport const inboxActions: InboxActionDefinition[] = [\n {\n type: 'create_contact',\n requiredFeature: 'customers.people.manage',\n payloadSchema: createContactPayloadSchema,\n label: 'Create Contact',\n promptSchema: `create_contact payload:\n{ type: \"person\"|\"company\", name: string, email?: string, phone?: string, companyName?: string, role?: string, source: \"inbox_ops\" }`,\n promptRules: [\n 'For create_contact: always include email when available from the thread. Set source to \"inbox_ops\", type must be lowercase \"person\" or \"company\".',\n 'For create_contact with type \"person\": if the sender\\'s display name is not available in the email header or signature, attempt to derive a human-readable name from the email address (e.g., john.doe@company.com -> \"John Doe\", m.smith@corp.net -> \"M Smith\"). If the email address does not contain a derivable name (e.g., info@, noreply@), use the full email address as the name. Always aim to provide both a first and last name when possible.',\n ],\n execute: executeCreateContactAction,\n },\n {\n type: 'link_contact',\n requiredFeature: 'customers.people.manage',\n payloadSchema: linkContactPayloadSchema,\n label: 'Link Contact',\n promptSchema: `link_contact payload:\n{ emailAddress: string (email), contactId: uuid, contactType: \"person\"|\"company\", contactName: string }`,\n execute: (action) => Promise.resolve(executeLinkContactAction(action)),\n },\n {\n type: 'log_activity',\n requiredFeature: 'customers.activities.manage',\n payloadSchema: logActivityPayloadSchema,\n label: 'Log Activity',\n promptSchema: `log_activity payload:\n{ contactId?: uuid, contactType: \"person\"|\"company\", contactName: string, activityType: \"email\"|\"call\"|\"meeting\"|\"note\", subject: string, body: string }`,\n execute: executeLogActivityAction,\n },\n {\n type: 'draft_reply',\n requiredFeature: 'inbox_ops.replies.send',\n payloadSchema: draftReplyPayloadSchema,\n label: 'Draft Reply',\n promptSchema: `draft_reply payload:\n{ to: string (email), toName?: string, subject: string, body: string, context?: string }`,\n promptRules: ['For draft_reply: include ERP context when available.'],\n execute: executeDraftReplyAction,\n },\n]\n\nexport default inboxActions\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["result"]
|
|
7
|
+
}
|
|
@@ -7,7 +7,8 @@ import {
|
|
|
7
7
|
UnauthorizedError
|
|
8
8
|
} from "../../routeHelpers.js";
|
|
9
9
|
const metadata = {
|
|
10
|
-
GET: { requireAuth: true, requireFeatures: ["inbox_ops.log.view"] }
|
|
10
|
+
GET: { requireAuth: true, requireFeatures: ["inbox_ops.log.view"] },
|
|
11
|
+
DELETE: { requireAuth: true, requireFeatures: ["inbox_ops.proposals.manage"] }
|
|
11
12
|
};
|
|
12
13
|
async function GET(req) {
|
|
13
14
|
try {
|
|
@@ -41,6 +42,36 @@ async function GET(req) {
|
|
|
41
42
|
return NextResponse.json({ error: "Failed to load email" }, { status: 500 });
|
|
42
43
|
}
|
|
43
44
|
}
|
|
45
|
+
async function DELETE(req) {
|
|
46
|
+
try {
|
|
47
|
+
const url = new URL(req.url);
|
|
48
|
+
const id = extractPathSegment(url, "emails");
|
|
49
|
+
if (!id) {
|
|
50
|
+
return NextResponse.json({ error: "Missing email ID" }, { status: 400 });
|
|
51
|
+
}
|
|
52
|
+
const ctx = await resolveRequestContext(req);
|
|
53
|
+
const updated = await ctx.em.nativeUpdate(
|
|
54
|
+
InboxEmail,
|
|
55
|
+
{
|
|
56
|
+
id,
|
|
57
|
+
organizationId: ctx.organizationId,
|
|
58
|
+
tenantId: ctx.tenantId,
|
|
59
|
+
deletedAt: null
|
|
60
|
+
},
|
|
61
|
+
{ deletedAt: /* @__PURE__ */ new Date() }
|
|
62
|
+
);
|
|
63
|
+
if (updated === 0) {
|
|
64
|
+
return NextResponse.json({ error: "Email not found" }, { status: 404 });
|
|
65
|
+
}
|
|
66
|
+
return NextResponse.json({ ok: true });
|
|
67
|
+
} catch (err) {
|
|
68
|
+
if (err instanceof UnauthorizedError) {
|
|
69
|
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
70
|
+
}
|
|
71
|
+
console.error("[inbox_ops:emails:delete] Error:", err);
|
|
72
|
+
return NextResponse.json({ error: "Failed to delete email" }, { status: 500 });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
44
75
|
const openApi = {
|
|
45
76
|
tag: "InboxOps",
|
|
46
77
|
summary: "Email detail",
|
|
@@ -51,10 +82,18 @@ const openApi = {
|
|
|
51
82
|
{ status: 200, description: "Email detail" },
|
|
52
83
|
{ status: 404, description: "Email not found" }
|
|
53
84
|
]
|
|
85
|
+
},
|
|
86
|
+
DELETE: {
|
|
87
|
+
summary: "Soft-delete an inbox email",
|
|
88
|
+
responses: [
|
|
89
|
+
{ status: 200, description: "Email deleted" },
|
|
90
|
+
{ status: 404, description: "Email not found" }
|
|
91
|
+
]
|
|
54
92
|
}
|
|
55
93
|
}
|
|
56
94
|
};
|
|
57
95
|
export {
|
|
96
|
+
DELETE,
|
|
58
97
|
GET,
|
|
59
98
|
metadata,
|
|
60
99
|
openApi
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/inbox_ops/api/emails/%5Bid%5D/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 { InboxEmail } from '../../../data/entities'\nimport {\n resolveRequestContext,\n extractPathSegment,\n UnauthorizedError,\n} from '../../routeHelpers'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['inbox_ops.log.view'] },\n}\n\nexport async function GET(req: Request) {\n try {\n const url = new URL(req.url)\n const id = extractPathSegment(url, 'emails')\n\n if (!id) {\n return NextResponse.json({ error: 'Missing email ID' }, { status: 400 })\n }\n\n const ctx = await resolveRequestContext(req)\n\n const email = await findOneWithDecryption(\n ctx.em,\n InboxEmail,\n {\n id,\n organizationId: ctx.organizationId,\n tenantId: ctx.tenantId,\n deletedAt: null,\n },\n undefined,\n ctx.scope,\n )\n\n if (!email) {\n return NextResponse.json({ error: 'Email not found' }, { status: 404 })\n }\n\n return NextResponse.json({ email })\n } catch (err) {\n if (err instanceof UnauthorizedError) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n console.error('[inbox_ops:emails:detail] Error:', err)\n return NextResponse.json({ error: 'Failed to load email' }, { status: 500 })\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'InboxOps',\n summary: 'Email detail',\n methods: {\n GET: {\n summary: 'Get email detail with parsed thread',\n responses: [\n { status: 200, description: 'Email detail' },\n { status: 404, description: 'Email not found' },\n ],\n },\n },\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAE7B,SAAS,6BAA6B;AACtC,SAAS,kBAAkB;AAC3B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEA,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,oBAAoB,EAAE;
|
|
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 { InboxEmail } from '../../../data/entities'\nimport {\n resolveRequestContext,\n extractPathSegment,\n UnauthorizedError,\n} from '../../routeHelpers'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['inbox_ops.log.view'] },\n DELETE: { requireAuth: true, requireFeatures: ['inbox_ops.proposals.manage'] },\n}\n\nexport async function GET(req: Request) {\n try {\n const url = new URL(req.url)\n const id = extractPathSegment(url, 'emails')\n\n if (!id) {\n return NextResponse.json({ error: 'Missing email ID' }, { status: 400 })\n }\n\n const ctx = await resolveRequestContext(req)\n\n const email = await findOneWithDecryption(\n ctx.em,\n InboxEmail,\n {\n id,\n organizationId: ctx.organizationId,\n tenantId: ctx.tenantId,\n deletedAt: null,\n },\n undefined,\n ctx.scope,\n )\n\n if (!email) {\n return NextResponse.json({ error: 'Email not found' }, { status: 404 })\n }\n\n return NextResponse.json({ email })\n } catch (err) {\n if (err instanceof UnauthorizedError) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n console.error('[inbox_ops:emails:detail] Error:', err)\n return NextResponse.json({ error: 'Failed to load email' }, { status: 500 })\n }\n}\n\nexport async function DELETE(req: Request) {\n try {\n const url = new URL(req.url)\n const id = extractPathSegment(url, 'emails')\n\n if (!id) {\n return NextResponse.json({ error: 'Missing email ID' }, { status: 400 })\n }\n\n const ctx = await resolveRequestContext(req)\n\n const updated = await ctx.em.nativeUpdate(\n InboxEmail,\n {\n id,\n organizationId: ctx.organizationId,\n tenantId: ctx.tenantId,\n deletedAt: null,\n },\n { deletedAt: new Date() },\n )\n\n if (updated === 0) {\n return NextResponse.json({ error: 'Email not found' }, { status: 404 })\n }\n\n return NextResponse.json({ ok: true })\n } catch (err) {\n if (err instanceof UnauthorizedError) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n console.error('[inbox_ops:emails:delete] Error:', err)\n return NextResponse.json({ error: 'Failed to delete email' }, { status: 500 })\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'InboxOps',\n summary: 'Email detail',\n methods: {\n GET: {\n summary: 'Get email detail with parsed thread',\n responses: [\n { status: 200, description: 'Email detail' },\n { status: 404, description: 'Email not found' },\n ],\n },\n DELETE: {\n summary: 'Soft-delete an inbox email',\n responses: [\n { status: 200, description: 'Email deleted' },\n { status: 404, description: 'Email not found' },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { InboxEmail } from "../../data/entities.js";
|
|
4
|
+
import { emitInboxOpsEvent } from "../../events.js";
|
|
5
|
+
import { resolveRequestContext, handleRouteError } from "../routeHelpers.js";
|
|
6
|
+
const metadata = {
|
|
7
|
+
POST: { requireAuth: true, requireFeatures: ["inbox_ops.proposals.manage"] }
|
|
8
|
+
};
|
|
9
|
+
const extractRequestSchema = z.object({
|
|
10
|
+
text: z.string().min(1, "Text is required").max(1e5, "Text exceeds maximum length"),
|
|
11
|
+
title: z.string().max(500).optional(),
|
|
12
|
+
metadata: z.record(z.string(), z.unknown()).optional()
|
|
13
|
+
});
|
|
14
|
+
async function POST(req) {
|
|
15
|
+
try {
|
|
16
|
+
const ctx = await resolveRequestContext(req);
|
|
17
|
+
let body;
|
|
18
|
+
try {
|
|
19
|
+
body = await req.json();
|
|
20
|
+
} catch {
|
|
21
|
+
return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
|
|
22
|
+
}
|
|
23
|
+
const parsed = extractRequestSchema.safeParse(body);
|
|
24
|
+
if (!parsed.success) {
|
|
25
|
+
const errors = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
26
|
+
return NextResponse.json({ error: errors }, { status: 400 });
|
|
27
|
+
}
|
|
28
|
+
const { text, title, metadata: inputMetadata } = parsed.data;
|
|
29
|
+
const maxTextSize = parseInt(process.env.INBOX_OPS_MAX_TEXT_SIZE || "204800", 10);
|
|
30
|
+
const truncatedText = text.slice(0, maxTextSize);
|
|
31
|
+
const email = ctx.em.create(InboxEmail, {
|
|
32
|
+
forwardedByAddress: ctx.userId,
|
|
33
|
+
forwardedByName: null,
|
|
34
|
+
toAddress: "text-extract",
|
|
35
|
+
subject: title || "Text extraction",
|
|
36
|
+
cleanedText: truncatedText,
|
|
37
|
+
rawText: truncatedText,
|
|
38
|
+
receivedAt: /* @__PURE__ */ new Date(),
|
|
39
|
+
status: "received",
|
|
40
|
+
isActive: true,
|
|
41
|
+
organizationId: ctx.organizationId,
|
|
42
|
+
tenantId: ctx.tenantId,
|
|
43
|
+
metadata: {
|
|
44
|
+
...inputMetadata,
|
|
45
|
+
source: "text_extract",
|
|
46
|
+
submittedByUserId: ctx.userId
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
ctx.em.persist(email);
|
|
50
|
+
await ctx.em.flush();
|
|
51
|
+
try {
|
|
52
|
+
await emitInboxOpsEvent("inbox_ops.email.received", {
|
|
53
|
+
emailId: email.id,
|
|
54
|
+
tenantId: ctx.tenantId,
|
|
55
|
+
organizationId: ctx.organizationId,
|
|
56
|
+
forwardedByAddress: ctx.userId,
|
|
57
|
+
subject: title || "Text extraction"
|
|
58
|
+
});
|
|
59
|
+
} catch (eventError) {
|
|
60
|
+
console.error("[inbox_ops:extract] Failed to emit email.received event:", eventError);
|
|
61
|
+
}
|
|
62
|
+
return NextResponse.json({ ok: true, emailId: email.id });
|
|
63
|
+
} catch (err) {
|
|
64
|
+
return handleRouteError(err, "extract text");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const openApi = {
|
|
68
|
+
tag: "InboxOps",
|
|
69
|
+
summary: "Extract actions from raw text",
|
|
70
|
+
methods: {
|
|
71
|
+
POST: {
|
|
72
|
+
summary: "Submit raw text for LLM extraction",
|
|
73
|
+
description: "Creates an InboxEmail record from raw text and triggers the extraction pipeline. The extraction runs asynchronously.",
|
|
74
|
+
responses: [
|
|
75
|
+
{ status: 200, description: "Extraction queued successfully" },
|
|
76
|
+
{ status: 400, description: "Invalid request body" },
|
|
77
|
+
{ status: 401, description: "Unauthorized" }
|
|
78
|
+
]
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
export {
|
|
83
|
+
POST,
|
|
84
|
+
metadata,
|
|
85
|
+
openApi
|
|
86
|
+
};
|
|
87
|
+
//# sourceMappingURL=route.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 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;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -17,7 +17,12 @@ async function POST(req) {
|
|
|
17
17
|
const ctx = await resolveRequestContext(req);
|
|
18
18
|
const proposal = await resolveProposal(new URL(req.url), ctx);
|
|
19
19
|
if (isErrorResponse(proposal)) return proposal;
|
|
20
|
-
|
|
20
|
+
let body;
|
|
21
|
+
try {
|
|
22
|
+
body = await req.json();
|
|
23
|
+
} catch {
|
|
24
|
+
return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
|
|
25
|
+
}
|
|
21
26
|
const parsed = translateProposalSchema.safeParse(body);
|
|
22
27
|
if (!parsed.success) {
|
|
23
28
|
return NextResponse.json({ error: "Invalid request", details: parsed.error.issues }, { status: 400 });
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../src/modules/inbox_ops/api/proposals/%5Bid%5D/translate/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { InboxProposalAction } from '../../../../data/entities'\nimport type { ProposalTranslationEntry } from '../../../../data/entities'\nimport { translateProposalSchema } from '../../../../data/validators'\nimport { translateProposalContent } from '../../../../lib/translationProvider'\nimport {\n resolveRequestContext,\n resolveProposal,\n handleRouteError,\n isErrorResponse,\n} from '../../../routeHelpers'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['inbox_ops.proposals.view'] },\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
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAE7B,SAAS,0BAA0B;AACnC,SAAS,2BAA2B;AAEpC,SAAS,+BAA+B;AACxC,SAAS,gCAAgC;AACzC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEA,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,0BAA0B,EAAE;AAC3E;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,
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { InboxProposalAction } from '../../../../data/entities'\nimport type { ProposalTranslationEntry } from '../../../../data/entities'\nimport { translateProposalSchema } from '../../../../data/validators'\nimport { translateProposalContent } from '../../../../lib/translationProvider'\nimport {\n resolveRequestContext,\n resolveProposal,\n handleRouteError,\n isErrorResponse,\n} from '../../../routeHelpers'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['inbox_ops.proposals.view'] },\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 let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })\n }\n const parsed = translateProposalSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid request', details: parsed.error.issues }, { status: 400 })\n }\n\n const { targetLocale } = parsed.data\n const proposalLanguage = proposal.workingLanguage || 'en'\n\n if (proposalLanguage === targetLocale) {\n return NextResponse.json({ error: 'Proposal is already in the requested language' }, { status: 400 })\n }\n\n // Return cached translation if available\n const cached = proposal.translations?.[targetLocale]\n if (cached) {\n return NextResponse.json({ translation: cached, cached: true })\n }\n\n // Load actions for translation\n const actions = await findWithDecryption(\n ctx.em,\n InboxProposalAction,\n { proposalId: proposal.id, organizationId: ctx.organizationId, tenantId: ctx.tenantId, deletedAt: null },\n { orderBy: { sortOrder: 'ASC' } },\n ctx.scope,\n )\n\n const actionDescriptions: Record<string, string> = {}\n for (const action of actions) {\n actionDescriptions[action.id] = action.description\n }\n\n const result = await translateProposalContent({\n summary: proposal.summary,\n actionDescriptions,\n sourceLanguage: proposalLanguage,\n targetLocale,\n })\n\n const entry: ProposalTranslationEntry = {\n summary: result.summary,\n actions: result.actions,\n translatedAt: new Date().toISOString(),\n }\n\n // Cache the translation on the proposal entity\n const translations = proposal.translations || {}\n translations[targetLocale] = entry\n proposal.translations = translations\n await ctx.em.flush()\n\n return NextResponse.json({ translation: entry, cached: false })\n } catch (err) {\n return handleRouteError(err, 'translate proposal')\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'InboxOps',\n summary: 'Translate proposal',\n methods: {\n POST: {\n summary: 'Translate proposal content',\n description: 'Translates the proposal summary and action descriptions to the target locale. Results are cached.',\n responses: [\n { status: 200, description: 'Translation result' },\n { status: 400, description: 'Invalid target locale or same language' },\n { status: 404, description: 'Proposal not found' },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAoB;AAE7B,SAAS,0BAA0B;AACnC,SAAS,2BAA2B;AAEpC,SAAS,+BAA+B;AACxC,SAAS,gCAAgC;AACzC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEA,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,0BAA0B,EAAE;AAC3E;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,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;AACA,UAAM,SAAS,wBAAwB,UAAU,IAAI;AACrD,QAAI,CAAC,OAAO,SAAS;AACnB,aAAO,aAAa,KAAK,EAAE,OAAO,mBAAmB,SAAS,OAAO,MAAM,OAAO,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IACtG;AAEA,UAAM,EAAE,aAAa,IAAI,OAAO;AAChC,UAAM,mBAAmB,SAAS,mBAAmB;AAErD,QAAI,qBAAqB,cAAc;AACrC,aAAO,aAAa,KAAK,EAAE,OAAO,gDAAgD,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IACtG;AAGA,UAAM,SAAS,SAAS,eAAe,YAAY;AACnD,QAAI,QAAQ;AACV,aAAO,aAAa,KAAK,EAAE,aAAa,QAAQ,QAAQ,KAAK,CAAC;AAAA,IAChE;AAGA,UAAM,UAAU,MAAM;AAAA,MACpB,IAAI;AAAA,MACJ;AAAA,MACA,EAAE,YAAY,SAAS,IAAI,gBAAgB,IAAI,gBAAgB,UAAU,IAAI,UAAU,WAAW,KAAK;AAAA,MACvG,EAAE,SAAS,EAAE,WAAW,MAAM,EAAE;AAAA,MAChC,IAAI;AAAA,IACN;AAEA,UAAM,qBAA6C,CAAC;AACpD,eAAW,UAAU,SAAS;AAC5B,yBAAmB,OAAO,EAAE,IAAI,OAAO;AAAA,IACzC;AAEA,UAAM,SAAS,MAAM,yBAAyB;AAAA,MAC5C,SAAS,SAAS;AAAA,MAClB;AAAA,MACA,gBAAgB;AAAA,MAChB;AAAA,IACF,CAAC;AAED,UAAM,QAAkC;AAAA,MACtC,SAAS,OAAO;AAAA,MAChB,SAAS,OAAO;AAAA,MAChB,eAAc,oBAAI,KAAK,GAAE,YAAY;AAAA,IACvC;AAGA,UAAM,eAAe,SAAS,gBAAgB,CAAC;AAC/C,iBAAa,YAAY,IAAI;AAC7B,aAAS,eAAe;AACxB,UAAM,IAAI,GAAG,MAAM;AAEnB,WAAO,aAAa,KAAK,EAAE,aAAa,OAAO,QAAQ,MAAM,CAAC;AAAA,EAChE,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,aAAa;AAAA,MACb,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,qBAAqB;AAAA,QACjD,EAAE,QAAQ,KAAK,aAAa,yCAAyC;AAAA,QACrE,EAAE,QAAQ,KAAK,aAAa,qBAAqB;AAAA,MACnD;AAAA,IACF;AAAA,EACF;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/inbox_ops/api/proposals/counts/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { InboxProposal } from '../../../data/entities'\nimport { resolveRequestContext, UnauthorizedError } from '../../routeHelpers'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['inbox_ops.proposals.view'] },\n}\n\nexport async function GET(req: Request) {\n try {\n const ctx = await resolveRequestContext(req)\n\n const scope = {\n organizationId: ctx.organizationId,\n tenantId: ctx.tenantId,\n deletedAt: null,\n isActive: true,\n }\n\n const [pending, partial, accepted, rejected] = await Promise.all([\n ctx.em.count(InboxProposal, { ...scope, status: 'pending' }),\n ctx.em.count(InboxProposal, { ...scope, status: 'partial' }),\n ctx.em.count(InboxProposal, { ...scope, status: 'accepted' }),\n ctx.em.count(InboxProposal, { ...scope, status: 'rejected' }),\n ])\n\n return NextResponse.json({ pending, partial, accepted, rejected })\n } catch (err) {\n if (err instanceof UnauthorizedError) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n console.error('[inbox_ops:proposals:counts] Error:', err)\n return NextResponse.json({ error: 'Failed to get counts' }, { status: 500 })\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'InboxOps',\n summary: 'Proposal counts',\n methods: {\n GET: {\n summary: 'Get proposal status counts',\n description: 'Returns counts by status for tab badges',\n responses: [\n { status: 200, description: 'Status counts object' },\n ],\n },\n },\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAE7B,SAAS,qBAAqB;AAC9B,SAAS,uBAAuB,yBAAyB;AAElD,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,0BAA0B,EAAE;AAC1E;AAEA,eAAsB,IAAI,KAAc;AACtC,MAAI;AACF,UAAM,MAAM,MAAM,sBAAsB,GAAG;AAE3C,UAAM,QAAQ;AAAA,MACZ,gBAAgB,IAAI;AAAA,MACpB,UAAU,IAAI;AAAA,MACd,WAAW;AAAA,MACX,UAAU;AAAA,IACZ;
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { InboxProposal } from '../../../data/entities'\nimport { resolveRequestContext, UnauthorizedError } from '../../routeHelpers'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['inbox_ops.proposals.view'] },\n}\n\nexport async function GET(req: Request) {\n try {\n const ctx = await resolveRequestContext(req)\n\n const scope = {\n organizationId: ctx.organizationId,\n tenantId: ctx.tenantId,\n deletedAt: null,\n isActive: true,\n }\n\n // em.count() is safe here \u2014 filter fields (status, organizationId, tenantId,\n // deletedAt, isActive) are not encrypted, so decryption helpers are not needed.\n const [pending, partial, accepted, rejected] = await Promise.all([\n ctx.em.count(InboxProposal, { ...scope, status: 'pending' }),\n ctx.em.count(InboxProposal, { ...scope, status: 'partial' }),\n ctx.em.count(InboxProposal, { ...scope, status: 'accepted' }),\n ctx.em.count(InboxProposal, { ...scope, status: 'rejected' }),\n ])\n\n return NextResponse.json({ pending, partial, accepted, rejected })\n } catch (err) {\n if (err instanceof UnauthorizedError) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n console.error('[inbox_ops:proposals:counts] Error:', err)\n return NextResponse.json({ error: 'Failed to get counts' }, { status: 500 })\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'InboxOps',\n summary: 'Proposal counts',\n methods: {\n GET: {\n summary: 'Get proposal status counts',\n description: 'Returns counts by status for tab badges',\n responses: [\n { status: 200, description: 'Status counts object' },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAoB;AAE7B,SAAS,qBAAqB;AAC9B,SAAS,uBAAuB,yBAAyB;AAElD,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,0BAA0B,EAAE;AAC1E;AAEA,eAAsB,IAAI,KAAc;AACtC,MAAI;AACF,UAAM,MAAM,MAAM,sBAAsB,GAAG;AAE3C,UAAM,QAAQ;AAAA,MACZ,gBAAgB,IAAI;AAAA,MACpB,UAAU,IAAI;AAAA,MACd,WAAW;AAAA,MACX,UAAU;AAAA,IACZ;AAIA,UAAM,CAAC,SAAS,SAAS,UAAU,QAAQ,IAAI,MAAM,QAAQ,IAAI;AAAA,MAC/D,IAAI,GAAG,MAAM,eAAe,EAAE,GAAG,OAAO,QAAQ,UAAU,CAAC;AAAA,MAC3D,IAAI,GAAG,MAAM,eAAe,EAAE,GAAG,OAAO,QAAQ,UAAU,CAAC;AAAA,MAC3D,IAAI,GAAG,MAAM,eAAe,EAAE,GAAG,OAAO,QAAQ,WAAW,CAAC;AAAA,MAC5D,IAAI,GAAG,MAAM,eAAe,EAAE,GAAG,OAAO,QAAQ,WAAW,CAAC;AAAA,IAC9D,CAAC;AAED,WAAO,aAAa,KAAK,EAAE,SAAS,SAAS,UAAU,SAAS,CAAC;AAAA,EACnE,SAAS,KAAK;AACZ,QAAI,eAAe,mBAAmB;AACpC,aAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IACrE;AACA,YAAQ,MAAM,uCAAuC,GAAG;AACxD,WAAO,aAAa,KAAK,EAAE,OAAO,uBAAuB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC7E;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,KAAK;AAAA,MACH,SAAS;AAAA,MACT,aAAa;AAAA,MACb,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,uBAAuB;AAAA,MACrD;AAAA,IACF;AAAA,EACF;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|