@open-mercato/core 0.4.5-develop-f4858e0ef3 → 0.4.5-develop-4849712ccb
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/generated/entities/catalog_product/index.js +16 -0
- package/dist/generated/entities/catalog_product/index.js.map +2 -2
- package/dist/generated/entities/catalog_product_unit_conversion/index.js +27 -0
- package/dist/generated/entities/catalog_product_unit_conversion/index.js.map +7 -0
- package/dist/generated/entities/sales_credit_memo_line/index.js +7 -1
- package/dist/generated/entities/sales_credit_memo_line/index.js.map +2 -2
- package/dist/generated/entities/sales_invoice_line/index.js +7 -1
- package/dist/generated/entities/sales_invoice_line/index.js.map +2 -2
- package/dist/generated/entities/sales_order_line/index.js +6 -0
- package/dist/generated/entities/sales_order_line/index.js.map +2 -2
- package/dist/generated/entities/sales_quote_line/index.js +6 -0
- package/dist/generated/entities/sales_quote_line/index.js.map +2 -2
- package/dist/generated/entities.ids.generated.js +1 -0
- package/dist/generated/entities.ids.generated.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +2 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/catalog/api/prices/route.js +123 -8
- package/dist/modules/catalog/api/prices/route.js.map +2 -2
- package/dist/modules/catalog/api/product-unit-conversions/route.js +194 -0
- package/dist/modules/catalog/api/product-unit-conversions/route.js.map +7 -0
- package/dist/modules/catalog/api/products/route.js +351 -201
- package/dist/modules/catalog/api/products/route.js.map +2 -2
- package/dist/modules/catalog/backend/catalog/products/[id]/page.js +1267 -497
- package/dist/modules/catalog/backend/catalog/products/[id]/page.js.map +2 -2
- package/dist/modules/catalog/backend/catalog/products/create/page.js +733 -210
- package/dist/modules/catalog/backend/catalog/products/create/page.js.map +2 -2
- package/dist/modules/catalog/commands/index.js +1 -0
- package/dist/modules/catalog/commands/index.js.map +2 -2
- package/dist/modules/catalog/commands/productUnitConversions.js +503 -0
- package/dist/modules/catalog/commands/productUnitConversions.js.map +7 -0
- package/dist/modules/catalog/commands/products.js +355 -73
- package/dist/modules/catalog/commands/products.js.map +2 -2
- package/dist/modules/catalog/commands/shared.js +18 -4
- package/dist/modules/catalog/commands/shared.js.map +2 -2
- package/dist/modules/catalog/components/products/ProductUomSection.js +591 -0
- package/dist/modules/catalog/components/products/ProductUomSection.js.map +7 -0
- package/dist/modules/catalog/components/products/productForm.js +66 -5
- package/dist/modules/catalog/components/products/productForm.js.map +2 -2
- package/dist/modules/catalog/components/products/productFormUtils.js +68 -0
- package/dist/modules/catalog/components/products/productFormUtils.js.map +7 -0
- package/dist/modules/catalog/data/entities.js +86 -0
- package/dist/modules/catalog/data/entities.js.map +2 -2
- package/dist/modules/catalog/data/validators.js +65 -3
- package/dist/modules/catalog/data/validators.js.map +2 -2
- package/dist/modules/catalog/events.js +3 -0
- package/dist/modules/catalog/events.js.map +2 -2
- package/dist/modules/catalog/lib/unitCodes.js +7 -0
- package/dist/modules/catalog/lib/unitCodes.js.map +7 -0
- package/dist/modules/catalog/lib/unitResolution.js +53 -0
- package/dist/modules/catalog/lib/unitResolution.js.map +7 -0
- package/dist/modules/catalog/migrations/Migration20260218225422.js +19 -0
- package/dist/modules/catalog/migrations/Migration20260218225422.js.map +7 -0
- package/dist/modules/catalog/migrations/Migration20260219084500.js +27 -0
- package/dist/modules/catalog/migrations/Migration20260219084500.js.map +7 -0
- package/dist/modules/catalog/search.js +69 -1
- package/dist/modules/catalog/search.js.map +2 -2
- package/dist/modules/catalog/seed/examples.js +91 -42
- package/dist/modules/catalog/seed/examples.js.map +2 -2
- package/dist/modules/dashboards/seed/analytics.js +3 -0
- package/dist/modules/dashboards/seed/analytics.js.map +2 -2
- package/dist/modules/sales/api/order-lines/route.js +98 -15
- package/dist/modules/sales/api/order-lines/route.js.map +2 -2
- package/dist/modules/sales/api/quote-lines/route.js +101 -14
- package/dist/modules/sales/api/quote-lines/route.js.map +2 -2
- package/dist/modules/sales/api/quotes/public/[token]/route.js +87 -12
- package/dist/modules/sales/api/quotes/public/[token]/route.js.map +2 -2
- package/dist/modules/sales/commands/documents.js +1424 -260
- package/dist/modules/sales/commands/documents.js.map +3 -3
- package/dist/modules/sales/commands/shared.js +6 -2
- package/dist/modules/sales/commands/shared.js.map +2 -2
- package/dist/modules/sales/components/documents/ItemsSection.js +216 -86
- package/dist/modules/sales/components/documents/ItemsSection.js.map +2 -2
- package/dist/modules/sales/components/documents/LineItemDialog.js +913 -241
- package/dist/modules/sales/components/documents/LineItemDialog.js.map +3 -3
- package/dist/modules/sales/components/documents/ShipmentsSection.js +15 -3
- package/dist/modules/sales/components/documents/ShipmentsSection.js.map +2 -2
- package/dist/modules/sales/data/entities.js +59 -3
- package/dist/modules/sales/data/entities.js.map +2 -2
- package/dist/modules/sales/data/validators.js +35 -0
- package/dist/modules/sales/data/validators.js.map +2 -2
- package/dist/modules/sales/frontend/quote/[token]/page.js +15 -1
- package/dist/modules/sales/frontend/quote/[token]/page.js.map +2 -2
- package/dist/modules/sales/migrations/Migration20260218225423.js +31 -0
- package/dist/modules/sales/migrations/Migration20260218225423.js.map +7 -0
- package/dist/modules/sales/migrations/Migration20260219084501.js +71 -0
- package/dist/modules/sales/migrations/Migration20260219084501.js.map +7 -0
- package/dist/modules/sales/search.js +28 -0
- package/dist/modules/sales/search.js.map +2 -2
- package/dist/modules/sales/seed/examples.js +14 -1
- package/dist/modules/sales/seed/examples.js.map +2 -2
- package/dist/modules/sales/widgets/injection/document-history/widget.client.js +1 -1
- package/dist/modules/sales/widgets/injection/document-history/widget.client.js.map +2 -2
- package/generated/entities/catalog_product/index.ts +8 -0
- package/generated/entities/catalog_product_unit_conversion/index.ts +12 -0
- package/generated/entities/sales_credit_memo_line/index.ts +3 -0
- package/generated/entities/sales_invoice_line/index.ts +3 -0
- package/generated/entities/sales_order_line/index.ts +3 -0
- package/generated/entities/sales_quote_line/index.ts +3 -0
- package/generated/entities.ids.generated.ts +1 -0
- package/generated/entity-fields-registry.ts +2 -0
- package/package.json +2 -2
- package/src/modules/auth/i18n/de.json +1 -1
- package/src/modules/auth/i18n/en.json +1 -1
- package/src/modules/auth/i18n/es.json +1 -1
- package/src/modules/auth/i18n/pl.json +1 -1
- package/src/modules/catalog/api/prices/route.ts +213 -81
- package/src/modules/catalog/api/product-unit-conversions/route.ts +195 -0
- package/src/modules/catalog/api/products/route.ts +638 -402
- package/src/modules/catalog/backend/catalog/products/[id]/page.tsx +2085 -1072
- package/src/modules/catalog/backend/catalog/products/create/page.tsx +1288 -593
- package/src/modules/catalog/commands/index.ts +1 -0
- package/src/modules/catalog/commands/productUnitConversions.ts +626 -0
- package/src/modules/catalog/commands/products.ts +1151 -693
- package/src/modules/catalog/commands/shared.ts +19 -5
- package/src/modules/catalog/components/products/ProductUomSection.tsx +745 -0
- package/src/modules/catalog/components/products/productForm.ts +369 -256
- package/src/modules/catalog/components/products/productFormUtils.ts +82 -0
- package/src/modules/catalog/data/entities.ts +82 -1
- package/src/modules/catalog/data/validators.ts +118 -34
- package/src/modules/catalog/events.ts +3 -0
- package/src/modules/catalog/i18n/de.json +56 -0
- package/src/modules/catalog/i18n/en.json +56 -0
- package/src/modules/catalog/i18n/es.json +56 -0
- package/src/modules/catalog/i18n/pl.json +56 -0
- package/src/modules/catalog/lib/unitCodes.ts +1 -0
- package/src/modules/catalog/lib/unitResolution.ts +62 -0
- package/src/modules/catalog/migrations/.snapshot-open-mercato.json +245 -0
- package/src/modules/catalog/migrations/Migration20260218225422.ts +21 -0
- package/src/modules/catalog/migrations/Migration20260219084500.ts +26 -0
- package/src/modules/catalog/search.ts +73 -1
- package/src/modules/catalog/seed/examples.ts +552 -479
- package/src/modules/dashboards/i18n/de.json +1 -1
- package/src/modules/dashboards/i18n/en.json +1 -1
- package/src/modules/dashboards/i18n/es.json +1 -1
- package/src/modules/dashboards/i18n/pl.json +1 -1
- package/src/modules/dashboards/seed/analytics.ts +3 -0
- package/src/modules/sales/api/order-lines/route.ts +158 -68
- package/src/modules/sales/api/quote-lines/route.ts +161 -67
- package/src/modules/sales/api/quotes/public/[token]/route.ts +122 -36
- package/src/modules/sales/commands/documents.ts +4250 -2424
- package/src/modules/sales/commands/shared.ts +7 -2
- package/src/modules/sales/components/documents/ItemsSection.tsx +580 -310
- package/src/modules/sales/components/documents/LineItemDialog.tsx +1988 -833
- package/src/modules/sales/components/documents/ShipmentsSection.tsx +17 -3
- package/src/modules/sales/components/documents/lineItemTypes.ts +6 -0
- package/src/modules/sales/data/entities.ts +53 -0
- package/src/modules/sales/data/validators.ts +36 -0
- package/src/modules/sales/frontend/quote/[token]/page.tsx +25 -1
- package/src/modules/sales/i18n/de.json +23 -3
- package/src/modules/sales/i18n/en.json +23 -3
- package/src/modules/sales/i18n/es.json +23 -3
- package/src/modules/sales/i18n/pl.json +23 -3
- package/src/modules/sales/lib/types.ts +30 -0
- package/src/modules/sales/migrations/.snapshot-open-mercato.json +172 -0
- package/src/modules/sales/migrations/Migration20260218225423.ts +37 -0
- package/src/modules/sales/migrations/Migration20260219084501.ts +73 -0
- package/src/modules/sales/search.ts +28 -0
- package/src/modules/sales/seed/examples.ts +20 -1
- package/src/modules/sales/widgets/injection/document-history/widget.client.tsx +1 -1
- package/src/modules/workflows/i18n/de.json +4 -4
- package/src/modules/workflows/i18n/en.json +4 -4
- package/src/modules/workflows/i18n/es.json +4 -4
- package/src/modules/workflows/i18n/pl.json +4 -4
|
@@ -1,46 +1,58 @@
|
|
|
1
|
-
import { randomUUID } from
|
|
2
|
-
import { registerCommand } from
|
|
3
|
-
import type {
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { registerCommand } from "@open-mercato/shared/lib/commands";
|
|
3
|
+
import type {
|
|
4
|
+
CommandHandler,
|
|
5
|
+
CommandRuntimeContext,
|
|
6
|
+
} from "@open-mercato/shared/lib/commands";
|
|
4
7
|
import {
|
|
8
|
+
buildChanges,
|
|
5
9
|
requireId,
|
|
6
10
|
parseWithCustomFields,
|
|
7
11
|
setCustomFieldsIfAny,
|
|
8
12
|
emitCrudSideEffects,
|
|
9
13
|
emitCrudUndoSideEffects,
|
|
10
|
-
} from
|
|
11
|
-
import type { EntityManager } from
|
|
12
|
-
import { UniqueConstraintViolationException } from
|
|
13
|
-
import { resolveTranslations } from
|
|
14
|
-
import { CrudHttpError } from
|
|
15
|
-
import type {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
import {
|
|
14
|
+
} from "@open-mercato/shared/lib/commands/helpers";
|
|
15
|
+
import type { EntityManager } from "@mikro-orm/postgresql";
|
|
16
|
+
import { UniqueConstraintViolationException } from "@mikro-orm/core";
|
|
17
|
+
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
18
|
+
import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
19
|
+
import type {
|
|
20
|
+
CrudEventAction,
|
|
21
|
+
CrudEventsConfig,
|
|
22
|
+
CrudIndexerConfig,
|
|
23
|
+
} from "@open-mercato/shared/lib/crud/types";
|
|
24
|
+
import type { DataEngine } from "@open-mercato/shared/lib/data/engine";
|
|
25
|
+
import {
|
|
26
|
+
loadCustomFieldSnapshot,
|
|
27
|
+
buildCustomFieldResetMap,
|
|
28
|
+
} from "@open-mercato/shared/lib/commands/customFieldSnapshots";
|
|
29
|
+
import { E } from "#generated/entities.ids.generated";
|
|
30
|
+
import { slugifyTagLabel } from "@open-mercato/shared/lib/utils";
|
|
31
|
+
import { parseObjectLike } from "@open-mercato/shared/lib/json/parseObjectLike";
|
|
21
32
|
import {
|
|
22
33
|
CatalogOffer,
|
|
23
34
|
CatalogProduct,
|
|
24
35
|
CatalogProductVariant,
|
|
25
36
|
CatalogProductPrice,
|
|
37
|
+
CatalogProductUnitConversion,
|
|
26
38
|
CatalogOptionSchemaTemplate,
|
|
27
39
|
CatalogProductCategory,
|
|
28
40
|
CatalogProductCategoryAssignment,
|
|
29
41
|
CatalogProductTag,
|
|
30
42
|
CatalogProductTagAssignment,
|
|
31
|
-
} from
|
|
32
|
-
import { SalesTaxRate } from
|
|
43
|
+
} from "../data/entities";
|
|
44
|
+
import { SalesTaxRate } from "@open-mercato/core/modules/sales/data/entities";
|
|
33
45
|
import {
|
|
34
46
|
productCreateSchema,
|
|
35
47
|
productUpdateSchema,
|
|
36
48
|
type OfferInput,
|
|
37
49
|
type ProductCreateInput,
|
|
38
50
|
type ProductUpdateInput,
|
|
39
|
-
} from
|
|
51
|
+
} from "../data/validators";
|
|
40
52
|
import type {
|
|
41
53
|
CatalogProductOptionSchema,
|
|
42
54
|
CatalogProductType,
|
|
43
|
-
} from
|
|
55
|
+
} from "../data/types";
|
|
44
56
|
import {
|
|
45
57
|
cloneJson,
|
|
46
58
|
ensureOrganizationScope,
|
|
@@ -52,50 +64,171 @@ import {
|
|
|
52
64
|
emitCatalogQueryIndexEvent,
|
|
53
65
|
randomSuffix,
|
|
54
66
|
toNumericString,
|
|
55
|
-
|
|
56
|
-
|
|
67
|
+
getErrorConstraint,
|
|
68
|
+
getErrorMessage,
|
|
69
|
+
} from "./shared";
|
|
70
|
+
import {
|
|
71
|
+
findWithDecryption,
|
|
72
|
+
findOneWithDecryption,
|
|
73
|
+
} from "@open-mercato/shared/lib/encryption/find";
|
|
74
|
+
import { canonicalizeUnitCode } from "../lib/unitCodes";
|
|
75
|
+
import {
|
|
76
|
+
resolveCanonicalUnitCode,
|
|
77
|
+
} from "../lib/unitResolution";
|
|
57
78
|
|
|
58
79
|
type ProductSnapshot = {
|
|
59
|
-
id: string
|
|
60
|
-
organizationId: string
|
|
61
|
-
tenantId: string
|
|
62
|
-
title: string
|
|
63
|
-
subtitle: string | null
|
|
64
|
-
description: string | null
|
|
65
|
-
sku: string | null
|
|
66
|
-
handle: string | null
|
|
67
|
-
taxRateId: string | null
|
|
68
|
-
taxRate: string | null
|
|
69
|
-
productType: CatalogProductType
|
|
70
|
-
statusEntryId: string | null
|
|
71
|
-
primaryCurrencyCode: string | null
|
|
72
|
-
defaultUnit: string | null
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
80
|
+
id: string;
|
|
81
|
+
organizationId: string;
|
|
82
|
+
tenantId: string;
|
|
83
|
+
title: string;
|
|
84
|
+
subtitle: string | null;
|
|
85
|
+
description: string | null;
|
|
86
|
+
sku: string | null;
|
|
87
|
+
handle: string | null;
|
|
88
|
+
taxRateId: string | null;
|
|
89
|
+
taxRate: string | null;
|
|
90
|
+
productType: CatalogProductType;
|
|
91
|
+
statusEntryId: string | null;
|
|
92
|
+
primaryCurrencyCode: string | null;
|
|
93
|
+
defaultUnit: string | null;
|
|
94
|
+
defaultSalesUnit: string | null;
|
|
95
|
+
defaultSalesUnitQuantity: string;
|
|
96
|
+
uomRoundingScale: number;
|
|
97
|
+
uomRoundingMode: "half_up" | "down" | "up";
|
|
98
|
+
unitPriceEnabled: boolean;
|
|
99
|
+
unitPriceReferenceUnit: "kg" | "l" | "m2" | "m3" | "pc" | null;
|
|
100
|
+
unitPriceBaseQuantity: string | null;
|
|
101
|
+
defaultMediaId: string | null;
|
|
102
|
+
defaultMediaUrl: string | null;
|
|
103
|
+
weightValue: string | null;
|
|
104
|
+
weightUnit: string | null;
|
|
105
|
+
dimensions: Record<string, unknown> | null;
|
|
106
|
+
optionSchemaId: string | null;
|
|
107
|
+
customFieldsetCode: string | null;
|
|
108
|
+
metadata: Record<string, unknown> | null;
|
|
109
|
+
isConfigurable: boolean;
|
|
110
|
+
isActive: boolean;
|
|
111
|
+
createdAt: string;
|
|
112
|
+
updatedAt: string;
|
|
113
|
+
offers: OfferSnapshot[];
|
|
114
|
+
tags: string[];
|
|
115
|
+
categoryIds: string[];
|
|
116
|
+
custom: Record<string, unknown> | null;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
async function resolveProductUnitDefaults(
|
|
120
|
+
em: EntityManager,
|
|
121
|
+
params: {
|
|
122
|
+
organizationId: string;
|
|
123
|
+
tenantId: string;
|
|
124
|
+
defaultUnit: string | null | undefined;
|
|
125
|
+
defaultSalesUnit: string | null | undefined;
|
|
126
|
+
},
|
|
127
|
+
): Promise<{ defaultUnit: string | null; defaultSalesUnit: string | null }> {
|
|
128
|
+
const defaultUnitInput = canonicalizeUnitCode(params.defaultUnit);
|
|
129
|
+
const defaultSalesUnitInput = canonicalizeUnitCode(params.defaultSalesUnit);
|
|
130
|
+
if (!defaultUnitInput && defaultSalesUnitInput) {
|
|
131
|
+
throw new CrudHttpError(400, { error: "uom.default_unit_missing" });
|
|
132
|
+
}
|
|
133
|
+
const defaultUnit = defaultUnitInput
|
|
134
|
+
? await resolveCanonicalUnitCode(em, {
|
|
135
|
+
organizationId: params.organizationId,
|
|
136
|
+
tenantId: params.tenantId,
|
|
137
|
+
unitCode: defaultUnitInput,
|
|
138
|
+
})
|
|
139
|
+
: null;
|
|
140
|
+
const defaultSalesUnit = defaultSalesUnitInput
|
|
141
|
+
? await resolveCanonicalUnitCode(em, {
|
|
142
|
+
organizationId: params.organizationId,
|
|
143
|
+
tenantId: params.tenantId,
|
|
144
|
+
unitCode: defaultSalesUnitInput,
|
|
145
|
+
})
|
|
146
|
+
: null;
|
|
147
|
+
return { defaultUnit, defaultSalesUnit };
|
|
89
148
|
}
|
|
90
149
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
150
|
+
async function ensureBaseUnitCanBeRemoved(
|
|
151
|
+
em: EntityManager,
|
|
152
|
+
params: {
|
|
153
|
+
productId: string;
|
|
154
|
+
organizationId: string;
|
|
155
|
+
tenantId: string;
|
|
156
|
+
defaultUnit: string | null;
|
|
157
|
+
defaultSalesUnit: string | null;
|
|
158
|
+
},
|
|
159
|
+
): Promise<void> {
|
|
160
|
+
if (params.defaultUnit) return;
|
|
161
|
+
if (params.defaultSalesUnit) {
|
|
162
|
+
throw new CrudHttpError(400, { error: "uom.default_unit_missing" });
|
|
163
|
+
}
|
|
164
|
+
const activeConversionCount = await em.count(CatalogProductUnitConversion, {
|
|
165
|
+
product: params.productId,
|
|
166
|
+
organizationId: params.organizationId,
|
|
167
|
+
tenantId: params.tenantId,
|
|
168
|
+
deletedAt: null,
|
|
169
|
+
isActive: true,
|
|
170
|
+
});
|
|
171
|
+
if (activeConversionCount > 0) {
|
|
172
|
+
throw new CrudHttpError(400, { error: "uom.default_unit_missing" });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Resolves unit-price configuration from product input.
|
|
178
|
+
* Supports two input paths:
|
|
179
|
+
* - Nested: `unitPrice.enabled`, `unitPrice.referenceUnit`, `unitPrice.baseQuantity` (preferred)
|
|
180
|
+
* - Flat: `unitPriceEnabled`, `unitPriceReferenceUnit`, `unitPriceBaseQuantity` (legacy compat)
|
|
181
|
+
* Nested values take precedence when both are provided.
|
|
182
|
+
*/
|
|
183
|
+
function resolveUnitPriceInput(
|
|
184
|
+
parsed: ProductCreateInput | ProductUpdateInput,
|
|
185
|
+
): {
|
|
186
|
+
enabled?: boolean;
|
|
187
|
+
referenceUnit?: "kg" | "l" | "m2" | "m3" | "pc" | null;
|
|
188
|
+
baseQuantity?: string | null;
|
|
189
|
+
enabledProvided: boolean;
|
|
190
|
+
referenceProvided: boolean;
|
|
191
|
+
baseProvided: boolean;
|
|
192
|
+
} {
|
|
193
|
+
const enabledFromNested = parsed.unitPrice?.enabled;
|
|
194
|
+
const enabledFromFlat = parsed.unitPriceEnabled;
|
|
195
|
+
const referenceFromNested = parsed.unitPrice?.referenceUnit;
|
|
196
|
+
const referenceFromFlat = parsed.unitPriceReferenceUnit;
|
|
197
|
+
const baseFromNested = parsed.unitPrice?.baseQuantity;
|
|
198
|
+
const baseFromFlat = parsed.unitPriceBaseQuantity;
|
|
199
|
+
const enabledProvided =
|
|
200
|
+
enabledFromNested !== undefined || enabledFromFlat !== undefined;
|
|
201
|
+
const referenceProvided =
|
|
202
|
+
referenceFromNested !== undefined || referenceFromFlat !== undefined;
|
|
203
|
+
const baseProvided =
|
|
204
|
+
baseFromNested !== undefined || baseFromFlat !== undefined;
|
|
205
|
+
const enabled = enabledFromNested ?? enabledFromFlat;
|
|
206
|
+
const referenceUnit = canonicalizeUnitCode(
|
|
207
|
+
referenceFromNested ?? referenceFromFlat ?? null,
|
|
208
|
+
) as "kg" | "l" | "m2" | "m3" | "pc" | null | undefined;
|
|
209
|
+
const baseQuantitySource = baseFromNested ?? baseFromFlat;
|
|
210
|
+
const baseQuantity =
|
|
211
|
+
baseQuantitySource === undefined
|
|
212
|
+
? undefined
|
|
213
|
+
: (toNumericString(baseQuantitySource) ?? null);
|
|
214
|
+
return {
|
|
215
|
+
enabled,
|
|
216
|
+
referenceUnit,
|
|
217
|
+
baseQuantity,
|
|
218
|
+
enabledProvided,
|
|
219
|
+
referenceProvided,
|
|
220
|
+
baseProvided,
|
|
221
|
+
};
|
|
94
222
|
}
|
|
95
223
|
|
|
224
|
+
type ProductUndoPayload = {
|
|
225
|
+
before?: ProductSnapshot | null;
|
|
226
|
+
after?: ProductSnapshot | null;
|
|
227
|
+
};
|
|
228
|
+
|
|
96
229
|
const productCrudEvents: CrudEventsConfig<CatalogProduct> = {
|
|
97
|
-
module:
|
|
98
|
-
entity:
|
|
230
|
+
module: "catalog",
|
|
231
|
+
entity: "product",
|
|
99
232
|
persistent: true,
|
|
100
233
|
buildPayload: (ctx) => ({
|
|
101
234
|
id: ctx.identifiers.id,
|
|
@@ -105,7 +238,7 @@ const productCrudEvents: CrudEventsConfig<CatalogProduct> = {
|
|
|
105
238
|
statusEntryId: ctx.entity.statusEntryId ?? null,
|
|
106
239
|
isActive: ctx.entity.isActive,
|
|
107
240
|
}),
|
|
108
|
-
}
|
|
241
|
+
};
|
|
109
242
|
|
|
110
243
|
const productCrudIndexer: CrudIndexerConfig<CatalogProduct> = {
|
|
111
244
|
entityType: E.catalog.catalog_product,
|
|
@@ -121,22 +254,22 @@ const productCrudIndexer: CrudIndexerConfig<CatalogProduct> = {
|
|
|
121
254
|
tenantId: ctx.identifiers.tenantId,
|
|
122
255
|
organizationId: ctx.identifiers.organizationId,
|
|
123
256
|
}),
|
|
124
|
-
}
|
|
257
|
+
};
|
|
125
258
|
|
|
126
259
|
function buildProductCrudIdentifiers(product: CatalogProduct) {
|
|
127
260
|
return {
|
|
128
261
|
id: product.id,
|
|
129
262
|
organizationId: product.organizationId,
|
|
130
263
|
tenantId: product.tenantId,
|
|
131
|
-
}
|
|
264
|
+
};
|
|
132
265
|
}
|
|
133
266
|
|
|
134
267
|
async function emitProductCrudChange(opts: {
|
|
135
|
-
dataEngine: DataEngine
|
|
136
|
-
action: CrudEventAction
|
|
137
|
-
product: CatalogProduct
|
|
268
|
+
dataEngine: DataEngine;
|
|
269
|
+
action: CrudEventAction;
|
|
270
|
+
product: CatalogProduct;
|
|
138
271
|
}) {
|
|
139
|
-
const { dataEngine, action, product } = opts
|
|
272
|
+
const { dataEngine, action, product } = opts;
|
|
140
273
|
await emitCrudSideEffects({
|
|
141
274
|
dataEngine,
|
|
142
275
|
action,
|
|
@@ -144,15 +277,15 @@ async function emitProductCrudChange(opts: {
|
|
|
144
277
|
identifiers: buildProductCrudIdentifiers(product),
|
|
145
278
|
events: productCrudEvents,
|
|
146
279
|
indexer: productCrudIndexer,
|
|
147
|
-
})
|
|
280
|
+
});
|
|
148
281
|
}
|
|
149
282
|
|
|
150
283
|
async function emitProductCrudUndoChange(opts: {
|
|
151
|
-
dataEngine: DataEngine
|
|
152
|
-
action: CrudEventAction
|
|
153
|
-
product: CatalogProduct
|
|
284
|
+
dataEngine: DataEngine;
|
|
285
|
+
action: CrudEventAction;
|
|
286
|
+
product: CatalogProduct;
|
|
154
287
|
}) {
|
|
155
|
-
const { dataEngine, action, product } = opts
|
|
288
|
+
const { dataEngine, action, product } = opts;
|
|
156
289
|
await emitCrudUndoSideEffects({
|
|
157
290
|
dataEngine,
|
|
158
291
|
action,
|
|
@@ -160,277 +293,347 @@ async function emitProductCrudUndoChange(opts: {
|
|
|
160
293
|
identifiers: buildProductCrudIdentifiers(product),
|
|
161
294
|
events: productCrudEvents,
|
|
162
295
|
indexer: productCrudIndexer,
|
|
163
|
-
})
|
|
296
|
+
});
|
|
164
297
|
}
|
|
165
298
|
|
|
166
299
|
type OfferSnapshot = {
|
|
167
|
-
id: string
|
|
168
|
-
channelId: string
|
|
169
|
-
title: string
|
|
170
|
-
description: string | null
|
|
171
|
-
defaultMediaId: string | null
|
|
172
|
-
defaultMediaUrl: string | null
|
|
173
|
-
metadata: Record<string, unknown> | null
|
|
174
|
-
isActive: boolean
|
|
175
|
-
}
|
|
300
|
+
id: string;
|
|
301
|
+
channelId: string;
|
|
302
|
+
title: string;
|
|
303
|
+
description: string | null;
|
|
304
|
+
defaultMediaId: string | null;
|
|
305
|
+
defaultMediaUrl: string | null;
|
|
306
|
+
metadata: Record<string, unknown> | null;
|
|
307
|
+
isActive: boolean;
|
|
308
|
+
};
|
|
176
309
|
|
|
177
310
|
async function resolveScopedTaxRate(
|
|
178
311
|
em: EntityManager,
|
|
179
312
|
taxRateId: string | null | undefined,
|
|
180
313
|
taxRateInput: number | string | null | undefined,
|
|
181
314
|
organizationId: string,
|
|
182
|
-
tenantId: string
|
|
315
|
+
tenantId: string,
|
|
183
316
|
): Promise<{ taxRateId: string | null; taxRate: string | null }> {
|
|
184
317
|
const normalizedRate =
|
|
185
318
|
taxRateInput === null || taxRateInput === undefined
|
|
186
319
|
? null
|
|
187
320
|
: (() => {
|
|
188
|
-
const numeric =
|
|
189
|
-
|
|
190
|
-
|
|
321
|
+
const numeric =
|
|
322
|
+
typeof taxRateInput === "string"
|
|
323
|
+
? Number(taxRateInput)
|
|
324
|
+
: taxRateInput;
|
|
325
|
+
return Number.isFinite(numeric) ? toNumericString(numeric) : null;
|
|
326
|
+
})();
|
|
191
327
|
if (!taxRateId) {
|
|
192
|
-
return { taxRateId: null, taxRate: normalizedRate }
|
|
328
|
+
return { taxRateId: null, taxRate: normalizedRate };
|
|
193
329
|
}
|
|
194
|
-
const record = await em
|
|
330
|
+
const record = await findOneWithDecryption(em, SalesTaxRate, {
|
|
195
331
|
id: taxRateId,
|
|
196
332
|
organizationId,
|
|
197
333
|
tenantId,
|
|
198
334
|
deletedAt: null,
|
|
199
|
-
})
|
|
335
|
+
});
|
|
200
336
|
if (!record) {
|
|
201
|
-
|
|
337
|
+
const { translate } = await resolveTranslations();
|
|
338
|
+
throw new CrudHttpError(400, {
|
|
339
|
+
error: translate(
|
|
340
|
+
"catalog.products.errors.taxClassNotFound",
|
|
341
|
+
"Tax class not found",
|
|
342
|
+
),
|
|
343
|
+
});
|
|
202
344
|
}
|
|
203
|
-
return { taxRateId, taxRate: record.rate ?? normalizedRate }
|
|
345
|
+
return { taxRateId, taxRate: record.rate ?? normalizedRate };
|
|
204
346
|
}
|
|
205
347
|
|
|
206
348
|
function slugifyCode(input: string): string {
|
|
207
349
|
return input
|
|
208
350
|
.toLowerCase()
|
|
209
351
|
.trim()
|
|
210
|
-
.replace(/[^a-z0-9\-]+/g,
|
|
211
|
-
.replace(/^-+|-+$/g,
|
|
352
|
+
.replace(/[^a-z0-9\-]+/g, "-")
|
|
353
|
+
.replace(/^-+|-+$/g, "");
|
|
212
354
|
}
|
|
213
355
|
|
|
214
356
|
function normalizeCatalogOptionSchema(
|
|
215
|
-
input?: CatalogProductOptionSchema | null
|
|
357
|
+
input?: CatalogProductOptionSchema | null,
|
|
216
358
|
): CatalogProductOptionSchema | null {
|
|
217
|
-
if (!input || !Array.isArray(input.options) || !input.options.length)
|
|
359
|
+
if (!input || !Array.isArray(input.options) || !input.options.length)
|
|
360
|
+
return null;
|
|
218
361
|
const options = input.options
|
|
219
362
|
.map((option) => {
|
|
220
|
-
if (!option) return null
|
|
363
|
+
if (!option) return null;
|
|
221
364
|
const label =
|
|
222
|
-
typeof option.label ===
|
|
365
|
+
typeof option.label === "string" && option.label.trim().length
|
|
366
|
+
? option.label.trim()
|
|
367
|
+
: null;
|
|
223
368
|
const codeSource =
|
|
224
|
-
typeof option.code ===
|
|
225
|
-
|
|
226
|
-
|
|
369
|
+
typeof option.code === "string" && option.code.trim().length
|
|
370
|
+
? option.code.trim()
|
|
371
|
+
: label;
|
|
372
|
+
const code = slugifyCode(codeSource ?? "");
|
|
373
|
+
if (!label && !code) return null;
|
|
227
374
|
const choices = Array.isArray(option.choices)
|
|
228
375
|
? option.choices
|
|
229
376
|
.map((choice) => {
|
|
230
|
-
if (!choice) return null
|
|
377
|
+
if (!choice) return null;
|
|
231
378
|
const choiceLabel =
|
|
232
|
-
typeof choice.label ===
|
|
379
|
+
typeof choice.label === "string" && choice.label.trim().length
|
|
380
|
+
? choice.label.trim()
|
|
381
|
+
: null;
|
|
233
382
|
const choiceCodeSource =
|
|
234
|
-
typeof choice.code ===
|
|
383
|
+
typeof choice.code === "string" && choice.code.trim().length
|
|
235
384
|
? choice.code.trim()
|
|
236
|
-
: choiceLabel
|
|
237
|
-
const choiceCode = slugifyCode(choiceCodeSource ??
|
|
238
|
-
if (!choiceLabel && !choiceCode) return null
|
|
385
|
+
: choiceLabel;
|
|
386
|
+
const choiceCode = slugifyCode(choiceCodeSource ?? "");
|
|
387
|
+
if (!choiceLabel && !choiceCode) return null;
|
|
239
388
|
return {
|
|
240
389
|
code: choiceCode || `choice-${randomSuffix()}`,
|
|
241
|
-
label:
|
|
242
|
-
|
|
390
|
+
label:
|
|
391
|
+
choiceLabel ?? (choiceCode || `Choice ${randomSuffix()}`),
|
|
392
|
+
};
|
|
243
393
|
})
|
|
244
394
|
.filter(
|
|
245
395
|
(entry): entry is { code: string; label: string } =>
|
|
246
|
-
!!entry &&
|
|
396
|
+
!!entry &&
|
|
397
|
+
entry.code.trim().length > 0 &&
|
|
398
|
+
entry.label.trim().length > 0,
|
|
247
399
|
)
|
|
248
|
-
: []
|
|
400
|
+
: [];
|
|
249
401
|
return {
|
|
250
402
|
code: code || `option-${randomSuffix()}`,
|
|
251
403
|
label: label ?? (code || `Option ${randomSuffix()}`),
|
|
252
404
|
description:
|
|
253
|
-
typeof option.description ===
|
|
405
|
+
typeof option.description === "string" &&
|
|
406
|
+
option.description.trim().length
|
|
254
407
|
? option.description.trim()
|
|
255
408
|
: null,
|
|
256
409
|
inputType:
|
|
257
|
-
option.inputType ===
|
|
258
|
-
option.inputType ===
|
|
259
|
-
option.inputType ===
|
|
410
|
+
option.inputType === "text" ||
|
|
411
|
+
option.inputType === "textarea" ||
|
|
412
|
+
option.inputType === "number"
|
|
260
413
|
? option.inputType
|
|
261
|
-
:
|
|
414
|
+
: "select",
|
|
262
415
|
isRequired: option.isRequired ?? false,
|
|
263
416
|
isMultiple: option.isMultiple ?? false,
|
|
264
417
|
choices,
|
|
265
|
-
}
|
|
418
|
+
};
|
|
266
419
|
})
|
|
267
420
|
.filter((entry) => !!entry && entry.code.trim().length > 0) as Array<
|
|
268
|
-
CatalogProductOptionSchema[
|
|
269
|
-
|
|
270
|
-
if (!options.length) return null
|
|
421
|
+
CatalogProductOptionSchema["options"][number]
|
|
422
|
+
>;
|
|
423
|
+
if (!options.length) return null;
|
|
271
424
|
return {
|
|
272
|
-
version:
|
|
273
|
-
|
|
425
|
+
version:
|
|
426
|
+
typeof input.version === "number" && input.version > 0
|
|
427
|
+
? input.version
|
|
428
|
+
: 1,
|
|
429
|
+
name:
|
|
430
|
+
typeof input.name === "string" && input.name.trim().length
|
|
431
|
+
? input.name.trim()
|
|
432
|
+
: undefined,
|
|
274
433
|
description:
|
|
275
|
-
typeof input.description ===
|
|
434
|
+
typeof input.description === "string" && input.description.trim().length
|
|
276
435
|
? input.description.trim()
|
|
277
436
|
: undefined,
|
|
278
437
|
options,
|
|
279
|
-
}
|
|
438
|
+
};
|
|
280
439
|
}
|
|
281
440
|
|
|
282
|
-
function convertLegacyOptionSchema(
|
|
283
|
-
|
|
441
|
+
function convertLegacyOptionSchema(
|
|
442
|
+
raw: unknown,
|
|
443
|
+
): CatalogProductOptionSchema | null {
|
|
444
|
+
if (!Array.isArray(raw)) return null;
|
|
284
445
|
const options = raw
|
|
285
446
|
.map((entry) => {
|
|
286
|
-
if (!entry || typeof entry !==
|
|
287
|
-
const source = entry as Record<string, unknown
|
|
447
|
+
if (!entry || typeof entry !== "object") return null;
|
|
448
|
+
const source = entry as Record<string, unknown>;
|
|
288
449
|
const title =
|
|
289
|
-
typeof source[
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
450
|
+
typeof source["title"] === "string" &&
|
|
451
|
+
(source["title"] as string).trim().length
|
|
452
|
+
? (source["title"] as string).trim()
|
|
453
|
+
: null;
|
|
454
|
+
if (!title) return null;
|
|
455
|
+
const values = Array.isArray(source["values"])
|
|
456
|
+
? (source["values"] as unknown[])
|
|
457
|
+
.map((value: unknown) => {
|
|
458
|
+
if (!value || typeof value !== "object") return null;
|
|
459
|
+
const choice = value as Record<string, unknown>;
|
|
297
460
|
const label =
|
|
298
|
-
typeof
|
|
299
|
-
|
|
300
|
-
|
|
461
|
+
typeof choice.label === "string" && (choice.label as string).trim().length
|
|
462
|
+
? (choice.label as string).trim()
|
|
463
|
+
: null;
|
|
464
|
+
if (!label) return null;
|
|
465
|
+
return { code: slugifyCode(label), label };
|
|
301
466
|
})
|
|
302
|
-
.filter(
|
|
303
|
-
|
|
467
|
+
.filter(
|
|
468
|
+
(choice): choice is { code: string; label: string } => !!choice,
|
|
469
|
+
)
|
|
470
|
+
: [];
|
|
304
471
|
return {
|
|
305
472
|
code: slugifyCode(title),
|
|
306
473
|
label: title,
|
|
307
|
-
inputType:
|
|
474
|
+
inputType: "select" as const,
|
|
308
475
|
choices: values,
|
|
309
|
-
}
|
|
476
|
+
};
|
|
310
477
|
})
|
|
311
|
-
.filter((option) => !!option) as CatalogProductOptionSchema[
|
|
312
|
-
if (!options.length) return null
|
|
478
|
+
.filter((option) => !!option) as CatalogProductOptionSchema["options"];
|
|
479
|
+
if (!options.length) return null;
|
|
313
480
|
return {
|
|
314
481
|
version: 1,
|
|
315
482
|
options,
|
|
316
|
-
}
|
|
483
|
+
};
|
|
317
484
|
}
|
|
318
485
|
|
|
319
486
|
function extractOptionSchemaInput(source: {
|
|
320
|
-
metadata?: Record<string, unknown> | null | undefined
|
|
321
|
-
optionSchema?: CatalogProductOptionSchema | null | undefined
|
|
322
|
-
}): {
|
|
487
|
+
metadata?: Record<string, unknown> | null | undefined;
|
|
488
|
+
optionSchema?: CatalogProductOptionSchema | null | undefined;
|
|
489
|
+
}): {
|
|
490
|
+
schema: CatalogProductOptionSchema | null;
|
|
491
|
+
metadata: Record<string, unknown> | null;
|
|
492
|
+
} {
|
|
323
493
|
const metadata =
|
|
324
|
-
source.metadata && typeof source.metadata ===
|
|
494
|
+
source.metadata && typeof source.metadata === "object"
|
|
325
495
|
? { ...(source.metadata as Record<string, unknown>) }
|
|
326
|
-
: null
|
|
327
|
-
let schema = normalizeCatalogOptionSchema(source.optionSchema)
|
|
496
|
+
: null;
|
|
497
|
+
let schema = normalizeCatalogOptionSchema(source.optionSchema);
|
|
328
498
|
if (!schema && metadata) {
|
|
329
499
|
const legacy = convertLegacyOptionSchema(
|
|
330
|
-
(metadata as Record<string, unknown>)[
|
|
331
|
-
(metadata as Record<string, unknown>)[
|
|
332
|
-
)
|
|
333
|
-
schema = normalizeCatalogOptionSchema(legacy)
|
|
500
|
+
(metadata as Record<string, unknown>)["optionSchema"] ??
|
|
501
|
+
(metadata as Record<string, unknown>)["option_schema"],
|
|
502
|
+
);
|
|
503
|
+
schema = normalizeCatalogOptionSchema(legacy);
|
|
334
504
|
}
|
|
335
505
|
if (metadata) {
|
|
336
|
-
delete (metadata as Record<string, unknown>)[
|
|
337
|
-
delete (metadata as Record<string, unknown>)[
|
|
338
|
-
delete (metadata as Record<string, unknown>)[
|
|
339
|
-
delete (metadata as Record<string, unknown>)[
|
|
506
|
+
delete (metadata as Record<string, unknown>)["optionSchema"];
|
|
507
|
+
delete (metadata as Record<string, unknown>)["option_schema"];
|
|
508
|
+
delete (metadata as Record<string, unknown>)["dimensions"];
|
|
509
|
+
delete (metadata as Record<string, unknown>)["weight"];
|
|
340
510
|
}
|
|
341
511
|
return {
|
|
342
512
|
schema,
|
|
343
513
|
metadata: metadata && Object.keys(metadata).length ? metadata : null,
|
|
344
|
-
}
|
|
514
|
+
};
|
|
345
515
|
}
|
|
346
516
|
|
|
347
517
|
function parseNumeric(value: unknown): number | null {
|
|
348
|
-
const numeric = typeof value ===
|
|
349
|
-
if (!Number.isFinite(numeric) || numeric < 0) return null
|
|
350
|
-
return numeric
|
|
518
|
+
const numeric = typeof value === "number" ? value : Number(value);
|
|
519
|
+
if (!Number.isFinite(numeric) || numeric < 0) return null;
|
|
520
|
+
return numeric;
|
|
351
521
|
}
|
|
352
522
|
|
|
353
523
|
function normalizeDimensionsInput(raw: unknown): {
|
|
354
|
-
width?: number
|
|
355
|
-
height?: number
|
|
356
|
-
depth?: number
|
|
357
|
-
unit?: string
|
|
524
|
+
width?: number;
|
|
525
|
+
height?: number;
|
|
526
|
+
depth?: number;
|
|
527
|
+
unit?: string;
|
|
358
528
|
} | null {
|
|
359
|
-
const source = parseObjectLike(raw)
|
|
360
|
-
if (!source) return null
|
|
361
|
-
const clean: Record<string, unknown> = {}
|
|
362
|
-
const width = parseNumeric(source.width)
|
|
363
|
-
const height = parseNumeric(source.height)
|
|
364
|
-
const depth = parseNumeric(source.depth)
|
|
365
|
-
const unit =
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
if (
|
|
370
|
-
|
|
529
|
+
const source = parseObjectLike(raw);
|
|
530
|
+
if (!source) return null;
|
|
531
|
+
const clean: Record<string, unknown> = {};
|
|
532
|
+
const width = parseNumeric(source.width);
|
|
533
|
+
const height = parseNumeric(source.height);
|
|
534
|
+
const depth = parseNumeric(source.depth);
|
|
535
|
+
const unit =
|
|
536
|
+
typeof source.unit === "string" && source.unit.trim().length
|
|
537
|
+
? source.unit.trim()
|
|
538
|
+
: null;
|
|
539
|
+
if (width !== null) clean.width = width;
|
|
540
|
+
if (height !== null) clean.height = height;
|
|
541
|
+
if (depth !== null) clean.depth = depth;
|
|
542
|
+
if (unit) clean.unit = unit;
|
|
543
|
+
return Object.keys(clean).length
|
|
544
|
+
? (clean as {
|
|
545
|
+
width?: number;
|
|
546
|
+
height?: number;
|
|
547
|
+
depth?: number;
|
|
548
|
+
unit?: string;
|
|
549
|
+
})
|
|
550
|
+
: null;
|
|
371
551
|
}
|
|
372
552
|
|
|
373
|
-
function normalizeWeightInput(
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
553
|
+
function normalizeWeightInput(
|
|
554
|
+
raw: unknown,
|
|
555
|
+
): { value?: number; unit?: string } | null {
|
|
556
|
+
const source = parseObjectLike(raw);
|
|
557
|
+
if (!source) return null;
|
|
558
|
+
const value = parseNumeric(source.value);
|
|
559
|
+
const unit =
|
|
560
|
+
typeof source.unit === "string" && source.unit.trim().length
|
|
561
|
+
? source.unit.trim()
|
|
562
|
+
: null;
|
|
563
|
+
if (value === null && !unit) return null;
|
|
564
|
+
const clean: { value?: number; unit?: string } = {};
|
|
565
|
+
if (value !== null) clean.value = value;
|
|
566
|
+
if (unit) clean.unit = unit;
|
|
567
|
+
return clean;
|
|
383
568
|
}
|
|
384
569
|
|
|
385
|
-
function extractMeasurementsFromMetadata(
|
|
386
|
-
metadata: Record<string, unknown> | null
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
570
|
+
function extractMeasurementsFromMetadata(
|
|
571
|
+
metadata: Record<string, unknown> | null | undefined,
|
|
572
|
+
): {
|
|
573
|
+
metadata: Record<string, unknown> | null;
|
|
574
|
+
dimensions: {
|
|
575
|
+
width?: number;
|
|
576
|
+
height?: number;
|
|
577
|
+
depth?: number;
|
|
578
|
+
unit?: string;
|
|
579
|
+
} | null;
|
|
580
|
+
weightValue: number | null;
|
|
581
|
+
weightUnit: string | null;
|
|
390
582
|
} {
|
|
391
|
-
if (!metadata || typeof metadata !==
|
|
392
|
-
return {
|
|
583
|
+
if (!metadata || typeof metadata !== "object") {
|
|
584
|
+
return {
|
|
585
|
+
metadata: null,
|
|
586
|
+
dimensions: null,
|
|
587
|
+
weightValue: null,
|
|
588
|
+
weightUnit: null,
|
|
589
|
+
};
|
|
393
590
|
}
|
|
394
|
-
const clone = { ...(metadata as Record<string, unknown>) }
|
|
395
|
-
const dimensions = normalizeDimensionsInput(clone.dimensions)
|
|
396
|
-
const weight = normalizeWeightInput(clone.weight)
|
|
397
|
-
delete clone.dimensions
|
|
398
|
-
delete clone.weight
|
|
399
|
-
const cleanedMetadata = Object.keys(clone).length ? clone : null
|
|
591
|
+
const clone = { ...(metadata as Record<string, unknown>) };
|
|
592
|
+
const dimensions = normalizeDimensionsInput(clone.dimensions);
|
|
593
|
+
const weight = normalizeWeightInput(clone.weight);
|
|
594
|
+
delete clone.dimensions;
|
|
595
|
+
delete clone.weight;
|
|
596
|
+
const cleanedMetadata = Object.keys(clone).length ? clone : null;
|
|
400
597
|
return {
|
|
401
598
|
metadata: cleanedMetadata,
|
|
402
599
|
dimensions,
|
|
403
600
|
weightValue: weight?.value ?? null,
|
|
404
601
|
weightUnit: weight?.unit ?? null,
|
|
405
|
-
}
|
|
602
|
+
};
|
|
406
603
|
}
|
|
407
604
|
|
|
408
|
-
function ensureSchemaName(
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
605
|
+
function ensureSchemaName(
|
|
606
|
+
name?: string | null,
|
|
607
|
+
fallback?: string | null,
|
|
608
|
+
): string {
|
|
609
|
+
if (name && name.trim().length) return name.trim();
|
|
610
|
+
if (fallback && fallback.trim().length) return fallback.trim();
|
|
611
|
+
return "Product option schema";
|
|
412
612
|
}
|
|
413
613
|
|
|
414
614
|
async function assignOptionSchemaTemplate(
|
|
415
615
|
em: EntityManager,
|
|
416
616
|
product: CatalogProduct,
|
|
417
617
|
schema: CatalogProductOptionSchema,
|
|
418
|
-
preferredName?: string | null
|
|
618
|
+
preferredName?: string | null,
|
|
419
619
|
): Promise<CatalogOptionSchemaTemplate> {
|
|
420
|
-
const resolvedName = ensureSchemaName(
|
|
620
|
+
const resolvedName = ensureSchemaName(
|
|
621
|
+
schema.name,
|
|
622
|
+
preferredName ?? product.title,
|
|
623
|
+
);
|
|
421
624
|
const templateCode = resolveOptionSchemaCode({
|
|
422
625
|
name: schema.name ?? resolvedName,
|
|
423
626
|
fallback: `${resolvedName}-${product.id}`,
|
|
424
627
|
uniqueHint: product.id?.slice(0, 8),
|
|
425
|
-
})
|
|
426
|
-
let template = product.optionSchemaTemplate ?? null
|
|
628
|
+
});
|
|
629
|
+
let template = product.optionSchemaTemplate ?? null;
|
|
427
630
|
if (!template) {
|
|
428
631
|
template = await em.findOne(CatalogOptionSchemaTemplate, {
|
|
429
632
|
organizationId: product.organizationId,
|
|
430
633
|
tenantId: product.tenantId,
|
|
431
634
|
code: templateCode,
|
|
432
635
|
deletedAt: null,
|
|
433
|
-
})
|
|
636
|
+
});
|
|
434
637
|
}
|
|
435
638
|
if (!template) {
|
|
436
639
|
template = em.create(CatalogOptionSchemaTemplate, {
|
|
@@ -440,18 +643,18 @@ async function assignOptionSchemaTemplate(
|
|
|
440
643
|
code: templateCode,
|
|
441
644
|
description: schema.description ?? null,
|
|
442
645
|
schema: cloneJson(schema),
|
|
443
|
-
metadata: { source:
|
|
646
|
+
metadata: { source: "product" },
|
|
444
647
|
isActive: true,
|
|
445
|
-
})
|
|
446
|
-
em.persist(template)
|
|
648
|
+
});
|
|
649
|
+
em.persist(template);
|
|
447
650
|
} else {
|
|
448
|
-
template.code = templateCode
|
|
449
|
-
template.name = resolvedName
|
|
450
|
-
template.description = schema.description ?? template.description ?? null
|
|
451
|
-
template.schema = cloneJson(schema)
|
|
651
|
+
template.code = templateCode;
|
|
652
|
+
template.name = resolvedName;
|
|
653
|
+
template.description = schema.description ?? template.description ?? null;
|
|
654
|
+
template.schema = cloneJson(schema);
|
|
452
655
|
}
|
|
453
|
-
product.optionSchemaTemplate = template
|
|
454
|
-
return template
|
|
656
|
+
product.optionSchemaTemplate = template;
|
|
657
|
+
return template;
|
|
455
658
|
}
|
|
456
659
|
|
|
457
660
|
function serializeOffer(record: CatalogOffer): OfferSnapshot {
|
|
@@ -464,35 +667,38 @@ function serializeOffer(record: CatalogOffer): OfferSnapshot {
|
|
|
464
667
|
defaultMediaUrl: record.defaultMediaUrl ?? null,
|
|
465
668
|
metadata: record.metadata ? cloneJson(record.metadata) : null,
|
|
466
669
|
isActive: record.isActive,
|
|
467
|
-
}
|
|
670
|
+
};
|
|
468
671
|
}
|
|
469
672
|
|
|
470
|
-
async function loadOfferSnapshots(
|
|
673
|
+
async function loadOfferSnapshots(
|
|
674
|
+
em: EntityManager,
|
|
675
|
+
productId: string,
|
|
676
|
+
): Promise<OfferSnapshot[]> {
|
|
471
677
|
const offerRecords = await em.find(
|
|
472
678
|
CatalogOffer,
|
|
473
679
|
{ product: productId },
|
|
474
|
-
{ orderBy: { createdAt:
|
|
475
|
-
)
|
|
476
|
-
return offerRecords.map((offer) => serializeOffer(offer))
|
|
680
|
+
{ orderBy: { createdAt: "asc" } },
|
|
681
|
+
);
|
|
682
|
+
return offerRecords.map((offer) => serializeOffer(offer));
|
|
477
683
|
}
|
|
478
684
|
|
|
479
685
|
async function restoreOffersFromSnapshot(
|
|
480
686
|
em: EntityManager,
|
|
481
687
|
product: CatalogProduct,
|
|
482
|
-
snapshot: OfferSnapshot[] | null | undefined
|
|
688
|
+
snapshot: OfferSnapshot[] | null | undefined,
|
|
483
689
|
): Promise<void> {
|
|
484
|
-
const existing = await em.find(CatalogOffer, { product })
|
|
485
|
-
const keepIds = new Set<string>()
|
|
486
|
-
const list = Array.isArray(snapshot) ? snapshot : []
|
|
690
|
+
const existing = await em.find(CatalogOffer, { product });
|
|
691
|
+
const keepIds = new Set<string>();
|
|
692
|
+
const list = Array.isArray(snapshot) ? snapshot : [];
|
|
487
693
|
for (const offer of existing) {
|
|
488
694
|
if (!list.some((snap) => snap.id === offer.id)) {
|
|
489
|
-
em.remove(offer)
|
|
695
|
+
em.remove(offer);
|
|
490
696
|
} else {
|
|
491
|
-
keepIds.add(offer.id)
|
|
697
|
+
keepIds.add(offer.id);
|
|
492
698
|
}
|
|
493
699
|
}
|
|
494
700
|
for (const snap of list) {
|
|
495
|
-
let target = existing.find((entry) => entry.id === snap.id)
|
|
701
|
+
let target = existing.find((entry) => entry.id === snap.id);
|
|
496
702
|
if (!target) {
|
|
497
703
|
target = em.create(CatalogOffer, {
|
|
498
704
|
id: snap.id,
|
|
@@ -502,22 +708,22 @@ async function restoreOffersFromSnapshot(
|
|
|
502
708
|
channelId: snap.channelId,
|
|
503
709
|
title: snap.title,
|
|
504
710
|
isActive: snap.isActive,
|
|
505
|
-
})
|
|
506
|
-
em.persist(target)
|
|
711
|
+
});
|
|
712
|
+
em.persist(target);
|
|
507
713
|
}
|
|
508
|
-
target.channelId = snap.channelId
|
|
509
|
-
target.title = snap.title
|
|
510
|
-
target.description = snap.description ?? null
|
|
511
|
-
target.defaultMediaId = snap.defaultMediaId ?? null
|
|
512
|
-
target.defaultMediaUrl = snap.defaultMediaUrl ?? null
|
|
513
|
-
target.metadata = snap.metadata ? cloneJson(snap.metadata) : null
|
|
514
|
-
target.isActive = snap.isActive
|
|
515
|
-
keepIds.add(target.id)
|
|
714
|
+
target.channelId = snap.channelId;
|
|
715
|
+
target.title = snap.title;
|
|
716
|
+
target.description = snap.description ?? null;
|
|
717
|
+
target.defaultMediaId = snap.defaultMediaId ?? null;
|
|
718
|
+
target.defaultMediaUrl = snap.defaultMediaUrl ?? null;
|
|
719
|
+
target.metadata = snap.metadata ? cloneJson(snap.metadata) : null;
|
|
720
|
+
target.isActive = snap.isActive;
|
|
721
|
+
keepIds.add(target.id);
|
|
516
722
|
}
|
|
517
|
-
const toRemove = existing.filter((offer) => !keepIds.has(offer.id))
|
|
723
|
+
const toRemove = existing.filter((offer) => !keepIds.has(offer.id));
|
|
518
724
|
if (toRemove.length) {
|
|
519
725
|
for (const offer of toRemove) {
|
|
520
|
-
em.remove(offer)
|
|
726
|
+
em.remove(offer);
|
|
521
727
|
}
|
|
522
728
|
}
|
|
523
729
|
}
|
|
@@ -525,45 +731,46 @@ async function restoreOffersFromSnapshot(
|
|
|
525
731
|
async function syncOffers(
|
|
526
732
|
em: EntityManager,
|
|
527
733
|
product: CatalogProduct,
|
|
528
|
-
inputs: OfferInput[] | undefined
|
|
734
|
+
inputs: OfferInput[] | undefined,
|
|
529
735
|
): Promise<void> {
|
|
530
|
-
if (!inputs) return
|
|
531
|
-
const normalized = inputs
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
description
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
defaultMediaId
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
const
|
|
552
|
-
const
|
|
736
|
+
if (!inputs) return;
|
|
737
|
+
const normalized = inputs.map((input) => ({
|
|
738
|
+
...input,
|
|
739
|
+
title: input.title?.trim().length ? input.title.trim() : product.title,
|
|
740
|
+
description:
|
|
741
|
+
input.description != null && input.description.trim().length
|
|
742
|
+
? input.description.trim()
|
|
743
|
+
: (product.description ?? null),
|
|
744
|
+
defaultMediaId:
|
|
745
|
+
typeof input.defaultMediaId === "string" &&
|
|
746
|
+
input.defaultMediaId.trim().length
|
|
747
|
+
? input.defaultMediaId.trim()
|
|
748
|
+
: null,
|
|
749
|
+
defaultMediaUrl:
|
|
750
|
+
typeof input.defaultMediaUrl === "string" &&
|
|
751
|
+
input.defaultMediaUrl.trim().length
|
|
752
|
+
? input.defaultMediaUrl.trim()
|
|
753
|
+
: null,
|
|
754
|
+
metadata: input.metadata ? cloneJson(input.metadata) : null,
|
|
755
|
+
isActive: input.isActive !== false,
|
|
756
|
+
}));
|
|
757
|
+
const existing = await em.find(CatalogOffer, { product });
|
|
758
|
+
const claimed = new Set<string>();
|
|
759
|
+
const channelMap = new Map<string, CatalogOffer>();
|
|
553
760
|
for (const offer of existing) {
|
|
554
|
-
channelMap.set(offer.channelId, offer)
|
|
761
|
+
channelMap.set(offer.channelId, offer);
|
|
555
762
|
}
|
|
556
|
-
const updates: CatalogOffer[] = []
|
|
763
|
+
const updates: CatalogOffer[] = [];
|
|
557
764
|
for (const input of normalized) {
|
|
558
|
-
if (!input.channelId) continue
|
|
559
|
-
let target: CatalogOffer | undefined
|
|
765
|
+
if (!input.channelId) continue;
|
|
766
|
+
let target: CatalogOffer | undefined;
|
|
560
767
|
if (input.id) {
|
|
561
|
-
target = existing.find((item) => item.id === input.id)
|
|
768
|
+
target = existing.find((item) => item.id === input.id);
|
|
562
769
|
}
|
|
563
770
|
if (!target) {
|
|
564
|
-
const existingByChannel = channelMap.get(input.channelId)
|
|
771
|
+
const existingByChannel = channelMap.get(input.channelId);
|
|
565
772
|
if (existingByChannel && !claimed.has(existingByChannel.id)) {
|
|
566
|
-
target = existingByChannel
|
|
773
|
+
target = existingByChannel;
|
|
567
774
|
}
|
|
568
775
|
}
|
|
569
776
|
if (!target) {
|
|
@@ -574,65 +781,65 @@ async function syncOffers(
|
|
|
574
781
|
channelId: input.channelId,
|
|
575
782
|
title: input.title || product.title,
|
|
576
783
|
isActive: input.isActive !== false,
|
|
577
|
-
})
|
|
578
|
-
em.persist(target)
|
|
579
|
-
existing.push(target)
|
|
580
|
-
channelMap.set(input.channelId, target)
|
|
784
|
+
});
|
|
785
|
+
em.persist(target);
|
|
786
|
+
existing.push(target);
|
|
787
|
+
channelMap.set(input.channelId, target);
|
|
581
788
|
}
|
|
582
|
-
target.channelId = input.channelId
|
|
583
|
-
target.title = input.title || product.title
|
|
584
|
-
target.description = input.description ?? null
|
|
585
|
-
target.defaultMediaId = input.defaultMediaId ?? null
|
|
586
|
-
target.defaultMediaUrl = input.defaultMediaUrl ?? null
|
|
587
|
-
target.metadata = input.metadata ? cloneJson(input.metadata) : null
|
|
588
|
-
target.isActive = input.isActive !== false
|
|
589
|
-
claimed.add(target.id)
|
|
590
|
-
updates.push(target)
|
|
789
|
+
target.channelId = input.channelId;
|
|
790
|
+
target.title = input.title || product.title;
|
|
791
|
+
target.description = input.description ?? null;
|
|
792
|
+
target.defaultMediaId = input.defaultMediaId ?? null;
|
|
793
|
+
target.defaultMediaUrl = input.defaultMediaUrl ?? null;
|
|
794
|
+
target.metadata = input.metadata ? cloneJson(input.metadata) : null;
|
|
795
|
+
target.isActive = input.isActive !== false;
|
|
796
|
+
claimed.add(target.id);
|
|
797
|
+
updates.push(target);
|
|
591
798
|
}
|
|
592
|
-
const toRemove = existing.filter((offer) => !claimed.has(offer.id))
|
|
799
|
+
const toRemove = existing.filter((offer) => !claimed.has(offer.id));
|
|
593
800
|
for (const offer of toRemove) {
|
|
594
|
-
em.remove(offer)
|
|
801
|
+
em.remove(offer);
|
|
595
802
|
}
|
|
596
803
|
}
|
|
597
804
|
|
|
598
805
|
async function syncCategoryAssignments(
|
|
599
806
|
em: EntityManager,
|
|
600
807
|
product: CatalogProduct,
|
|
601
|
-
categoryIds: string[] | undefined
|
|
808
|
+
categoryIds: string[] | undefined,
|
|
602
809
|
): Promise<void> {
|
|
603
810
|
const normalized = Array.from(
|
|
604
811
|
new Set(
|
|
605
812
|
(Array.isArray(categoryIds) ? categoryIds : [])
|
|
606
|
-
.map((id) => (typeof id ===
|
|
607
|
-
.filter((id) => id.length)
|
|
608
|
-
)
|
|
609
|
-
)
|
|
610
|
-
const existing = await em.find(CatalogProductCategoryAssignment, { product })
|
|
813
|
+
.map((id) => (typeof id === "string" ? id.trim() : ""))
|
|
814
|
+
.filter((id) => id.length),
|
|
815
|
+
),
|
|
816
|
+
);
|
|
817
|
+
const existing = await em.find(CatalogProductCategoryAssignment, { product });
|
|
611
818
|
if (!normalized.length) {
|
|
612
819
|
if (existing.length) {
|
|
613
820
|
for (const assignment of existing) {
|
|
614
|
-
em.remove(assignment)
|
|
821
|
+
em.remove(assignment);
|
|
615
822
|
}
|
|
616
823
|
}
|
|
617
|
-
return
|
|
824
|
+
return;
|
|
618
825
|
}
|
|
619
|
-
const categories = await em.find(
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
)
|
|
627
|
-
const
|
|
628
|
-
const claimed = new Set<string>()
|
|
826
|
+
const categories = await em.find(CatalogProductCategory, {
|
|
827
|
+
id: { $in: normalized },
|
|
828
|
+
organizationId: product.organizationId,
|
|
829
|
+
tenantId: product.tenantId,
|
|
830
|
+
});
|
|
831
|
+
const categoryMap = new Map(
|
|
832
|
+
categories.map((category) => [category.id, category]),
|
|
833
|
+
);
|
|
834
|
+
const claimed = new Set<string>();
|
|
629
835
|
normalized.forEach((categoryId, index) => {
|
|
630
|
-
const category = categoryMap.get(categoryId)
|
|
631
|
-
if (!category) return
|
|
836
|
+
const category = categoryMap.get(categoryId);
|
|
837
|
+
if (!category) return;
|
|
632
838
|
let assignment = existing.find((item) => {
|
|
633
|
-
const value =
|
|
634
|
-
|
|
635
|
-
|
|
839
|
+
const value =
|
|
840
|
+
typeof item.category === "string" ? item.category : item.category?.id;
|
|
841
|
+
return value === categoryId;
|
|
842
|
+
});
|
|
636
843
|
if (!assignment) {
|
|
637
844
|
assignment = em.create(CatalogProductCategoryAssignment, {
|
|
638
845
|
product,
|
|
@@ -640,16 +847,16 @@ async function syncCategoryAssignments(
|
|
|
640
847
|
organizationId: product.organizationId,
|
|
641
848
|
tenantId: product.tenantId,
|
|
642
849
|
position: index,
|
|
643
|
-
})
|
|
644
|
-
em.persist(assignment)
|
|
645
|
-
existing.push(assignment)
|
|
850
|
+
});
|
|
851
|
+
em.persist(assignment);
|
|
852
|
+
existing.push(assignment);
|
|
646
853
|
}
|
|
647
|
-
assignment.position = index
|
|
648
|
-
claimed.add(assignment.id)
|
|
649
|
-
})
|
|
854
|
+
assignment.position = index;
|
|
855
|
+
claimed.add(assignment.id);
|
|
856
|
+
});
|
|
650
857
|
for (const assignment of existing) {
|
|
651
858
|
if (!claimed.has(assignment.id)) {
|
|
652
|
-
em.remove(assignment)
|
|
859
|
+
em.remove(assignment);
|
|
653
860
|
}
|
|
654
861
|
}
|
|
655
862
|
}
|
|
@@ -657,102 +864,99 @@ async function syncCategoryAssignments(
|
|
|
657
864
|
async function syncProductTags(
|
|
658
865
|
em: EntityManager,
|
|
659
866
|
product: CatalogProduct,
|
|
660
|
-
tags: string[] | undefined
|
|
867
|
+
tags: string[] | undefined,
|
|
661
868
|
): Promise<void> {
|
|
662
|
-
const labelMap = new Map<string, string>()
|
|
869
|
+
const labelMap = new Map<string, string>();
|
|
663
870
|
if (Array.isArray(tags)) {
|
|
664
871
|
tags.forEach((raw) => {
|
|
665
|
-
const label = typeof raw ===
|
|
666
|
-
if (!label) return
|
|
667
|
-
const slug = slugifyTagLabel(label)
|
|
872
|
+
const label = typeof raw === "string" ? raw.trim() : "";
|
|
873
|
+
if (!label) return;
|
|
874
|
+
const slug = slugifyTagLabel(label);
|
|
668
875
|
if (!labelMap.has(slug)) {
|
|
669
|
-
labelMap.set(slug, label)
|
|
876
|
+
labelMap.set(slug, label);
|
|
670
877
|
}
|
|
671
|
-
})
|
|
878
|
+
});
|
|
672
879
|
}
|
|
673
|
-
const slugs = Array.from(labelMap.keys())
|
|
880
|
+
const slugs = Array.from(labelMap.keys());
|
|
674
881
|
const existingAssignments = await findWithDecryption(
|
|
675
882
|
em,
|
|
676
883
|
CatalogProductTagAssignment,
|
|
677
884
|
{ product },
|
|
678
|
-
{ populate: [
|
|
885
|
+
{ populate: ["tag"] },
|
|
679
886
|
{ tenantId: product.tenantId, organizationId: product.organizationId },
|
|
680
|
-
)
|
|
887
|
+
);
|
|
681
888
|
if (!slugs.length) {
|
|
682
889
|
if (existingAssignments.length) {
|
|
683
890
|
for (const assignment of existingAssignments) {
|
|
684
|
-
em.remove(assignment)
|
|
891
|
+
em.remove(assignment);
|
|
685
892
|
}
|
|
686
893
|
}
|
|
687
|
-
return
|
|
894
|
+
return;
|
|
688
895
|
}
|
|
689
|
-
const existingTags = await em.find(
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
}
|
|
696
|
-
)
|
|
697
|
-
const tagsBySlug = new Map(existingTags.map((tag) => [tag.slug, tag]))
|
|
896
|
+
const existingTags = await em.find(CatalogProductTag, {
|
|
897
|
+
organizationId: product.organizationId,
|
|
898
|
+
tenantId: product.tenantId,
|
|
899
|
+
slug: { $in: slugs },
|
|
900
|
+
});
|
|
901
|
+
const tagsBySlug = new Map(existingTags.map((tag) => [tag.slug, tag]));
|
|
698
902
|
for (const slug of slugs) {
|
|
699
|
-
if (tagsBySlug.has(slug)) continue
|
|
700
|
-
const label = labelMap.get(slug) ?? slug
|
|
903
|
+
if (tagsBySlug.has(slug)) continue;
|
|
904
|
+
const label = labelMap.get(slug) ?? slug;
|
|
701
905
|
const tag = em.create(CatalogProductTag, {
|
|
702
906
|
organizationId: product.organizationId,
|
|
703
907
|
tenantId: product.tenantId,
|
|
704
908
|
slug,
|
|
705
909
|
label,
|
|
706
|
-
})
|
|
707
|
-
em.persist(tag)
|
|
708
|
-
tagsBySlug.set(slug, tag)
|
|
910
|
+
});
|
|
911
|
+
em.persist(tag);
|
|
912
|
+
tagsBySlug.set(slug, tag);
|
|
709
913
|
}
|
|
710
914
|
const assignmentByTagId = new Map(
|
|
711
915
|
existingAssignments.map((assignment) => [
|
|
712
|
-
typeof assignment.tag ===
|
|
916
|
+
typeof assignment.tag === "string" ? assignment.tag : assignment.tag.id,
|
|
713
917
|
assignment,
|
|
714
|
-
])
|
|
715
|
-
)
|
|
716
|
-
const keepIds = new Set<string>()
|
|
918
|
+
]),
|
|
919
|
+
);
|
|
920
|
+
const keepIds = new Set<string>();
|
|
717
921
|
for (const slug of slugs) {
|
|
718
|
-
const tag = tagsBySlug.get(slug)
|
|
719
|
-
if (!tag) continue
|
|
720
|
-
const tagId = tag.id
|
|
721
|
-
let assignment = assignmentByTagId.get(tagId)
|
|
922
|
+
const tag = tagsBySlug.get(slug);
|
|
923
|
+
if (!tag) continue;
|
|
924
|
+
const tagId = tag.id;
|
|
925
|
+
let assignment = assignmentByTagId.get(tagId);
|
|
722
926
|
if (!assignment) {
|
|
723
927
|
assignment = em.create(CatalogProductTagAssignment, {
|
|
724
928
|
product,
|
|
725
929
|
tag,
|
|
726
930
|
organizationId: product.organizationId,
|
|
727
931
|
tenantId: product.tenantId,
|
|
728
|
-
})
|
|
729
|
-
em.persist(assignment)
|
|
932
|
+
});
|
|
933
|
+
em.persist(assignment);
|
|
730
934
|
}
|
|
731
|
-
keepIds.add(assignment.id)
|
|
935
|
+
keepIds.add(assignment.id);
|
|
732
936
|
}
|
|
733
937
|
for (const assignment of existingAssignments) {
|
|
734
938
|
if (!keepIds.has(assignment.id)) {
|
|
735
|
-
em.remove(assignment)
|
|
939
|
+
em.remove(assignment);
|
|
736
940
|
}
|
|
737
941
|
}
|
|
738
942
|
}
|
|
739
943
|
|
|
740
944
|
type VariantCleanupSnapshot = {
|
|
741
|
-
id: string
|
|
742
|
-
organizationId: string
|
|
743
|
-
tenantId: string
|
|
744
|
-
custom: Record<string, unknown> | null
|
|
745
|
-
}
|
|
945
|
+
id: string;
|
|
946
|
+
organizationId: string;
|
|
947
|
+
tenantId: string;
|
|
948
|
+
custom: Record<string, unknown> | null;
|
|
949
|
+
};
|
|
746
950
|
|
|
747
951
|
async function deleteProductVariantsAndRelatedData(opts: {
|
|
748
|
-
em: EntityManager
|
|
749
|
-
product: CatalogProduct
|
|
750
|
-
dataEngine: DataEngine
|
|
751
|
-
ctx: CommandRuntimeContext
|
|
952
|
+
em: EntityManager;
|
|
953
|
+
product: CatalogProduct;
|
|
954
|
+
dataEngine: DataEngine;
|
|
955
|
+
ctx: CommandRuntimeContext;
|
|
752
956
|
}): Promise<void> {
|
|
753
|
-
const { em, product, dataEngine, ctx } = opts
|
|
754
|
-
const variants = await em.find(CatalogProductVariant, { product })
|
|
755
|
-
if (!variants.length) return
|
|
957
|
+
const { em, product, dataEngine, ctx } = opts;
|
|
958
|
+
const variants = await em.find(CatalogProductVariant, { product });
|
|
959
|
+
if (!variants.length) return;
|
|
756
960
|
const cleanupEntries: VariantCleanupSnapshot[] = await Promise.all(
|
|
757
961
|
variants.map(async (variant) => {
|
|
758
962
|
const custom = await loadCustomFieldSnapshot(em, {
|
|
@@ -760,27 +964,29 @@ async function deleteProductVariantsAndRelatedData(opts: {
|
|
|
760
964
|
recordId: variant.id,
|
|
761
965
|
organizationId: variant.organizationId,
|
|
762
966
|
tenantId: variant.tenantId,
|
|
763
|
-
})
|
|
967
|
+
});
|
|
764
968
|
return {
|
|
765
969
|
id: variant.id,
|
|
766
970
|
organizationId: variant.organizationId,
|
|
767
971
|
tenantId: variant.tenantId,
|
|
768
972
|
custom: Object.keys(custom).length ? custom : null,
|
|
769
|
-
}
|
|
770
|
-
})
|
|
771
|
-
)
|
|
772
|
-
const variantIds = variants.map((variant) => variant.id)
|
|
973
|
+
};
|
|
974
|
+
}),
|
|
975
|
+
);
|
|
976
|
+
const variantIds = variants.map((variant) => variant.id);
|
|
773
977
|
if (variantIds.length) {
|
|
774
|
-
await em.nativeDelete(CatalogProductPrice, {
|
|
978
|
+
await em.nativeDelete(CatalogProductPrice, {
|
|
979
|
+
variant: { $in: variantIds },
|
|
980
|
+
});
|
|
775
981
|
}
|
|
776
982
|
for (const variant of variants) {
|
|
777
|
-
em.remove(variant)
|
|
983
|
+
em.remove(variant);
|
|
778
984
|
}
|
|
779
|
-
await em.flush()
|
|
985
|
+
await em.flush();
|
|
780
986
|
for (const cleanup of cleanupEntries) {
|
|
781
|
-
if (!cleanup.custom) continue
|
|
782
|
-
const resetValues = buildCustomFieldResetMap(cleanup.custom, undefined)
|
|
783
|
-
if (!Object.keys(resetValues).length) continue
|
|
987
|
+
if (!cleanup.custom) continue;
|
|
988
|
+
const resetValues = buildCustomFieldResetMap(cleanup.custom, undefined);
|
|
989
|
+
if (!Object.keys(resetValues).length) continue;
|
|
784
990
|
await setCustomFieldsIfAny({
|
|
785
991
|
dataEngine,
|
|
786
992
|
entityId: E.catalog.catalog_product_variant,
|
|
@@ -788,7 +994,7 @@ async function deleteProductVariantsAndRelatedData(opts: {
|
|
|
788
994
|
organizationId: cleanup.organizationId,
|
|
789
995
|
tenantId: cleanup.tenantId,
|
|
790
996
|
values: resetValues,
|
|
791
|
-
})
|
|
997
|
+
});
|
|
792
998
|
}
|
|
793
999
|
for (const cleanup of cleanupEntries) {
|
|
794
1000
|
await emitCatalogQueryIndexEvent(ctx, {
|
|
@@ -796,100 +1002,112 @@ async function deleteProductVariantsAndRelatedData(opts: {
|
|
|
796
1002
|
recordId: cleanup.id,
|
|
797
1003
|
organizationId: cleanup.organizationId,
|
|
798
1004
|
tenantId: cleanup.tenantId,
|
|
799
|
-
action:
|
|
800
|
-
})
|
|
1005
|
+
action: "deleted",
|
|
1006
|
+
});
|
|
801
1007
|
}
|
|
802
1008
|
}
|
|
803
1009
|
|
|
804
1010
|
function isProductOwnedOptionSchemaTemplate(
|
|
805
|
-
template: CatalogOptionSchemaTemplate | string | null | undefined
|
|
1011
|
+
template: CatalogOptionSchemaTemplate | string | null | undefined,
|
|
806
1012
|
): template is CatalogOptionSchemaTemplate {
|
|
807
|
-
if (!template || typeof template ===
|
|
808
|
-
const metadata = template.metadata
|
|
809
|
-
if (!metadata || typeof metadata !==
|
|
810
|
-
const source = (metadata as Record<string, unknown>).source
|
|
811
|
-
return source ===
|
|
1013
|
+
if (!template || typeof template === "string") return false;
|
|
1014
|
+
const metadata = template.metadata;
|
|
1015
|
+
if (!metadata || typeof metadata !== "object") return false;
|
|
1016
|
+
const source = (metadata as Record<string, unknown>).source;
|
|
1017
|
+
return source === "product";
|
|
812
1018
|
}
|
|
813
1019
|
|
|
814
1020
|
async function resolveOptionSchemaTemplateForRemoval(
|
|
815
1021
|
em: EntityManager,
|
|
816
|
-
product: CatalogProduct
|
|
1022
|
+
product: CatalogProduct,
|
|
817
1023
|
): Promise<CatalogOptionSchemaTemplate | null> {
|
|
818
|
-
const template = product.optionSchemaTemplate
|
|
1024
|
+
const template = product.optionSchemaTemplate;
|
|
819
1025
|
if (!isProductOwnedOptionSchemaTemplate(template)) {
|
|
820
|
-
return null
|
|
1026
|
+
return null;
|
|
821
1027
|
}
|
|
822
1028
|
const otherUsage = await em.count(CatalogProduct, {
|
|
823
1029
|
optionSchemaTemplate: template,
|
|
824
1030
|
id: { $ne: product.id },
|
|
825
1031
|
deletedAt: null,
|
|
826
|
-
})
|
|
827
|
-
if (otherUsage > 0) return null
|
|
828
|
-
return template
|
|
1032
|
+
});
|
|
1033
|
+
if (otherUsage > 0) return null;
|
|
1034
|
+
return template;
|
|
829
1035
|
}
|
|
830
1036
|
|
|
831
1037
|
async function loadProductSnapshot(
|
|
832
1038
|
em: EntityManager,
|
|
833
|
-
id: string
|
|
1039
|
+
id: string,
|
|
834
1040
|
): Promise<ProductSnapshot | null> {
|
|
835
1041
|
const record = await findOneWithDecryption(
|
|
836
1042
|
em,
|
|
837
1043
|
CatalogProduct,
|
|
838
1044
|
{ id, deletedAt: null },
|
|
839
|
-
{ populate: [
|
|
840
|
-
)
|
|
841
|
-
if (!record) return null
|
|
1045
|
+
{ populate: ["optionSchemaTemplate"] },
|
|
1046
|
+
);
|
|
1047
|
+
if (!record) return null;
|
|
842
1048
|
const [offers, tagAssignments, categoryAssignments] = await Promise.all([
|
|
843
1049
|
loadOfferSnapshots(em, record.id),
|
|
844
1050
|
findWithDecryption(
|
|
845
1051
|
em,
|
|
846
1052
|
CatalogProductTagAssignment,
|
|
847
1053
|
{ product: record.id },
|
|
848
|
-
{ populate: [
|
|
1054
|
+
{ populate: ["tag"] },
|
|
1055
|
+
{ tenantId: record.tenantId, organizationId: record.organizationId },
|
|
1056
|
+
),
|
|
1057
|
+
findWithDecryption(
|
|
1058
|
+
em,
|
|
1059
|
+
CatalogProductCategoryAssignment,
|
|
1060
|
+
{ product: record.id },
|
|
1061
|
+
{ populate: ["category"] },
|
|
849
1062
|
{ tenantId: record.tenantId, organizationId: record.organizationId },
|
|
850
1063
|
),
|
|
851
|
-
|
|
852
|
-
])
|
|
1064
|
+
]);
|
|
853
1065
|
const tags = tagAssignments
|
|
854
1066
|
.map((assignment) => {
|
|
855
1067
|
const tag =
|
|
856
|
-
typeof assignment.tag ===
|
|
857
|
-
const label = tag?.label ?? null
|
|
858
|
-
return typeof label ===
|
|
1068
|
+
typeof assignment.tag === "string" ? null : (assignment.tag ?? null);
|
|
1069
|
+
const label = tag?.label ?? null;
|
|
1070
|
+
return typeof label === "string" && label.trim().length ? label : null;
|
|
859
1071
|
})
|
|
860
1072
|
.filter((label): label is string => !!label)
|
|
861
|
-
.sort((a, b) => a.localeCompare(b))
|
|
1073
|
+
.sort((a, b) => a.localeCompare(b));
|
|
862
1074
|
const categoryIds = categoryAssignments
|
|
863
1075
|
.slice()
|
|
864
1076
|
.sort((a, b) => (a.position ?? 0) - (b.position ?? 0))
|
|
865
1077
|
.map((assignment) => {
|
|
866
|
-
if (typeof assignment.category ===
|
|
867
|
-
return assignment.category?.id ?? null
|
|
1078
|
+
if (typeof assignment.category === "string") return assignment.category;
|
|
1079
|
+
return assignment.category?.id ?? null;
|
|
868
1080
|
})
|
|
869
|
-
.filter((value): value is string => !!value)
|
|
1081
|
+
.filter((value): value is string => !!value);
|
|
870
1082
|
const custom = await loadCustomFieldSnapshot(em, {
|
|
871
1083
|
entityId: E.catalog.catalog_product,
|
|
872
1084
|
recordId: record.id,
|
|
873
1085
|
tenantId: record.tenantId,
|
|
874
1086
|
organizationId: record.organizationId,
|
|
875
|
-
})
|
|
876
|
-
const optionSchemaTemplate = record.optionSchemaTemplate
|
|
1087
|
+
});
|
|
1088
|
+
const optionSchemaTemplate = record.optionSchemaTemplate;
|
|
877
1089
|
const optionTemplateId =
|
|
878
|
-
typeof optionSchemaTemplate ===
|
|
1090
|
+
typeof optionSchemaTemplate === "string"
|
|
879
1091
|
? optionSchemaTemplate
|
|
880
|
-
: optionSchemaTemplate?.id ?? null
|
|
881
|
-
const measurements = extractMeasurementsFromMetadata(
|
|
1092
|
+
: (optionSchemaTemplate?.id ?? null);
|
|
1093
|
+
const measurements = extractMeasurementsFromMetadata(
|
|
1094
|
+
record.metadata ? cloneJson(record.metadata) : null,
|
|
1095
|
+
);
|
|
882
1096
|
const dimensions =
|
|
883
1097
|
record.dimensions && Object.keys(record.dimensions).length
|
|
884
1098
|
? cloneJson(record.dimensions)
|
|
885
1099
|
: measurements.dimensions
|
|
886
1100
|
? cloneJson(measurements.dimensions)
|
|
887
|
-
: null
|
|
1101
|
+
: null;
|
|
888
1102
|
const weightValue =
|
|
889
1103
|
record.weightValue ??
|
|
890
|
-
(measurements.weightValue !== null
|
|
891
|
-
|
|
892
|
-
|
|
1104
|
+
(measurements.weightValue !== null
|
|
1105
|
+
? toNumericString(measurements.weightValue)
|
|
1106
|
+
: null);
|
|
1107
|
+
const weightUnit = record.weightUnit ?? measurements.weightUnit ?? null;
|
|
1108
|
+
const metadata = measurements.metadata
|
|
1109
|
+
? cloneJson(measurements.metadata)
|
|
1110
|
+
: null;
|
|
893
1111
|
return {
|
|
894
1112
|
id: record.id,
|
|
895
1113
|
organizationId: record.organizationId,
|
|
@@ -905,6 +1123,13 @@ async function loadProductSnapshot(
|
|
|
905
1123
|
statusEntryId: record.statusEntryId ?? null,
|
|
906
1124
|
primaryCurrencyCode: record.primaryCurrencyCode ?? null,
|
|
907
1125
|
defaultUnit: record.defaultUnit ?? null,
|
|
1126
|
+
defaultSalesUnit: record.defaultSalesUnit ?? null,
|
|
1127
|
+
defaultSalesUnitQuantity: record.defaultSalesUnitQuantity ?? "1",
|
|
1128
|
+
uomRoundingScale: record.uomRoundingScale ?? 4,
|
|
1129
|
+
uomRoundingMode: record.uomRoundingMode ?? "half_up",
|
|
1130
|
+
unitPriceEnabled: record.unitPriceEnabled ?? false,
|
|
1131
|
+
unitPriceReferenceUnit: record.unitPriceReferenceUnit ?? null,
|
|
1132
|
+
unitPriceBaseQuantity: record.unitPriceBaseQuantity ?? null,
|
|
908
1133
|
defaultMediaId: record.defaultMediaId ?? null,
|
|
909
1134
|
defaultMediaUrl: record.defaultMediaUrl ?? null,
|
|
910
1135
|
weightValue,
|
|
@@ -921,71 +1146,101 @@ async function loadProductSnapshot(
|
|
|
921
1146
|
tags,
|
|
922
1147
|
categoryIds,
|
|
923
1148
|
custom: Object.keys(custom).length ? custom : null,
|
|
924
|
-
}
|
|
1149
|
+
};
|
|
925
1150
|
}
|
|
926
1151
|
|
|
927
1152
|
function applyProductSnapshot(
|
|
928
1153
|
em: EntityManager,
|
|
929
1154
|
record: CatalogProduct,
|
|
930
|
-
snapshot: ProductSnapshot
|
|
1155
|
+
snapshot: ProductSnapshot,
|
|
931
1156
|
): void {
|
|
932
|
-
record.organizationId = snapshot.organizationId
|
|
933
|
-
record.tenantId = snapshot.tenantId
|
|
934
|
-
record.title = snapshot.title
|
|
935
|
-
record.subtitle = snapshot.subtitle ?? null
|
|
936
|
-
record.description = snapshot.description ?? null
|
|
937
|
-
record.sku = snapshot.sku ?? null
|
|
938
|
-
record.handle = snapshot.handle ?? null
|
|
939
|
-
record.taxRateId = snapshot.taxRateId ?? null
|
|
940
|
-
record.taxRate = snapshot.taxRate ?? null
|
|
941
|
-
record.productType = snapshot.productType
|
|
942
|
-
record.statusEntryId = snapshot.statusEntryId ?? null
|
|
943
|
-
record.primaryCurrencyCode = snapshot.primaryCurrencyCode ?? null
|
|
944
|
-
record.defaultUnit = snapshot.defaultUnit ?? null
|
|
945
|
-
record.
|
|
946
|
-
record.
|
|
947
|
-
record.
|
|
948
|
-
record.
|
|
949
|
-
record.
|
|
950
|
-
record.
|
|
951
|
-
record.
|
|
1157
|
+
record.organizationId = snapshot.organizationId;
|
|
1158
|
+
record.tenantId = snapshot.tenantId;
|
|
1159
|
+
record.title = snapshot.title;
|
|
1160
|
+
record.subtitle = snapshot.subtitle ?? null;
|
|
1161
|
+
record.description = snapshot.description ?? null;
|
|
1162
|
+
record.sku = snapshot.sku ?? null;
|
|
1163
|
+
record.handle = snapshot.handle ?? null;
|
|
1164
|
+
record.taxRateId = snapshot.taxRateId ?? null;
|
|
1165
|
+
record.taxRate = snapshot.taxRate ?? null;
|
|
1166
|
+
record.productType = snapshot.productType;
|
|
1167
|
+
record.statusEntryId = snapshot.statusEntryId ?? null;
|
|
1168
|
+
record.primaryCurrencyCode = snapshot.primaryCurrencyCode ?? null;
|
|
1169
|
+
record.defaultUnit = snapshot.defaultUnit ?? null;
|
|
1170
|
+
record.defaultSalesUnit = snapshot.defaultSalesUnit ?? null;
|
|
1171
|
+
record.defaultSalesUnitQuantity = snapshot.defaultSalesUnitQuantity ?? "1";
|
|
1172
|
+
record.uomRoundingScale = snapshot.uomRoundingScale;
|
|
1173
|
+
record.uomRoundingMode = snapshot.uomRoundingMode;
|
|
1174
|
+
record.unitPriceEnabled = snapshot.unitPriceEnabled;
|
|
1175
|
+
record.unitPriceReferenceUnit = snapshot.unitPriceReferenceUnit ?? null;
|
|
1176
|
+
record.unitPriceBaseQuantity = snapshot.unitPriceBaseQuantity ?? null;
|
|
1177
|
+
record.defaultMediaId = snapshot.defaultMediaId ?? null;
|
|
1178
|
+
record.defaultMediaUrl = snapshot.defaultMediaUrl ?? null;
|
|
1179
|
+
record.weightValue = snapshot.weightValue ?? null;
|
|
1180
|
+
record.weightUnit = snapshot.weightUnit ?? null;
|
|
1181
|
+
record.dimensions = snapshot.dimensions
|
|
1182
|
+
? cloneJson(snapshot.dimensions)
|
|
1183
|
+
: null;
|
|
1184
|
+
record.metadata = snapshot.metadata ? cloneJson(snapshot.metadata) : null;
|
|
1185
|
+
record.customFieldsetCode = snapshot.customFieldsetCode ?? null;
|
|
952
1186
|
record.optionSchemaTemplate = snapshot.optionSchemaId
|
|
953
1187
|
? em.getReference(CatalogOptionSchemaTemplate, snapshot.optionSchemaId)
|
|
954
|
-
: null
|
|
955
|
-
record.isConfigurable = snapshot.isConfigurable
|
|
956
|
-
record.isActive = snapshot.isActive
|
|
957
|
-
record.createdAt = new Date(snapshot.createdAt)
|
|
958
|
-
record.updatedAt = new Date(snapshot.updatedAt)
|
|
1188
|
+
: null;
|
|
1189
|
+
record.isConfigurable = snapshot.isConfigurable;
|
|
1190
|
+
record.isActive = snapshot.isActive;
|
|
1191
|
+
record.createdAt = new Date(snapshot.createdAt);
|
|
1192
|
+
record.updatedAt = new Date(snapshot.updatedAt);
|
|
959
1193
|
}
|
|
960
1194
|
|
|
961
|
-
const createProductCommand: CommandHandler<
|
|
962
|
-
|
|
1195
|
+
const createProductCommand: CommandHandler<
|
|
1196
|
+
ProductCreateInput,
|
|
1197
|
+
{ productId: string }
|
|
1198
|
+
> = {
|
|
1199
|
+
id: "catalog.products.create",
|
|
963
1200
|
async execute(rawInput, ctx) {
|
|
964
|
-
const { parsed, custom } = parseWithCustomFields(
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
1201
|
+
const { parsed, custom } = parseWithCustomFields(
|
|
1202
|
+
productCreateSchema,
|
|
1203
|
+
rawInput,
|
|
1204
|
+
);
|
|
1205
|
+
ensureTenantScope(ctx, parsed.tenantId);
|
|
1206
|
+
ensureOrganizationScope(ctx, parsed.organizationId);
|
|
1207
|
+
const em = (ctx.container.resolve("em") as EntityManager).fork();
|
|
1208
|
+
const { translate } = await resolveTranslations();
|
|
1209
|
+
const now = new Date();
|
|
969
1210
|
const { taxRateId, taxRate } = await resolveScopedTaxRate(
|
|
970
1211
|
em,
|
|
971
1212
|
parsed.taxRateId ?? null,
|
|
972
1213
|
parsed.taxRate,
|
|
973
1214
|
parsed.organizationId,
|
|
974
|
-
parsed.tenantId
|
|
975
|
-
)
|
|
976
|
-
const { schema: optionSchemaDefinition, metadata: sanitizedMetadata } =
|
|
977
|
-
|
|
978
|
-
const
|
|
1215
|
+
parsed.tenantId,
|
|
1216
|
+
);
|
|
1217
|
+
const { schema: optionSchemaDefinition, metadata: sanitizedMetadata } =
|
|
1218
|
+
extractOptionSchemaInput(parsed);
|
|
1219
|
+
const measurements = extractMeasurementsFromMetadata(sanitizedMetadata);
|
|
1220
|
+
const dimensions =
|
|
1221
|
+
normalizeDimensionsInput(parsed.dimensions) ?? measurements.dimensions;
|
|
979
1222
|
const weightValue =
|
|
980
1223
|
parsed.weightValue !== undefined
|
|
981
1224
|
? toNumericString(parsed.weightValue)
|
|
982
1225
|
: measurements.weightValue !== null
|
|
983
1226
|
? toNumericString(measurements.weightValue)
|
|
984
|
-
: null
|
|
1227
|
+
: null;
|
|
985
1228
|
const weightUnit =
|
|
986
|
-
parsed.weightUnit !== undefined
|
|
987
|
-
|
|
988
|
-
|
|
1229
|
+
parsed.weightUnit !== undefined
|
|
1230
|
+
? (parsed.weightUnit ?? null)
|
|
1231
|
+
: (measurements.weightUnit ?? null);
|
|
1232
|
+
const metadata = measurements.metadata
|
|
1233
|
+
? cloneJson(measurements.metadata)
|
|
1234
|
+
: null;
|
|
1235
|
+
const unitPriceInput = resolveUnitPriceInput(parsed);
|
|
1236
|
+
const unitPriceEnabled = unitPriceInput.enabled ?? false;
|
|
1237
|
+
const resolvedUnits = await resolveProductUnitDefaults(em, {
|
|
1238
|
+
organizationId: parsed.organizationId,
|
|
1239
|
+
tenantId: parsed.tenantId,
|
|
1240
|
+
defaultUnit: parsed.defaultUnit ?? null,
|
|
1241
|
+
defaultSalesUnit: parsed.defaultSalesUnit ?? parsed.defaultUnit ?? null,
|
|
1242
|
+
});
|
|
1243
|
+
const productId = randomUUID();
|
|
989
1244
|
const record = em.create(CatalogProduct, {
|
|
990
1245
|
id: productId,
|
|
991
1246
|
organizationId: parsed.organizationId,
|
|
@@ -997,10 +1252,23 @@ const createProductCommand: CommandHandler<ProductCreateInput, { productId: stri
|
|
|
997
1252
|
handle: parsed.handle ?? null,
|
|
998
1253
|
taxRateId,
|
|
999
1254
|
taxRate,
|
|
1000
|
-
productType: parsed.productType ??
|
|
1255
|
+
productType: parsed.productType ?? "simple",
|
|
1001
1256
|
statusEntryId: parsed.statusEntryId ?? null,
|
|
1002
1257
|
primaryCurrencyCode: parsed.primaryCurrencyCode ?? null,
|
|
1003
|
-
defaultUnit:
|
|
1258
|
+
defaultUnit: resolvedUnits.defaultUnit,
|
|
1259
|
+
defaultSalesUnit:
|
|
1260
|
+
resolvedUnits.defaultSalesUnit ?? resolvedUnits.defaultUnit,
|
|
1261
|
+
defaultSalesUnitQuantity:
|
|
1262
|
+
toNumericString(parsed.defaultSalesUnitQuantity ?? 1) ?? "1",
|
|
1263
|
+
uomRoundingScale: parsed.uomRoundingScale ?? 4,
|
|
1264
|
+
uomRoundingMode: parsed.uomRoundingMode ?? "half_up",
|
|
1265
|
+
unitPriceEnabled,
|
|
1266
|
+
unitPriceReferenceUnit: unitPriceEnabled
|
|
1267
|
+
? (unitPriceInput.referenceUnit ?? null)
|
|
1268
|
+
: null,
|
|
1269
|
+
unitPriceBaseQuantity: unitPriceEnabled
|
|
1270
|
+
? (unitPriceInput.baseQuantity ?? null)
|
|
1271
|
+
: null,
|
|
1004
1272
|
defaultMediaId: parsed.defaultMediaId ?? null,
|
|
1005
1273
|
defaultMediaUrl: parsed.defaultMediaUrl ?? null,
|
|
1006
1274
|
weightValue,
|
|
@@ -1012,39 +1280,43 @@ const createProductCommand: CommandHandler<ProductCreateInput, { productId: stri
|
|
|
1012
1280
|
isActive: parsed.isActive ?? true,
|
|
1013
1281
|
createdAt: now,
|
|
1014
1282
|
updatedAt: now,
|
|
1015
|
-
})
|
|
1016
|
-
let optionSchemaTemplate: CatalogOptionSchemaTemplate | null = null
|
|
1283
|
+
});
|
|
1284
|
+
let optionSchemaTemplate: CatalogOptionSchemaTemplate | null = null;
|
|
1017
1285
|
if (parsed.optionSchemaId) {
|
|
1018
1286
|
optionSchemaTemplate = await requireOptionSchemaTemplate(
|
|
1019
1287
|
em,
|
|
1020
1288
|
parsed.optionSchemaId,
|
|
1021
|
-
|
|
1022
|
-
)
|
|
1023
|
-
ensureSameScope(
|
|
1024
|
-
|
|
1289
|
+
translate("catalog.errors.optionSchemaNotFound", "Option schema not found"),
|
|
1290
|
+
);
|
|
1291
|
+
ensureSameScope(
|
|
1292
|
+
optionSchemaTemplate,
|
|
1293
|
+
parsed.organizationId,
|
|
1294
|
+
parsed.tenantId,
|
|
1295
|
+
);
|
|
1296
|
+
record.optionSchemaTemplate = optionSchemaTemplate;
|
|
1025
1297
|
} else if (optionSchemaDefinition) {
|
|
1026
1298
|
optionSchemaTemplate = await assignOptionSchemaTemplate(
|
|
1027
1299
|
em,
|
|
1028
1300
|
record,
|
|
1029
1301
|
optionSchemaDefinition,
|
|
1030
|
-
optionSchemaDefinition.name ?? parsed.title
|
|
1031
|
-
)
|
|
1302
|
+
optionSchemaDefinition.name ?? parsed.title,
|
|
1303
|
+
);
|
|
1032
1304
|
}
|
|
1033
|
-
em.persist(record)
|
|
1305
|
+
em.persist(record);
|
|
1034
1306
|
try {
|
|
1035
|
-
await em.flush()
|
|
1307
|
+
await em.flush();
|
|
1036
1308
|
} catch (error) {
|
|
1037
|
-
await rethrowProductUniqueConstraint(error)
|
|
1309
|
+
await rethrowProductUniqueConstraint(error);
|
|
1038
1310
|
}
|
|
1039
|
-
await syncOffers(em, record, parsed.offers)
|
|
1040
|
-
await syncCategoryAssignments(em, record, parsed.categoryIds)
|
|
1041
|
-
await syncProductTags(em, record, parsed.tags)
|
|
1311
|
+
await syncOffers(em, record, parsed.offers);
|
|
1312
|
+
await syncCategoryAssignments(em, record, parsed.categoryIds);
|
|
1313
|
+
await syncProductTags(em, record, parsed.tags);
|
|
1042
1314
|
try {
|
|
1043
|
-
await em.flush()
|
|
1315
|
+
await em.flush();
|
|
1044
1316
|
} catch (error) {
|
|
1045
|
-
await rethrowProductUniqueConstraint(error)
|
|
1317
|
+
await rethrowProductUniqueConstraint(error);
|
|
1046
1318
|
}
|
|
1047
|
-
const dataEngine = ctx.container.resolve(
|
|
1319
|
+
const dataEngine = ctx.container.resolve("dataEngine") as DataEngine;
|
|
1048
1320
|
await setCustomFieldsIfAny({
|
|
1049
1321
|
dataEngine,
|
|
1050
1322
|
entityId: E.catalog.catalog_product,
|
|
@@ -1052,25 +1324,28 @@ const createProductCommand: CommandHandler<ProductCreateInput, { productId: stri
|
|
|
1052
1324
|
organizationId: record.organizationId,
|
|
1053
1325
|
tenantId: record.tenantId,
|
|
1054
1326
|
values: custom,
|
|
1055
|
-
})
|
|
1327
|
+
});
|
|
1056
1328
|
await emitProductCrudChange({
|
|
1057
1329
|
dataEngine,
|
|
1058
|
-
action:
|
|
1330
|
+
action: "created",
|
|
1059
1331
|
product: record,
|
|
1060
|
-
})
|
|
1061
|
-
return { productId: record.id }
|
|
1332
|
+
});
|
|
1333
|
+
return { productId: record.id };
|
|
1062
1334
|
},
|
|
1063
1335
|
captureAfter: async (_input, result, ctx) => {
|
|
1064
|
-
const em = (ctx.container.resolve(
|
|
1065
|
-
return loadProductSnapshot(em, result.productId)
|
|
1336
|
+
const em = (ctx.container.resolve("em") as EntityManager).fork();
|
|
1337
|
+
return loadProductSnapshot(em, result.productId);
|
|
1066
1338
|
},
|
|
1067
1339
|
buildLog: async ({ result, snapshots }) => {
|
|
1068
|
-
const after = snapshots.after as ProductSnapshot | undefined
|
|
1069
|
-
if (!after) return null
|
|
1070
|
-
const { translate } = await resolveTranslations()
|
|
1340
|
+
const after = snapshots.after as ProductSnapshot | undefined;
|
|
1341
|
+
if (!after) return null;
|
|
1342
|
+
const { translate } = await resolveTranslations();
|
|
1071
1343
|
return {
|
|
1072
|
-
actionLabel: translate(
|
|
1073
|
-
|
|
1344
|
+
actionLabel: translate(
|
|
1345
|
+
"catalog.audit.products.create",
|
|
1346
|
+
"Create catalog product",
|
|
1347
|
+
),
|
|
1348
|
+
resourceKind: "catalog.product",
|
|
1074
1349
|
resourceId: result.productId,
|
|
1075
1350
|
tenantId: after.tenantId,
|
|
1076
1351
|
organizationId: after.organizationId,
|
|
@@ -1080,21 +1355,24 @@ const createProductCommand: CommandHandler<ProductCreateInput, { productId: stri
|
|
|
1080
1355
|
after,
|
|
1081
1356
|
} satisfies ProductUndoPayload,
|
|
1082
1357
|
},
|
|
1083
|
-
}
|
|
1358
|
+
};
|
|
1084
1359
|
},
|
|
1085
1360
|
undo: async ({ logEntry, ctx }) => {
|
|
1086
|
-
const payload = extractUndoPayload<ProductUndoPayload>(logEntry)
|
|
1087
|
-
const after = payload?.after
|
|
1088
|
-
if (!after) return
|
|
1089
|
-
const em = (ctx.container.resolve(
|
|
1090
|
-
const record = await em
|
|
1091
|
-
if (!record) return
|
|
1092
|
-
ensureTenantScope(ctx, record.tenantId)
|
|
1093
|
-
ensureOrganizationScope(ctx, record.organizationId)
|
|
1094
|
-
em.remove(record)
|
|
1095
|
-
await em.flush()
|
|
1096
|
-
const dataEngine = ctx.container.resolve(
|
|
1097
|
-
const resetValues = buildCustomFieldResetMap(
|
|
1361
|
+
const payload = extractUndoPayload<ProductUndoPayload>(logEntry);
|
|
1362
|
+
const after = payload?.after;
|
|
1363
|
+
if (!after) return;
|
|
1364
|
+
const em = (ctx.container.resolve("em") as EntityManager).fork();
|
|
1365
|
+
const record = await findOneWithDecryption(em, CatalogProduct, { id: after.id });
|
|
1366
|
+
if (!record) return;
|
|
1367
|
+
ensureTenantScope(ctx, record.tenantId);
|
|
1368
|
+
ensureOrganizationScope(ctx, record.organizationId);
|
|
1369
|
+
em.remove(record);
|
|
1370
|
+
await em.flush();
|
|
1371
|
+
const dataEngine = ctx.container.resolve("dataEngine") as DataEngine;
|
|
1372
|
+
const resetValues = buildCustomFieldResetMap(
|
|
1373
|
+
undefined,
|
|
1374
|
+
after.custom ?? undefined,
|
|
1375
|
+
);
|
|
1098
1376
|
if (Object.keys(resetValues).length) {
|
|
1099
1377
|
await setCustomFieldsIfAny({
|
|
1100
1378
|
dataEngine,
|
|
@@ -1103,73 +1381,176 @@ const createProductCommand: CommandHandler<ProductCreateInput, { productId: stri
|
|
|
1103
1381
|
organizationId: after.organizationId,
|
|
1104
1382
|
tenantId: after.tenantId,
|
|
1105
1383
|
values: resetValues,
|
|
1106
|
-
})
|
|
1384
|
+
});
|
|
1107
1385
|
}
|
|
1108
1386
|
await emitProductCrudUndoChange({
|
|
1109
1387
|
dataEngine,
|
|
1110
|
-
action:
|
|
1388
|
+
action: "deleted",
|
|
1111
1389
|
product: record,
|
|
1112
|
-
})
|
|
1390
|
+
});
|
|
1113
1391
|
},
|
|
1114
|
-
}
|
|
1392
|
+
};
|
|
1115
1393
|
|
|
1116
|
-
const updateProductCommand: CommandHandler<
|
|
1117
|
-
|
|
1394
|
+
const updateProductCommand: CommandHandler<
|
|
1395
|
+
ProductUpdateInput,
|
|
1396
|
+
{ productId: string }
|
|
1397
|
+
> = {
|
|
1398
|
+
id: "catalog.products.update",
|
|
1118
1399
|
async prepare(input, ctx) {
|
|
1119
|
-
const id = requireId(input,
|
|
1120
|
-
const em =
|
|
1121
|
-
const snapshot = await loadProductSnapshot(em, id)
|
|
1400
|
+
const id = requireId(input, "Product id is required");
|
|
1401
|
+
const em = ctx.container.resolve("em") as EntityManager;
|
|
1402
|
+
const snapshot = await loadProductSnapshot(em, id);
|
|
1122
1403
|
if (snapshot) {
|
|
1123
|
-
ensureTenantScope(ctx, snapshot.tenantId)
|
|
1124
|
-
ensureOrganizationScope(ctx, snapshot.organizationId)
|
|
1404
|
+
ensureTenantScope(ctx, snapshot.tenantId);
|
|
1405
|
+
ensureOrganizationScope(ctx, snapshot.organizationId);
|
|
1125
1406
|
}
|
|
1126
|
-
return snapshot ? { before: snapshot } : {}
|
|
1407
|
+
return snapshot ? { before: snapshot } : {};
|
|
1127
1408
|
},
|
|
1128
1409
|
async execute(rawInput, ctx) {
|
|
1129
|
-
const { parsed, custom } = parseWithCustomFields(
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
const
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
const
|
|
1410
|
+
const { parsed, custom } = parseWithCustomFields(
|
|
1411
|
+
productUpdateSchema,
|
|
1412
|
+
rawInput,
|
|
1413
|
+
);
|
|
1414
|
+
const rawPayload =
|
|
1415
|
+
rawInput && typeof rawInput === "object"
|
|
1416
|
+
? (rawInput as Record<string, unknown>)
|
|
1417
|
+
: null;
|
|
1418
|
+
const hasDefaultUnit = Boolean(
|
|
1419
|
+
rawPayload &&
|
|
1420
|
+
Object.prototype.hasOwnProperty.call(rawPayload, "defaultUnit"),
|
|
1421
|
+
);
|
|
1422
|
+
const hasDefaultSalesUnit = Boolean(
|
|
1423
|
+
rawPayload &&
|
|
1424
|
+
Object.prototype.hasOwnProperty.call(rawPayload, "defaultSalesUnit"),
|
|
1425
|
+
);
|
|
1426
|
+
const requestedDefaultUnit = hasDefaultUnit
|
|
1427
|
+
? rawPayload?.defaultUnit
|
|
1428
|
+
: parsed.defaultUnit;
|
|
1429
|
+
const requestedDefaultSalesUnit = hasDefaultSalesUnit
|
|
1430
|
+
? rawPayload?.defaultSalesUnit
|
|
1431
|
+
: parsed.defaultSalesUnit;
|
|
1432
|
+
const em = (ctx.container.resolve("em") as EntityManager).fork();
|
|
1433
|
+
const { translate } = await resolveTranslations();
|
|
1434
|
+
const record = await findOneWithDecryption(em, CatalogProduct, {
|
|
1435
|
+
id: parsed.id,
|
|
1436
|
+
deletedAt: null,
|
|
1437
|
+
});
|
|
1438
|
+
if (!record)
|
|
1439
|
+
throw new CrudHttpError(404, {
|
|
1440
|
+
error: translate("catalog.errors.productNotFound", "Catalog product not found"),
|
|
1441
|
+
});
|
|
1442
|
+
const organizationId = parsed.organizationId ?? record.organizationId;
|
|
1443
|
+
const tenantId = parsed.tenantId ?? record.tenantId;
|
|
1444
|
+
ensureTenantScope(ctx, tenantId);
|
|
1445
|
+
ensureOrganizationScope(ctx, organizationId);
|
|
1446
|
+
ensureSameScope(record, organizationId, tenantId);
|
|
1447
|
+
const dataEngine = ctx.container.resolve("dataEngine") as DataEngine;
|
|
1448
|
+
const lookupEm = em.fork();
|
|
1449
|
+
const taxRateProvided =
|
|
1450
|
+
parsed.taxRateId !== undefined || parsed.taxRate !== undefined;
|
|
1142
1451
|
const resolvedTaxRate = taxRateProvided
|
|
1143
|
-
? await resolveScopedTaxRate(
|
|
1144
|
-
|
|
1452
|
+
? await resolveScopedTaxRate(
|
|
1453
|
+
lookupEm,
|
|
1454
|
+
parsed.taxRateId ?? null,
|
|
1455
|
+
parsed.taxRate,
|
|
1456
|
+
organizationId,
|
|
1457
|
+
tenantId,
|
|
1458
|
+
)
|
|
1459
|
+
: null;
|
|
1460
|
+
record.organizationId = organizationId;
|
|
1461
|
+
record.tenantId = tenantId;
|
|
1145
1462
|
|
|
1146
|
-
if (parsed.title !== undefined) record.title = parsed.title
|
|
1147
|
-
if (parsed.subtitle !== undefined)
|
|
1148
|
-
|
|
1149
|
-
if (parsed.
|
|
1150
|
-
|
|
1463
|
+
if (parsed.title !== undefined) record.title = parsed.title;
|
|
1464
|
+
if (parsed.subtitle !== undefined)
|
|
1465
|
+
record.subtitle = parsed.subtitle ?? null;
|
|
1466
|
+
if (parsed.description !== undefined)
|
|
1467
|
+
record.description = parsed.description ?? null;
|
|
1468
|
+
if (parsed.sku !== undefined) record.sku = parsed.sku ?? null;
|
|
1469
|
+
if (parsed.handle !== undefined) record.handle = parsed.handle ?? null;
|
|
1151
1470
|
if (taxRateProvided) {
|
|
1152
|
-
record.taxRateId = resolvedTaxRate?.taxRateId ?? null
|
|
1153
|
-
record.taxRate = resolvedTaxRate?.taxRate ?? null
|
|
1471
|
+
record.taxRateId = resolvedTaxRate?.taxRateId ?? null;
|
|
1472
|
+
record.taxRate = resolvedTaxRate?.taxRate ?? null;
|
|
1154
1473
|
}
|
|
1155
|
-
if (parsed.productType !== undefined)
|
|
1156
|
-
|
|
1474
|
+
if (parsed.productType !== undefined)
|
|
1475
|
+
record.productType = parsed.productType;
|
|
1476
|
+
if (parsed.statusEntryId !== undefined)
|
|
1477
|
+
record.statusEntryId = parsed.statusEntryId ?? null;
|
|
1157
1478
|
if (parsed.primaryCurrencyCode !== undefined) {
|
|
1158
|
-
record.primaryCurrencyCode = parsed.primaryCurrencyCode ?? null
|
|
1479
|
+
record.primaryCurrencyCode = parsed.primaryCurrencyCode ?? null;
|
|
1480
|
+
}
|
|
1481
|
+
const uomDefaultsTouched =
|
|
1482
|
+
hasDefaultUnit ||
|
|
1483
|
+
hasDefaultSalesUnit ||
|
|
1484
|
+
parsed.defaultUnit !== undefined ||
|
|
1485
|
+
parsed.defaultSalesUnit !== undefined ||
|
|
1486
|
+
parsed.organizationId !== undefined ||
|
|
1487
|
+
parsed.tenantId !== undefined;
|
|
1488
|
+
if (uomDefaultsTouched) {
|
|
1489
|
+
const resolvedUnits = await resolveProductUnitDefaults(lookupEm, {
|
|
1490
|
+
organizationId,
|
|
1491
|
+
tenantId,
|
|
1492
|
+
defaultUnit: hasDefaultUnit
|
|
1493
|
+
? (requestedDefaultUnit as string | null | undefined)
|
|
1494
|
+
: parsed.defaultUnit !== undefined
|
|
1495
|
+
? parsed.defaultUnit
|
|
1496
|
+
: record.defaultUnit,
|
|
1497
|
+
defaultSalesUnit: hasDefaultSalesUnit
|
|
1498
|
+
? (requestedDefaultSalesUnit as string | null | undefined)
|
|
1499
|
+
: parsed.defaultSalesUnit !== undefined
|
|
1500
|
+
? parsed.defaultSalesUnit
|
|
1501
|
+
: record.defaultSalesUnit,
|
|
1502
|
+
});
|
|
1503
|
+
await ensureBaseUnitCanBeRemoved(lookupEm, {
|
|
1504
|
+
productId: record.id,
|
|
1505
|
+
organizationId,
|
|
1506
|
+
tenantId,
|
|
1507
|
+
defaultUnit: resolvedUnits.defaultUnit,
|
|
1508
|
+
defaultSalesUnit: resolvedUnits.defaultSalesUnit,
|
|
1509
|
+
});
|
|
1510
|
+
record.defaultUnit = resolvedUnits.defaultUnit;
|
|
1511
|
+
record.defaultSalesUnit = resolvedUnits.defaultSalesUnit;
|
|
1512
|
+
}
|
|
1513
|
+
if (parsed.defaultSalesUnitQuantity !== undefined) {
|
|
1514
|
+
record.defaultSalesUnitQuantity =
|
|
1515
|
+
toNumericString(parsed.defaultSalesUnitQuantity) ?? "1";
|
|
1516
|
+
}
|
|
1517
|
+
if (parsed.uomRoundingScale !== undefined) {
|
|
1518
|
+
record.uomRoundingScale = parsed.uomRoundingScale;
|
|
1519
|
+
}
|
|
1520
|
+
if (parsed.uomRoundingMode !== undefined) {
|
|
1521
|
+
record.uomRoundingMode = parsed.uomRoundingMode;
|
|
1522
|
+
}
|
|
1523
|
+
const unitPriceInput = resolveUnitPriceInput(parsed);
|
|
1524
|
+
if (unitPriceInput.enabledProvided) {
|
|
1525
|
+
record.unitPriceEnabled = unitPriceInput.enabled ?? false;
|
|
1526
|
+
if (!record.unitPriceEnabled) {
|
|
1527
|
+
record.unitPriceReferenceUnit = null;
|
|
1528
|
+
record.unitPriceBaseQuantity = null;
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
if (unitPriceInput.referenceProvided && record.unitPriceEnabled) {
|
|
1532
|
+
record.unitPriceReferenceUnit = unitPriceInput.referenceUnit ?? null;
|
|
1533
|
+
}
|
|
1534
|
+
if (unitPriceInput.baseProvided && record.unitPriceEnabled) {
|
|
1535
|
+
record.unitPriceBaseQuantity = unitPriceInput.baseQuantity ?? null;
|
|
1159
1536
|
}
|
|
1160
|
-
if (parsed.defaultUnit !== undefined) record.defaultUnit = parsed.defaultUnit ?? null
|
|
1161
1537
|
if (parsed.defaultMediaId !== undefined) {
|
|
1162
|
-
record.defaultMediaId = parsed.defaultMediaId ?? null
|
|
1538
|
+
record.defaultMediaId = parsed.defaultMediaId ?? null;
|
|
1163
1539
|
}
|
|
1164
1540
|
if (parsed.defaultMediaUrl !== undefined) {
|
|
1165
|
-
record.defaultMediaUrl = parsed.defaultMediaUrl ?? null
|
|
1541
|
+
record.defaultMediaUrl = parsed.defaultMediaUrl ?? null;
|
|
1166
1542
|
}
|
|
1167
1543
|
const metadataProvided =
|
|
1168
|
-
rawInput &&
|
|
1169
|
-
|
|
1170
|
-
|
|
1544
|
+
rawInput &&
|
|
1545
|
+
typeof rawInput === "object" &&
|
|
1546
|
+
Object.prototype.hasOwnProperty.call(rawInput, "metadata");
|
|
1547
|
+
const { schema: optionSchemaDefinition, metadata: sanitizedMetadata } =
|
|
1548
|
+
extractOptionSchemaInput(parsed);
|
|
1549
|
+
const measurements = extractMeasurementsFromMetadata(sanitizedMetadata);
|
|
1171
1550
|
const normalizedDimensions =
|
|
1172
|
-
parsed.dimensions !== undefined
|
|
1551
|
+
parsed.dimensions !== undefined
|
|
1552
|
+
? normalizeDimensionsInput(parsed.dimensions)
|
|
1553
|
+
: measurements.dimensions;
|
|
1173
1554
|
const weightValueFromInput =
|
|
1174
1555
|
parsed.weightValue === null
|
|
1175
1556
|
? null
|
|
@@ -1177,35 +1558,41 @@ const updateProductCommand: CommandHandler<ProductUpdateInput, { productId: stri
|
|
|
1177
1558
|
? toNumericString(parsed.weightValue)
|
|
1178
1559
|
: measurements.weightValue !== null
|
|
1179
1560
|
? toNumericString(measurements.weightValue)
|
|
1180
|
-
: null
|
|
1561
|
+
: null;
|
|
1181
1562
|
const weightUnitFromInput =
|
|
1182
|
-
parsed.weightUnit !== undefined
|
|
1563
|
+
parsed.weightUnit !== undefined
|
|
1564
|
+
? (parsed.weightUnit ?? null)
|
|
1565
|
+
: (measurements.weightUnit ?? null);
|
|
1183
1566
|
const weightProvided =
|
|
1184
1567
|
parsed.weightValue !== undefined ||
|
|
1185
1568
|
parsed.weightUnit !== undefined ||
|
|
1186
1569
|
measurements.weightValue !== null ||
|
|
1187
|
-
measurements.weightUnit !== null
|
|
1570
|
+
measurements.weightUnit !== null;
|
|
1188
1571
|
if (normalizedDimensions !== null || parsed.dimensions !== undefined) {
|
|
1189
|
-
record.dimensions = normalizedDimensions
|
|
1572
|
+
record.dimensions = normalizedDimensions
|
|
1573
|
+
? cloneJson(normalizedDimensions)
|
|
1574
|
+
: null;
|
|
1190
1575
|
}
|
|
1191
1576
|
if (weightProvided) {
|
|
1192
|
-
record.weightValue = weightValueFromInput
|
|
1193
|
-
record.weightUnit = weightUnitFromInput
|
|
1577
|
+
record.weightValue = weightValueFromInput;
|
|
1578
|
+
record.weightUnit = weightUnitFromInput;
|
|
1194
1579
|
}
|
|
1195
1580
|
if (metadataProvided) {
|
|
1196
|
-
record.metadata = measurements.metadata
|
|
1581
|
+
record.metadata = measurements.metadata
|
|
1582
|
+
? cloneJson(measurements.metadata)
|
|
1583
|
+
: null;
|
|
1197
1584
|
}
|
|
1198
1585
|
if (parsed.optionSchemaId !== undefined) {
|
|
1199
1586
|
if (!parsed.optionSchemaId) {
|
|
1200
|
-
record.optionSchemaTemplate = null
|
|
1587
|
+
record.optionSchemaTemplate = null;
|
|
1201
1588
|
} else {
|
|
1202
1589
|
const optionTemplate = await requireOptionSchemaTemplate(
|
|
1203
|
-
|
|
1590
|
+
lookupEm,
|
|
1204
1591
|
parsed.optionSchemaId,
|
|
1205
|
-
|
|
1206
|
-
)
|
|
1207
|
-
ensureSameScope(optionTemplate, organizationId, tenantId)
|
|
1208
|
-
record.optionSchemaTemplate = optionTemplate
|
|
1592
|
+
translate("catalog.errors.optionSchemaNotFound", "Option schema not found"),
|
|
1593
|
+
);
|
|
1594
|
+
ensureSameScope(optionTemplate, organizationId, tenantId);
|
|
1595
|
+
record.optionSchemaTemplate = optionTemplate;
|
|
1209
1596
|
}
|
|
1210
1597
|
}
|
|
1211
1598
|
if (optionSchemaDefinition) {
|
|
@@ -1213,26 +1600,27 @@ const updateProductCommand: CommandHandler<ProductUpdateInput, { productId: stri
|
|
|
1213
1600
|
em,
|
|
1214
1601
|
record,
|
|
1215
1602
|
optionSchemaDefinition,
|
|
1216
|
-
optionSchemaDefinition.name ?? parsed.title ?? record.title
|
|
1217
|
-
)
|
|
1603
|
+
optionSchemaDefinition.name ?? parsed.title ?? record.title,
|
|
1604
|
+
);
|
|
1218
1605
|
}
|
|
1219
1606
|
if (parsed.customFieldsetCode !== undefined) {
|
|
1220
|
-
record.customFieldsetCode = parsed.customFieldsetCode ?? null
|
|
1607
|
+
record.customFieldsetCode = parsed.customFieldsetCode ?? null;
|
|
1221
1608
|
}
|
|
1222
|
-
if (parsed.isConfigurable !== undefined)
|
|
1223
|
-
|
|
1609
|
+
if (parsed.isConfigurable !== undefined)
|
|
1610
|
+
record.isConfigurable = parsed.isConfigurable;
|
|
1611
|
+
if (parsed.isActive !== undefined) record.isActive = parsed.isActive;
|
|
1224
1612
|
try {
|
|
1225
|
-
await em.flush()
|
|
1613
|
+
await em.flush();
|
|
1226
1614
|
} catch (error) {
|
|
1227
|
-
await rethrowProductUniqueConstraint(error)
|
|
1615
|
+
await rethrowProductUniqueConstraint(error);
|
|
1228
1616
|
}
|
|
1229
|
-
await syncOffers(em, record, parsed.offers)
|
|
1230
|
-
await syncCategoryAssignments(em, record, parsed.categoryIds)
|
|
1231
|
-
await syncProductTags(em, record, parsed.tags)
|
|
1617
|
+
await syncOffers(em, record, parsed.offers);
|
|
1618
|
+
await syncCategoryAssignments(em, record, parsed.categoryIds);
|
|
1619
|
+
await syncProductTags(em, record, parsed.tags);
|
|
1232
1620
|
try {
|
|
1233
|
-
await em.flush()
|
|
1621
|
+
await em.flush();
|
|
1234
1622
|
} catch (error) {
|
|
1235
|
-
await rethrowProductUniqueConstraint(error)
|
|
1623
|
+
await rethrowProductUniqueConstraint(error);
|
|
1236
1624
|
}
|
|
1237
1625
|
if (custom && Object.keys(custom).length) {
|
|
1238
1626
|
await setCustomFieldsIfAny({
|
|
@@ -1242,30 +1630,47 @@ const updateProductCommand: CommandHandler<ProductUpdateInput, { productId: stri
|
|
|
1242
1630
|
organizationId: record.organizationId,
|
|
1243
1631
|
tenantId: record.tenantId,
|
|
1244
1632
|
values: custom,
|
|
1245
|
-
})
|
|
1633
|
+
});
|
|
1246
1634
|
}
|
|
1247
1635
|
await emitProductCrudChange({
|
|
1248
1636
|
dataEngine,
|
|
1249
|
-
action:
|
|
1637
|
+
action: "updated",
|
|
1250
1638
|
product: record,
|
|
1251
|
-
})
|
|
1252
|
-
return { productId: record.id }
|
|
1639
|
+
});
|
|
1640
|
+
return { productId: record.id };
|
|
1253
1641
|
},
|
|
1254
1642
|
captureAfter: async (_input, result, ctx) => {
|
|
1255
|
-
const em = (ctx.container.resolve(
|
|
1256
|
-
return loadProductSnapshot(em, result.productId)
|
|
1643
|
+
const em = (ctx.container.resolve("em") as EntityManager).fork();
|
|
1644
|
+
return loadProductSnapshot(em, result.productId);
|
|
1257
1645
|
},
|
|
1258
1646
|
buildLog: async ({ snapshots }) => {
|
|
1259
|
-
const before = snapshots.before as ProductSnapshot | undefined
|
|
1260
|
-
const after = snapshots.after as ProductSnapshot | undefined
|
|
1261
|
-
if (!before || !after) return null
|
|
1262
|
-
const { translate } = await resolveTranslations()
|
|
1647
|
+
const before = snapshots.before as ProductSnapshot | undefined;
|
|
1648
|
+
const after = snapshots.after as ProductSnapshot | undefined;
|
|
1649
|
+
if (!before || !after) return null;
|
|
1650
|
+
const { translate } = await resolveTranslations();
|
|
1263
1651
|
return {
|
|
1264
|
-
actionLabel: translate(
|
|
1265
|
-
|
|
1652
|
+
actionLabel: translate(
|
|
1653
|
+
"catalog.audit.products.update",
|
|
1654
|
+
"Update catalog product",
|
|
1655
|
+
),
|
|
1656
|
+
resourceKind: "catalog.product",
|
|
1266
1657
|
resourceId: before.id,
|
|
1267
1658
|
tenantId: before.tenantId,
|
|
1268
1659
|
organizationId: before.organizationId,
|
|
1660
|
+
changes: buildChanges(before, after, [
|
|
1661
|
+
"title",
|
|
1662
|
+
"sku",
|
|
1663
|
+
"productType",
|
|
1664
|
+
"defaultUnit",
|
|
1665
|
+
"defaultSalesUnit",
|
|
1666
|
+
"defaultSalesUnitQuantity",
|
|
1667
|
+
"uomRoundingScale",
|
|
1668
|
+
"uomRoundingMode",
|
|
1669
|
+
"unitPriceEnabled",
|
|
1670
|
+
"unitPriceReferenceUnit",
|
|
1671
|
+
"unitPriceBaseQuantity",
|
|
1672
|
+
"isActive",
|
|
1673
|
+
]),
|
|
1269
1674
|
snapshotBefore: before,
|
|
1270
1675
|
snapshotAfter: after,
|
|
1271
1676
|
payload: {
|
|
@@ -1274,14 +1679,14 @@ const updateProductCommand: CommandHandler<ProductUpdateInput, { productId: stri
|
|
|
1274
1679
|
after,
|
|
1275
1680
|
} satisfies ProductUndoPayload,
|
|
1276
1681
|
},
|
|
1277
|
-
}
|
|
1682
|
+
};
|
|
1278
1683
|
},
|
|
1279
1684
|
undo: async ({ logEntry, ctx }) => {
|
|
1280
|
-
const payload = extractUndoPayload<ProductUndoPayload>(logEntry)
|
|
1281
|
-
const before = payload?.before
|
|
1282
|
-
if (!before) return
|
|
1283
|
-
const em = (ctx.container.resolve(
|
|
1284
|
-
let record = await em
|
|
1685
|
+
const payload = extractUndoPayload<ProductUndoPayload>(logEntry);
|
|
1686
|
+
const before = payload?.before;
|
|
1687
|
+
if (!before) return;
|
|
1688
|
+
const em = (ctx.container.resolve("em") as EntityManager).fork();
|
|
1689
|
+
let record = await findOneWithDecryption(em, CatalogProduct, { id: before.id });
|
|
1285
1690
|
if (!record) {
|
|
1286
1691
|
record = em.create(CatalogProduct, {
|
|
1287
1692
|
id: before.id,
|
|
@@ -1297,6 +1702,13 @@ const updateProductCommand: CommandHandler<ProductUpdateInput, { productId: stri
|
|
|
1297
1702
|
statusEntryId: before.statusEntryId ?? null,
|
|
1298
1703
|
primaryCurrencyCode: before.primaryCurrencyCode ?? null,
|
|
1299
1704
|
defaultUnit: before.defaultUnit ?? null,
|
|
1705
|
+
defaultSalesUnit: before.defaultSalesUnit ?? null,
|
|
1706
|
+
defaultSalesUnitQuantity: before.defaultSalesUnitQuantity ?? "1",
|
|
1707
|
+
uomRoundingScale: before.uomRoundingScale,
|
|
1708
|
+
uomRoundingMode: before.uomRoundingMode,
|
|
1709
|
+
unitPriceEnabled: before.unitPriceEnabled,
|
|
1710
|
+
unitPriceReferenceUnit: before.unitPriceReferenceUnit ?? null,
|
|
1711
|
+
unitPriceBaseQuantity: before.unitPriceBaseQuantity ?? null,
|
|
1300
1712
|
weightValue: before.weightValue ?? null,
|
|
1301
1713
|
weightUnit: before.weightUnit ?? null,
|
|
1302
1714
|
dimensions: before.dimensions ? cloneJson(before.dimensions) : null,
|
|
@@ -1305,25 +1717,32 @@ const updateProductCommand: CommandHandler<ProductUpdateInput, { productId: stri
|
|
|
1305
1717
|
optionSchemaTemplate: before.optionSchemaId
|
|
1306
1718
|
? em.getReference(CatalogOptionSchemaTemplate, before.optionSchemaId)
|
|
1307
1719
|
: null,
|
|
1308
|
-
productType: before.productType ??
|
|
1720
|
+
productType: before.productType ?? "simple",
|
|
1309
1721
|
isConfigurable: before.isConfigurable,
|
|
1310
1722
|
isActive: before.isActive,
|
|
1311
1723
|
createdAt: new Date(before.createdAt),
|
|
1312
1724
|
updatedAt: new Date(before.updatedAt),
|
|
1313
|
-
})
|
|
1314
|
-
em.persist(record)
|
|
1725
|
+
});
|
|
1726
|
+
em.persist(record);
|
|
1315
1727
|
}
|
|
1316
|
-
ensureTenantScope(ctx, before.tenantId)
|
|
1317
|
-
ensureOrganizationScope(ctx, before.organizationId)
|
|
1318
|
-
applyProductSnapshot(em, record, before)
|
|
1319
|
-
await em.flush()
|
|
1728
|
+
ensureTenantScope(ctx, before.tenantId);
|
|
1729
|
+
ensureOrganizationScope(ctx, before.organizationId);
|
|
1730
|
+
applyProductSnapshot(em, record, before);
|
|
1731
|
+
await em.flush();
|
|
1320
1732
|
|
|
1321
|
-
|
|
1322
|
-
await
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1733
|
+
const relationEm = em.fork();
|
|
1734
|
+
const relationRecord = await findOneWithDecryption(relationEm, CatalogProduct, { id: before.id });
|
|
1735
|
+
if (relationRecord) {
|
|
1736
|
+
await restoreOffersFromSnapshot(relationEm, relationRecord, before.offers);
|
|
1737
|
+
await syncCategoryAssignments(relationEm, relationRecord, before.categoryIds);
|
|
1738
|
+
await syncProductTags(relationEm, relationRecord, before.tags);
|
|
1739
|
+
await relationEm.flush();
|
|
1740
|
+
}
|
|
1741
|
+
const dataEngine = ctx.container.resolve("dataEngine") as DataEngine;
|
|
1742
|
+
const resetValues = buildCustomFieldResetMap(
|
|
1743
|
+
before.custom ?? undefined,
|
|
1744
|
+
payload?.after?.custom ?? undefined,
|
|
1745
|
+
);
|
|
1327
1746
|
if (Object.keys(resetValues).length) {
|
|
1328
1747
|
await setCustomFieldsIfAny({
|
|
1329
1748
|
dataEngine,
|
|
@@ -1332,57 +1751,73 @@ const updateProductCommand: CommandHandler<ProductUpdateInput, { productId: stri
|
|
|
1332
1751
|
organizationId: before.organizationId,
|
|
1333
1752
|
tenantId: before.tenantId,
|
|
1334
1753
|
values: resetValues,
|
|
1335
|
-
})
|
|
1754
|
+
});
|
|
1336
1755
|
}
|
|
1337
1756
|
await emitProductCrudUndoChange({
|
|
1338
1757
|
dataEngine,
|
|
1339
|
-
action:
|
|
1758
|
+
action: "updated",
|
|
1340
1759
|
product: record,
|
|
1341
|
-
})
|
|
1760
|
+
});
|
|
1342
1761
|
},
|
|
1343
|
-
}
|
|
1762
|
+
};
|
|
1344
1763
|
|
|
1345
1764
|
const deleteProductCommand: CommandHandler<
|
|
1346
1765
|
{ body?: Record<string, unknown>; query?: Record<string, unknown> },
|
|
1347
1766
|
{ productId: string }
|
|
1348
1767
|
> = {
|
|
1349
|
-
id:
|
|
1768
|
+
id: "catalog.products.delete",
|
|
1350
1769
|
async prepare(input, ctx) {
|
|
1351
|
-
const id = requireId(input,
|
|
1352
|
-
const em =
|
|
1353
|
-
const snapshot = await loadProductSnapshot(em, id)
|
|
1770
|
+
const id = requireId(input, "Product id is required");
|
|
1771
|
+
const em = ctx.container.resolve("em") as EntityManager;
|
|
1772
|
+
const snapshot = await loadProductSnapshot(em, id);
|
|
1354
1773
|
if (snapshot) {
|
|
1355
|
-
ensureTenantScope(ctx, snapshot.tenantId)
|
|
1356
|
-
ensureOrganizationScope(ctx, snapshot.organizationId)
|
|
1774
|
+
ensureTenantScope(ctx, snapshot.tenantId);
|
|
1775
|
+
ensureOrganizationScope(ctx, snapshot.organizationId);
|
|
1357
1776
|
}
|
|
1358
|
-
return snapshot ? { before: snapshot } : {}
|
|
1777
|
+
return snapshot ? { before: snapshot } : {};
|
|
1359
1778
|
},
|
|
1360
1779
|
async execute(input, ctx) {
|
|
1361
|
-
const id = requireId(input,
|
|
1362
|
-
const em = (ctx.container.resolve(
|
|
1780
|
+
const id = requireId(input, "Product id is required");
|
|
1781
|
+
const em = (ctx.container.resolve("em") as EntityManager).fork();
|
|
1363
1782
|
const record = await findOneWithDecryption(
|
|
1364
1783
|
em,
|
|
1365
1784
|
CatalogProduct,
|
|
1366
1785
|
{ id },
|
|
1367
|
-
{ populate: [
|
|
1368
|
-
)
|
|
1369
|
-
if (!record)
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1786
|
+
{ populate: ["optionSchemaTemplate"] },
|
|
1787
|
+
);
|
|
1788
|
+
if (!record) {
|
|
1789
|
+
const { translate } = await resolveTranslations();
|
|
1790
|
+
throw new CrudHttpError(404, {
|
|
1791
|
+
error: translate(
|
|
1792
|
+
"catalog.products.errors.notFound",
|
|
1793
|
+
"Catalog product not found",
|
|
1794
|
+
),
|
|
1795
|
+
});
|
|
1796
|
+
}
|
|
1797
|
+
const baseEm = ctx.container.resolve("em") as EntityManager;
|
|
1798
|
+
const snapshot = await loadProductSnapshot(baseEm, id);
|
|
1799
|
+
ensureTenantScope(ctx, record.tenantId);
|
|
1800
|
+
ensureOrganizationScope(ctx, record.organizationId);
|
|
1801
|
+
const dataEngine = ctx.container.resolve("dataEngine") as DataEngine;
|
|
1802
|
+
await deleteProductVariantsAndRelatedData({
|
|
1803
|
+
em,
|
|
1804
|
+
product: record,
|
|
1805
|
+
dataEngine,
|
|
1806
|
+
ctx,
|
|
1807
|
+
});
|
|
1808
|
+
await em.nativeDelete(CatalogProductPrice, { product: record.id });
|
|
1809
|
+
const templateToRemove = await resolveOptionSchemaTemplateForRemoval(
|
|
1810
|
+
em,
|
|
1811
|
+
record,
|
|
1812
|
+
);
|
|
1378
1813
|
if (templateToRemove) {
|
|
1379
|
-
record.optionSchemaTemplate = null
|
|
1380
|
-
em.remove(templateToRemove)
|
|
1814
|
+
record.optionSchemaTemplate = null;
|
|
1815
|
+
em.remove(templateToRemove);
|
|
1381
1816
|
}
|
|
1382
|
-
em.remove(record)
|
|
1383
|
-
await em.flush()
|
|
1817
|
+
em.remove(record);
|
|
1818
|
+
await em.flush();
|
|
1384
1819
|
if (snapshot?.custom && Object.keys(snapshot.custom).length) {
|
|
1385
|
-
const resetValues = buildCustomFieldResetMap(snapshot.custom, undefined)
|
|
1820
|
+
const resetValues = buildCustomFieldResetMap(snapshot.custom, undefined);
|
|
1386
1821
|
if (Object.keys(resetValues).length) {
|
|
1387
1822
|
await setCustomFieldsIfAny({
|
|
1388
1823
|
dataEngine,
|
|
@@ -1391,23 +1826,26 @@ const deleteProductCommand: CommandHandler<
|
|
|
1391
1826
|
organizationId: record.organizationId,
|
|
1392
1827
|
tenantId: record.tenantId,
|
|
1393
1828
|
values: resetValues,
|
|
1394
|
-
})
|
|
1829
|
+
});
|
|
1395
1830
|
}
|
|
1396
1831
|
}
|
|
1397
1832
|
await emitProductCrudChange({
|
|
1398
1833
|
dataEngine,
|
|
1399
|
-
action:
|
|
1834
|
+
action: "deleted",
|
|
1400
1835
|
product: record,
|
|
1401
|
-
})
|
|
1402
|
-
return { productId: id }
|
|
1836
|
+
});
|
|
1837
|
+
return { productId: id };
|
|
1403
1838
|
},
|
|
1404
1839
|
buildLog: async ({ snapshots }) => {
|
|
1405
|
-
const before = snapshots.before as ProductSnapshot | undefined
|
|
1406
|
-
if (!before) return null
|
|
1407
|
-
const { translate } = await resolveTranslations()
|
|
1840
|
+
const before = snapshots.before as ProductSnapshot | undefined;
|
|
1841
|
+
if (!before) return null;
|
|
1842
|
+
const { translate } = await resolveTranslations();
|
|
1408
1843
|
return {
|
|
1409
|
-
actionLabel: translate(
|
|
1410
|
-
|
|
1844
|
+
actionLabel: translate(
|
|
1845
|
+
"catalog.audit.products.delete",
|
|
1846
|
+
"Delete catalog product",
|
|
1847
|
+
),
|
|
1848
|
+
resourceKind: "catalog.product",
|
|
1411
1849
|
resourceId: before.id,
|
|
1412
1850
|
tenantId: before.tenantId,
|
|
1413
1851
|
organizationId: before.organizationId,
|
|
@@ -1417,14 +1855,14 @@ const deleteProductCommand: CommandHandler<
|
|
|
1417
1855
|
before,
|
|
1418
1856
|
} satisfies ProductUndoPayload,
|
|
1419
1857
|
},
|
|
1420
|
-
}
|
|
1858
|
+
};
|
|
1421
1859
|
},
|
|
1422
1860
|
undo: async ({ logEntry, ctx }) => {
|
|
1423
|
-
const payload = extractUndoPayload<ProductUndoPayload>(logEntry)
|
|
1424
|
-
const before = payload?.before
|
|
1425
|
-
if (!before) return
|
|
1426
|
-
const em = (ctx.container.resolve(
|
|
1427
|
-
let record = await em
|
|
1861
|
+
const payload = extractUndoPayload<ProductUndoPayload>(logEntry);
|
|
1862
|
+
const before = payload?.before;
|
|
1863
|
+
if (!before) return;
|
|
1864
|
+
const em = (ctx.container.resolve("em") as EntityManager).fork();
|
|
1865
|
+
let record = await findOneWithDecryption(em, CatalogProduct, { id: before.id });
|
|
1428
1866
|
if (!record) {
|
|
1429
1867
|
record = em.create(CatalogProduct, {
|
|
1430
1868
|
id: before.id,
|
|
@@ -1440,6 +1878,13 @@ const deleteProductCommand: CommandHandler<
|
|
|
1440
1878
|
statusEntryId: before.statusEntryId ?? null,
|
|
1441
1879
|
primaryCurrencyCode: before.primaryCurrencyCode ?? null,
|
|
1442
1880
|
defaultUnit: before.defaultUnit ?? null,
|
|
1881
|
+
defaultSalesUnit: before.defaultSalesUnit ?? null,
|
|
1882
|
+
defaultSalesUnitQuantity: before.defaultSalesUnitQuantity ?? "1",
|
|
1883
|
+
uomRoundingScale: before.uomRoundingScale,
|
|
1884
|
+
uomRoundingMode: before.uomRoundingMode,
|
|
1885
|
+
unitPriceEnabled: before.unitPriceEnabled,
|
|
1886
|
+
unitPriceReferenceUnit: before.unitPriceReferenceUnit ?? null,
|
|
1887
|
+
unitPriceBaseQuantity: before.unitPriceBaseQuantity ?? null,
|
|
1443
1888
|
weightValue: before.weightValue ?? null,
|
|
1444
1889
|
weightUnit: before.weightUnit ?? null,
|
|
1445
1890
|
dimensions: before.dimensions ? cloneJson(before.dimensions) : null,
|
|
@@ -1448,22 +1893,28 @@ const deleteProductCommand: CommandHandler<
|
|
|
1448
1893
|
optionSchemaTemplate: before.optionSchemaId
|
|
1449
1894
|
? em.getReference(CatalogOptionSchemaTemplate, before.optionSchemaId)
|
|
1450
1895
|
: null,
|
|
1451
|
-
productType: before.productType ??
|
|
1896
|
+
productType: before.productType ?? "simple",
|
|
1452
1897
|
isConfigurable: before.isConfigurable,
|
|
1453
1898
|
isActive: before.isActive,
|
|
1454
1899
|
createdAt: new Date(before.createdAt),
|
|
1455
1900
|
updatedAt: new Date(before.updatedAt),
|
|
1456
|
-
})
|
|
1457
|
-
em.persist(record)
|
|
1901
|
+
});
|
|
1902
|
+
em.persist(record);
|
|
1903
|
+
}
|
|
1904
|
+
ensureTenantScope(ctx, before.tenantId);
|
|
1905
|
+
ensureOrganizationScope(ctx, before.organizationId);
|
|
1906
|
+
applyProductSnapshot(em, record, before);
|
|
1907
|
+
await em.flush();
|
|
1908
|
+
|
|
1909
|
+
const relationEm = em.fork();
|
|
1910
|
+
const relationRecord = await findOneWithDecryption(relationEm, CatalogProduct, { id: before.id });
|
|
1911
|
+
if (relationRecord) {
|
|
1912
|
+
await restoreOffersFromSnapshot(relationEm, relationRecord, before.offers);
|
|
1913
|
+
await syncCategoryAssignments(relationEm, relationRecord, before.categoryIds);
|
|
1914
|
+
await syncProductTags(relationEm, relationRecord, before.tags);
|
|
1915
|
+
await relationEm.flush();
|
|
1458
1916
|
}
|
|
1459
|
-
|
|
1460
|
-
ensureOrganizationScope(ctx, before.organizationId)
|
|
1461
|
-
applyProductSnapshot(em, record, before)
|
|
1462
|
-
await restoreOffersFromSnapshot(em, record, before.offers)
|
|
1463
|
-
await syncCategoryAssignments(em, record, before.categoryIds)
|
|
1464
|
-
await syncProductTags(em, record, before.tags)
|
|
1465
|
-
await em.flush()
|
|
1466
|
-
const dataEngine = ctx.container.resolve('dataEngine') as DataEngine
|
|
1917
|
+
const dataEngine = ctx.container.resolve("dataEngine") as DataEngine;
|
|
1467
1918
|
if (before.custom && Object.keys(before.custom).length) {
|
|
1468
1919
|
await setCustomFieldsIfAny({
|
|
1469
1920
|
dataEngine,
|
|
@@ -1472,69 +1923,76 @@ const deleteProductCommand: CommandHandler<
|
|
|
1472
1923
|
organizationId: before.organizationId,
|
|
1473
1924
|
tenantId: before.tenantId,
|
|
1474
1925
|
values: before.custom,
|
|
1475
|
-
})
|
|
1926
|
+
});
|
|
1476
1927
|
}
|
|
1477
1928
|
await emitProductCrudUndoChange({
|
|
1478
1929
|
dataEngine,
|
|
1479
|
-
action:
|
|
1930
|
+
action: "created",
|
|
1480
1931
|
product: record,
|
|
1481
|
-
})
|
|
1932
|
+
});
|
|
1482
1933
|
},
|
|
1483
|
-
}
|
|
1934
|
+
};
|
|
1484
1935
|
|
|
1485
|
-
registerCommand(createProductCommand)
|
|
1486
|
-
registerCommand(updateProductCommand)
|
|
1487
|
-
registerCommand(deleteProductCommand)
|
|
1936
|
+
registerCommand(createProductCommand);
|
|
1937
|
+
registerCommand(updateProductCommand);
|
|
1938
|
+
registerCommand(deleteProductCommand);
|
|
1488
1939
|
|
|
1489
|
-
function resolveProductUniqueConstraint(
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
if (constraint ===
|
|
1495
|
-
if (constraint ===
|
|
1496
|
-
const message =
|
|
1497
|
-
? (error as { message?: string }).message
|
|
1498
|
-
: ''
|
|
1499
|
-
const normalized = message ? message.toLowerCase() : ''
|
|
1940
|
+
function resolveProductUniqueConstraint(
|
|
1941
|
+
error: unknown,
|
|
1942
|
+
): "handle" | "sku" | null {
|
|
1943
|
+
if (!(error instanceof UniqueConstraintViolationException)) return null;
|
|
1944
|
+
const constraint = getErrorConstraint(error);
|
|
1945
|
+
if (constraint === "catalog_products_handle_scope_unique") return "handle";
|
|
1946
|
+
if (constraint === "catalog_products_sku_scope_unique") return "sku";
|
|
1947
|
+
const message = getErrorMessage(error).toLowerCase();
|
|
1500
1948
|
if (
|
|
1501
|
-
|
|
1502
|
-
|
|
1949
|
+
message.includes("catalog_products_handle_scope_unique") ||
|
|
1950
|
+
message.includes(" handle")
|
|
1503
1951
|
) {
|
|
1504
|
-
return
|
|
1952
|
+
return "handle";
|
|
1505
1953
|
}
|
|
1506
1954
|
if (
|
|
1507
|
-
|
|
1508
|
-
|
|
1955
|
+
message.includes("catalog_products_sku_scope_unique") ||
|
|
1956
|
+
message.includes(" sku")
|
|
1509
1957
|
) {
|
|
1510
|
-
return
|
|
1958
|
+
return "sku";
|
|
1511
1959
|
}
|
|
1512
|
-
return null
|
|
1960
|
+
return null;
|
|
1513
1961
|
}
|
|
1514
1962
|
|
|
1515
1963
|
async function rethrowProductUniqueConstraint(error: unknown): Promise<never> {
|
|
1516
|
-
const target = resolveProductUniqueConstraint(error)
|
|
1517
|
-
if (target ===
|
|
1518
|
-
if (target ===
|
|
1519
|
-
throw error
|
|
1964
|
+
const target = resolveProductUniqueConstraint(error);
|
|
1965
|
+
if (target === "handle") await throwDuplicateHandleError();
|
|
1966
|
+
if (target === "sku") await throwDuplicateSkuError();
|
|
1967
|
+
throw error;
|
|
1520
1968
|
}
|
|
1521
1969
|
|
|
1522
1970
|
async function throwDuplicateHandleError(): Promise<never> {
|
|
1523
|
-
const { translate } = await resolveTranslations()
|
|
1524
|
-
const message = translate(
|
|
1971
|
+
const { translate } = await resolveTranslations();
|
|
1972
|
+
const message = translate(
|
|
1973
|
+
"catalog.products.errors.handleExists",
|
|
1974
|
+
"Handle already in use.",
|
|
1975
|
+
);
|
|
1525
1976
|
throw new CrudHttpError(400, {
|
|
1526
1977
|
error: message,
|
|
1527
1978
|
fieldErrors: { handle: message },
|
|
1528
|
-
details: [
|
|
1529
|
-
|
|
1979
|
+
details: [
|
|
1980
|
+
{ path: ["handle"], message, code: "duplicate", origin: "validation" },
|
|
1981
|
+
],
|
|
1982
|
+
});
|
|
1530
1983
|
}
|
|
1531
1984
|
|
|
1532
1985
|
async function throwDuplicateSkuError(): Promise<never> {
|
|
1533
|
-
const { translate } = await resolveTranslations()
|
|
1534
|
-
const message = translate(
|
|
1986
|
+
const { translate } = await resolveTranslations();
|
|
1987
|
+
const message = translate(
|
|
1988
|
+
"catalog.products.errors.skuExists",
|
|
1989
|
+
"SKU already in use.",
|
|
1990
|
+
);
|
|
1535
1991
|
throw new CrudHttpError(400, {
|
|
1536
1992
|
error: message,
|
|
1537
1993
|
fieldErrors: { sku: message },
|
|
1538
|
-
details: [
|
|
1539
|
-
|
|
1994
|
+
details: [
|
|
1995
|
+
{ path: ["sku"], message, code: "duplicate", origin: "validation" },
|
|
1996
|
+
],
|
|
1997
|
+
});
|
|
1540
1998
|
}
|