@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.
- package/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +13 -1
- package/dist/helpers/integration/api.js +29 -16
- package/dist/helpers/integration/api.js.map +2 -2
- package/dist/helpers/integration/auth.js +11 -6
- package/dist/helpers/integration/auth.js.map +3 -3
- package/dist/modules/auth/commands/roles.js +9 -12
- package/dist/modules/auth/commands/roles.js.map +2 -2
- package/dist/modules/catalog/ai-agents-context.js +147 -0
- package/dist/modules/catalog/ai-agents-context.js.map +7 -0
- package/dist/modules/catalog/ai-agents.js +383 -0
- package/dist/modules/catalog/ai-agents.js.map +7 -0
- package/dist/modules/catalog/ai-tools/_shared.js +318 -0
- package/dist/modules/catalog/ai-tools/_shared.js.map +7 -0
- package/dist/modules/catalog/ai-tools/authoring-pack.js +391 -0
- package/dist/modules/catalog/ai-tools/authoring-pack.js.map +7 -0
- package/dist/modules/catalog/ai-tools/categories-pack.js +167 -0
- package/dist/modules/catalog/ai-tools/categories-pack.js.map +7 -0
- package/dist/modules/catalog/ai-tools/configuration-pack.js +120 -0
- package/dist/modules/catalog/ai-tools/configuration-pack.js.map +7 -0
- package/dist/modules/catalog/ai-tools/media-tags-pack.js +107 -0
- package/dist/modules/catalog/ai-tools/media-tags-pack.js.map +7 -0
- package/dist/modules/catalog/ai-tools/merchandising-pack.js +429 -0
- package/dist/modules/catalog/ai-tools/merchandising-pack.js.map +7 -0
- package/dist/modules/catalog/ai-tools/mutation-pack.js +576 -0
- package/dist/modules/catalog/ai-tools/mutation-pack.js.map +7 -0
- package/dist/modules/catalog/ai-tools/prices-offers-pack.js +208 -0
- package/dist/modules/catalog/ai-tools/prices-offers-pack.js.map +7 -0
- package/dist/modules/catalog/ai-tools/products-pack.js +298 -0
- package/dist/modules/catalog/ai-tools/products-pack.js.map +7 -0
- package/dist/modules/catalog/ai-tools/stats-pack.js +57 -0
- package/dist/modules/catalog/ai-tools/stats-pack.js.map +7 -0
- package/dist/modules/catalog/ai-tools/types.js +10 -0
- package/dist/modules/catalog/ai-tools/types.js.map +7 -0
- package/dist/modules/catalog/ai-tools/variants-pack.js +75 -0
- package/dist/modules/catalog/ai-tools/variants-pack.js.map +7 -0
- package/dist/modules/catalog/ai-tools.js +28 -0
- package/dist/modules/catalog/ai-tools.js.map +7 -0
- package/dist/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.js +466 -0
- package/dist/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.js.map +7 -0
- package/dist/modules/catalog/backend/catalog/products/page.js +7 -1
- package/dist/modules/catalog/backend/catalog/products/page.js.map +2 -2
- package/dist/modules/catalog/components/CatalogStatsCard.js +91 -0
- package/dist/modules/catalog/components/CatalogStatsCard.js.map +7 -0
- package/dist/modules/catalog/components/products/ProductsDataTable.js +23 -3
- package/dist/modules/catalog/components/products/ProductsDataTable.js.map +2 -2
- package/dist/modules/catalog/events.js +7 -4
- package/dist/modules/catalog/events.js.map +2 -2
- package/dist/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.client.js +59 -0
- package/dist/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.client.js.map +7 -0
- package/dist/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.js +17 -0
- package/dist/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.js.map +7 -0
- package/dist/modules/catalog/widgets/injection/product-seo/widget.client.js +1 -1
- package/dist/modules/catalog/widgets/injection/product-seo/widget.client.js.map +2 -2
- package/dist/modules/catalog/widgets/injection-table.js +13 -1
- package/dist/modules/catalog/widgets/injection-table.js.map +2 -2
- package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.client.js +94 -0
- package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.client.js.map +7 -0
- package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.js +17 -0
- package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.js.map +7 -0
- package/dist/modules/customer_accounts/widgets/injection-table.js +9 -0
- package/dist/modules/customer_accounts/widgets/injection-table.js.map +2 -2
- package/dist/modules/customers/ai-agents-context.js +96 -0
- package/dist/modules/customers/ai-agents-context.js.map +7 -0
- package/dist/modules/customers/ai-agents.js +244 -0
- package/dist/modules/customers/ai-agents.js.map +7 -0
- package/dist/modules/customers/ai-tools/activities-tasks-pack.js +1015 -0
- package/dist/modules/customers/ai-tools/activities-tasks-pack.js.map +7 -0
- package/dist/modules/customers/ai-tools/addresses-tags-pack.js +134 -0
- package/dist/modules/customers/ai-tools/addresses-tags-pack.js.map +7 -0
- package/dist/modules/customers/ai-tools/companies-pack.js +249 -0
- package/dist/modules/customers/ai-tools/companies-pack.js.map +7 -0
- package/dist/modules/customers/ai-tools/deals-pack.js +348 -0
- package/dist/modules/customers/ai-tools/deals-pack.js.map +7 -0
- package/dist/modules/customers/ai-tools/people-pack.js +261 -0
- package/dist/modules/customers/ai-tools/people-pack.js.map +7 -0
- package/dist/modules/customers/ai-tools/settings-pack.js +102 -0
- package/dist/modules/customers/ai-tools/settings-pack.js.map +7 -0
- package/dist/modules/customers/ai-tools/types.js +10 -0
- package/dist/modules/customers/ai-tools/types.js.map +7 -0
- package/dist/modules/customers/ai-tools.js +20 -0
- package/dist/modules/customers/ai-tools.js.map +7 -0
- package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.js +469 -0
- package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.js.map +7 -0
- package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.js +17 -0
- package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.js.map +7 -0
- package/dist/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.client.js +117 -0
- package/dist/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.client.js.map +7 -0
- package/dist/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.js +17 -0
- package/dist/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.js.map +7 -0
- package/dist/modules/customers/widgets/injection-table.js +26 -0
- package/dist/modules/customers/widgets/injection-table.js.map +7 -0
- package/dist/modules/inbox_ops/ai-tools.js +4 -0
- package/dist/modules/inbox_ops/ai-tools.js.map +2 -2
- package/dist/modules/inbox_ops/lib/llmProvider.js +52 -7
- package/dist/modules/inbox_ops/lib/llmProvider.js.map +2 -2
- package/dist/modules/notifications/setup.js +13 -0
- package/dist/modules/notifications/setup.js.map +7 -0
- package/jest.config.cjs +1 -0
- package/jest.setup.ts +18 -0
- package/package.json +5 -3
- package/src/helpers/integration/api.ts +38 -16
- package/src/helpers/integration/auth.ts +13 -6
- package/src/modules/auth/commands/roles.ts +10 -12
- package/src/modules/catalog/AGENTS.md +11 -0
- package/src/modules/catalog/ai-agents-context.ts +239 -0
- package/src/modules/catalog/ai-agents.ts +525 -0
- package/src/modules/catalog/ai-tools/_shared.ts +487 -0
- package/src/modules/catalog/ai-tools/authoring-pack.ts +600 -0
- package/src/modules/catalog/ai-tools/categories-pack.ts +192 -0
- package/src/modules/catalog/ai-tools/configuration-pack.ts +218 -0
- package/src/modules/catalog/ai-tools/media-tags-pack.ts +127 -0
- package/src/modules/catalog/ai-tools/merchandising-pack.ts +608 -0
- package/src/modules/catalog/ai-tools/mutation-pack.ts +761 -0
- package/src/modules/catalog/ai-tools/prices-offers-pack.ts +376 -0
- package/src/modules/catalog/ai-tools/products-pack.ts +387 -0
- package/src/modules/catalog/ai-tools/stats-pack.ts +84 -0
- package/src/modules/catalog/ai-tools/types.ts +81 -0
- package/src/modules/catalog/ai-tools/variants-pack.ts +147 -0
- package/src/modules/catalog/ai-tools.ts +78 -0
- package/src/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.tsx +597 -0
- package/src/modules/catalog/backend/catalog/products/page.tsx +23 -2
- package/src/modules/catalog/components/CatalogStatsCard.tsx +118 -0
- package/src/modules/catalog/components/products/ProductsDataTable.tsx +54 -6
- package/src/modules/catalog/events.ts +7 -4
- package/src/modules/catalog/i18n/de.json +17 -0
- package/src/modules/catalog/i18n/en.json +17 -0
- package/src/modules/catalog/i18n/es.json +17 -0
- package/src/modules/catalog/i18n/pl.json +17 -0
- package/src/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.client.tsx +109 -0
- package/src/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.ts +29 -0
- package/src/modules/catalog/widgets/injection/product-seo/widget.client.tsx +1 -1
- package/src/modules/catalog/widgets/injection-table.ts +12 -0
- package/src/modules/customer_accounts/i18n/de.json +5 -0
- package/src/modules/customer_accounts/i18n/en.json +5 -0
- package/src/modules/customer_accounts/i18n/es.json +5 -0
- package/src/modules/customer_accounts/i18n/pl.json +5 -0
- package/src/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.client.tsx +136 -0
- package/src/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.ts +43 -0
- package/src/modules/customer_accounts/widgets/injection-table.ts +9 -0
- package/src/modules/customers/AGENTS.md +13 -0
- package/src/modules/customers/ai-agents-context.ts +150 -0
- package/src/modules/customers/ai-agents.ts +355 -0
- package/src/modules/customers/ai-tools/activities-tasks-pack.ts +1248 -0
- package/src/modules/customers/ai-tools/addresses-tags-pack.ts +145 -0
- package/src/modules/customers/ai-tools/companies-pack.ts +362 -0
- package/src/modules/customers/ai-tools/deals-pack.ts +505 -0
- package/src/modules/customers/ai-tools/people-pack.ts +369 -0
- package/src/modules/customers/ai-tools/settings-pack.ts +121 -0
- package/src/modules/customers/ai-tools/types.ts +76 -0
- package/src/modules/customers/ai-tools.ts +34 -0
- package/src/modules/customers/i18n/de.json +25 -0
- package/src/modules/customers/i18n/en.json +25 -0
- package/src/modules/customers/i18n/es.json +25 -0
- package/src/modules/customers/i18n/pl.json +25 -0
- package/src/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.tsx +580 -0
- package/src/modules/customers/widgets/injection/ai-assistant-trigger/widget.ts +36 -0
- package/src/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.client.tsx +191 -0
- package/src/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.ts +37 -0
- package/src/modules/customers/widgets/injection-table.ts +41 -0
- package/src/modules/inbox_ops/ai-tools.ts +4 -0
- package/src/modules/inbox_ops/lib/llmProvider.ts +83 -7
- package/src/modules/notifications/setup.ts +11 -0
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for catalog AI tool packs (Phase 1 WS-C, Steps 3.10 / 3.11 / 3.12).
|
|
3
|
+
*
|
|
4
|
+
* Step 3.10/3.11 centralized the price-kind enumeration query used by both the
|
|
5
|
+
* base tool (`catalog.list_price_kinds_base`) and the D18 spec-named tool
|
|
6
|
+
* (`catalog.list_price_kinds`).
|
|
7
|
+
*
|
|
8
|
+
* Step 3.12 lifts the **product-bundle builder** (and the merged
|
|
9
|
+
* attribute-schema resolver) here too, so the D18 AI-authoring tools
|
|
10
|
+
* (`draft_description_from_attributes`, `extract_attributes_from_description`,
|
|
11
|
+
* `draft_description_from_media`, `suggest_title_variants`,
|
|
12
|
+
* `suggest_price_adjustment`) can reuse them verbatim without either
|
|
13
|
+
* duplicating the logic or depending on an internal symbol in
|
|
14
|
+
* `merchandising-pack.ts`. Both packs (`merchandising-pack.ts` and
|
|
15
|
+
* `authoring-pack.ts`) consume these helpers; neither pack owns the bundle
|
|
16
|
+
* loader any more.
|
|
17
|
+
*
|
|
18
|
+
* Keeping the shared pieces query-shaped (not tool-shaped) means each tool is
|
|
19
|
+
* free to project its own output shape without leaking concerns between packs.
|
|
20
|
+
*/
|
|
21
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
22
|
+
import {
|
|
23
|
+
findOneWithDecryption,
|
|
24
|
+
findWithDecryption,
|
|
25
|
+
} from '@open-mercato/shared/lib/encryption/find'
|
|
26
|
+
import {
|
|
27
|
+
loadCustomFieldDefinitionIndex,
|
|
28
|
+
loadCustomFieldValues,
|
|
29
|
+
type CustomFieldDefinitionSummary,
|
|
30
|
+
} from '@open-mercato/shared/lib/crud/custom-fields'
|
|
31
|
+
import { E } from '#generated/entities.ids.generated'
|
|
32
|
+
import { Attachment } from '@open-mercato/core/modules/attachments/data/entities'
|
|
33
|
+
import {
|
|
34
|
+
CatalogPriceKind,
|
|
35
|
+
CatalogProduct,
|
|
36
|
+
CatalogProductCategoryAssignment,
|
|
37
|
+
CatalogProductPrice,
|
|
38
|
+
CatalogProductTag,
|
|
39
|
+
CatalogProductTagAssignment,
|
|
40
|
+
CatalogProductUnitConversion,
|
|
41
|
+
CatalogProductVariant,
|
|
42
|
+
} from '../data/entities'
|
|
43
|
+
import type { CatalogPricingService } from '../services/catalogPricingService'
|
|
44
|
+
import type { PriceRow, PricingContext } from '../lib/pricing'
|
|
45
|
+
import type { CatalogToolContext } from './types'
|
|
46
|
+
|
|
47
|
+
/* -------------------------------------------------------------------------- */
|
|
48
|
+
/* Price-kind enumeration shared core */
|
|
49
|
+
/* -------------------------------------------------------------------------- */
|
|
50
|
+
|
|
51
|
+
export type ListPriceKindsCoreInput = {
|
|
52
|
+
limit?: number
|
|
53
|
+
offset?: number
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type ListPriceKindsCoreRow = {
|
|
57
|
+
id: string
|
|
58
|
+
code: string
|
|
59
|
+
title: string
|
|
60
|
+
displayMode: string
|
|
61
|
+
currencyCode: string | null
|
|
62
|
+
isPromotion: boolean
|
|
63
|
+
isActive: boolean
|
|
64
|
+
organizationId: string | null
|
|
65
|
+
tenantId: string | null
|
|
66
|
+
createdAt: string | null
|
|
67
|
+
updatedAt: string | null
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export type ListPriceKindsCoreResult = {
|
|
71
|
+
items: ListPriceKindsCoreRow[]
|
|
72
|
+
total: number
|
|
73
|
+
limit: number
|
|
74
|
+
offset: number
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function resolveEm(ctx: CatalogToolContext): EntityManager {
|
|
78
|
+
return ctx.container.resolve<EntityManager>('em')
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function buildScope(ctx: CatalogToolContext, tenantId: string) {
|
|
82
|
+
return { tenantId, organizationId: ctx.organizationId }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Shared tenant-scoped enumeration of `CatalogPriceKind` rows.
|
|
87
|
+
*
|
|
88
|
+
* Uses `findWithDecryption` + post-filter. Price kinds are tenant-owned and
|
|
89
|
+
* can be either organization-scoped (match `ctx.organizationId`) or
|
|
90
|
+
* null-scoped (shared across the tenant); the `$or` below mirrors the
|
|
91
|
+
* filter the base tool used pre-refactor so behavior stays identical.
|
|
92
|
+
*/
|
|
93
|
+
export async function listPriceKindsCore(
|
|
94
|
+
ctx: CatalogToolContext,
|
|
95
|
+
input: ListPriceKindsCoreInput,
|
|
96
|
+
tenantId: string,
|
|
97
|
+
): Promise<ListPriceKindsCoreResult> {
|
|
98
|
+
const em = resolveEm(ctx)
|
|
99
|
+
const limit = input.limit ?? 50
|
|
100
|
+
const offset = input.offset ?? 0
|
|
101
|
+
const where: Record<string, unknown> = { tenantId, deletedAt: null }
|
|
102
|
+
if (ctx.organizationId) {
|
|
103
|
+
where.$or = [{ organizationId: ctx.organizationId }, { organizationId: null }]
|
|
104
|
+
}
|
|
105
|
+
const [rows, total] = await Promise.all([
|
|
106
|
+
findWithDecryption<CatalogPriceKind>(
|
|
107
|
+
em,
|
|
108
|
+
CatalogPriceKind,
|
|
109
|
+
where as any,
|
|
110
|
+
{ limit, offset, orderBy: { code: 'asc' } as any } as any,
|
|
111
|
+
buildScope(ctx, tenantId),
|
|
112
|
+
),
|
|
113
|
+
em.count(CatalogPriceKind, where as any),
|
|
114
|
+
])
|
|
115
|
+
const filtered = rows.filter((row) => row.tenantId === tenantId)
|
|
116
|
+
return {
|
|
117
|
+
items: filtered.map((row) => ({
|
|
118
|
+
id: row.id,
|
|
119
|
+
code: row.code,
|
|
120
|
+
title: row.title,
|
|
121
|
+
displayMode: row.displayMode,
|
|
122
|
+
currencyCode: row.currencyCode ?? null,
|
|
123
|
+
isPromotion: !!row.isPromotion,
|
|
124
|
+
isActive: !!row.isActive,
|
|
125
|
+
organizationId: row.organizationId ?? null,
|
|
126
|
+
tenantId: row.tenantId ?? null,
|
|
127
|
+
createdAt: row.createdAt ? new Date(row.createdAt).toISOString() : null,
|
|
128
|
+
updatedAt: row.updatedAt ? new Date(row.updatedAt).toISOString() : null,
|
|
129
|
+
})),
|
|
130
|
+
total,
|
|
131
|
+
limit,
|
|
132
|
+
offset,
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/* -------------------------------------------------------------------------- */
|
|
137
|
+
/* Product summary + bundle builder */
|
|
138
|
+
/* -------------------------------------------------------------------------- */
|
|
139
|
+
|
|
140
|
+
export type ProductSummary = {
|
|
141
|
+
id: string
|
|
142
|
+
title: string
|
|
143
|
+
subtitle: string | null
|
|
144
|
+
sku: string | null
|
|
145
|
+
handle: string | null
|
|
146
|
+
productType: string
|
|
147
|
+
statusEntryId: string | null
|
|
148
|
+
primaryCurrencyCode: string | null
|
|
149
|
+
defaultMediaId: string | null
|
|
150
|
+
defaultMediaUrl: string | null
|
|
151
|
+
/**
|
|
152
|
+
* Alias of `defaultMediaUrl`. Surfaced under the same key the
|
|
153
|
+
* `open-mercato:product` record card consumes so the model can pass it
|
|
154
|
+
* straight through without renaming. Null when the product has no
|
|
155
|
+
* default media.
|
|
156
|
+
*/
|
|
157
|
+
imageUrl: string | null
|
|
158
|
+
isActive: boolean
|
|
159
|
+
isConfigurable: boolean
|
|
160
|
+
organizationId: string | null
|
|
161
|
+
tenantId: string | null
|
|
162
|
+
createdAt: string | null
|
|
163
|
+
updatedAt: string | null
|
|
164
|
+
description: string | null
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function toProductSummary(row: CatalogProduct): ProductSummary {
|
|
168
|
+
return {
|
|
169
|
+
id: row.id,
|
|
170
|
+
title: row.title,
|
|
171
|
+
subtitle: row.subtitle ?? null,
|
|
172
|
+
sku: row.sku ?? null,
|
|
173
|
+
handle: row.handle ?? null,
|
|
174
|
+
productType: row.productType,
|
|
175
|
+
statusEntryId: row.statusEntryId ?? null,
|
|
176
|
+
primaryCurrencyCode: row.primaryCurrencyCode ?? null,
|
|
177
|
+
defaultMediaId: row.defaultMediaId ?? null,
|
|
178
|
+
defaultMediaUrl: row.defaultMediaUrl ?? null,
|
|
179
|
+
imageUrl: row.defaultMediaUrl ?? null,
|
|
180
|
+
isActive: !!row.isActive,
|
|
181
|
+
isConfigurable: !!row.isConfigurable,
|
|
182
|
+
organizationId: row.organizationId ?? null,
|
|
183
|
+
tenantId: row.tenantId ?? null,
|
|
184
|
+
createdAt: row.createdAt ? new Date(row.createdAt).toISOString() : null,
|
|
185
|
+
updatedAt: row.updatedAt ? new Date(row.updatedAt).toISOString() : null,
|
|
186
|
+
// `description` is a product field used by D18 authoring tools to seed
|
|
187
|
+
// extract-attributes-from-description; falls back to null when absent.
|
|
188
|
+
description: (row as any).description ?? null,
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function toPriceNumeric(value: string | null | undefined): number | null {
|
|
193
|
+
if (value === null || value === undefined) return null
|
|
194
|
+
const parsed = Number(value)
|
|
195
|
+
return Number.isFinite(parsed) ? parsed : null
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export type AttributeSchemaField = {
|
|
199
|
+
key: string
|
|
200
|
+
label: string | null
|
|
201
|
+
type: string | null
|
|
202
|
+
required: boolean
|
|
203
|
+
options: unknown | null
|
|
204
|
+
scope: 'module' | 'category' | 'product'
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export type AttributeSchemaResult = {
|
|
208
|
+
fields: AttributeSchemaField[]
|
|
209
|
+
resolvedFor: { productId?: string; categoryId?: string }
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function summarizeDefinitionAsField(
|
|
213
|
+
summary: CustomFieldDefinitionSummary,
|
|
214
|
+
scope: AttributeSchemaField['scope'],
|
|
215
|
+
): AttributeSchemaField {
|
|
216
|
+
return {
|
|
217
|
+
key: summary.key,
|
|
218
|
+
label: summary.label ?? null,
|
|
219
|
+
type: summary.kind ?? null,
|
|
220
|
+
required: false,
|
|
221
|
+
options: null,
|
|
222
|
+
scope,
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export async function resolveAttributeSchema(
|
|
227
|
+
ctx: CatalogToolContext,
|
|
228
|
+
tenantId: string,
|
|
229
|
+
productId?: string,
|
|
230
|
+
categoryId?: string,
|
|
231
|
+
): Promise<AttributeSchemaResult> {
|
|
232
|
+
const em = resolveEm(ctx)
|
|
233
|
+
const organizationIds = ctx.organizationId ? [ctx.organizationId] : []
|
|
234
|
+
const moduleDefs = await loadCustomFieldDefinitionIndex({
|
|
235
|
+
em,
|
|
236
|
+
entityIds: [E.catalog.catalog_product, E.catalog.catalog_product_category],
|
|
237
|
+
tenantId,
|
|
238
|
+
organizationIds,
|
|
239
|
+
})
|
|
240
|
+
const fields: AttributeSchemaField[] = []
|
|
241
|
+
moduleDefs.forEach((entries) => {
|
|
242
|
+
const pick = entries[0]
|
|
243
|
+
if (!pick) return
|
|
244
|
+
const scope: AttributeSchemaField['scope'] = pick.organizationId ? 'product' : 'module'
|
|
245
|
+
fields.push(summarizeDefinitionAsField(pick, scope))
|
|
246
|
+
})
|
|
247
|
+
return {
|
|
248
|
+
fields,
|
|
249
|
+
resolvedFor: {
|
|
250
|
+
...(productId ? { productId } : {}),
|
|
251
|
+
...(categoryId ? { categoryId } : {}),
|
|
252
|
+
},
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export type ProductBundleMediaEntry = {
|
|
257
|
+
mediaId: string
|
|
258
|
+
attachmentId: string
|
|
259
|
+
fileName: string
|
|
260
|
+
mediaType: string | null
|
|
261
|
+
size: number | null
|
|
262
|
+
altText: string | null
|
|
263
|
+
sortOrder: number
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export type ProductBundle = {
|
|
267
|
+
found: true
|
|
268
|
+
id: string
|
|
269
|
+
product: ProductSummary
|
|
270
|
+
categories: Array<{ id: string; name: string | null; slug: string | null; path: string | null }>
|
|
271
|
+
tags: Array<{ id: string; label: string; slug: string }>
|
|
272
|
+
variants: Array<Record<string, unknown>>
|
|
273
|
+
prices: {
|
|
274
|
+
all: Array<Record<string, unknown>>
|
|
275
|
+
best: Record<string, unknown> | null
|
|
276
|
+
}
|
|
277
|
+
media: ProductBundleMediaEntry[]
|
|
278
|
+
customFields: Record<string, unknown>
|
|
279
|
+
attributeSchema: AttributeSchemaResult
|
|
280
|
+
translations: null
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export type ProductBundleResult = ProductBundle | { found: false; productId: string }
|
|
284
|
+
|
|
285
|
+
function resolvePricingService(ctx: CatalogToolContext): CatalogPricingService | null {
|
|
286
|
+
try {
|
|
287
|
+
return ctx.container.resolve<CatalogPricingService>('catalogPricingService')
|
|
288
|
+
} catch {
|
|
289
|
+
return null
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export async function buildProductBundle(
|
|
294
|
+
em: EntityManager,
|
|
295
|
+
ctx: CatalogToolContext,
|
|
296
|
+
tenantId: string,
|
|
297
|
+
productId: string,
|
|
298
|
+
): Promise<ProductBundleResult> {
|
|
299
|
+
const where: Record<string, unknown> = {
|
|
300
|
+
id: productId,
|
|
301
|
+
tenantId,
|
|
302
|
+
deletedAt: null,
|
|
303
|
+
}
|
|
304
|
+
if (ctx.organizationId) where.organizationId = ctx.organizationId
|
|
305
|
+
const product = await findOneWithDecryption<CatalogProduct>(
|
|
306
|
+
em,
|
|
307
|
+
CatalogProduct,
|
|
308
|
+
where as any,
|
|
309
|
+
undefined,
|
|
310
|
+
buildScope(ctx, tenantId),
|
|
311
|
+
)
|
|
312
|
+
if (!product || product.tenantId !== tenantId) {
|
|
313
|
+
return { found: false as const, productId }
|
|
314
|
+
}
|
|
315
|
+
const scope = buildScope(ctx, tenantId)
|
|
316
|
+
const [
|
|
317
|
+
categoryAssignments,
|
|
318
|
+
tagAssignments,
|
|
319
|
+
variants,
|
|
320
|
+
prices,
|
|
321
|
+
mediaAttachments,
|
|
322
|
+
unitConversions,
|
|
323
|
+
customFieldValues,
|
|
324
|
+
attributeSchema,
|
|
325
|
+
] = await Promise.all([
|
|
326
|
+
findWithDecryption<CatalogProductCategoryAssignment>(
|
|
327
|
+
em,
|
|
328
|
+
CatalogProductCategoryAssignment,
|
|
329
|
+
{ tenantId, product: product.id } as any,
|
|
330
|
+
{ limit: 100, populate: ['category'] as any } as any,
|
|
331
|
+
scope,
|
|
332
|
+
),
|
|
333
|
+
findWithDecryption<CatalogProductTagAssignment>(
|
|
334
|
+
em,
|
|
335
|
+
CatalogProductTagAssignment,
|
|
336
|
+
{ tenantId, product: product.id } as any,
|
|
337
|
+
{ limit: 100, populate: ['tag'] as any } as any,
|
|
338
|
+
scope,
|
|
339
|
+
),
|
|
340
|
+
findWithDecryption<CatalogProductVariant>(
|
|
341
|
+
em,
|
|
342
|
+
CatalogProductVariant,
|
|
343
|
+
{ tenantId, product: product.id, deletedAt: null } as any,
|
|
344
|
+
{ limit: 100, orderBy: { createdAt: 'asc' } as any } as any,
|
|
345
|
+
scope,
|
|
346
|
+
),
|
|
347
|
+
findWithDecryption<CatalogProductPrice>(
|
|
348
|
+
em,
|
|
349
|
+
CatalogProductPrice,
|
|
350
|
+
{ tenantId, product: product.id } as any,
|
|
351
|
+
{ limit: 100, orderBy: { createdAt: 'asc' } as any } as any,
|
|
352
|
+
scope,
|
|
353
|
+
),
|
|
354
|
+
findWithDecryption<Attachment>(
|
|
355
|
+
em,
|
|
356
|
+
Attachment,
|
|
357
|
+
{ tenantId, entityId: E.catalog.catalog_product, recordId: product.id } as any,
|
|
358
|
+
{ limit: 100, orderBy: { createdAt: 'asc' } as any } as any,
|
|
359
|
+
scope,
|
|
360
|
+
),
|
|
361
|
+
findWithDecryption<CatalogProductUnitConversion>(
|
|
362
|
+
em,
|
|
363
|
+
CatalogProductUnitConversion,
|
|
364
|
+
{ tenantId, product: product.id, deletedAt: null } as any,
|
|
365
|
+
{ limit: 100, orderBy: { sortOrder: 'asc', createdAt: 'asc' } as any } as any,
|
|
366
|
+
scope,
|
|
367
|
+
),
|
|
368
|
+
loadCustomFieldValues({
|
|
369
|
+
em,
|
|
370
|
+
entityId: E.catalog.catalog_product,
|
|
371
|
+
recordIds: [product.id],
|
|
372
|
+
tenantIdByRecord: { [product.id]: product.tenantId ?? null },
|
|
373
|
+
organizationIdByRecord: { [product.id]: product.organizationId ?? null },
|
|
374
|
+
tenantFallbacks: [product.tenantId ?? tenantId].filter((value): value is string => !!value),
|
|
375
|
+
}),
|
|
376
|
+
resolveAttributeSchema(ctx, tenantId, product.id, undefined),
|
|
377
|
+
])
|
|
378
|
+
|
|
379
|
+
const categories = categoryAssignments
|
|
380
|
+
.map((assignment) => {
|
|
381
|
+
const category = (assignment as any).category
|
|
382
|
+
if (!category || typeof category === 'string') {
|
|
383
|
+
const fallbackId = typeof category === 'string' ? category : null
|
|
384
|
+
return fallbackId ? { id: fallbackId, name: null, slug: null, path: null } : null
|
|
385
|
+
}
|
|
386
|
+
return {
|
|
387
|
+
id: category.id,
|
|
388
|
+
name: category.name ?? null,
|
|
389
|
+
slug: category.slug ?? null,
|
|
390
|
+
path: category.treePath ?? null,
|
|
391
|
+
}
|
|
392
|
+
})
|
|
393
|
+
.filter((value): value is { id: string; name: string | null; slug: string | null; path: string | null } => value !== null)
|
|
394
|
+
|
|
395
|
+
const tags = tagAssignments
|
|
396
|
+
.map((assignment) => {
|
|
397
|
+
const tag = (assignment as any).tag as CatalogProductTag | string | null
|
|
398
|
+
if (!tag || typeof tag === 'string') return null
|
|
399
|
+
return { id: tag.id, label: tag.label, slug: tag.slug }
|
|
400
|
+
})
|
|
401
|
+
.filter((value): value is { id: string; label: string; slug: string } => value !== null)
|
|
402
|
+
|
|
403
|
+
const priceRows = prices.map((row) => ({
|
|
404
|
+
id: row.id,
|
|
405
|
+
priceKindId: (row as any).priceKind && typeof (row as any).priceKind === 'object'
|
|
406
|
+
? (row as any).priceKind.id
|
|
407
|
+
: (row as any).priceKind ?? null,
|
|
408
|
+
currencyCode: row.currencyCode,
|
|
409
|
+
kind: row.kind,
|
|
410
|
+
minQuantity: row.minQuantity,
|
|
411
|
+
maxQuantity: row.maxQuantity ?? null,
|
|
412
|
+
unitPriceNet: row.unitPriceNet ?? null,
|
|
413
|
+
unitPriceGross: row.unitPriceGross ?? null,
|
|
414
|
+
taxRate: row.taxRate ?? null,
|
|
415
|
+
taxAmount: row.taxAmount ?? null,
|
|
416
|
+
channelId: row.channelId ?? null,
|
|
417
|
+
offerId: (row as any).offer && typeof (row as any).offer === 'object'
|
|
418
|
+
? (row as any).offer.id
|
|
419
|
+
: (row as any).offer ?? null,
|
|
420
|
+
variantId: (row as any).variant && typeof (row as any).variant === 'object'
|
|
421
|
+
? (row as any).variant.id
|
|
422
|
+
: (row as any).variant ?? null,
|
|
423
|
+
startsAt: row.startsAt ? new Date(row.startsAt).toISOString() : null,
|
|
424
|
+
endsAt: row.endsAt ? new Date(row.endsAt).toISOString() : null,
|
|
425
|
+
}))
|
|
426
|
+
|
|
427
|
+
let bestPrice: Record<string, unknown> | null = null
|
|
428
|
+
const pricingService = resolvePricingService(ctx)
|
|
429
|
+
if (pricingService && prices.length > 0) {
|
|
430
|
+
const pricingContext: PricingContext = {
|
|
431
|
+
quantity: 1,
|
|
432
|
+
date: new Date(),
|
|
433
|
+
}
|
|
434
|
+
try {
|
|
435
|
+
const resolved = await pricingService.resolvePrice(prices as unknown as PriceRow[], pricingContext)
|
|
436
|
+
if (resolved) {
|
|
437
|
+
bestPrice = {
|
|
438
|
+
id: (resolved as any).id,
|
|
439
|
+
currencyCode: (resolved as any).currencyCode,
|
|
440
|
+
kind: (resolved as any).kind,
|
|
441
|
+
unitPriceNet: (resolved as any).unitPriceNet ?? null,
|
|
442
|
+
unitPriceGross: (resolved as any).unitPriceGross ?? null,
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
} catch (error) {
|
|
446
|
+
console.warn('[catalog.get_product_bundle] resolvePrice failed, omitting best price', error)
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return {
|
|
451
|
+
found: true,
|
|
452
|
+
id: product.id,
|
|
453
|
+
product: toProductSummary(product),
|
|
454
|
+
categories,
|
|
455
|
+
tags,
|
|
456
|
+
variants: variants.map((variant) => ({
|
|
457
|
+
id: variant.id,
|
|
458
|
+
name: variant.name ?? null,
|
|
459
|
+
sku: variant.sku ?? null,
|
|
460
|
+
barcode: variant.barcode ?? null,
|
|
461
|
+
optionValues: variant.optionValues ?? null,
|
|
462
|
+
defaultMediaId: variant.defaultMediaId ?? null,
|
|
463
|
+
defaultMediaUrl: variant.defaultMediaUrl ?? null,
|
|
464
|
+
isDefault: !!variant.isDefault,
|
|
465
|
+
isActive: !!variant.isActive,
|
|
466
|
+
})),
|
|
467
|
+
prices: {
|
|
468
|
+
all: priceRows,
|
|
469
|
+
best: bestPrice,
|
|
470
|
+
},
|
|
471
|
+
media: mediaAttachments.map((attachment) => ({
|
|
472
|
+
mediaId: attachment.id,
|
|
473
|
+
attachmentId: attachment.id,
|
|
474
|
+
fileName: attachment.fileName,
|
|
475
|
+
mediaType: attachment.mimeType,
|
|
476
|
+
size: attachment.fileSize,
|
|
477
|
+
altText: null,
|
|
478
|
+
sortOrder: 0,
|
|
479
|
+
})),
|
|
480
|
+
customFields: customFieldValues[product.id] ?? {},
|
|
481
|
+
attributeSchema,
|
|
482
|
+
// No translation resolver exists for catalog (no `translations.ts` at
|
|
483
|
+
// module root yet); returning null is an explicit null-surface contract
|
|
484
|
+
// and a hint for Step 5+ to add the translations resolver.
|
|
485
|
+
translations: null,
|
|
486
|
+
}
|
|
487
|
+
}
|