@floristcloud/api-lib 1.2.134 → 1.2.135

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/build/commands/attribute-normalization/analyze-attribute-normalization.query.js +19 -0
  2. package/build/commands/attribute-normalization/execute-attribute-normalization.command.js +28 -0
  3. package/build/commands/attribute-normalization/get-grower-suggestions.query.js +26 -0
  4. package/build/commands/attribute-normalization/index.js +9 -0
  5. package/build/commands/index.js +1 -0
  6. package/build/commands/product-image/analyze-product-image-merge.query.js +22 -0
  7. package/build/commands/product-image/execute-product-image-merge.command.js +25 -0
  8. package/build/commands/product-image/index.js +5 -0
  9. package/build/constant/attribute-normalization.constant.js +12 -0
  10. package/build/constant/error.js +50 -0
  11. package/build/constant/index.js +2 -0
  12. package/build/constant/product-image-merge.constant.js +11 -0
  13. package/build/enum/action-log-type.enum.js +2 -0
  14. package/build/enum/index.js +5 -0
  15. package/build/enum/normalization-decision-action.enum.js +13 -0
  16. package/build/enum/normalization-target.enum.js +12 -0
  17. package/build/enum/normalization-value-state.enum.js +13 -0
  18. package/build/enum/normalized-attribute.enum.js +10 -0
  19. package/build/enum/product-image-duplicate-mode.enum.js +11 -0
  20. package/build/schemas/action-log/action-log-payload.schema.js +4 -0
  21. package/build/schemas/action-log/merge-product-images-action-log.schema.js +11 -0
  22. package/build/schemas/action-log/normalize-attributes-action-log.schema.js +21 -0
  23. package/build/schemas/attribute-normalization/attribute-normalization-scope.schema.js +32 -0
  24. package/build/schemas/attribute-normalization/normalization-decision.schema.js +20 -0
  25. package/build/schemas/attribute-normalization/normalization-row-count.schema.js +8 -0
  26. package/build/schemas/attribute-normalization/normalization-summary.schema.js +30 -0
  27. package/build/schemas/attribute-normalization/normalization-value.schema.js +19 -0
  28. package/build/schemas/attribute-normalization/product-image-duplicate-forecast.schema.js +15 -0
  29. package/build/schemas/index.js +8 -0
  30. package/build/schemas/product-image/product-image-merge-candidate.schema.js +22 -0
  31. package/build/schemas/product-image/product-image-merge-group.schema.js +11 -0
  32. package/commands/attribute-normalization/analyze-attribute-normalization.query.ts +24 -0
  33. package/commands/attribute-normalization/execute-attribute-normalization.command.ts +32 -0
  34. package/commands/attribute-normalization/get-grower-suggestions.query.ts +28 -0
  35. package/commands/attribute-normalization/index.ts +3 -0
  36. package/commands/index.ts +1 -0
  37. package/commands/product-image/analyze-product-image-merge.query.ts +28 -0
  38. package/commands/product-image/execute-product-image-merge.command.ts +27 -0
  39. package/commands/product-image/index.ts +2 -0
  40. package/constant/attribute-normalization.constant.ts +9 -0
  41. package/constant/error.ts +50 -0
  42. package/constant/index.ts +2 -0
  43. package/constant/product-image-merge.constant.ts +8 -0
  44. package/enum/action-log-type.enum.ts +2 -0
  45. package/enum/index.ts +5 -0
  46. package/enum/normalization-decision-action.enum.ts +9 -0
  47. package/enum/normalization-target.enum.ts +8 -0
  48. package/enum/normalization-value-state.enum.ts +9 -0
  49. package/enum/normalized-attribute.enum.ts +6 -0
  50. package/enum/product-image-duplicate-mode.enum.ts +7 -0
  51. package/package.json +1 -1
  52. package/schemas/action-log/action-log-payload.schema.ts +4 -0
  53. package/schemas/action-log/merge-product-images-action-log.schema.ts +9 -0
  54. package/schemas/action-log/normalize-attributes-action-log.schema.ts +21 -0
  55. package/schemas/attribute-normalization/attribute-normalization-scope.schema.ts +31 -0
  56. package/schemas/attribute-normalization/normalization-decision.schema.ts +18 -0
  57. package/schemas/attribute-normalization/normalization-row-count.schema.ts +6 -0
  58. package/schemas/attribute-normalization/normalization-summary.schema.ts +28 -0
  59. package/schemas/attribute-normalization/normalization-value.schema.ts +17 -0
  60. package/schemas/attribute-normalization/product-image-duplicate-forecast.schema.ts +13 -0
  61. package/schemas/index.ts +8 -0
  62. package/schemas/product-image/product-image-merge-candidate.schema.ts +20 -0
  63. package/schemas/product-image/product-image-merge-group.schema.ts +9 -0
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AnalyzeAttributeNormalizationContractQuery = void 0;
4
+ const zod_1 = require("zod");
5
+ const attribute_normalization_scope_schema_1 = require("../../schemas/attribute-normalization/attribute-normalization-scope.schema");
6
+ const normalization_value_schema_1 = require("../../schemas/attribute-normalization/normalization-value.schema");
7
+ const normalization_summary_schema_1 = require("../../schemas/attribute-normalization/normalization-summary.schema");
8
+ const AnalyzeAttributeNormalizationRequestSchema = attribute_normalization_scope_schema_1.AttributeNormalizationScopeSchema;
9
+ const AnalyzeAttributeNormalizationResponseSchema = zod_1.z.object({
10
+ data: zod_1.z.object({
11
+ values: zod_1.z.array(normalization_value_schema_1.NormalizationValueSchema),
12
+ summary: normalization_summary_schema_1.NormalizationSummarySchema,
13
+ }),
14
+ });
15
+ var AnalyzeAttributeNormalizationContractQuery;
16
+ (function (AnalyzeAttributeNormalizationContractQuery) {
17
+ AnalyzeAttributeNormalizationContractQuery.RequestSchema = AnalyzeAttributeNormalizationRequestSchema;
18
+ AnalyzeAttributeNormalizationContractQuery.ResponseSchema = AnalyzeAttributeNormalizationResponseSchema;
19
+ })(AnalyzeAttributeNormalizationContractQuery || (exports.AnalyzeAttributeNormalizationContractQuery = AnalyzeAttributeNormalizationContractQuery = {}));
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ExecuteAttributeNormalizationContractCommand = void 0;
4
+ const zod_1 = require("zod");
5
+ const attribute_normalization_scope_schema_1 = require("../../schemas/attribute-normalization/attribute-normalization-scope.schema");
6
+ const normalization_decision_schema_1 = require("../../schemas/attribute-normalization/normalization-decision.schema");
7
+ const normalization_row_count_schema_1 = require("../../schemas/attribute-normalization/normalization-row-count.schema");
8
+ const ExecuteAttributeNormalizationRequestSchema = attribute_normalization_scope_schema_1.AttributeNormalizationScopeSchema.extend({
9
+ // Верхней границы нет намеренно: анализ отдаёт все написания компании, и применение обязано
10
+ // принять их целиком — иначе прогон пришлось бы дробить, а разбиение на части снова открывает
11
+ // цепочки A→B и B→C, которые валидатор ловит только внутри одного запроса.
12
+ // Объём ограничен размером тела запроса, а стоимость — чанкованием при записи.
13
+ decisions: zod_1.z.array(normalization_decision_schema_1.NormalizationDecisionSchema).min(1),
14
+ });
15
+ const ExecuteAttributeNormalizationResponseSchema = zod_1.z.object({
16
+ data: zod_1.z.object({
17
+ // Фактически изменённых строк по каждой таблице — считается по результату UPDATE,
18
+ // а не по предсказанию анализа: охват мог измениться.
19
+ updatedRows: normalization_row_count_schema_1.NormalizationRowCountSchema,
20
+ appliedDecisions: zod_1.z.number(),
21
+ actionLogUUID: zod_1.z.string().nullable(),
22
+ }),
23
+ });
24
+ var ExecuteAttributeNormalizationContractCommand;
25
+ (function (ExecuteAttributeNormalizationContractCommand) {
26
+ ExecuteAttributeNormalizationContractCommand.RequestSchema = ExecuteAttributeNormalizationRequestSchema;
27
+ ExecuteAttributeNormalizationContractCommand.ResponseSchema = ExecuteAttributeNormalizationResponseSchema;
28
+ })(ExecuteAttributeNormalizationContractCommand || (exports.ExecuteAttributeNormalizationContractCommand = ExecuteAttributeNormalizationContractCommand = {}));
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GetGrowerSuggestionsContractQuery = void 0;
4
+ const zod_1 = require("zod");
5
+ const attribute_normalization_constant_1 = require("../../constant/attribute-normalization.constant");
6
+ // Автокомплит производителя на фронте: union уникальных написаний по товарам, карточкам
7
+ // и позициям коллекций. Справочника Grower нет и мы его не заводим.
8
+ const GetGrowerSuggestionsRequestSchema = zod_1.z.object({
9
+ search: zod_1.z.string().trim().min(1).max(attribute_normalization_constant_1.ATTRIBUTE_NORMALIZATION_LIMITS.MAX_VALUE_LENGTH).optional(),
10
+ count: zod_1.z.coerce
11
+ .number()
12
+ .int()
13
+ .min(1)
14
+ .max(attribute_normalization_constant_1.ATTRIBUTE_NORMALIZATION_LIMITS.MAX_GROWER_SUGGESTIONS)
15
+ .default(attribute_normalization_constant_1.ATTRIBUTE_NORMALIZATION_LIMITS.MAX_GROWER_SUGGESTIONS),
16
+ });
17
+ const GetGrowerSuggestionsResponseSchema = zod_1.z.object({
18
+ data: zod_1.z.object({
19
+ growers: zod_1.z.array(zod_1.z.string()),
20
+ }),
21
+ });
22
+ var GetGrowerSuggestionsContractQuery;
23
+ (function (GetGrowerSuggestionsContractQuery) {
24
+ GetGrowerSuggestionsContractQuery.RequestSchema = GetGrowerSuggestionsRequestSchema;
25
+ GetGrowerSuggestionsContractQuery.ResponseSchema = GetGrowerSuggestionsResponseSchema;
26
+ })(GetGrowerSuggestionsContractQuery || (exports.GetGrowerSuggestionsContractQuery = GetGrowerSuggestionsContractQuery = {}));
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GetGrowerSuggestionsContractQuery = exports.ExecuteAttributeNormalizationContractCommand = exports.AnalyzeAttributeNormalizationContractQuery = void 0;
4
+ var analyze_attribute_normalization_query_1 = require("./analyze-attribute-normalization.query");
5
+ Object.defineProperty(exports, "AnalyzeAttributeNormalizationContractQuery", { enumerable: true, get: function () { return analyze_attribute_normalization_query_1.AnalyzeAttributeNormalizationContractQuery; } });
6
+ var execute_attribute_normalization_command_1 = require("./execute-attribute-normalization.command");
7
+ Object.defineProperty(exports, "ExecuteAttributeNormalizationContractCommand", { enumerable: true, get: function () { return execute_attribute_normalization_command_1.ExecuteAttributeNormalizationContractCommand; } });
8
+ var get_grower_suggestions_query_1 = require("./get-grower-suggestions.query");
9
+ Object.defineProperty(exports, "GetGrowerSuggestionsContractQuery", { enumerable: true, get: function () { return get_grower_suggestions_query_1.GetGrowerSuggestionsContractQuery; } });
@@ -84,3 +84,4 @@ var validate_ws_ticket_s2s_query_1 = require("./session/validate-ws-ticket-s2s.q
84
84
  Object.defineProperty(exports, "ValidateWsTicketS2SContractQuery", { enumerable: true, get: function () { return validate_ws_ticket_s2s_query_1.ValidateWsTicketS2SContractQuery; } });
85
85
  __exportStar(require("./consignment-transfer"), exports);
86
86
  __exportStar(require("./transfer-document"), exports);
87
+ __exportStar(require("./attribute-normalization"), exports);
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AnalyzeProductImageMergeContractQuery = void 0;
4
+ const zod_1 = require("zod");
5
+ const product_image_duplicate_mode_enum_1 = require("../../enum/product-image-duplicate-mode.enum");
6
+ const product_image_merge_group_schema_1 = require("../../schemas/product-image/product-image-merge-group.schema");
7
+ const AnalyzeProductImageMergeRequestSchema = zod_1.z.object({
8
+ mode: zod_1.z.nativeEnum(product_image_duplicate_mode_enum_1.ProductImageDuplicateModeEnum).default(product_image_duplicate_mode_enum_1.ProductImageDuplicateModeEnum.CURRENT),
9
+ });
10
+ const AnalyzeProductImageMergeResponseSchema = zod_1.z.object({
11
+ data: zod_1.z.object({
12
+ groups: zod_1.z.array(product_image_merge_group_schema_1.ProductImageMergeGroupSchema),
13
+ // Живых карточек у компании всего — чтобы фронт показал масштаб.
14
+ totalImages: zod_1.z.number(),
15
+ duplicateImages: zod_1.z.number(),
16
+ }),
17
+ });
18
+ var AnalyzeProductImageMergeContractQuery;
19
+ (function (AnalyzeProductImageMergeContractQuery) {
20
+ AnalyzeProductImageMergeContractQuery.RequestSchema = AnalyzeProductImageMergeRequestSchema;
21
+ AnalyzeProductImageMergeContractQuery.ResponseSchema = AnalyzeProductImageMergeResponseSchema;
22
+ })(AnalyzeProductImageMergeContractQuery || (exports.AnalyzeProductImageMergeContractQuery = AnalyzeProductImageMergeContractQuery = {}));
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ExecuteProductImageMergeContractCommand = void 0;
4
+ const zod_1 = require("zod");
5
+ const product_image_merge_constant_1 = require("../../constant/product-image-merge.constant");
6
+ const ExecuteProductImageMergeRequestSchema = zod_1.z.object({
7
+ primaryUUID: zod_1.z.string().uuid(),
8
+ // Верхняя граница на группу: склейка идёт одной транзакцией, и неограниченный список
9
+ // упёрся бы в её таймаут. Реальные группы дублей — единицы карточек.
10
+ mergedUUIDList: zod_1.z.array(zod_1.z.string().uuid()).min(1).max(product_image_merge_constant_1.PRODUCT_IMAGE_MERGE_LIMITS.MAX_MERGED_PER_REQUEST),
11
+ });
12
+ const ExecuteProductImageMergeResponseSchema = zod_1.z.object({
13
+ data: zod_1.z.object({
14
+ relinkedProducts: zod_1.z.number(),
15
+ relinkedPreOrderCollectionItems: zod_1.z.number(),
16
+ movedAliases: zod_1.z.number(),
17
+ droppedAliases: zod_1.z.number(),
18
+ mergedImages: zod_1.z.number(),
19
+ }),
20
+ });
21
+ var ExecuteProductImageMergeContractCommand;
22
+ (function (ExecuteProductImageMergeContractCommand) {
23
+ ExecuteProductImageMergeContractCommand.RequestSchema = ExecuteProductImageMergeRequestSchema;
24
+ ExecuteProductImageMergeContractCommand.ResponseSchema = ExecuteProductImageMergeResponseSchema;
25
+ })(ExecuteProductImageMergeContractCommand || (exports.ExecuteProductImageMergeContractCommand = ExecuteProductImageMergeContractCommand = {}));
@@ -14,6 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.ExecuteProductImageMergeContractCommand = exports.AnalyzeProductImageMergeContractQuery = void 0;
17
18
  __exportStar(require("./create-product-image.command"), exports);
18
19
  __exportStar(require("./update-product-image.command"), exports);
19
20
  __exportStar(require("./delete-product-image.command"), exports);
@@ -30,3 +31,7 @@ __exportStar(require("./alias/update-product-image-alias.command"), exports);
30
31
  __exportStar(require("./alias/delete-product-image-alias.command"), exports);
31
32
  __exportStar(require("./alias/bulk-create-product-image-alias.command"), exports);
32
33
  __exportStar(require("./alias/bulk-create-product-image-alias-from-pre-order-collection-item.command"), exports);
34
+ var analyze_product_image_merge_query_1 = require("./analyze-product-image-merge.query");
35
+ Object.defineProperty(exports, "AnalyzeProductImageMergeContractQuery", { enumerable: true, get: function () { return analyze_product_image_merge_query_1.AnalyzeProductImageMergeContractQuery; } });
36
+ var execute_product_image_merge_command_1 = require("./execute-product-image-merge.command");
37
+ Object.defineProperty(exports, "ExecuteProductImageMergeContractCommand", { enumerable: true, get: function () { return execute_product_image_merge_command_1.ExecuteProductImageMergeContractCommand; } });
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ATTRIBUTE_NORMALIZATION_LIMITS = void 0;
4
+ exports.ATTRIBUTE_NORMALIZATION_LIMITS = {
5
+ // Размер чанка для UPDATE ... WHERE поле IN (...).
6
+ UPDATE_CHUNK_SIZE: 200,
7
+ // Сколько уникальных написаний производителя отдавать в автокомплит.
8
+ MAX_GROWER_SUGGESTIONS: 500,
9
+ // Колонки атрибутов — text без ограничения; без границы одно решение раскатало бы
10
+ // многомегабайтную строку по всей истории компании и осело бы в журнале действий.
11
+ MAX_VALUE_LENGTH: 255,
12
+ };
@@ -1511,4 +1511,54 @@ exports.ERRORS = {
1511
1511
  message: 'Transfer document number start cannot be decreased',
1512
1512
  httpCode: 400,
1513
1513
  },
1514
+ ATTRIBUTE_NORMALIZATION_ANALYZE_FAILED: {
1515
+ code: 'ANORM001',
1516
+ message: 'Error when analyzing attribute normalization',
1517
+ httpCode: 500,
1518
+ },
1519
+ ATTRIBUTE_NORMALIZATION_EXECUTE_FAILED: {
1520
+ code: 'ANORM002',
1521
+ message: 'Error when applying attribute normalization',
1522
+ httpCode: 500,
1523
+ },
1524
+ ATTRIBUTE_NORMALIZATION_EMPTY_SCOPE: {
1525
+ code: 'ANORM003',
1526
+ message: 'None of the selected targets stores the selected attributes',
1527
+ httpCode: 400,
1528
+ },
1529
+ PRODUCT_IMAGE_MERGE_TOO_MANY_IMAGES: {
1530
+ code: 'PI015',
1531
+ message: 'Too many product images to analyze duplicates in one pass',
1532
+ httpCode: 400,
1533
+ },
1534
+ ATTRIBUTE_NORMALIZATION_INVALID_DECISIONS: {
1535
+ code: 'ANORM006',
1536
+ message: 'Normalization decisions are inconsistent',
1537
+ httpCode: 400,
1538
+ },
1539
+ CATALOG_BULK_REWRITE_LOCK_UNAVAILABLE: {
1540
+ code: 'ANORM007',
1541
+ message: 'Cannot guarantee exclusive execution right now, try again later',
1542
+ httpCode: 503,
1543
+ },
1544
+ ATTRIBUTE_NORMALIZATION_ALREADY_RUNNING: {
1545
+ code: 'ANORM005',
1546
+ message: 'Another attribute normalization run is already in progress for this company',
1547
+ httpCode: 409,
1548
+ },
1549
+ PRODUCT_IMAGE_MERGE_NOT_DUPLICATES: {
1550
+ code: 'PI017',
1551
+ message: 'Selected product images are not duplicates of the primary one',
1552
+ httpCode: 409,
1553
+ },
1554
+ PRODUCT_IMAGE_MERGE_ALREADY_RUNNING: {
1555
+ code: 'PI016',
1556
+ message: 'Another product image merge is already in progress for this company',
1557
+ httpCode: 409,
1558
+ },
1559
+ GROWER_SUGGESTIONS_FETCH_FAILED: {
1560
+ code: 'ANORM004',
1561
+ message: 'Error when retrieving grower suggestions',
1562
+ httpCode: 500,
1563
+ },
1514
1564
  };
@@ -26,3 +26,5 @@ __exportStar(require("./okei"), exports);
26
26
  __exportStar(require("./contact-phone.constant"), exports);
27
27
  __exportStar(require("./settings.constant"), exports);
28
28
  __exportStar(require("./consignment-product-enrichment.constant"), exports);
29
+ __exportStar(require("./attribute-normalization.constant"), exports);
30
+ __exportStar(require("./product-image-merge.constant"), exports);
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PRODUCT_IMAGE_MERGE_LIMITS = void 0;
4
+ exports.PRODUCT_IMAGE_MERGE_LIMITS = {
5
+ // Поиск дублей группирует карточки в приложении, а не в SQL: канон цвета живёт в словаре
6
+ // на стороне кода. Значит все карточки компании оказываются в памяти, и объём надо ограничить
7
+ // явно — иначе на очень крупной компании анализ молча съест память процесса.
8
+ MAX_ANALYZED_IMAGES: 100000,
9
+ // Сколько карточек можно поглотить за один запрос: всё идёт одной транзакцией.
10
+ MAX_MERGED_PER_REQUEST: 200,
11
+ };
@@ -23,4 +23,6 @@ var ActionLogTypeEnum;
23
23
  ActionLogTypeEnum["DELETE_CONSIGNMENT_ITEMS"] = "DELETE_CONSIGNMENT_ITEMS";
24
24
  ActionLogTypeEnum["GROUP_UPDATE_CONSIGNMENT_ITEMS"] = "GROUP_UPDATE_CONSIGNMENT_ITEMS";
25
25
  ActionLogTypeEnum["TRANSFER_CONSIGNMENT_ITEMS"] = "TRANSFER_CONSIGNMENT_ITEMS";
26
+ ActionLogTypeEnum["NORMALIZE_ATTRIBUTES"] = "NORMALIZE_ATTRIBUTES";
27
+ ActionLogTypeEnum["MERGE_PRODUCT_IMAGES"] = "MERGE_PRODUCT_IMAGES";
26
28
  })(ActionLogTypeEnum || (exports.ActionLogTypeEnum = ActionLogTypeEnum = {}));
@@ -124,3 +124,8 @@ __exportStar(require("./transfer-document-item-kind.enum"), exports);
124
124
  __exportStar(require("./write-off-report-date-basis.enum"), exports);
125
125
  __exportStar(require("./product-enrichment-field.enum"), exports);
126
126
  __exportStar(require("./product-enrichment-field-state.enum"), exports);
127
+ __exportStar(require("./normalized-attribute.enum"), exports);
128
+ __exportStar(require("./normalization-target.enum"), exports);
129
+ __exportStar(require("./normalization-value-state.enum"), exports);
130
+ __exportStar(require("./normalization-decision-action.enum"), exports);
131
+ __exportStar(require("./product-image-duplicate-mode.enum"), exports);
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NormalizationDecisionActionEnum = void 0;
4
+ // Решение человека по одному написанию. Правила в БД не сохраняются — решение живёт один прогон.
5
+ var NormalizationDecisionActionEnum;
6
+ (function (NormalizationDecisionActionEnum) {
7
+ // Переписать на указанное значение.
8
+ NormalizationDecisionActionEnum["MAP"] = "MAP";
9
+ // Очистить поле (значение — мусор).
10
+ NormalizationDecisionActionEnum["CLEAR"] = "CLEAR";
11
+ // Оставить как есть.
12
+ NormalizationDecisionActionEnum["KEEP"] = "KEEP";
13
+ })(NormalizationDecisionActionEnum || (exports.NormalizationDecisionActionEnum = NormalizationDecisionActionEnum = {}));
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NormalizationTargetEnum = void 0;
4
+ // Таблицы, попадающие в охват нормализации. PRODUCT_CONFIGURATION сужается только компанией:
5
+ // правила кратности нормализуются целиком, иначе они разъедутся с товарами.
6
+ var NormalizationTargetEnum;
7
+ (function (NormalizationTargetEnum) {
8
+ NormalizationTargetEnum["PRODUCT"] = "PRODUCT";
9
+ NormalizationTargetEnum["PRODUCT_IMAGE"] = "PRODUCT_IMAGE";
10
+ NormalizationTargetEnum["PRE_ORDER_COLLECTION_ITEM"] = "PRE_ORDER_COLLECTION_ITEM";
11
+ NormalizationTargetEnum["PRODUCT_CONFIGURATION"] = "PRODUCT_CONFIGURATION";
12
+ })(NormalizationTargetEnum || (exports.NormalizationTargetEnum = NormalizationTargetEnum = {}));
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NormalizationValueStateEnum = void 0;
4
+ // Что анализ знает про конкретное написание атрибута.
5
+ var NormalizationValueStateEnum;
6
+ (function (NormalizationValueStateEnum) {
7
+ // Словарь (или кластеризация для производителя) предложил другое написание.
8
+ NormalizationValueStateEnum["SUGGESTED"] = "SUGGESTED";
9
+ // Значение уже каноничное, менять нечего.
10
+ NormalizationValueStateEnum["CANONICAL"] = "CANONICAL";
11
+ // Словарь значение не узнал — решение принимает человек в рамках прогона.
12
+ NormalizationValueStateEnum["UNRECOGNIZED"] = "UNRECOGNIZED";
13
+ })(NormalizationValueStateEnum || (exports.NormalizationValueStateEnum = NormalizationValueStateEnum = {}));
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NormalizedAttributeEnum = void 0;
4
+ // Справочные атрибуты, которые умеет приводить к единому написанию операция нормализации.
5
+ var NormalizedAttributeEnum;
6
+ (function (NormalizedAttributeEnum) {
7
+ NormalizedAttributeEnum["COLOR"] = "COLOR";
8
+ NormalizedAttributeEnum["COUNTRY"] = "COUNTRY";
9
+ NormalizedAttributeEnum["GROWER"] = "GROWER";
10
+ })(NormalizedAttributeEnum || (exports.NormalizedAttributeEnum = NormalizedAttributeEnum = {}));
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ProductImageDuplicateModeEnum = void 0;
4
+ // В каком виде искать дубли карточек.
5
+ var ProductImageDuplicateModeEnum;
6
+ (function (ProductImageDuplicateModeEnum) {
7
+ // По значениям как они лежат сейчас.
8
+ ProductImageDuplicateModeEnum["CURRENT"] = "CURRENT";
9
+ // По значениям, приведённым к канону — превью того, что слипнется после нормализации.
10
+ ProductImageDuplicateModeEnum["AFTER_NORMALIZATION"] = "AFTER_NORMALIZATION";
11
+ })(ProductImageDuplicateModeEnum || (exports.ProductImageDuplicateModeEnum = ProductImageDuplicateModeEnum = {}));
@@ -20,6 +20,8 @@ const consignment_items_create_action_log_schema_1 = require("./consignment-item
20
20
  const consignment_items_delete_action_log_schema_1 = require("./consignment-items-delete-action-log.schema");
21
21
  const group_update_consignment_items_action_log_schema_1 = require("./group-update-consignment-items-action-log.schema");
22
22
  const transfer_consignment_items_action_log_schema_1 = require("./transfer-consignment-items-action-log.schema");
23
+ const normalize_attributes_action_log_schema_1 = require("./normalize-attributes-action-log.schema");
24
+ const merge_product_images_action_log_schema_1 = require("./merge-product-images-action-log.schema");
23
25
  exports.ActionLogPayloadSchema = zod_1.z.union([
24
26
  order_delete_action_log_payload_schema_1.OrderDeleteActionLogPayloadSchema,
25
27
  order_merge_action_log_schema_1.OrderMergeActionLogSchema,
@@ -39,5 +41,7 @@ exports.ActionLogPayloadSchema = zod_1.z.union([
39
41
  consignment_items_delete_action_log_schema_1.ConsignmentItemsDeleteActionLogSchema,
40
42
  group_update_consignment_items_action_log_schema_1.GroupUpdateConsignmentItemsActionLogSchema,
41
43
  transfer_consignment_items_action_log_schema_1.TransferConsignmentItemsActionLogSchema,
44
+ normalize_attributes_action_log_schema_1.NormalizeAttributesActionLogSchema,
45
+ merge_product_images_action_log_schema_1.MergeProductImagesActionLogSchema,
42
46
  zod_1.z.null(),
43
47
  ]);
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MergeProductImagesActionLogSchema = void 0;
4
+ const zod_1 = require("zod");
5
+ exports.MergeProductImagesActionLogSchema = zod_1.z.object({
6
+ primaryImageUUID: zod_1.z.string(),
7
+ mergedImageUUIDList: zod_1.z.array(zod_1.z.string()),
8
+ relinkedProducts: zod_1.z.number(),
9
+ relinkedPreOrderCollectionItems: zod_1.z.number(),
10
+ movedAliases: zod_1.z.number(),
11
+ });
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NormalizeAttributesActionLogSchema = void 0;
4
+ const zod_1 = require("zod");
5
+ const normalized_attribute_enum_1 = require("../../enum/normalized-attribute.enum");
6
+ const normalization_target_enum_1 = require("../../enum/normalization-target.enum");
7
+ const normalization_decision_action_enum_1 = require("../../enum/normalization-decision-action.enum");
8
+ // Одна агрегатная запись на прогон: построчный лог тут не нужен, операция работает
9
+ // на уникальных значениях, а не на строках.
10
+ exports.NormalizeAttributesActionLogSchema = zod_1.z.object({
11
+ attributes: zod_1.z.array(zod_1.z.nativeEnum(normalized_attribute_enum_1.NormalizedAttributeEnum)),
12
+ targets: zod_1.z.array(zod_1.z.nativeEnum(normalization_target_enum_1.NormalizationTargetEnum)),
13
+ decisions: zod_1.z.array(zod_1.z.object({
14
+ attribute: zod_1.z.nativeEnum(normalized_attribute_enum_1.NormalizedAttributeEnum),
15
+ raw: zod_1.z.string(),
16
+ action: zod_1.z.nativeEnum(normalization_decision_action_enum_1.NormalizationDecisionActionEnum),
17
+ value: zod_1.z.string().nullable(),
18
+ })),
19
+ updatedRows: zod_1.z.partialRecord(zod_1.z.nativeEnum(normalization_target_enum_1.NormalizationTargetEnum), zod_1.z.number()),
20
+ totalUpdatedRows: zod_1.z.number(),
21
+ });
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AttributeNormalizationScopeSchema = void 0;
4
+ const zod_1 = require("zod");
5
+ const normalized_attribute_enum_1 = require("../../enum/normalized-attribute.enum");
6
+ const normalization_target_enum_1 = require("../../enum/normalization-target.enum");
7
+ /**
8
+ * Охват прогона. Общий для анализа и применения: execute перепроверяет его заново, потому что
9
+ * между анализом и применением в таблицы могли приехать новые строки.
10
+ *
11
+ * Ограничения охвата, которые нельзя выразить схемой и которые проверяет сервис:
12
+ * - consignmentUUIDList сужает только PRODUCT — у карточек и позиций коллекций нет поставки;
13
+ * - preOrderCollectionUUIDList сужает только PRE_ORDER_COLLECTION_ITEM;
14
+ * - PRODUCT_CONFIGURATION не сужается ничем, кроме компании: правила нормализуются целиком,
15
+ * иначе часть из них разъедется с товарами.
16
+ */
17
+ // Дедупликация в схеме, а не в сервисе: списки читают и analyze, и execute, и запись в журнал —
18
+ // повтор «PRODUCT» тысячу раз означал бы тысячу полных проходов по таблице товаров.
19
+ const dedupe = (list) => Array.from(new Set(list));
20
+ exports.AttributeNormalizationScopeSchema = zod_1.z.object({
21
+ attributes: zod_1.z.array(zod_1.z.nativeEnum(normalized_attribute_enum_1.NormalizedAttributeEnum)).min(1).max(Object.keys(normalized_attribute_enum_1.NormalizedAttributeEnum).length).transform(dedupe),
22
+ targets: zod_1.z.array(zod_1.z.nativeEnum(normalization_target_enum_1.NormalizationTargetEnum)).min(1).max(Object.keys(normalization_target_enum_1.NormalizationTargetEnum).length).transform(dedupe),
23
+ // Без верхней границы: списки уходят в запрос одним параметром-массивом, длина упирается
24
+ // только в размер тела запроса.
25
+ consignmentUUIDList: zod_1.z.array(zod_1.z.string().uuid()).optional(),
26
+ preOrderCollectionUUIDList: zod_1.z.array(zod_1.z.string().uuid()).optional(),
27
+ createdFrom: zod_1.z.coerce.date().optional(),
28
+ createdTo: zod_1.z.coerce.date().optional(),
29
+ // У поставки нет признака закрытости — закрытость живёт на товаре (Product.isOpen).
30
+ // По умолчанию закрытые товары не трогаем: это распроданная история.
31
+ isIncludingClosedProducts: zod_1.z.boolean().default(false),
32
+ });
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NormalizationDecisionSchema = void 0;
4
+ const zod_1 = require("zod");
5
+ const normalized_attribute_enum_1 = require("../../enum/normalized-attribute.enum");
6
+ const normalization_decision_action_enum_1 = require("../../enum/normalization-decision-action.enum");
7
+ const attribute_normalization_constant_1 = require("../../constant/attribute-normalization.constant");
8
+ // Решение человека по одному написанию. Правила в БД не сохраняются — решение живёт один прогон.
9
+ exports.NormalizationDecisionSchema = zod_1.z
10
+ .object({
11
+ attribute: zod_1.z.nativeEnum(normalized_attribute_enum_1.NormalizedAttributeEnum),
12
+ raw: zod_1.z.string().min(1).max(attribute_normalization_constant_1.ATTRIBUTE_NORMALIZATION_LIMITS.MAX_VALUE_LENGTH),
13
+ action: zod_1.z.nativeEnum(normalization_decision_action_enum_1.NormalizationDecisionActionEnum),
14
+ // Целевое написание — обязательно и только для MAP.
15
+ value: zod_1.z.string().min(1).max(attribute_normalization_constant_1.ATTRIBUTE_NORMALIZATION_LIMITS.MAX_VALUE_LENGTH).optional(),
16
+ })
17
+ .refine(decision => (decision.action === normalization_decision_action_enum_1.NormalizationDecisionActionEnum.MAP) === (decision.value !== undefined), {
18
+ message: 'value is required for MAP and forbidden otherwise',
19
+ path: ['value'],
20
+ });
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NormalizationRowCountSchema = void 0;
4
+ const zod_1 = require("zod");
5
+ const normalization_target_enum_1 = require("../../enum/normalization-target.enum");
6
+ // Сколько строк приходится на каждую таблицу охвата. Ключи неполные: значение живёт
7
+ // не во всех таблицах сразу, а таблица может не хранить нужный атрибут.
8
+ exports.NormalizationRowCountSchema = zod_1.z.partialRecord(zod_1.z.nativeEnum(normalization_target_enum_1.NormalizationTargetEnum), zod_1.z.number());
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NormalizationSummarySchema = void 0;
4
+ const zod_1 = require("zod");
5
+ const normalized_attribute_enum_1 = require("../../enum/normalized-attribute.enum");
6
+ const normalization_target_enum_1 = require("../../enum/normalization-target.enum");
7
+ const normalization_row_count_schema_1 = require("./normalization-row-count.schema");
8
+ const product_image_duplicate_forecast_schema_1 = require("./product-image-duplicate-forecast.schema");
9
+ // Итоги анализа: всё, что фронт показывает перед запуском необратимой операции.
10
+ exports.NormalizationSummarySchema = zod_1.z.object({
11
+ // Уникальных написаний по каждому атрибуту.
12
+ uniqueValues: zod_1.z.partialRecord(zod_1.z.nativeEnum(normalized_attribute_enum_1.NormalizedAttributeEnum), zod_1.z.number()),
13
+ // Строк к изменению по каждой таблице, если принять все предложения.
14
+ affectedRows: normalization_row_count_schema_1.NormalizationRowCountSchema,
15
+ // Строк, которые прогон пропустит из-за того, что позиция закрыта. Нужно фронту, чтобы
16
+ // предложить включить их флагом: по умолчанию закрытая история не трогается.
17
+ // null означает «пропускать нечего»: либо флаг уже включён, либо ни одна таблица охвата
18
+ // не различает открытые и закрытые позиции.
19
+ skippedClosedRows: normalization_row_count_schema_1.NormalizationRowCountSchema.nullable(),
20
+ // Строк заказов, отображающих затрагиваемые товары. Нужно фронту для предупреждения:
21
+ // операция необратима, а в заказах видно старое написание.
22
+ affectedOrderProductRows: zod_1.z.number(),
23
+ // Таблицы, которых не касается сужение по выбранным поставкам и коллекциям: у карточки
24
+ // номенклатуры нет поставки, у товара нет коллекции. Для них охват остаётся широким —
25
+ // фронт обязан предупредить, иначе админ будет думать, что тронет одну поставку,
26
+ // а прогон перепишет всю компанию.
27
+ targetsIgnoringUuidScope: zod_1.z.array(zod_1.z.nativeEnum(normalization_target_enum_1.NormalizationTargetEnum)),
28
+ // null означает, что карточки в охват не входят и прогон их не трогает.
29
+ productImageDuplicatesAfterRun: product_image_duplicate_forecast_schema_1.ProductImageDuplicateForecastSchema.nullable(),
30
+ });
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NormalizationValueSchema = void 0;
4
+ const zod_1 = require("zod");
5
+ const normalized_attribute_enum_1 = require("../../enum/normalized-attribute.enum");
6
+ const normalization_value_state_enum_1 = require("../../enum/normalization-value-state.enum");
7
+ const normalization_row_count_schema_1 = require("./normalization-row-count.schema");
8
+ // Одно написание атрибута в истории компании и то, что операция про него знает.
9
+ exports.NormalizationValueSchema = zod_1.z.object({
10
+ attribute: zod_1.z.nativeEnum(normalized_attribute_enum_1.NormalizedAttributeEnum),
11
+ // Написание как оно лежит в базе — оно же ключ решения в execute.
12
+ raw: zod_1.z.string(),
13
+ // Что предлагает словарь (для производителя — кластеризация). null, если предложить нечего.
14
+ suggested: zod_1.z.string().nullable(),
15
+ state: zod_1.z.nativeEnum(normalization_value_state_enum_1.NormalizationValueStateEnum),
16
+ rowCount: normalization_row_count_schema_1.NormalizationRowCountSchema,
17
+ // Другие написания того же кластера — только для производителя, у цвета и страны словарь.
18
+ clusterRaw: zod_1.z.array(zod_1.z.string()),
19
+ });
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ProductImageDuplicateForecastSchema = void 0;
4
+ const zod_1 = require("zod");
5
+ // Прогноз по дублям карточек номенклатуры после прогона. Уникального ключа по идентичности
6
+ // у карточек нет, поэтому прогон может сделать две карточки неразличимыми, и резолвер начнёт
7
+ // выбирать между ними произвольно. Апдейт при этом не упадёт — предупредить можно только заранее.
8
+ exports.ProductImageDuplicateForecastSchema = zod_1.z.object({
9
+ // false — посчитать не удалось (карточек больше предела). Это НЕ то же самое,
10
+ // что «дублей не будет»: при false числа ниже не значат ничего.
11
+ isCounted: zod_1.z.boolean(),
12
+ total: zod_1.z.number(),
13
+ // Из них те, что сегодня ещё различимы: именно их создаст этот прогон.
14
+ new: zod_1.z.number(),
15
+ });
@@ -100,3 +100,11 @@ __exportStar(require("./positive-money.schema"), exports);
100
100
  __exportStar(require("./report/report-order-filter.schema"), exports);
101
101
  __exportStar(require("./transfer-document/transfer-document.schema"), exports);
102
102
  __exportStar(require("./transfer-document/transfer-document-detailed.schema"), exports);
103
+ __exportStar(require("./attribute-normalization/attribute-normalization-scope.schema"), exports);
104
+ __exportStar(require("./product-image/product-image-merge-candidate.schema"), exports);
105
+ __exportStar(require("./product-image/product-image-merge-group.schema"), exports);
106
+ __exportStar(require("./attribute-normalization/normalization-row-count.schema"), exports);
107
+ __exportStar(require("./attribute-normalization/normalization-value.schema"), exports);
108
+ __exportStar(require("./attribute-normalization/normalization-summary.schema"), exports);
109
+ __exportStar(require("./attribute-normalization/normalization-decision.schema"), exports);
110
+ __exportStar(require("./attribute-normalization/product-image-duplicate-forecast.schema"), exports);
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ProductImageMergeCandidateSchema = void 0;
4
+ const zod_1 = require("zod");
5
+ // Карточка — участник группы дублей, с числом ссылок на неё.
6
+ exports.ProductImageMergeCandidateSchema = zod_1.z.object({
7
+ uuid: zod_1.z.string(),
8
+ name: zod_1.z.string(),
9
+ // Ключ файла в хранилище: без него нечем показать превью фото группы, а основную
10
+ // карточку админ выбирает в том числе по картинке.
11
+ productImageUrl: zod_1.z.string(),
12
+ categoryUUID: zod_1.z.string().nullable(),
13
+ grower: zod_1.z.string().nullable(),
14
+ country: zod_1.z.string().nullable(),
15
+ color: zod_1.z.string().nullable(),
16
+ productCount: zod_1.z.number(),
17
+ preOrderCollectionItemCount: zod_1.z.number(),
18
+ aliasCount: zod_1.z.number(),
19
+ // Карточка, которую операция предлагает оставить: больше всего ссылок, при равенстве —
20
+ // меньший uuid, чтобы повторный анализ давал тот же ответ.
21
+ isPrimary: zod_1.z.boolean(),
22
+ });
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ProductImageMergeGroupSchema = void 0;
4
+ const zod_1 = require("zod");
5
+ const product_image_merge_candidate_schema_1 = require("./product-image-merge-candidate.schema");
6
+ // Группа карточек, неразличимых по идентичности.
7
+ exports.ProductImageMergeGroupSchema = zod_1.z.object({
8
+ // Ключ группы — нормализованная идентичность, по которой карточки признаны одинаковыми.
9
+ key: zod_1.z.string(),
10
+ candidates: zod_1.z.array(product_image_merge_candidate_schema_1.ProductImageMergeCandidateSchema),
11
+ });
@@ -0,0 +1,24 @@
1
+ import { z } from 'zod';
2
+ import { AttributeNormalizationScopeSchema } from '../../schemas/attribute-normalization/attribute-normalization-scope.schema';
3
+ import { NormalizationValueSchema } from '../../schemas/attribute-normalization/normalization-value.schema';
4
+ import { NormalizationSummarySchema } from '../../schemas/attribute-normalization/normalization-summary.schema';
5
+
6
+ const AnalyzeAttributeNormalizationRequestSchema = AttributeNormalizationScopeSchema;
7
+
8
+ const AnalyzeAttributeNormalizationResponseSchema = z.object({
9
+ data: z.object({
10
+ values: z.array(NormalizationValueSchema),
11
+ summary: NormalizationSummarySchema,
12
+ }),
13
+ });
14
+
15
+ export namespace AnalyzeAttributeNormalizationContractQuery {
16
+ export const RequestSchema = AnalyzeAttributeNormalizationRequestSchema;
17
+ export type Request = z.infer<typeof RequestSchema>;
18
+
19
+ export const ResponseSchema = AnalyzeAttributeNormalizationResponseSchema;
20
+ export type Response = z.infer<typeof ResponseSchema>;
21
+
22
+ export type Value = z.infer<typeof NormalizationValueSchema>;
23
+ export type Summary = z.infer<typeof NormalizationSummarySchema>;
24
+ }
@@ -0,0 +1,32 @@
1
+ import { z } from 'zod';
2
+ import { AttributeNormalizationScopeSchema } from '../../schemas/attribute-normalization/attribute-normalization-scope.schema';
3
+ import { NormalizationDecisionSchema } from '../../schemas/attribute-normalization/normalization-decision.schema';
4
+ import { NormalizationRowCountSchema } from '../../schemas/attribute-normalization/normalization-row-count.schema';
5
+
6
+ const ExecuteAttributeNormalizationRequestSchema = AttributeNormalizationScopeSchema.extend({
7
+ // Верхней границы нет намеренно: анализ отдаёт все написания компании, и применение обязано
8
+ // принять их целиком — иначе прогон пришлось бы дробить, а разбиение на части снова открывает
9
+ // цепочки A→B и B→C, которые валидатор ловит только внутри одного запроса.
10
+ // Объём ограничен размером тела запроса, а стоимость — чанкованием при записи.
11
+ decisions: z.array(NormalizationDecisionSchema).min(1),
12
+ });
13
+
14
+ const ExecuteAttributeNormalizationResponseSchema = z.object({
15
+ data: z.object({
16
+ // Фактически изменённых строк по каждой таблице — считается по результату UPDATE,
17
+ // а не по предсказанию анализа: охват мог измениться.
18
+ updatedRows: NormalizationRowCountSchema,
19
+ appliedDecisions: z.number(),
20
+ actionLogUUID: z.string().nullable(),
21
+ }),
22
+ });
23
+
24
+ export namespace ExecuteAttributeNormalizationContractCommand {
25
+ export const RequestSchema = ExecuteAttributeNormalizationRequestSchema;
26
+ export type Request = z.infer<typeof RequestSchema>;
27
+
28
+ export const ResponseSchema = ExecuteAttributeNormalizationResponseSchema;
29
+ export type Response = z.infer<typeof ResponseSchema>;
30
+
31
+ export type Decision = z.infer<typeof NormalizationDecisionSchema>;
32
+ }
@@ -0,0 +1,28 @@
1
+ import { z } from 'zod';
2
+ import { ATTRIBUTE_NORMALIZATION_LIMITS } from '../../constant/attribute-normalization.constant';
3
+
4
+ // Автокомплит производителя на фронте: union уникальных написаний по товарам, карточкам
5
+ // и позициям коллекций. Справочника Grower нет и мы его не заводим.
6
+ const GetGrowerSuggestionsRequestSchema = z.object({
7
+ search: z.string().trim().min(1).max(ATTRIBUTE_NORMALIZATION_LIMITS.MAX_VALUE_LENGTH).optional(),
8
+ count: z.coerce
9
+ .number()
10
+ .int()
11
+ .min(1)
12
+ .max(ATTRIBUTE_NORMALIZATION_LIMITS.MAX_GROWER_SUGGESTIONS)
13
+ .default(ATTRIBUTE_NORMALIZATION_LIMITS.MAX_GROWER_SUGGESTIONS),
14
+ });
15
+
16
+ const GetGrowerSuggestionsResponseSchema = z.object({
17
+ data: z.object({
18
+ growers: z.array(z.string()),
19
+ }),
20
+ });
21
+
22
+ export namespace GetGrowerSuggestionsContractQuery {
23
+ export const RequestSchema = GetGrowerSuggestionsRequestSchema;
24
+ export type Request = z.infer<typeof RequestSchema>;
25
+
26
+ export const ResponseSchema = GetGrowerSuggestionsResponseSchema;
27
+ export type Response = z.infer<typeof ResponseSchema>;
28
+ }
@@ -0,0 +1,3 @@
1
+ export { AnalyzeAttributeNormalizationContractQuery } from './analyze-attribute-normalization.query';
2
+ export { ExecuteAttributeNormalizationContractCommand } from './execute-attribute-normalization.command';
3
+ export { GetGrowerSuggestionsContractQuery } from './get-grower-suggestions.query';
package/commands/index.ts CHANGED
@@ -61,3 +61,4 @@ export { CreateWsTicketContractCommand } from './session/create-ws-ticket.comman
61
61
  export { ValidateWsTicketS2SContractQuery } from './session/validate-ws-ticket-s2s.query';
62
62
  export * from './consignment-transfer';
63
63
  export * from './transfer-document';
64
+ export * from './attribute-normalization';
@@ -0,0 +1,28 @@
1
+ import { z } from 'zod';
2
+ import { ProductImageDuplicateModeEnum } from '../../enum/product-image-duplicate-mode.enum';
3
+ import { ProductImageMergeGroupSchema } from '../../schemas/product-image/product-image-merge-group.schema';
4
+ import { ProductImageMergeCandidateSchema } from '../../schemas/product-image/product-image-merge-candidate.schema';
5
+
6
+ const AnalyzeProductImageMergeRequestSchema = z.object({
7
+ mode: z.nativeEnum(ProductImageDuplicateModeEnum).default(ProductImageDuplicateModeEnum.CURRENT),
8
+ });
9
+
10
+ const AnalyzeProductImageMergeResponseSchema = z.object({
11
+ data: z.object({
12
+ groups: z.array(ProductImageMergeGroupSchema),
13
+ // Живых карточек у компании всего — чтобы фронт показал масштаб.
14
+ totalImages: z.number(),
15
+ duplicateImages: z.number(),
16
+ }),
17
+ });
18
+
19
+ export namespace AnalyzeProductImageMergeContractQuery {
20
+ export const RequestSchema = AnalyzeProductImageMergeRequestSchema;
21
+ export type Request = z.infer<typeof RequestSchema>;
22
+
23
+ export const ResponseSchema = AnalyzeProductImageMergeResponseSchema;
24
+ export type Response = z.infer<typeof ResponseSchema>;
25
+
26
+ export type Group = z.infer<typeof ProductImageMergeGroupSchema>;
27
+ export type Candidate = z.infer<typeof ProductImageMergeCandidateSchema>;
28
+ }
@@ -0,0 +1,27 @@
1
+ import { z } from 'zod';
2
+ import { PRODUCT_IMAGE_MERGE_LIMITS } from '../../constant/product-image-merge.constant';
3
+
4
+ const ExecuteProductImageMergeRequestSchema = z.object({
5
+ primaryUUID: z.string().uuid(),
6
+ // Верхняя граница на группу: склейка идёт одной транзакцией, и неограниченный список
7
+ // упёрся бы в её таймаут. Реальные группы дублей — единицы карточек.
8
+ mergedUUIDList: z.array(z.string().uuid()).min(1).max(PRODUCT_IMAGE_MERGE_LIMITS.MAX_MERGED_PER_REQUEST),
9
+ });
10
+
11
+ const ExecuteProductImageMergeResponseSchema = z.object({
12
+ data: z.object({
13
+ relinkedProducts: z.number(),
14
+ relinkedPreOrderCollectionItems: z.number(),
15
+ movedAliases: z.number(),
16
+ droppedAliases: z.number(),
17
+ mergedImages: z.number(),
18
+ }),
19
+ });
20
+
21
+ export namespace ExecuteProductImageMergeContractCommand {
22
+ export const RequestSchema = ExecuteProductImageMergeRequestSchema;
23
+ export type Request = z.infer<typeof RequestSchema>;
24
+
25
+ export const ResponseSchema = ExecuteProductImageMergeResponseSchema;
26
+ export type Response = z.infer<typeof ResponseSchema>;
27
+ }
@@ -14,3 +14,5 @@ export * from './alias/update-product-image-alias.command';
14
14
  export * from './alias/delete-product-image-alias.command';
15
15
  export * from './alias/bulk-create-product-image-alias.command';
16
16
  export * from './alias/bulk-create-product-image-alias-from-pre-order-collection-item.command';
17
+ export { AnalyzeProductImageMergeContractQuery } from './analyze-product-image-merge.query';
18
+ export { ExecuteProductImageMergeContractCommand } from './execute-product-image-merge.command';
@@ -0,0 +1,9 @@
1
+ export const ATTRIBUTE_NORMALIZATION_LIMITS = {
2
+ // Размер чанка для UPDATE ... WHERE поле IN (...).
3
+ UPDATE_CHUNK_SIZE: 200,
4
+ // Сколько уникальных написаний производителя отдавать в автокомплит.
5
+ MAX_GROWER_SUGGESTIONS: 500,
6
+ // Колонки атрибутов — text без ограничения; без границы одно решение раскатало бы
7
+ // многомегабайтную строку по всей истории компании и осело бы в журнале действий.
8
+ MAX_VALUE_LENGTH: 255,
9
+ } as const;
package/constant/error.ts CHANGED
@@ -1579,4 +1579,54 @@ export const ERRORS = {
1579
1579
  message: 'Transfer document number start cannot be decreased',
1580
1580
  httpCode: 400,
1581
1581
  },
1582
+ ATTRIBUTE_NORMALIZATION_ANALYZE_FAILED: {
1583
+ code: 'ANORM001',
1584
+ message: 'Error when analyzing attribute normalization',
1585
+ httpCode: 500,
1586
+ },
1587
+ ATTRIBUTE_NORMALIZATION_EXECUTE_FAILED: {
1588
+ code: 'ANORM002',
1589
+ message: 'Error when applying attribute normalization',
1590
+ httpCode: 500,
1591
+ },
1592
+ ATTRIBUTE_NORMALIZATION_EMPTY_SCOPE: {
1593
+ code: 'ANORM003',
1594
+ message: 'None of the selected targets stores the selected attributes',
1595
+ httpCode: 400,
1596
+ },
1597
+ PRODUCT_IMAGE_MERGE_TOO_MANY_IMAGES: {
1598
+ code: 'PI015',
1599
+ message: 'Too many product images to analyze duplicates in one pass',
1600
+ httpCode: 400,
1601
+ },
1602
+ ATTRIBUTE_NORMALIZATION_INVALID_DECISIONS: {
1603
+ code: 'ANORM006',
1604
+ message: 'Normalization decisions are inconsistent',
1605
+ httpCode: 400,
1606
+ },
1607
+ CATALOG_BULK_REWRITE_LOCK_UNAVAILABLE: {
1608
+ code: 'ANORM007',
1609
+ message: 'Cannot guarantee exclusive execution right now, try again later',
1610
+ httpCode: 503,
1611
+ },
1612
+ ATTRIBUTE_NORMALIZATION_ALREADY_RUNNING: {
1613
+ code: 'ANORM005',
1614
+ message: 'Another attribute normalization run is already in progress for this company',
1615
+ httpCode: 409,
1616
+ },
1617
+ PRODUCT_IMAGE_MERGE_NOT_DUPLICATES: {
1618
+ code: 'PI017',
1619
+ message: 'Selected product images are not duplicates of the primary one',
1620
+ httpCode: 409,
1621
+ },
1622
+ PRODUCT_IMAGE_MERGE_ALREADY_RUNNING: {
1623
+ code: 'PI016',
1624
+ message: 'Another product image merge is already in progress for this company',
1625
+ httpCode: 409,
1626
+ },
1627
+ GROWER_SUGGESTIONS_FETCH_FAILED: {
1628
+ code: 'ANORM004',
1629
+ message: 'Error when retrieving grower suggestions',
1630
+ httpCode: 500,
1631
+ },
1582
1632
  } as const;
package/constant/index.ts CHANGED
@@ -10,3 +10,5 @@ export * from './okei';
10
10
  export * from './contact-phone.constant';
11
11
  export * from './settings.constant';
12
12
  export * from './consignment-product-enrichment.constant';
13
+ export * from './attribute-normalization.constant';
14
+ export * from './product-image-merge.constant';
@@ -0,0 +1,8 @@
1
+ export const PRODUCT_IMAGE_MERGE_LIMITS = {
2
+ // Поиск дублей группирует карточки в приложении, а не в SQL: канон цвета живёт в словаре
3
+ // на стороне кода. Значит все карточки компании оказываются в памяти, и объём надо ограничить
4
+ // явно — иначе на очень крупной компании анализ молча съест память процесса.
5
+ MAX_ANALYZED_IMAGES: 100_000,
6
+ // Сколько карточек можно поглотить за один запрос: всё идёт одной транзакцией.
7
+ MAX_MERGED_PER_REQUEST: 200,
8
+ } as const;
@@ -19,4 +19,6 @@ export enum ActionLogTypeEnum {
19
19
  DELETE_CONSIGNMENT_ITEMS = 'DELETE_CONSIGNMENT_ITEMS',
20
20
  GROUP_UPDATE_CONSIGNMENT_ITEMS = 'GROUP_UPDATE_CONSIGNMENT_ITEMS',
21
21
  TRANSFER_CONSIGNMENT_ITEMS = 'TRANSFER_CONSIGNMENT_ITEMS',
22
+ NORMALIZE_ATTRIBUTES = 'NORMALIZE_ATTRIBUTES',
23
+ MERGE_PRODUCT_IMAGES = 'MERGE_PRODUCT_IMAGES',
22
24
  }
package/enum/index.ts CHANGED
@@ -108,3 +108,8 @@ export * from './transfer-document-item-kind.enum';
108
108
  export * from './write-off-report-date-basis.enum';
109
109
  export * from './product-enrichment-field.enum';
110
110
  export * from './product-enrichment-field-state.enum';
111
+ export * from './normalized-attribute.enum';
112
+ export * from './normalization-target.enum';
113
+ export * from './normalization-value-state.enum';
114
+ export * from './normalization-decision-action.enum';
115
+ export * from './product-image-duplicate-mode.enum';
@@ -0,0 +1,9 @@
1
+ // Решение человека по одному написанию. Правила в БД не сохраняются — решение живёт один прогон.
2
+ export enum NormalizationDecisionActionEnum {
3
+ // Переписать на указанное значение.
4
+ MAP = 'MAP',
5
+ // Очистить поле (значение — мусор).
6
+ CLEAR = 'CLEAR',
7
+ // Оставить как есть.
8
+ KEEP = 'KEEP',
9
+ }
@@ -0,0 +1,8 @@
1
+ // Таблицы, попадающие в охват нормализации. PRODUCT_CONFIGURATION сужается только компанией:
2
+ // правила кратности нормализуются целиком, иначе они разъедутся с товарами.
3
+ export enum NormalizationTargetEnum {
4
+ PRODUCT = 'PRODUCT',
5
+ PRODUCT_IMAGE = 'PRODUCT_IMAGE',
6
+ PRE_ORDER_COLLECTION_ITEM = 'PRE_ORDER_COLLECTION_ITEM',
7
+ PRODUCT_CONFIGURATION = 'PRODUCT_CONFIGURATION',
8
+ }
@@ -0,0 +1,9 @@
1
+ // Что анализ знает про конкретное написание атрибута.
2
+ export enum NormalizationValueStateEnum {
3
+ // Словарь (или кластеризация для производителя) предложил другое написание.
4
+ SUGGESTED = 'SUGGESTED',
5
+ // Значение уже каноничное, менять нечего.
6
+ CANONICAL = 'CANONICAL',
7
+ // Словарь значение не узнал — решение принимает человек в рамках прогона.
8
+ UNRECOGNIZED = 'UNRECOGNIZED',
9
+ }
@@ -0,0 +1,6 @@
1
+ // Справочные атрибуты, которые умеет приводить к единому написанию операция нормализации.
2
+ export enum NormalizedAttributeEnum {
3
+ COLOR = 'COLOR',
4
+ COUNTRY = 'COUNTRY',
5
+ GROWER = 'GROWER',
6
+ }
@@ -0,0 +1,7 @@
1
+ // В каком виде искать дубли карточек.
2
+ export enum ProductImageDuplicateModeEnum {
3
+ // По значениям как они лежат сейчас.
4
+ CURRENT = 'CURRENT',
5
+ // По значениям, приведённым к канону — превью того, что слипнется после нормализации.
6
+ AFTER_NORMALIZATION = 'AFTER_NORMALIZATION',
7
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floristcloud/api-lib",
3
- "version": "1.2.134",
3
+ "version": "1.2.135",
4
4
  "description": "",
5
5
  "main": "./build/index.js",
6
6
  "scripts": {
@@ -17,6 +17,8 @@ import { ConsignmentItemsCreateActionLogSchema } from './consignment-items-creat
17
17
  import { ConsignmentItemsDeleteActionLogSchema } from './consignment-items-delete-action-log.schema';
18
18
  import { GroupUpdateConsignmentItemsActionLogSchema } from './group-update-consignment-items-action-log.schema';
19
19
  import { TransferConsignmentItemsActionLogSchema } from './transfer-consignment-items-action-log.schema';
20
+ import { NormalizeAttributesActionLogSchema } from './normalize-attributes-action-log.schema';
21
+ import { MergeProductImagesActionLogSchema } from './merge-product-images-action-log.schema';
20
22
 
21
23
  export const ActionLogPayloadSchema = z.union([
22
24
  OrderDeleteActionLogPayloadSchema,
@@ -37,5 +39,7 @@ export const ActionLogPayloadSchema = z.union([
37
39
  ConsignmentItemsDeleteActionLogSchema,
38
40
  GroupUpdateConsignmentItemsActionLogSchema,
39
41
  TransferConsignmentItemsActionLogSchema,
42
+ NormalizeAttributesActionLogSchema,
43
+ MergeProductImagesActionLogSchema,
40
44
  z.null(),
41
45
  ]);
@@ -0,0 +1,9 @@
1
+ import { z } from 'zod';
2
+
3
+ export const MergeProductImagesActionLogSchema = z.object({
4
+ primaryImageUUID: z.string(),
5
+ mergedImageUUIDList: z.array(z.string()),
6
+ relinkedProducts: z.number(),
7
+ relinkedPreOrderCollectionItems: z.number(),
8
+ movedAliases: z.number(),
9
+ });
@@ -0,0 +1,21 @@
1
+ import { z } from 'zod';
2
+ import { NormalizedAttributeEnum } from '../../enum/normalized-attribute.enum';
3
+ import { NormalizationTargetEnum } from '../../enum/normalization-target.enum';
4
+ import { NormalizationDecisionActionEnum } from '../../enum/normalization-decision-action.enum';
5
+
6
+ // Одна агрегатная запись на прогон: построчный лог тут не нужен, операция работает
7
+ // на уникальных значениях, а не на строках.
8
+ export const NormalizeAttributesActionLogSchema = z.object({
9
+ attributes: z.array(z.nativeEnum(NormalizedAttributeEnum)),
10
+ targets: z.array(z.nativeEnum(NormalizationTargetEnum)),
11
+ decisions: z.array(
12
+ z.object({
13
+ attribute: z.nativeEnum(NormalizedAttributeEnum),
14
+ raw: z.string(),
15
+ action: z.nativeEnum(NormalizationDecisionActionEnum),
16
+ value: z.string().nullable(),
17
+ }),
18
+ ),
19
+ updatedRows: z.partialRecord(z.nativeEnum(NormalizationTargetEnum), z.number()),
20
+ totalUpdatedRows: z.number(),
21
+ });
@@ -0,0 +1,31 @@
1
+ import { z } from 'zod';
2
+ import { NormalizedAttributeEnum } from '../../enum/normalized-attribute.enum';
3
+ import { NormalizationTargetEnum } from '../../enum/normalization-target.enum';
4
+
5
+ /**
6
+ * Охват прогона. Общий для анализа и применения: execute перепроверяет его заново, потому что
7
+ * между анализом и применением в таблицы могли приехать новые строки.
8
+ *
9
+ * Ограничения охвата, которые нельзя выразить схемой и которые проверяет сервис:
10
+ * - consignmentUUIDList сужает только PRODUCT — у карточек и позиций коллекций нет поставки;
11
+ * - preOrderCollectionUUIDList сужает только PRE_ORDER_COLLECTION_ITEM;
12
+ * - PRODUCT_CONFIGURATION не сужается ничем, кроме компании: правила нормализуются целиком,
13
+ * иначе часть из них разъедется с товарами.
14
+ */
15
+ // Дедупликация в схеме, а не в сервисе: списки читают и analyze, и execute, и запись в журнал —
16
+ // повтор «PRODUCT» тысячу раз означал бы тысячу полных проходов по таблице товаров.
17
+ const dedupe = <T>(list: T[]): T[] => Array.from(new Set(list));
18
+
19
+ export const AttributeNormalizationScopeSchema = z.object({
20
+ attributes: z.array(z.nativeEnum(NormalizedAttributeEnum)).min(1).max(Object.keys(NormalizedAttributeEnum).length).transform(dedupe),
21
+ targets: z.array(z.nativeEnum(NormalizationTargetEnum)).min(1).max(Object.keys(NormalizationTargetEnum).length).transform(dedupe),
22
+ // Без верхней границы: списки уходят в запрос одним параметром-массивом, длина упирается
23
+ // только в размер тела запроса.
24
+ consignmentUUIDList: z.array(z.string().uuid()).optional(),
25
+ preOrderCollectionUUIDList: z.array(z.string().uuid()).optional(),
26
+ createdFrom: z.coerce.date().optional(),
27
+ createdTo: z.coerce.date().optional(),
28
+ // У поставки нет признака закрытости — закрытость живёт на товаре (Product.isOpen).
29
+ // По умолчанию закрытые товары не трогаем: это распроданная история.
30
+ isIncludingClosedProducts: z.boolean().default(false),
31
+ });
@@ -0,0 +1,18 @@
1
+ import { z } from 'zod';
2
+ import { NormalizedAttributeEnum } from '../../enum/normalized-attribute.enum';
3
+ import { NormalizationDecisionActionEnum } from '../../enum/normalization-decision-action.enum';
4
+ import { ATTRIBUTE_NORMALIZATION_LIMITS } from '../../constant/attribute-normalization.constant';
5
+
6
+ // Решение человека по одному написанию. Правила в БД не сохраняются — решение живёт один прогон.
7
+ export const NormalizationDecisionSchema = z
8
+ .object({
9
+ attribute: z.nativeEnum(NormalizedAttributeEnum),
10
+ raw: z.string().min(1).max(ATTRIBUTE_NORMALIZATION_LIMITS.MAX_VALUE_LENGTH),
11
+ action: z.nativeEnum(NormalizationDecisionActionEnum),
12
+ // Целевое написание — обязательно и только для MAP.
13
+ value: z.string().min(1).max(ATTRIBUTE_NORMALIZATION_LIMITS.MAX_VALUE_LENGTH).optional(),
14
+ })
15
+ .refine(decision => (decision.action === NormalizationDecisionActionEnum.MAP) === (decision.value !== undefined), {
16
+ message: 'value is required for MAP and forbidden otherwise',
17
+ path: ['value'],
18
+ });
@@ -0,0 +1,6 @@
1
+ import { z } from 'zod';
2
+ import { NormalizationTargetEnum } from '../../enum/normalization-target.enum';
3
+
4
+ // Сколько строк приходится на каждую таблицу охвата. Ключи неполные: значение живёт
5
+ // не во всех таблицах сразу, а таблица может не хранить нужный атрибут.
6
+ export const NormalizationRowCountSchema = z.partialRecord(z.nativeEnum(NormalizationTargetEnum), z.number());
@@ -0,0 +1,28 @@
1
+ import { z } from 'zod';
2
+ import { NormalizedAttributeEnum } from '../../enum/normalized-attribute.enum';
3
+ import { NormalizationTargetEnum } from '../../enum/normalization-target.enum';
4
+ import { NormalizationRowCountSchema } from './normalization-row-count.schema';
5
+ import { ProductImageDuplicateForecastSchema } from './product-image-duplicate-forecast.schema';
6
+
7
+ // Итоги анализа: всё, что фронт показывает перед запуском необратимой операции.
8
+ export const NormalizationSummarySchema = z.object({
9
+ // Уникальных написаний по каждому атрибуту.
10
+ uniqueValues: z.partialRecord(z.nativeEnum(NormalizedAttributeEnum), z.number()),
11
+ // Строк к изменению по каждой таблице, если принять все предложения.
12
+ affectedRows: NormalizationRowCountSchema,
13
+ // Строк, которые прогон пропустит из-за того, что позиция закрыта. Нужно фронту, чтобы
14
+ // предложить включить их флагом: по умолчанию закрытая история не трогается.
15
+ // null означает «пропускать нечего»: либо флаг уже включён, либо ни одна таблица охвата
16
+ // не различает открытые и закрытые позиции.
17
+ skippedClosedRows: NormalizationRowCountSchema.nullable(),
18
+ // Строк заказов, отображающих затрагиваемые товары. Нужно фронту для предупреждения:
19
+ // операция необратима, а в заказах видно старое написание.
20
+ affectedOrderProductRows: z.number(),
21
+ // Таблицы, которых не касается сужение по выбранным поставкам и коллекциям: у карточки
22
+ // номенклатуры нет поставки, у товара нет коллекции. Для них охват остаётся широким —
23
+ // фронт обязан предупредить, иначе админ будет думать, что тронет одну поставку,
24
+ // а прогон перепишет всю компанию.
25
+ targetsIgnoringUuidScope: z.array(z.nativeEnum(NormalizationTargetEnum)),
26
+ // null означает, что карточки в охват не входят и прогон их не трогает.
27
+ productImageDuplicatesAfterRun: ProductImageDuplicateForecastSchema.nullable(),
28
+ });
@@ -0,0 +1,17 @@
1
+ import { z } from 'zod';
2
+ import { NormalizedAttributeEnum } from '../../enum/normalized-attribute.enum';
3
+ import { NormalizationValueStateEnum } from '../../enum/normalization-value-state.enum';
4
+ import { NormalizationRowCountSchema } from './normalization-row-count.schema';
5
+
6
+ // Одно написание атрибута в истории компании и то, что операция про него знает.
7
+ export const NormalizationValueSchema = z.object({
8
+ attribute: z.nativeEnum(NormalizedAttributeEnum),
9
+ // Написание как оно лежит в базе — оно же ключ решения в execute.
10
+ raw: z.string(),
11
+ // Что предлагает словарь (для производителя — кластеризация). null, если предложить нечего.
12
+ suggested: z.string().nullable(),
13
+ state: z.nativeEnum(NormalizationValueStateEnum),
14
+ rowCount: NormalizationRowCountSchema,
15
+ // Другие написания того же кластера — только для производителя, у цвета и страны словарь.
16
+ clusterRaw: z.array(z.string()),
17
+ });
@@ -0,0 +1,13 @@
1
+ import { z } from 'zod';
2
+
3
+ // Прогноз по дублям карточек номенклатуры после прогона. Уникального ключа по идентичности
4
+ // у карточек нет, поэтому прогон может сделать две карточки неразличимыми, и резолвер начнёт
5
+ // выбирать между ними произвольно. Апдейт при этом не упадёт — предупредить можно только заранее.
6
+ export const ProductImageDuplicateForecastSchema = z.object({
7
+ // false — посчитать не удалось (карточек больше предела). Это НЕ то же самое,
8
+ // что «дублей не будет»: при false числа ниже не значат ничего.
9
+ isCounted: z.boolean(),
10
+ total: z.number(),
11
+ // Из них те, что сегодня ещё различимы: именно их создаст этот прогон.
12
+ new: z.number(),
13
+ });
package/schemas/index.ts CHANGED
@@ -84,3 +84,11 @@ export * from './positive-money.schema';
84
84
  export * from './report/report-order-filter.schema';
85
85
  export * from './transfer-document/transfer-document.schema';
86
86
  export * from './transfer-document/transfer-document-detailed.schema';
87
+ export * from './attribute-normalization/attribute-normalization-scope.schema';
88
+ export * from './product-image/product-image-merge-candidate.schema';
89
+ export * from './product-image/product-image-merge-group.schema';
90
+ export * from './attribute-normalization/normalization-row-count.schema';
91
+ export * from './attribute-normalization/normalization-value.schema';
92
+ export * from './attribute-normalization/normalization-summary.schema';
93
+ export * from './attribute-normalization/normalization-decision.schema';
94
+ export * from './attribute-normalization/product-image-duplicate-forecast.schema';
@@ -0,0 +1,20 @@
1
+ import { z } from 'zod';
2
+
3
+ // Карточка — участник группы дублей, с числом ссылок на неё.
4
+ export const ProductImageMergeCandidateSchema = z.object({
5
+ uuid: z.string(),
6
+ name: z.string(),
7
+ // Ключ файла в хранилище: без него нечем показать превью фото группы, а основную
8
+ // карточку админ выбирает в том числе по картинке.
9
+ productImageUrl: z.string(),
10
+ categoryUUID: z.string().nullable(),
11
+ grower: z.string().nullable(),
12
+ country: z.string().nullable(),
13
+ color: z.string().nullable(),
14
+ productCount: z.number(),
15
+ preOrderCollectionItemCount: z.number(),
16
+ aliasCount: z.number(),
17
+ // Карточка, которую операция предлагает оставить: больше всего ссылок, при равенстве —
18
+ // меньший uuid, чтобы повторный анализ давал тот же ответ.
19
+ isPrimary: z.boolean(),
20
+ });
@@ -0,0 +1,9 @@
1
+ import { z } from 'zod';
2
+ import { ProductImageMergeCandidateSchema } from './product-image-merge-candidate.schema';
3
+
4
+ // Группа карточек, неразличимых по идентичности.
5
+ export const ProductImageMergeGroupSchema = z.object({
6
+ // Ключ группы — нормализованная идентичность, по которой карточки признаны одинаковыми.
7
+ key: z.string(),
8
+ candidates: z.array(ProductImageMergeCandidateSchema),
9
+ });