@open-mercato/core 0.6.5-develop.5240.2.bbd9d30275 → 0.6.5-develop.5266.2.9acdca82ec
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.
|
@@ -131,7 +131,11 @@ const listOffersTool = defineApiBackedAiTool({
|
|
|
131
131
|
displayName: "List offers",
|
|
132
132
|
description: "List catalog offers for the caller tenant + organization, optionally narrowed to a product (or a variant via its prices).",
|
|
133
133
|
inputSchema: listOffersInput,
|
|
134
|
-
|
|
134
|
+
// Must cover the underlying route. `GET /catalog/offers` requires
|
|
135
|
+
// `sales.channels.manage` (offers are sales-channel scoped); the API-backed
|
|
136
|
+
// runner fails closed when the tool's features don't cover the route's.
|
|
137
|
+
// `catalog.products.view` stays for the variant→offer resolution path.
|
|
138
|
+
requiredFeatures: ["catalog.products.view", "sales.channels.manage"],
|
|
135
139
|
toOperation: async (input, ctx) => {
|
|
136
140
|
const { tenantId } = assertTenantScope(ctx);
|
|
137
141
|
const limit = input.limit ?? 50;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/catalog/ai-tools/prices-offers-pack.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * `catalog.list_prices`, `catalog.list_price_kinds_base`, `catalog.list_offers`\n * (Phase 1 WS-C, Step 3.10).\n *\n * Read-only enumeration of prices (base + offer-bound), price kinds, and\n * offers for the caller tenant + organization. Mutation tools land in Step\n * 5.14 under the pending-action contract.\n *\n * `catalog.list_price_kinds_base` uses a distinct name on purpose \u2014 Step\n * 3.11 (D18) will own `catalog.list_price_kinds` verbatim; we keep both\n * names available so the D18 tool can layer merchandising-specific shape\n * over the base enumerator.\n *\n * Phase 3b of `.ai/specs/2026-04-27-ai-tools-api-backed-dry-refactor.md`:\n * `catalog.list_prices` and `catalog.list_offers` are now API-backed wrappers\n * over `GET /api/catalog/prices` and `GET /api/catalog/offers`. Tool names,\n * schemas, requiredFeatures, and output shapes are unchanged. The offers\n * route does not expose a `variantId` filter; the AI input is pre-resolved\n * via `CatalogProductPrice` to the matching offer ids and threaded through\n * the route's `id` filter (or post-filtered when more than one matches),\n * mirroring Phase 3a's `companyId` \u2192 `ids` trick for `customers.list_people`.\n */\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { z } from 'zod'\nimport { defineApiBackedAiTool } from '@open-mercato/ai-assistant/modules/ai_assistant/lib/api-backed-tool'\nimport type {\n AiApiOperationRequest,\n AiToolExecutionContext,\n} from '@open-mercato/ai-assistant/modules/ai_assistant/lib/ai-api-operation-runner'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { CatalogProductPrice } from '../data/entities'\nimport { assertTenantScope, type CatalogAiToolDefinition, type CatalogToolContext } from './types'\nimport { listPriceKindsCore } from './_shared'\n\nfunction resolveEm(ctx: CatalogToolContext | AiToolExecutionContext): EntityManager {\n return ctx.container.resolve<EntityManager>('em')\n}\n\nfunction buildScope(ctx: CatalogToolContext | AiToolExecutionContext, tenantId: string) {\n return { tenantId, organizationId: ctx.organizationId }\n}\n\nconst listPricesInput = z\n .object({\n productId: z.string().uuid().optional().describe('Restrict to prices attached to this product.'),\n variantId: z.string().uuid().optional().describe('Restrict to prices attached to this variant.'),\n priceKindId: z.string().uuid().optional().describe('Restrict to this price kind.'),\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\ntype ListPricesInput = z.infer<typeof listPricesInput>\n\ntype ListPricesApiItem = {\n id?: string\n product_id?: string | null\n productId?: string | null\n variant_id?: string | null\n variantId?: string | null\n offer_id?: string | null\n offerId?: string | null\n price_kind_id?: string | null\n priceKindId?: string | null\n currency_code?: string | null\n currencyCode?: string | null\n kind?: string | null\n min_quantity?: number | null\n minQuantity?: number | null\n max_quantity?: number | null\n maxQuantity?: number | null\n unit_price_net?: string | number | null\n unitPriceNet?: string | number | null\n unit_price_gross?: string | number | null\n unitPriceGross?: string | number | null\n tax_rate?: string | number | null\n taxRate?: string | number | null\n tax_amount?: string | number | null\n taxAmount?: string | number | null\n channel_id?: string | null\n channelId?: string | null\n user_id?: string | null\n userId?: string | null\n user_group_id?: string | null\n userGroupId?: string | null\n customer_id?: string | null\n customerId?: string | null\n customer_group_id?: string | null\n customerGroupId?: string | null\n starts_at?: string | null\n startsAt?: string | null\n ends_at?: string | null\n endsAt?: string | null\n organization_id?: string | null\n organizationId?: string | null\n tenant_id?: string | null\n tenantId?: string | null\n created_at?: string | null\n createdAt?: string | null\n}\n\ntype ListPricesApiResponse = {\n items?: ListPricesApiItem[]\n total?: number\n}\n\ntype ListPricesOutput = {\n items: Array<Record<string, unknown>>\n total: number\n limit: number\n offset: number\n}\n\nconst listPricesTool = defineApiBackedAiTool<\n ListPricesInput,\n ListPricesApiResponse,\n ListPricesOutput\n>({\n name: 'catalog.list_prices',\n displayName: 'List prices',\n description:\n 'List catalog prices (base + offer-scoped) for the caller tenant + organization. Filters: product, variant, or price kind.',\n inputSchema: listPricesInput,\n requiredFeatures: ['catalog.products.view'],\n toOperation: (input, ctx) => {\n assertTenantScope(ctx as unknown as CatalogToolContext)\n const limit = input.limit ?? 50\n const offset = input.offset ?? 0\n const page = Math.floor(offset / limit) + 1\n\n const query: Record<string, string | number | boolean | null | undefined> = {\n page,\n pageSize: limit,\n }\n if (input.productId) query.productId = input.productId\n if (input.variantId) query.variantId = input.variantId\n if (input.priceKindId) query.priceKindId = input.priceKindId\n\n const operation: AiApiOperationRequest = {\n method: 'GET',\n path: '/catalog/prices',\n query,\n }\n return operation\n },\n mapResponse: (response, input) => {\n const limit = input.limit ?? 50\n const offset = input.offset ?? 0\n const data = (response.data ?? {}) as ListPricesApiResponse\n const rawItems: ListPricesApiItem[] = Array.isArray(data.items) ? data.items : []\n return {\n items: rawItems.map((row) => {\n const startsAtRaw = row.starts_at ?? row.startsAt ?? null\n const startsAt = startsAtRaw ? new Date(String(startsAtRaw)).toISOString() : null\n const endsAtRaw = row.ends_at ?? row.endsAt ?? null\n const endsAt = endsAtRaw ? new Date(String(endsAtRaw)).toISOString() : null\n const createdAtRaw = row.created_at ?? row.createdAt ?? null\n const createdAt = createdAtRaw ? new Date(String(createdAtRaw)).toISOString() : null\n return {\n id: row.id,\n priceKindId: row.price_kind_id ?? row.priceKindId ?? null,\n productId: row.product_id ?? row.productId ?? null,\n variantId: row.variant_id ?? row.variantId ?? null,\n offerId: row.offer_id ?? row.offerId ?? null,\n currencyCode: row.currency_code ?? row.currencyCode ?? null,\n kind: row.kind ?? null,\n minQuantity: row.min_quantity ?? row.minQuantity ?? null,\n maxQuantity: row.max_quantity ?? row.maxQuantity ?? null,\n unitPriceNet: row.unit_price_net ?? row.unitPriceNet ?? null,\n unitPriceGross: row.unit_price_gross ?? row.unitPriceGross ?? null,\n taxRate: row.tax_rate ?? row.taxRate ?? null,\n taxAmount: row.tax_amount ?? row.taxAmount ?? null,\n channelId: row.channel_id ?? row.channelId ?? null,\n userId: row.user_id ?? row.userId ?? null,\n userGroupId: row.user_group_id ?? row.userGroupId ?? null,\n customerId: row.customer_id ?? row.customerId ?? null,\n customerGroupId: row.customer_group_id ?? row.customerGroupId ?? null,\n startsAt,\n endsAt,\n organizationId: row.organization_id ?? row.organizationId ?? null,\n tenantId: row.tenant_id ?? row.tenantId ?? null,\n createdAt,\n }\n }),\n total: typeof data.total === 'number' ? data.total : 0,\n limit,\n offset,\n }\n },\n}) as unknown as CatalogAiToolDefinition\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_base',\n displayName: 'List price kinds (base)',\n description:\n 'Enumerate the tenant price kinds. Base coverage tool \u2014 Step 3.11 (D18) owns `catalog.list_price_kinds` verbatim; this tool uses a distinct name to avoid collision.',\n inputSchema: listPriceKindsInput,\n requiredFeatures: ['catalog.settings.manage'],\n tags: ['read', 'catalog'],\n handler: async (rawInput, ctx) => {\n const { tenantId } = assertTenantScope(ctx)\n const input = listPriceKindsInput.parse(rawInput)\n // Shared helper; Step 3.11 `catalog.list_price_kinds` uses the same core\n // so both tools cannot drift.\n return listPriceKindsCore(ctx, input, tenantId)\n },\n}\n\nconst listOffersInput = z\n .object({\n productId: z.string().uuid().optional().describe('Restrict to offers for this product.'),\n variantId: z.string().uuid().optional().describe('Restrict to offers whose prices are variant-scoped.'),\n active: z.boolean().optional().describe('When true, only active (non-archived) offers are returned.'),\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 NIL_UUID = '00000000-0000-0000-0000-000000000000'\n\ntype ListOffersInput = z.infer<typeof listOffersInput>\n\ntype ListOffersApiItem = {\n id?: string\n product_id?: string | null\n productId?: string | null\n channel_id?: string | null\n channelId?: string | null\n title?: string | null\n description?: string | null\n default_media_id?: string | null\n defaultMediaId?: string | null\n default_media_url?: string | null\n defaultMediaUrl?: string | null\n is_active?: boolean | null\n isActive?: boolean | null\n organization_id?: string | null\n organizationId?: string | null\n tenant_id?: string | null\n tenantId?: string | null\n created_at?: string | null\n createdAt?: string | null\n}\n\ntype ListOffersApiResponse = {\n items?: ListOffersApiItem[]\n total?: number\n}\n\ntype ListOffersOutput = {\n items: Array<Record<string, unknown>>\n total: number\n limit: number\n offset: number\n}\n\nasync function resolveOfferIdsForVariant(\n ctx: AiToolExecutionContext | CatalogToolContext,\n tenantId: string,\n variantId: string,\n): Promise<string[]> {\n const em = resolveEm(ctx)\n const priceWhere: Record<string, unknown> = { tenantId, variant: variantId }\n if (ctx.organizationId) priceWhere.organizationId = ctx.organizationId\n const prices = await findWithDecryption<CatalogProductPrice>(\n em,\n CatalogProductPrice,\n priceWhere as any,\n undefined,\n buildScope(ctx, tenantId),\n )\n const offerIds = prices\n .map((price) => (price as any).offer)\n .map((offer) => (offer && typeof offer === 'object' ? offer.id : offer))\n .filter((value: string | null): value is string => typeof value === 'string' && value.length > 0)\n return Array.from(new Set(offerIds))\n}\n\nconst listOffersTool = defineApiBackedAiTool<\n ListOffersInput,\n ListOffersApiResponse,\n ListOffersOutput\n>({\n name: 'catalog.list_offers',\n displayName: 'List offers',\n description:\n 'List catalog offers for the caller tenant + organization, optionally narrowed to a product (or a variant via its prices).',\n inputSchema: listOffersInput,\n requiredFeatures: ['catalog.products.view'],\n toOperation: async (input, ctx) => {\n const { tenantId } = assertTenantScope(ctx as unknown as CatalogToolContext)\n const limit = input.limit ?? 50\n const offset = input.offset ?? 0\n const page = Math.floor(offset / limit) + 1\n\n const query: Record<string, string | number | boolean | null | undefined> = {\n page,\n pageSize: limit,\n }\n if (input.productId) query.productId = input.productId\n if (input.active === true) query.isActive = 'true'\n\n if (input.variantId) {\n const offerIds = await resolveOfferIdsForVariant(ctx, tenantId, input.variantId)\n if (offerIds.length === 0) {\n // Empty match \u2014 feed a non-existent uuid so the route returns an\n // empty page without us bypassing the API.\n query.id = NIL_UUID\n } else if (offerIds.length === 1) {\n query.id = offerIds[0]\n }\n // For >1 offer ids the route's single-id filter cannot narrow; the\n // mapper post-filters the unfiltered response by the resolved ids.\n }\n\n const operation: AiApiOperationRequest = {\n method: 'GET',\n path: '/catalog/offers',\n query,\n }\n return operation\n },\n mapResponse: async (response, input, ctx) => {\n const limit = input.limit ?? 50\n const offset = input.offset ?? 0\n const data = (response.data ?? {}) as ListOffersApiResponse\n let rawItems: ListOffersApiItem[] = Array.isArray(data.items) ? data.items : []\n let total = typeof data.total === 'number' ? data.total : 0\n\n if (input.variantId) {\n const { tenantId } = assertTenantScope(ctx as unknown as CatalogToolContext)\n const offerIds = await resolveOfferIdsForVariant(ctx, tenantId, input.variantId)\n const offerIdSet = new Set(offerIds)\n rawItems = rawItems.filter((row) => typeof row.id === 'string' && offerIdSet.has(row.id))\n total = rawItems.length\n }\n\n return {\n items: rawItems.map((row) => {\n const createdAtRaw = row.created_at ?? row.createdAt ?? null\n const createdAt = createdAtRaw ? new Date(String(createdAtRaw)).toISOString() : null\n return {\n id: row.id,\n title: row.title ?? '',\n description: row.description ?? null,\n channelId: row.channel_id ?? row.channelId ?? null,\n productId: row.product_id ?? row.productId ?? null,\n defaultMediaId: row.default_media_id ?? row.defaultMediaId ?? null,\n defaultMediaUrl: row.default_media_url ?? row.defaultMediaUrl ?? null,\n isActive: !!(row.is_active ?? row.isActive),\n organizationId: row.organization_id ?? row.organizationId ?? null,\n tenantId: row.tenant_id ?? row.tenantId ?? null,\n createdAt,\n }\n }),\n total,\n limit,\n offset,\n }\n },\n}) as unknown as CatalogAiToolDefinition\n\nexport const pricesOffersAiTools: CatalogAiToolDefinition[] = [\n listPricesTool,\n listPriceKindsTool,\n listOffersTool,\n]\n\nexport default pricesOffersAiTools\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["/**\n * `catalog.list_prices`, `catalog.list_price_kinds_base`, `catalog.list_offers`\n * (Phase 1 WS-C, Step 3.10).\n *\n * Read-only enumeration of prices (base + offer-bound), price kinds, and\n * offers for the caller tenant + organization. Mutation tools land in Step\n * 5.14 under the pending-action contract.\n *\n * `catalog.list_price_kinds_base` uses a distinct name on purpose \u2014 Step\n * 3.11 (D18) will own `catalog.list_price_kinds` verbatim; we keep both\n * names available so the D18 tool can layer merchandising-specific shape\n * over the base enumerator.\n *\n * Phase 3b of `.ai/specs/2026-04-27-ai-tools-api-backed-dry-refactor.md`:\n * `catalog.list_prices` and `catalog.list_offers` are now API-backed wrappers\n * over `GET /api/catalog/prices` and `GET /api/catalog/offers`. Tool names,\n * schemas, requiredFeatures, and output shapes are unchanged. The offers\n * route does not expose a `variantId` filter; the AI input is pre-resolved\n * via `CatalogProductPrice` to the matching offer ids and threaded through\n * the route's `id` filter (or post-filtered when more than one matches),\n * mirroring Phase 3a's `companyId` \u2192 `ids` trick for `customers.list_people`.\n */\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { z } from 'zod'\nimport { defineApiBackedAiTool } from '@open-mercato/ai-assistant/modules/ai_assistant/lib/api-backed-tool'\nimport type {\n AiApiOperationRequest,\n AiToolExecutionContext,\n} from '@open-mercato/ai-assistant/modules/ai_assistant/lib/ai-api-operation-runner'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { CatalogProductPrice } from '../data/entities'\nimport { assertTenantScope, type CatalogAiToolDefinition, type CatalogToolContext } from './types'\nimport { listPriceKindsCore } from './_shared'\n\nfunction resolveEm(ctx: CatalogToolContext | AiToolExecutionContext): EntityManager {\n return ctx.container.resolve<EntityManager>('em')\n}\n\nfunction buildScope(ctx: CatalogToolContext | AiToolExecutionContext, tenantId: string) {\n return { tenantId, organizationId: ctx.organizationId }\n}\n\nconst listPricesInput = z\n .object({\n productId: z.string().uuid().optional().describe('Restrict to prices attached to this product.'),\n variantId: z.string().uuid().optional().describe('Restrict to prices attached to this variant.'),\n priceKindId: z.string().uuid().optional().describe('Restrict to this price kind.'),\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\ntype ListPricesInput = z.infer<typeof listPricesInput>\n\ntype ListPricesApiItem = {\n id?: string\n product_id?: string | null\n productId?: string | null\n variant_id?: string | null\n variantId?: string | null\n offer_id?: string | null\n offerId?: string | null\n price_kind_id?: string | null\n priceKindId?: string | null\n currency_code?: string | null\n currencyCode?: string | null\n kind?: string | null\n min_quantity?: number | null\n minQuantity?: number | null\n max_quantity?: number | null\n maxQuantity?: number | null\n unit_price_net?: string | number | null\n unitPriceNet?: string | number | null\n unit_price_gross?: string | number | null\n unitPriceGross?: string | number | null\n tax_rate?: string | number | null\n taxRate?: string | number | null\n tax_amount?: string | number | null\n taxAmount?: string | number | null\n channel_id?: string | null\n channelId?: string | null\n user_id?: string | null\n userId?: string | null\n user_group_id?: string | null\n userGroupId?: string | null\n customer_id?: string | null\n customerId?: string | null\n customer_group_id?: string | null\n customerGroupId?: string | null\n starts_at?: string | null\n startsAt?: string | null\n ends_at?: string | null\n endsAt?: string | null\n organization_id?: string | null\n organizationId?: string | null\n tenant_id?: string | null\n tenantId?: string | null\n created_at?: string | null\n createdAt?: string | null\n}\n\ntype ListPricesApiResponse = {\n items?: ListPricesApiItem[]\n total?: number\n}\n\ntype ListPricesOutput = {\n items: Array<Record<string, unknown>>\n total: number\n limit: number\n offset: number\n}\n\nconst listPricesTool = defineApiBackedAiTool<\n ListPricesInput,\n ListPricesApiResponse,\n ListPricesOutput\n>({\n name: 'catalog.list_prices',\n displayName: 'List prices',\n description:\n 'List catalog prices (base + offer-scoped) for the caller tenant + organization. Filters: product, variant, or price kind.',\n inputSchema: listPricesInput,\n requiredFeatures: ['catalog.products.view'],\n toOperation: (input, ctx) => {\n assertTenantScope(ctx as unknown as CatalogToolContext)\n const limit = input.limit ?? 50\n const offset = input.offset ?? 0\n const page = Math.floor(offset / limit) + 1\n\n const query: Record<string, string | number | boolean | null | undefined> = {\n page,\n pageSize: limit,\n }\n if (input.productId) query.productId = input.productId\n if (input.variantId) query.variantId = input.variantId\n if (input.priceKindId) query.priceKindId = input.priceKindId\n\n const operation: AiApiOperationRequest = {\n method: 'GET',\n path: '/catalog/prices',\n query,\n }\n return operation\n },\n mapResponse: (response, input) => {\n const limit = input.limit ?? 50\n const offset = input.offset ?? 0\n const data = (response.data ?? {}) as ListPricesApiResponse\n const rawItems: ListPricesApiItem[] = Array.isArray(data.items) ? data.items : []\n return {\n items: rawItems.map((row) => {\n const startsAtRaw = row.starts_at ?? row.startsAt ?? null\n const startsAt = startsAtRaw ? new Date(String(startsAtRaw)).toISOString() : null\n const endsAtRaw = row.ends_at ?? row.endsAt ?? null\n const endsAt = endsAtRaw ? new Date(String(endsAtRaw)).toISOString() : null\n const createdAtRaw = row.created_at ?? row.createdAt ?? null\n const createdAt = createdAtRaw ? new Date(String(createdAtRaw)).toISOString() : null\n return {\n id: row.id,\n priceKindId: row.price_kind_id ?? row.priceKindId ?? null,\n productId: row.product_id ?? row.productId ?? null,\n variantId: row.variant_id ?? row.variantId ?? null,\n offerId: row.offer_id ?? row.offerId ?? null,\n currencyCode: row.currency_code ?? row.currencyCode ?? null,\n kind: row.kind ?? null,\n minQuantity: row.min_quantity ?? row.minQuantity ?? null,\n maxQuantity: row.max_quantity ?? row.maxQuantity ?? null,\n unitPriceNet: row.unit_price_net ?? row.unitPriceNet ?? null,\n unitPriceGross: row.unit_price_gross ?? row.unitPriceGross ?? null,\n taxRate: row.tax_rate ?? row.taxRate ?? null,\n taxAmount: row.tax_amount ?? row.taxAmount ?? null,\n channelId: row.channel_id ?? row.channelId ?? null,\n userId: row.user_id ?? row.userId ?? null,\n userGroupId: row.user_group_id ?? row.userGroupId ?? null,\n customerId: row.customer_id ?? row.customerId ?? null,\n customerGroupId: row.customer_group_id ?? row.customerGroupId ?? null,\n startsAt,\n endsAt,\n organizationId: row.organization_id ?? row.organizationId ?? null,\n tenantId: row.tenant_id ?? row.tenantId ?? null,\n createdAt,\n }\n }),\n total: typeof data.total === 'number' ? data.total : 0,\n limit,\n offset,\n }\n },\n}) as unknown as CatalogAiToolDefinition\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_base',\n displayName: 'List price kinds (base)',\n description:\n 'Enumerate the tenant price kinds. Base coverage tool \u2014 Step 3.11 (D18) owns `catalog.list_price_kinds` verbatim; this tool uses a distinct name to avoid collision.',\n inputSchema: listPriceKindsInput,\n requiredFeatures: ['catalog.settings.manage'],\n tags: ['read', 'catalog'],\n handler: async (rawInput, ctx) => {\n const { tenantId } = assertTenantScope(ctx)\n const input = listPriceKindsInput.parse(rawInput)\n // Shared helper; Step 3.11 `catalog.list_price_kinds` uses the same core\n // so both tools cannot drift.\n return listPriceKindsCore(ctx, input, tenantId)\n },\n}\n\nconst listOffersInput = z\n .object({\n productId: z.string().uuid().optional().describe('Restrict to offers for this product.'),\n variantId: z.string().uuid().optional().describe('Restrict to offers whose prices are variant-scoped.'),\n active: z.boolean().optional().describe('When true, only active (non-archived) offers are returned.'),\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 NIL_UUID = '00000000-0000-0000-0000-000000000000'\n\ntype ListOffersInput = z.infer<typeof listOffersInput>\n\ntype ListOffersApiItem = {\n id?: string\n product_id?: string | null\n productId?: string | null\n channel_id?: string | null\n channelId?: string | null\n title?: string | null\n description?: string | null\n default_media_id?: string | null\n defaultMediaId?: string | null\n default_media_url?: string | null\n defaultMediaUrl?: string | null\n is_active?: boolean | null\n isActive?: boolean | null\n organization_id?: string | null\n organizationId?: string | null\n tenant_id?: string | null\n tenantId?: string | null\n created_at?: string | null\n createdAt?: string | null\n}\n\ntype ListOffersApiResponse = {\n items?: ListOffersApiItem[]\n total?: number\n}\n\ntype ListOffersOutput = {\n items: Array<Record<string, unknown>>\n total: number\n limit: number\n offset: number\n}\n\nasync function resolveOfferIdsForVariant(\n ctx: AiToolExecutionContext | CatalogToolContext,\n tenantId: string,\n variantId: string,\n): Promise<string[]> {\n const em = resolveEm(ctx)\n const priceWhere: Record<string, unknown> = { tenantId, variant: variantId }\n if (ctx.organizationId) priceWhere.organizationId = ctx.organizationId\n const prices = await findWithDecryption<CatalogProductPrice>(\n em,\n CatalogProductPrice,\n priceWhere as any,\n undefined,\n buildScope(ctx, tenantId),\n )\n const offerIds = prices\n .map((price) => (price as any).offer)\n .map((offer) => (offer && typeof offer === 'object' ? offer.id : offer))\n .filter((value: string | null): value is string => typeof value === 'string' && value.length > 0)\n return Array.from(new Set(offerIds))\n}\n\nconst listOffersTool = defineApiBackedAiTool<\n ListOffersInput,\n ListOffersApiResponse,\n ListOffersOutput\n>({\n name: 'catalog.list_offers',\n displayName: 'List offers',\n description:\n 'List catalog offers for the caller tenant + organization, optionally narrowed to a product (or a variant via its prices).',\n inputSchema: listOffersInput,\n // Must cover the underlying route. `GET /catalog/offers` requires\n // `sales.channels.manage` (offers are sales-channel scoped); the API-backed\n // runner fails closed when the tool's features don't cover the route's.\n // `catalog.products.view` stays for the variant\u2192offer resolution path.\n requiredFeatures: ['catalog.products.view', 'sales.channels.manage'],\n toOperation: async (input, ctx) => {\n const { tenantId } = assertTenantScope(ctx as unknown as CatalogToolContext)\n const limit = input.limit ?? 50\n const offset = input.offset ?? 0\n const page = Math.floor(offset / limit) + 1\n\n const query: Record<string, string | number | boolean | null | undefined> = {\n page,\n pageSize: limit,\n }\n if (input.productId) query.productId = input.productId\n if (input.active === true) query.isActive = 'true'\n\n if (input.variantId) {\n const offerIds = await resolveOfferIdsForVariant(ctx, tenantId, input.variantId)\n if (offerIds.length === 0) {\n // Empty match \u2014 feed a non-existent uuid so the route returns an\n // empty page without us bypassing the API.\n query.id = NIL_UUID\n } else if (offerIds.length === 1) {\n query.id = offerIds[0]\n }\n // For >1 offer ids the route's single-id filter cannot narrow; the\n // mapper post-filters the unfiltered response by the resolved ids.\n }\n\n const operation: AiApiOperationRequest = {\n method: 'GET',\n path: '/catalog/offers',\n query,\n }\n return operation\n },\n mapResponse: async (response, input, ctx) => {\n const limit = input.limit ?? 50\n const offset = input.offset ?? 0\n const data = (response.data ?? {}) as ListOffersApiResponse\n let rawItems: ListOffersApiItem[] = Array.isArray(data.items) ? data.items : []\n let total = typeof data.total === 'number' ? data.total : 0\n\n if (input.variantId) {\n const { tenantId } = assertTenantScope(ctx as unknown as CatalogToolContext)\n const offerIds = await resolveOfferIdsForVariant(ctx, tenantId, input.variantId)\n const offerIdSet = new Set(offerIds)\n rawItems = rawItems.filter((row) => typeof row.id === 'string' && offerIdSet.has(row.id))\n total = rawItems.length\n }\n\n return {\n items: rawItems.map((row) => {\n const createdAtRaw = row.created_at ?? row.createdAt ?? null\n const createdAt = createdAtRaw ? new Date(String(createdAtRaw)).toISOString() : null\n return {\n id: row.id,\n title: row.title ?? '',\n description: row.description ?? null,\n channelId: row.channel_id ?? row.channelId ?? null,\n productId: row.product_id ?? row.productId ?? null,\n defaultMediaId: row.default_media_id ?? row.defaultMediaId ?? null,\n defaultMediaUrl: row.default_media_url ?? row.defaultMediaUrl ?? null,\n isActive: !!(row.is_active ?? row.isActive),\n organizationId: row.organization_id ?? row.organizationId ?? null,\n tenantId: row.tenant_id ?? row.tenantId ?? null,\n createdAt,\n }\n }),\n total,\n limit,\n offset,\n }\n },\n}) as unknown as CatalogAiToolDefinition\n\nexport const pricesOffersAiTools: CatalogAiToolDefinition[] = [\n listPricesTool,\n listPriceKindsTool,\n listOffersTool,\n]\n\nexport default pricesOffersAiTools\n"],
|
|
5
|
+
"mappings": "AAuBA,SAAS,SAAS;AAClB,SAAS,6BAA6B;AAKtC,SAAS,0BAA0B;AACnC,SAAS,2BAA2B;AACpC,SAAS,yBAAgF;AACzF,SAAS,0BAA0B;AAEnC,SAAS,UAAU,KAAiE;AAClF,SAAO,IAAI,UAAU,QAAuB,IAAI;AAClD;AAEA,SAAS,WAAW,KAAkD,UAAkB;AACtF,SAAO,EAAE,UAAU,gBAAgB,IAAI,eAAe;AACxD;AAEA,MAAM,kBAAkB,EACrB,OAAO;AAAA,EACN,WAAW,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,8CAA8C;AAAA,EAC/F,WAAW,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,8CAA8C;AAAA,EAC/F,aAAa,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,8BAA8B;AAAA,EACjF,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,SAAS,EAAE,SAAS,iCAAiC;AAAA,EAC7F,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,SAAS,EAAE,SAAS,2BAA2B;AACjF,CAAC,EACA,YAAY;AA+Df,MAAM,iBAAiB,sBAIrB;AAAA,EACA,MAAM;AAAA,EACN,aAAa;AAAA,EACb,aACE;AAAA,EACF,aAAa;AAAA,EACb,kBAAkB,CAAC,uBAAuB;AAAA,EAC1C,aAAa,CAAC,OAAO,QAAQ;AAC3B,sBAAkB,GAAoC;AACtD,UAAM,QAAQ,MAAM,SAAS;AAC7B,UAAM,SAAS,MAAM,UAAU;AAC/B,UAAM,OAAO,KAAK,MAAM,SAAS,KAAK,IAAI;AAE1C,UAAM,QAAsE;AAAA,MAC1E;AAAA,MACA,UAAU;AAAA,IACZ;AACA,QAAI,MAAM,UAAW,OAAM,YAAY,MAAM;AAC7C,QAAI,MAAM,UAAW,OAAM,YAAY,MAAM;AAC7C,QAAI,MAAM,YAAa,OAAM,cAAc,MAAM;AAEjD,UAAM,YAAmC;AAAA,MACvC,QAAQ;AAAA,MACR,MAAM;AAAA,MACN;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAAA,EACA,aAAa,CAAC,UAAU,UAAU;AAChC,UAAM,QAAQ,MAAM,SAAS;AAC7B,UAAM,SAAS,MAAM,UAAU;AAC/B,UAAM,OAAQ,SAAS,QAAQ,CAAC;AAChC,UAAM,WAAgC,MAAM,QAAQ,KAAK,KAAK,IAAI,KAAK,QAAQ,CAAC;AAChF,WAAO;AAAA,MACL,OAAO,SAAS,IAAI,CAAC,QAAQ;AAC3B,cAAM,cAAc,IAAI,aAAa,IAAI,YAAY;AACrD,cAAM,WAAW,cAAc,IAAI,KAAK,OAAO,WAAW,CAAC,EAAE,YAAY,IAAI;AAC7E,cAAM,YAAY,IAAI,WAAW,IAAI,UAAU;AAC/C,cAAM,SAAS,YAAY,IAAI,KAAK,OAAO,SAAS,CAAC,EAAE,YAAY,IAAI;AACvE,cAAM,eAAe,IAAI,cAAc,IAAI,aAAa;AACxD,cAAM,YAAY,eAAe,IAAI,KAAK,OAAO,YAAY,CAAC,EAAE,YAAY,IAAI;AAChF,eAAO;AAAA,UACL,IAAI,IAAI;AAAA,UACR,aAAa,IAAI,iBAAiB,IAAI,eAAe;AAAA,UACrD,WAAW,IAAI,cAAc,IAAI,aAAa;AAAA,UAC9C,WAAW,IAAI,cAAc,IAAI,aAAa;AAAA,UAC9C,SAAS,IAAI,YAAY,IAAI,WAAW;AAAA,UACxC,cAAc,IAAI,iBAAiB,IAAI,gBAAgB;AAAA,UACvD,MAAM,IAAI,QAAQ;AAAA,UAClB,aAAa,IAAI,gBAAgB,IAAI,eAAe;AAAA,UACpD,aAAa,IAAI,gBAAgB,IAAI,eAAe;AAAA,UACpD,cAAc,IAAI,kBAAkB,IAAI,gBAAgB;AAAA,UACxD,gBAAgB,IAAI,oBAAoB,IAAI,kBAAkB;AAAA,UAC9D,SAAS,IAAI,YAAY,IAAI,WAAW;AAAA,UACxC,WAAW,IAAI,cAAc,IAAI,aAAa;AAAA,UAC9C,WAAW,IAAI,cAAc,IAAI,aAAa;AAAA,UAC9C,QAAQ,IAAI,WAAW,IAAI,UAAU;AAAA,UACrC,aAAa,IAAI,iBAAiB,IAAI,eAAe;AAAA,UACrD,YAAY,IAAI,eAAe,IAAI,cAAc;AAAA,UACjD,iBAAiB,IAAI,qBAAqB,IAAI,mBAAmB;AAAA,UACjE;AAAA,UACA;AAAA,UACA,gBAAgB,IAAI,mBAAmB,IAAI,kBAAkB;AAAA,UAC7D,UAAU,IAAI,aAAa,IAAI,YAAY;AAAA,UAC3C;AAAA,QACF;AAAA,MACF,CAAC;AAAA,MACD,OAAO,OAAO,KAAK,UAAU,WAAW,KAAK,QAAQ;AAAA,MACrD;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACF,CAAC;AAED,MAAM,sBAAsB,EACzB,OAAO;AAAA,EACN,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,SAAS,EAAE,SAAS,iCAAiC;AAAA,EAC7F,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,SAAS,EAAE,SAAS,2BAA2B;AACjF,CAAC,EACA,YAAY;AAEf,MAAM,qBAA8C;AAAA,EAClD,MAAM;AAAA,EACN,aAAa;AAAA,EACb,aACE;AAAA,EACF,aAAa;AAAA,EACb,kBAAkB,CAAC,yBAAyB;AAAA,EAC5C,MAAM,CAAC,QAAQ,SAAS;AAAA,EACxB,SAAS,OAAO,UAAU,QAAQ;AAChC,UAAM,EAAE,SAAS,IAAI,kBAAkB,GAAG;AAC1C,UAAM,QAAQ,oBAAoB,MAAM,QAAQ;AAGhD,WAAO,mBAAmB,KAAK,OAAO,QAAQ;AAAA,EAChD;AACF;AAEA,MAAM,kBAAkB,EACrB,OAAO;AAAA,EACN,WAAW,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,sCAAsC;AAAA,EACvF,WAAW,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,qDAAqD;AAAA,EACtG,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,4DAA4D;AAAA,EACpG,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,SAAS,EAAE,SAAS,iCAAiC;AAAA,EAC7F,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,SAAS,EAAE,SAAS,2BAA2B;AACjF,CAAC,EACA,YAAY;AAEf,MAAM,WAAW;AAsCjB,eAAe,0BACb,KACA,UACA,WACmB;AACnB,QAAM,KAAK,UAAU,GAAG;AACxB,QAAM,aAAsC,EAAE,UAAU,SAAS,UAAU;AAC3E,MAAI,IAAI,eAAgB,YAAW,iBAAiB,IAAI;AACxD,QAAM,SAAS,MAAM;AAAA,IACnB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,WAAW,KAAK,QAAQ;AAAA,EAC1B;AACA,QAAM,WAAW,OACd,IAAI,CAAC,UAAW,MAAc,KAAK,EACnC,IAAI,CAAC,UAAW,SAAS,OAAO,UAAU,WAAW,MAAM,KAAK,KAAM,EACtE,OAAO,CAAC,UAA0C,OAAO,UAAU,YAAY,MAAM,SAAS,CAAC;AAClG,SAAO,MAAM,KAAK,IAAI,IAAI,QAAQ,CAAC;AACrC;AAEA,MAAM,iBAAiB,sBAIrB;AAAA,EACA,MAAM;AAAA,EACN,aAAa;AAAA,EACb,aACE;AAAA,EACF,aAAa;AAAA;AAAA;AAAA;AAAA;AAAA,EAKb,kBAAkB,CAAC,yBAAyB,uBAAuB;AAAA,EACnE,aAAa,OAAO,OAAO,QAAQ;AACjC,UAAM,EAAE,SAAS,IAAI,kBAAkB,GAAoC;AAC3E,UAAM,QAAQ,MAAM,SAAS;AAC7B,UAAM,SAAS,MAAM,UAAU;AAC/B,UAAM,OAAO,KAAK,MAAM,SAAS,KAAK,IAAI;AAE1C,UAAM,QAAsE;AAAA,MAC1E;AAAA,MACA,UAAU;AAAA,IACZ;AACA,QAAI,MAAM,UAAW,OAAM,YAAY,MAAM;AAC7C,QAAI,MAAM,WAAW,KAAM,OAAM,WAAW;AAE5C,QAAI,MAAM,WAAW;AACnB,YAAM,WAAW,MAAM,0BAA0B,KAAK,UAAU,MAAM,SAAS;AAC/E,UAAI,SAAS,WAAW,GAAG;AAGzB,cAAM,KAAK;AAAA,MACb,WAAW,SAAS,WAAW,GAAG;AAChC,cAAM,KAAK,SAAS,CAAC;AAAA,MACvB;AAAA,IAGF;AAEA,UAAM,YAAmC;AAAA,MACvC,QAAQ;AAAA,MACR,MAAM;AAAA,MACN;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAAA,EACA,aAAa,OAAO,UAAU,OAAO,QAAQ;AAC3C,UAAM,QAAQ,MAAM,SAAS;AAC7B,UAAM,SAAS,MAAM,UAAU;AAC/B,UAAM,OAAQ,SAAS,QAAQ,CAAC;AAChC,QAAI,WAAgC,MAAM,QAAQ,KAAK,KAAK,IAAI,KAAK,QAAQ,CAAC;AAC9E,QAAI,QAAQ,OAAO,KAAK,UAAU,WAAW,KAAK,QAAQ;AAE1D,QAAI,MAAM,WAAW;AACnB,YAAM,EAAE,SAAS,IAAI,kBAAkB,GAAoC;AAC3E,YAAM,WAAW,MAAM,0BAA0B,KAAK,UAAU,MAAM,SAAS;AAC/E,YAAM,aAAa,IAAI,IAAI,QAAQ;AACnC,iBAAW,SAAS,OAAO,CAAC,QAAQ,OAAO,IAAI,OAAO,YAAY,WAAW,IAAI,IAAI,EAAE,CAAC;AACxF,cAAQ,SAAS;AAAA,IACnB;AAEA,WAAO;AAAA,MACL,OAAO,SAAS,IAAI,CAAC,QAAQ;AAC3B,cAAM,eAAe,IAAI,cAAc,IAAI,aAAa;AACxD,cAAM,YAAY,eAAe,IAAI,KAAK,OAAO,YAAY,CAAC,EAAE,YAAY,IAAI;AAChF,eAAO;AAAA,UACL,IAAI,IAAI;AAAA,UACR,OAAO,IAAI,SAAS;AAAA,UACpB,aAAa,IAAI,eAAe;AAAA,UAChC,WAAW,IAAI,cAAc,IAAI,aAAa;AAAA,UAC9C,WAAW,IAAI,cAAc,IAAI,aAAa;AAAA,UAC9C,gBAAgB,IAAI,oBAAoB,IAAI,kBAAkB;AAAA,UAC9D,iBAAiB,IAAI,qBAAqB,IAAI,mBAAmB;AAAA,UACjE,UAAU,CAAC,EAAE,IAAI,aAAa,IAAI;AAAA,UAClC,gBAAgB,IAAI,mBAAmB,IAAI,kBAAkB;AAAA,UAC7D,UAAU,IAAI,aAAa,IAAI,YAAY;AAAA,UAC3C;AAAA,QACF;AAAA,MACF,CAAC;AAAA,MACD;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACF,CAAC;AAEM,MAAM,sBAAiD;AAAA,EAC5D;AAAA,EACA;AAAA,EACA;AACF;AAEA,IAAO,6BAAQ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.6.5-develop.
|
|
3
|
+
"version": "0.6.5-develop.5266.2.9acdca82ec",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -245,16 +245,16 @@
|
|
|
245
245
|
"zod": "^4.4.3"
|
|
246
246
|
},
|
|
247
247
|
"peerDependencies": {
|
|
248
|
-
"@open-mercato/ai-assistant": "0.6.5-develop.
|
|
249
|
-
"@open-mercato/shared": "0.6.5-develop.
|
|
250
|
-
"@open-mercato/ui": "0.6.5-develop.
|
|
248
|
+
"@open-mercato/ai-assistant": "0.6.5-develop.5266.2.9acdca82ec",
|
|
249
|
+
"@open-mercato/shared": "0.6.5-develop.5266.2.9acdca82ec",
|
|
250
|
+
"@open-mercato/ui": "0.6.5-develop.5266.2.9acdca82ec",
|
|
251
251
|
"react": "^19.0.0",
|
|
252
252
|
"react-dom": "^19.0.0"
|
|
253
253
|
},
|
|
254
254
|
"devDependencies": {
|
|
255
|
-
"@open-mercato/ai-assistant": "0.6.5-develop.
|
|
256
|
-
"@open-mercato/shared": "0.6.5-develop.
|
|
257
|
-
"@open-mercato/ui": "0.6.5-develop.
|
|
255
|
+
"@open-mercato/ai-assistant": "0.6.5-develop.5266.2.9acdca82ec",
|
|
256
|
+
"@open-mercato/shared": "0.6.5-develop.5266.2.9acdca82ec",
|
|
257
|
+
"@open-mercato/ui": "0.6.5-develop.5266.2.9acdca82ec",
|
|
258
258
|
"@testing-library/dom": "^10.4.1",
|
|
259
259
|
"@testing-library/jest-dom": "^6.9.1",
|
|
260
260
|
"@testing-library/react": "^16.3.1",
|
|
@@ -293,7 +293,11 @@ const listOffersTool = defineApiBackedAiTool<
|
|
|
293
293
|
description:
|
|
294
294
|
'List catalog offers for the caller tenant + organization, optionally narrowed to a product (or a variant via its prices).',
|
|
295
295
|
inputSchema: listOffersInput,
|
|
296
|
-
|
|
296
|
+
// Must cover the underlying route. `GET /catalog/offers` requires
|
|
297
|
+
// `sales.channels.manage` (offers are sales-channel scoped); the API-backed
|
|
298
|
+
// runner fails closed when the tool's features don't cover the route's.
|
|
299
|
+
// `catalog.products.view` stays for the variant→offer resolution path.
|
|
300
|
+
requiredFeatures: ['catalog.products.view', 'sales.channels.manage'],
|
|
297
301
|
toOperation: async (input, ctx) => {
|
|
298
302
|
const { tenantId } = assertTenantScope(ctx as unknown as CatalogToolContext)
|
|
299
303
|
const limit = input.limit ?? 50
|