@open-mercato/core 0.4.5-develop-3ce83a8b24 → 0.4.5-develop-539cff4960
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/dist/modules/staff/backend/staff/team-members/[id]/page.js +28 -15
- package/dist/modules/staff/backend/staff/team-members/[id]/page.js.map +2 -2
- package/dist/modules/staff/translations.js +9 -0
- package/dist/modules/staff/translations.js.map +7 -0
- package/dist/modules/translations/components/TranslationDrawerAction.js +97 -0
- package/dist/modules/translations/components/TranslationDrawerAction.js.map +7 -0
- package/dist/modules/translations/lib/extract-record-id.js +31 -2
- package/dist/modules/translations/lib/extract-record-id.js.map +2 -2
- package/dist/modules/translations/lib/resolve-field-list.js +3 -0
- package/dist/modules/translations/lib/resolve-field-list.js.map +2 -2
- package/dist/modules/translations/widgets/injection/translation-manager/widget.client.js +105 -36
- package/dist/modules/translations/widgets/injection/translation-manager/widget.client.js.map +2 -2
- package/dist/modules/translations/widgets/injection-table.js +18 -29
- package/dist/modules/translations/widgets/injection-table.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/staff/backend/staff/team-members/[id]/page.tsx +8 -0
- package/src/modules/staff/translations.ts +5 -0
- package/src/modules/translations/components/TranslationDrawerAction.tsx +107 -0
- package/src/modules/translations/lib/extract-record-id.ts +47 -3
- package/src/modules/translations/lib/resolve-field-list.ts +4 -0
- package/src/modules/translations/widgets/injection/translation-manager/widget.client.tsx +108 -36
- package/src/modules/translations/widgets/injection-table.ts +19 -33
- 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,29 +1,36 @@
|
|
|
1
|
-
import { z } from
|
|
2
|
-
import type { EntityManager } from
|
|
3
|
-
import { makeCrudRoute } from
|
|
4
|
-
import { CrudHttpError } from
|
|
5
|
-
import {
|
|
6
|
-
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { EntityManager } from "@mikro-orm/postgresql";
|
|
3
|
+
import { makeCrudRoute } from "@open-mercato/shared/lib/crud/factory";
|
|
4
|
+
import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
5
|
+
import {
|
|
6
|
+
buildCustomFieldFiltersFromQuery,
|
|
7
|
+
extractAllCustomFieldEntries,
|
|
8
|
+
} from "@open-mercato/shared/lib/crud/custom-fields";
|
|
9
|
+
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
7
10
|
import {
|
|
8
11
|
CatalogOffer,
|
|
9
12
|
CatalogProduct,
|
|
10
13
|
CatalogProductCategory,
|
|
11
14
|
CatalogProductCategoryAssignment,
|
|
12
15
|
CatalogProductPrice,
|
|
16
|
+
CatalogProductUnitConversion,
|
|
13
17
|
CatalogProductVariant,
|
|
14
18
|
CatalogProductTagAssignment,
|
|
15
|
-
} from
|
|
16
|
-
import { CATALOG_PRODUCT_TYPES } from
|
|
17
|
-
import type { CatalogProductType } from
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
import
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import
|
|
26
|
-
import {
|
|
19
|
+
} from "../../data/entities";
|
|
20
|
+
import { CATALOG_PRODUCT_TYPES } from "../../data/types";
|
|
21
|
+
import type { CatalogProductType } from "../../data/types";
|
|
22
|
+
import {
|
|
23
|
+
productCreateSchema,
|
|
24
|
+
productUpdateSchema,
|
|
25
|
+
} from "../../data/validators";
|
|
26
|
+
import { parseScopedCommandInput, resolveCrudRecordId } from "../utils";
|
|
27
|
+
import { splitCustomFieldPayload } from "@open-mercato/shared/lib/crud/custom-fields";
|
|
28
|
+
import { E } from "#generated/entities.ids.generated";
|
|
29
|
+
import * as F from "#generated/entities/catalog_product";
|
|
30
|
+
import { parseBooleanFlag, sanitizeSearchTerm } from "../helpers";
|
|
31
|
+
import { escapeLikePattern } from "@open-mercato/shared/lib/db/escapeLikePattern";
|
|
32
|
+
import type { CrudCtx } from "@open-mercato/shared/lib/crud/factory";
|
|
33
|
+
import { buildScopedWhere } from "@open-mercato/shared/lib/api/crud";
|
|
27
34
|
import {
|
|
28
35
|
resolvePriceChannelId,
|
|
29
36
|
resolvePriceOfferId,
|
|
@@ -31,19 +38,21 @@ import {
|
|
|
31
38
|
resolvePriceKindCode,
|
|
32
39
|
type PricingContext,
|
|
33
40
|
type PriceRow,
|
|
34
|
-
} from
|
|
35
|
-
import type { CatalogPricingService } from
|
|
36
|
-
import { fieldsetCodeRegex } from
|
|
37
|
-
import { SalesChannel } from
|
|
41
|
+
} from "../../lib/pricing";
|
|
42
|
+
import type { CatalogPricingService } from "../../services/catalogPricingService";
|
|
43
|
+
import { fieldsetCodeRegex } from "@open-mercato/core/modules/entities/data/validators";
|
|
44
|
+
import { SalesChannel } from "@open-mercato/core/modules/sales/data/entities";
|
|
38
45
|
import {
|
|
39
46
|
createCatalogCrudOpenApi,
|
|
40
47
|
createPagedListResponseSchema,
|
|
41
48
|
defaultOkResponseSchema,
|
|
42
|
-
} from
|
|
43
|
-
import { findWithDecryption } from
|
|
44
|
-
|
|
49
|
+
} from "../openapi";
|
|
50
|
+
import { findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
51
|
+
import { canonicalizeUnitCode, toUnitLookupKey } from "../../lib/unitCodes";
|
|
52
|
+
const rawBodySchema = z.object({}).passthrough();
|
|
45
53
|
|
|
46
|
-
const UUID_REGEX =
|
|
54
|
+
const UUID_REGEX =
|
|
55
|
+
/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/;
|
|
47
56
|
|
|
48
57
|
const listSchema = z
|
|
49
58
|
.object({
|
|
@@ -65,186 +74,221 @@ const listSchema = z
|
|
|
65
74
|
customerId: z.string().uuid().optional(),
|
|
66
75
|
customerGroupId: z.string().uuid().optional(),
|
|
67
76
|
quantity: z.coerce.number().min(1).max(100000).optional(),
|
|
77
|
+
quantityUnit: z.string().trim().max(50).optional(),
|
|
68
78
|
priceDate: z.string().optional(),
|
|
69
79
|
sortField: z.string().optional(),
|
|
70
|
-
sortDir: z.enum([
|
|
80
|
+
sortDir: z.enum(["asc", "desc"]).optional(),
|
|
71
81
|
withDeleted: z.coerce.boolean().optional(),
|
|
72
82
|
customFieldset: z.string().regex(fieldsetCodeRegex).optional(),
|
|
73
83
|
})
|
|
74
|
-
.passthrough()
|
|
84
|
+
.passthrough();
|
|
75
85
|
|
|
76
|
-
type ProductsQuery = z.infer<typeof listSchema
|
|
86
|
+
type ProductsQuery = z.infer<typeof listSchema>;
|
|
77
87
|
|
|
78
88
|
const routeMetadata = {
|
|
79
|
-
GET: { requireAuth: true, requireFeatures: [
|
|
80
|
-
POST: { requireAuth: true, requireFeatures: [
|
|
81
|
-
PUT: { requireAuth: true, requireFeatures: [
|
|
82
|
-
DELETE: { requireAuth: true, requireFeatures: [
|
|
83
|
-
}
|
|
89
|
+
GET: { requireAuth: true, requireFeatures: ["catalog.products.view"] },
|
|
90
|
+
POST: { requireAuth: true, requireFeatures: ["catalog.products.manage"] },
|
|
91
|
+
PUT: { requireAuth: true, requireFeatures: ["catalog.products.manage"] },
|
|
92
|
+
DELETE: { requireAuth: true, requireFeatures: ["catalog.products.manage"] },
|
|
93
|
+
};
|
|
84
94
|
|
|
85
|
-
export const metadata = routeMetadata
|
|
95
|
+
export const metadata = routeMetadata;
|
|
86
96
|
|
|
87
97
|
export function parseIdList(raw?: string): string[] {
|
|
88
|
-
if (!raw) return []
|
|
98
|
+
if (!raw) return [];
|
|
89
99
|
return raw
|
|
90
|
-
.split(
|
|
100
|
+
.split(",")
|
|
91
101
|
.map((value) => value.trim())
|
|
92
|
-
.filter((value) => UUID_REGEX.test(value))
|
|
102
|
+
.filter((value) => UUID_REGEX.test(value));
|
|
93
103
|
}
|
|
94
104
|
|
|
95
105
|
export async function buildProductFilters(
|
|
96
106
|
query: ProductsQuery,
|
|
97
|
-
ctx: CrudCtx
|
|
107
|
+
ctx: CrudCtx,
|
|
98
108
|
): Promise<Record<string, unknown>> {
|
|
99
|
-
const filters: Record<string, unknown> = {}
|
|
100
|
-
const em = (ctx.container.resolve(
|
|
101
|
-
const restrictedProductIds: { value: Set<string> | null } = { value: null }
|
|
109
|
+
const filters: Record<string, unknown> = {};
|
|
110
|
+
const em = (ctx.container.resolve("em") as EntityManager).fork();
|
|
111
|
+
const restrictedProductIds: { value: Set<string> | null } = { value: null };
|
|
102
112
|
|
|
103
113
|
const intersectProductIds = (ids: string[]) => {
|
|
104
114
|
const normalized = ids.filter(
|
|
105
|
-
(id): id is string => typeof id ===
|
|
106
|
-
)
|
|
107
|
-
const current = new Set(normalized)
|
|
115
|
+
(id): id is string => typeof id === "string" && id.trim().length > 0,
|
|
116
|
+
);
|
|
117
|
+
const current = new Set(normalized);
|
|
108
118
|
if (!current.size) {
|
|
109
|
-
restrictedProductIds.value = new Set()
|
|
110
|
-
return
|
|
119
|
+
restrictedProductIds.value = new Set();
|
|
120
|
+
return;
|
|
111
121
|
}
|
|
112
122
|
if (!restrictedProductIds.value) {
|
|
113
|
-
restrictedProductIds.value = current
|
|
114
|
-
return
|
|
123
|
+
restrictedProductIds.value = current;
|
|
124
|
+
return;
|
|
115
125
|
}
|
|
116
126
|
restrictedProductIds.value = new Set(
|
|
117
|
-
Array.from(restrictedProductIds.value).filter((id) => current.has(id))
|
|
118
|
-
)
|
|
119
|
-
}
|
|
127
|
+
Array.from(restrictedProductIds.value).filter((id) => current.has(id)),
|
|
128
|
+
);
|
|
129
|
+
};
|
|
120
130
|
|
|
121
131
|
const applyRestrictedProducts = () => {
|
|
122
|
-
if (!restrictedProductIds.value) return
|
|
132
|
+
if (!restrictedProductIds.value) return;
|
|
123
133
|
if (restrictedProductIds.value.size === 0) {
|
|
124
|
-
filters.id = { $eq:
|
|
125
|
-
return
|
|
134
|
+
filters.id = { $eq: "00000000-0000-0000-0000-000000000000" };
|
|
135
|
+
return;
|
|
126
136
|
}
|
|
127
|
-
const ids = Array.from(restrictedProductIds.value)
|
|
128
|
-
const existing = filters.id as Record<string, unknown> | undefined
|
|
129
|
-
if (existing && typeof existing ===
|
|
130
|
-
if (
|
|
131
|
-
|
|
137
|
+
const ids = Array.from(restrictedProductIds.value);
|
|
138
|
+
const existing = filters.id as Record<string, unknown> | undefined;
|
|
139
|
+
if (existing && typeof existing === "object") {
|
|
140
|
+
if (
|
|
141
|
+
"$eq" in existing &&
|
|
142
|
+
typeof (existing as { $eq?: unknown }).$eq === "string"
|
|
143
|
+
) {
|
|
144
|
+
const target = (existing as { $eq: string }).$eq;
|
|
132
145
|
if (!restrictedProductIds.value.has(target)) {
|
|
133
|
-
filters.id = { $eq:
|
|
146
|
+
filters.id = { $eq: "00000000-0000-0000-0000-000000000000" };
|
|
134
147
|
}
|
|
135
|
-
return
|
|
148
|
+
return;
|
|
136
149
|
}
|
|
137
|
-
if (
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
150
|
+
if (
|
|
151
|
+
"$in" in existing &&
|
|
152
|
+
Array.isArray((existing as { $in?: unknown }).$in)
|
|
153
|
+
) {
|
|
154
|
+
const subset = (existing as { $in: string[] }).$in.filter((id) =>
|
|
155
|
+
restrictedProductIds.value!.has(id),
|
|
156
|
+
);
|
|
141
157
|
filters.id = subset.length
|
|
142
158
|
? { $in: subset }
|
|
143
|
-
: { $eq:
|
|
144
|
-
return
|
|
159
|
+
: { $eq: "00000000-0000-0000-0000-000000000000" };
|
|
160
|
+
return;
|
|
145
161
|
}
|
|
146
162
|
}
|
|
147
|
-
filters.id = ids.length === 1 ? { $eq: ids[0] } : { $in: ids }
|
|
148
|
-
}
|
|
163
|
+
filters.id = ids.length === 1 ? { $eq: ids[0] } : { $in: ids };
|
|
164
|
+
};
|
|
149
165
|
if (query.id) {
|
|
150
|
-
filters.id = { $eq: query.id }
|
|
166
|
+
filters.id = { $eq: query.id };
|
|
151
167
|
}
|
|
152
|
-
const term = sanitizeSearchTerm(query.search)
|
|
168
|
+
const term = sanitizeSearchTerm(query.search);
|
|
153
169
|
if (term) {
|
|
154
|
-
const like = `%${escapeLikePattern(term)}
|
|
170
|
+
const like = `%${escapeLikePattern(term)}%`;
|
|
155
171
|
filters.$or = [
|
|
156
172
|
{ title: { $ilike: like } },
|
|
157
173
|
{ subtitle: { $ilike: like } },
|
|
158
174
|
{ sku: { $ilike: like } },
|
|
159
175
|
{ handle: { $ilike: like } },
|
|
160
176
|
{ description: { $ilike: like } },
|
|
161
|
-
]
|
|
177
|
+
];
|
|
162
178
|
}
|
|
163
179
|
if (query.status && query.status.trim()) {
|
|
164
|
-
filters.status_entry_id = { $eq: query.status.trim() }
|
|
180
|
+
filters.status_entry_id = { $eq: query.status.trim() };
|
|
165
181
|
}
|
|
166
|
-
const active = parseBooleanFlag(query.isActive)
|
|
182
|
+
const active = parseBooleanFlag(query.isActive);
|
|
167
183
|
if (active !== undefined) {
|
|
168
|
-
filters.is_active = active
|
|
184
|
+
filters.is_active = active;
|
|
169
185
|
}
|
|
170
|
-
const configurable = parseBooleanFlag(query.configurable)
|
|
186
|
+
const configurable = parseBooleanFlag(query.configurable);
|
|
171
187
|
if (configurable !== undefined) {
|
|
172
|
-
filters.is_configurable = configurable
|
|
188
|
+
filters.is_configurable = configurable;
|
|
173
189
|
}
|
|
174
190
|
if (query.productType) {
|
|
175
|
-
filters.product_type = { $eq: query.productType }
|
|
191
|
+
filters.product_type = { $eq: query.productType };
|
|
176
192
|
}
|
|
177
|
-
const
|
|
193
|
+
const scope = {
|
|
194
|
+
organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,
|
|
195
|
+
tenantId: ctx.auth?.tenantId ?? null,
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const channelFilterIds = parseIdList(query.channelIds);
|
|
178
199
|
if (channelFilterIds.length) {
|
|
179
|
-
const offerRows = await
|
|
200
|
+
const offerRows = await findWithDecryption(
|
|
201
|
+
em,
|
|
180
202
|
CatalogOffer,
|
|
181
203
|
{
|
|
182
204
|
channelId: { $in: channelFilterIds },
|
|
183
205
|
deletedAt: null,
|
|
206
|
+
...scope,
|
|
184
207
|
},
|
|
185
|
-
{ fields: [
|
|
186
|
-
|
|
208
|
+
{ fields: ["id", "product"] },
|
|
209
|
+
scope,
|
|
210
|
+
);
|
|
187
211
|
const productIds = offerRows
|
|
188
|
-
.map((offer) =>
|
|
189
|
-
|
|
190
|
-
|
|
212
|
+
.map((offer) =>
|
|
213
|
+
typeof offer.product === "string"
|
|
214
|
+
? offer.product
|
|
215
|
+
: (offer.product?.id ?? null),
|
|
216
|
+
)
|
|
217
|
+
.filter((id): id is string => !!id);
|
|
218
|
+
intersectProductIds(productIds);
|
|
191
219
|
}
|
|
192
220
|
|
|
193
|
-
const categoryFilterIds = parseIdList(query.categoryIds)
|
|
221
|
+
const categoryFilterIds = parseIdList(query.categoryIds);
|
|
194
222
|
if (categoryFilterIds.length) {
|
|
195
|
-
const assignments = await
|
|
223
|
+
const assignments = await findWithDecryption(
|
|
224
|
+
em,
|
|
196
225
|
CatalogProductCategoryAssignment,
|
|
197
|
-
{ category: { $in: categoryFilterIds } },
|
|
198
|
-
{ fields: [
|
|
199
|
-
|
|
226
|
+
{ category: { $in: categoryFilterIds }, ...scope },
|
|
227
|
+
{ fields: ["id", "product"] },
|
|
228
|
+
scope,
|
|
229
|
+
);
|
|
200
230
|
const productIds = assignments
|
|
201
231
|
.map((assignment) =>
|
|
202
|
-
typeof assignment.product ===
|
|
232
|
+
typeof assignment.product === "string"
|
|
233
|
+
? assignment.product
|
|
234
|
+
: (assignment.product?.id ?? null),
|
|
203
235
|
)
|
|
204
|
-
.filter((id): id is string => !!id)
|
|
205
|
-
intersectProductIds(productIds)
|
|
236
|
+
.filter((id): id is string => !!id);
|
|
237
|
+
intersectProductIds(productIds);
|
|
206
238
|
}
|
|
207
239
|
|
|
208
|
-
const tagFilterIds = parseIdList(query.tagIds)
|
|
240
|
+
const tagFilterIds = parseIdList(query.tagIds);
|
|
209
241
|
if (tagFilterIds.length) {
|
|
210
|
-
const assignments = await
|
|
242
|
+
const assignments = await findWithDecryption(
|
|
243
|
+
em,
|
|
211
244
|
CatalogProductTagAssignment,
|
|
212
|
-
{ tag: { $in: tagFilterIds } },
|
|
213
|
-
{ fields: [
|
|
214
|
-
|
|
245
|
+
{ tag: { $in: tagFilterIds }, ...scope },
|
|
246
|
+
{ fields: ["id", "product"] },
|
|
247
|
+
scope,
|
|
248
|
+
);
|
|
215
249
|
const productIds = assignments
|
|
216
250
|
.map((assignment) =>
|
|
217
|
-
typeof assignment.product ===
|
|
251
|
+
typeof assignment.product === "string"
|
|
252
|
+
? assignment.product
|
|
253
|
+
: (assignment.product?.id ?? null),
|
|
218
254
|
)
|
|
219
|
-
.filter((id): id is string => !!id)
|
|
220
|
-
intersectProductIds(productIds)
|
|
255
|
+
.filter((id): id is string => !!id);
|
|
256
|
+
intersectProductIds(productIds);
|
|
221
257
|
}
|
|
222
258
|
const customFieldset =
|
|
223
|
-
typeof query.customFieldset ===
|
|
259
|
+
typeof query.customFieldset === "string" &&
|
|
260
|
+
query.customFieldset.trim().length
|
|
224
261
|
? query.customFieldset.trim()
|
|
225
|
-
: null
|
|
226
|
-
const tenantId = ctx.auth?.tenantId ?? null
|
|
262
|
+
: null;
|
|
263
|
+
const tenantId = ctx.auth?.tenantId ?? null;
|
|
227
264
|
try {
|
|
228
|
-
const scopedEm = ctx.container.resolve(
|
|
265
|
+
const scopedEm = ctx.container.resolve("em") as EntityManager;
|
|
229
266
|
const cfFilters = await buildCustomFieldFiltersFromQuery({
|
|
230
267
|
entityIds: [E.catalog.catalog_product],
|
|
231
268
|
query,
|
|
232
269
|
em: scopedEm,
|
|
233
270
|
tenantId,
|
|
234
271
|
fieldset: customFieldset ?? undefined,
|
|
235
|
-
})
|
|
236
|
-
Object.assign(filters, cfFilters)
|
|
237
|
-
} catch {
|
|
238
|
-
//
|
|
272
|
+
});
|
|
273
|
+
Object.assign(filters, cfFilters);
|
|
274
|
+
} catch (err) {
|
|
275
|
+
// Custom field filter parsing may fail for non-existent or misconfigured fields.
|
|
276
|
+
// Fall back to base filters to avoid blocking the product listing.
|
|
277
|
+
if (process.env.NODE_ENV === 'development') console.warn('[catalog:products] custom field filter error', err);
|
|
239
278
|
}
|
|
240
|
-
applyRestrictedProducts()
|
|
241
|
-
return filters
|
|
279
|
+
applyRestrictedProducts();
|
|
280
|
+
return filters;
|
|
242
281
|
}
|
|
243
282
|
|
|
244
|
-
export function buildPricingContext(
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
283
|
+
export function buildPricingContext(
|
|
284
|
+
query: ProductsQuery,
|
|
285
|
+
channelFallback: string | null,
|
|
286
|
+
): PricingContext {
|
|
287
|
+
const quantity = Number.isFinite(Number(query.quantity))
|
|
288
|
+
? Number(query.quantity)
|
|
289
|
+
: 1;
|
|
290
|
+
const parsedDate = query.priceDate ? new Date(query.priceDate) : new Date();
|
|
291
|
+
const channelId = query.channelId ?? channelFallback ?? null;
|
|
248
292
|
return {
|
|
249
293
|
channelId,
|
|
250
294
|
offerId: query.offerId ?? null,
|
|
@@ -254,267 +298,397 @@ export function buildPricingContext(query: ProductsQuery, channelFallback: strin
|
|
|
254
298
|
customerGroupId: query.customerGroupId ?? null,
|
|
255
299
|
quantity: Number.isFinite(quantity) && quantity > 0 ? quantity : 1,
|
|
256
300
|
date: Number.isNaN(parsedDate.getTime()) ? new Date() : parsedDate,
|
|
257
|
-
}
|
|
301
|
+
};
|
|
258
302
|
}
|
|
259
303
|
|
|
260
|
-
|
|
261
304
|
type ProductListItem = Record<string, unknown> & {
|
|
262
|
-
id?: string
|
|
263
|
-
title?: string | null
|
|
264
|
-
subtitle?: string | null
|
|
265
|
-
description?: string | null
|
|
266
|
-
sku?: string | null
|
|
267
|
-
handle?: string | null
|
|
268
|
-
product_type?: CatalogProductType | null
|
|
269
|
-
primary_currency_code?: string | null
|
|
270
|
-
default_unit?: string | null
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
305
|
+
id?: string;
|
|
306
|
+
title?: string | null;
|
|
307
|
+
subtitle?: string | null;
|
|
308
|
+
description?: string | null;
|
|
309
|
+
sku?: string | null;
|
|
310
|
+
handle?: string | null;
|
|
311
|
+
product_type?: CatalogProductType | null;
|
|
312
|
+
primary_currency_code?: string | null;
|
|
313
|
+
default_unit?: string | null;
|
|
314
|
+
default_sales_unit?: string | null;
|
|
315
|
+
default_sales_unit_quantity?: number | null;
|
|
316
|
+
uom_rounding_scale?: number | null;
|
|
317
|
+
uom_rounding_mode?: "half_up" | "down" | "up" | null;
|
|
318
|
+
unit_price_enabled?: boolean | null;
|
|
319
|
+
unit_price_reference_unit?: "kg" | "l" | "m2" | "m3" | "pc" | null;
|
|
320
|
+
unit_price_base_quantity?: number | null;
|
|
321
|
+
default_media_id?: string | null;
|
|
322
|
+
default_media_url?: string | null;
|
|
323
|
+
weight_value?: string | null;
|
|
324
|
+
weightValue?: string | null;
|
|
325
|
+
weight_unit?: string | null;
|
|
326
|
+
weightUnit?: string | null;
|
|
327
|
+
dimensions?: Record<string, unknown> | null;
|
|
328
|
+
custom_fieldset_code?: string | null;
|
|
329
|
+
option_schema_id?: string | null;
|
|
330
|
+
offers?: Array<Record<string, unknown>>;
|
|
331
|
+
channelIds?: string[];
|
|
332
|
+
categories?: Array<Record<string, unknown>>;
|
|
333
|
+
categoryIds?: string[];
|
|
334
|
+
tags?: string[];
|
|
335
|
+
};
|
|
286
336
|
|
|
287
337
|
async function decorateProductsAfterList(
|
|
288
338
|
payload: { items?: ProductListItem[] },
|
|
289
|
-
ctx: CrudCtx & { query: ProductsQuery }
|
|
339
|
+
ctx: CrudCtx & { query: ProductsQuery },
|
|
290
340
|
): Promise<void> {
|
|
291
|
-
const items = Array.isArray(payload?.items) ? payload.items : []
|
|
292
|
-
if (!items.length) return
|
|
341
|
+
const items = Array.isArray(payload?.items) ? payload.items : [];
|
|
342
|
+
if (!items.length) return;
|
|
293
343
|
const productIds = items
|
|
294
|
-
.map((item) => (typeof item.id ===
|
|
295
|
-
.filter((id): id is string => !!id)
|
|
296
|
-
if (!productIds.length) return
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
344
|
+
.map((item) => (typeof item.id === "string" ? item.id : null))
|
|
345
|
+
.filter((id): id is string => !!id);
|
|
346
|
+
if (!productIds.length) return;
|
|
347
|
+
try {
|
|
348
|
+
const em = (ctx.container.resolve("em") as EntityManager).fork();
|
|
349
|
+
const scope = {
|
|
350
|
+
organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,
|
|
351
|
+
tenantId: ctx.auth?.tenantId ?? null,
|
|
352
|
+
};
|
|
353
|
+
const offers = await findWithDecryption(
|
|
354
|
+
em,
|
|
355
|
+
CatalogOffer,
|
|
356
|
+
{ product: { $in: productIds }, deletedAt: null, ...scope },
|
|
357
|
+
{ orderBy: { createdAt: "asc" } },
|
|
358
|
+
scope,
|
|
359
|
+
);
|
|
360
|
+
const channelIds = Array.from(
|
|
361
|
+
new Set(
|
|
362
|
+
offers
|
|
363
|
+
.map((offer) => offer.channelId)
|
|
364
|
+
.filter(
|
|
365
|
+
(id): id is string => typeof id === "string" && id.length > 0,
|
|
366
|
+
),
|
|
367
|
+
),
|
|
368
|
+
);
|
|
369
|
+
const channelLookup = new Map<
|
|
370
|
+
string,
|
|
371
|
+
{ name?: string | null; code?: string | null }
|
|
372
|
+
>();
|
|
373
|
+
if (channelIds.length) {
|
|
374
|
+
const scopedChannelsWhere = buildScopedWhere(
|
|
375
|
+
{ id: { $in: channelIds } },
|
|
376
|
+
{
|
|
377
|
+
organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,
|
|
378
|
+
organizationIds: Array.isArray(ctx.organizationIds)
|
|
379
|
+
? ctx.organizationIds
|
|
380
|
+
: undefined,
|
|
381
|
+
tenantId: ctx.auth?.tenantId ?? null,
|
|
382
|
+
},
|
|
383
|
+
);
|
|
384
|
+
const channels = await findWithDecryption(em, SalesChannel, scopedChannelsWhere, {
|
|
385
|
+
fields: ["id", "name", "code"],
|
|
386
|
+
});
|
|
387
|
+
for (const channel of channels) {
|
|
388
|
+
channelLookup.set(channel.id, {
|
|
389
|
+
name: channel.name,
|
|
390
|
+
code: channel.code ?? null,
|
|
391
|
+
});
|
|
318
392
|
}
|
|
319
|
-
)
|
|
320
|
-
const channels = await em.find(
|
|
321
|
-
SalesChannel,
|
|
322
|
-
scopedChannelsWhere,
|
|
323
|
-
{ fields: ['id', 'name', 'code'] }
|
|
324
|
-
)
|
|
325
|
-
for (const channel of channels) {
|
|
326
|
-
channelLookup.set(channel.id, {
|
|
327
|
-
name: channel.name,
|
|
328
|
-
code: channel.code ?? null,
|
|
329
|
-
})
|
|
330
393
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
const categoryAssignments = await em.find(
|
|
355
|
-
CatalogProductCategoryAssignment,
|
|
356
|
-
{ product: { $in: productIds } },
|
|
357
|
-
{ populate: ['category'], orderBy: { position: 'asc' } }
|
|
358
|
-
)
|
|
359
|
-
const parentIds = new Set<string>()
|
|
360
|
-
for (const assignment of categoryAssignments) {
|
|
361
|
-
const category =
|
|
362
|
-
typeof assignment.category === 'string' ? null : assignment.category ?? null
|
|
363
|
-
if (!category) continue
|
|
364
|
-
const parentId = category.parentId ?? null
|
|
365
|
-
if (parentId) parentIds.add(parentId)
|
|
366
|
-
}
|
|
367
|
-
const parentCategories = parentIds.size
|
|
368
|
-
? await em.find(
|
|
369
|
-
CatalogProductCategory,
|
|
370
|
-
{ id: { $in: Array.from(parentIds) } },
|
|
371
|
-
{ fields: ['id', 'name'] }
|
|
372
|
-
)
|
|
373
|
-
: []
|
|
374
|
-
const parentNameById = new Map<string, string | null>()
|
|
375
|
-
for (const parent of parentCategories) {
|
|
376
|
-
parentNameById.set(parent.id, parent.name ?? null)
|
|
377
|
-
}
|
|
378
|
-
const categoriesByProduct = new Map<
|
|
379
|
-
string,
|
|
380
|
-
Array<{ id: string; name: string | null; treePath: string | null; parentId: string | null; parentName: string | null }>
|
|
381
|
-
>()
|
|
382
|
-
for (const assignment of categoryAssignments) {
|
|
383
|
-
const productId =
|
|
384
|
-
typeof assignment.product === 'string' ? assignment.product : assignment.product?.id ?? null
|
|
385
|
-
if (!productId) continue
|
|
386
|
-
const category =
|
|
387
|
-
typeof assignment.category === 'string' ? null : assignment.category ?? null
|
|
388
|
-
if (!category) continue
|
|
389
|
-
const parentId = category.parentId ?? null
|
|
390
|
-
const parentName = parentId ? parentNameById.get(parentId) ?? null : null
|
|
391
|
-
const bucket = categoriesByProduct.get(productId) ?? []
|
|
392
|
-
bucket.push({
|
|
393
|
-
id: category.id,
|
|
394
|
-
name: category.name ?? null,
|
|
395
|
-
treePath: category.treePath ?? null,
|
|
396
|
-
parentId,
|
|
397
|
-
parentName,
|
|
398
|
-
})
|
|
399
|
-
categoriesByProduct.set(productId, bucket)
|
|
400
|
-
}
|
|
394
|
+
const offersByProduct = new Map<string, Array<Record<string, unknown>>>();
|
|
395
|
+
for (const offer of offers) {
|
|
396
|
+
const productId =
|
|
397
|
+
typeof offer.product === "string"
|
|
398
|
+
? offer.product
|
|
399
|
+
: (offer.product?.id ?? null);
|
|
400
|
+
if (!productId) continue;
|
|
401
|
+
const channelInfo = channelLookup.get(offer.channelId);
|
|
402
|
+
const entry = offersByProduct.get(productId) ?? [];
|
|
403
|
+
entry.push({
|
|
404
|
+
id: offer.id,
|
|
405
|
+
channelId: offer.channelId,
|
|
406
|
+
channelName: channelInfo?.name ?? null,
|
|
407
|
+
channelCode: channelInfo?.code ?? null,
|
|
408
|
+
title: offer.title,
|
|
409
|
+
description: offer.description ?? null,
|
|
410
|
+
isActive: offer.isActive,
|
|
411
|
+
defaultMediaId: offer.defaultMediaId ?? null,
|
|
412
|
+
defaultMediaUrl: offer.defaultMediaUrl ?? null,
|
|
413
|
+
metadata: offer.metadata ?? null,
|
|
414
|
+
});
|
|
415
|
+
offersByProduct.set(productId, entry);
|
|
416
|
+
}
|
|
401
417
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
const
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
418
|
+
const categoryAssignments = await findWithDecryption(
|
|
419
|
+
em,
|
|
420
|
+
CatalogProductCategoryAssignment,
|
|
421
|
+
{ product: { $in: productIds }, ...scope },
|
|
422
|
+
{ populate: ["category"], orderBy: { position: "asc" } },
|
|
423
|
+
scope,
|
|
424
|
+
);
|
|
425
|
+
const parentIds = new Set<string>();
|
|
426
|
+
for (const assignment of categoryAssignments) {
|
|
427
|
+
const category =
|
|
428
|
+
typeof assignment.category === "string"
|
|
429
|
+
? null
|
|
430
|
+
: (assignment.category ?? null);
|
|
431
|
+
if (!category) continue;
|
|
432
|
+
const parentId = category.parentId ?? null;
|
|
433
|
+
if (parentId) parentIds.add(parentId);
|
|
434
|
+
}
|
|
435
|
+
const parentCategories = parentIds.size
|
|
436
|
+
? await findWithDecryption(
|
|
437
|
+
em,
|
|
438
|
+
CatalogProductCategory,
|
|
439
|
+
{ id: { $in: Array.from(parentIds) }, ...scope },
|
|
440
|
+
{ fields: ["id", "name"] },
|
|
441
|
+
scope,
|
|
442
|
+
)
|
|
443
|
+
: [];
|
|
444
|
+
const parentNameById = new Map<string, string | null>();
|
|
445
|
+
for (const parent of parentCategories) {
|
|
446
|
+
parentNameById.set(parent.id, parent.name ?? null);
|
|
447
|
+
}
|
|
448
|
+
const categoriesByProduct = new Map<
|
|
449
|
+
string,
|
|
450
|
+
Array<{
|
|
451
|
+
id: string;
|
|
452
|
+
name: string | null;
|
|
453
|
+
treePath: string | null;
|
|
454
|
+
parentId: string | null;
|
|
455
|
+
parentName: string | null;
|
|
456
|
+
}>
|
|
457
|
+
>();
|
|
458
|
+
for (const assignment of categoryAssignments) {
|
|
459
|
+
const productId =
|
|
460
|
+
typeof assignment.product === "string"
|
|
461
|
+
? assignment.product
|
|
462
|
+
: (assignment.product?.id ?? null);
|
|
463
|
+
if (!productId) continue;
|
|
464
|
+
const category =
|
|
465
|
+
typeof assignment.category === "string"
|
|
466
|
+
? null
|
|
467
|
+
: (assignment.category ?? null);
|
|
468
|
+
if (!category) continue;
|
|
469
|
+
const parentId = category.parentId ?? null;
|
|
470
|
+
const parentName = parentId
|
|
471
|
+
? (parentNameById.get(parentId) ?? null)
|
|
472
|
+
: null;
|
|
473
|
+
const bucket = categoriesByProduct.get(productId) ?? [];
|
|
474
|
+
bucket.push({
|
|
475
|
+
id: category.id,
|
|
476
|
+
name: category.name ?? null,
|
|
477
|
+
treePath: category.treePath ?? null,
|
|
478
|
+
parentId,
|
|
479
|
+
parentName,
|
|
480
|
+
});
|
|
481
|
+
categoriesByProduct.set(productId, bucket);
|
|
482
|
+
}
|
|
423
483
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
productId =
|
|
453
|
-
typeof price.product === 'string' ? price.product : price.product?.id ?? null
|
|
454
|
-
} else if (price.variant) {
|
|
455
|
-
const variantId = typeof price.variant === 'string' ? price.variant : price.variant.id
|
|
456
|
-
productId = variantToProduct.get(variantId) ?? null
|
|
484
|
+
const tagAssignments = await findWithDecryption(
|
|
485
|
+
em,
|
|
486
|
+
CatalogProductTagAssignment,
|
|
487
|
+
{ product: { $in: productIds } },
|
|
488
|
+
{ populate: ["tag"] },
|
|
489
|
+
{
|
|
490
|
+
tenantId: ctx.auth?.tenantId ?? null,
|
|
491
|
+
organizationId: ctx.auth?.orgId ?? null,
|
|
492
|
+
},
|
|
493
|
+
);
|
|
494
|
+
const tagsByProduct = new Map<string, string[]>();
|
|
495
|
+
for (const assignment of tagAssignments) {
|
|
496
|
+
const productId =
|
|
497
|
+
typeof assignment.product === "string"
|
|
498
|
+
? assignment.product
|
|
499
|
+
: (assignment.product?.id ?? null);
|
|
500
|
+
if (!productId) continue;
|
|
501
|
+
const tag =
|
|
502
|
+
typeof assignment.tag === "string" ? null : (assignment.tag ?? null);
|
|
503
|
+
if (!tag) continue;
|
|
504
|
+
const label =
|
|
505
|
+
typeof tag.label === "string" && tag.label.trim().length
|
|
506
|
+
? tag.label
|
|
507
|
+
: null;
|
|
508
|
+
if (!label) continue;
|
|
509
|
+
const bucket = tagsByProduct.get(productId) ?? [];
|
|
510
|
+
bucket.push(label);
|
|
511
|
+
tagsByProduct.set(productId, bucket);
|
|
457
512
|
}
|
|
458
|
-
if (!productId) continue
|
|
459
|
-
const entry = pricesByProduct.get(productId) ?? []
|
|
460
|
-
entry.push(price)
|
|
461
|
-
pricesByProduct.set(productId, entry)
|
|
462
|
-
}
|
|
463
513
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
514
|
+
const variants = await findWithDecryption(
|
|
515
|
+
em,
|
|
516
|
+
CatalogProductVariant,
|
|
517
|
+
{ product: { $in: productIds }, deletedAt: null, ...scope },
|
|
518
|
+
{ fields: ["id", "product"] },
|
|
519
|
+
scope,
|
|
520
|
+
);
|
|
521
|
+
const variantToProduct = new Map<string, string>();
|
|
522
|
+
for (const variant of variants) {
|
|
523
|
+
const productId =
|
|
524
|
+
typeof variant.product === "string"
|
|
525
|
+
? variant.product
|
|
526
|
+
: (variant.product?.id ?? null);
|
|
527
|
+
if (!productId) continue;
|
|
528
|
+
variantToProduct.set(variant.id, productId);
|
|
529
|
+
}
|
|
530
|
+
const variantIds = Array.from(variantToProduct.keys());
|
|
531
|
+
const priceWhere =
|
|
532
|
+
variantIds.length > 0
|
|
533
|
+
? {
|
|
534
|
+
$or: [
|
|
535
|
+
{ product: { $in: productIds } },
|
|
536
|
+
{ variant: { $in: variantIds } },
|
|
537
|
+
],
|
|
538
|
+
}
|
|
539
|
+
: { product: { $in: productIds } };
|
|
540
|
+
const priceRows = await findWithDecryption(
|
|
541
|
+
em,
|
|
542
|
+
CatalogProductPrice,
|
|
543
|
+
{ ...priceWhere, ...scope },
|
|
544
|
+
{ populate: ["offer", "variant", "product", "priceKind"] },
|
|
545
|
+
scope,
|
|
546
|
+
);
|
|
547
|
+
const pricesByProduct = new Map<string, PriceRow[]>();
|
|
548
|
+
for (const price of priceRows) {
|
|
549
|
+
let productId: string | null = null;
|
|
550
|
+
if (price.product) {
|
|
551
|
+
productId =
|
|
552
|
+
typeof price.product === "string"
|
|
553
|
+
? price.product
|
|
554
|
+
: (price.product?.id ?? null);
|
|
555
|
+
} else if (price.variant) {
|
|
556
|
+
const variantId =
|
|
557
|
+
typeof price.variant === "string" ? price.variant : price.variant.id;
|
|
558
|
+
productId = variantToProduct.get(variantId) ?? null;
|
|
559
|
+
}
|
|
560
|
+
if (!productId) continue;
|
|
561
|
+
const entry = pricesByProduct.get(productId) ?? [];
|
|
562
|
+
entry.push(price);
|
|
563
|
+
pricesByProduct.set(productId, entry);
|
|
564
|
+
}
|
|
469
565
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
const
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
const best = await pricingService.resolvePrice(priceCandidates, channelScopedContext)
|
|
493
|
-
if (best) {
|
|
494
|
-
item.pricing = {
|
|
495
|
-
kind: resolvePriceKindCode(best),
|
|
496
|
-
price_kind_id: typeof best.priceKind === 'string' ? best.priceKind : best.priceKind?.id ?? null,
|
|
497
|
-
price_kind_code: resolvePriceKindCode(best),
|
|
498
|
-
currency_code: best.currencyCode,
|
|
499
|
-
unit_price_net: best.unitPriceNet,
|
|
500
|
-
unit_price_gross: best.unitPriceGross,
|
|
501
|
-
min_quantity: best.minQuantity,
|
|
502
|
-
max_quantity: best.maxQuantity ?? null,
|
|
503
|
-
tax_rate: best.taxRate ?? null,
|
|
504
|
-
tax_amount: best.taxAmount ?? null,
|
|
505
|
-
scope: {
|
|
506
|
-
variant_id: resolvePriceVariantId(best),
|
|
507
|
-
offer_id: resolvePriceOfferId(best),
|
|
508
|
-
channel_id: resolvePriceChannelId(best),
|
|
509
|
-
user_id: best.userId ?? null,
|
|
510
|
-
user_group_id: best.userGroupId ?? null,
|
|
511
|
-
customer_id: best.customerId ?? null,
|
|
512
|
-
customer_group_id: best.customerGroupId ?? null,
|
|
566
|
+
const requestQuantityUnitKey = toUnitLookupKey(
|
|
567
|
+
ctx.query.quantityUnit,
|
|
568
|
+
);
|
|
569
|
+
const conversionsByProduct = new Map<string, Map<string, number>>();
|
|
570
|
+
const conversionOrganizationId =
|
|
571
|
+
ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null;
|
|
572
|
+
const conversionTenantId = ctx.auth?.tenantId ?? null;
|
|
573
|
+
if (
|
|
574
|
+
requestQuantityUnitKey &&
|
|
575
|
+
productIds.length &&
|
|
576
|
+
conversionOrganizationId &&
|
|
577
|
+
conversionTenantId
|
|
578
|
+
) {
|
|
579
|
+
const conversionRows = await findWithDecryption(
|
|
580
|
+
em,
|
|
581
|
+
CatalogProductUnitConversion,
|
|
582
|
+
{
|
|
583
|
+
product: { $in: productIds },
|
|
584
|
+
organizationId: conversionOrganizationId,
|
|
585
|
+
tenantId: conversionTenantId,
|
|
586
|
+
deletedAt: null,
|
|
587
|
+
isActive: true,
|
|
513
588
|
},
|
|
589
|
+
{ fields: ["id", "product", "unitCode", "toBaseFactor"] },
|
|
590
|
+
{ organizationId: conversionOrganizationId, tenantId: conversionTenantId },
|
|
591
|
+
);
|
|
592
|
+
for (const row of conversionRows) {
|
|
593
|
+
const productId =
|
|
594
|
+
typeof row.product === "string"
|
|
595
|
+
? row.product
|
|
596
|
+
: (row.product?.id ?? null);
|
|
597
|
+
const unitKey = toUnitLookupKey(row.unitCode);
|
|
598
|
+
const factor = Number(row.toBaseFactor);
|
|
599
|
+
if (!productId || !unitKey || !Number.isFinite(factor) || factor <= 0)
|
|
600
|
+
continue;
|
|
601
|
+
const bucket =
|
|
602
|
+
conversionsByProduct.get(productId) ?? new Map<string, number>();
|
|
603
|
+
bucket.set(unitKey, factor);
|
|
604
|
+
conversionsByProduct.set(productId, bucket);
|
|
514
605
|
}
|
|
515
|
-
} else {
|
|
516
|
-
item.pricing = null
|
|
517
606
|
}
|
|
607
|
+
|
|
608
|
+
const channelFilterIds = parseIdList(ctx.query.channelIds);
|
|
609
|
+
const channelContext =
|
|
610
|
+
ctx.query.channelId ??
|
|
611
|
+
(channelFilterIds.length === 1 ? channelFilterIds[0] : null);
|
|
612
|
+
const pricingContext = buildPricingContext(ctx.query, channelContext);
|
|
613
|
+
const pricingService = ctx.container.resolve<CatalogPricingService>(
|
|
614
|
+
"catalogPricingService",
|
|
615
|
+
);
|
|
616
|
+
|
|
617
|
+
for (const item of items) {
|
|
618
|
+
const id = typeof item.id === "string" ? item.id : null;
|
|
619
|
+
if (!id) continue;
|
|
620
|
+
const offerEntries = offersByProduct.get(id) ?? [];
|
|
621
|
+
item.offers = offerEntries;
|
|
622
|
+
const channelIds = Array.from(
|
|
623
|
+
new Set(
|
|
624
|
+
offerEntries
|
|
625
|
+
.map((offer) =>
|
|
626
|
+
typeof offer.channelId === "string" ? offer.channelId : null,
|
|
627
|
+
)
|
|
628
|
+
.filter((channelId): channelId is string => !!channelId),
|
|
629
|
+
),
|
|
630
|
+
);
|
|
631
|
+
item.channelIds = channelIds;
|
|
632
|
+
const categories = categoriesByProduct.get(id) ?? [];
|
|
633
|
+
item.categories = categories;
|
|
634
|
+
item.categoryIds = categories.map((category) => category.id);
|
|
635
|
+
item.tags = tagsByProduct.get(id) ?? [];
|
|
636
|
+
const priceCandidates = pricesByProduct.get(id) ?? [];
|
|
637
|
+
const normalizedQuantityForPricing = (() => {
|
|
638
|
+
if (!requestQuantityUnitKey) return pricingContext.quantity;
|
|
639
|
+
const baseUnit = toUnitLookupKey(item.default_unit);
|
|
640
|
+
if (!baseUnit || requestQuantityUnitKey === baseUnit)
|
|
641
|
+
return pricingContext.quantity;
|
|
642
|
+
const productConversions = conversionsByProduct.get(id);
|
|
643
|
+
const factor = productConversions?.get(requestQuantityUnitKey) ?? null;
|
|
644
|
+
if (!factor || !Number.isFinite(factor) || factor <= 0) {
|
|
645
|
+
if (process.env.NODE_ENV === 'development') console.warn(`[catalog.products] Invalid conversion factor for product=${id} unit=${requestQuantityUnitKey} factor=${factor}`);
|
|
646
|
+
return pricingContext.quantity;
|
|
647
|
+
}
|
|
648
|
+
const normalized = pricingContext.quantity * factor;
|
|
649
|
+
return Number.isFinite(normalized) && normalized > 0
|
|
650
|
+
? normalized
|
|
651
|
+
: pricingContext.quantity;
|
|
652
|
+
})();
|
|
653
|
+
const channelScopedContext =
|
|
654
|
+
pricingContext.channelId || channelIds.length !== 1
|
|
655
|
+
? pricingContext
|
|
656
|
+
: { ...pricingContext, channelId: channelIds[0] };
|
|
657
|
+
const best = await pricingService.resolvePrice(priceCandidates, {
|
|
658
|
+
...channelScopedContext,
|
|
659
|
+
quantity: normalizedQuantityForPricing,
|
|
660
|
+
});
|
|
661
|
+
if (best) {
|
|
662
|
+
item.pricing = {
|
|
663
|
+
kind: resolvePriceKindCode(best),
|
|
664
|
+
price_kind_id:
|
|
665
|
+
typeof best.priceKind === "string"
|
|
666
|
+
? best.priceKind
|
|
667
|
+
: (best.priceKind?.id ?? null),
|
|
668
|
+
price_kind_code: resolvePriceKindCode(best),
|
|
669
|
+
currency_code: best.currencyCode,
|
|
670
|
+
unit_price_net: best.unitPriceNet,
|
|
671
|
+
unit_price_gross: best.unitPriceGross,
|
|
672
|
+
min_quantity: best.minQuantity,
|
|
673
|
+
max_quantity: best.maxQuantity ?? null,
|
|
674
|
+
tax_rate: best.taxRate ?? null,
|
|
675
|
+
tax_amount: best.taxAmount ?? null,
|
|
676
|
+
scope: {
|
|
677
|
+
variant_id: resolvePriceVariantId(best),
|
|
678
|
+
offer_id: resolvePriceOfferId(best),
|
|
679
|
+
channel_id: resolvePriceChannelId(best),
|
|
680
|
+
user_id: best.userId ?? null,
|
|
681
|
+
user_group_id: best.userGroupId ?? null,
|
|
682
|
+
customer_id: best.customerId ?? null,
|
|
683
|
+
customer_group_id: best.customerGroupId ?? null,
|
|
684
|
+
},
|
|
685
|
+
};
|
|
686
|
+
} else {
|
|
687
|
+
item.pricing = null;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
} catch (error) {
|
|
691
|
+
console.error("[decorateProductsAfterList] Failed to load unit conversions", error);
|
|
518
692
|
}
|
|
519
693
|
}
|
|
520
694
|
|
|
@@ -522,10 +696,10 @@ const crud = makeCrudRoute({
|
|
|
522
696
|
metadata: routeMetadata,
|
|
523
697
|
orm: {
|
|
524
698
|
entity: CatalogProduct,
|
|
525
|
-
idField:
|
|
526
|
-
orgField:
|
|
527
|
-
tenantField:
|
|
528
|
-
softDeleteField:
|
|
699
|
+
idField: "id",
|
|
700
|
+
orgField: "organizationId",
|
|
701
|
+
tenantField: "tenantId",
|
|
702
|
+
softDeleteField: "deletedAt",
|
|
529
703
|
},
|
|
530
704
|
indexer: {
|
|
531
705
|
entityType: E.catalog.catalog_product,
|
|
@@ -540,12 +714,19 @@ const crud = makeCrudRoute({
|
|
|
540
714
|
F.description,
|
|
541
715
|
F.sku,
|
|
542
716
|
F.handle,
|
|
543
|
-
|
|
544
|
-
|
|
717
|
+
"tax_rate_id",
|
|
718
|
+
"tax_rate",
|
|
545
719
|
F.product_type,
|
|
546
720
|
F.status_entry_id,
|
|
547
721
|
F.primary_currency_code,
|
|
548
722
|
F.default_unit,
|
|
723
|
+
"default_sales_unit",
|
|
724
|
+
"default_sales_unit_quantity",
|
|
725
|
+
"uom_rounding_scale",
|
|
726
|
+
"uom_rounding_mode",
|
|
727
|
+
"unit_price_enabled",
|
|
728
|
+
"unit_price_reference_unit",
|
|
729
|
+
"unit_price_base_quantity",
|
|
549
730
|
F.default_media_id,
|
|
550
731
|
F.default_media_url,
|
|
551
732
|
F.weight_value,
|
|
@@ -554,8 +735,8 @@ const crud = makeCrudRoute({
|
|
|
554
735
|
F.is_configurable,
|
|
555
736
|
F.is_active,
|
|
556
737
|
F.metadata,
|
|
557
|
-
|
|
558
|
-
|
|
738
|
+
"custom_fieldset_code",
|
|
739
|
+
"option_schema_id",
|
|
559
740
|
F.created_at,
|
|
560
741
|
F.updated_at,
|
|
561
742
|
],
|
|
@@ -568,15 +749,31 @@ const crud = makeCrudRoute({
|
|
|
568
749
|
},
|
|
569
750
|
buildFilters: buildProductFilters,
|
|
570
751
|
transformItem: (item: ProductListItem | null | undefined) => {
|
|
571
|
-
if (!item) return item
|
|
572
|
-
const normalized = { ...item }
|
|
573
|
-
const cfEntries = extractAllCustomFieldEntries(item)
|
|
752
|
+
if (!item) return item;
|
|
753
|
+
const normalized = { ...item };
|
|
754
|
+
const cfEntries = extractAllCustomFieldEntries(item);
|
|
574
755
|
for (const key of Object.keys(normalized)) {
|
|
575
|
-
if (key.startsWith(
|
|
576
|
-
delete normalized[key]
|
|
756
|
+
if (key.startsWith("cf:")) {
|
|
757
|
+
delete normalized[key];
|
|
577
758
|
}
|
|
578
759
|
}
|
|
579
|
-
|
|
760
|
+
const defaultUnit = canonicalizeUnitCode(normalized.default_unit) ?? null;
|
|
761
|
+
const defaultSalesUnit =
|
|
762
|
+
canonicalizeUnitCode(normalized.default_sales_unit) ?? null;
|
|
763
|
+
const unitPriceReferenceUnit =
|
|
764
|
+
canonicalizeUnitCode(normalized.unit_price_reference_unit) ?? null;
|
|
765
|
+
return {
|
|
766
|
+
...normalized,
|
|
767
|
+
default_unit: defaultUnit,
|
|
768
|
+
default_sales_unit: defaultSalesUnit,
|
|
769
|
+
unit_price_reference_unit: unitPriceReferenceUnit,
|
|
770
|
+
...cfEntries,
|
|
771
|
+
unit_price: {
|
|
772
|
+
enabled: Boolean(normalized.unit_price_enabled),
|
|
773
|
+
reference_unit: unitPriceReferenceUnit,
|
|
774
|
+
base_quantity: normalized.unit_price_base_quantity ?? null,
|
|
775
|
+
},
|
|
776
|
+
};
|
|
580
777
|
},
|
|
581
778
|
},
|
|
582
779
|
hooks: {
|
|
@@ -584,46 +781,68 @@ const crud = makeCrudRoute({
|
|
|
584
781
|
},
|
|
585
782
|
actions: {
|
|
586
783
|
create: {
|
|
587
|
-
commandId:
|
|
784
|
+
commandId: "catalog.products.create",
|
|
588
785
|
schema: rawBodySchema,
|
|
589
786
|
mapInput: async ({ raw, ctx }) => {
|
|
590
|
-
const { translate } = await resolveTranslations()
|
|
591
|
-
const parsed = parseScopedCommandInput(
|
|
592
|
-
|
|
593
|
-
|
|
787
|
+
const { translate } = await resolveTranslations();
|
|
788
|
+
const parsed = parseScopedCommandInput(
|
|
789
|
+
productCreateSchema,
|
|
790
|
+
raw ?? {},
|
|
791
|
+
ctx,
|
|
792
|
+
translate,
|
|
793
|
+
);
|
|
794
|
+
const { base, custom } = splitCustomFieldPayload(parsed);
|
|
795
|
+
return Object.keys(custom).length
|
|
796
|
+
? { ...base, customFields: custom }
|
|
797
|
+
: base;
|
|
594
798
|
},
|
|
595
|
-
response: ({ result }) => ({
|
|
799
|
+
response: ({ result }) => ({
|
|
800
|
+
id: result?.productId ?? result?.id ?? null,
|
|
801
|
+
}),
|
|
596
802
|
status: 201,
|
|
597
803
|
},
|
|
598
804
|
update: {
|
|
599
|
-
commandId:
|
|
805
|
+
commandId: "catalog.products.update",
|
|
600
806
|
schema: rawBodySchema,
|
|
601
807
|
mapInput: async ({ raw, ctx }) => {
|
|
602
|
-
const { translate } = await resolveTranslations()
|
|
603
|
-
const parsed = parseScopedCommandInput(
|
|
604
|
-
|
|
605
|
-
|
|
808
|
+
const { translate } = await resolveTranslations();
|
|
809
|
+
const parsed = parseScopedCommandInput(
|
|
810
|
+
productUpdateSchema,
|
|
811
|
+
raw ?? {},
|
|
812
|
+
ctx,
|
|
813
|
+
translate,
|
|
814
|
+
);
|
|
815
|
+
const { base, custom } = splitCustomFieldPayload(parsed);
|
|
816
|
+
return Object.keys(custom).length
|
|
817
|
+
? { ...base, customFields: custom }
|
|
818
|
+
: base;
|
|
606
819
|
},
|
|
607
820
|
response: () => ({ ok: true }),
|
|
608
821
|
},
|
|
609
822
|
delete: {
|
|
610
|
-
commandId:
|
|
823
|
+
commandId: "catalog.products.delete",
|
|
611
824
|
schema: rawBodySchema,
|
|
612
825
|
mapInput: async ({ parsed, ctx }) => {
|
|
613
|
-
const { translate } = await resolveTranslations()
|
|
614
|
-
const id = resolveCrudRecordId(parsed, ctx, translate)
|
|
615
|
-
if (!id)
|
|
616
|
-
|
|
826
|
+
const { translate } = await resolveTranslations();
|
|
827
|
+
const id = resolveCrudRecordId(parsed, ctx, translate);
|
|
828
|
+
if (!id)
|
|
829
|
+
throw new CrudHttpError(400, {
|
|
830
|
+
error: translate(
|
|
831
|
+
"catalog.errors.id_required",
|
|
832
|
+
"Product id is required.",
|
|
833
|
+
),
|
|
834
|
+
});
|
|
835
|
+
return { id };
|
|
617
836
|
},
|
|
618
837
|
response: () => ({ ok: true }),
|
|
619
838
|
},
|
|
620
839
|
},
|
|
621
|
-
})
|
|
840
|
+
});
|
|
622
841
|
|
|
623
|
-
export const GET = crud.GET
|
|
624
|
-
export const POST = crud.POST
|
|
625
|
-
export const PUT = crud.PUT
|
|
626
|
-
export const DELETE = crud.DELETE
|
|
842
|
+
export const GET = crud.GET;
|
|
843
|
+
export const POST = crud.POST;
|
|
844
|
+
export const PUT = crud.PUT;
|
|
845
|
+
export const DELETE = crud.DELETE;
|
|
627
846
|
|
|
628
847
|
const productListItemSchema = z.object({
|
|
629
848
|
id: z.string().uuid(),
|
|
@@ -636,6 +855,23 @@ const productListItemSchema = z.object({
|
|
|
636
855
|
status_entry_id: z.string().uuid().nullable().optional(),
|
|
637
856
|
primary_currency_code: z.string().nullable().optional(),
|
|
638
857
|
default_unit: z.string().nullable().optional(),
|
|
858
|
+
default_sales_unit: z.string().nullable().optional(),
|
|
859
|
+
default_sales_unit_quantity: z.number().nullable().optional(),
|
|
860
|
+
uom_rounding_scale: z.number().nullable().optional(),
|
|
861
|
+
uom_rounding_mode: z.enum(["half_up", "down", "up"]).nullable().optional(),
|
|
862
|
+
unit_price_enabled: z.boolean().nullable().optional(),
|
|
863
|
+
unit_price_reference_unit: z
|
|
864
|
+
.enum(["kg", "l", "m2", "m3", "pc"])
|
|
865
|
+
.nullable()
|
|
866
|
+
.optional(),
|
|
867
|
+
unit_price_base_quantity: z.number().nullable().optional(),
|
|
868
|
+
unit_price: z
|
|
869
|
+
.object({
|
|
870
|
+
enabled: z.boolean(),
|
|
871
|
+
reference_unit: z.enum(["kg", "l", "m2", "m3", "pc"]).nullable(),
|
|
872
|
+
base_quantity: z.number().nullable(),
|
|
873
|
+
})
|
|
874
|
+
.optional(),
|
|
639
875
|
default_media_id: z.string().uuid().nullable().optional(),
|
|
640
876
|
default_media_url: z.string().nullable().optional(),
|
|
641
877
|
weight_value: z.number().nullable().optional(),
|
|
@@ -654,25 +890,25 @@ const productListItemSchema = z.object({
|
|
|
654
890
|
categoryIds: z.array(z.string()).optional(),
|
|
655
891
|
tags: z.array(z.string()).optional(),
|
|
656
892
|
pricing: z.record(z.string(), z.unknown()).nullable().optional(),
|
|
657
|
-
})
|
|
893
|
+
});
|
|
658
894
|
|
|
659
895
|
export const openApi = createCatalogCrudOpenApi({
|
|
660
|
-
resourceName:
|
|
661
|
-
pluralName:
|
|
896
|
+
resourceName: "Product",
|
|
897
|
+
pluralName: "Products",
|
|
662
898
|
querySchema: listSchema,
|
|
663
899
|
listResponseSchema: createPagedListResponseSchema(productListItemSchema),
|
|
664
900
|
create: {
|
|
665
901
|
schema: productCreateSchema,
|
|
666
|
-
description:
|
|
902
|
+
description: "Creates a new product in the catalog.",
|
|
667
903
|
},
|
|
668
904
|
update: {
|
|
669
905
|
schema: productUpdateSchema,
|
|
670
906
|
responseSchema: defaultOkResponseSchema,
|
|
671
|
-
description:
|
|
907
|
+
description: "Updates an existing product by id.",
|
|
672
908
|
},
|
|
673
909
|
del: {
|
|
674
910
|
schema: z.object({ id: z.string().uuid() }),
|
|
675
911
|
responseSchema: defaultOkResponseSchema,
|
|
676
|
-
description:
|
|
912
|
+
description: "Deletes a product by id.",
|
|
677
913
|
},
|
|
678
|
-
})
|
|
914
|
+
});
|