@open-mercato/core 0.5.1-develop.3036.f02c281f23 → 0.5.1-develop.3045.b4b3320cc2

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,376 @@
1
+ /**
2
+ * `catalog.list_prices`, `catalog.list_price_kinds_base`, `catalog.list_offers`
3
+ * (Phase 1 WS-C, Step 3.10).
4
+ *
5
+ * Read-only enumeration of prices (base + offer-bound), price kinds, and
6
+ * offers for the caller tenant + organization. Mutation tools land in Step
7
+ * 5.14 under the pending-action contract.
8
+ *
9
+ * `catalog.list_price_kinds_base` uses a distinct name on purpose — Step
10
+ * 3.11 (D18) will own `catalog.list_price_kinds` verbatim; we keep both
11
+ * names available so the D18 tool can layer merchandising-specific shape
12
+ * over the base enumerator.
13
+ *
14
+ * Phase 3b of `.ai/specs/2026-04-27-ai-tools-api-backed-dry-refactor.md`:
15
+ * `catalog.list_prices` and `catalog.list_offers` are now API-backed wrappers
16
+ * over `GET /api/catalog/prices` and `GET /api/catalog/offers`. Tool names,
17
+ * schemas, requiredFeatures, and output shapes are unchanged. The offers
18
+ * route does not expose a `variantId` filter; the AI input is pre-resolved
19
+ * via `CatalogProductPrice` to the matching offer ids and threaded through
20
+ * the route's `id` filter (or post-filtered when more than one matches),
21
+ * mirroring Phase 3a's `companyId` → `ids` trick for `customers.list_people`.
22
+ */
23
+ import type { EntityManager } from '@mikro-orm/postgresql'
24
+ import { z } from 'zod'
25
+ import { defineApiBackedAiTool } from '@open-mercato/ai-assistant/modules/ai_assistant/lib/api-backed-tool'
26
+ import type {
27
+ AiApiOperationRequest,
28
+ AiToolExecutionContext,
29
+ } from '@open-mercato/ai-assistant/modules/ai_assistant/lib/ai-api-operation-runner'
30
+ import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
31
+ import { CatalogProductPrice } from '../data/entities'
32
+ import { assertTenantScope, type CatalogAiToolDefinition, type CatalogToolContext } from './types'
33
+ import { listPriceKindsCore } from './_shared'
34
+
35
+ function resolveEm(ctx: CatalogToolContext | AiToolExecutionContext): EntityManager {
36
+ return ctx.container.resolve<EntityManager>('em')
37
+ }
38
+
39
+ function buildScope(ctx: CatalogToolContext | AiToolExecutionContext, tenantId: string) {
40
+ return { tenantId, organizationId: ctx.organizationId }
41
+ }
42
+
43
+ const listPricesInput = z
44
+ .object({
45
+ productId: z.string().uuid().optional().describe('Restrict to prices attached to this product.'),
46
+ variantId: z.string().uuid().optional().describe('Restrict to prices attached to this variant.'),
47
+ priceKindId: z.string().uuid().optional().describe('Restrict to this price kind.'),
48
+ limit: z.number().int().min(1).max(100).optional().describe('Max rows (default 50, max 100).'),
49
+ offset: z.number().int().min(0).optional().describe('Rows to skip (default 0).'),
50
+ })
51
+ .passthrough()
52
+
53
+ type ListPricesInput = z.infer<typeof listPricesInput>
54
+
55
+ type ListPricesApiItem = {
56
+ id?: string
57
+ product_id?: string | null
58
+ productId?: string | null
59
+ variant_id?: string | null
60
+ variantId?: string | null
61
+ offer_id?: string | null
62
+ offerId?: string | null
63
+ price_kind_id?: string | null
64
+ priceKindId?: string | null
65
+ currency_code?: string | null
66
+ currencyCode?: string | null
67
+ kind?: string | null
68
+ min_quantity?: number | null
69
+ minQuantity?: number | null
70
+ max_quantity?: number | null
71
+ maxQuantity?: number | null
72
+ unit_price_net?: string | number | null
73
+ unitPriceNet?: string | number | null
74
+ unit_price_gross?: string | number | null
75
+ unitPriceGross?: string | number | null
76
+ tax_rate?: string | number | null
77
+ taxRate?: string | number | null
78
+ tax_amount?: string | number | null
79
+ taxAmount?: string | number | null
80
+ channel_id?: string | null
81
+ channelId?: string | null
82
+ user_id?: string | null
83
+ userId?: string | null
84
+ user_group_id?: string | null
85
+ userGroupId?: string | null
86
+ customer_id?: string | null
87
+ customerId?: string | null
88
+ customer_group_id?: string | null
89
+ customerGroupId?: string | null
90
+ starts_at?: string | null
91
+ startsAt?: string | null
92
+ ends_at?: string | null
93
+ endsAt?: string | null
94
+ organization_id?: string | null
95
+ organizationId?: string | null
96
+ tenant_id?: string | null
97
+ tenantId?: string | null
98
+ created_at?: string | null
99
+ createdAt?: string | null
100
+ }
101
+
102
+ type ListPricesApiResponse = {
103
+ items?: ListPricesApiItem[]
104
+ total?: number
105
+ }
106
+
107
+ type ListPricesOutput = {
108
+ items: Array<Record<string, unknown>>
109
+ total: number
110
+ limit: number
111
+ offset: number
112
+ }
113
+
114
+ const listPricesTool = defineApiBackedAiTool<
115
+ ListPricesInput,
116
+ ListPricesApiResponse,
117
+ ListPricesOutput
118
+ >({
119
+ name: 'catalog.list_prices',
120
+ displayName: 'List prices',
121
+ description:
122
+ 'List catalog prices (base + offer-scoped) for the caller tenant + organization. Filters: product, variant, or price kind.',
123
+ inputSchema: listPricesInput,
124
+ requiredFeatures: ['catalog.products.view'],
125
+ toOperation: (input, ctx) => {
126
+ assertTenantScope(ctx as unknown as CatalogToolContext)
127
+ const limit = input.limit ?? 50
128
+ const offset = input.offset ?? 0
129
+ const page = Math.floor(offset / limit) + 1
130
+
131
+ const query: Record<string, string | number | boolean | null | undefined> = {
132
+ page,
133
+ pageSize: limit,
134
+ }
135
+ if (input.productId) query.productId = input.productId
136
+ if (input.variantId) query.variantId = input.variantId
137
+ if (input.priceKindId) query.priceKindId = input.priceKindId
138
+
139
+ const operation: AiApiOperationRequest = {
140
+ method: 'GET',
141
+ path: '/catalog/prices',
142
+ query,
143
+ }
144
+ return operation
145
+ },
146
+ mapResponse: (response, input) => {
147
+ const limit = input.limit ?? 50
148
+ const offset = input.offset ?? 0
149
+ const data = (response.data ?? {}) as ListPricesApiResponse
150
+ const rawItems: ListPricesApiItem[] = Array.isArray(data.items) ? data.items : []
151
+ return {
152
+ items: rawItems.map((row) => {
153
+ const startsAtRaw = row.starts_at ?? row.startsAt ?? null
154
+ const startsAt = startsAtRaw ? new Date(String(startsAtRaw)).toISOString() : null
155
+ const endsAtRaw = row.ends_at ?? row.endsAt ?? null
156
+ const endsAt = endsAtRaw ? new Date(String(endsAtRaw)).toISOString() : null
157
+ const createdAtRaw = row.created_at ?? row.createdAt ?? null
158
+ const createdAt = createdAtRaw ? new Date(String(createdAtRaw)).toISOString() : null
159
+ return {
160
+ id: row.id,
161
+ priceKindId: row.price_kind_id ?? row.priceKindId ?? null,
162
+ productId: row.product_id ?? row.productId ?? null,
163
+ variantId: row.variant_id ?? row.variantId ?? null,
164
+ offerId: row.offer_id ?? row.offerId ?? null,
165
+ currencyCode: row.currency_code ?? row.currencyCode ?? null,
166
+ kind: row.kind ?? null,
167
+ minQuantity: row.min_quantity ?? row.minQuantity ?? null,
168
+ maxQuantity: row.max_quantity ?? row.maxQuantity ?? null,
169
+ unitPriceNet: row.unit_price_net ?? row.unitPriceNet ?? null,
170
+ unitPriceGross: row.unit_price_gross ?? row.unitPriceGross ?? null,
171
+ taxRate: row.tax_rate ?? row.taxRate ?? null,
172
+ taxAmount: row.tax_amount ?? row.taxAmount ?? null,
173
+ channelId: row.channel_id ?? row.channelId ?? null,
174
+ userId: row.user_id ?? row.userId ?? null,
175
+ userGroupId: row.user_group_id ?? row.userGroupId ?? null,
176
+ customerId: row.customer_id ?? row.customerId ?? null,
177
+ customerGroupId: row.customer_group_id ?? row.customerGroupId ?? null,
178
+ startsAt,
179
+ endsAt,
180
+ organizationId: row.organization_id ?? row.organizationId ?? null,
181
+ tenantId: row.tenant_id ?? row.tenantId ?? null,
182
+ createdAt,
183
+ }
184
+ }),
185
+ total: typeof data.total === 'number' ? data.total : 0,
186
+ limit,
187
+ offset,
188
+ }
189
+ },
190
+ }) as unknown as CatalogAiToolDefinition
191
+
192
+ const listPriceKindsInput = z
193
+ .object({
194
+ limit: z.number().int().min(1).max(100).optional().describe('Max rows (default 50, max 100).'),
195
+ offset: z.number().int().min(0).optional().describe('Rows to skip (default 0).'),
196
+ })
197
+ .passthrough()
198
+
199
+ const listPriceKindsTool: CatalogAiToolDefinition = {
200
+ name: 'catalog.list_price_kinds_base',
201
+ displayName: 'List price kinds (base)',
202
+ description:
203
+ 'Enumerate the tenant price kinds. Base coverage tool — Step 3.11 (D18) owns `catalog.list_price_kinds` verbatim; this tool uses a distinct name to avoid collision.',
204
+ inputSchema: listPriceKindsInput,
205
+ requiredFeatures: ['catalog.settings.manage'],
206
+ tags: ['read', 'catalog'],
207
+ handler: async (rawInput, ctx) => {
208
+ const { tenantId } = assertTenantScope(ctx)
209
+ const input = listPriceKindsInput.parse(rawInput)
210
+ // Shared helper; Step 3.11 `catalog.list_price_kinds` uses the same core
211
+ // so both tools cannot drift.
212
+ return listPriceKindsCore(ctx, input, tenantId)
213
+ },
214
+ }
215
+
216
+ const listOffersInput = z
217
+ .object({
218
+ productId: z.string().uuid().optional().describe('Restrict to offers for this product.'),
219
+ variantId: z.string().uuid().optional().describe('Restrict to offers whose prices are variant-scoped.'),
220
+ active: z.boolean().optional().describe('When true, only active (non-archived) offers are returned.'),
221
+ limit: z.number().int().min(1).max(100).optional().describe('Max rows (default 50, max 100).'),
222
+ offset: z.number().int().min(0).optional().describe('Rows to skip (default 0).'),
223
+ })
224
+ .passthrough()
225
+
226
+ const NIL_UUID = '00000000-0000-0000-0000-000000000000'
227
+
228
+ type ListOffersInput = z.infer<typeof listOffersInput>
229
+
230
+ type ListOffersApiItem = {
231
+ id?: string
232
+ product_id?: string | null
233
+ productId?: string | null
234
+ channel_id?: string | null
235
+ channelId?: string | null
236
+ title?: string | null
237
+ description?: string | null
238
+ default_media_id?: string | null
239
+ defaultMediaId?: string | null
240
+ default_media_url?: string | null
241
+ defaultMediaUrl?: string | null
242
+ is_active?: boolean | null
243
+ isActive?: boolean | null
244
+ organization_id?: string | null
245
+ organizationId?: string | null
246
+ tenant_id?: string | null
247
+ tenantId?: string | null
248
+ created_at?: string | null
249
+ createdAt?: string | null
250
+ }
251
+
252
+ type ListOffersApiResponse = {
253
+ items?: ListOffersApiItem[]
254
+ total?: number
255
+ }
256
+
257
+ type ListOffersOutput = {
258
+ items: Array<Record<string, unknown>>
259
+ total: number
260
+ limit: number
261
+ offset: number
262
+ }
263
+
264
+ async function resolveOfferIdsForVariant(
265
+ ctx: AiToolExecutionContext | CatalogToolContext,
266
+ tenantId: string,
267
+ variantId: string,
268
+ ): Promise<string[]> {
269
+ const em = resolveEm(ctx)
270
+ const priceWhere: Record<string, unknown> = { tenantId, variant: variantId }
271
+ if (ctx.organizationId) priceWhere.organizationId = ctx.organizationId
272
+ const prices = await findWithDecryption<CatalogProductPrice>(
273
+ em,
274
+ CatalogProductPrice,
275
+ priceWhere as any,
276
+ undefined,
277
+ buildScope(ctx, tenantId),
278
+ )
279
+ const offerIds = prices
280
+ .map((price) => (price as any).offer)
281
+ .map((offer) => (offer && typeof offer === 'object' ? offer.id : offer))
282
+ .filter((value: string | null): value is string => typeof value === 'string' && value.length > 0)
283
+ return Array.from(new Set(offerIds))
284
+ }
285
+
286
+ const listOffersTool = defineApiBackedAiTool<
287
+ ListOffersInput,
288
+ ListOffersApiResponse,
289
+ ListOffersOutput
290
+ >({
291
+ name: 'catalog.list_offers',
292
+ displayName: 'List offers',
293
+ description:
294
+ 'List catalog offers for the caller tenant + organization, optionally narrowed to a product (or a variant via its prices).',
295
+ inputSchema: listOffersInput,
296
+ requiredFeatures: ['catalog.products.view'],
297
+ toOperation: async (input, ctx) => {
298
+ const { tenantId } = assertTenantScope(ctx as unknown as CatalogToolContext)
299
+ const limit = input.limit ?? 50
300
+ const offset = input.offset ?? 0
301
+ const page = Math.floor(offset / limit) + 1
302
+
303
+ const query: Record<string, string | number | boolean | null | undefined> = {
304
+ page,
305
+ pageSize: limit,
306
+ }
307
+ if (input.productId) query.productId = input.productId
308
+ if (input.active === true) query.isActive = 'true'
309
+
310
+ if (input.variantId) {
311
+ const offerIds = await resolveOfferIdsForVariant(ctx, tenantId, input.variantId)
312
+ if (offerIds.length === 0) {
313
+ // Empty match — feed a non-existent uuid so the route returns an
314
+ // empty page without us bypassing the API.
315
+ query.id = NIL_UUID
316
+ } else if (offerIds.length === 1) {
317
+ query.id = offerIds[0]
318
+ }
319
+ // For >1 offer ids the route's single-id filter cannot narrow; the
320
+ // mapper post-filters the unfiltered response by the resolved ids.
321
+ }
322
+
323
+ const operation: AiApiOperationRequest = {
324
+ method: 'GET',
325
+ path: '/catalog/offers',
326
+ query,
327
+ }
328
+ return operation
329
+ },
330
+ mapResponse: async (response, input, ctx) => {
331
+ const limit = input.limit ?? 50
332
+ const offset = input.offset ?? 0
333
+ const data = (response.data ?? {}) as ListOffersApiResponse
334
+ let rawItems: ListOffersApiItem[] = Array.isArray(data.items) ? data.items : []
335
+ let total = typeof data.total === 'number' ? data.total : 0
336
+
337
+ if (input.variantId) {
338
+ const { tenantId } = assertTenantScope(ctx as unknown as CatalogToolContext)
339
+ const offerIds = await resolveOfferIdsForVariant(ctx, tenantId, input.variantId)
340
+ const offerIdSet = new Set(offerIds)
341
+ rawItems = rawItems.filter((row) => typeof row.id === 'string' && offerIdSet.has(row.id))
342
+ total = rawItems.length
343
+ }
344
+
345
+ return {
346
+ items: rawItems.map((row) => {
347
+ const createdAtRaw = row.created_at ?? row.createdAt ?? null
348
+ const createdAt = createdAtRaw ? new Date(String(createdAtRaw)).toISOString() : null
349
+ return {
350
+ id: row.id,
351
+ title: row.title ?? '',
352
+ description: row.description ?? null,
353
+ channelId: row.channel_id ?? row.channelId ?? null,
354
+ productId: row.product_id ?? row.productId ?? null,
355
+ defaultMediaId: row.default_media_id ?? row.defaultMediaId ?? null,
356
+ defaultMediaUrl: row.default_media_url ?? row.defaultMediaUrl ?? null,
357
+ isActive: !!(row.is_active ?? row.isActive),
358
+ organizationId: row.organization_id ?? row.organizationId ?? null,
359
+ tenantId: row.tenant_id ?? row.tenantId ?? null,
360
+ createdAt,
361
+ }
362
+ }),
363
+ total,
364
+ limit,
365
+ offset,
366
+ }
367
+ },
368
+ }) as unknown as CatalogAiToolDefinition
369
+
370
+ export const pricesOffersAiTools: CatalogAiToolDefinition[] = [
371
+ listPricesTool,
372
+ listPriceKindsTool,
373
+ listOffersTool,
374
+ ]
375
+
376
+ export default pricesOffersAiTools