@floristcloud/api-lib 1.2.134 → 1.2.136

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 (65) 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/permission-key.enum.js +18 -0
  20. package/build/enum/product-image-duplicate-mode.enum.js +11 -0
  21. package/build/schemas/action-log/action-log-payload.schema.js +4 -0
  22. package/build/schemas/action-log/merge-product-images-action-log.schema.js +11 -0
  23. package/build/schemas/action-log/normalize-attributes-action-log.schema.js +21 -0
  24. package/build/schemas/attribute-normalization/attribute-normalization-scope.schema.js +32 -0
  25. package/build/schemas/attribute-normalization/normalization-decision.schema.js +20 -0
  26. package/build/schemas/attribute-normalization/normalization-row-count.schema.js +8 -0
  27. package/build/schemas/attribute-normalization/normalization-summary.schema.js +31 -0
  28. package/build/schemas/attribute-normalization/normalization-value.schema.js +19 -0
  29. package/build/schemas/attribute-normalization/product-image-duplicate-forecast.schema.js +15 -0
  30. package/build/schemas/index.js +8 -0
  31. package/build/schemas/product-image/product-image-merge-candidate.schema.js +22 -0
  32. package/build/schemas/product-image/product-image-merge-group.schema.js +11 -0
  33. package/commands/attribute-normalization/analyze-attribute-normalization.query.ts +24 -0
  34. package/commands/attribute-normalization/execute-attribute-normalization.command.ts +32 -0
  35. package/commands/attribute-normalization/get-grower-suggestions.query.ts +28 -0
  36. package/commands/attribute-normalization/index.ts +3 -0
  37. package/commands/index.ts +1 -0
  38. package/commands/product-image/analyze-product-image-merge.query.ts +28 -0
  39. package/commands/product-image/execute-product-image-merge.command.ts +27 -0
  40. package/commands/product-image/index.ts +2 -0
  41. package/constant/attribute-normalization.constant.ts +9 -0
  42. package/constant/error.ts +50 -0
  43. package/constant/index.ts +2 -0
  44. package/constant/product-image-merge.constant.ts +8 -0
  45. package/enum/action-log-type.enum.ts +2 -0
  46. package/enum/index.ts +5 -0
  47. package/enum/normalization-decision-action.enum.ts +9 -0
  48. package/enum/normalization-target.enum.ts +8 -0
  49. package/enum/normalization-value-state.enum.ts +9 -0
  50. package/enum/normalized-attribute.enum.ts +6 -0
  51. package/enum/permission-key.enum.ts +18 -0
  52. package/enum/product-image-duplicate-mode.enum.ts +7 -0
  53. package/package.json +1 -1
  54. package/schemas/action-log/action-log-payload.schema.ts +4 -0
  55. package/schemas/action-log/merge-product-images-action-log.schema.ts +9 -0
  56. package/schemas/action-log/normalize-attributes-action-log.schema.ts +21 -0
  57. package/schemas/attribute-normalization/attribute-normalization-scope.schema.ts +31 -0
  58. package/schemas/attribute-normalization/normalization-decision.schema.ts +18 -0
  59. package/schemas/attribute-normalization/normalization-row-count.schema.ts +6 -0
  60. package/schemas/attribute-normalization/normalization-summary.schema.ts +29 -0
  61. package/schemas/attribute-normalization/normalization-value.schema.ts +17 -0
  62. package/schemas/attribute-normalization/product-image-duplicate-forecast.schema.ts +13 -0
  63. package/schemas/index.ts +8 -0
  64. package/schemas/product-image/product-image-merge-candidate.schema.ts +20 -0
  65. package/schemas/product-image/product-image-merge-group.schema.ts +9 -0
@@ -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
+ }
@@ -17,6 +17,16 @@ export enum PermissionKeyEnum {
17
17
  PAGE_PRE_ORDERS_VIEW = 'page.pre_orders.view',
18
18
  PAGE_REPORT_VIEW = 'page.report.view',
19
19
  PAGE_STAFF_VIEW = 'page.staff.view',
20
+ ORDER_CREATE = 'order.create',
21
+ ORDER_EDIT = 'order.edit',
22
+ ORDER_DELETE = 'order.delete',
23
+ // Order-level money edits: discount and delivery cost (скидка и стоимость доставки).
24
+ ORDER_PRICE_EDIT = 'order.price.edit',
25
+ ORDER_STATUS_CHANGE = 'order.status.change',
26
+ WRITE_OFF_CREATE = 'write_off.create',
27
+ WRITE_OFF_DELETE = 'write_off.delete',
28
+ // Moving a write-off out of PENDING (принятие / отклонение списания).
29
+ WRITE_OFF_APPROVE = 'write_off.approve',
20
30
  }
21
31
 
22
32
  export const PERMISSION_KEY_LIST: PermissionKeyEnum[] = [
@@ -36,4 +46,12 @@ export const PERMISSION_KEY_LIST: PermissionKeyEnum[] = [
36
46
  PermissionKeyEnum.PAGE_PRE_ORDERS_VIEW,
37
47
  PermissionKeyEnum.PAGE_REPORT_VIEW,
38
48
  PermissionKeyEnum.PAGE_STAFF_VIEW,
49
+ PermissionKeyEnum.ORDER_CREATE,
50
+ PermissionKeyEnum.ORDER_EDIT,
51
+ PermissionKeyEnum.ORDER_DELETE,
52
+ PermissionKeyEnum.ORDER_PRICE_EDIT,
53
+ PermissionKeyEnum.ORDER_STATUS_CHANGE,
54
+ PermissionKeyEnum.WRITE_OFF_CREATE,
55
+ PermissionKeyEnum.WRITE_OFF_DELETE,
56
+ PermissionKeyEnum.WRITE_OFF_APPROVE,
39
57
  ];
@@ -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.136",
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,29 @@
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
+ // строка, у которой меняются и цвет, и страна, — это одна строка, а не две.
13
+ affectedRows: NormalizationRowCountSchema,
14
+ // Строк, которые прогон пропустит из-за того, что позиция закрыта. Нужно фронту, чтобы
15
+ // предложить включить их флагом: по умолчанию закрытая история не трогается.
16
+ // null означает «пропускать нечего»: либо флаг уже включён, либо ни одна таблица охвата
17
+ // не различает открытые и закрытые позиции.
18
+ skippedClosedRows: NormalizationRowCountSchema.nullable(),
19
+ // Строк заказов, отображающих затрагиваемые товары. Нужно фронту для предупреждения:
20
+ // операция необратима, а в заказах видно старое написание.
21
+ affectedOrderProductRows: z.number(),
22
+ // Таблицы, которых не касается сужение по выбранным поставкам и коллекциям: у карточки
23
+ // номенклатуры нет поставки, у товара нет коллекции. Для них охват остаётся широким —
24
+ // фронт обязан предупредить, иначе админ будет думать, что тронет одну поставку,
25
+ // а прогон перепишет всю компанию.
26
+ targetsIgnoringUuidScope: z.array(z.nativeEnum(NormalizationTargetEnum)),
27
+ // null означает, что карточки в охват не входят и прогон их не трогает.
28
+ productImageDuplicatesAfterRun: ProductImageDuplicateForecastSchema.nullable(),
29
+ });
@@ -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
+ });