@open-mercato/core 0.4.5-develop-f4858e0ef3 → 0.4.5-develop-4849712ccb
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/generated/entities/catalog_product/index.js +16 -0
- package/dist/generated/entities/catalog_product/index.js.map +2 -2
- package/dist/generated/entities/catalog_product_unit_conversion/index.js +27 -0
- package/dist/generated/entities/catalog_product_unit_conversion/index.js.map +7 -0
- package/dist/generated/entities/sales_credit_memo_line/index.js +7 -1
- package/dist/generated/entities/sales_credit_memo_line/index.js.map +2 -2
- package/dist/generated/entities/sales_invoice_line/index.js +7 -1
- package/dist/generated/entities/sales_invoice_line/index.js.map +2 -2
- package/dist/generated/entities/sales_order_line/index.js +6 -0
- package/dist/generated/entities/sales_order_line/index.js.map +2 -2
- package/dist/generated/entities/sales_quote_line/index.js +6 -0
- package/dist/generated/entities/sales_quote_line/index.js.map +2 -2
- package/dist/generated/entities.ids.generated.js +1 -0
- package/dist/generated/entities.ids.generated.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +2 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/catalog/api/prices/route.js +123 -8
- package/dist/modules/catalog/api/prices/route.js.map +2 -2
- package/dist/modules/catalog/api/product-unit-conversions/route.js +194 -0
- package/dist/modules/catalog/api/product-unit-conversions/route.js.map +7 -0
- package/dist/modules/catalog/api/products/route.js +351 -201
- package/dist/modules/catalog/api/products/route.js.map +2 -2
- package/dist/modules/catalog/backend/catalog/products/[id]/page.js +1267 -497
- package/dist/modules/catalog/backend/catalog/products/[id]/page.js.map +2 -2
- package/dist/modules/catalog/backend/catalog/products/create/page.js +733 -210
- package/dist/modules/catalog/backend/catalog/products/create/page.js.map +2 -2
- package/dist/modules/catalog/commands/index.js +1 -0
- package/dist/modules/catalog/commands/index.js.map +2 -2
- package/dist/modules/catalog/commands/productUnitConversions.js +503 -0
- package/dist/modules/catalog/commands/productUnitConversions.js.map +7 -0
- package/dist/modules/catalog/commands/products.js +355 -73
- package/dist/modules/catalog/commands/products.js.map +2 -2
- package/dist/modules/catalog/commands/shared.js +18 -4
- package/dist/modules/catalog/commands/shared.js.map +2 -2
- package/dist/modules/catalog/components/products/ProductUomSection.js +591 -0
- package/dist/modules/catalog/components/products/ProductUomSection.js.map +7 -0
- package/dist/modules/catalog/components/products/productForm.js +66 -5
- package/dist/modules/catalog/components/products/productForm.js.map +2 -2
- package/dist/modules/catalog/components/products/productFormUtils.js +68 -0
- package/dist/modules/catalog/components/products/productFormUtils.js.map +7 -0
- package/dist/modules/catalog/data/entities.js +86 -0
- package/dist/modules/catalog/data/entities.js.map +2 -2
- package/dist/modules/catalog/data/validators.js +65 -3
- package/dist/modules/catalog/data/validators.js.map +2 -2
- package/dist/modules/catalog/events.js +3 -0
- package/dist/modules/catalog/events.js.map +2 -2
- package/dist/modules/catalog/lib/unitCodes.js +7 -0
- package/dist/modules/catalog/lib/unitCodes.js.map +7 -0
- package/dist/modules/catalog/lib/unitResolution.js +53 -0
- package/dist/modules/catalog/lib/unitResolution.js.map +7 -0
- package/dist/modules/catalog/migrations/Migration20260218225422.js +19 -0
- package/dist/modules/catalog/migrations/Migration20260218225422.js.map +7 -0
- package/dist/modules/catalog/migrations/Migration20260219084500.js +27 -0
- package/dist/modules/catalog/migrations/Migration20260219084500.js.map +7 -0
- package/dist/modules/catalog/search.js +69 -1
- package/dist/modules/catalog/search.js.map +2 -2
- package/dist/modules/catalog/seed/examples.js +91 -42
- package/dist/modules/catalog/seed/examples.js.map +2 -2
- package/dist/modules/dashboards/seed/analytics.js +3 -0
- package/dist/modules/dashboards/seed/analytics.js.map +2 -2
- package/dist/modules/sales/api/order-lines/route.js +98 -15
- package/dist/modules/sales/api/order-lines/route.js.map +2 -2
- package/dist/modules/sales/api/quote-lines/route.js +101 -14
- package/dist/modules/sales/api/quote-lines/route.js.map +2 -2
- package/dist/modules/sales/api/quotes/public/[token]/route.js +87 -12
- package/dist/modules/sales/api/quotes/public/[token]/route.js.map +2 -2
- package/dist/modules/sales/commands/documents.js +1424 -260
- package/dist/modules/sales/commands/documents.js.map +3 -3
- package/dist/modules/sales/commands/shared.js +6 -2
- package/dist/modules/sales/commands/shared.js.map +2 -2
- package/dist/modules/sales/components/documents/ItemsSection.js +216 -86
- package/dist/modules/sales/components/documents/ItemsSection.js.map +2 -2
- package/dist/modules/sales/components/documents/LineItemDialog.js +913 -241
- package/dist/modules/sales/components/documents/LineItemDialog.js.map +3 -3
- package/dist/modules/sales/components/documents/ShipmentsSection.js +15 -3
- package/dist/modules/sales/components/documents/ShipmentsSection.js.map +2 -2
- package/dist/modules/sales/data/entities.js +59 -3
- package/dist/modules/sales/data/entities.js.map +2 -2
- package/dist/modules/sales/data/validators.js +35 -0
- package/dist/modules/sales/data/validators.js.map +2 -2
- package/dist/modules/sales/frontend/quote/[token]/page.js +15 -1
- package/dist/modules/sales/frontend/quote/[token]/page.js.map +2 -2
- package/dist/modules/sales/migrations/Migration20260218225423.js +31 -0
- package/dist/modules/sales/migrations/Migration20260218225423.js.map +7 -0
- package/dist/modules/sales/migrations/Migration20260219084501.js +71 -0
- package/dist/modules/sales/migrations/Migration20260219084501.js.map +7 -0
- package/dist/modules/sales/search.js +28 -0
- package/dist/modules/sales/search.js.map +2 -2
- package/dist/modules/sales/seed/examples.js +14 -1
- package/dist/modules/sales/seed/examples.js.map +2 -2
- package/dist/modules/sales/widgets/injection/document-history/widget.client.js +1 -1
- package/dist/modules/sales/widgets/injection/document-history/widget.client.js.map +2 -2
- package/generated/entities/catalog_product/index.ts +8 -0
- package/generated/entities/catalog_product_unit_conversion/index.ts +12 -0
- package/generated/entities/sales_credit_memo_line/index.ts +3 -0
- package/generated/entities/sales_invoice_line/index.ts +3 -0
- package/generated/entities/sales_order_line/index.ts +3 -0
- package/generated/entities/sales_quote_line/index.ts +3 -0
- package/generated/entities.ids.generated.ts +1 -0
- package/generated/entity-fields-registry.ts +2 -0
- package/package.json +2 -2
- package/src/modules/auth/i18n/de.json +1 -1
- package/src/modules/auth/i18n/en.json +1 -1
- package/src/modules/auth/i18n/es.json +1 -1
- package/src/modules/auth/i18n/pl.json +1 -1
- package/src/modules/catalog/api/prices/route.ts +213 -81
- package/src/modules/catalog/api/product-unit-conversions/route.ts +195 -0
- package/src/modules/catalog/api/products/route.ts +638 -402
- package/src/modules/catalog/backend/catalog/products/[id]/page.tsx +2085 -1072
- package/src/modules/catalog/backend/catalog/products/create/page.tsx +1288 -593
- package/src/modules/catalog/commands/index.ts +1 -0
- package/src/modules/catalog/commands/productUnitConversions.ts +626 -0
- package/src/modules/catalog/commands/products.ts +1151 -693
- package/src/modules/catalog/commands/shared.ts +19 -5
- package/src/modules/catalog/components/products/ProductUomSection.tsx +745 -0
- package/src/modules/catalog/components/products/productForm.ts +369 -256
- package/src/modules/catalog/components/products/productFormUtils.ts +82 -0
- package/src/modules/catalog/data/entities.ts +82 -1
- package/src/modules/catalog/data/validators.ts +118 -34
- package/src/modules/catalog/events.ts +3 -0
- package/src/modules/catalog/i18n/de.json +56 -0
- package/src/modules/catalog/i18n/en.json +56 -0
- package/src/modules/catalog/i18n/es.json +56 -0
- package/src/modules/catalog/i18n/pl.json +56 -0
- package/src/modules/catalog/lib/unitCodes.ts +1 -0
- package/src/modules/catalog/lib/unitResolution.ts +62 -0
- package/src/modules/catalog/migrations/.snapshot-open-mercato.json +245 -0
- package/src/modules/catalog/migrations/Migration20260218225422.ts +21 -0
- package/src/modules/catalog/migrations/Migration20260219084500.ts +26 -0
- package/src/modules/catalog/search.ts +73 -1
- package/src/modules/catalog/seed/examples.ts +552 -479
- package/src/modules/dashboards/i18n/de.json +1 -1
- package/src/modules/dashboards/i18n/en.json +1 -1
- package/src/modules/dashboards/i18n/es.json +1 -1
- package/src/modules/dashboards/i18n/pl.json +1 -1
- package/src/modules/dashboards/seed/analytics.ts +3 -0
- package/src/modules/sales/api/order-lines/route.ts +158 -68
- package/src/modules/sales/api/quote-lines/route.ts +161 -67
- package/src/modules/sales/api/quotes/public/[token]/route.ts +122 -36
- package/src/modules/sales/commands/documents.ts +4250 -2424
- package/src/modules/sales/commands/shared.ts +7 -2
- package/src/modules/sales/components/documents/ItemsSection.tsx +580 -310
- package/src/modules/sales/components/documents/LineItemDialog.tsx +1988 -833
- package/src/modules/sales/components/documents/ShipmentsSection.tsx +17 -3
- package/src/modules/sales/components/documents/lineItemTypes.ts +6 -0
- package/src/modules/sales/data/entities.ts +53 -0
- package/src/modules/sales/data/validators.ts +36 -0
- package/src/modules/sales/frontend/quote/[token]/page.tsx +25 -1
- package/src/modules/sales/i18n/de.json +23 -3
- package/src/modules/sales/i18n/en.json +23 -3
- package/src/modules/sales/i18n/es.json +23 -3
- package/src/modules/sales/i18n/pl.json +23 -3
- package/src/modules/sales/lib/types.ts +30 -0
- package/src/modules/sales/migrations/.snapshot-open-mercato.json +172 -0
- package/src/modules/sales/migrations/Migration20260218225423.ts +37 -0
- package/src/modules/sales/migrations/Migration20260219084501.ts +73 -0
- package/src/modules/sales/search.ts +28 -0
- package/src/modules/sales/seed/examples.ts +20 -1
- package/src/modules/sales/widgets/injection/document-history/widget.client.tsx +1 -1
- package/src/modules/workflows/i18n/de.json +4 -4
- package/src/modules/workflows/i18n/en.json +4 -4
- package/src/modules/workflows/i18n/es.json +4 -4
- package/src/modules/workflows/i18n/pl.json +4 -4
|
@@ -1,57 +1,79 @@
|
|
|
1
|
-
import { z } from
|
|
2
|
-
import { makeCrudRoute } from
|
|
3
|
-
import { resolveTranslations } from
|
|
4
|
-
import { CrudHttpError } from
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { makeCrudRoute } from "@open-mercato/shared/lib/crud/factory";
|
|
3
|
+
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
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 { SalesQuoteLine } from "../../data/entities";
|
|
10
|
+
import { quoteLineCreateSchema } from "../../data/validators";
|
|
11
|
+
import {
|
|
12
|
+
createPagedListResponseSchema,
|
|
13
|
+
createSalesCrudOpenApi,
|
|
14
|
+
defaultOkResponseSchema,
|
|
15
|
+
} from "../openapi";
|
|
16
|
+
import { withScopedPayload } from "../utils";
|
|
17
|
+
import { E } from "#generated/entities.ids.generated";
|
|
18
|
+
import * as F from "#generated/entities/sales_quote_line";
|
|
19
|
+
import { canonicalizeUnitCode, REFERENCE_UNIT_CODES } from "@open-mercato/shared/lib/units/unitCodes";
|
|
12
20
|
|
|
13
|
-
const rawBodySchema = z.object({}).passthrough()
|
|
14
|
-
const resolveRawBody = (raw: unknown)
|
|
21
|
+
const rawBodySchema = z.object({}).passthrough();
|
|
22
|
+
const resolveRawBody = (raw: unknown): Record<string, unknown> => {
|
|
23
|
+
if (!raw || typeof raw !== "object") return {};
|
|
24
|
+
if ("body" in raw) {
|
|
25
|
+
const payload = raw as { body?: unknown };
|
|
26
|
+
if (payload.body && typeof payload.body === "object") {
|
|
27
|
+
return payload.body as Record<string, unknown>;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return raw as Record<string, unknown>;
|
|
31
|
+
};
|
|
15
32
|
|
|
16
33
|
const listSchema = z
|
|
17
34
|
.object({
|
|
18
35
|
page: z.coerce.number().min(1).default(1),
|
|
19
|
-
pageSize: z.coerce.number().min(1).max(
|
|
36
|
+
pageSize: z.coerce.number().min(1).max(100).default(50),
|
|
20
37
|
id: z.string().uuid().optional(),
|
|
21
38
|
quoteId: z.string().uuid().optional(),
|
|
22
39
|
sortField: z.string().optional(),
|
|
23
|
-
sortDir: z.enum([
|
|
40
|
+
sortDir: z.enum(["asc", "desc"]).optional(),
|
|
24
41
|
})
|
|
25
|
-
.passthrough()
|
|
42
|
+
.passthrough();
|
|
26
43
|
|
|
27
44
|
const routeMetadata = {
|
|
28
|
-
GET: { requireAuth: true, requireFeatures: [
|
|
29
|
-
POST: { requireAuth: true, requireFeatures: [
|
|
30
|
-
PUT: { requireAuth: true, requireFeatures: [
|
|
31
|
-
DELETE: { requireAuth: true, requireFeatures: [
|
|
32
|
-
}
|
|
45
|
+
GET: { requireAuth: true, requireFeatures: ["sales.quotes.view"] },
|
|
46
|
+
POST: { requireAuth: true, requireFeatures: ["sales.quotes.manage"] },
|
|
47
|
+
PUT: { requireAuth: true, requireFeatures: ["sales.quotes.manage"] },
|
|
48
|
+
DELETE: { requireAuth: true, requireFeatures: ["sales.quotes.manage"] },
|
|
49
|
+
};
|
|
33
50
|
|
|
34
|
-
const upsertSchema = quoteLineCreateSchema.extend({
|
|
51
|
+
const upsertSchema = quoteLineCreateSchema.extend({
|
|
52
|
+
id: z.string().uuid().optional(),
|
|
53
|
+
});
|
|
35
54
|
const deleteSchema = z.object({
|
|
36
55
|
id: z.string().uuid(),
|
|
37
56
|
quoteId: z.string().uuid(),
|
|
38
|
-
})
|
|
57
|
+
});
|
|
39
58
|
|
|
40
59
|
const crud = makeCrudRoute({
|
|
41
60
|
metadata: routeMetadata,
|
|
42
61
|
orm: {
|
|
43
62
|
entity: SalesQuoteLine,
|
|
44
|
-
idField:
|
|
45
|
-
orgField:
|
|
46
|
-
tenantField:
|
|
47
|
-
softDeleteField:
|
|
63
|
+
idField: "id",
|
|
64
|
+
orgField: "organizationId",
|
|
65
|
+
tenantField: "tenantId",
|
|
66
|
+
softDeleteField: "deletedAt",
|
|
67
|
+
},
|
|
68
|
+
indexer: {
|
|
69
|
+
entityType: E.sales.sales_quote_line,
|
|
48
70
|
},
|
|
49
71
|
list: {
|
|
50
72
|
schema: listSchema,
|
|
51
73
|
entityId: E.sales.sales_quote_line,
|
|
52
74
|
fields: [
|
|
53
75
|
F.id,
|
|
54
|
-
|
|
76
|
+
"quote_id",
|
|
55
77
|
F.line_number,
|
|
56
78
|
F.kind,
|
|
57
79
|
F.status_entry_id,
|
|
@@ -64,6 +86,9 @@ const crud = makeCrudRoute({
|
|
|
64
86
|
F.comment,
|
|
65
87
|
F.quantity,
|
|
66
88
|
F.quantity_unit,
|
|
89
|
+
F.normalized_quantity,
|
|
90
|
+
F.normalized_unit,
|
|
91
|
+
F.uom_snapshot,
|
|
67
92
|
F.currency_code,
|
|
68
93
|
F.unit_price_net,
|
|
69
94
|
F.unit_price_gross,
|
|
@@ -87,74 +112,134 @@ const crud = makeCrudRoute({
|
|
|
87
112
|
lineNumber: F.line_number,
|
|
88
113
|
},
|
|
89
114
|
buildFilters: async (query, ctx) => {
|
|
90
|
-
const filters: Record<string, unknown> = {}
|
|
91
|
-
if (query.id) filters.id = { $eq: query.id }
|
|
92
|
-
if (query.quoteId) filters.quote_id = { $eq: query.quoteId }
|
|
115
|
+
const filters: Record<string, unknown> = {};
|
|
116
|
+
if (query.id) filters.id = { $eq: query.id };
|
|
117
|
+
if (query.quoteId) filters.quote_id = { $eq: query.quoteId };
|
|
93
118
|
try {
|
|
94
|
-
const em = ctx.container.resolve(
|
|
119
|
+
const em = ctx.container.resolve("em");
|
|
95
120
|
const cfFilters = await buildCustomFieldFiltersFromQuery({
|
|
96
121
|
entityId: E.sales.sales_quote_line,
|
|
97
122
|
query,
|
|
98
123
|
em,
|
|
99
124
|
tenantId: ctx.auth?.tenantId ?? null,
|
|
100
|
-
})
|
|
101
|
-
Object.assign(filters, cfFilters)
|
|
125
|
+
});
|
|
126
|
+
Object.assign(filters, cfFilters);
|
|
102
127
|
} catch {
|
|
103
128
|
// ignore
|
|
104
129
|
}
|
|
105
|
-
return filters
|
|
130
|
+
return filters;
|
|
106
131
|
},
|
|
107
|
-
transformItem: (item:
|
|
108
|
-
if (!item) return item
|
|
109
|
-
const normalized = { ...item }
|
|
110
|
-
const cfEntries = extractAllCustomFieldEntries(item)
|
|
132
|
+
transformItem: (item: Record<string, unknown> | null | undefined) => {
|
|
133
|
+
if (!item) return item;
|
|
134
|
+
const normalized = { ...item };
|
|
135
|
+
const cfEntries = extractAllCustomFieldEntries(item);
|
|
111
136
|
for (const key of Object.keys(normalized)) {
|
|
112
|
-
if (key.startsWith(
|
|
137
|
+
if (key.startsWith("cf:")) delete normalized[key];
|
|
113
138
|
}
|
|
114
|
-
|
|
139
|
+
const quantityUnit = canonicalizeUnitCode(
|
|
140
|
+
normalized["quantity_unit"] ?? normalized["quantityUnit"],
|
|
141
|
+
);
|
|
142
|
+
const normalizedUnit =
|
|
143
|
+
canonicalizeUnitCode(
|
|
144
|
+
normalized["normalized_unit"] ?? normalized["normalizedUnit"],
|
|
145
|
+
) ?? quantityUnit;
|
|
146
|
+
return {
|
|
147
|
+
...normalized,
|
|
148
|
+
quantity_unit: quantityUnit,
|
|
149
|
+
normalized_unit: normalizedUnit,
|
|
150
|
+
...cfEntries,
|
|
151
|
+
};
|
|
115
152
|
},
|
|
116
153
|
},
|
|
117
154
|
actions: {
|
|
118
155
|
create: {
|
|
119
|
-
commandId:
|
|
156
|
+
commandId: "sales.quotes.lines.upsert",
|
|
120
157
|
schema: rawBodySchema,
|
|
121
158
|
mapInput: async ({ raw, ctx }) => {
|
|
122
|
-
const { translate } = await resolveTranslations()
|
|
123
|
-
const payload = upsertSchema.parse(
|
|
124
|
-
|
|
159
|
+
const { translate } = await resolveTranslations();
|
|
160
|
+
const payload = upsertSchema.parse(
|
|
161
|
+
withScopedPayload(resolveRawBody(raw) ?? {}, ctx, translate),
|
|
162
|
+
);
|
|
163
|
+
return { body: payload };
|
|
125
164
|
},
|
|
126
|
-
response: ({ result }) => ({
|
|
165
|
+
response: ({ result }) => ({
|
|
166
|
+
id: result?.lineId ?? null,
|
|
167
|
+
quoteId: result?.quoteId ?? null,
|
|
168
|
+
}),
|
|
127
169
|
status: 201,
|
|
128
170
|
},
|
|
129
171
|
update: {
|
|
130
|
-
commandId:
|
|
172
|
+
commandId: "sales.quotes.lines.upsert",
|
|
131
173
|
schema: rawBodySchema,
|
|
132
174
|
mapInput: async ({ raw, ctx }) => {
|
|
133
|
-
const { translate } = await resolveTranslations()
|
|
134
|
-
const payload = upsertSchema.parse(
|
|
135
|
-
|
|
175
|
+
const { translate } = await resolveTranslations();
|
|
176
|
+
const payload = upsertSchema.parse(
|
|
177
|
+
withScopedPayload(resolveRawBody(raw) ?? {}, ctx, translate),
|
|
178
|
+
);
|
|
179
|
+
return { body: payload };
|
|
136
180
|
},
|
|
137
|
-
response: ({ result }) => ({
|
|
181
|
+
response: ({ result }) => ({
|
|
182
|
+
id: result?.lineId ?? null,
|
|
183
|
+
quoteId: result?.quoteId ?? null,
|
|
184
|
+
}),
|
|
138
185
|
},
|
|
139
186
|
delete: {
|
|
140
|
-
commandId:
|
|
187
|
+
commandId: "sales.quotes.lines.delete",
|
|
141
188
|
schema: rawBodySchema,
|
|
142
189
|
mapInput: async ({ raw, ctx }) => {
|
|
143
|
-
const { translate } = await resolveTranslations()
|
|
144
|
-
const payload = deleteSchema.parse(
|
|
190
|
+
const { translate } = await resolveTranslations();
|
|
191
|
+
const payload = deleteSchema.parse(
|
|
192
|
+
withScopedPayload(resolveRawBody(raw) ?? {}, ctx, translate),
|
|
193
|
+
);
|
|
145
194
|
if (!payload.id || !payload.quoteId) {
|
|
146
|
-
throw new CrudHttpError(400, {
|
|
195
|
+
throw new CrudHttpError(400, {
|
|
196
|
+
error: translate(
|
|
197
|
+
"sales.documents.detail.error",
|
|
198
|
+
"Document not found or inaccessible.",
|
|
199
|
+
),
|
|
200
|
+
});
|
|
147
201
|
}
|
|
148
|
-
return { body: payload }
|
|
202
|
+
return { body: payload };
|
|
149
203
|
},
|
|
150
204
|
response: () => ({ ok: true }),
|
|
151
205
|
},
|
|
152
206
|
},
|
|
153
|
-
})
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const { GET, POST, PUT, DELETE } = crud;
|
|
154
210
|
|
|
155
|
-
|
|
211
|
+
export { GET, POST, PUT, DELETE };
|
|
156
212
|
|
|
157
|
-
|
|
213
|
+
const uomSnapshotOpenApiSchema = z
|
|
214
|
+
.object({
|
|
215
|
+
version: z.literal(1),
|
|
216
|
+
productId: z.string().nullable(),
|
|
217
|
+
productVariantId: z.string().nullable(),
|
|
218
|
+
baseUnitCode: z.string().nullable(),
|
|
219
|
+
enteredUnitCode: z.string().nullable(),
|
|
220
|
+
enteredQuantity: z.string(),
|
|
221
|
+
toBaseFactor: z.string(),
|
|
222
|
+
normalizedQuantity: z.string(),
|
|
223
|
+
rounding: z.object({
|
|
224
|
+
mode: z.enum(["half_up", "down", "up"]),
|
|
225
|
+
scale: z.number().int(),
|
|
226
|
+
}),
|
|
227
|
+
source: z.object({
|
|
228
|
+
conversionId: z.string().nullable(),
|
|
229
|
+
resolvedAt: z.string(),
|
|
230
|
+
}),
|
|
231
|
+
unitPriceReference: z
|
|
232
|
+
.object({
|
|
233
|
+
enabled: z.boolean(),
|
|
234
|
+
referenceUnitCode: z.enum(REFERENCE_UNIT_CODES).nullable(),
|
|
235
|
+
baseQuantity: z.string().nullable(),
|
|
236
|
+
grossPerReference: z.string().nullable().optional(),
|
|
237
|
+
netPerReference: z.string().nullable().optional(),
|
|
238
|
+
})
|
|
239
|
+
.optional(),
|
|
240
|
+
})
|
|
241
|
+
.nullable()
|
|
242
|
+
.optional();
|
|
158
243
|
|
|
159
244
|
const quoteLineSchema = z.object({
|
|
160
245
|
id: z.string().uuid(),
|
|
@@ -171,6 +256,9 @@ const quoteLineSchema = z.object({
|
|
|
171
256
|
comment: z.string().nullable().optional(),
|
|
172
257
|
quantity: z.number(),
|
|
173
258
|
quantity_unit: z.string().nullable().optional(),
|
|
259
|
+
normalized_quantity: z.number(),
|
|
260
|
+
normalized_unit: z.string().nullable().optional(),
|
|
261
|
+
uom_snapshot: uomSnapshotOpenApiSchema,
|
|
174
262
|
currency_code: z.string(),
|
|
175
263
|
unit_price_net: z.number(),
|
|
176
264
|
unit_price_gross: z.number(),
|
|
@@ -187,25 +275,31 @@ const quoteLineSchema = z.object({
|
|
|
187
275
|
custom_field_set_id: z.string().uuid().nullable().optional(),
|
|
188
276
|
created_at: z.string(),
|
|
189
277
|
updated_at: z.string(),
|
|
190
|
-
})
|
|
278
|
+
});
|
|
191
279
|
|
|
192
280
|
export const openApi = createSalesCrudOpenApi({
|
|
193
|
-
resourceName:
|
|
281
|
+
resourceName: "Quote line",
|
|
194
282
|
querySchema: listSchema,
|
|
195
283
|
listResponseSchema: createPagedListResponseSchema(quoteLineSchema),
|
|
196
284
|
create: {
|
|
197
285
|
schema: upsertSchema,
|
|
198
|
-
responseSchema: z.object({
|
|
199
|
-
|
|
286
|
+
responseSchema: z.object({
|
|
287
|
+
id: z.string().uuid().nullable(),
|
|
288
|
+
quoteId: z.string().uuid().nullable(),
|
|
289
|
+
}),
|
|
290
|
+
description: "Creates a quote line and recalculates totals.",
|
|
200
291
|
},
|
|
201
292
|
update: {
|
|
202
293
|
schema: upsertSchema,
|
|
203
|
-
responseSchema: z.object({
|
|
204
|
-
|
|
294
|
+
responseSchema: z.object({
|
|
295
|
+
id: z.string().uuid().nullable(),
|
|
296
|
+
quoteId: z.string().uuid().nullable(),
|
|
297
|
+
}),
|
|
298
|
+
description: "Updates a quote line and recalculates totals.",
|
|
205
299
|
},
|
|
206
300
|
del: {
|
|
207
301
|
schema: deleteSchema,
|
|
208
302
|
responseSchema: defaultOkResponseSchema,
|
|
209
|
-
description:
|
|
303
|
+
description: "Deletes a quote line and recalculates totals.",
|
|
210
304
|
},
|
|
211
|
-
})
|
|
305
|
+
});
|
|
@@ -1,38 +1,60 @@
|
|
|
1
|
-
import { NextResponse } from
|
|
2
|
-
import { z } from
|
|
3
|
-
import { createRequestContainer } from
|
|
4
|
-
import { resolveTranslations } from
|
|
5
|
-
import { CrudHttpError } from
|
|
6
|
-
import type { OpenApiRouteDoc } from
|
|
7
|
-
import type { EntityManager } from
|
|
8
|
-
import {
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
4
|
+
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
5
|
+
import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
6
|
+
import type { OpenApiRouteDoc } from "@open-mercato/shared/lib/openapi";
|
|
7
|
+
import type { EntityManager } from "@mikro-orm/postgresql";
|
|
8
|
+
import { findOneWithDecryption, findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
9
|
+
import {
|
|
10
|
+
SalesQuote,
|
|
11
|
+
SalesQuoteLine,
|
|
12
|
+
SalesQuoteAdjustment,
|
|
13
|
+
} from "../../../../data/entities";
|
|
14
|
+
import { canonicalizeUnitCode } from "@open-mercato/shared/lib/units/unitCodes";
|
|
9
15
|
|
|
10
16
|
const paramsSchema = z.object({
|
|
11
17
|
token: z.string().uuid(),
|
|
12
|
-
})
|
|
18
|
+
});
|
|
13
19
|
|
|
14
20
|
export const metadata = {
|
|
15
21
|
GET: { requireAuth: false },
|
|
16
|
-
}
|
|
22
|
+
};
|
|
17
23
|
|
|
18
24
|
export async function GET(_req: Request, ctx: { params: { token: string } }) {
|
|
19
25
|
try {
|
|
20
|
-
const { token } = paramsSchema.parse(ctx.params ?? {})
|
|
21
|
-
const container = await createRequestContainer()
|
|
22
|
-
const em = container.resolve(
|
|
23
|
-
const quote = await em
|
|
24
|
-
|
|
26
|
+
const { token } = paramsSchema.parse(ctx.params ?? {});
|
|
27
|
+
const container = await createRequestContainer();
|
|
28
|
+
const em = container.resolve("em") as EntityManager;
|
|
29
|
+
const quote = await findOneWithDecryption(em, SalesQuote, {
|
|
30
|
+
acceptanceToken: token,
|
|
31
|
+
deletedAt: null,
|
|
32
|
+
});
|
|
33
|
+
const { translate } = await resolveTranslations();
|
|
25
34
|
if (!quote) {
|
|
26
|
-
throw new CrudHttpError(404, {
|
|
35
|
+
throw new CrudHttpError(404, {
|
|
36
|
+
error: translate("sales.quotes.public.notFound", "Quote not found."),
|
|
37
|
+
});
|
|
27
38
|
}
|
|
28
39
|
|
|
29
|
-
const now = new Date()
|
|
30
|
-
const isExpired =
|
|
40
|
+
const now = new Date();
|
|
41
|
+
const isExpired =
|
|
42
|
+
!!quote.validUntil && quote.validUntil.getTime() < now.getTime();
|
|
31
43
|
|
|
32
44
|
const [lines, adjustments] = await Promise.all([
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
45
|
+
findWithDecryption(
|
|
46
|
+
em,
|
|
47
|
+
SalesQuoteLine,
|
|
48
|
+
{ quote: quote.id, organizationId: quote.organizationId, tenantId: quote.tenantId, deletedAt: null },
|
|
49
|
+
{ orderBy: { lineNumber: "asc" } },
|
|
50
|
+
),
|
|
51
|
+
findWithDecryption(
|
|
52
|
+
em,
|
|
53
|
+
SalesQuoteAdjustment,
|
|
54
|
+
{ quote: quote.id, organizationId: quote.organizationId, tenantId: quote.tenantId },
|
|
55
|
+
{ orderBy: { position: "asc" } },
|
|
56
|
+
),
|
|
57
|
+
]);
|
|
36
58
|
|
|
37
59
|
return NextResponse.json({
|
|
38
60
|
quote: {
|
|
@@ -54,7 +76,17 @@ export async function GET(_req: Request, ctx: { params: { token: string } }) {
|
|
|
54
76
|
name: line.name ?? null,
|
|
55
77
|
description: line.description ?? null,
|
|
56
78
|
quantity: line.quantity,
|
|
57
|
-
quantityUnit: line.quantityUnit ?? null,
|
|
79
|
+
quantityUnit: canonicalizeUnitCode(line.quantityUnit) ?? null,
|
|
80
|
+
normalizedQuantity: line.normalizedQuantity ?? line.quantity,
|
|
81
|
+
normalizedUnit:
|
|
82
|
+
canonicalizeUnitCode(line.normalizedUnit ?? line.quantityUnit) ??
|
|
83
|
+
null,
|
|
84
|
+
uomSnapshot: line.uomSnapshot
|
|
85
|
+
? {
|
|
86
|
+
baseUnitCode: line.uomSnapshot.baseUnitCode ?? null,
|
|
87
|
+
enteredUnitCode: line.uomSnapshot.enteredUnitCode ?? null,
|
|
88
|
+
}
|
|
89
|
+
: null,
|
|
58
90
|
currencyCode: line.currencyCode,
|
|
59
91
|
unitPriceNet: line.unitPriceNet,
|
|
60
92
|
unitPriceGross: line.unitPriceGross,
|
|
@@ -64,6 +96,18 @@ export async function GET(_req: Request, ctx: { params: { token: string } }) {
|
|
|
64
96
|
taxAmount: line.taxAmount,
|
|
65
97
|
totalNetAmount: line.totalNetAmount,
|
|
66
98
|
totalGrossAmount: line.totalGrossAmount,
|
|
99
|
+
unitPriceReference: (() => {
|
|
100
|
+
if (!line.uomSnapshot) return null;
|
|
101
|
+
const ref = line.uomSnapshot.unitPriceReference;
|
|
102
|
+
if (!ref) return null;
|
|
103
|
+
return {
|
|
104
|
+
enabled: ref.enabled ?? null,
|
|
105
|
+
referenceUnitCode: ref.referenceUnitCode ?? null,
|
|
106
|
+
baseQuantity: ref.baseQuantity ?? null,
|
|
107
|
+
grossPerReference: ref.grossPerReference ?? null,
|
|
108
|
+
netPerReference: ref.netPerReference ?? null,
|
|
109
|
+
};
|
|
110
|
+
})(),
|
|
67
111
|
})),
|
|
68
112
|
adjustments: adjustments.map((adj) => ({
|
|
69
113
|
scope: adj.scope,
|
|
@@ -77,14 +121,19 @@ export async function GET(_req: Request, ctx: { params: { token: string } }) {
|
|
|
77
121
|
quoteLineId: adj.quoteLine?.id ?? null,
|
|
78
122
|
})),
|
|
79
123
|
isExpired,
|
|
80
|
-
})
|
|
124
|
+
});
|
|
81
125
|
} catch (err) {
|
|
82
126
|
if (err instanceof CrudHttpError) {
|
|
83
|
-
return NextResponse.json(err.body, { status: err.status })
|
|
127
|
+
return NextResponse.json(err.body, { status: err.status });
|
|
84
128
|
}
|
|
85
|
-
const { translate } = await resolveTranslations()
|
|
86
|
-
console.error(
|
|
87
|
-
return NextResponse.json(
|
|
129
|
+
const { translate } = await resolveTranslations();
|
|
130
|
+
console.error("sales.quotes.public failed", err);
|
|
131
|
+
return NextResponse.json(
|
|
132
|
+
{
|
|
133
|
+
error: translate("sales.quotes.public.failed", "Failed to load quote."),
|
|
134
|
+
},
|
|
135
|
+
{ status: 400 },
|
|
136
|
+
);
|
|
88
137
|
}
|
|
89
138
|
}
|
|
90
139
|
|
|
@@ -110,9 +159,35 @@ const publicQuoteResponseSchema = z.object({
|
|
|
110
159
|
description: z.string().nullable(),
|
|
111
160
|
quantity: z.string(),
|
|
112
161
|
quantityUnit: z.string().nullable(),
|
|
162
|
+
normalizedQuantity: z.string(),
|
|
163
|
+
normalizedUnit: z.string().nullable(),
|
|
164
|
+
uomSnapshot: z
|
|
165
|
+
.object({
|
|
166
|
+
baseUnitCode: z.string().nullable(),
|
|
167
|
+
enteredUnitCode: z.string().nullable(),
|
|
168
|
+
})
|
|
169
|
+
.nullable()
|
|
170
|
+
.optional(),
|
|
113
171
|
currencyCode: z.string(),
|
|
172
|
+
unitPriceNet: z.string(),
|
|
173
|
+
unitPriceGross: z.string(),
|
|
174
|
+
discountAmount: z.string(),
|
|
175
|
+
discountPercent: z.string(),
|
|
176
|
+
taxRate: z.string(),
|
|
177
|
+
taxAmount: z.string(),
|
|
178
|
+
totalNetAmount: z.string(),
|
|
114
179
|
totalGrossAmount: z.string(),
|
|
115
|
-
|
|
180
|
+
unitPriceReference: z
|
|
181
|
+
.object({
|
|
182
|
+
enabled: z.boolean().nullable().optional(),
|
|
183
|
+
referenceUnitCode: z.string().nullable().optional(),
|
|
184
|
+
baseQuantity: z.string().nullable().optional(),
|
|
185
|
+
grossPerReference: z.string().nullable().optional(),
|
|
186
|
+
netPerReference: z.string().nullable().optional(),
|
|
187
|
+
})
|
|
188
|
+
.nullable()
|
|
189
|
+
.optional(),
|
|
190
|
+
}),
|
|
116
191
|
),
|
|
117
192
|
adjustments: z.array(
|
|
118
193
|
z.object({
|
|
@@ -122,22 +197,33 @@ const publicQuoteResponseSchema = z.object({
|
|
|
122
197
|
rate: z.string().nullable(),
|
|
123
198
|
amountNet: z.string().nullable(),
|
|
124
199
|
amountGross: z.string().nullable(),
|
|
125
|
-
|
|
200
|
+
currencyCode: z.string().nullable(),
|
|
201
|
+
position: z.number().nullable(),
|
|
202
|
+
quoteLineId: z.string().uuid().nullable(),
|
|
203
|
+
}),
|
|
126
204
|
),
|
|
127
205
|
isExpired: z.boolean(),
|
|
128
|
-
})
|
|
206
|
+
});
|
|
129
207
|
|
|
130
208
|
export const openApi: OpenApiRouteDoc = {
|
|
131
|
-
tag:
|
|
132
|
-
summary:
|
|
209
|
+
tag: "Sales",
|
|
210
|
+
summary: "View a quote (public)",
|
|
133
211
|
pathParams: z.object({ token: z.string().uuid() }),
|
|
134
212
|
methods: {
|
|
135
213
|
GET: {
|
|
136
|
-
summary:
|
|
214
|
+
summary: "Get quote details by acceptance token",
|
|
137
215
|
responses: [
|
|
138
|
-
{
|
|
139
|
-
|
|
216
|
+
{
|
|
217
|
+
status: 200,
|
|
218
|
+
description: "Quote details",
|
|
219
|
+
schema: publicQuoteResponseSchema,
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
status: 404,
|
|
223
|
+
description: "Quote not found",
|
|
224
|
+
schema: z.object({ error: z.string() }),
|
|
225
|
+
},
|
|
140
226
|
],
|
|
141
227
|
},
|
|
142
228
|
},
|
|
143
|
-
}
|
|
229
|
+
};
|