@open-mercato/core 0.6.2-develop.3439.1.94b9d9733c → 0.6.2-develop.3446.1.bd060c6017
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/.turbo/turbo-build.log +1 -1
- package/dist/generated/entities/entity_index_row/index.js +2 -0
- package/dist/generated/entities/entity_index_row/index.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +1 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/helpers/integration/queue-runner.js +58 -0
- package/dist/helpers/integration/queue-runner.js.map +7 -0
- package/dist/helpers/integration/queue.js +67 -0
- package/dist/helpers/integration/queue.js.map +7 -0
- package/dist/helpers/integration/salesUi.js +33 -15
- package/dist/helpers/integration/salesUi.js.map +2 -2
- package/dist/modules/customers/ai-agents.js +5 -2
- package/dist/modules/customers/ai-agents.js.map +2 -2
- package/dist/modules/customers/ai-tools/deals-pack.js +52 -8
- package/dist/modules/customers/ai-tools/deals-pack.js.map +2 -2
- package/dist/modules/customers/ai-tools/types.js.map +2 -2
- package/dist/modules/customers/api/people/[id]/route.js +10 -1
- package/dist/modules/customers/api/people/[id]/route.js.map +2 -2
- package/dist/modules/query_index/data/entities.js +13 -1
- package/dist/modules/query_index/data/entities.js.map +2 -2
- package/dist/modules/query_index/migrations/Migration20260518162054_query_index.js +43 -0
- package/dist/modules/query_index/migrations/Migration20260518162054_query_index.js.map +7 -0
- package/generated/entities/entity_index_row/index.ts +1 -0
- package/generated/entity-fields-registry.ts +1 -0
- package/package.json +8 -8
- package/src/helpers/integration/queue-runner.ts +72 -0
- package/src/helpers/integration/queue.ts +70 -0
- package/src/helpers/integration/salesUi.ts +38 -23
- package/src/modules/attachments/i18n/de.json +13 -0
- package/src/modules/attachments/i18n/en.json +13 -0
- package/src/modules/attachments/i18n/es.json +13 -0
- package/src/modules/attachments/i18n/pl.json +13 -0
- package/src/modules/customers/ai-agents.ts +4 -1
- package/src/modules/customers/ai-tools/deals-pack.ts +68 -10
- package/src/modules/customers/ai-tools/types.ts +6 -0
- package/src/modules/customers/api/people/[id]/route.ts +10 -1
- package/src/modules/query_index/data/entities.ts +12 -0
- package/src/modules/query_index/migrations/.snapshot-open-mercato.json +792 -343
- package/src/modules/query_index/migrations/Migration20260518162054_query_index.ts +43 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.6.2-develop.
|
|
3
|
+
"version": "0.6.2-develop.3446.1.bd060c6017",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -232,7 +232,7 @@
|
|
|
232
232
|
"@mikro-orm/postgresql": "^7.0.15",
|
|
233
233
|
"@xyflow/react": "^12.10.2",
|
|
234
234
|
"ai": "^6.0.177",
|
|
235
|
-
"date-fns": "
|
|
235
|
+
"date-fns": "4.1.0",
|
|
236
236
|
"date-fns-tz": "^3.2.0",
|
|
237
237
|
"html-to-text": "^9.0.5",
|
|
238
238
|
"mammoth": "^1.9.0",
|
|
@@ -242,16 +242,16 @@
|
|
|
242
242
|
"zod": "^4.3.6"
|
|
243
243
|
},
|
|
244
244
|
"peerDependencies": {
|
|
245
|
-
"@open-mercato/ai-assistant": "0.6.2-develop.
|
|
246
|
-
"@open-mercato/shared": "0.6.2-develop.
|
|
247
|
-
"@open-mercato/ui": "0.6.2-develop.
|
|
245
|
+
"@open-mercato/ai-assistant": "0.6.2-develop.3446.1.bd060c6017",
|
|
246
|
+
"@open-mercato/shared": "0.6.2-develop.3446.1.bd060c6017",
|
|
247
|
+
"@open-mercato/ui": "0.6.2-develop.3446.1.bd060c6017",
|
|
248
248
|
"react": "^19.0.0",
|
|
249
249
|
"react-dom": "^19.0.0"
|
|
250
250
|
},
|
|
251
251
|
"devDependencies": {
|
|
252
|
-
"@open-mercato/ai-assistant": "0.6.2-develop.
|
|
253
|
-
"@open-mercato/shared": "0.6.2-develop.
|
|
254
|
-
"@open-mercato/ui": "0.6.2-develop.
|
|
252
|
+
"@open-mercato/ai-assistant": "0.6.2-develop.3446.1.bd060c6017",
|
|
253
|
+
"@open-mercato/shared": "0.6.2-develop.3446.1.bd060c6017",
|
|
254
|
+
"@open-mercato/ui": "0.6.2-develop.3446.1.bd060c6017",
|
|
255
255
|
"@testing-library/dom": "^10.4.1",
|
|
256
256
|
"@testing-library/jest-dom": "^6.9.1",
|
|
257
257
|
"@testing-library/react": "^16.3.1",
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import { fileURLToPath } from 'node:url'
|
|
3
|
+
import { bootstrapFromAppRoot } from '@open-mercato/shared/lib/bootstrap/dynamicLoader'
|
|
4
|
+
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
5
|
+
import { createQueue } from '@open-mercato/queue'
|
|
6
|
+
|
|
7
|
+
export type QueueDrainRunnerOptions = {
|
|
8
|
+
appRoot: string
|
|
9
|
+
jobLimit: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function drainQueueFromAppRoot(
|
|
13
|
+
queueName: string,
|
|
14
|
+
options: QueueDrainRunnerOptions,
|
|
15
|
+
): Promise<number> {
|
|
16
|
+
const data = await bootstrapFromAppRoot(options.appRoot)
|
|
17
|
+
const worker = data.modules
|
|
18
|
+
.flatMap((module) => module.workers ?? [])
|
|
19
|
+
.find((entry) => entry.queue === queueName)
|
|
20
|
+
if (!worker) return 0
|
|
21
|
+
|
|
22
|
+
const container = await createRequestContainer()
|
|
23
|
+
const queue = createQueue(queueName, 'local', {
|
|
24
|
+
baseDir: path.resolve(options.appRoot, '.mercato/queue'),
|
|
25
|
+
concurrency: 1,
|
|
26
|
+
})
|
|
27
|
+
const resolve = <T = unknown>(name: string): T => container.resolve(name) as T
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
let processedJobs = 0
|
|
31
|
+
while (true) {
|
|
32
|
+
const result = await queue.process(
|
|
33
|
+
async (job, ctx) => {
|
|
34
|
+
await Promise.resolve(worker.handler(job, { ...ctx, resolve }))
|
|
35
|
+
},
|
|
36
|
+
{ limit: options.jobLimit },
|
|
37
|
+
)
|
|
38
|
+
const handled = result.processed + result.failed
|
|
39
|
+
processedJobs += handled
|
|
40
|
+
if (handled === 0) return processedJobs
|
|
41
|
+
}
|
|
42
|
+
} finally {
|
|
43
|
+
await queue.close()
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function runQueueDrainFromEnv(): Promise<void> {
|
|
48
|
+
const queueName = process.env.OM_INTEGRATION_QUEUE_NAME?.trim()
|
|
49
|
+
const appRoot = process.env.OM_INTEGRATION_APP_ROOT?.trim()
|
|
50
|
+
const jobLimit = Number.parseInt(process.env.OM_INTEGRATION_QUEUE_JOB_LIMIT || '100', 10) || 100
|
|
51
|
+
if (!queueName || !appRoot) throw new Error('Missing queue drain environment')
|
|
52
|
+
|
|
53
|
+
const processed = await drainQueueFromAppRoot(queueName, {
|
|
54
|
+
appRoot: path.resolve(appRoot),
|
|
55
|
+
jobLimit,
|
|
56
|
+
})
|
|
57
|
+
console.log(JSON.stringify({ processed }))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const currentFile = fileURLToPath(import.meta.url)
|
|
61
|
+
const invokedFile = process.argv[1] ? path.resolve(process.argv[1]) : null
|
|
62
|
+
|
|
63
|
+
if (invokedFile === currentFile) {
|
|
64
|
+
runQueueDrainFromEnv()
|
|
65
|
+
.then(() => {
|
|
66
|
+
process.exit(0)
|
|
67
|
+
})
|
|
68
|
+
.catch((error) => {
|
|
69
|
+
console.error(error)
|
|
70
|
+
process.exit(1)
|
|
71
|
+
})
|
|
72
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
|
+
import { createRequire } from 'node:module'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { drainQueueFromAppRoot } from './queue-runner'
|
|
5
|
+
|
|
6
|
+
type DrainQueueOptions = {
|
|
7
|
+
appRoot?: string
|
|
8
|
+
jobLimit?: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const DEFAULT_JOB_LIMIT = 100
|
|
12
|
+
|
|
13
|
+
function resolveAppRoot(input?: string): string {
|
|
14
|
+
return path.resolve(input?.trim() || process.env.OM_TEST_APP_ROOT?.trim() || path.resolve(process.cwd(), 'apps/mercato'))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function drainQueueInCurrentProcess(queueName: string, options: Required<DrainQueueOptions>): Promise<number> {
|
|
18
|
+
return drainQueueFromAppRoot(queueName, options)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function resolveAppRunnerPath(appRoot: string): string {
|
|
22
|
+
const requireFromApp = createRequire(path.join(appRoot, 'package.json'))
|
|
23
|
+
return requireFromApp.resolve('@open-mercato/core/helpers/integration/queue-runner')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function drainQueueInAppProcess(queueName: string, options: Required<DrainQueueOptions>): Promise<number> {
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
const child = spawn(process.execPath, [resolveAppRunnerPath(options.appRoot)], {
|
|
29
|
+
cwd: options.appRoot,
|
|
30
|
+
env: {
|
|
31
|
+
...process.env,
|
|
32
|
+
OM_INTEGRATION_APP_ROOT: options.appRoot,
|
|
33
|
+
OM_INTEGRATION_QUEUE_NAME: queueName,
|
|
34
|
+
OM_INTEGRATION_QUEUE_JOB_LIMIT: String(options.jobLimit),
|
|
35
|
+
QUEUE_BASE_DIR: path.resolve(options.appRoot, '.mercato/queue'),
|
|
36
|
+
},
|
|
37
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
let stdout = ''
|
|
41
|
+
let stderr = ''
|
|
42
|
+
child.stdout.on('data', (chunk) => { stdout += chunk.toString() })
|
|
43
|
+
child.stderr.on('data', (chunk) => { stderr += chunk.toString() })
|
|
44
|
+
child.on('error', reject)
|
|
45
|
+
child.on('close', (code) => {
|
|
46
|
+
if (code !== 0) {
|
|
47
|
+
reject(new Error(`Queue drain failed for "${queueName}" in ${options.appRoot} (exit ${code}).\n${stderr || stdout}`))
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
const lastLine = stdout.trim().split(/\r?\n/).reverse().find((line) => line.trim().startsWith('{')) ?? '{}'
|
|
51
|
+
try {
|
|
52
|
+
const parsed = JSON.parse(lastLine) as { processed?: unknown }
|
|
53
|
+
resolve(Number(parsed.processed ?? 0))
|
|
54
|
+
} catch {
|
|
55
|
+
resolve(0)
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function drainIntegrationQueue(queueName: string, options: DrainQueueOptions = {}): Promise<number> {
|
|
62
|
+
const resolved = {
|
|
63
|
+
appRoot: resolveAppRoot(options.appRoot),
|
|
64
|
+
jobLimit: options.jobLimit ?? DEFAULT_JOB_LIMIT,
|
|
65
|
+
}
|
|
66
|
+
if (process.env.OM_TEST_APP_ROOT?.trim()) {
|
|
67
|
+
return drainQueueInAppProcess(queueName, resolved)
|
|
68
|
+
}
|
|
69
|
+
return drainQueueInCurrentProcess(queueName, resolved)
|
|
70
|
+
}
|
|
@@ -1011,16 +1011,22 @@ export async function addCustomLine(page: Page, options: AddLineOptions): Promis
|
|
|
1011
1011
|
await dialog.getByRole('textbox', { name: '1' }).fill(String(options.quantity));
|
|
1012
1012
|
|
|
1013
1013
|
if (options.taxClassName) {
|
|
1014
|
-
|
|
1015
|
-
const taxClassTrigger = dialog
|
|
1016
|
-
.locator('[role="combobox"]')
|
|
1017
|
-
.filter({ hasText: /No tax class selected/i })
|
|
1018
|
-
.first();
|
|
1014
|
+
const taxClassTrigger = dialog.locator('[data-crud-field-id="taxRateId"] [role="combobox"]').first();
|
|
1019
1015
|
if ((await taxClassTrigger.count()) > 0) {
|
|
1020
|
-
await taxClassTrigger.
|
|
1021
|
-
const
|
|
1022
|
-
|
|
1023
|
-
|
|
1016
|
+
await expect(taxClassTrigger).toBeEnabled({ timeout: TEST_WAIT_TIMEOUT_MS });
|
|
1017
|
+
const selectedLabel = options.taxClassName.split('•')[0].trim();
|
|
1018
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
1019
|
+
await taxClassTrigger.click();
|
|
1020
|
+
const opt = page.getByRole('option', { name: options.taxClassName, exact: true }).first();
|
|
1021
|
+
await opt.waitFor({ state: 'visible', timeout: 3_000 });
|
|
1022
|
+
await opt.click();
|
|
1023
|
+
const selected = await taxClassTrigger
|
|
1024
|
+
.filter({ hasText: new RegExp(escapeRegExp(selectedLabel), 'i') })
|
|
1025
|
+
.waitFor({ state: 'visible', timeout: 1_500 })
|
|
1026
|
+
.then(() => true, () => false);
|
|
1027
|
+
if (selected) break;
|
|
1028
|
+
}
|
|
1029
|
+
await expect(taxClassTrigger).toContainText(selectedLabel, { timeout: TEST_WAIT_TIMEOUT_MS });
|
|
1024
1030
|
}
|
|
1025
1031
|
}
|
|
1026
1032
|
|
|
@@ -1104,21 +1110,30 @@ export async function addAdjustment(page: Page, options: AddAdjustmentOptions):
|
|
|
1104
1110
|
TEST_WAIT_TIMEOUT_MS,
|
|
1105
1111
|
);
|
|
1106
1112
|
await expect(kindTrigger).toBeEnabled({ timeout: TEST_WAIT_TIMEOUT_MS });
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
.
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1113
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
1114
|
+
const currentLabel = await kindTrigger.textContent().catch(() => '');
|
|
1115
|
+
if (currentLabel?.includes(kindLabel)) return;
|
|
1116
|
+
const expanded = await kindTrigger.getAttribute('aria-expanded').catch(() => null);
|
|
1117
|
+
if (expanded === 'true') {
|
|
1118
|
+
await page.keyboard.press('Escape').catch(() => {});
|
|
1119
|
+
}
|
|
1120
|
+
await kindTrigger.click();
|
|
1121
|
+
const kindOption = page.getByRole('option', { name: new RegExp(`^${escapeRegExp(kindLabel)}$`, 'i') }).first();
|
|
1122
|
+
const optionVisible = await kindOption
|
|
1123
|
+
.waitFor({ state: 'visible', timeout: TEST_WAIT_TIMEOUT_MS })
|
|
1124
|
+
.then(() => true, () => false);
|
|
1125
|
+
if (optionVisible) {
|
|
1126
|
+
await kindOption.click({ force: true });
|
|
1127
|
+
} else {
|
|
1128
|
+
// Fallback: drive selection via keyboard when modal-backdrop click interception
|
|
1129
|
+
// hides the rendered option. Brought forward from develop.
|
|
1130
|
+
await page.keyboard.type(kindLabel.charAt(0));
|
|
1131
|
+
await page.waitForTimeout(150);
|
|
1132
|
+
await page.keyboard.press('Enter');
|
|
1133
|
+
}
|
|
1134
|
+
await expect(kindTrigger).toContainText(kindLabel, { timeout: 2_000 }).catch(() => {});
|
|
1120
1135
|
}
|
|
1121
|
-
await expect(kindTrigger).toContainText(kindLabel, { timeout:
|
|
1136
|
+
await expect(kindTrigger).toContainText(kindLabel, { timeout: 5_000 });
|
|
1122
1137
|
};
|
|
1123
1138
|
|
|
1124
1139
|
// Select kind first — the Radix Select interaction can trigger CrudForm re-renders that
|
|
@@ -122,6 +122,18 @@
|
|
|
122
122
|
"attachments.partitions.form.ocrModelOptions.gpt-4o": "GPT-4o (Empfohlen)",
|
|
123
123
|
"attachments.partitions.form.ocrModelOptions.gpt-4o-mini": "GPT-4o Mini (Schneller, günstiger)",
|
|
124
124
|
"attachments.partitions.form.publicLabel": "Öffentlich zugänglich",
|
|
125
|
+
"attachments.partitions.form.s3BucketLabel": "Bucket",
|
|
126
|
+
"attachments.partitions.form.s3BucketOverrideLabel": "Bucket überschreiben (optional)",
|
|
127
|
+
"attachments.partitions.form.s3ConfigTitle": "S3-Konfiguration",
|
|
128
|
+
"attachments.partitions.form.s3CredsPrefixLabel": "Präfix für Zugangsdaten-Umgebungsvariablen",
|
|
129
|
+
"attachments.partitions.form.s3CredsSourceLabel": "Quelle der Zugangsdaten",
|
|
130
|
+
"attachments.partitions.form.s3EndpointHelp": "Für AWS S3 leer lassen. Erforderlich für MinIO, DigitalOcean Spaces usw.",
|
|
131
|
+
"attachments.partitions.form.s3EndpointLabel": "Benutzerdefinierter Endpoint",
|
|
132
|
+
"attachments.partitions.form.s3EndpointOverrideLabel": "Benutzerdefinierten Endpoint überschreiben (optional)",
|
|
133
|
+
"attachments.partitions.form.s3ForcePathStyleLabel": "Path-Style erzwingen (für MinIO erforderlich)",
|
|
134
|
+
"attachments.partitions.form.s3RegionLabel": "Region",
|
|
135
|
+
"attachments.partitions.form.s3RegionOverrideLabel": "Region überschreiben (optional)",
|
|
136
|
+
"attachments.partitions.form.storageDriverLabel": "Speichertreiber",
|
|
125
137
|
"attachments.partitions.form.titleLabel": "Titel",
|
|
126
138
|
"attachments.partitions.form.titlePlaceholder": "z. B. Marketing-Assets",
|
|
127
139
|
"attachments.partitions.locked.description": "Anhang-Partitionen werden durch Umgebungsstandardwerte verwaltet, solange der Demo- oder Onboarding-Modus aktiv ist.",
|
|
@@ -139,6 +151,7 @@
|
|
|
139
151
|
"attachments.partitions.table.private": "Privat",
|
|
140
152
|
"attachments.partitions.table.public": "Öffentlich",
|
|
141
153
|
"attachments.partitions.table.search": "Partitionen suchen…",
|
|
154
|
+
"attachments.partitions.table.storageDriver": "Speicher",
|
|
142
155
|
"attachments.partitions.table.title": "Titel",
|
|
143
156
|
"attachments.partitions.table.visibility": "Sichtbarkeit",
|
|
144
157
|
"attachments.partitions.title": "Anhang-Partitionen",
|
|
@@ -122,6 +122,18 @@
|
|
|
122
122
|
"attachments.partitions.form.ocrModelOptions.gpt-4o": "GPT-4o (Recommended)",
|
|
123
123
|
"attachments.partitions.form.ocrModelOptions.gpt-4o-mini": "GPT-4o Mini (Faster, Lower Cost)",
|
|
124
124
|
"attachments.partitions.form.publicLabel": "Publicly accessible",
|
|
125
|
+
"attachments.partitions.form.s3BucketLabel": "Bucket",
|
|
126
|
+
"attachments.partitions.form.s3BucketOverrideLabel": "Bucket override (optional)",
|
|
127
|
+
"attachments.partitions.form.s3ConfigTitle": "S3 Configuration",
|
|
128
|
+
"attachments.partitions.form.s3CredsPrefixLabel": "Credentials Env Prefix",
|
|
129
|
+
"attachments.partitions.form.s3CredsSourceLabel": "Credentials source",
|
|
130
|
+
"attachments.partitions.form.s3EndpointHelp": "Leave empty for AWS S3. Required for MinIO, DigitalOcean Spaces, etc.",
|
|
131
|
+
"attachments.partitions.form.s3EndpointLabel": "Custom Endpoint",
|
|
132
|
+
"attachments.partitions.form.s3EndpointOverrideLabel": "Custom endpoint override (optional)",
|
|
133
|
+
"attachments.partitions.form.s3ForcePathStyleLabel": "Force Path Style (required for MinIO)",
|
|
134
|
+
"attachments.partitions.form.s3RegionLabel": "Region",
|
|
135
|
+
"attachments.partitions.form.s3RegionOverrideLabel": "Region override (optional)",
|
|
136
|
+
"attachments.partitions.form.storageDriverLabel": "Storage Driver",
|
|
125
137
|
"attachments.partitions.form.titleLabel": "Title",
|
|
126
138
|
"attachments.partitions.form.titlePlaceholder": "e.g. Marketing assets",
|
|
127
139
|
"attachments.partitions.locked.description": "Attachment partitions are managed by environment defaults while demo or onboarding mode is enabled.",
|
|
@@ -139,6 +151,7 @@
|
|
|
139
151
|
"attachments.partitions.table.private": "Private",
|
|
140
152
|
"attachments.partitions.table.public": "Public",
|
|
141
153
|
"attachments.partitions.table.search": "Search partitions…",
|
|
154
|
+
"attachments.partitions.table.storageDriver": "Storage",
|
|
142
155
|
"attachments.partitions.table.title": "Title",
|
|
143
156
|
"attachments.partitions.table.visibility": "Visibility",
|
|
144
157
|
"attachments.partitions.title": "Attachment partitions",
|
|
@@ -122,6 +122,18 @@
|
|
|
122
122
|
"attachments.partitions.form.ocrModelOptions.gpt-4o": "GPT-4o (Recomendado)",
|
|
123
123
|
"attachments.partitions.form.ocrModelOptions.gpt-4o-mini": "GPT-4o Mini (Más rápido, menor costo)",
|
|
124
124
|
"attachments.partitions.form.publicLabel": "Acceso público",
|
|
125
|
+
"attachments.partitions.form.s3BucketLabel": "Bucket",
|
|
126
|
+
"attachments.partitions.form.s3BucketOverrideLabel": "Sobrescribir bucket (opcional)",
|
|
127
|
+
"attachments.partitions.form.s3ConfigTitle": "Configuración S3",
|
|
128
|
+
"attachments.partitions.form.s3CredsPrefixLabel": "Prefijo de variables de entorno de credenciales",
|
|
129
|
+
"attachments.partitions.form.s3CredsSourceLabel": "Origen de credenciales",
|
|
130
|
+
"attachments.partitions.form.s3EndpointHelp": "Déjelo vacío para AWS S3. Es obligatorio para MinIO, DigitalOcean Spaces, etc.",
|
|
131
|
+
"attachments.partitions.form.s3EndpointLabel": "Endpoint personalizado",
|
|
132
|
+
"attachments.partitions.form.s3EndpointOverrideLabel": "Sobrescribir endpoint personalizado (opcional)",
|
|
133
|
+
"attachments.partitions.form.s3ForcePathStyleLabel": "Forzar Path Style (obligatorio para MinIO)",
|
|
134
|
+
"attachments.partitions.form.s3RegionLabel": "Región",
|
|
135
|
+
"attachments.partitions.form.s3RegionOverrideLabel": "Sobrescribir región (opcional)",
|
|
136
|
+
"attachments.partitions.form.storageDriverLabel": "Controlador de almacenamiento",
|
|
125
137
|
"attachments.partitions.form.titleLabel": "Título",
|
|
126
138
|
"attachments.partitions.form.titlePlaceholder": "ej. Recursos de marketing",
|
|
127
139
|
"attachments.partitions.locked.description": "Las particiones de archivos adjuntos están administradas por valores predeterminados del entorno mientras el modo demo o de incorporación está activo.",
|
|
@@ -139,6 +151,7 @@
|
|
|
139
151
|
"attachments.partitions.table.private": "Privado",
|
|
140
152
|
"attachments.partitions.table.public": "Público",
|
|
141
153
|
"attachments.partitions.table.search": "Buscar particiones…",
|
|
154
|
+
"attachments.partitions.table.storageDriver": "Almacenamiento",
|
|
142
155
|
"attachments.partitions.table.title": "Título",
|
|
143
156
|
"attachments.partitions.table.visibility": "Visibilidad",
|
|
144
157
|
"attachments.partitions.title": "Particiones de archivos adjuntos",
|
|
@@ -122,6 +122,18 @@
|
|
|
122
122
|
"attachments.partitions.form.ocrModelOptions.gpt-4o": "GPT-4o (zalecany)",
|
|
123
123
|
"attachments.partitions.form.ocrModelOptions.gpt-4o-mini": "GPT-4o Mini (szybszy, niższy koszt)",
|
|
124
124
|
"attachments.partitions.form.publicLabel": "Publicznie dostępna",
|
|
125
|
+
"attachments.partitions.form.s3BucketLabel": "Bucket",
|
|
126
|
+
"attachments.partitions.form.s3BucketOverrideLabel": "Nadpisanie bucketu (opcjonalne)",
|
|
127
|
+
"attachments.partitions.form.s3ConfigTitle": "Konfiguracja S3",
|
|
128
|
+
"attachments.partitions.form.s3CredsPrefixLabel": "Prefiks zmiennych środowiskowych poświadczeń",
|
|
129
|
+
"attachments.partitions.form.s3CredsSourceLabel": "Źródło poświadczeń",
|
|
130
|
+
"attachments.partitions.form.s3EndpointHelp": "Pozostaw puste dla AWS S3. Wymagane dla MinIO, DigitalOcean Spaces itp.",
|
|
131
|
+
"attachments.partitions.form.s3EndpointLabel": "Niestandardowy endpoint",
|
|
132
|
+
"attachments.partitions.form.s3EndpointOverrideLabel": "Nadpisanie niestandardowego endpointu (opcjonalne)",
|
|
133
|
+
"attachments.partitions.form.s3ForcePathStyleLabel": "Wymuś Path Style (wymagane dla MinIO)",
|
|
134
|
+
"attachments.partitions.form.s3RegionLabel": "Region",
|
|
135
|
+
"attachments.partitions.form.s3RegionOverrideLabel": "Nadpisanie regionu (opcjonalne)",
|
|
136
|
+
"attachments.partitions.form.storageDriverLabel": "Sterownik magazynu",
|
|
125
137
|
"attachments.partitions.form.titleLabel": "Tytuł",
|
|
126
138
|
"attachments.partitions.form.titlePlaceholder": "np. Zasoby marketingowe",
|
|
127
139
|
"attachments.partitions.locked.description": "Partycje załączników są zarządzane przez domyślne zmienne środowiskowe, gdy włączony jest tryb demonstracyjny lub onboardingowy.",
|
|
@@ -139,6 +151,7 @@
|
|
|
139
151
|
"attachments.partitions.table.private": "Prywatna",
|
|
140
152
|
"attachments.partitions.table.public": "Publiczna",
|
|
141
153
|
"attachments.partitions.table.search": "Szukaj partycji…",
|
|
154
|
+
"attachments.partitions.table.storageDriver": "Magazyn",
|
|
142
155
|
"attachments.partitions.table.title": "Tytuł",
|
|
143
156
|
"attachments.partitions.table.visibility": "Widoczność",
|
|
144
157
|
"attachments.partitions.title": "Partycje załączników",
|
|
@@ -281,6 +281,7 @@ const agent: AiAgentDefinition = {
|
|
|
281
281
|
executionMode: 'chat',
|
|
282
282
|
acceptedMediaTypes: ['image', 'pdf', 'file'],
|
|
283
283
|
requiredFeatures: [...REQUIRED_FEATURES],
|
|
284
|
+
taskPlan: { enabled: true },
|
|
284
285
|
readOnly: false,
|
|
285
286
|
// Default for write-capable agents: every mutation must be confirmed by
|
|
286
287
|
// the operator. Per-tenant override can downgrade to `read-only` to lock
|
|
@@ -339,7 +340,7 @@ const DEAL_ANALYZER_PROMPT_SECTIONS: DealAnalyzerPromptSection[] = [
|
|
|
339
340
|
content: [
|
|
340
341
|
'SCOPE',
|
|
341
342
|
'Stay inside the customers module. Respect tenant and organization isolation.',
|
|
342
|
-
'ALWAYS call customers.analyze_deals as your FIRST tool
|
|
343
|
+
'ALWAYS call customers.analyze_deals as your FIRST domain tool — do not skip this.',
|
|
343
344
|
'Reason about stalled deals: any deal with no activity for more than 14 days',
|
|
344
345
|
'is considered stalled. For each stalled deal with a value greater than $5,000',
|
|
345
346
|
'propose a stage move via customers.update_deal_stage.',
|
|
@@ -448,6 +449,7 @@ function buildDealAnalyzerPrepareStep() {
|
|
|
448
449
|
'customers.list_activities',
|
|
449
450
|
'search.hybrid_search',
|
|
450
451
|
'meta.describe_agent',
|
|
452
|
+
'meta.update_task_plan',
|
|
451
453
|
],
|
|
452
454
|
}
|
|
453
455
|
}
|
|
@@ -466,6 +468,7 @@ const dealAnalyzer: AiAgentDefinition = {
|
|
|
466
468
|
executionMode: 'chat',
|
|
467
469
|
executionEngine: 'stream-text',
|
|
468
470
|
allowRuntimeOverride: true,
|
|
471
|
+
taskPlan: { enabled: true },
|
|
469
472
|
readOnly: false,
|
|
470
473
|
mutationPolicy: 'confirm-required',
|
|
471
474
|
requiredFeatures: ['customers.deals.view'],
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* the spec's residual N+1 budget; deeper aggregation can earn a first-class
|
|
15
15
|
* API later without touching the AI surface.
|
|
16
16
|
*/
|
|
17
|
-
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
17
|
+
import type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'
|
|
18
18
|
import { z } from 'zod'
|
|
19
19
|
import { defineApiBackedAiTool } from '@open-mercato/ai-assistant/modules/ai_assistant/lib/api-backed-tool'
|
|
20
20
|
import {
|
|
@@ -390,6 +390,15 @@ function recordVersionFromUpdatedAt(updatedAt: Date | null | undefined): string
|
|
|
390
390
|
return value.toISOString()
|
|
391
391
|
}
|
|
392
392
|
|
|
393
|
+
function titleStatus(value: string | null | undefined): string | undefined {
|
|
394
|
+
if (!value) return undefined
|
|
395
|
+
return value
|
|
396
|
+
.split(/[_\s-]+/)
|
|
397
|
+
.filter(Boolean)
|
|
398
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
399
|
+
.join(' ')
|
|
400
|
+
}
|
|
401
|
+
|
|
393
402
|
async function loadDealWithStage(
|
|
394
403
|
em: EntityManager,
|
|
395
404
|
ctx: CustomersToolContext,
|
|
@@ -401,7 +410,7 @@ async function loadDealWithStage(
|
|
|
401
410
|
const deal = await findOneWithDecryption<CustomerDeal>(
|
|
402
411
|
em,
|
|
403
412
|
CustomerDeal,
|
|
404
|
-
where as
|
|
413
|
+
where as FilterQuery<CustomerDeal>,
|
|
405
414
|
undefined,
|
|
406
415
|
buildScope(ctx, tenantId),
|
|
407
416
|
)
|
|
@@ -410,6 +419,26 @@ async function loadDealWithStage(
|
|
|
410
419
|
return deal
|
|
411
420
|
}
|
|
412
421
|
|
|
422
|
+
async function loadPipelineStage(
|
|
423
|
+
em: EntityManager,
|
|
424
|
+
ctx: CustomersToolContext,
|
|
425
|
+
tenantId: string,
|
|
426
|
+
stageId: string,
|
|
427
|
+
organizationId: string,
|
|
428
|
+
): Promise<CustomerPipelineStage | null> {
|
|
429
|
+
return findOneWithDecryption<CustomerPipelineStage>(
|
|
430
|
+
em,
|
|
431
|
+
CustomerPipelineStage,
|
|
432
|
+
{
|
|
433
|
+
id: stageId,
|
|
434
|
+
tenantId,
|
|
435
|
+
organizationId,
|
|
436
|
+
},
|
|
437
|
+
undefined,
|
|
438
|
+
buildScope(ctx, tenantId),
|
|
439
|
+
)
|
|
440
|
+
}
|
|
441
|
+
|
|
413
442
|
const updateDealStageTool: CustomersAiToolDefinition = {
|
|
414
443
|
name: 'customers.update_deal_stage',
|
|
415
444
|
displayName: 'Update deal stage',
|
|
@@ -425,14 +454,47 @@ const updateDealStageTool: CustomersAiToolDefinition = {
|
|
|
425
454
|
const em = resolveEm(ctx)
|
|
426
455
|
const deal = await loadDealWithStage(em, ctx, tenantId, input.dealId)
|
|
427
456
|
if (!deal) return null
|
|
457
|
+
let afterStatus = deal.status ?? null
|
|
458
|
+
let afterPipelineStageId = deal.pipelineStageId ?? null
|
|
459
|
+
let afterPipelineStageLabel = deal.pipelineStage ?? null
|
|
460
|
+
if (input.toPipelineStageId) {
|
|
461
|
+
const organizationId = deal.organizationId ?? ctx.organizationId ?? null
|
|
462
|
+
const stage = organizationId
|
|
463
|
+
? await loadPipelineStage(em, ctx, tenantId, input.toPipelineStageId, organizationId)
|
|
464
|
+
: null
|
|
465
|
+
afterPipelineStageId = input.toPipelineStageId
|
|
466
|
+
afterPipelineStageLabel = stage?.label ?? input.toPipelineStageId
|
|
467
|
+
} else if (input.toStage) {
|
|
468
|
+
afterStatus = input.toStage
|
|
469
|
+
}
|
|
470
|
+
const beforeStatus = deal.status ?? null
|
|
471
|
+
const beforePipelineStageId = deal.pipelineStageId ?? null
|
|
472
|
+
const beforePipelineStageLabel = deal.pipelineStage ?? beforePipelineStageId
|
|
428
473
|
return {
|
|
429
474
|
recordId: deal.id,
|
|
430
475
|
entityType: 'customers.deal',
|
|
431
476
|
recordVersion: recordVersionFromUpdatedAt(deal.updatedAt),
|
|
432
477
|
before: {
|
|
433
|
-
status:
|
|
434
|
-
|
|
435
|
-
|
|
478
|
+
status: beforeStatus,
|
|
479
|
+
pipelineStageId: beforePipelineStageId,
|
|
480
|
+
},
|
|
481
|
+
after: {
|
|
482
|
+
status: afterStatus,
|
|
483
|
+
pipelineStageId: afterPipelineStageId,
|
|
484
|
+
},
|
|
485
|
+
display: {
|
|
486
|
+
fieldLabels: {
|
|
487
|
+
status: 'Status',
|
|
488
|
+
pipelineStageId: 'Pipeline stage',
|
|
489
|
+
},
|
|
490
|
+
before: {
|
|
491
|
+
...(beforeStatus ? { status: titleStatus(beforeStatus) } : {}),
|
|
492
|
+
...(beforePipelineStageLabel ? { pipelineStageId: beforePipelineStageLabel } : {}),
|
|
493
|
+
},
|
|
494
|
+
after: {
|
|
495
|
+
...(afterStatus ? { status: titleStatus(afterStatus) } : {}),
|
|
496
|
+
...(afterPipelineStageLabel ? { pipelineStageId: afterPipelineStageLabel } : {}),
|
|
497
|
+
},
|
|
436
498
|
},
|
|
437
499
|
}
|
|
438
500
|
},
|
|
@@ -461,11 +523,7 @@ const updateDealStageTool: CustomersAiToolDefinition = {
|
|
|
461
523
|
organizationId,
|
|
462
524
|
}
|
|
463
525
|
if (input.toPipelineStageId) {
|
|
464
|
-
const stage = await em.
|
|
465
|
-
id: input.toPipelineStageId,
|
|
466
|
-
tenantId,
|
|
467
|
-
organizationId,
|
|
468
|
-
})
|
|
526
|
+
const stage = await loadPipelineStage(em, ctx, tenantId, input.toPipelineStageId, organizationId)
|
|
469
527
|
if (!stage) {
|
|
470
528
|
throw new Error('Pipeline stage not found.')
|
|
471
529
|
}
|
|
@@ -34,6 +34,12 @@ export interface CustomersToolLoadBeforeSingleRecord {
|
|
|
34
34
|
entityType: string
|
|
35
35
|
recordVersion: string | null
|
|
36
36
|
before: Record<string, unknown>
|
|
37
|
+
after?: Record<string, unknown>
|
|
38
|
+
display?: {
|
|
39
|
+
fieldLabels?: Record<string, string>
|
|
40
|
+
before?: Record<string, unknown>
|
|
41
|
+
after?: Record<string, unknown>
|
|
42
|
+
}
|
|
37
43
|
}
|
|
38
44
|
|
|
39
45
|
export interface CustomersAiToolDefinition<TInput = unknown, TOutput = unknown> {
|
|
@@ -452,7 +452,16 @@ export async function GET(_req: Request, ctx: { params?: { id?: string } }) {
|
|
|
452
452
|
})
|
|
453
453
|
const em = (container.resolve('em') as EntityManager)
|
|
454
454
|
|
|
455
|
-
const person = await findOneWithDecryption(
|
|
455
|
+
const person = await findOneWithDecryption(
|
|
456
|
+
em,
|
|
457
|
+
CustomerEntity,
|
|
458
|
+
{ id: parse.data.id, kind: 'person', deletedAt: null },
|
|
459
|
+
{},
|
|
460
|
+
{
|
|
461
|
+
tenantId: scope?.tenantId ?? auth.tenantId ?? null,
|
|
462
|
+
organizationId: scope?.selectedId ?? auth.orgId ?? null,
|
|
463
|
+
},
|
|
464
|
+
)
|
|
456
465
|
profiler.mark('person_loaded', { found: !!person })
|
|
457
466
|
if (!person) {
|
|
458
467
|
statusCode = 404
|
|
@@ -33,6 +33,10 @@ import { Entity, Index, PrimaryKey, Property, Unique } from '@mikro-orm/decorato
|
|
|
33
33
|
`create index "entity_indexes_customer_company_profile_tenant_doc_idx" on "entity_indexes" ("tenant_id", "entity_id") include ("doc") where deleted_at is null and entity_type = 'customers:customer_company_profile' and organization_id is null and tenant_id is not null`,
|
|
34
34
|
})
|
|
35
35
|
@Index({ name: 'entity_indexes_type_tenant_idx', properties: ['entityType', 'tenantId'] })
|
|
36
|
+
@Unique({
|
|
37
|
+
name: 'entity_indexes_type_entity_org_coalesced_unique',
|
|
38
|
+
properties: ['entityType', 'entityId', 'organizationIdCoalesced'],
|
|
39
|
+
})
|
|
36
40
|
export class EntityIndexRow {
|
|
37
41
|
@PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })
|
|
38
42
|
id!: string
|
|
@@ -51,6 +55,14 @@ export class EntityIndexRow {
|
|
|
51
55
|
@Index({ name: 'entity_indexes_org_idx' })
|
|
52
56
|
organizationId?: string | null
|
|
53
57
|
|
|
58
|
+
@Property({
|
|
59
|
+
name: 'organization_id_coalesced',
|
|
60
|
+
type: 'uuid',
|
|
61
|
+
generated: (cols) => `(coalesce(${cols.organizationId}, '00000000-0000-0000-0000-000000000000'::uuid)) stored`,
|
|
62
|
+
hidden: true,
|
|
63
|
+
})
|
|
64
|
+
organizationIdCoalesced!: string
|
|
65
|
+
|
|
54
66
|
@Property({ name: 'tenant_id', type: 'uuid', nullable: true })
|
|
55
67
|
tenantId?: string | null
|
|
56
68
|
|