@hed-hog/catalog 0.0.293 → 0.0.294
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/README.md +409 -379
- package/dist/catalog-resource.config.d.ts.map +1 -1
- package/dist/catalog-resource.config.js +51 -24
- package/dist/catalog-resource.config.js.map +1 -1
- package/dist/catalog.controller.d.ts +420 -0
- package/dist/catalog.controller.d.ts.map +1 -1
- package/dist/catalog.controller.js +98 -0
- package/dist/catalog.controller.js.map +1 -1
- package/dist/catalog.module.d.ts.map +1 -1
- package/dist/catalog.module.js +5 -1
- package/dist/catalog.module.js.map +1 -1
- package/dist/catalog.service.d.ts +216 -1
- package/dist/catalog.service.d.ts.map +1 -1
- package/dist/catalog.service.js +1111 -5
- package/dist/catalog.service.js.map +1 -1
- package/hedhog/data/catalog_attribute.yaml +202 -0
- package/hedhog/data/catalog_attribute_option.yaml +109 -0
- package/hedhog/data/catalog_category.yaml +47 -0
- package/hedhog/data/catalog_category_attribute.yaml +209 -0
- package/hedhog/data/menu.yaml +133 -99
- package/hedhog/data/route.yaml +72 -8
- package/hedhog/frontend/app/[resource]/page.tsx.ejs +391 -33
- package/hedhog/frontend/app/_components/catalog-ai-form-assist-dialog.tsx.ejs +340 -0
- package/hedhog/frontend/app/_components/catalog-resource-form-sheet.tsx.ejs +907 -92
- package/hedhog/frontend/app/_lib/catalog-resources.tsx.ejs +929 -1161
- package/hedhog/frontend/messages/en.json +389 -299
- package/hedhog/frontend/messages/pt.json +389 -299
- package/hedhog/table/catalog_attribute.yaml +67 -52
- package/hedhog/table/catalog_attribute_option.yaml +40 -0
- package/hedhog/table/catalog_category.yaml +40 -0
- package/hedhog/table/catalog_category_attribute.yaml +37 -31
- package/hedhog/table/catalog_comparison.yaml +19 -22
- package/hedhog/table/catalog_product.yaml +30 -28
- package/hedhog/table/catalog_product_attribute_value.yaml +44 -31
- package/hedhog/table/catalog_product_category.yaml +13 -13
- package/hedhog/table/catalog_score_criterion.yaml +42 -25
- package/hedhog/table/catalog_seo_page_rule.yaml +10 -10
- package/hedhog/table/catalog_similarity_rule.yaml +33 -20
- package/hedhog/table/catalog_site.yaml +21 -13
- package/hedhog/table/catalog_site_category.yaml +12 -12
- package/package.json +6 -6
- package/src/catalog-resource.config.ts +132 -105
- package/src/catalog.controller.ts +91 -24
- package/src/catalog.module.ts +16 -12
- package/src/catalog.service.ts +1569 -56
package/dist/catalog.service.js
CHANGED
|
@@ -13,12 +13,14 @@ exports.CatalogService = void 0;
|
|
|
13
13
|
const api_locale_1 = require("@hed-hog/api-locale");
|
|
14
14
|
const api_pagination_1 = require("@hed-hog/api-pagination");
|
|
15
15
|
const api_prisma_1 = require("@hed-hog/api-prisma");
|
|
16
|
+
const core_1 = require("@hed-hog/core");
|
|
16
17
|
const common_1 = require("@nestjs/common");
|
|
17
18
|
const catalog_resource_config_1 = require("./catalog-resource.config");
|
|
18
19
|
let CatalogService = class CatalogService {
|
|
19
|
-
constructor(prisma, pagination) {
|
|
20
|
+
constructor(prisma, pagination, aiService) {
|
|
20
21
|
this.prisma = prisma;
|
|
21
22
|
this.pagination = pagination;
|
|
23
|
+
this.aiService = aiService;
|
|
22
24
|
}
|
|
23
25
|
getConfig(resource, locale) {
|
|
24
26
|
const config = catalog_resource_config_1.catalogResourceMap.get(resource);
|
|
@@ -43,11 +45,500 @@ let CatalogService = class CatalogService {
|
|
|
43
45
|
}
|
|
44
46
|
return value;
|
|
45
47
|
}
|
|
48
|
+
extractJsonObject(content) {
|
|
49
|
+
var _a;
|
|
50
|
+
const trimmed = String(content !== null && content !== void 0 ? content : '').trim();
|
|
51
|
+
if (!trimmed) {
|
|
52
|
+
throw new common_1.BadRequestException('AI returned an empty response');
|
|
53
|
+
}
|
|
54
|
+
const fencedMatch = trimmed.match(/```(?:json)?\s*([\s\S]*?)\s*```/i);
|
|
55
|
+
const candidate = ((_a = fencedMatch === null || fencedMatch === void 0 ? void 0 : fencedMatch[1]) === null || _a === void 0 ? void 0 : _a.trim()) || trimmed;
|
|
56
|
+
try {
|
|
57
|
+
return JSON.parse(candidate);
|
|
58
|
+
}
|
|
59
|
+
catch (_b) {
|
|
60
|
+
const firstBraceIndex = candidate.indexOf('{');
|
|
61
|
+
const lastBraceIndex = candidate.lastIndexOf('}');
|
|
62
|
+
if (firstBraceIndex >= 0 && lastBraceIndex > firstBraceIndex) {
|
|
63
|
+
try {
|
|
64
|
+
return JSON.parse(candidate.slice(firstBraceIndex, lastBraceIndex + 1));
|
|
65
|
+
}
|
|
66
|
+
catch (_c) {
|
|
67
|
+
throw new common_1.BadRequestException('AI returned an invalid JSON payload');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
throw new common_1.BadRequestException('AI returned an invalid JSON payload');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
normalizeComparableText(value) {
|
|
74
|
+
return String(value !== null && value !== void 0 ? value : '')
|
|
75
|
+
.normalize('NFD')
|
|
76
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
77
|
+
.trim()
|
|
78
|
+
.toLowerCase();
|
|
79
|
+
}
|
|
80
|
+
normalizeAiFieldContext(resource, fields) {
|
|
81
|
+
const config = catalog_resource_config_1.catalogResourceMap.get(resource);
|
|
82
|
+
if (!config) {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
const allowedKeys = new Set(config.fields);
|
|
86
|
+
return fields
|
|
87
|
+
.map((field) => {
|
|
88
|
+
var _a, _b, _c, _d;
|
|
89
|
+
const key = String((_a = field.key) !== null && _a !== void 0 ? _a : '').trim();
|
|
90
|
+
if (!key || !allowedKeys.has(key)) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
const options = Array.isArray(field.options)
|
|
94
|
+
? field.options
|
|
95
|
+
.map((option) => {
|
|
96
|
+
var _a, _b;
|
|
97
|
+
return ({
|
|
98
|
+
value: String((_a = option.value) !== null && _a !== void 0 ? _a : '').trim(),
|
|
99
|
+
label: String((_b = option.label) !== null && _b !== void 0 ? _b : '').trim(),
|
|
100
|
+
});
|
|
101
|
+
})
|
|
102
|
+
.filter((option) => option.value && option.label)
|
|
103
|
+
: [];
|
|
104
|
+
const relationData = field.relation && typeof field.relation === 'object'
|
|
105
|
+
? field.relation
|
|
106
|
+
: null;
|
|
107
|
+
return {
|
|
108
|
+
key,
|
|
109
|
+
label: String((_b = field.label) !== null && _b !== void 0 ? _b : key),
|
|
110
|
+
type: String((_c = field.type) !== null && _c !== void 0 ? _c : 'text'),
|
|
111
|
+
required: Boolean((_d = field.required) !== null && _d !== void 0 ? _d : false),
|
|
112
|
+
options,
|
|
113
|
+
relation: relationData
|
|
114
|
+
? {
|
|
115
|
+
endpoint: relationData.endpoint
|
|
116
|
+
? String(relationData.endpoint)
|
|
117
|
+
: undefined,
|
|
118
|
+
resource: relationData.resource
|
|
119
|
+
? String(relationData.resource)
|
|
120
|
+
: undefined,
|
|
121
|
+
labelKeys: Array.isArray(relationData.labelKeys)
|
|
122
|
+
? relationData.labelKeys.map((item) => String(item))
|
|
123
|
+
: [],
|
|
124
|
+
}
|
|
125
|
+
: null,
|
|
126
|
+
};
|
|
127
|
+
})
|
|
128
|
+
.filter(Boolean);
|
|
129
|
+
}
|
|
130
|
+
normalizeBooleanSuggestion(value) {
|
|
131
|
+
if (typeof value === 'boolean') {
|
|
132
|
+
return value;
|
|
133
|
+
}
|
|
134
|
+
if (typeof value === 'number') {
|
|
135
|
+
return value !== 0;
|
|
136
|
+
}
|
|
137
|
+
const normalized = this.normalizeComparableText(value);
|
|
138
|
+
if (!normalized) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
if (['true', '1', 'yes', 'y', 'sim', 'ativo', 'active', 'enabled'].includes(normalized)) {
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
if (['false', '0', 'no', 'n', 'nao', 'não', 'inativo', 'inactive', 'disabled'].includes(normalized)) {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
normalizeNumberSuggestion(value) {
|
|
150
|
+
if (typeof value === 'number') {
|
|
151
|
+
return Number.isFinite(value) ? value : null;
|
|
152
|
+
}
|
|
153
|
+
const raw = String(value !== null && value !== void 0 ? value : '').trim();
|
|
154
|
+
if (!raw) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
const normalized = raw.replace(/[^\d,.\-]/g, '').replace(',', '.');
|
|
158
|
+
const parsed = Number(normalized);
|
|
159
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
160
|
+
}
|
|
161
|
+
normalizeSelectSuggestion(rawValue, options) {
|
|
162
|
+
var _a;
|
|
163
|
+
const normalized = this.normalizeComparableText(rawValue);
|
|
164
|
+
if (!normalized) {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
const exact = options.find((option) => {
|
|
168
|
+
return (this.normalizeComparableText(option.value) === normalized ||
|
|
169
|
+
this.normalizeComparableText(option.label) === normalized);
|
|
170
|
+
});
|
|
171
|
+
if (exact) {
|
|
172
|
+
return exact.value;
|
|
173
|
+
}
|
|
174
|
+
const partial = options.find((option) => {
|
|
175
|
+
return (this.normalizeComparableText(option.value).includes(normalized) ||
|
|
176
|
+
normalized.includes(this.normalizeComparableText(option.value)) ||
|
|
177
|
+
this.normalizeComparableText(option.label).includes(normalized) ||
|
|
178
|
+
normalized.includes(this.normalizeComparableText(option.label)));
|
|
179
|
+
});
|
|
180
|
+
return (_a = partial === null || partial === void 0 ? void 0 : partial.value) !== null && _a !== void 0 ? _a : null;
|
|
181
|
+
}
|
|
182
|
+
async resolveRelationSuggestion(field, rawValue) {
|
|
183
|
+
if (rawValue === null || rawValue === undefined || rawValue === '') {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
const relation = field.relation;
|
|
187
|
+
if (!relation) {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
const modelInfo = (() => {
|
|
191
|
+
var _a, _b, _c;
|
|
192
|
+
if (relation.resource) {
|
|
193
|
+
const config = catalog_resource_config_1.catalogResourceMap.get(relation.resource);
|
|
194
|
+
if (config) {
|
|
195
|
+
return {
|
|
196
|
+
model: config.model,
|
|
197
|
+
searchFields: ((_a = relation.labelKeys) === null || _a === void 0 ? void 0 : _a.length) ? relation.labelKeys : config.searchFields,
|
|
198
|
+
idField: 'id',
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (relation.endpoint === '/locale') {
|
|
203
|
+
return {
|
|
204
|
+
model: 'locale',
|
|
205
|
+
searchFields: ((_b = relation.labelKeys) === null || _b === void 0 ? void 0 : _b.length) ? relation.labelKeys : ['name', 'code'],
|
|
206
|
+
idField: 'id',
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
if (relation.endpoint === '/content') {
|
|
210
|
+
return {
|
|
211
|
+
model: 'content',
|
|
212
|
+
searchFields: ((_c = relation.labelKeys) === null || _c === void 0 ? void 0 : _c.length) ? relation.labelKeys : ['title', 'slug'],
|
|
213
|
+
idField: 'id',
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
return null;
|
|
217
|
+
})();
|
|
218
|
+
if (!modelInfo) {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
const model = this.prisma[modelInfo.model];
|
|
222
|
+
if (!(model === null || model === void 0 ? void 0 : model.findMany)) {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
const numericId = this.normalizeNumberSuggestion(rawValue);
|
|
226
|
+
if (numericId && Number.isInteger(numericId)) {
|
|
227
|
+
const byId = await model.findUnique({
|
|
228
|
+
where: { [modelInfo.idField]: numericId },
|
|
229
|
+
});
|
|
230
|
+
if (byId) {
|
|
231
|
+
return Number(byId[modelInfo.idField]);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const needle = String(rawValue !== null && rawValue !== void 0 ? rawValue : '').trim();
|
|
235
|
+
if (!needle) {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
const results = await model.findMany({
|
|
239
|
+
where: {
|
|
240
|
+
OR: modelInfo.searchFields.map((fieldKey) => ({
|
|
241
|
+
[fieldKey]: {
|
|
242
|
+
contains: needle,
|
|
243
|
+
mode: 'insensitive',
|
|
244
|
+
},
|
|
245
|
+
})),
|
|
246
|
+
},
|
|
247
|
+
take: 5,
|
|
248
|
+
});
|
|
249
|
+
if (!results.length) {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
const normalizedNeedle = this.normalizeComparableText(needle);
|
|
253
|
+
const exact = results.find((result) => modelInfo.searchFields.some((fieldKey) => {
|
|
254
|
+
return this.normalizeComparableText(result[fieldKey]) === normalizedNeedle;
|
|
255
|
+
}));
|
|
256
|
+
if (exact) {
|
|
257
|
+
return Number(exact[modelInfo.idField]);
|
|
258
|
+
}
|
|
259
|
+
return results.length === 1 ? Number(results[0][modelInfo.idField]) : null;
|
|
260
|
+
}
|
|
261
|
+
async normalizeAiFieldSuggestion(field, rawValue, warnings) {
|
|
262
|
+
if (rawValue === undefined) {
|
|
263
|
+
return undefined;
|
|
264
|
+
}
|
|
265
|
+
if (rawValue === null) {
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
switch (field.type) {
|
|
269
|
+
case 'switch': {
|
|
270
|
+
const value = this.normalizeBooleanSuggestion(rawValue);
|
|
271
|
+
if (value === null) {
|
|
272
|
+
warnings.push(`Boolean field "${field.label}" could not be resolved safely.`);
|
|
273
|
+
return undefined;
|
|
274
|
+
}
|
|
275
|
+
return value;
|
|
276
|
+
}
|
|
277
|
+
case 'number':
|
|
278
|
+
case 'currency': {
|
|
279
|
+
const value = this.normalizeNumberSuggestion(rawValue);
|
|
280
|
+
if (value === null) {
|
|
281
|
+
warnings.push(`Numeric field "${field.label}" could not be resolved safely.`);
|
|
282
|
+
return undefined;
|
|
283
|
+
}
|
|
284
|
+
return value;
|
|
285
|
+
}
|
|
286
|
+
case 'select': {
|
|
287
|
+
const value = this.normalizeSelectSuggestion(rawValue, field.options);
|
|
288
|
+
if (!value) {
|
|
289
|
+
warnings.push(`Select field "${field.label}" returned an invalid option.`);
|
|
290
|
+
return undefined;
|
|
291
|
+
}
|
|
292
|
+
return value;
|
|
293
|
+
}
|
|
294
|
+
case 'relation': {
|
|
295
|
+
const value = await this.resolveRelationSuggestion(field, rawValue);
|
|
296
|
+
if (!value) {
|
|
297
|
+
warnings.push(`Relation field "${field.label}" could not be resolved safely.`);
|
|
298
|
+
return undefined;
|
|
299
|
+
}
|
|
300
|
+
return value;
|
|
301
|
+
}
|
|
302
|
+
case 'upload':
|
|
303
|
+
warnings.push(`File field "${field.label}" must still be filled manually.`);
|
|
304
|
+
return undefined;
|
|
305
|
+
case 'json':
|
|
306
|
+
if (typeof rawValue === 'object') {
|
|
307
|
+
return rawValue;
|
|
308
|
+
}
|
|
309
|
+
try {
|
|
310
|
+
return JSON.parse(String(rawValue));
|
|
311
|
+
}
|
|
312
|
+
catch (_a) {
|
|
313
|
+
warnings.push(`JSON field "${field.label}" returned an invalid value.`);
|
|
314
|
+
return undefined;
|
|
315
|
+
}
|
|
316
|
+
case 'date':
|
|
317
|
+
case 'datetime':
|
|
318
|
+
case 'text':
|
|
319
|
+
case 'url':
|
|
320
|
+
case 'textarea':
|
|
321
|
+
case 'richtext':
|
|
322
|
+
default:
|
|
323
|
+
return String(rawValue).trim() || null;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
buildCurrentAttributeValueMap(values) {
|
|
327
|
+
var _a;
|
|
328
|
+
const valueMap = new Map();
|
|
329
|
+
for (const value of values) {
|
|
330
|
+
const attributeId = Number((_a = value.attribute_id) !== null && _a !== void 0 ? _a : 0);
|
|
331
|
+
if (!attributeId) {
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
valueMap.set(attributeId, value);
|
|
335
|
+
}
|
|
336
|
+
return valueMap;
|
|
337
|
+
}
|
|
338
|
+
buildAttributeContext(attributesPayload, currentAttributeValues) {
|
|
339
|
+
var _a;
|
|
340
|
+
const currentMap = this.buildCurrentAttributeValueMap(currentAttributeValues);
|
|
341
|
+
return ((_a = attributesPayload.groups) !== null && _a !== void 0 ? _a : []).flatMap((group) => group.attributes.map((attribute) => {
|
|
342
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
|
|
343
|
+
const current = (_b = (_a = currentMap.get(Number(attribute.id))) !== null && _a !== void 0 ? _a : attribute.value) !== null && _b !== void 0 ? _b : null;
|
|
344
|
+
const relation = ((_c = attribute.category_attribute) !== null && _c !== void 0 ? _c : {});
|
|
345
|
+
return {
|
|
346
|
+
id: Number(attribute.id),
|
|
347
|
+
slug: String((_d = attribute.slug) !== null && _d !== void 0 ? _d : ''),
|
|
348
|
+
name: String((_f = (_e = attribute.name) !== null && _e !== void 0 ? _e : attribute.slug) !== null && _f !== void 0 ? _f : ''),
|
|
349
|
+
data_type: String((_g = attribute.data_type) !== null && _g !== void 0 ? _g : 'text'),
|
|
350
|
+
unit: attribute.unit ? String(attribute.unit) : null,
|
|
351
|
+
group_name: String((_j = (_h = group.name) !== null && _h !== void 0 ? _h : attribute.group_name) !== null && _j !== void 0 ? _j : 'General'),
|
|
352
|
+
is_required: Boolean((_k = relation.is_required) !== null && _k !== void 0 ? _k : false),
|
|
353
|
+
is_highlight: Boolean((_l = relation.is_highlight) !== null && _l !== void 0 ? _l : false),
|
|
354
|
+
is_filter_visible: Boolean((_m = relation.is_filter_visible) !== null && _m !== void 0 ? _m : false),
|
|
355
|
+
is_comparison_visible: Boolean((_o = relation.is_comparison_visible) !== null && _o !== void 0 ? _o : false),
|
|
356
|
+
options: Array.isArray(attribute.options)
|
|
357
|
+
? attribute.options.map((option) => {
|
|
358
|
+
var _a, _b, _c, _d;
|
|
359
|
+
return ({
|
|
360
|
+
id: Number(option.id),
|
|
361
|
+
slug: String((_a = option.slug) !== null && _a !== void 0 ? _a : ''),
|
|
362
|
+
label: String((_c = (_b = option.label) !== null && _b !== void 0 ? _b : option.option_value) !== null && _c !== void 0 ? _c : ''),
|
|
363
|
+
option_value: String((_d = option.option_value) !== null && _d !== void 0 ? _d : ''),
|
|
364
|
+
normalized_value: option.normalized_value
|
|
365
|
+
? String(option.normalized_value)
|
|
366
|
+
: null,
|
|
367
|
+
});
|
|
368
|
+
})
|
|
369
|
+
: [],
|
|
370
|
+
current_value: current,
|
|
371
|
+
};
|
|
372
|
+
}));
|
|
373
|
+
}
|
|
374
|
+
async normalizeAiProductAttributeSuggestions(categoryId, rawSuggestions, currentAttributeValues, warnings) {
|
|
375
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
|
|
376
|
+
if (!categoryId || rawSuggestions === undefined || rawSuggestions === null) {
|
|
377
|
+
return [];
|
|
378
|
+
}
|
|
379
|
+
const attributesPayload = await this.buildCategoryAttributePayload(categoryId);
|
|
380
|
+
const attributeContext = this.buildAttributeContext(attributesPayload, currentAttributeValues);
|
|
381
|
+
const normalizedEntries = Array.isArray(rawSuggestions)
|
|
382
|
+
? rawSuggestions
|
|
383
|
+
: rawSuggestions && typeof rawSuggestions === 'object'
|
|
384
|
+
? Object.entries(rawSuggestions).map(([slug, value]) => (Object.assign({ slug }, (value && typeof value === 'object'
|
|
385
|
+
? value
|
|
386
|
+
: { value }))))
|
|
387
|
+
: [];
|
|
388
|
+
const normalizedSuggestions = [];
|
|
389
|
+
for (const entry of normalizedEntries) {
|
|
390
|
+
const entryRecord = (entry && typeof entry === 'object'
|
|
391
|
+
? entry
|
|
392
|
+
: { value: entry });
|
|
393
|
+
const rawIdentifier = (_e = (_d = (_c = (_b = (_a = entryRecord.slug) !== null && _a !== void 0 ? _a : entryRecord.attribute_slug) !== null && _b !== void 0 ? _b : entryRecord.code) !== null && _c !== void 0 ? _c : entryRecord.name) !== null && _d !== void 0 ? _d : entryRecord.attribute) !== null && _e !== void 0 ? _e : null;
|
|
394
|
+
const normalizedIdentifier = this.normalizeComparableText(rawIdentifier);
|
|
395
|
+
const attribute = attributeContext.find((item) => {
|
|
396
|
+
var _a;
|
|
397
|
+
return (item.id === Number((_a = entryRecord.attribute_id) !== null && _a !== void 0 ? _a : 0) ||
|
|
398
|
+
this.normalizeComparableText(item.slug) === normalizedIdentifier ||
|
|
399
|
+
this.normalizeComparableText(item.name) === normalizedIdentifier);
|
|
400
|
+
});
|
|
401
|
+
if (!attribute) {
|
|
402
|
+
if (rawIdentifier) {
|
|
403
|
+
warnings.push(`Structured attribute "${String(rawIdentifier)}" is not available for the selected category.`);
|
|
404
|
+
}
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
const rawValue = (_l = (_k = (_j = (_h = (_g = (_f = entryRecord.value) !== null && _f !== void 0 ? _f : entryRecord.value_text) !== null && _g !== void 0 ? _g : entryRecord.value_number) !== null && _h !== void 0 ? _h : entryRecord.value_boolean) !== null && _j !== void 0 ? _j : entryRecord.option) !== null && _k !== void 0 ? _k : entryRecord.option_value) !== null && _l !== void 0 ? _l : entryRecord.label;
|
|
408
|
+
const draft = {
|
|
409
|
+
attribute_id: attribute.id,
|
|
410
|
+
attribute_slug: attribute.slug,
|
|
411
|
+
attribute_name: attribute.name,
|
|
412
|
+
data_type: attribute.data_type,
|
|
413
|
+
group_name: attribute.group_name,
|
|
414
|
+
value_unit: (_m = attribute.unit) !== null && _m !== void 0 ? _m : null,
|
|
415
|
+
};
|
|
416
|
+
if (attribute.data_type === 'option') {
|
|
417
|
+
const normalizedOption = this.normalizeComparableText((_q = (_p = (_o = entryRecord.option) !== null && _o !== void 0 ? _o : entryRecord.option_value) !== null && _p !== void 0 ? _p : entryRecord.label) !== null && _q !== void 0 ? _q : rawValue);
|
|
418
|
+
const option = attribute.options.find((item) => {
|
|
419
|
+
return (this.normalizeComparableText(item.option_value) === normalizedOption ||
|
|
420
|
+
this.normalizeComparableText(item.label) === normalizedOption ||
|
|
421
|
+
this.normalizeComparableText(item.slug) === normalizedOption);
|
|
422
|
+
});
|
|
423
|
+
if (!option) {
|
|
424
|
+
warnings.push(`Option value for "${attribute.name}" could not be resolved safely.`);
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
draft.attribute_option_id = option.id;
|
|
428
|
+
draft.value_text = option.option_value;
|
|
429
|
+
}
|
|
430
|
+
else if (attribute.data_type === 'number') {
|
|
431
|
+
const numberValue = this.normalizeNumberSuggestion(rawValue);
|
|
432
|
+
if (numberValue === null) {
|
|
433
|
+
warnings.push(`Numeric attribute "${attribute.name}" could not be resolved safely.`);
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
draft.value_number = numberValue;
|
|
437
|
+
draft.raw_value = String(numberValue);
|
|
438
|
+
draft.normalized_value = String(numberValue);
|
|
439
|
+
if (entryRecord.unit) {
|
|
440
|
+
draft.value_unit = String(entryRecord.unit).trim() || attribute.unit || null;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
else if (attribute.data_type === 'boolean') {
|
|
444
|
+
const booleanValue = this.normalizeBooleanSuggestion(rawValue);
|
|
445
|
+
if (booleanValue === null) {
|
|
446
|
+
warnings.push(`Boolean attribute "${attribute.name}" could not be resolved safely.`);
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
draft.value_boolean = booleanValue;
|
|
450
|
+
draft.raw_value = String(booleanValue);
|
|
451
|
+
draft.normalized_value = String(booleanValue);
|
|
452
|
+
}
|
|
453
|
+
else {
|
|
454
|
+
const textValue = String(rawValue !== null && rawValue !== void 0 ? rawValue : '').trim();
|
|
455
|
+
if (!textValue) {
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
draft.value_text = textValue;
|
|
459
|
+
draft.raw_value = textValue;
|
|
460
|
+
draft.normalized_value = textValue;
|
|
461
|
+
}
|
|
462
|
+
normalizedSuggestions.push(draft);
|
|
463
|
+
}
|
|
464
|
+
return normalizedSuggestions;
|
|
465
|
+
}
|
|
466
|
+
buildAiSystemPrompt(resource, locale, fieldContext, currentValues, productAttributes) {
|
|
467
|
+
const isPt = locale === null || locale === void 0 ? void 0 : locale.startsWith('pt');
|
|
468
|
+
const fieldLines = fieldContext.map((field) => {
|
|
469
|
+
const optionText = field.options.length
|
|
470
|
+
? ` options=${field.options.map((option) => `${option.label} (${option.value})`).join(', ')}`
|
|
471
|
+
: '';
|
|
472
|
+
const relationText = field.relation
|
|
473
|
+
? ` relation=${field.relation.resource || field.relation.endpoint || 'external'}`
|
|
474
|
+
: '';
|
|
475
|
+
return `- ${field.key}: type=${field.type}; required=${field.required ? 'yes' : 'no'}; label="${field.label}"${optionText}${relationText}`;
|
|
476
|
+
});
|
|
477
|
+
const productAttributeLines = productAttributes.map((attribute) => {
|
|
478
|
+
const options = Array.isArray(attribute.options) ? attribute.options : [];
|
|
479
|
+
const optionText = options.length
|
|
480
|
+
? ` options=${options
|
|
481
|
+
.map((option) => { var _a, _b; return `${String((_a = option.label) !== null && _a !== void 0 ? _a : '')} (${String((_b = option.option_value) !== null && _b !== void 0 ? _b : '')})`; })
|
|
482
|
+
.join(', ')}`
|
|
483
|
+
: '';
|
|
484
|
+
return `- ${String(attribute.slug)}: type=${String(attribute.data_type)}; name="${String(attribute.name)}"; required=${Boolean(attribute.is_required) ? 'yes' : 'no'}; group="${String(attribute.group_name)}"${optionText}`;
|
|
485
|
+
});
|
|
486
|
+
return [
|
|
487
|
+
isPt
|
|
488
|
+
? 'Voce preenche formularios administrativos do catalogo. Responda apenas com JSON valido.'
|
|
489
|
+
: 'You fill catalog admin forms. Respond with valid JSON only.',
|
|
490
|
+
isPt
|
|
491
|
+
? `Recurso atual: ${resource}.`
|
|
492
|
+
: `Current resource: ${resource}.`,
|
|
493
|
+
isPt
|
|
494
|
+
? 'Use apenas os campos permitidos listados abaixo. Nao invente ids, arquivos ou campos extras.'
|
|
495
|
+
: 'Use only the allowed fields listed below. Do not invent ids, files, or extra fields.',
|
|
496
|
+
isPt
|
|
497
|
+
? 'Para campos relacionais, prefira nomes/slug legiveis; o backend tentara resolver com seguranca.'
|
|
498
|
+
: 'For relation fields, prefer readable names/slugs; the backend will resolve them safely.',
|
|
499
|
+
isPt
|
|
500
|
+
? 'Para selects, prefira os valores validos informados.'
|
|
501
|
+
: 'For selects, prefer the valid values provided.',
|
|
502
|
+
isPt
|
|
503
|
+
? 'Estrutura da resposta: {"fields": { ... }, "product_attributes": { ... }, "notes": ["..."]}.'
|
|
504
|
+
: 'Response shape: {"fields": { ... }, "product_attributes": { ... }, "notes": ["..."]}.',
|
|
505
|
+
isPt
|
|
506
|
+
? 'Se nao souber um campo, simplesmente omita.'
|
|
507
|
+
: 'If you are unsure about a field, omit it.',
|
|
508
|
+
`Allowed fields:\n${fieldLines.join('\n')}`,
|
|
509
|
+
productAttributeLines.length
|
|
510
|
+
? `Structured product attributes:\n${productAttributeLines.join('\n')}`
|
|
511
|
+
: '',
|
|
512
|
+
`Current values: ${JSON.stringify(currentValues)}`,
|
|
513
|
+
]
|
|
514
|
+
.filter(Boolean)
|
|
515
|
+
.join('\n\n');
|
|
516
|
+
}
|
|
46
517
|
sanitizePayload(config, body) {
|
|
518
|
+
const normalizedBody = Object.assign({}, body);
|
|
519
|
+
if (config.resource === 'attributes') {
|
|
520
|
+
if (normalizedBody.name === undefined &&
|
|
521
|
+
normalizedBody.label !== undefined) {
|
|
522
|
+
normalizedBody.name = normalizedBody.label;
|
|
523
|
+
}
|
|
524
|
+
if (normalizedBody.data_type === 'select') {
|
|
525
|
+
normalizedBody.data_type = 'option';
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
if (config.resource === 'category-attributes') {
|
|
529
|
+
if (normalizedBody.is_highlight === undefined &&
|
|
530
|
+
normalizedBody.is_highlighted !== undefined) {
|
|
531
|
+
normalizedBody.is_highlight = normalizedBody.is_highlighted;
|
|
532
|
+
}
|
|
533
|
+
if (normalizedBody.sort_order === undefined &&
|
|
534
|
+
normalizedBody.display_order !== undefined) {
|
|
535
|
+
normalizedBody.sort_order = normalizedBody.display_order;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
47
538
|
const payload = {};
|
|
48
539
|
for (const field of config.fields) {
|
|
49
|
-
if (Object.prototype.hasOwnProperty.call(
|
|
50
|
-
payload[field] =
|
|
540
|
+
if (Object.prototype.hasOwnProperty.call(normalizedBody, field) && normalizedBody[field] !== undefined) {
|
|
541
|
+
payload[field] = normalizedBody[field];
|
|
51
542
|
}
|
|
52
543
|
}
|
|
53
544
|
return payload;
|
|
@@ -68,15 +559,358 @@ let CatalogService = class CatalogService {
|
|
|
68
559
|
}
|
|
69
560
|
return { OR, AND };
|
|
70
561
|
}
|
|
562
|
+
async ensureCategoryExists(categoryId) {
|
|
563
|
+
const category = await this.prisma.catalog_category.findUnique({
|
|
564
|
+
where: { id: categoryId },
|
|
565
|
+
});
|
|
566
|
+
if (!category) {
|
|
567
|
+
throw new common_1.NotFoundException('Catalog category not found');
|
|
568
|
+
}
|
|
569
|
+
return category;
|
|
570
|
+
}
|
|
571
|
+
normalizeCategoryTree(items) {
|
|
572
|
+
var _a;
|
|
573
|
+
const byId = new Map();
|
|
574
|
+
const roots = [];
|
|
575
|
+
for (const item of items) {
|
|
576
|
+
byId.set(item.id, Object.assign(Object.assign({}, item), { children: [] }));
|
|
577
|
+
}
|
|
578
|
+
for (const item of byId.values()) {
|
|
579
|
+
if (item.parent_category_id && byId.has(item.parent_category_id)) {
|
|
580
|
+
(_a = byId.get(item.parent_category_id)) === null || _a === void 0 ? void 0 : _a.children.push(item);
|
|
581
|
+
}
|
|
582
|
+
else {
|
|
583
|
+
roots.push(item);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
return roots;
|
|
587
|
+
}
|
|
588
|
+
async buildCategoryAttributePayload(categoryId, productId) {
|
|
589
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
590
|
+
const category = await this.ensureCategoryExists(categoryId);
|
|
591
|
+
const categoryAttributes = await this.prisma.catalog_category_attribute.findMany({
|
|
592
|
+
where: { catalog_category_id: categoryId },
|
|
593
|
+
orderBy: [{ sort_order: 'asc' }, { id: 'asc' }],
|
|
594
|
+
});
|
|
595
|
+
const attributeIds = [...new Set(categoryAttributes.map((item) => item.attribute_id))];
|
|
596
|
+
const attributes = attributeIds.length
|
|
597
|
+
? await this.prisma.catalog_attribute.findMany({
|
|
598
|
+
where: { id: { in: attributeIds } },
|
|
599
|
+
orderBy: [{ display_order: 'asc' }, { id: 'asc' }],
|
|
600
|
+
})
|
|
601
|
+
: [];
|
|
602
|
+
const groupIds = [...new Set(attributes.map((item) => item.group_id).filter((value) => typeof value === 'number'))];
|
|
603
|
+
const groups = groupIds.length
|
|
604
|
+
? await this.prisma.catalog_attribute_group.findMany({
|
|
605
|
+
where: { id: { in: groupIds } },
|
|
606
|
+
})
|
|
607
|
+
: [];
|
|
608
|
+
const options = attributeIds.length
|
|
609
|
+
? await this.prisma.catalog_attribute_option.findMany({
|
|
610
|
+
where: { attribute_id: { in: attributeIds } },
|
|
611
|
+
orderBy: [{ sort_order: 'asc' }, { id: 'asc' }],
|
|
612
|
+
})
|
|
613
|
+
: [];
|
|
614
|
+
const values = productId && attributeIds.length
|
|
615
|
+
? await this.prisma.catalog_product_attribute_value.findMany({
|
|
616
|
+
where: { product_id: productId, attribute_id: { in: attributeIds } },
|
|
617
|
+
include: {
|
|
618
|
+
catalog_attribute_option: true,
|
|
619
|
+
},
|
|
620
|
+
})
|
|
621
|
+
: [];
|
|
622
|
+
const categoryAttributeMap = new Map(categoryAttributes.map((item) => [item.attribute_id, item]));
|
|
623
|
+
const optionMap = new Map();
|
|
624
|
+
const valueMap = new Map(values.map((item) => [item.attribute_id, item]));
|
|
625
|
+
const groupMap = new Map(groups.map((item) => [item.id, item]));
|
|
626
|
+
for (const option of options) {
|
|
627
|
+
const bucket = (_a = optionMap.get(option.attribute_id)) !== null && _a !== void 0 ? _a : [];
|
|
628
|
+
bucket.push(option);
|
|
629
|
+
optionMap.set(option.attribute_id, bucket);
|
|
630
|
+
}
|
|
631
|
+
const grouped = new Map();
|
|
632
|
+
for (const attribute of attributes) {
|
|
633
|
+
const relation = categoryAttributeMap.get(attribute.id);
|
|
634
|
+
if (!relation) {
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
const groupId = (_b = attribute.group_id) !== null && _b !== void 0 ? _b : 0;
|
|
638
|
+
const group = attribute.group_id ? groupMap.get(attribute.group_id) : null;
|
|
639
|
+
const fallbackGroupName = String((_c = attribute.group_name) !== null && _c !== void 0 ? _c : '').trim() || 'General';
|
|
640
|
+
const groupKey = attribute.group_id ? String(groupId) : `name:${fallbackGroupName}`;
|
|
641
|
+
const existingGroup = (_d = grouped.get(groupKey)) !== null && _d !== void 0 ? _d : {
|
|
642
|
+
id: (_e = group === null || group === void 0 ? void 0 : group.id) !== null && _e !== void 0 ? _e : null,
|
|
643
|
+
slug: (_f = group === null || group === void 0 ? void 0 : group.slug) !== null && _f !== void 0 ? _f : fallbackGroupName.toLowerCase().replace(/\s+/g, '-'),
|
|
644
|
+
name: (_g = group === null || group === void 0 ? void 0 : group.name) !== null && _g !== void 0 ? _g : fallbackGroupName,
|
|
645
|
+
attributes: [],
|
|
646
|
+
};
|
|
647
|
+
existingGroup.attributes.push(Object.assign(Object.assign({}, attribute), { category_attribute: relation, options: (_h = optionMap.get(attribute.id)) !== null && _h !== void 0 ? _h : [], value: (_j = valueMap.get(attribute.id)) !== null && _j !== void 0 ? _j : null }));
|
|
648
|
+
grouped.set(groupKey, existingGroup);
|
|
649
|
+
}
|
|
650
|
+
const resultGroups = [...grouped.values()].map((group) => (Object.assign(Object.assign({}, group), { attributes: group.attributes.sort((left, right) => {
|
|
651
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
652
|
+
const leftRelation = left.category_attribute;
|
|
653
|
+
const rightRelation = right.category_attribute;
|
|
654
|
+
return (Number((_a = leftRelation.sort_order) !== null && _a !== void 0 ? _a : 0) - Number((_b = rightRelation.sort_order) !== null && _b !== void 0 ? _b : 0) ||
|
|
655
|
+
Number((_c = leftRelation.display_order) !== null && _c !== void 0 ? _c : 0) - Number((_d = rightRelation.display_order) !== null && _d !== void 0 ? _d : 0) ||
|
|
656
|
+
Number((_e = left.display_order) !== null && _e !== void 0 ? _e : 0) - Number((_f = right.display_order) !== null && _f !== void 0 ? _f : 0) ||
|
|
657
|
+
Number((_g = left.id) !== null && _g !== void 0 ? _g : 0) - Number((_h = right.id) !== null && _h !== void 0 ? _h : 0));
|
|
658
|
+
}) })));
|
|
659
|
+
resultGroups.sort((left, right) => String(left.name).localeCompare(String(right.name)));
|
|
660
|
+
return {
|
|
661
|
+
category,
|
|
662
|
+
product_id: productId !== null && productId !== void 0 ? productId : null,
|
|
663
|
+
groups: resultGroups,
|
|
664
|
+
attributes: resultGroups.flatMap((group) => group.attributes),
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
isMeaningfulAttributeValue(value) {
|
|
668
|
+
var _a;
|
|
669
|
+
if (value.attribute_option_id !== undefined && value.attribute_option_id !== null) {
|
|
670
|
+
return true;
|
|
671
|
+
}
|
|
672
|
+
if (value.value_number !== undefined && value.value_number !== null) {
|
|
673
|
+
return true;
|
|
674
|
+
}
|
|
675
|
+
if (value.value_boolean !== undefined && value.value_boolean !== null) {
|
|
676
|
+
return true;
|
|
677
|
+
}
|
|
678
|
+
return String((_a = value.value_text) !== null && _a !== void 0 ? _a : '').trim().length > 0;
|
|
679
|
+
}
|
|
680
|
+
buildAttributeSnapshotValue(attribute) {
|
|
681
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
682
|
+
const value = ((_a = attribute.value) !== null && _a !== void 0 ? _a : null);
|
|
683
|
+
const dataType = String((_b = attribute.data_type) !== null && _b !== void 0 ? _b : 'text');
|
|
684
|
+
const option = (_c = value === null || value === void 0 ? void 0 : value.catalog_attribute_option) !== null && _c !== void 0 ? _c : null;
|
|
685
|
+
if (!value) {
|
|
686
|
+
return {
|
|
687
|
+
value: null,
|
|
688
|
+
value_text: null,
|
|
689
|
+
value_number: null,
|
|
690
|
+
value_boolean: null,
|
|
691
|
+
normalized_value: null,
|
|
692
|
+
raw_value: null,
|
|
693
|
+
option: null,
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
if (dataType === 'option') {
|
|
697
|
+
const optionPayload = option
|
|
698
|
+
? {
|
|
699
|
+
id: Number(option.id),
|
|
700
|
+
slug: String((_d = option.slug) !== null && _d !== void 0 ? _d : ''),
|
|
701
|
+
label: String((_f = (_e = option.label) !== null && _e !== void 0 ? _e : option.option_value) !== null && _f !== void 0 ? _f : ''),
|
|
702
|
+
option_value: String((_g = option.option_value) !== null && _g !== void 0 ? _g : ''),
|
|
703
|
+
}
|
|
704
|
+
: null;
|
|
705
|
+
return {
|
|
706
|
+
value: (_j = (_h = optionPayload === null || optionPayload === void 0 ? void 0 : optionPayload.option_value) !== null && _h !== void 0 ? _h : value.value_text) !== null && _j !== void 0 ? _j : null,
|
|
707
|
+
value_text: value.value_text ? String(value.value_text) : null,
|
|
708
|
+
value_number: null,
|
|
709
|
+
value_boolean: null,
|
|
710
|
+
normalized_value: value.normalized_value ? String(value.normalized_value) : null,
|
|
711
|
+
raw_value: value.raw_value ? String(value.raw_value) : null,
|
|
712
|
+
option: optionPayload,
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
if (dataType === 'number') {
|
|
716
|
+
return {
|
|
717
|
+
value: value.value_number === null || value.value_number === undefined
|
|
718
|
+
? null
|
|
719
|
+
: Number(value.value_number),
|
|
720
|
+
value_text: value.value_text ? String(value.value_text) : null,
|
|
721
|
+
value_number: value.value_number === null || value.value_number === undefined
|
|
722
|
+
? null
|
|
723
|
+
: Number(value.value_number),
|
|
724
|
+
value_boolean: null,
|
|
725
|
+
normalized_value: value.normalized_value ? String(value.normalized_value) : null,
|
|
726
|
+
raw_value: value.raw_value ? String(value.raw_value) : null,
|
|
727
|
+
option: null,
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
if (dataType === 'boolean') {
|
|
731
|
+
return {
|
|
732
|
+
value: value.value_boolean === null || value.value_boolean === undefined
|
|
733
|
+
? null
|
|
734
|
+
: Boolean(value.value_boolean),
|
|
735
|
+
value_text: value.value_text ? String(value.value_text) : null,
|
|
736
|
+
value_number: null,
|
|
737
|
+
value_boolean: value.value_boolean === null || value.value_boolean === undefined
|
|
738
|
+
? null
|
|
739
|
+
: Boolean(value.value_boolean),
|
|
740
|
+
normalized_value: value.normalized_value ? String(value.normalized_value) : null,
|
|
741
|
+
raw_value: value.raw_value ? String(value.raw_value) : null,
|
|
742
|
+
option: null,
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
return {
|
|
746
|
+
value: value.value_text ? String(value.value_text) : null,
|
|
747
|
+
value_text: value.value_text ? String(value.value_text) : null,
|
|
748
|
+
value_number: null,
|
|
749
|
+
value_boolean: null,
|
|
750
|
+
normalized_value: value.normalized_value ? String(value.normalized_value) : null,
|
|
751
|
+
raw_value: value.raw_value ? String(value.raw_value) : null,
|
|
752
|
+
option: null,
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
toAttributeSnapshot(attribute) {
|
|
756
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
|
|
757
|
+
const relation = ((_a = attribute.category_attribute) !== null && _a !== void 0 ? _a : {});
|
|
758
|
+
const snapshotValue = this.buildAttributeSnapshotValue(attribute);
|
|
759
|
+
return Object.assign({ slug: String((_b = attribute.slug) !== null && _b !== void 0 ? _b : ''), name: String((_d = (_c = attribute.name) !== null && _c !== void 0 ? _c : attribute.slug) !== null && _d !== void 0 ? _d : ''), data_type: String((_e = attribute.data_type) !== null && _e !== void 0 ? _e : 'text'), unit: attribute.unit ? String(attribute.unit) : null, group_name: String((_f = attribute.group_name) !== null && _f !== void 0 ? _f : 'General'), is_required: Boolean((_g = relation.is_required) !== null && _g !== void 0 ? _g : false), is_highlight: Boolean((_h = relation.is_highlight) !== null && _h !== void 0 ? _h : false), is_filter_visible: Boolean((_j = relation.is_filter_visible) !== null && _j !== void 0 ? _j : false), is_comparison_visible: Boolean((_k = relation.is_comparison_visible) !== null && _k !== void 0 ? _k : false) }, snapshotValue);
|
|
760
|
+
}
|
|
761
|
+
async decorateProductRecords(records) {
|
|
762
|
+
if (!records.length) {
|
|
763
|
+
return records;
|
|
764
|
+
}
|
|
765
|
+
const brandIds = [...new Set(records
|
|
766
|
+
.map((record) => record.brand_id)
|
|
767
|
+
.filter((value) => typeof value === 'number'))];
|
|
768
|
+
const categoryIds = [...new Set(records
|
|
769
|
+
.map((record) => record.catalog_category_id)
|
|
770
|
+
.filter((value) => typeof value === 'number'))];
|
|
771
|
+
const [brands, categories] = await Promise.all([
|
|
772
|
+
brandIds.length
|
|
773
|
+
? this.prisma.catalog_brand.findMany({
|
|
774
|
+
where: { id: { in: brandIds } },
|
|
775
|
+
select: { id: true, name: true, slug: true, logo_file_id: true },
|
|
776
|
+
})
|
|
777
|
+
: Promise.resolve([]),
|
|
778
|
+
categoryIds.length
|
|
779
|
+
? this.prisma.catalog_category.findMany({
|
|
780
|
+
where: { id: { in: categoryIds } },
|
|
781
|
+
select: { id: true, name: true, slug: true },
|
|
782
|
+
})
|
|
783
|
+
: Promise.resolve([]),
|
|
784
|
+
]);
|
|
785
|
+
const brandMap = new Map(brands.map((item) => [item.id, item]));
|
|
786
|
+
const categoryMap = new Map(categories.map((item) => [item.id, item]));
|
|
787
|
+
return records.map((record) => {
|
|
788
|
+
var _a, _b, _c, _d, _e;
|
|
789
|
+
const brand = record.brand_id ? brandMap.get(record.brand_id) : null;
|
|
790
|
+
const category = record.catalog_category_id
|
|
791
|
+
? categoryMap.get(record.catalog_category_id)
|
|
792
|
+
: null;
|
|
793
|
+
return Object.assign(Object.assign({}, record), { brand_name: (_a = brand === null || brand === void 0 ? void 0 : brand.name) !== null && _a !== void 0 ? _a : null, brand_slug: (_b = brand === null || brand === void 0 ? void 0 : brand.slug) !== null && _b !== void 0 ? _b : null, brand_logo_file_id: (_c = brand === null || brand === void 0 ? void 0 : brand.logo_file_id) !== null && _c !== void 0 ? _c : null, category_name: (_d = category === null || category === void 0 ? void 0 : category.name) !== null && _d !== void 0 ? _d : null, category_slug: (_e = category === null || category === void 0 ? void 0 : category.slug) !== null && _e !== void 0 ? _e : null });
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
validateAttributeValueByType(attribute, value) {
|
|
797
|
+
var _a;
|
|
798
|
+
const hasText = String((_a = value.value_text) !== null && _a !== void 0 ? _a : '').trim().length > 0;
|
|
799
|
+
const hasNumber = value.value_number !== undefined && value.value_number !== null;
|
|
800
|
+
const hasBoolean = value.value_boolean !== undefined && value.value_boolean !== null;
|
|
801
|
+
const hasOption = value.attribute_option_id !== undefined && value.attribute_option_id !== null;
|
|
802
|
+
const filledKinds = [hasText, hasNumber, hasBoolean, hasOption].filter(Boolean).length;
|
|
803
|
+
const attributeLabel = attribute.name || attribute.slug || `#${attribute.id}`;
|
|
804
|
+
if (filledKinds > 1) {
|
|
805
|
+
throw new common_1.BadRequestException(`Attribute "${attributeLabel}" must use a single value field compatible with its type`);
|
|
806
|
+
}
|
|
807
|
+
switch (attribute.data_type) {
|
|
808
|
+
case 'text':
|
|
809
|
+
case 'long_text':
|
|
810
|
+
if (hasNumber || hasBoolean || hasOption) {
|
|
811
|
+
throw new common_1.BadRequestException(`Attribute "${attributeLabel}" only accepts text values`);
|
|
812
|
+
}
|
|
813
|
+
break;
|
|
814
|
+
case 'number':
|
|
815
|
+
if (hasText || hasBoolean || hasOption) {
|
|
816
|
+
throw new common_1.BadRequestException(`Attribute "${attributeLabel}" only accepts number values`);
|
|
817
|
+
}
|
|
818
|
+
break;
|
|
819
|
+
case 'boolean':
|
|
820
|
+
if (hasText || hasNumber || hasOption) {
|
|
821
|
+
throw new common_1.BadRequestException(`Attribute "${attributeLabel}" only accepts boolean values`);
|
|
822
|
+
}
|
|
823
|
+
break;
|
|
824
|
+
case 'option':
|
|
825
|
+
if (hasText || hasNumber || hasBoolean) {
|
|
826
|
+
throw new common_1.BadRequestException(`Attribute "${attributeLabel}" only accepts option values`);
|
|
827
|
+
}
|
|
828
|
+
break;
|
|
829
|
+
default:
|
|
830
|
+
break;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
async buildProductStructuredPayload(productId) {
|
|
834
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
835
|
+
const product = await this.getById('products', productId, 'en');
|
|
836
|
+
const attributes = await this.listProductAttributes(productId);
|
|
837
|
+
const groups = ((_a = attributes.groups) !== null && _a !== void 0 ? _a : []).map((group) => ({
|
|
838
|
+
id: group.id,
|
|
839
|
+
slug: group.slug,
|
|
840
|
+
name: group.name,
|
|
841
|
+
attributes: group.attributes.map((attribute) => this.toAttributeSnapshot(attribute)),
|
|
842
|
+
}));
|
|
843
|
+
const flatAttributes = groups.flatMap((group) => group.attributes);
|
|
844
|
+
return {
|
|
845
|
+
product,
|
|
846
|
+
category: attributes.category,
|
|
847
|
+
groups,
|
|
848
|
+
attributes: flatAttributes,
|
|
849
|
+
comparison: {
|
|
850
|
+
category: {
|
|
851
|
+
id: (_c = (_b = attributes.category) === null || _b === void 0 ? void 0 : _b.id) !== null && _c !== void 0 ? _c : null,
|
|
852
|
+
slug: (_e = (_d = attributes.category) === null || _d === void 0 ? void 0 : _d.slug) !== null && _e !== void 0 ? _e : null,
|
|
853
|
+
name: (_g = (_f = attributes.category) === null || _f === void 0 ? void 0 : _f.name) !== null && _g !== void 0 ? _g : null,
|
|
854
|
+
},
|
|
855
|
+
highlights: flatAttributes.filter((attribute) => attribute.is_highlight),
|
|
856
|
+
comparable_attributes: flatAttributes.filter((attribute) => attribute.is_comparison_visible),
|
|
857
|
+
filter_attributes: flatAttributes.filter((attribute) => attribute.is_filter_visible),
|
|
858
|
+
},
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
async materializeProductSnapshots(productId) {
|
|
862
|
+
const structured = await this.buildProductStructuredPayload(productId);
|
|
863
|
+
const specSnapshot = {
|
|
864
|
+
version: 1,
|
|
865
|
+
source: 'catalog_product_attribute_value',
|
|
866
|
+
category: structured.category
|
|
867
|
+
? {
|
|
868
|
+
id: structured.category.id,
|
|
869
|
+
slug: structured.category.slug,
|
|
870
|
+
name: structured.category.name,
|
|
871
|
+
}
|
|
872
|
+
: null,
|
|
873
|
+
groups: structured.groups,
|
|
874
|
+
attributes: structured.attributes.reduce((acc, attribute) => {
|
|
875
|
+
acc[attribute.slug] = attribute;
|
|
876
|
+
return acc;
|
|
877
|
+
}, {}),
|
|
878
|
+
generated_at: new Date().toISOString(),
|
|
879
|
+
};
|
|
880
|
+
const comparisonSnapshot = {
|
|
881
|
+
version: 1,
|
|
882
|
+
source: 'catalog_product_attribute_value',
|
|
883
|
+
category: structured.comparison.category,
|
|
884
|
+
highlights: structured.comparison.highlights,
|
|
885
|
+
comparable_attributes: structured.comparison.comparable_attributes,
|
|
886
|
+
generated_at: new Date().toISOString(),
|
|
887
|
+
};
|
|
888
|
+
await this.prisma.catalog_product.update({
|
|
889
|
+
where: { id: productId },
|
|
890
|
+
data: {
|
|
891
|
+
// Snapshots remain secondary/derived fields for legacy reads and fast payloads.
|
|
892
|
+
spec_snapshot_json: specSnapshot,
|
|
893
|
+
comparison_snapshot_json: comparisonSnapshot,
|
|
894
|
+
},
|
|
895
|
+
});
|
|
896
|
+
return {
|
|
897
|
+
spec_snapshot_json: specSnapshot,
|
|
898
|
+
comparison_snapshot_json: comparisonSnapshot,
|
|
899
|
+
};
|
|
900
|
+
}
|
|
71
901
|
async list(resource, locale, paginationParams, query) {
|
|
72
902
|
var _a;
|
|
73
903
|
const config = this.getConfig(resource, locale);
|
|
74
904
|
const model = this.getModel(resource, locale);
|
|
75
905
|
const where = this.buildWhere(config, paginationParams, query);
|
|
76
|
-
|
|
906
|
+
const result = await this.pagination.paginate(model, paginationParams, {
|
|
77
907
|
where,
|
|
78
908
|
orderBy: (_a = config.defaultOrderBy) !== null && _a !== void 0 ? _a : { id: 'desc' },
|
|
79
909
|
}, locale);
|
|
910
|
+
if (resource === 'products' && Array.isArray(result.data)) {
|
|
911
|
+
return Object.assign(Object.assign({}, result), { data: await this.decorateProductRecords(result.data) });
|
|
912
|
+
}
|
|
913
|
+
return result;
|
|
80
914
|
}
|
|
81
915
|
async stats(resource, locale) {
|
|
82
916
|
const config = this.getConfig(resource, locale);
|
|
@@ -93,6 +927,44 @@ let CatalogService = class CatalogService {
|
|
|
93
927
|
return result;
|
|
94
928
|
}
|
|
95
929
|
async getById(resource, id, locale) {
|
|
930
|
+
if (resource === 'products') {
|
|
931
|
+
const item = await this.prisma.catalog_product.findUnique({
|
|
932
|
+
where: { id },
|
|
933
|
+
include: {
|
|
934
|
+
catalog_brand: {
|
|
935
|
+
select: {
|
|
936
|
+
id: true,
|
|
937
|
+
name: true,
|
|
938
|
+
slug: true,
|
|
939
|
+
logo_file_id: true,
|
|
940
|
+
},
|
|
941
|
+
},
|
|
942
|
+
catalog_category: {
|
|
943
|
+
select: {
|
|
944
|
+
id: true,
|
|
945
|
+
name: true,
|
|
946
|
+
slug: true,
|
|
947
|
+
},
|
|
948
|
+
},
|
|
949
|
+
catalog_product_image: {
|
|
950
|
+
orderBy: [{ sort_order: 'asc' }, { id: 'asc' }],
|
|
951
|
+
select: {
|
|
952
|
+
id: true,
|
|
953
|
+
file_id: true,
|
|
954
|
+
role: true,
|
|
955
|
+
sort_order: true,
|
|
956
|
+
is_primary: true,
|
|
957
|
+
alt_text: true,
|
|
958
|
+
},
|
|
959
|
+
},
|
|
960
|
+
},
|
|
961
|
+
});
|
|
962
|
+
if (!item) {
|
|
963
|
+
throw new common_1.NotFoundException((0, api_locale_1.getLocaleText)('resourceNotFound', locale, 'Catalog resource not found'));
|
|
964
|
+
}
|
|
965
|
+
const [decorated] = await this.decorateProductRecords([item]);
|
|
966
|
+
return decorated;
|
|
967
|
+
}
|
|
96
968
|
const model = this.getModel(resource, locale);
|
|
97
969
|
const item = await model.findUnique({ where: { id } });
|
|
98
970
|
if (!item) {
|
|
@@ -132,11 +1004,245 @@ let CatalogService = class CatalogService {
|
|
|
132
1004
|
},
|
|
133
1005
|
}, locale);
|
|
134
1006
|
}
|
|
1007
|
+
async getCategoriesTree() {
|
|
1008
|
+
const categories = await this.prisma.catalog_category.findMany({
|
|
1009
|
+
orderBy: [{ sort_order: 'asc' }, { name: 'asc' }],
|
|
1010
|
+
});
|
|
1011
|
+
return this.normalizeCategoryTree(categories);
|
|
1012
|
+
}
|
|
1013
|
+
async listCategoryAttributes(categoryId) {
|
|
1014
|
+
return this.buildCategoryAttributePayload(categoryId);
|
|
1015
|
+
}
|
|
1016
|
+
async listProductAttributes(productId) {
|
|
1017
|
+
const product = await this.prisma.catalog_product.findUnique({
|
|
1018
|
+
where: { id: productId },
|
|
1019
|
+
});
|
|
1020
|
+
if (!product) {
|
|
1021
|
+
throw new common_1.NotFoundException('Catalog product not found');
|
|
1022
|
+
}
|
|
1023
|
+
return this.buildCategoryAttributePayload(product.catalog_category_id, productId);
|
|
1024
|
+
}
|
|
1025
|
+
async getProductStructuredPayload(productId) {
|
|
1026
|
+
return this.buildProductStructuredPayload(productId);
|
|
1027
|
+
}
|
|
1028
|
+
async getProductComparisonPayload(productId) {
|
|
1029
|
+
const structured = await this.buildProductStructuredPayload(productId);
|
|
1030
|
+
return structured.comparison;
|
|
1031
|
+
}
|
|
1032
|
+
async updateProductAttributes(productId, values) {
|
|
1033
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v;
|
|
1034
|
+
const product = await this.prisma.catalog_product.findUnique({
|
|
1035
|
+
where: { id: productId },
|
|
1036
|
+
});
|
|
1037
|
+
if (!product) {
|
|
1038
|
+
throw new common_1.NotFoundException('Catalog product not found');
|
|
1039
|
+
}
|
|
1040
|
+
const categoryAttributes = await this.prisma.catalog_category_attribute.findMany({
|
|
1041
|
+
where: { catalog_category_id: product.catalog_category_id },
|
|
1042
|
+
});
|
|
1043
|
+
const allowedAttributeIds = new Set(categoryAttributes.map((item) => item.attribute_id));
|
|
1044
|
+
const requiredAttributeIds = new Set(categoryAttributes
|
|
1045
|
+
.filter((item) => item.is_required)
|
|
1046
|
+
.map((item) => item.attribute_id));
|
|
1047
|
+
const attributes = allowedAttributeIds.size
|
|
1048
|
+
? await this.prisma.catalog_attribute.findMany({
|
|
1049
|
+
where: { id: { in: [...allowedAttributeIds] } },
|
|
1050
|
+
select: {
|
|
1051
|
+
id: true,
|
|
1052
|
+
slug: true,
|
|
1053
|
+
name: true,
|
|
1054
|
+
data_type: true,
|
|
1055
|
+
},
|
|
1056
|
+
})
|
|
1057
|
+
: [];
|
|
1058
|
+
const attributeMap = new Map(attributes.map((item) => [item.id, item]));
|
|
1059
|
+
const existingValues = await this.prisma.catalog_product_attribute_value.findMany({
|
|
1060
|
+
where: { product_id: productId },
|
|
1061
|
+
});
|
|
1062
|
+
const optionIds = values
|
|
1063
|
+
.map((value) => value.attribute_option_id)
|
|
1064
|
+
.filter((value) => typeof value === 'number');
|
|
1065
|
+
const options = optionIds.length
|
|
1066
|
+
? await this.prisma.catalog_attribute_option.findMany({
|
|
1067
|
+
where: { id: { in: optionIds } },
|
|
1068
|
+
})
|
|
1069
|
+
: [];
|
|
1070
|
+
const optionMap = new Map(options.map((item) => [item.id, item]));
|
|
1071
|
+
const incomingByAttribute = new Map();
|
|
1072
|
+
for (const value of values) {
|
|
1073
|
+
const attributeId = Number((_a = value.attribute_id) !== null && _a !== void 0 ? _a : 0);
|
|
1074
|
+
if (!attributeId || !allowedAttributeIds.has(attributeId)) {
|
|
1075
|
+
throw new common_1.BadRequestException(`Attribute ${attributeId} is not allowed for this category`);
|
|
1076
|
+
}
|
|
1077
|
+
const attribute = attributeMap.get(attributeId);
|
|
1078
|
+
if (!attribute) {
|
|
1079
|
+
throw new common_1.BadRequestException(`Attribute ${attributeId} was not found`);
|
|
1080
|
+
}
|
|
1081
|
+
this.validateAttributeValueByType(attribute, value);
|
|
1082
|
+
incomingByAttribute.set(attributeId, value);
|
|
1083
|
+
}
|
|
1084
|
+
const incomingAttributeIds = new Set(incomingByAttribute.keys());
|
|
1085
|
+
for (const attributeId of requiredAttributeIds) {
|
|
1086
|
+
const requiredValue = incomingByAttribute.get(attributeId);
|
|
1087
|
+
if (!requiredValue || !this.isMeaningfulAttributeValue(requiredValue)) {
|
|
1088
|
+
const attribute = attributeMap.get(attributeId);
|
|
1089
|
+
throw new common_1.BadRequestException(`Attribute "${(_c = (_b = attribute === null || attribute === void 0 ? void 0 : attribute.name) !== null && _b !== void 0 ? _b : attribute === null || attribute === void 0 ? void 0 : attribute.slug) !== null && _c !== void 0 ? _c : attributeId}" is required for this category`);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
for (const existingValue of existingValues) {
|
|
1093
|
+
if (!incomingAttributeIds.has(existingValue.attribute_id)) {
|
|
1094
|
+
await this.prisma.catalog_product_attribute_value.delete({
|
|
1095
|
+
where: { id: existingValue.id },
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
for (const [attributeId, value] of incomingByAttribute.entries()) {
|
|
1100
|
+
const option = value.attribute_option_id ? optionMap.get(value.attribute_option_id) : null;
|
|
1101
|
+
const attribute = attributeMap.get(attributeId);
|
|
1102
|
+
if (value.attribute_option_id && (!option || option.attribute_id !== attributeId)) {
|
|
1103
|
+
throw new common_1.BadRequestException(`Option ${value.attribute_option_id} does not belong to attribute ${(_d = attribute === null || attribute === void 0 ? void 0 : attribute.name) !== null && _d !== void 0 ? _d : attributeId}`);
|
|
1104
|
+
}
|
|
1105
|
+
const normalizedText = (_h = (_g = (_f = (_e = value.normalized_text) !== null && _e !== void 0 ? _e : option === null || option === void 0 ? void 0 : option.normalized_value) !== null && _f !== void 0 ? _f : option === null || option === void 0 ? void 0 : option.option_value) !== null && _g !== void 0 ? _g : value.value_text) !== null && _h !== void 0 ? _h : null;
|
|
1106
|
+
const sourceType = value.source_type === 'import' || value.source_type === 'computed'
|
|
1107
|
+
? value.source_type
|
|
1108
|
+
: 'manual';
|
|
1109
|
+
const payload = {
|
|
1110
|
+
attribute_option_id: (_j = value.attribute_option_id) !== null && _j !== void 0 ? _j : null,
|
|
1111
|
+
value_text: (_k = option === null || option === void 0 ? void 0 : option.option_value) !== null && _k !== void 0 ? _k : (String((_l = value.value_text) !== null && _l !== void 0 ? _l : '').trim() || null),
|
|
1112
|
+
value_number: (_m = value.value_number) !== null && _m !== void 0 ? _m : null,
|
|
1113
|
+
value_boolean: (_o = value.value_boolean) !== null && _o !== void 0 ? _o : null,
|
|
1114
|
+
raw_value: (_p = option === null || option === void 0 ? void 0 : option.option_value) !== null && _p !== void 0 ? _p : (value.value_number !== undefined && value.value_number !== null
|
|
1115
|
+
? String(value.value_number)
|
|
1116
|
+
: value.value_boolean !== undefined && value.value_boolean !== null
|
|
1117
|
+
? String(value.value_boolean)
|
|
1118
|
+
: String((_q = value.value_text) !== null && _q !== void 0 ? _q : '').trim() || null),
|
|
1119
|
+
value_unit: String((_r = value.value_unit) !== null && _r !== void 0 ? _r : '').trim() || null,
|
|
1120
|
+
normalized_value: normalizedText
|
|
1121
|
+
? String(normalizedText).trim()
|
|
1122
|
+
: value.value_number !== undefined && value.value_number !== null
|
|
1123
|
+
? String(value.value_number)
|
|
1124
|
+
: value.value_boolean !== undefined && value.value_boolean !== null
|
|
1125
|
+
? String(value.value_boolean)
|
|
1126
|
+
: null,
|
|
1127
|
+
normalized_text: normalizedText ? String(normalizedText).trim() : null,
|
|
1128
|
+
normalized_number: (_t = (_s = value.normalized_number) !== null && _s !== void 0 ? _s : value.value_number) !== null && _t !== void 0 ? _t : null,
|
|
1129
|
+
source_type: sourceType,
|
|
1130
|
+
confidence_score: (_u = value.confidence_score) !== null && _u !== void 0 ? _u : null,
|
|
1131
|
+
is_verified: Boolean((_v = value.is_verified) !== null && _v !== void 0 ? _v : false),
|
|
1132
|
+
};
|
|
1133
|
+
const existingValue = existingValues.find((item) => item.attribute_id === attributeId);
|
|
1134
|
+
if (!this.isMeaningfulAttributeValue(payload)) {
|
|
1135
|
+
if (existingValue) {
|
|
1136
|
+
await this.prisma.catalog_product_attribute_value.delete({
|
|
1137
|
+
where: { id: existingValue.id },
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1140
|
+
continue;
|
|
1141
|
+
}
|
|
1142
|
+
if (existingValue) {
|
|
1143
|
+
const updatePayload = {
|
|
1144
|
+
attribute_option_id: payload.attribute_option_id,
|
|
1145
|
+
value_text: payload.value_text,
|
|
1146
|
+
value_number: payload.value_number,
|
|
1147
|
+
value_boolean: payload.value_boolean,
|
|
1148
|
+
raw_value: payload.raw_value,
|
|
1149
|
+
value_unit: payload.value_unit,
|
|
1150
|
+
normalized_value: payload.normalized_value,
|
|
1151
|
+
normalized_text: payload.normalized_text,
|
|
1152
|
+
normalized_number: payload.normalized_number,
|
|
1153
|
+
source_type: payload.source_type,
|
|
1154
|
+
confidence_score: payload.confidence_score,
|
|
1155
|
+
is_verified: payload.is_verified,
|
|
1156
|
+
};
|
|
1157
|
+
await this.prisma.catalog_product_attribute_value.update({
|
|
1158
|
+
where: { id: existingValue.id },
|
|
1159
|
+
data: updatePayload,
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
else {
|
|
1163
|
+
const createPayload = {
|
|
1164
|
+
product_id: productId,
|
|
1165
|
+
attribute_id: attributeId,
|
|
1166
|
+
attribute_option_id: payload.attribute_option_id,
|
|
1167
|
+
value_text: payload.value_text,
|
|
1168
|
+
value_number: payload.value_number,
|
|
1169
|
+
value_boolean: payload.value_boolean,
|
|
1170
|
+
raw_value: payload.raw_value,
|
|
1171
|
+
value_unit: payload.value_unit,
|
|
1172
|
+
normalized_value: payload.normalized_value,
|
|
1173
|
+
normalized_text: payload.normalized_text,
|
|
1174
|
+
normalized_number: payload.normalized_number,
|
|
1175
|
+
source_type: payload.source_type,
|
|
1176
|
+
confidence_score: payload.confidence_score,
|
|
1177
|
+
is_verified: payload.is_verified,
|
|
1178
|
+
};
|
|
1179
|
+
await this.prisma.catalog_product_attribute_value.create({
|
|
1180
|
+
data: createPayload,
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
await this.materializeProductSnapshots(productId);
|
|
1185
|
+
return this.listProductAttributes(productId);
|
|
1186
|
+
}
|
|
1187
|
+
async generateFormAssistSuggestion(resource, input, locale) {
|
|
1188
|
+
var _a, _b, _c, _d, _e, _f;
|
|
1189
|
+
const config = this.getConfig(resource, locale);
|
|
1190
|
+
const fieldContext = this.normalizeAiFieldContext(resource, (_a = input.fields) !== null && _a !== void 0 ? _a : []);
|
|
1191
|
+
const currentValues = (_b = input.current_values) !== null && _b !== void 0 ? _b : {};
|
|
1192
|
+
const warnings = [];
|
|
1193
|
+
const categoryId = resource === 'products'
|
|
1194
|
+
? Number((_c = currentValues.catalog_category_id) !== null && _c !== void 0 ? _c : 0) || null
|
|
1195
|
+
: null;
|
|
1196
|
+
const productAttributeContext = categoryId
|
|
1197
|
+
? this.buildAttributeContext(await this.buildCategoryAttributePayload(categoryId), (_d = input.current_attribute_values) !== null && _d !== void 0 ? _d : [])
|
|
1198
|
+
: [];
|
|
1199
|
+
const systemPrompt = this.buildAiSystemPrompt(resource, locale, fieldContext, currentValues, productAttributeContext);
|
|
1200
|
+
const aiResponse = await this.aiService.chat({
|
|
1201
|
+
provider: 'openai',
|
|
1202
|
+
model: 'gpt-4o-mini',
|
|
1203
|
+
message: input.prompt,
|
|
1204
|
+
systemPrompt,
|
|
1205
|
+
});
|
|
1206
|
+
const parsed = this.extractJsonObject(String((_e = aiResponse.content) !== null && _e !== void 0 ? _e : ''));
|
|
1207
|
+
const rawFields = parsed.fields && typeof parsed.fields === 'object'
|
|
1208
|
+
? parsed.fields
|
|
1209
|
+
: {};
|
|
1210
|
+
const normalizedFields = {};
|
|
1211
|
+
for (const field of fieldContext) {
|
|
1212
|
+
if (!Object.prototype.hasOwnProperty.call(rawFields, field.key)) {
|
|
1213
|
+
continue;
|
|
1214
|
+
}
|
|
1215
|
+
const normalizedValue = await this.normalizeAiFieldSuggestion(field, rawFields[field.key], warnings);
|
|
1216
|
+
if (normalizedValue !== undefined) {
|
|
1217
|
+
normalizedFields[field.key] = normalizedValue;
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
const productAttributes = await this.normalizeAiProductAttributeSuggestions(categoryId, parsed.product_attributes, (_f = input.current_attribute_values) !== null && _f !== void 0 ? _f : [], warnings);
|
|
1221
|
+
const appliedFields = fieldContext
|
|
1222
|
+
.filter((field) => Object.prototype.hasOwnProperty.call(normalizedFields, field.key))
|
|
1223
|
+
.map((field) => ({
|
|
1224
|
+
key: field.key,
|
|
1225
|
+
label: field.label,
|
|
1226
|
+
type: field.type,
|
|
1227
|
+
value: normalizedFields[field.key],
|
|
1228
|
+
}));
|
|
1229
|
+
return {
|
|
1230
|
+
resource: config.resource,
|
|
1231
|
+
fields: normalizedFields,
|
|
1232
|
+
applied_fields: appliedFields,
|
|
1233
|
+
product_attributes: productAttributes,
|
|
1234
|
+
warnings: [
|
|
1235
|
+
...warnings,
|
|
1236
|
+
...(Array.isArray(parsed.notes) ? parsed.notes.map((note) => String(note)) : []),
|
|
1237
|
+
],
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
135
1240
|
};
|
|
136
1241
|
exports.CatalogService = CatalogService;
|
|
137
1242
|
exports.CatalogService = CatalogService = __decorate([
|
|
138
1243
|
(0, common_1.Injectable)(),
|
|
139
1244
|
__metadata("design:paramtypes", [api_prisma_1.PrismaService,
|
|
140
|
-
api_pagination_1.PaginationService
|
|
1245
|
+
api_pagination_1.PaginationService,
|
|
1246
|
+
core_1.AiService])
|
|
141
1247
|
], CatalogService);
|
|
142
1248
|
//# sourceMappingURL=catalog.service.js.map
|