@open-mercato/core 0.4.5-develop-3ce83a8b24 → 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
|
@@ -0,0 +1,626 @@
|
|
|
1
|
+
import { registerCommand } from "@open-mercato/shared/lib/commands";
|
|
2
|
+
import type { CommandHandler } from "@open-mercato/shared/lib/commands";
|
|
3
|
+
import {
|
|
4
|
+
buildChanges,
|
|
5
|
+
requireId,
|
|
6
|
+
emitCrudSideEffects,
|
|
7
|
+
emitCrudUndoSideEffects,
|
|
8
|
+
} from "@open-mercato/shared/lib/commands/helpers";
|
|
9
|
+
import type {
|
|
10
|
+
CrudEventAction,
|
|
11
|
+
CrudEventsConfig,
|
|
12
|
+
CrudIndexerConfig,
|
|
13
|
+
} from "@open-mercato/shared/lib/crud/types";
|
|
14
|
+
import type { EntityManager } from "@mikro-orm/postgresql";
|
|
15
|
+
import { UniqueConstraintViolationException } from "@mikro-orm/core";
|
|
16
|
+
import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
17
|
+
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
18
|
+
import type { DataEngine } from "@open-mercato/shared/lib/data/engine";
|
|
19
|
+
import { E } from "#generated/entities.ids.generated";
|
|
20
|
+
import {
|
|
21
|
+
findOneWithDecryption,
|
|
22
|
+
} from "@open-mercato/shared/lib/encryption/find";
|
|
23
|
+
import { CatalogProduct, CatalogProductUnitConversion } from "../data/entities";
|
|
24
|
+
import {
|
|
25
|
+
productUnitConversionCreateSchema,
|
|
26
|
+
productUnitConversionUpdateSchema,
|
|
27
|
+
productUnitConversionDeleteSchema,
|
|
28
|
+
type ProductUnitConversionCreateInput,
|
|
29
|
+
type ProductUnitConversionUpdateInput,
|
|
30
|
+
type ProductUnitConversionDeleteInput,
|
|
31
|
+
} from "../data/validators";
|
|
32
|
+
import {
|
|
33
|
+
ensureOrganizationScope,
|
|
34
|
+
ensureSameScope,
|
|
35
|
+
cloneJson,
|
|
36
|
+
ensureTenantScope,
|
|
37
|
+
extractUndoPayload,
|
|
38
|
+
requireProduct,
|
|
39
|
+
toNumericString,
|
|
40
|
+
getErrorConstraint,
|
|
41
|
+
getErrorMessage,
|
|
42
|
+
} from "./shared";
|
|
43
|
+
import { toUnitLookupKey } from "../lib/unitCodes";
|
|
44
|
+
import { resolveCanonicalUnitCode } from "../lib/unitResolution";
|
|
45
|
+
|
|
46
|
+
type ProductUnitConversionSnapshot = {
|
|
47
|
+
id: string;
|
|
48
|
+
productId: string;
|
|
49
|
+
organizationId: string;
|
|
50
|
+
tenantId: string;
|
|
51
|
+
unitCode: string;
|
|
52
|
+
toBaseFactor: string;
|
|
53
|
+
sortOrder: number;
|
|
54
|
+
isActive: boolean;
|
|
55
|
+
metadata: Record<string, unknown> | null;
|
|
56
|
+
createdAt: string;
|
|
57
|
+
updatedAt: string;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
type ProductUnitConversionUndoPayload = {
|
|
61
|
+
before?: ProductUnitConversionSnapshot | null;
|
|
62
|
+
after?: ProductUnitConversionSnapshot | null;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const conversionCrudEvents: CrudEventsConfig<CatalogProductUnitConversion> = {
|
|
66
|
+
module: "catalog",
|
|
67
|
+
entity: "product_unit_conversion",
|
|
68
|
+
persistent: true,
|
|
69
|
+
buildPayload: (ctx) => ({
|
|
70
|
+
id: ctx.identifiers.id,
|
|
71
|
+
productId:
|
|
72
|
+
ctx.entity.product && typeof ctx.entity.product !== "string"
|
|
73
|
+
? ctx.entity.product.id
|
|
74
|
+
: null,
|
|
75
|
+
unitCode: ctx.entity.unitCode,
|
|
76
|
+
tenantId: ctx.identifiers.tenantId,
|
|
77
|
+
organizationId: ctx.identifiers.organizationId,
|
|
78
|
+
}),
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const conversionCrudIndexer: CrudIndexerConfig<CatalogProductUnitConversion> = {
|
|
82
|
+
entityType: E.catalog.catalog_product_unit_conversion,
|
|
83
|
+
buildUpsertPayload: (ctx) => ({
|
|
84
|
+
entityType: E.catalog.catalog_product_unit_conversion,
|
|
85
|
+
recordId: ctx.identifiers.id,
|
|
86
|
+
tenantId: ctx.identifiers.tenantId,
|
|
87
|
+
organizationId: ctx.identifiers.organizationId,
|
|
88
|
+
}),
|
|
89
|
+
buildDeletePayload: (ctx) => ({
|
|
90
|
+
entityType: E.catalog.catalog_product_unit_conversion,
|
|
91
|
+
recordId: ctx.identifiers.id,
|
|
92
|
+
tenantId: ctx.identifiers.tenantId,
|
|
93
|
+
organizationId: ctx.identifiers.organizationId,
|
|
94
|
+
}),
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
function buildIdentifiers(record: CatalogProductUnitConversion) {
|
|
98
|
+
return {
|
|
99
|
+
id: record.id,
|
|
100
|
+
organizationId: record.organizationId,
|
|
101
|
+
tenantId: record.tenantId,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function emitConversionCrudChange(opts: {
|
|
106
|
+
dataEngine: DataEngine;
|
|
107
|
+
action: CrudEventAction;
|
|
108
|
+
conversion: CatalogProductUnitConversion;
|
|
109
|
+
}) {
|
|
110
|
+
const { dataEngine, action, conversion } = opts;
|
|
111
|
+
await emitCrudSideEffects({
|
|
112
|
+
dataEngine,
|
|
113
|
+
action,
|
|
114
|
+
entity: conversion,
|
|
115
|
+
identifiers: buildIdentifiers(conversion),
|
|
116
|
+
events: conversionCrudEvents,
|
|
117
|
+
indexer: conversionCrudIndexer,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function emitConversionCrudUndoChange(opts: {
|
|
122
|
+
dataEngine: DataEngine;
|
|
123
|
+
action: CrudEventAction;
|
|
124
|
+
conversion: CatalogProductUnitConversion;
|
|
125
|
+
}) {
|
|
126
|
+
const { dataEngine, action, conversion } = opts;
|
|
127
|
+
await emitCrudUndoSideEffects({
|
|
128
|
+
dataEngine,
|
|
129
|
+
action,
|
|
130
|
+
entity: conversion,
|
|
131
|
+
identifiers: buildIdentifiers(conversion),
|
|
132
|
+
events: conversionCrudEvents,
|
|
133
|
+
indexer: conversionCrudIndexer,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function loadConversionSnapshot(
|
|
138
|
+
em: EntityManager,
|
|
139
|
+
id: string,
|
|
140
|
+
): Promise<ProductUnitConversionSnapshot | null> {
|
|
141
|
+
const record = await findOneWithDecryption(
|
|
142
|
+
em,
|
|
143
|
+
CatalogProductUnitConversion,
|
|
144
|
+
{ id, deletedAt: null },
|
|
145
|
+
{ populate: ["product"] },
|
|
146
|
+
);
|
|
147
|
+
if (!record) return null;
|
|
148
|
+
const productId =
|
|
149
|
+
typeof record.product === "string"
|
|
150
|
+
? record.product
|
|
151
|
+
: (record.product?.id ?? null);
|
|
152
|
+
if (!productId) return null;
|
|
153
|
+
return {
|
|
154
|
+
id: record.id,
|
|
155
|
+
productId,
|
|
156
|
+
organizationId: record.organizationId,
|
|
157
|
+
tenantId: record.tenantId,
|
|
158
|
+
unitCode: record.unitCode,
|
|
159
|
+
toBaseFactor: record.toBaseFactor,
|
|
160
|
+
sortOrder: record.sortOrder,
|
|
161
|
+
isActive: record.isActive,
|
|
162
|
+
metadata: record.metadata
|
|
163
|
+
? cloneJson(record.metadata)
|
|
164
|
+
: null,
|
|
165
|
+
createdAt: record.createdAt.toISOString(),
|
|
166
|
+
updatedAt: record.updatedAt.toISOString(),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function applyConversionSnapshot(
|
|
171
|
+
em: EntityManager,
|
|
172
|
+
record: CatalogProductUnitConversion,
|
|
173
|
+
snapshot: ProductUnitConversionSnapshot,
|
|
174
|
+
): void {
|
|
175
|
+
record.organizationId = snapshot.organizationId;
|
|
176
|
+
record.tenantId = snapshot.tenantId;
|
|
177
|
+
record.product = em.getReference(CatalogProduct, snapshot.productId);
|
|
178
|
+
record.unitCode = snapshot.unitCode;
|
|
179
|
+
record.toBaseFactor = snapshot.toBaseFactor;
|
|
180
|
+
record.sortOrder = snapshot.sortOrder;
|
|
181
|
+
record.isActive = snapshot.isActive;
|
|
182
|
+
record.metadata = snapshot.metadata
|
|
183
|
+
? cloneJson(snapshot.metadata)
|
|
184
|
+
: null;
|
|
185
|
+
record.createdAt = new Date(snapshot.createdAt);
|
|
186
|
+
record.updatedAt = new Date(snapshot.updatedAt);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function ensureDefaultSalesUnitIsNotRemoved(
|
|
190
|
+
em: EntityManager,
|
|
191
|
+
record: CatalogProductUnitConversion,
|
|
192
|
+
nextIsActive: boolean,
|
|
193
|
+
): Promise<void> {
|
|
194
|
+
if (nextIsActive) return;
|
|
195
|
+
const product =
|
|
196
|
+
typeof record.product === "string"
|
|
197
|
+
? await findOneWithDecryption(em, CatalogProduct, {
|
|
198
|
+
id: record.product,
|
|
199
|
+
deletedAt: null,
|
|
200
|
+
})
|
|
201
|
+
: record.product;
|
|
202
|
+
if (!product) return;
|
|
203
|
+
const defaultSalesUnitKey = toUnitLookupKey(product.defaultSalesUnit);
|
|
204
|
+
const conversionUnitKey = toUnitLookupKey(record.unitCode);
|
|
205
|
+
if (
|
|
206
|
+
defaultSalesUnitKey &&
|
|
207
|
+
conversionUnitKey &&
|
|
208
|
+
defaultSalesUnitKey === conversionUnitKey
|
|
209
|
+
) {
|
|
210
|
+
throw new CrudHttpError(409, {
|
|
211
|
+
error: "uom.default_sales_unit_conversion_required",
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function resolveConversionUniqueConstraint(error: unknown): boolean {
|
|
217
|
+
if (error instanceof UniqueConstraintViolationException) {
|
|
218
|
+
const constraint = getErrorConstraint(error);
|
|
219
|
+
if (constraint === "catalog_product_unit_conversions_unique") return true;
|
|
220
|
+
const message = getErrorMessage(error);
|
|
221
|
+
return message.toLowerCase().includes("catalog_product_unit_conversions_unique");
|
|
222
|
+
}
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function rethrowConversionUniqueConstraint(
|
|
227
|
+
error: unknown,
|
|
228
|
+
): never {
|
|
229
|
+
if (resolveConversionUniqueConstraint(error)) {
|
|
230
|
+
throw new CrudHttpError(409, { error: "uom.duplicate_conversion" });
|
|
231
|
+
}
|
|
232
|
+
throw error;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const createProductUnitConversionCommand: CommandHandler<
|
|
236
|
+
ProductUnitConversionCreateInput,
|
|
237
|
+
{ conversionId: string }
|
|
238
|
+
> = {
|
|
239
|
+
id: "catalog.product-unit-conversions.create",
|
|
240
|
+
async execute(rawInput, ctx) {
|
|
241
|
+
const parsed = productUnitConversionCreateSchema.parse(rawInput);
|
|
242
|
+
ensureTenantScope(ctx, parsed.tenantId);
|
|
243
|
+
ensureOrganizationScope(ctx, parsed.organizationId);
|
|
244
|
+
|
|
245
|
+
const em = (ctx.container.resolve("em") as EntityManager).fork();
|
|
246
|
+
const { translate } = await resolveTranslations();
|
|
247
|
+
const product = await requireProduct(
|
|
248
|
+
em,
|
|
249
|
+
parsed.productId,
|
|
250
|
+
translate("catalog.errors.productNotFound", "Catalog product not found"),
|
|
251
|
+
);
|
|
252
|
+
ensureSameScope(product, parsed.organizationId, parsed.tenantId);
|
|
253
|
+
const canonicalUnitCode = await resolveCanonicalUnitCode(em, {
|
|
254
|
+
organizationId: parsed.organizationId,
|
|
255
|
+
tenantId: parsed.tenantId,
|
|
256
|
+
unitCode: parsed.unitCode,
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
const toBaseFactorStr = toNumericString(parsed.toBaseFactor);
|
|
260
|
+
if (!toBaseFactorStr) {
|
|
261
|
+
throw new CrudHttpError(400, {
|
|
262
|
+
error: "uom.conversion_factor_required",
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const conversion = em.create(CatalogProductUnitConversion, {
|
|
267
|
+
product,
|
|
268
|
+
organizationId: parsed.organizationId,
|
|
269
|
+
tenantId: parsed.tenantId,
|
|
270
|
+
unitCode: canonicalUnitCode,
|
|
271
|
+
toBaseFactor: toBaseFactorStr,
|
|
272
|
+
sortOrder: parsed.sortOrder ?? 0,
|
|
273
|
+
isActive: parsed.isActive !== false,
|
|
274
|
+
metadata: parsed.metadata
|
|
275
|
+
? cloneJson(parsed.metadata)
|
|
276
|
+
: null,
|
|
277
|
+
createdAt: new Date(),
|
|
278
|
+
updatedAt: new Date(),
|
|
279
|
+
});
|
|
280
|
+
em.persist(conversion);
|
|
281
|
+
try {
|
|
282
|
+
await em.flush();
|
|
283
|
+
} catch (error) {
|
|
284
|
+
rethrowConversionUniqueConstraint(error);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const dataEngine = ctx.container.resolve("dataEngine") as DataEngine;
|
|
288
|
+
await emitConversionCrudChange({
|
|
289
|
+
dataEngine,
|
|
290
|
+
action: "created",
|
|
291
|
+
conversion,
|
|
292
|
+
});
|
|
293
|
+
return { conversionId: conversion.id };
|
|
294
|
+
},
|
|
295
|
+
captureAfter: async (_input, result, ctx) => {
|
|
296
|
+
const em = (ctx.container.resolve("em") as EntityManager).fork();
|
|
297
|
+
return loadConversionSnapshot(em, result.conversionId);
|
|
298
|
+
},
|
|
299
|
+
buildLog: async ({ snapshots }) => {
|
|
300
|
+
const after = snapshots.after as ProductUnitConversionSnapshot | undefined;
|
|
301
|
+
if (!after) return null;
|
|
302
|
+
const { translate } = await resolveTranslations();
|
|
303
|
+
return {
|
|
304
|
+
actionLabel: translate(
|
|
305
|
+
"catalog.audit.productUnitConversions.create",
|
|
306
|
+
"Create product unit conversion",
|
|
307
|
+
),
|
|
308
|
+
resourceKind: "catalog.product_unit_conversion",
|
|
309
|
+
resourceId: after.id,
|
|
310
|
+
tenantId: after.tenantId,
|
|
311
|
+
organizationId: after.organizationId,
|
|
312
|
+
snapshotAfter: after,
|
|
313
|
+
payload: {
|
|
314
|
+
undo: {
|
|
315
|
+
after,
|
|
316
|
+
} satisfies ProductUnitConversionUndoPayload,
|
|
317
|
+
},
|
|
318
|
+
};
|
|
319
|
+
},
|
|
320
|
+
undo: async ({ logEntry, ctx }) => {
|
|
321
|
+
const payload =
|
|
322
|
+
extractUndoPayload<ProductUnitConversionUndoPayload>(logEntry);
|
|
323
|
+
const after = payload?.after;
|
|
324
|
+
if (!after) return;
|
|
325
|
+
const em = (ctx.container.resolve("em") as EntityManager).fork();
|
|
326
|
+
const record = await findOneWithDecryption(
|
|
327
|
+
em,
|
|
328
|
+
CatalogProductUnitConversion,
|
|
329
|
+
{ id: after.id, deletedAt: null },
|
|
330
|
+
);
|
|
331
|
+
if (!record) return;
|
|
332
|
+
ensureTenantScope(ctx, record.tenantId);
|
|
333
|
+
ensureOrganizationScope(ctx, record.organizationId);
|
|
334
|
+
em.remove(record);
|
|
335
|
+
await em.flush();
|
|
336
|
+
const dataEngine = ctx.container.resolve("dataEngine") as DataEngine;
|
|
337
|
+
await emitConversionCrudUndoChange({
|
|
338
|
+
dataEngine,
|
|
339
|
+
action: "deleted",
|
|
340
|
+
conversion: record,
|
|
341
|
+
});
|
|
342
|
+
},
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
const updateProductUnitConversionCommand: CommandHandler<
|
|
346
|
+
ProductUnitConversionUpdateInput,
|
|
347
|
+
{ conversionId: string }
|
|
348
|
+
> = {
|
|
349
|
+
id: "catalog.product-unit-conversions.update",
|
|
350
|
+
async prepare(input, ctx) {
|
|
351
|
+
const id = requireId(input, "Product unit conversion id is required");
|
|
352
|
+
const em = ctx.container.resolve("em") as EntityManager;
|
|
353
|
+
const snapshot = await loadConversionSnapshot(em, id);
|
|
354
|
+
if (snapshot) {
|
|
355
|
+
ensureTenantScope(ctx, snapshot.tenantId);
|
|
356
|
+
ensureOrganizationScope(ctx, snapshot.organizationId);
|
|
357
|
+
}
|
|
358
|
+
return snapshot ? { before: snapshot } : {};
|
|
359
|
+
},
|
|
360
|
+
async execute(rawInput, ctx) {
|
|
361
|
+
const parsed = productUnitConversionUpdateSchema.parse(rawInput);
|
|
362
|
+
const em = (ctx.container.resolve("em") as EntityManager).fork();
|
|
363
|
+
const { translate } = await resolveTranslations();
|
|
364
|
+
const record = await findOneWithDecryption(
|
|
365
|
+
em,
|
|
366
|
+
CatalogProductUnitConversion,
|
|
367
|
+
{ id: parsed.id, deletedAt: null },
|
|
368
|
+
{ populate: ["product"] },
|
|
369
|
+
);
|
|
370
|
+
if (!record)
|
|
371
|
+
throw new CrudHttpError(404, {
|
|
372
|
+
error: translate("catalog.errors.conversionNotFound", "Catalog product unit conversion not found"),
|
|
373
|
+
});
|
|
374
|
+
const product =
|
|
375
|
+
typeof record.product === "string"
|
|
376
|
+
? await requireProduct(em, record.product, translate("catalog.errors.productNotFound", "Catalog product not found"))
|
|
377
|
+
: record.product;
|
|
378
|
+
ensureTenantScope(ctx, record.tenantId);
|
|
379
|
+
ensureOrganizationScope(ctx, record.organizationId);
|
|
380
|
+
ensureSameScope(product, record.organizationId, record.tenantId);
|
|
381
|
+
|
|
382
|
+
// Resolve all query-dependent values BEFORE applying scalar mutations
|
|
383
|
+
const resolvedUnitCode =
|
|
384
|
+
parsed.unitCode !== undefined
|
|
385
|
+
? await resolveCanonicalUnitCode(em, {
|
|
386
|
+
organizationId: record.organizationId,
|
|
387
|
+
tenantId: record.tenantId,
|
|
388
|
+
unitCode: parsed.unitCode,
|
|
389
|
+
})
|
|
390
|
+
: undefined;
|
|
391
|
+
if (parsed.isActive !== undefined) {
|
|
392
|
+
await ensureDefaultSalesUnitIsNotRemoved(em, record, parsed.isActive);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Apply all scalar mutations after queries are complete
|
|
396
|
+
if (resolvedUnitCode !== undefined) {
|
|
397
|
+
record.unitCode = resolvedUnitCode;
|
|
398
|
+
}
|
|
399
|
+
if (parsed.toBaseFactor !== undefined) {
|
|
400
|
+
record.toBaseFactor =
|
|
401
|
+
toNumericString(parsed.toBaseFactor) ?? record.toBaseFactor;
|
|
402
|
+
}
|
|
403
|
+
if (parsed.sortOrder !== undefined) {
|
|
404
|
+
record.sortOrder = parsed.sortOrder;
|
|
405
|
+
}
|
|
406
|
+
if (parsed.isActive !== undefined) {
|
|
407
|
+
record.isActive = parsed.isActive;
|
|
408
|
+
}
|
|
409
|
+
if (parsed.metadata !== undefined) {
|
|
410
|
+
record.metadata = parsed.metadata
|
|
411
|
+
? cloneJson(parsed.metadata)
|
|
412
|
+
: null;
|
|
413
|
+
}
|
|
414
|
+
try {
|
|
415
|
+
await em.flush();
|
|
416
|
+
} catch (error) {
|
|
417
|
+
rethrowConversionUniqueConstraint(error);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const dataEngine = ctx.container.resolve("dataEngine") as DataEngine;
|
|
421
|
+
await emitConversionCrudChange({
|
|
422
|
+
dataEngine,
|
|
423
|
+
action: "updated",
|
|
424
|
+
conversion: record,
|
|
425
|
+
});
|
|
426
|
+
return { conversionId: record.id };
|
|
427
|
+
},
|
|
428
|
+
captureAfter: async (_input, result, ctx) => {
|
|
429
|
+
const em = (ctx.container.resolve("em") as EntityManager).fork();
|
|
430
|
+
return loadConversionSnapshot(em, result.conversionId);
|
|
431
|
+
},
|
|
432
|
+
buildLog: async ({ snapshots }) => {
|
|
433
|
+
const before = snapshots.before as
|
|
434
|
+
| ProductUnitConversionSnapshot
|
|
435
|
+
| undefined;
|
|
436
|
+
const after = snapshots.after as ProductUnitConversionSnapshot | undefined;
|
|
437
|
+
if (!before || !after) return null;
|
|
438
|
+
const { translate } = await resolveTranslations();
|
|
439
|
+
return {
|
|
440
|
+
actionLabel: translate(
|
|
441
|
+
"catalog.audit.productUnitConversions.update",
|
|
442
|
+
"Update product unit conversion",
|
|
443
|
+
),
|
|
444
|
+
resourceKind: "catalog.product_unit_conversion",
|
|
445
|
+
resourceId: before.id,
|
|
446
|
+
tenantId: before.tenantId,
|
|
447
|
+
organizationId: before.organizationId,
|
|
448
|
+
changes: buildChanges(before, after, [
|
|
449
|
+
"unitCode",
|
|
450
|
+
"toBaseFactor",
|
|
451
|
+
"sortOrder",
|
|
452
|
+
"isActive",
|
|
453
|
+
"metadata",
|
|
454
|
+
]),
|
|
455
|
+
snapshotBefore: before,
|
|
456
|
+
snapshotAfter: after,
|
|
457
|
+
payload: {
|
|
458
|
+
undo: {
|
|
459
|
+
before,
|
|
460
|
+
after,
|
|
461
|
+
} satisfies ProductUnitConversionUndoPayload,
|
|
462
|
+
},
|
|
463
|
+
};
|
|
464
|
+
},
|
|
465
|
+
undo: async ({ logEntry, ctx }) => {
|
|
466
|
+
const payload =
|
|
467
|
+
extractUndoPayload<ProductUnitConversionUndoPayload>(logEntry);
|
|
468
|
+
const before = payload?.before;
|
|
469
|
+
if (!before) return;
|
|
470
|
+
const em = (ctx.container.resolve("em") as EntityManager).fork();
|
|
471
|
+
let record = await findOneWithDecryption(
|
|
472
|
+
em,
|
|
473
|
+
CatalogProductUnitConversion,
|
|
474
|
+
{ id: before.id, deletedAt: null },
|
|
475
|
+
);
|
|
476
|
+
let undoAction: "updated" | "created" = "updated";
|
|
477
|
+
if (!record) {
|
|
478
|
+
record = em.create(CatalogProductUnitConversion, {
|
|
479
|
+
id: before.id,
|
|
480
|
+
organizationId: before.organizationId,
|
|
481
|
+
tenantId: before.tenantId,
|
|
482
|
+
product: em.getReference(CatalogProduct, before.productId),
|
|
483
|
+
unitCode: before.unitCode,
|
|
484
|
+
toBaseFactor: before.toBaseFactor,
|
|
485
|
+
sortOrder: before.sortOrder,
|
|
486
|
+
isActive: before.isActive,
|
|
487
|
+
metadata: before.metadata
|
|
488
|
+
? cloneJson(before.metadata)
|
|
489
|
+
: null,
|
|
490
|
+
createdAt: new Date(before.createdAt),
|
|
491
|
+
updatedAt: new Date(before.updatedAt),
|
|
492
|
+
});
|
|
493
|
+
em.persist(record);
|
|
494
|
+
undoAction = "created";
|
|
495
|
+
}
|
|
496
|
+
ensureTenantScope(ctx, before.tenantId);
|
|
497
|
+
ensureOrganizationScope(ctx, before.organizationId);
|
|
498
|
+
applyConversionSnapshot(em, record, before);
|
|
499
|
+
await em.transactional(async () => {
|
|
500
|
+
await em.flush();
|
|
501
|
+
});
|
|
502
|
+
const dataEngine = ctx.container.resolve("dataEngine") as DataEngine;
|
|
503
|
+
await emitConversionCrudUndoChange({
|
|
504
|
+
dataEngine,
|
|
505
|
+
action: undoAction,
|
|
506
|
+
conversion: record,
|
|
507
|
+
});
|
|
508
|
+
},
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
const deleteProductUnitConversionCommand: CommandHandler<
|
|
512
|
+
ProductUnitConversionDeleteInput,
|
|
513
|
+
{ conversionId: string }
|
|
514
|
+
> = {
|
|
515
|
+
id: "catalog.product-unit-conversions.delete",
|
|
516
|
+
async prepare(input, ctx) {
|
|
517
|
+
const id = requireId(input, "Product unit conversion id is required");
|
|
518
|
+
const em = ctx.container.resolve("em") as EntityManager;
|
|
519
|
+
const snapshot = await loadConversionSnapshot(em, id);
|
|
520
|
+
if (snapshot) {
|
|
521
|
+
ensureTenantScope(ctx, snapshot.tenantId);
|
|
522
|
+
ensureOrganizationScope(ctx, snapshot.organizationId);
|
|
523
|
+
}
|
|
524
|
+
return snapshot ? { before: snapshot } : {};
|
|
525
|
+
},
|
|
526
|
+
async execute(rawInput, ctx) {
|
|
527
|
+
const parsed = productUnitConversionDeleteSchema.parse(rawInput);
|
|
528
|
+
const em = (ctx.container.resolve("em") as EntityManager).fork();
|
|
529
|
+
const { translate } = await resolveTranslations();
|
|
530
|
+
const record = await findOneWithDecryption(
|
|
531
|
+
em,
|
|
532
|
+
CatalogProductUnitConversion,
|
|
533
|
+
{ id: parsed.id, deletedAt: null },
|
|
534
|
+
{ populate: ["product"] },
|
|
535
|
+
);
|
|
536
|
+
if (!record)
|
|
537
|
+
throw new CrudHttpError(404, {
|
|
538
|
+
error: translate("catalog.errors.conversionNotFound", "Catalog product unit conversion not found"),
|
|
539
|
+
});
|
|
540
|
+
ensureTenantScope(ctx, record.tenantId);
|
|
541
|
+
ensureOrganizationScope(ctx, record.organizationId);
|
|
542
|
+
await ensureDefaultSalesUnitIsNotRemoved(em, record, false);
|
|
543
|
+
|
|
544
|
+
em.remove(record);
|
|
545
|
+
await em.transactional(async () => {
|
|
546
|
+
await em.flush();
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
const dataEngine = ctx.container.resolve("dataEngine") as DataEngine;
|
|
550
|
+
await emitConversionCrudChange({
|
|
551
|
+
dataEngine,
|
|
552
|
+
action: "deleted",
|
|
553
|
+
conversion: record,
|
|
554
|
+
});
|
|
555
|
+
return { conversionId: parsed.id };
|
|
556
|
+
},
|
|
557
|
+
buildLog: async ({ snapshots }) => {
|
|
558
|
+
const before = snapshots.before as
|
|
559
|
+
| ProductUnitConversionSnapshot
|
|
560
|
+
| undefined;
|
|
561
|
+
if (!before) return null;
|
|
562
|
+
const { translate } = await resolveTranslations();
|
|
563
|
+
return {
|
|
564
|
+
actionLabel: translate(
|
|
565
|
+
"catalog.audit.productUnitConversions.delete",
|
|
566
|
+
"Delete product unit conversion",
|
|
567
|
+
),
|
|
568
|
+
resourceKind: "catalog.product_unit_conversion",
|
|
569
|
+
resourceId: before.id,
|
|
570
|
+
tenantId: before.tenantId,
|
|
571
|
+
organizationId: before.organizationId,
|
|
572
|
+
snapshotBefore: before,
|
|
573
|
+
payload: {
|
|
574
|
+
undo: {
|
|
575
|
+
before,
|
|
576
|
+
} satisfies ProductUnitConversionUndoPayload,
|
|
577
|
+
},
|
|
578
|
+
};
|
|
579
|
+
},
|
|
580
|
+
undo: async ({ logEntry, ctx }) => {
|
|
581
|
+
const payload =
|
|
582
|
+
extractUndoPayload<ProductUnitConversionUndoPayload>(logEntry);
|
|
583
|
+
const before = payload?.before;
|
|
584
|
+
if (!before) return;
|
|
585
|
+
const em = (ctx.container.resolve("em") as EntityManager).fork();
|
|
586
|
+
let record = await findOneWithDecryption(
|
|
587
|
+
em,
|
|
588
|
+
CatalogProductUnitConversion,
|
|
589
|
+
{ id: before.id, deletedAt: null },
|
|
590
|
+
);
|
|
591
|
+
if (!record) {
|
|
592
|
+
record = em.create(CatalogProductUnitConversion, {
|
|
593
|
+
id: before.id,
|
|
594
|
+
organizationId: before.organizationId,
|
|
595
|
+
tenantId: before.tenantId,
|
|
596
|
+
product: em.getReference(CatalogProduct, before.productId),
|
|
597
|
+
unitCode: before.unitCode,
|
|
598
|
+
toBaseFactor: before.toBaseFactor,
|
|
599
|
+
sortOrder: before.sortOrder,
|
|
600
|
+
isActive: before.isActive,
|
|
601
|
+
metadata: before.metadata
|
|
602
|
+
? cloneJson(before.metadata)
|
|
603
|
+
: null,
|
|
604
|
+
createdAt: new Date(before.createdAt),
|
|
605
|
+
updatedAt: new Date(before.updatedAt),
|
|
606
|
+
});
|
|
607
|
+
em.persist(record);
|
|
608
|
+
}
|
|
609
|
+
ensureTenantScope(ctx, before.tenantId);
|
|
610
|
+
ensureOrganizationScope(ctx, before.organizationId);
|
|
611
|
+
applyConversionSnapshot(em, record, before);
|
|
612
|
+
await em.transactional(async () => {
|
|
613
|
+
await em.flush();
|
|
614
|
+
});
|
|
615
|
+
const dataEngine = ctx.container.resolve("dataEngine") as DataEngine;
|
|
616
|
+
await emitConversionCrudUndoChange({
|
|
617
|
+
dataEngine,
|
|
618
|
+
action: "created",
|
|
619
|
+
conversion: record,
|
|
620
|
+
});
|
|
621
|
+
},
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
registerCommand(createProductUnitConversionCommand);
|
|
625
|
+
registerCommand(updateProductUnitConversionCommand);
|
|
626
|
+
registerCommand(deleteProductUnitConversionCommand);
|