@open-mercato/core 0.5.1-develop.3036.f02c281f23 → 0.5.1-develop.3043.1a796c3920

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +13 -1
  3. package/dist/helpers/integration/api.js +29 -16
  4. package/dist/helpers/integration/api.js.map +2 -2
  5. package/dist/helpers/integration/auth.js +11 -6
  6. package/dist/helpers/integration/auth.js.map +3 -3
  7. package/dist/modules/auth/commands/roles.js +9 -12
  8. package/dist/modules/auth/commands/roles.js.map +2 -2
  9. package/dist/modules/catalog/ai-agents-context.js +147 -0
  10. package/dist/modules/catalog/ai-agents-context.js.map +7 -0
  11. package/dist/modules/catalog/ai-agents.js +383 -0
  12. package/dist/modules/catalog/ai-agents.js.map +7 -0
  13. package/dist/modules/catalog/ai-tools/_shared.js +318 -0
  14. package/dist/modules/catalog/ai-tools/_shared.js.map +7 -0
  15. package/dist/modules/catalog/ai-tools/authoring-pack.js +391 -0
  16. package/dist/modules/catalog/ai-tools/authoring-pack.js.map +7 -0
  17. package/dist/modules/catalog/ai-tools/categories-pack.js +167 -0
  18. package/dist/modules/catalog/ai-tools/categories-pack.js.map +7 -0
  19. package/dist/modules/catalog/ai-tools/configuration-pack.js +120 -0
  20. package/dist/modules/catalog/ai-tools/configuration-pack.js.map +7 -0
  21. package/dist/modules/catalog/ai-tools/media-tags-pack.js +107 -0
  22. package/dist/modules/catalog/ai-tools/media-tags-pack.js.map +7 -0
  23. package/dist/modules/catalog/ai-tools/merchandising-pack.js +429 -0
  24. package/dist/modules/catalog/ai-tools/merchandising-pack.js.map +7 -0
  25. package/dist/modules/catalog/ai-tools/mutation-pack.js +576 -0
  26. package/dist/modules/catalog/ai-tools/mutation-pack.js.map +7 -0
  27. package/dist/modules/catalog/ai-tools/prices-offers-pack.js +208 -0
  28. package/dist/modules/catalog/ai-tools/prices-offers-pack.js.map +7 -0
  29. package/dist/modules/catalog/ai-tools/products-pack.js +298 -0
  30. package/dist/modules/catalog/ai-tools/products-pack.js.map +7 -0
  31. package/dist/modules/catalog/ai-tools/stats-pack.js +57 -0
  32. package/dist/modules/catalog/ai-tools/stats-pack.js.map +7 -0
  33. package/dist/modules/catalog/ai-tools/types.js +10 -0
  34. package/dist/modules/catalog/ai-tools/types.js.map +7 -0
  35. package/dist/modules/catalog/ai-tools/variants-pack.js +75 -0
  36. package/dist/modules/catalog/ai-tools/variants-pack.js.map +7 -0
  37. package/dist/modules/catalog/ai-tools.js +28 -0
  38. package/dist/modules/catalog/ai-tools.js.map +7 -0
  39. package/dist/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.js +466 -0
  40. package/dist/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.js.map +7 -0
  41. package/dist/modules/catalog/backend/catalog/products/page.js +7 -1
  42. package/dist/modules/catalog/backend/catalog/products/page.js.map +2 -2
  43. package/dist/modules/catalog/components/CatalogStatsCard.js +91 -0
  44. package/dist/modules/catalog/components/CatalogStatsCard.js.map +7 -0
  45. package/dist/modules/catalog/components/products/ProductsDataTable.js +23 -3
  46. package/dist/modules/catalog/components/products/ProductsDataTable.js.map +2 -2
  47. package/dist/modules/catalog/events.js +7 -4
  48. package/dist/modules/catalog/events.js.map +2 -2
  49. package/dist/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.client.js +59 -0
  50. package/dist/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.client.js.map +7 -0
  51. package/dist/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.js +17 -0
  52. package/dist/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.js.map +7 -0
  53. package/dist/modules/catalog/widgets/injection/product-seo/widget.client.js +1 -1
  54. package/dist/modules/catalog/widgets/injection/product-seo/widget.client.js.map +2 -2
  55. package/dist/modules/catalog/widgets/injection-table.js +13 -1
  56. package/dist/modules/catalog/widgets/injection-table.js.map +2 -2
  57. package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.client.js +94 -0
  58. package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.client.js.map +7 -0
  59. package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.js +17 -0
  60. package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.js.map +7 -0
  61. package/dist/modules/customer_accounts/widgets/injection-table.js +9 -0
  62. package/dist/modules/customer_accounts/widgets/injection-table.js.map +2 -2
  63. package/dist/modules/customers/ai-agents-context.js +96 -0
  64. package/dist/modules/customers/ai-agents-context.js.map +7 -0
  65. package/dist/modules/customers/ai-agents.js +244 -0
  66. package/dist/modules/customers/ai-agents.js.map +7 -0
  67. package/dist/modules/customers/ai-tools/activities-tasks-pack.js +1015 -0
  68. package/dist/modules/customers/ai-tools/activities-tasks-pack.js.map +7 -0
  69. package/dist/modules/customers/ai-tools/addresses-tags-pack.js +134 -0
  70. package/dist/modules/customers/ai-tools/addresses-tags-pack.js.map +7 -0
  71. package/dist/modules/customers/ai-tools/companies-pack.js +249 -0
  72. package/dist/modules/customers/ai-tools/companies-pack.js.map +7 -0
  73. package/dist/modules/customers/ai-tools/deals-pack.js +348 -0
  74. package/dist/modules/customers/ai-tools/deals-pack.js.map +7 -0
  75. package/dist/modules/customers/ai-tools/people-pack.js +261 -0
  76. package/dist/modules/customers/ai-tools/people-pack.js.map +7 -0
  77. package/dist/modules/customers/ai-tools/settings-pack.js +102 -0
  78. package/dist/modules/customers/ai-tools/settings-pack.js.map +7 -0
  79. package/dist/modules/customers/ai-tools/types.js +10 -0
  80. package/dist/modules/customers/ai-tools/types.js.map +7 -0
  81. package/dist/modules/customers/ai-tools.js +20 -0
  82. package/dist/modules/customers/ai-tools.js.map +7 -0
  83. package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.js +469 -0
  84. package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.js.map +7 -0
  85. package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.js +17 -0
  86. package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.js.map +7 -0
  87. package/dist/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.client.js +117 -0
  88. package/dist/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.client.js.map +7 -0
  89. package/dist/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.js +17 -0
  90. package/dist/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.js.map +7 -0
  91. package/dist/modules/customers/widgets/injection-table.js +26 -0
  92. package/dist/modules/customers/widgets/injection-table.js.map +7 -0
  93. package/dist/modules/inbox_ops/ai-tools.js +4 -0
  94. package/dist/modules/inbox_ops/ai-tools.js.map +2 -2
  95. package/dist/modules/inbox_ops/lib/llmProvider.js +52 -7
  96. package/dist/modules/inbox_ops/lib/llmProvider.js.map +2 -2
  97. package/dist/modules/notifications/setup.js +13 -0
  98. package/dist/modules/notifications/setup.js.map +7 -0
  99. package/jest.config.cjs +1 -0
  100. package/jest.setup.ts +18 -0
  101. package/package.json +5 -3
  102. package/src/helpers/integration/api.ts +38 -16
  103. package/src/helpers/integration/auth.ts +13 -6
  104. package/src/modules/auth/commands/roles.ts +10 -12
  105. package/src/modules/catalog/AGENTS.md +11 -0
  106. package/src/modules/catalog/ai-agents-context.ts +239 -0
  107. package/src/modules/catalog/ai-agents.ts +525 -0
  108. package/src/modules/catalog/ai-tools/_shared.ts +487 -0
  109. package/src/modules/catalog/ai-tools/authoring-pack.ts +600 -0
  110. package/src/modules/catalog/ai-tools/categories-pack.ts +192 -0
  111. package/src/modules/catalog/ai-tools/configuration-pack.ts +218 -0
  112. package/src/modules/catalog/ai-tools/media-tags-pack.ts +127 -0
  113. package/src/modules/catalog/ai-tools/merchandising-pack.ts +608 -0
  114. package/src/modules/catalog/ai-tools/mutation-pack.ts +761 -0
  115. package/src/modules/catalog/ai-tools/prices-offers-pack.ts +376 -0
  116. package/src/modules/catalog/ai-tools/products-pack.ts +387 -0
  117. package/src/modules/catalog/ai-tools/stats-pack.ts +84 -0
  118. package/src/modules/catalog/ai-tools/types.ts +81 -0
  119. package/src/modules/catalog/ai-tools/variants-pack.ts +147 -0
  120. package/src/modules/catalog/ai-tools.ts +78 -0
  121. package/src/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.tsx +597 -0
  122. package/src/modules/catalog/backend/catalog/products/page.tsx +23 -2
  123. package/src/modules/catalog/components/CatalogStatsCard.tsx +118 -0
  124. package/src/modules/catalog/components/products/ProductsDataTable.tsx +54 -6
  125. package/src/modules/catalog/events.ts +7 -4
  126. package/src/modules/catalog/i18n/de.json +17 -0
  127. package/src/modules/catalog/i18n/en.json +17 -0
  128. package/src/modules/catalog/i18n/es.json +17 -0
  129. package/src/modules/catalog/i18n/pl.json +17 -0
  130. package/src/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.client.tsx +109 -0
  131. package/src/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.ts +29 -0
  132. package/src/modules/catalog/widgets/injection/product-seo/widget.client.tsx +1 -1
  133. package/src/modules/catalog/widgets/injection-table.ts +12 -0
  134. package/src/modules/customer_accounts/i18n/de.json +5 -0
  135. package/src/modules/customer_accounts/i18n/en.json +5 -0
  136. package/src/modules/customer_accounts/i18n/es.json +5 -0
  137. package/src/modules/customer_accounts/i18n/pl.json +5 -0
  138. package/src/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.client.tsx +136 -0
  139. package/src/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.ts +43 -0
  140. package/src/modules/customer_accounts/widgets/injection-table.ts +9 -0
  141. package/src/modules/customers/AGENTS.md +13 -0
  142. package/src/modules/customers/ai-agents-context.ts +150 -0
  143. package/src/modules/customers/ai-agents.ts +355 -0
  144. package/src/modules/customers/ai-tools/activities-tasks-pack.ts +1248 -0
  145. package/src/modules/customers/ai-tools/addresses-tags-pack.ts +145 -0
  146. package/src/modules/customers/ai-tools/companies-pack.ts +362 -0
  147. package/src/modules/customers/ai-tools/deals-pack.ts +505 -0
  148. package/src/modules/customers/ai-tools/people-pack.ts +369 -0
  149. package/src/modules/customers/ai-tools/settings-pack.ts +121 -0
  150. package/src/modules/customers/ai-tools/types.ts +76 -0
  151. package/src/modules/customers/ai-tools.ts +34 -0
  152. package/src/modules/customers/i18n/de.json +25 -0
  153. package/src/modules/customers/i18n/en.json +25 -0
  154. package/src/modules/customers/i18n/es.json +25 -0
  155. package/src/modules/customers/i18n/pl.json +25 -0
  156. package/src/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.tsx +580 -0
  157. package/src/modules/customers/widgets/injection/ai-assistant-trigger/widget.ts +36 -0
  158. package/src/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.client.tsx +191 -0
  159. package/src/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.ts +37 -0
  160. package/src/modules/customers/widgets/injection-table.ts +41 -0
  161. package/src/modules/inbox_ops/ai-tools.ts +4 -0
  162. package/src/modules/inbox_ops/lib/llmProvider.ts +83 -7
  163. package/src/modules/notifications/setup.ts +11 -0
@@ -0,0 +1,429 @@
1
+ import { z } from "zod";
2
+ import { findOneWithDecryption, findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
3
+ import { SortDir } from "@open-mercato/shared/lib/query/types";
4
+ import { E } from "../../../generated/entities.ids.generated.js";
5
+ import { Attachment } from "@open-mercato/core/modules/attachments/data/entities";
6
+ import {
7
+ CatalogProduct,
8
+ CatalogProductCategory,
9
+ CatalogProductCategoryAssignment,
10
+ CatalogProductPrice,
11
+ CatalogProductTag,
12
+ CatalogProductTagAssignment
13
+ } from "../data/entities.js";
14
+ import { assertTenantScope } from "./types.js";
15
+ import {
16
+ buildProductBundle,
17
+ buildScope,
18
+ listPriceKindsCore,
19
+ resolveAttributeSchema,
20
+ resolveEm,
21
+ toPriceNumeric,
22
+ toProductSummary
23
+ } from "./_shared.js";
24
+ const CATALOG_PRODUCT_ENTITY = "catalog:catalog_product";
25
+ function resolveSearchService(ctx) {
26
+ try {
27
+ return ctx.container.resolve("searchService");
28
+ } catch {
29
+ return null;
30
+ }
31
+ }
32
+ const searchProductsInput = z.object({
33
+ q: z.string().trim().optional().describe('Optional fulltext query (title / subtitle / sku / handle). Omit or leave empty (or do NOT pass it at all) to list all products. NEVER use "*", "**", or "%" \u2014 they are not wildcards and will be discarded.'),
34
+ limit: z.number().int().min(1).max(100).optional().describe("Page size. Default 50, hard maximum 100. Results are paginated: when `total` exceeds `limit + offset`, call again with the next `offset` instead of asking for more than 100 rows."),
35
+ offset: z.number().int().min(0).optional().describe("Rows to skip for pagination (default 0). Combine with `limit` to fetch subsequent pages \u2014 for example offset=100, limit=100 fetches rows 101..200."),
36
+ categoryId: z.string().optional().describe("Restrict to products assigned to this catalog category UUID. Only use a category ID returned by a previous tool call \u2014 do NOT guess. Empty string is treated the same as omitting the field."),
37
+ priceMin: z.number().optional().describe('Lower-bound (inclusive) on the gross unit price. OMIT this field when you do not want a lower bound \u2014 do NOT pass 0 as "no minimum", because 0 is a real bound that combined with priceMax=0 will return only free products.'),
38
+ priceMax: z.number().optional().describe('Upper-bound (inclusive) on the gross unit price. OMIT this field when you do not want an upper bound \u2014 do NOT pass 0 as "no maximum", because 0 is a real bound that will exclude every priced product.'),
39
+ tags: z.array(z.string()).optional().describe("Tag labels or slugs (any-match) the product carries. Omit or pass an empty array to skip the tag filter."),
40
+ active: z.boolean().optional().describe("When true, only active products are returned. Omit to include inactive products as well.")
41
+ }).passthrough();
42
+ async function queryProductsWithFilters(em, ctx, tenantId, input, restrictToIds) {
43
+ const limit = input.limit ?? 50;
44
+ const offset = input.offset ?? 0;
45
+ let idRestriction = restrictToIds ? Array.from(new Set(restrictToIds)) : null;
46
+ if (input.categoryId) {
47
+ const assignments = await findWithDecryption(
48
+ em,
49
+ CatalogProductCategoryAssignment,
50
+ { tenantId, category: input.categoryId },
51
+ void 0,
52
+ buildScope(ctx, tenantId)
53
+ );
54
+ const ids = assignments.map((assignment) => {
55
+ const product = assignment.product;
56
+ if (!product) return null;
57
+ return typeof product === "string" ? product : product.id ?? null;
58
+ }).filter((value) => typeof value === "string" && value.length > 0);
59
+ if (!ids.length) return { items: [], total: 0 };
60
+ idRestriction = idRestriction ? idRestriction.filter((id) => ids.includes(id)) : ids;
61
+ if (!idRestriction.length) return { items: [], total: 0 };
62
+ }
63
+ if (input.tags && input.tags.length > 0) {
64
+ const tagWhere = {
65
+ tenantId,
66
+ $or: [
67
+ { slug: { $in: input.tags } },
68
+ { label: { $in: input.tags } }
69
+ ]
70
+ };
71
+ if (ctx.organizationId) tagWhere.organizationId = ctx.organizationId;
72
+ const tags = await findWithDecryption(
73
+ em,
74
+ CatalogProductTag,
75
+ tagWhere,
76
+ void 0,
77
+ buildScope(ctx, tenantId)
78
+ );
79
+ const tagIds = tags.map((tag) => tag.id);
80
+ if (!tagIds.length) return { items: [], total: 0 };
81
+ const assignments = await findWithDecryption(
82
+ em,
83
+ CatalogProductTagAssignment,
84
+ { tenantId, tag: { $in: tagIds } },
85
+ void 0,
86
+ buildScope(ctx, tenantId)
87
+ );
88
+ const scopedIds = assignments.map((assignment) => {
89
+ const product = assignment.product;
90
+ if (!product) return null;
91
+ return typeof product === "string" ? product : product.id ?? null;
92
+ }).filter((value) => typeof value === "string" && value.length > 0);
93
+ if (!scopedIds.length) return { items: [], total: 0 };
94
+ idRestriction = idRestriction ? idRestriction.filter((id) => scopedIds.includes(id)) : scopedIds;
95
+ if (!idRestriction.length) return { items: [], total: 0 };
96
+ }
97
+ if (input.priceMin !== void 0 || input.priceMax !== void 0) {
98
+ const priceWhere = { tenantId };
99
+ if (ctx.organizationId) priceWhere.organizationId = ctx.organizationId;
100
+ const priceRows = await findWithDecryption(
101
+ em,
102
+ CatalogProductPrice,
103
+ priceWhere,
104
+ void 0,
105
+ buildScope(ctx, tenantId)
106
+ );
107
+ const bounded = priceRows.filter((row) => {
108
+ const net = toPriceNumeric(row.unitPriceNet ?? null);
109
+ const gross = toPriceNumeric(row.unitPriceGross ?? null);
110
+ const comparable = gross ?? net;
111
+ if (comparable === null) return false;
112
+ if (input.priceMin !== void 0 && comparable < input.priceMin) return false;
113
+ if (input.priceMax !== void 0 && comparable > input.priceMax) return false;
114
+ return true;
115
+ });
116
+ const scopedIds = bounded.map((row) => {
117
+ const product = row.product;
118
+ if (!product) return null;
119
+ return typeof product === "string" ? product : product.id ?? null;
120
+ }).filter((value) => typeof value === "string" && value.length > 0);
121
+ if (!scopedIds.length) return { items: [], total: 0 };
122
+ idRestriction = idRestriction ? idRestriction.filter((id) => scopedIds.includes(id)) : Array.from(new Set(scopedIds));
123
+ if (!idRestriction.length) return { items: [], total: 0 };
124
+ }
125
+ const filters = {};
126
+ if (input.active === true) filters.is_active = true;
127
+ if (input.q) {
128
+ const pattern = `%${input.q}%`;
129
+ filters.$or = [
130
+ { title: { $ilike: pattern } },
131
+ { subtitle: { $ilike: pattern } },
132
+ { sku: { $ilike: pattern } },
133
+ { handle: { $ilike: pattern } }
134
+ ];
135
+ }
136
+ if (idRestriction) {
137
+ filters.id = { $in: idRestriction };
138
+ }
139
+ const qe = ctx.container.resolve("queryEngine");
140
+ const result = await qe.query("catalog:catalog_product", {
141
+ filters,
142
+ sort: [{ field: "created_at", dir: SortDir.Desc }],
143
+ page: { page: Math.floor(offset / limit) + 1, pageSize: limit },
144
+ tenantId,
145
+ organizationId: ctx.organizationId ?? void 0
146
+ });
147
+ const productIds = result.items.map((row) => row.id).filter((id) => typeof id === "string");
148
+ if (!productIds.length) return { items: [], total: result.total };
149
+ const products = await findWithDecryption(
150
+ em,
151
+ CatalogProduct,
152
+ { tenantId, id: { $in: productIds }, deletedAt: null },
153
+ void 0,
154
+ buildScope(ctx, tenantId)
155
+ );
156
+ const productById = new Map(products.map((p) => [p.id, p]));
157
+ const ordered = productIds.map((id) => productById.get(id)).filter((p) => !!p);
158
+ return { items: ordered.map(toProductSummary), total: result.total };
159
+ }
160
+ const searchProductsTool = {
161
+ name: "catalog.search_products",
162
+ displayName: "Search products",
163
+ description: 'Hybrid search + filter across tenant products. When `q` is non-empty, routes through the search service (tenant + organization scoped) then hydrates tenant-scoped product summaries; when `q` is empty or omitted, runs the catalog query engine with the supplied filters and returns ALL products. Pagination: response shape is `{ items, total, limit, offset, source }`. Hard cap is 100 rows per call (`limit` max=100, default=50); if `total` exceeds `limit + offset`, call again with the next `offset` rather than asking for >100. Empty / sentinel inputs: pass an empty `q` (or omit it) to list all products. NEVER pass `0` for `priceMin`/`priceMax` to mean "no bound" \u2014 `0` is a real numeric bound (priceMin=0 + priceMax=0 returns only free products). To remove a bound, OMIT the field. Empty string `categoryId` and empty `tags` arrays are ignored. `source` in the output indicates which path executed (`search_service` vs `query_engine`).',
164
+ inputSchema: searchProductsInput,
165
+ requiredFeatures: ["catalog.products.view"],
166
+ tags: ["read", "catalog", "merchandising"],
167
+ isMutation: false,
168
+ handler: async (rawInput, ctx) => {
169
+ const { tenantId } = assertTenantScope(ctx);
170
+ const input = searchProductsInput.parse(rawInput);
171
+ const em = resolveEm(ctx);
172
+ const limit = input.limit ?? 50;
173
+ const offset = input.offset ?? 0;
174
+ if (!input.q || input.q === "*" || input.q === "**" || input.q === "%") {
175
+ input.q = void 0;
176
+ }
177
+ if (typeof input.categoryId === "string" && input.categoryId.trim() === "") {
178
+ input.categoryId = void 0;
179
+ }
180
+ if (Array.isArray(input.tags) && input.tags.length === 0) {
181
+ input.tags = void 0;
182
+ }
183
+ if (input.priceMin === 0 && input.priceMax === 0) {
184
+ input.priceMin = void 0;
185
+ input.priceMax = void 0;
186
+ }
187
+ if (input.categoryId) {
188
+ const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
189
+ if (!uuidPattern.test(input.categoryId)) {
190
+ input.categoryId = void 0;
191
+ } else {
192
+ try {
193
+ const catExists = await em.count(CatalogProductCategory, { id: input.categoryId, tenantId });
194
+ if (catExists === 0) input.categoryId = void 0;
195
+ } catch {
196
+ input.categoryId = void 0;
197
+ }
198
+ }
199
+ }
200
+ if (input.q && input.q.trim().length > 0) {
201
+ const service = resolveSearchService(ctx);
202
+ if (service) {
203
+ const hits = await service.search(input.q.trim(), {
204
+ tenantId,
205
+ organizationId: ctx.organizationId,
206
+ limit,
207
+ entityTypes: [CATALOG_PRODUCT_ENTITY]
208
+ });
209
+ const hitIds = hits.filter((hit) => hit.entityId === CATALOG_PRODUCT_ENTITY).map((hit) => hit.recordId).filter((id) => typeof id === "string" && id.length > 0);
210
+ if (!hitIds.length) {
211
+ return { items: [], total: 0, limit, offset, source: "search_service" };
212
+ }
213
+ const { items: items2, total: total2 } = await queryProductsWithFilters(em, ctx, tenantId, { ...input, q: void 0 }, hitIds);
214
+ return { items: items2, total: total2, limit, offset, source: "search_service" };
215
+ }
216
+ }
217
+ const { items, total } = await queryProductsWithFilters(em, ctx, tenantId, input, null);
218
+ return { items, total, limit, offset, source: "query_engine" };
219
+ }
220
+ };
221
+ const getProductBundleInput = z.object({
222
+ productId: z.string().uuid().describe("Catalog product id (UUID).")
223
+ });
224
+ const getProductBundleTool = {
225
+ name: "catalog.get_product_bundle",
226
+ displayName: "Get product bundle",
227
+ description: "Aggregate product snapshot for D18 merchandising: core fields + categories + tags + variants + prices (base + best via pricing service) + media metadata + custom-field values + merged attribute schema. Media bytes flow through the attachment bridge (use `catalog.get_product_media` then the bridge). Returns `{ found: false }` on miss or cross-tenant access.",
228
+ inputSchema: getProductBundleInput,
229
+ requiredFeatures: ["catalog.products.view"],
230
+ tags: ["read", "catalog", "merchandising"],
231
+ isMutation: false,
232
+ handler: async (rawInput, ctx) => {
233
+ const { tenantId } = assertTenantScope(ctx);
234
+ const input = getProductBundleInput.parse(rawInput);
235
+ const em = resolveEm(ctx);
236
+ return buildProductBundle(em, ctx, tenantId, input.productId);
237
+ }
238
+ };
239
+ const listSelectedProductsInput = z.object({
240
+ productIds: z.array(z.string().uuid()).min(1).max(50).describe("1..50 catalog product ids (UUIDs). Duplicates are collapsed; cross-tenant ids drop into `missingIds`.")
241
+ });
242
+ const listSelectedProductsTool = {
243
+ name: "catalog.list_selected_products",
244
+ displayName: "List selected products (bundles)",
245
+ description: "Bulk variant of `catalog.get_product_bundle`: resolves 1..50 product ids into tenant-scoped bundle aggregates. Missing / cross-tenant ids are returned in `missingIds` (not as an error) so selection-aware agents can render partial results.",
246
+ inputSchema: listSelectedProductsInput,
247
+ requiredFeatures: ["catalog.products.view"],
248
+ tags: ["read", "catalog", "merchandising"],
249
+ isMutation: false,
250
+ handler: async (rawInput, ctx) => {
251
+ const { tenantId } = assertTenantScope(ctx);
252
+ const input = listSelectedProductsInput.parse(rawInput);
253
+ const em = resolveEm(ctx);
254
+ const unique = Array.from(new Set(input.productIds));
255
+ const resolved = await Promise.all(
256
+ unique.map(async (productId) => ({ productId, result: await buildProductBundle(em, ctx, tenantId, productId) }))
257
+ );
258
+ const items = [];
259
+ const missingIds = [];
260
+ for (const entry of resolved) {
261
+ if (entry.result.found) {
262
+ items.push(entry.result);
263
+ } else {
264
+ missingIds.push(entry.productId);
265
+ console.warn(`[catalog.list_selected_products] product not in scope: ${entry.productId}`);
266
+ }
267
+ }
268
+ return { items, missingIds };
269
+ }
270
+ };
271
+ const getProductMediaInput = z.object({
272
+ productId: z.string().uuid().describe("Catalog product id (UUID)."),
273
+ limit: z.number().int().min(1).max(100).optional().describe("Max rows (default 50, max 100)."),
274
+ offset: z.number().int().min(0).optional().describe("Rows to skip (default 0).")
275
+ });
276
+ const getProductMediaTool = {
277
+ name: "catalog.get_product_media",
278
+ displayName: "Get product media (with attachment IDs)",
279
+ description: "List media records attached to a product with metadata (filename, mime, size, sort order) and the `attachmentId` string for each row. Does NOT invoke the attachment bridge \u2014 the Step 3.7 runtime bridge converts attachment ids into model file parts when the chat/object helper invokes this tool in-context. Returns `{ items, total, limit, offset }`.",
280
+ inputSchema: getProductMediaInput,
281
+ requiredFeatures: ["catalog.products.view"],
282
+ tags: ["read", "catalog", "merchandising"],
283
+ isMutation: false,
284
+ handler: async (rawInput, ctx) => {
285
+ const { tenantId } = assertTenantScope(ctx);
286
+ const input = getProductMediaInput.parse(rawInput);
287
+ const em = resolveEm(ctx);
288
+ const limit = input.limit ?? 50;
289
+ const offset = input.offset ?? 0;
290
+ const where = {
291
+ tenantId,
292
+ entityId: E.catalog.catalog_product,
293
+ recordId: input.productId
294
+ };
295
+ if (ctx.organizationId) where.organizationId = ctx.organizationId;
296
+ const [rows, total] = await Promise.all([
297
+ findWithDecryption(
298
+ em,
299
+ Attachment,
300
+ where,
301
+ { limit, offset, orderBy: { createdAt: "asc" } },
302
+ buildScope(ctx, tenantId)
303
+ ),
304
+ em.count(Attachment, where)
305
+ ]);
306
+ const filtered = rows.filter((row) => (row.tenantId ?? null) === tenantId);
307
+ return {
308
+ items: filtered.map((row) => ({
309
+ mediaId: row.id,
310
+ productId: input.productId,
311
+ attachmentId: row.id,
312
+ fileName: row.fileName,
313
+ mediaType: row.mimeType,
314
+ size: row.fileSize,
315
+ altText: null,
316
+ sortOrder: 0
317
+ })),
318
+ total,
319
+ limit,
320
+ offset
321
+ };
322
+ }
323
+ };
324
+ const getAttributeSchemaInput = z.object({
325
+ productId: z.string().uuid().optional().describe("Narrow schema resolution to this product (scope: `product`)."),
326
+ categoryId: z.string().uuid().optional().describe("Narrow schema resolution to this category (scope: `category`).")
327
+ });
328
+ const getAttributeSchemaTool = {
329
+ name: "catalog.get_attribute_schema",
330
+ displayName: "Get attribute schema",
331
+ description: "Resolve the merged custom-field attribute schema for catalog products. When both `productId` and `categoryId` are absent, returns module-level fields only. Reuses the shared `loadCustomFieldDefinitionIndex` resolver so tenant + organization scoping rules stay consistent with CrudForm / admin routes.",
332
+ inputSchema: getAttributeSchemaInput,
333
+ requiredFeatures: ["catalog.products.view"],
334
+ tags: ["read", "catalog", "merchandising"],
335
+ isMutation: false,
336
+ handler: async (rawInput, ctx) => {
337
+ const { tenantId } = assertTenantScope(ctx);
338
+ const input = getAttributeSchemaInput.parse(rawInput);
339
+ return resolveAttributeSchema(ctx, tenantId, input.productId, input.categoryId);
340
+ }
341
+ };
342
+ const getCategoryBriefInput = z.object({
343
+ categoryId: z.string().uuid().describe("Category id (UUID).")
344
+ });
345
+ const getCategoryBriefTool = {
346
+ name: "catalog.get_category_brief",
347
+ displayName: "Get category brief",
348
+ description: "Category name, full tree path, description, and merged attribute schema (same resolver as `catalog.get_attribute_schema` with `categoryId`). Returns `{ found: false }` on miss / cross-tenant.",
349
+ inputSchema: getCategoryBriefInput,
350
+ requiredFeatures: ["catalog.categories.view"],
351
+ tags: ["read", "catalog", "merchandising"],
352
+ isMutation: false,
353
+ handler: async (rawInput, ctx) => {
354
+ const { tenantId } = assertTenantScope(ctx);
355
+ const input = getCategoryBriefInput.parse(rawInput);
356
+ const em = resolveEm(ctx);
357
+ const where = {
358
+ id: input.categoryId,
359
+ tenantId,
360
+ deletedAt: null
361
+ };
362
+ if (ctx.organizationId) where.organizationId = ctx.organizationId;
363
+ const category = await findOneWithDecryption(
364
+ em,
365
+ CatalogProductCategory,
366
+ where,
367
+ void 0,
368
+ buildScope(ctx, tenantId)
369
+ );
370
+ if (!category || category.tenantId !== tenantId) {
371
+ return { found: false, categoryId: input.categoryId };
372
+ }
373
+ const attributeSchema = await resolveAttributeSchema(ctx, tenantId, void 0, category.id);
374
+ return {
375
+ found: true,
376
+ id: category.id,
377
+ name: category.name,
378
+ path: category.treePath ?? null,
379
+ description: category.description ?? null,
380
+ attributeSchema
381
+ };
382
+ }
383
+ };
384
+ const listPriceKindsInput = z.object({
385
+ limit: z.number().int().min(1).max(100).optional().describe("Max rows (default 50, max 100)."),
386
+ offset: z.number().int().min(0).optional().describe("Rows to skip (default 0).")
387
+ }).passthrough();
388
+ const listPriceKindsTool = {
389
+ name: "catalog.list_price_kinds",
390
+ displayName: "List price kinds",
391
+ description: "Enumerate tenant price kinds for the D18 merchandising assistant. Shares the tenant-scoped query path with `catalog.list_price_kinds_base`; the two tools differ only in description/framing (the base tool is the low-level settings enumerator, this one is the spec-named D18 surface).",
392
+ inputSchema: listPriceKindsInput,
393
+ requiredFeatures: ["catalog.settings.manage"],
394
+ tags: ["read", "catalog", "merchandising"],
395
+ isMutation: false,
396
+ handler: async (rawInput, ctx) => {
397
+ const { tenantId } = assertTenantScope(ctx);
398
+ const input = listPriceKindsInput.parse(rawInput);
399
+ const base = await listPriceKindsCore(ctx, input, tenantId);
400
+ return {
401
+ items: base.items.map((row) => ({
402
+ id: row.id,
403
+ code: row.code,
404
+ name: row.title,
405
+ scope: row.organizationId ? "organization" : "tenant",
406
+ currency: row.currencyCode,
407
+ appliesTo: row.isPromotion ? "promotion" : "regular"
408
+ })),
409
+ total: base.total,
410
+ limit: base.limit,
411
+ offset: base.offset
412
+ };
413
+ }
414
+ };
415
+ const merchandisingAiTools = [
416
+ searchProductsTool,
417
+ getProductBundleTool,
418
+ listSelectedProductsTool,
419
+ getProductMediaTool,
420
+ getAttributeSchemaTool,
421
+ getCategoryBriefTool,
422
+ listPriceKindsTool
423
+ ];
424
+ var merchandising_pack_default = merchandisingAiTools;
425
+ export {
426
+ merchandising_pack_default as default,
427
+ merchandisingAiTools
428
+ };
429
+ //# sourceMappingURL=merchandising-pack.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/catalog/ai-tools/merchandising-pack.ts"],
4
+ "sourcesContent": ["/**\n * D18 catalog merchandising read tools (Phase 1 WS-C, Step 3.11).\n *\n * Ships the seven canonical tool names the `catalog.merchandising_assistant`\n * agent (Step 4.9) whitelists verbatim:\n *\n * - `catalog.search_products` \u2014 fulltext + filter search (hybrid path).\n * - `catalog.get_product_bundle` \u2014 aggregate bundle for a single product.\n * - `catalog.list_selected_products` \u2014 bundle aggregate for an ID array.\n * - `catalog.get_product_media` \u2014 media metadata (attachment IDs only \u2014\n * the Step 3.7 attachment bridge converts these to model file parts at\n * runtime invocation; this tool does NOT call the bridge directly).\n * - `catalog.get_attribute_schema` \u2014 merged module + category + product\n * custom-field schema.\n * - `catalog.get_category_brief` \u2014 category snapshot with inherited schema.\n * - `catalog.list_price_kinds` \u2014 D18 spec-named price-kind enumerator.\n *\n * Every tool is read-only (no `isMutation: true`). Mutation tooling for D18\n * lands in Step 5.14 under the pending-action contract.\n *\n * Tenant scoping: all DB access uses `findWithDecryption` /\n * `findOneWithDecryption`, plus a defensive post-filter against\n * `row.tenantId === ctx.tenantId`. Cross-tenant IDs surface through the\n * `missingIds` output (not an error), so a chat agent receives a uniform\n * not-found signal whether the product is missing, deleted, or out of scope.\n *\n * Shared helpers:\n * `list_price_kinds` + Step 3.10's `list_price_kinds_base` both route\n * through `listPriceKindsCore` in `./_shared.ts`; there is no duplicate\n * query path.\n *\n * Step 3.12 (authoring pack) promoted the product-bundle builder plus\n * `toProductSummary` / `resolveAttributeSchema` into `./_shared.ts` so\n * both packs consume the same loader. This file now just wires the\n * shared helpers into tool handlers.\n */\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { z } from 'zod'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { type QueryEngine, type QueryResult, SortDir } from '@open-mercato/shared/lib/query/types'\nimport { E } from '#generated/entities.ids.generated'\nimport { Attachment } from '@open-mercato/core/modules/attachments/data/entities'\nimport {\n CatalogProduct,\n CatalogProductCategory,\n CatalogProductCategoryAssignment,\n CatalogProductPrice,\n CatalogProductTag,\n CatalogProductTagAssignment,\n} from '../data/entities'\nimport { assertTenantScope, type CatalogAiToolDefinition, type CatalogToolContext } from './types'\nimport {\n buildProductBundle,\n buildScope,\n listPriceKindsCore,\n resolveAttributeSchema,\n resolveEm,\n toPriceNumeric,\n toProductSummary,\n type ProductBundle,\n type ProductBundleResult,\n} from './_shared'\n\ntype SearchServiceLike = {\n search: (query: string, options: {\n tenantId: string\n organizationId?: string | null\n limit?: number\n entityTypes?: string[]\n }) => Promise<Array<{\n entityId: string\n recordId: string\n score: number\n source: string\n presenter?: unknown\n url?: string\n }>>\n}\n\nconst CATALOG_PRODUCT_ENTITY = 'catalog:catalog_product'\n\nfunction resolveSearchService(ctx: CatalogToolContext): SearchServiceLike | null {\n try {\n return ctx.container.resolve<SearchServiceLike>('searchService')\n } catch {\n return null\n }\n}\n\n/* -------------------------------------------------------------------------- */\n/* catalog.search_products */\n/* -------------------------------------------------------------------------- */\n\nconst searchProductsInput = z\n .object({\n q: z.string().trim().optional().describe('Optional fulltext query (title / subtitle / sku / handle). Omit or leave empty (or do NOT pass it at all) to list all products. NEVER use \"*\", \"**\", or \"%\" \u2014 they are not wildcards and will be discarded.'),\n limit: z.number().int().min(1).max(100).optional().describe('Page size. Default 50, hard maximum 100. Results are paginated: when `total` exceeds `limit + offset`, call again with the next `offset` instead of asking for more than 100 rows.'),\n offset: z.number().int().min(0).optional().describe('Rows to skip for pagination (default 0). Combine with `limit` to fetch subsequent pages \u2014 for example offset=100, limit=100 fetches rows 101..200.'),\n categoryId: z.string().optional().describe('Restrict to products assigned to this catalog category UUID. Only use a category ID returned by a previous tool call \u2014 do NOT guess. Empty string is treated the same as omitting the field.'),\n priceMin: z.number().optional().describe('Lower-bound (inclusive) on the gross unit price. OMIT this field when you do not want a lower bound \u2014 do NOT pass 0 as \"no minimum\", because 0 is a real bound that combined with priceMax=0 will return only free products.'),\n priceMax: z.number().optional().describe('Upper-bound (inclusive) on the gross unit price. OMIT this field when you do not want an upper bound \u2014 do NOT pass 0 as \"no maximum\", because 0 is a real bound that will exclude every priced product.'),\n tags: z.array(z.string()).optional().describe('Tag labels or slugs (any-match) the product carries. Omit or pass an empty array to skip the tag filter.'),\n active: z.boolean().optional().describe('When true, only active products are returned. Omit to include inactive products as well.'),\n })\n .passthrough()\n\ntype SearchProductsInput = z.infer<typeof searchProductsInput>\n\nasync function queryProductsWithFilters(\n em: EntityManager,\n ctx: CatalogToolContext,\n tenantId: string,\n input: SearchProductsInput,\n restrictToIds: string[] | null,\n): Promise<{ items: ReturnType<typeof toProductSummary>[]; total: number }> {\n const limit = input.limit ?? 50\n const offset = input.offset ?? 0\n\n let idRestriction: string[] | null = restrictToIds\n ? Array.from(new Set(restrictToIds))\n : null\n\n if (input.categoryId) {\n const assignments = await findWithDecryption<CatalogProductCategoryAssignment>(\n em,\n CatalogProductCategoryAssignment,\n { tenantId, category: input.categoryId } as any,\n undefined,\n buildScope(ctx, tenantId),\n )\n const ids = assignments\n .map((assignment) => {\n const product = (assignment as any).product\n if (!product) return null\n return typeof product === 'string' ? product : product.id ?? null\n })\n .filter((value: string | null): value is string => typeof value === 'string' && value.length > 0)\n if (!ids.length) return { items: [], total: 0 }\n idRestriction = idRestriction\n ? idRestriction.filter((id) => ids.includes(id))\n : ids\n if (!idRestriction.length) return { items: [], total: 0 }\n }\n\n if (input.tags && input.tags.length > 0) {\n const tagWhere: Record<string, unknown> = {\n tenantId,\n $or: [\n { slug: { $in: input.tags } },\n { label: { $in: input.tags } },\n ],\n }\n if (ctx.organizationId) tagWhere.organizationId = ctx.organizationId\n const tags = await findWithDecryption<CatalogProductTag>(\n em,\n CatalogProductTag,\n tagWhere as any,\n undefined,\n buildScope(ctx, tenantId),\n )\n const tagIds = tags.map((tag) => tag.id)\n if (!tagIds.length) return { items: [], total: 0 }\n const assignments = await findWithDecryption<CatalogProductTagAssignment>(\n em,\n CatalogProductTagAssignment,\n { tenantId, tag: { $in: tagIds } } as any,\n undefined,\n buildScope(ctx, tenantId),\n )\n const scopedIds = assignments\n .map((assignment) => {\n const product = (assignment as any).product\n if (!product) return null\n return typeof product === 'string' ? product : product.id ?? null\n })\n .filter((value: string | null): value is string => typeof value === 'string' && value.length > 0)\n if (!scopedIds.length) return { items: [], total: 0 }\n idRestriction = idRestriction\n ? idRestriction.filter((id) => scopedIds.includes(id))\n : scopedIds\n if (!idRestriction.length) return { items: [], total: 0 }\n }\n\n if (input.priceMin !== undefined || input.priceMax !== undefined) {\n const priceWhere: Record<string, unknown> = { tenantId }\n if (ctx.organizationId) priceWhere.organizationId = ctx.organizationId\n const priceRows = await findWithDecryption<CatalogProductPrice>(\n em,\n CatalogProductPrice,\n priceWhere as any,\n undefined,\n buildScope(ctx, tenantId),\n )\n const bounded = priceRows.filter((row) => {\n const net = toPriceNumeric(row.unitPriceNet ?? null)\n const gross = toPriceNumeric(row.unitPriceGross ?? null)\n const comparable = gross ?? net\n if (comparable === null) return false\n if (input.priceMin !== undefined && comparable < input.priceMin) return false\n if (input.priceMax !== undefined && comparable > input.priceMax) return false\n return true\n })\n const scopedIds = bounded\n .map((row) => {\n const product = (row as any).product\n if (!product) return null\n return typeof product === 'string' ? product : product.id ?? null\n })\n .filter((value: string | null): value is string => typeof value === 'string' && value.length > 0)\n if (!scopedIds.length) return { items: [], total: 0 }\n idRestriction = idRestriction\n ? idRestriction.filter((id) => scopedIds.includes(id))\n : Array.from(new Set(scopedIds))\n if (!idRestriction.length) return { items: [], total: 0 }\n }\n\n const filters: Record<string, unknown> = {}\n if (input.active === true) filters.is_active = true\n if (input.q) {\n const pattern = `%${input.q}%`\n filters.$or = [\n { title: { $ilike: pattern } },\n { subtitle: { $ilike: pattern } },\n { sku: { $ilike: pattern } },\n { handle: { $ilike: pattern } },\n ]\n }\n if (idRestriction) {\n filters.id = { $in: idRestriction }\n }\n\n const qe = ctx.container.resolve<QueryEngine>('queryEngine')\n const result: QueryResult = await qe.query('catalog:catalog_product', {\n filters,\n sort: [{ field: 'created_at', dir: SortDir.Desc }],\n page: { page: Math.floor(offset / limit) + 1, pageSize: limit },\n tenantId,\n organizationId: ctx.organizationId ?? undefined,\n })\n\n const productIds = result.items\n .map((row: Record<string, unknown>) => row.id as string)\n .filter((id): id is string => typeof id === 'string')\n if (!productIds.length) return { items: [], total: result.total }\n\n const products = await findWithDecryption<CatalogProduct>(\n em,\n CatalogProduct,\n { tenantId, id: { $in: productIds }, deletedAt: null } as any,\n undefined,\n buildScope(ctx, tenantId),\n )\n const productById = new Map(products.map((p) => [p.id, p]))\n const ordered = productIds\n .map((id) => productById.get(id))\n .filter((p): p is CatalogProduct => !!p)\n\n return { items: ordered.map(toProductSummary), total: result.total }\n}\n\nconst searchProductsTool: CatalogAiToolDefinition = {\n name: 'catalog.search_products',\n displayName: 'Search products',\n description:\n 'Hybrid search + filter across tenant products. When `q` is non-empty, routes through the search service (tenant + organization scoped) then hydrates tenant-scoped product summaries; when `q` is empty or omitted, runs the catalog query engine with the supplied filters and returns ALL products. ' +\n 'Pagination: response shape is `{ items, total, limit, offset, source }`. Hard cap is 100 rows per call (`limit` max=100, default=50); if `total` exceeds `limit + offset`, call again with the next `offset` rather than asking for >100. ' +\n 'Empty / sentinel inputs: pass an empty `q` (or omit it) to list all products. NEVER pass `0` for `priceMin`/`priceMax` to mean \"no bound\" \u2014 `0` is a real numeric bound (priceMin=0 + priceMax=0 returns only free products). To remove a bound, OMIT the field. Empty string `categoryId` and empty `tags` arrays are ignored. ' +\n '`source` in the output indicates which path executed (`search_service` vs `query_engine`).',\n inputSchema: searchProductsInput,\n requiredFeatures: ['catalog.products.view'],\n tags: ['read', 'catalog', 'merchandising'],\n isMutation: false,\n handler: async (rawInput, ctx) => {\n const { tenantId } = assertTenantScope(ctx)\n const input = searchProductsInput.parse(rawInput)\n const em = resolveEm(ctx)\n const limit = input.limit ?? 50\n const offset = input.offset ?? 0\n\n // Treat wildcard-style and empty queries as \"list all\".\n if (!input.q || input.q === '*' || input.q === '**' || input.q === '%') {\n input.q = undefined\n }\n\n // Empty-string sentinel for `categoryId` is the agent's way of saying\n // \"no category filter\" \u2014 normalize before the UUID guard runs.\n if (typeof input.categoryId === 'string' && input.categoryId.trim() === '') {\n input.categoryId = undefined\n }\n\n // Empty `tags` array is meaningless as a filter; drop it so the price /\n // category narrowing path doesn't execute a no-op tag join.\n if (Array.isArray(input.tags) && input.tags.length === 0) {\n input.tags = undefined\n }\n\n // Agents commonly pass `priceMin: 0` / `priceMax: 0` thinking 0 means\n // \"no bound\". It does not \u2014 0 is a real inclusive bound and the two\n // together ask for free products only. Treat the (0, 0) pair as\n // \"unset\" so the empty-bound default carries through. A standalone 0\n // (e.g. priceMin=0 with priceMax=50) is left intact because it can be\n // a meaningful \"everything from free up to 50\" filter.\n if (\n input.priceMin === 0 &&\n input.priceMax === 0\n ) {\n input.priceMin = undefined\n input.priceMax = undefined\n }\n\n // Guard against hallucinated or invalid category IDs.\n if (input.categoryId) {\n const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i\n if (!uuidPattern.test(input.categoryId)) {\n input.categoryId = undefined\n } else {\n try {\n const catExists = await em.count(CatalogProductCategory, { id: input.categoryId, tenantId } as any)\n if (catExists === 0) input.categoryId = undefined\n } catch {\n input.categoryId = undefined\n }\n }\n }\n\n if (input.q && input.q.trim().length > 0) {\n const service = resolveSearchService(ctx)\n if (service) {\n const hits = await service.search(input.q.trim(), {\n tenantId,\n organizationId: ctx.organizationId,\n limit,\n entityTypes: [CATALOG_PRODUCT_ENTITY],\n })\n const hitIds = hits\n .filter((hit) => hit.entityId === CATALOG_PRODUCT_ENTITY)\n .map((hit) => hit.recordId)\n .filter((id): id is string => typeof id === 'string' && id.length > 0)\n if (!hitIds.length) {\n return { items: [], total: 0, limit, offset, source: 'search_service' as const }\n }\n // Hydrate tenant-scoped products from the hit IDs, then apply any\n // structured filters the search service can't express (category /\n // price / tags / active). Search services in this repo do not\n // currently accept structured filters, so we narrow in-process and\n // document that in the return report + description.\n const { items, total } = await queryProductsWithFilters(em, ctx, tenantId, { ...input, q: undefined }, hitIds)\n return { items, total, limit, offset, source: 'search_service' as const }\n }\n // Fall through to the query-engine path if the search service is not\n // registered in the DI container \u2014 keeps the tool usable in test\n // harnesses and during early bring-up.\n }\n\n const { items, total } = await queryProductsWithFilters(em, ctx, tenantId, input, null)\n return { items, total, limit, offset, source: 'query_engine' as const }\n },\n}\n\n/* -------------------------------------------------------------------------- */\n/* catalog.get_product_bundle / catalog.list_selected_products */\n/* -------------------------------------------------------------------------- */\n\nconst getProductBundleInput = z.object({\n productId: z.string().uuid().describe('Catalog product id (UUID).'),\n})\n\nconst getProductBundleTool: CatalogAiToolDefinition = {\n name: 'catalog.get_product_bundle',\n displayName: 'Get product bundle',\n description:\n 'Aggregate product snapshot for D18 merchandising: core fields + categories + tags + variants + prices (base + best via pricing service) + media metadata + custom-field values + merged attribute schema. Media bytes flow through the attachment bridge (use `catalog.get_product_media` then the bridge). Returns `{ found: false }` on miss or cross-tenant access.',\n inputSchema: getProductBundleInput,\n requiredFeatures: ['catalog.products.view'],\n tags: ['read', 'catalog', 'merchandising'],\n isMutation: false,\n handler: async (rawInput, ctx) => {\n const { tenantId } = assertTenantScope(ctx)\n const input = getProductBundleInput.parse(rawInput)\n const em = resolveEm(ctx)\n return buildProductBundle(em, ctx, tenantId, input.productId)\n },\n}\n\nconst listSelectedProductsInput = z.object({\n productIds: z\n .array(z.string().uuid())\n .min(1)\n .max(50)\n .describe('1..50 catalog product ids (UUIDs). Duplicates are collapsed; cross-tenant ids drop into `missingIds`.'),\n})\n\nconst listSelectedProductsTool: CatalogAiToolDefinition = {\n name: 'catalog.list_selected_products',\n displayName: 'List selected products (bundles)',\n description:\n 'Bulk variant of `catalog.get_product_bundle`: resolves 1..50 product ids into tenant-scoped bundle aggregates. Missing / cross-tenant ids are returned in `missingIds` (not as an error) so selection-aware agents can render partial results.',\n inputSchema: listSelectedProductsInput,\n requiredFeatures: ['catalog.products.view'],\n tags: ['read', 'catalog', 'merchandising'],\n isMutation: false,\n handler: async (rawInput, ctx) => {\n const { tenantId } = assertTenantScope(ctx)\n const input = listSelectedProductsInput.parse(rawInput)\n const em = resolveEm(ctx)\n const unique = Array.from(new Set(input.productIds))\n const resolved = await Promise.all(\n unique.map(async (productId) => ({ productId, result: await buildProductBundle(em, ctx, tenantId, productId) })),\n )\n const items: ProductBundle[] = []\n const missingIds: string[] = []\n for (const entry of resolved) {\n if (entry.result.found) {\n items.push(entry.result)\n } else {\n missingIds.push(entry.productId)\n console.warn(`[catalog.list_selected_products] product not in scope: ${entry.productId}`)\n }\n }\n return { items, missingIds }\n },\n}\n\n/* -------------------------------------------------------------------------- */\n/* catalog.get_product_media */\n/* -------------------------------------------------------------------------- */\n\nconst getProductMediaInput = z.object({\n productId: z.string().uuid().describe('Catalog product id (UUID).'),\n limit: z.number().int().min(1).max(100).optional().describe('Max rows (default 50, max 100).'),\n offset: z.number().int().min(0).optional().describe('Rows to skip (default 0).'),\n})\n\nconst getProductMediaTool: CatalogAiToolDefinition = {\n name: 'catalog.get_product_media',\n displayName: 'Get product media (with attachment IDs)',\n description:\n 'List media records attached to a product with metadata (filename, mime, size, sort order) and the `attachmentId` string for each row. Does NOT invoke the attachment bridge \u2014 the Step 3.7 runtime bridge converts attachment ids into model file parts when the chat/object helper invokes this tool in-context. Returns `{ items, total, limit, offset }`.',\n inputSchema: getProductMediaInput,\n requiredFeatures: ['catalog.products.view'],\n tags: ['read', 'catalog', 'merchandising'],\n isMutation: false,\n handler: async (rawInput, ctx) => {\n const { tenantId } = assertTenantScope(ctx)\n const input = getProductMediaInput.parse(rawInput)\n const em = resolveEm(ctx)\n const limit = input.limit ?? 50\n const offset = input.offset ?? 0\n const where: Record<string, unknown> = {\n tenantId,\n entityId: E.catalog.catalog_product,\n recordId: input.productId,\n }\n if (ctx.organizationId) where.organizationId = ctx.organizationId\n const [rows, total] = await Promise.all([\n findWithDecryption<Attachment>(\n em,\n Attachment,\n where as any,\n { limit, offset, orderBy: { createdAt: 'asc' } as any } as any,\n buildScope(ctx, tenantId),\n ),\n em.count(Attachment, where as any),\n ])\n const filtered = rows.filter((row) => (row.tenantId ?? null) === tenantId)\n return {\n items: filtered.map((row) => ({\n mediaId: row.id,\n productId: input.productId,\n attachmentId: row.id,\n fileName: row.fileName,\n mediaType: row.mimeType,\n size: row.fileSize,\n altText: null,\n sortOrder: 0,\n })),\n total,\n limit,\n offset,\n }\n },\n}\n\n/* -------------------------------------------------------------------------- */\n/* catalog.get_attribute_schema / catalog.get_category_brief */\n/* -------------------------------------------------------------------------- */\n\nconst getAttributeSchemaInput = z.object({\n productId: z.string().uuid().optional().describe('Narrow schema resolution to this product (scope: `product`).'),\n categoryId: z.string().uuid().optional().describe('Narrow schema resolution to this category (scope: `category`).'),\n})\n\nconst getAttributeSchemaTool: CatalogAiToolDefinition = {\n name: 'catalog.get_attribute_schema',\n displayName: 'Get attribute schema',\n description:\n 'Resolve the merged custom-field attribute schema for catalog products. When both `productId` and `categoryId` are absent, returns module-level fields only. Reuses the shared `loadCustomFieldDefinitionIndex` resolver so tenant + organization scoping rules stay consistent with CrudForm / admin routes.',\n inputSchema: getAttributeSchemaInput,\n requiredFeatures: ['catalog.products.view'],\n tags: ['read', 'catalog', 'merchandising'],\n isMutation: false,\n handler: async (rawInput, ctx) => {\n const { tenantId } = assertTenantScope(ctx)\n const input = getAttributeSchemaInput.parse(rawInput)\n return resolveAttributeSchema(ctx, tenantId, input.productId, input.categoryId)\n },\n}\n\nconst getCategoryBriefInput = z.object({\n categoryId: z.string().uuid().describe('Category id (UUID).'),\n})\n\nconst getCategoryBriefTool: CatalogAiToolDefinition = {\n name: 'catalog.get_category_brief',\n displayName: 'Get category brief',\n description:\n 'Category name, full tree path, description, and merged attribute schema (same resolver as `catalog.get_attribute_schema` with `categoryId`). Returns `{ found: false }` on miss / cross-tenant.',\n inputSchema: getCategoryBriefInput,\n requiredFeatures: ['catalog.categories.view'],\n tags: ['read', 'catalog', 'merchandising'],\n isMutation: false,\n handler: async (rawInput, ctx) => {\n const { tenantId } = assertTenantScope(ctx)\n const input = getCategoryBriefInput.parse(rawInput)\n const em = resolveEm(ctx)\n const where: Record<string, unknown> = {\n id: input.categoryId,\n tenantId,\n deletedAt: null,\n }\n if (ctx.organizationId) where.organizationId = ctx.organizationId\n const category = await findOneWithDecryption<CatalogProductCategory>(\n em,\n CatalogProductCategory,\n where as any,\n undefined,\n buildScope(ctx, tenantId),\n )\n if (!category || category.tenantId !== tenantId) {\n return { found: false as const, categoryId: input.categoryId }\n }\n const attributeSchema = await resolveAttributeSchema(ctx, tenantId, undefined, category.id)\n return {\n found: true as const,\n id: category.id,\n name: category.name,\n path: category.treePath ?? null,\n description: category.description ?? null,\n attributeSchema,\n }\n },\n}\n\n/* -------------------------------------------------------------------------- */\n/* catalog.list_price_kinds */\n/* -------------------------------------------------------------------------- */\n\nconst listPriceKindsInput = z\n .object({\n limit: z.number().int().min(1).max(100).optional().describe('Max rows (default 50, max 100).'),\n offset: z.number().int().min(0).optional().describe('Rows to skip (default 0).'),\n })\n .passthrough()\n\nconst listPriceKindsTool: CatalogAiToolDefinition = {\n name: 'catalog.list_price_kinds',\n displayName: 'List price kinds',\n description:\n 'Enumerate tenant price kinds for the D18 merchandising assistant. Shares the tenant-scoped query path with `catalog.list_price_kinds_base`; the two tools differ only in description/framing (the base tool is the low-level settings enumerator, this one is the spec-named D18 surface).',\n inputSchema: listPriceKindsInput,\n requiredFeatures: ['catalog.settings.manage'],\n tags: ['read', 'catalog', 'merchandising'],\n isMutation: false,\n handler: async (rawInput, ctx) => {\n const { tenantId } = assertTenantScope(ctx)\n const input = listPriceKindsInput.parse(rawInput)\n const base = await listPriceKindsCore(ctx, input, tenantId)\n return {\n items: base.items.map((row) => ({\n id: row.id,\n code: row.code,\n name: row.title,\n scope: row.organizationId ? ('organization' as const) : ('tenant' as const),\n currency: row.currencyCode,\n appliesTo: row.isPromotion ? ('promotion' as const) : ('regular' as const),\n })),\n total: base.total,\n limit: base.limit,\n offset: base.offset,\n }\n },\n}\n\n/* -------------------------------------------------------------------------- */\n/* Export */\n/* -------------------------------------------------------------------------- */\n\nexport const merchandisingAiTools: CatalogAiToolDefinition[] = [\n searchProductsTool,\n getProductBundleTool,\n listSelectedProductsTool,\n getProductMediaTool,\n getAttributeSchemaTool,\n getCategoryBriefTool,\n listPriceKindsTool,\n]\n\nexport default merchandisingAiTools\n"],
5
+ "mappings": "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",
6
+ "names": ["items", "total"]
7
+ }