@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.
Files changed (163) hide show
  1. package/dist/generated/entities/catalog_product/index.js +16 -0
  2. package/dist/generated/entities/catalog_product/index.js.map +2 -2
  3. package/dist/generated/entities/catalog_product_unit_conversion/index.js +27 -0
  4. package/dist/generated/entities/catalog_product_unit_conversion/index.js.map +7 -0
  5. package/dist/generated/entities/sales_credit_memo_line/index.js +7 -1
  6. package/dist/generated/entities/sales_credit_memo_line/index.js.map +2 -2
  7. package/dist/generated/entities/sales_invoice_line/index.js +7 -1
  8. package/dist/generated/entities/sales_invoice_line/index.js.map +2 -2
  9. package/dist/generated/entities/sales_order_line/index.js +6 -0
  10. package/dist/generated/entities/sales_order_line/index.js.map +2 -2
  11. package/dist/generated/entities/sales_quote_line/index.js +6 -0
  12. package/dist/generated/entities/sales_quote_line/index.js.map +2 -2
  13. package/dist/generated/entities.ids.generated.js +1 -0
  14. package/dist/generated/entities.ids.generated.js.map +2 -2
  15. package/dist/generated/entity-fields-registry.js +2 -0
  16. package/dist/generated/entity-fields-registry.js.map +2 -2
  17. package/dist/modules/catalog/api/prices/route.js +123 -8
  18. package/dist/modules/catalog/api/prices/route.js.map +2 -2
  19. package/dist/modules/catalog/api/product-unit-conversions/route.js +194 -0
  20. package/dist/modules/catalog/api/product-unit-conversions/route.js.map +7 -0
  21. package/dist/modules/catalog/api/products/route.js +351 -201
  22. package/dist/modules/catalog/api/products/route.js.map +2 -2
  23. package/dist/modules/catalog/backend/catalog/products/[id]/page.js +1267 -497
  24. package/dist/modules/catalog/backend/catalog/products/[id]/page.js.map +2 -2
  25. package/dist/modules/catalog/backend/catalog/products/create/page.js +733 -210
  26. package/dist/modules/catalog/backend/catalog/products/create/page.js.map +2 -2
  27. package/dist/modules/catalog/commands/index.js +1 -0
  28. package/dist/modules/catalog/commands/index.js.map +2 -2
  29. package/dist/modules/catalog/commands/productUnitConversions.js +503 -0
  30. package/dist/modules/catalog/commands/productUnitConversions.js.map +7 -0
  31. package/dist/modules/catalog/commands/products.js +355 -73
  32. package/dist/modules/catalog/commands/products.js.map +2 -2
  33. package/dist/modules/catalog/commands/shared.js +18 -4
  34. package/dist/modules/catalog/commands/shared.js.map +2 -2
  35. package/dist/modules/catalog/components/products/ProductUomSection.js +591 -0
  36. package/dist/modules/catalog/components/products/ProductUomSection.js.map +7 -0
  37. package/dist/modules/catalog/components/products/productForm.js +66 -5
  38. package/dist/modules/catalog/components/products/productForm.js.map +2 -2
  39. package/dist/modules/catalog/components/products/productFormUtils.js +68 -0
  40. package/dist/modules/catalog/components/products/productFormUtils.js.map +7 -0
  41. package/dist/modules/catalog/data/entities.js +86 -0
  42. package/dist/modules/catalog/data/entities.js.map +2 -2
  43. package/dist/modules/catalog/data/validators.js +65 -3
  44. package/dist/modules/catalog/data/validators.js.map +2 -2
  45. package/dist/modules/catalog/events.js +3 -0
  46. package/dist/modules/catalog/events.js.map +2 -2
  47. package/dist/modules/catalog/lib/unitCodes.js +7 -0
  48. package/dist/modules/catalog/lib/unitCodes.js.map +7 -0
  49. package/dist/modules/catalog/lib/unitResolution.js +53 -0
  50. package/dist/modules/catalog/lib/unitResolution.js.map +7 -0
  51. package/dist/modules/catalog/migrations/Migration20260218225422.js +19 -0
  52. package/dist/modules/catalog/migrations/Migration20260218225422.js.map +7 -0
  53. package/dist/modules/catalog/migrations/Migration20260219084500.js +27 -0
  54. package/dist/modules/catalog/migrations/Migration20260219084500.js.map +7 -0
  55. package/dist/modules/catalog/search.js +69 -1
  56. package/dist/modules/catalog/search.js.map +2 -2
  57. package/dist/modules/catalog/seed/examples.js +91 -42
  58. package/dist/modules/catalog/seed/examples.js.map +2 -2
  59. package/dist/modules/dashboards/seed/analytics.js +3 -0
  60. package/dist/modules/dashboards/seed/analytics.js.map +2 -2
  61. package/dist/modules/sales/api/order-lines/route.js +98 -15
  62. package/dist/modules/sales/api/order-lines/route.js.map +2 -2
  63. package/dist/modules/sales/api/quote-lines/route.js +101 -14
  64. package/dist/modules/sales/api/quote-lines/route.js.map +2 -2
  65. package/dist/modules/sales/api/quotes/public/[token]/route.js +87 -12
  66. package/dist/modules/sales/api/quotes/public/[token]/route.js.map +2 -2
  67. package/dist/modules/sales/commands/documents.js +1424 -260
  68. package/dist/modules/sales/commands/documents.js.map +3 -3
  69. package/dist/modules/sales/commands/shared.js +6 -2
  70. package/dist/modules/sales/commands/shared.js.map +2 -2
  71. package/dist/modules/sales/components/documents/ItemsSection.js +216 -86
  72. package/dist/modules/sales/components/documents/ItemsSection.js.map +2 -2
  73. package/dist/modules/sales/components/documents/LineItemDialog.js +913 -241
  74. package/dist/modules/sales/components/documents/LineItemDialog.js.map +3 -3
  75. package/dist/modules/sales/components/documents/ShipmentsSection.js +15 -3
  76. package/dist/modules/sales/components/documents/ShipmentsSection.js.map +2 -2
  77. package/dist/modules/sales/data/entities.js +59 -3
  78. package/dist/modules/sales/data/entities.js.map +2 -2
  79. package/dist/modules/sales/data/validators.js +35 -0
  80. package/dist/modules/sales/data/validators.js.map +2 -2
  81. package/dist/modules/sales/frontend/quote/[token]/page.js +15 -1
  82. package/dist/modules/sales/frontend/quote/[token]/page.js.map +2 -2
  83. package/dist/modules/sales/migrations/Migration20260218225423.js +31 -0
  84. package/dist/modules/sales/migrations/Migration20260218225423.js.map +7 -0
  85. package/dist/modules/sales/migrations/Migration20260219084501.js +71 -0
  86. package/dist/modules/sales/migrations/Migration20260219084501.js.map +7 -0
  87. package/dist/modules/sales/search.js +28 -0
  88. package/dist/modules/sales/search.js.map +2 -2
  89. package/dist/modules/sales/seed/examples.js +14 -1
  90. package/dist/modules/sales/seed/examples.js.map +2 -2
  91. package/dist/modules/sales/widgets/injection/document-history/widget.client.js +1 -1
  92. package/dist/modules/sales/widgets/injection/document-history/widget.client.js.map +2 -2
  93. package/generated/entities/catalog_product/index.ts +8 -0
  94. package/generated/entities/catalog_product_unit_conversion/index.ts +12 -0
  95. package/generated/entities/sales_credit_memo_line/index.ts +3 -0
  96. package/generated/entities/sales_invoice_line/index.ts +3 -0
  97. package/generated/entities/sales_order_line/index.ts +3 -0
  98. package/generated/entities/sales_quote_line/index.ts +3 -0
  99. package/generated/entities.ids.generated.ts +1 -0
  100. package/generated/entity-fields-registry.ts +2 -0
  101. package/package.json +2 -2
  102. package/src/modules/auth/i18n/de.json +1 -1
  103. package/src/modules/auth/i18n/en.json +1 -1
  104. package/src/modules/auth/i18n/es.json +1 -1
  105. package/src/modules/auth/i18n/pl.json +1 -1
  106. package/src/modules/catalog/api/prices/route.ts +213 -81
  107. package/src/modules/catalog/api/product-unit-conversions/route.ts +195 -0
  108. package/src/modules/catalog/api/products/route.ts +638 -402
  109. package/src/modules/catalog/backend/catalog/products/[id]/page.tsx +2085 -1072
  110. package/src/modules/catalog/backend/catalog/products/create/page.tsx +1288 -593
  111. package/src/modules/catalog/commands/index.ts +1 -0
  112. package/src/modules/catalog/commands/productUnitConversions.ts +626 -0
  113. package/src/modules/catalog/commands/products.ts +1151 -693
  114. package/src/modules/catalog/commands/shared.ts +19 -5
  115. package/src/modules/catalog/components/products/ProductUomSection.tsx +745 -0
  116. package/src/modules/catalog/components/products/productForm.ts +369 -256
  117. package/src/modules/catalog/components/products/productFormUtils.ts +82 -0
  118. package/src/modules/catalog/data/entities.ts +82 -1
  119. package/src/modules/catalog/data/validators.ts +118 -34
  120. package/src/modules/catalog/events.ts +3 -0
  121. package/src/modules/catalog/i18n/de.json +56 -0
  122. package/src/modules/catalog/i18n/en.json +56 -0
  123. package/src/modules/catalog/i18n/es.json +56 -0
  124. package/src/modules/catalog/i18n/pl.json +56 -0
  125. package/src/modules/catalog/lib/unitCodes.ts +1 -0
  126. package/src/modules/catalog/lib/unitResolution.ts +62 -0
  127. package/src/modules/catalog/migrations/.snapshot-open-mercato.json +245 -0
  128. package/src/modules/catalog/migrations/Migration20260218225422.ts +21 -0
  129. package/src/modules/catalog/migrations/Migration20260219084500.ts +26 -0
  130. package/src/modules/catalog/search.ts +73 -1
  131. package/src/modules/catalog/seed/examples.ts +552 -479
  132. package/src/modules/dashboards/i18n/de.json +1 -1
  133. package/src/modules/dashboards/i18n/en.json +1 -1
  134. package/src/modules/dashboards/i18n/es.json +1 -1
  135. package/src/modules/dashboards/i18n/pl.json +1 -1
  136. package/src/modules/dashboards/seed/analytics.ts +3 -0
  137. package/src/modules/sales/api/order-lines/route.ts +158 -68
  138. package/src/modules/sales/api/quote-lines/route.ts +161 -67
  139. package/src/modules/sales/api/quotes/public/[token]/route.ts +122 -36
  140. package/src/modules/sales/commands/documents.ts +4250 -2424
  141. package/src/modules/sales/commands/shared.ts +7 -2
  142. package/src/modules/sales/components/documents/ItemsSection.tsx +580 -310
  143. package/src/modules/sales/components/documents/LineItemDialog.tsx +1988 -833
  144. package/src/modules/sales/components/documents/ShipmentsSection.tsx +17 -3
  145. package/src/modules/sales/components/documents/lineItemTypes.ts +6 -0
  146. package/src/modules/sales/data/entities.ts +53 -0
  147. package/src/modules/sales/data/validators.ts +36 -0
  148. package/src/modules/sales/frontend/quote/[token]/page.tsx +25 -1
  149. package/src/modules/sales/i18n/de.json +23 -3
  150. package/src/modules/sales/i18n/en.json +23 -3
  151. package/src/modules/sales/i18n/es.json +23 -3
  152. package/src/modules/sales/i18n/pl.json +23 -3
  153. package/src/modules/sales/lib/types.ts +30 -0
  154. package/src/modules/sales/migrations/.snapshot-open-mercato.json +172 -0
  155. package/src/modules/sales/migrations/Migration20260218225423.ts +37 -0
  156. package/src/modules/sales/migrations/Migration20260219084501.ts +73 -0
  157. package/src/modules/sales/search.ts +28 -0
  158. package/src/modules/sales/seed/examples.ts +20 -1
  159. package/src/modules/sales/widgets/injection/document-history/widget.client.tsx +1 -1
  160. package/src/modules/workflows/i18n/de.json +4 -4
  161. package/src/modules/workflows/i18n/en.json +4 -4
  162. package/src/modules/workflows/i18n/es.json +4 -4
  163. package/src/modules/workflows/i18n/pl.json +4 -4
@@ -12,6 +12,13 @@ export const product_type = 'product_type'
12
12
  export const status_entry_id = 'status_entry_id'
13
13
  export const primary_currency_code = 'primary_currency_code'
14
14
  export const default_unit = 'default_unit'
15
+ export const default_sales_unit = 'default_sales_unit'
16
+ export const default_sales_unit_quantity = 'default_sales_unit_quantity'
17
+ export const uom_rounding_scale = 'uom_rounding_scale'
18
+ export const uom_rounding_mode = 'uom_rounding_mode'
19
+ export const unit_price_enabled = 'unit_price_enabled'
20
+ export const unit_price_reference_unit = 'unit_price_reference_unit'
21
+ export const unit_price_base_quantity = 'unit_price_base_quantity'
15
22
  export const default_media_id = 'default_media_id'
16
23
  export const default_media_url = 'default_media_url'
17
24
  export const weight_value = 'weight_value'
@@ -29,3 +36,4 @@ export const variants = 'variants'
29
36
  export const offers = 'offers'
30
37
  export const category_assignments = 'category_assignments'
31
38
  export const tag_assignments = 'tag_assignments'
39
+ export const unit_conversions = 'unit_conversions'
@@ -0,0 +1,12 @@
1
+ export const id = 'id'
2
+ export const product = 'product'
3
+ export const organization_id = 'organization_id'
4
+ export const tenant_id = 'tenant_id'
5
+ export const unit_code = 'unit_code'
6
+ export const to_base_factor = 'to_base_factor'
7
+ export const sort_order = 'sort_order'
8
+ export const is_active = 'is_active'
9
+ export const metadata = 'metadata'
10
+ export const created_at = 'created_at'
11
+ export const updated_at = 'updated_at'
12
+ export const deleted_at = 'deleted_at'
@@ -7,6 +7,9 @@ export const line_number = 'line_number'
7
7
  export const description = 'description'
8
8
  export const quantity = 'quantity'
9
9
  export const quantity_unit = 'quantity_unit'
10
+ export const normalized_quantity = 'normalized_quantity'
11
+ export const normalized_unit = 'normalized_unit'
12
+ export const uom_snapshot = 'uom_snapshot'
10
13
  export const currency_code = 'currency_code'
11
14
  export const unit_price_net = 'unit_price_net'
12
15
  export const unit_price_gross = 'unit_price_gross'
@@ -8,6 +8,9 @@ export const kind = 'kind'
8
8
  export const description = 'description'
9
9
  export const quantity = 'quantity'
10
10
  export const quantity_unit = 'quantity_unit'
11
+ export const normalized_quantity = 'normalized_quantity'
12
+ export const normalized_unit = 'normalized_unit'
13
+ export const uom_snapshot = 'uom_snapshot'
11
14
  export const currency_code = 'currency_code'
12
15
  export const unit_price_net = 'unit_price_net'
13
16
  export const unit_price_gross = 'unit_price_gross'
@@ -14,6 +14,9 @@ export const description = 'description'
14
14
  export const comment = 'comment'
15
15
  export const quantity = 'quantity'
16
16
  export const quantity_unit = 'quantity_unit'
17
+ export const normalized_quantity = 'normalized_quantity'
18
+ export const normalized_unit = 'normalized_unit'
19
+ export const uom_snapshot = 'uom_snapshot'
17
20
  export const reserved_quantity = 'reserved_quantity'
18
21
  export const fulfilled_quantity = 'fulfilled_quantity'
19
22
  export const invoiced_quantity = 'invoiced_quantity'
@@ -14,6 +14,9 @@ export const description = 'description'
14
14
  export const comment = 'comment'
15
15
  export const quantity = 'quantity'
16
16
  export const quantity_unit = 'quantity_unit'
17
+ export const normalized_quantity = 'normalized_quantity'
18
+ export const normalized_unit = 'normalized_unit'
19
+ export const uom_snapshot = 'uom_snapshot'
17
20
  export const currency_code = 'currency_code'
18
21
  export const unit_price_net = 'unit_price_net'
19
22
  export const unit_price_gross = 'unit_price_gross'
@@ -100,6 +100,7 @@ export const E = {
100
100
  "catalog": {
101
101
  "catalog_option_schema_template": "catalog:catalog_option_schema_template",
102
102
  "catalog_product": "catalog:catalog_product",
103
+ "catalog_product_unit_conversion": "catalog:catalog_product_unit_conversion",
103
104
  "catalog_product_category": "catalog:catalog_product_category",
104
105
  "catalog_product_category_assignment": "catalog:catalog_product_category_assignment",
105
106
  "catalog_product_tag": "catalog:catalog_product_tag",
@@ -15,6 +15,7 @@ import * as catalog_product_category_assignment from './entities/catalog_product
15
15
  import * as catalog_product_price from './entities/catalog_product_price/index'
16
16
  import * as catalog_product_tag from './entities/catalog_product_tag/index'
17
17
  import * as catalog_product_tag_assignment from './entities/catalog_product_tag_assignment/index'
18
+ import * as catalog_product_unit_conversion from './entities/catalog_product_unit_conversion/index'
18
19
  import * as catalog_product_variant from './entities/catalog_product_variant/index'
19
20
  import * as catalog_product_variant_relation from './entities/catalog_product_variant_relation/index'
20
21
  import * as currency from './entities/currency/index'
@@ -148,6 +149,7 @@ export const entityFieldsRegistry: Record<string, Record<string, string>> = {
148
149
  catalog_product_price,
149
150
  catalog_product_tag,
150
151
  catalog_product_tag_assignment,
152
+ catalog_product_unit_conversion,
151
153
  catalog_product_variant,
152
154
  catalog_product_variant_relation,
153
155
  currency,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.4.5-develop-f4858e0ef3",
3
+ "version": "0.4.5-develop-4849712ccb",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -207,7 +207,7 @@
207
207
  }
208
208
  },
209
209
  "dependencies": {
210
- "@open-mercato/shared": "0.4.5-develop-f4858e0ef3",
210
+ "@open-mercato/shared": "0.4.5-develop-4849712ccb",
211
211
  "@types/semver": "^7.5.8",
212
212
  "@xyflow/react": "^12.6.0",
213
213
  "ai": "^6.0.0",
@@ -78,10 +78,10 @@
78
78
  "auth.reset.description": "Gib deine E-Mail-Adresse ein, um einen Zurücksetzungslink zu erhalten",
79
79
  "auth.reset.error": "Etwas ist schiefgelaufen",
80
80
  "auth.reset.errors.failed": "Passwort konnte nicht zurückgesetzt werden",
81
- "auth.reset.sent": "Falls ein Konto mit dieser E-Mail-Adresse existiert, haben wir einen Zurücksetzungslink gesendet. Überprüfe deinen Posteingang.",
82
81
  "auth.reset.form.loading": "...",
83
82
  "auth.reset.form.password": "Neues Passwort",
84
83
  "auth.reset.form.submit": "Passwort aktualisieren",
84
+ "auth.reset.sent": "Falls ein Konto mit dieser E-Mail-Adresse existiert, haben wir einen Zurücksetzungslink gesendet. Überprüfe deinen Posteingang.",
85
85
  "auth.reset.subtitle": "Wähle ein sicheres Passwort für dein Konto.",
86
86
  "auth.reset.title": "Neues Passwort festlegen",
87
87
  "auth.resetPassword": "Passwort zurücksetzen",
@@ -78,10 +78,10 @@
78
78
  "auth.reset.description": "Enter your email to receive reset link",
79
79
  "auth.reset.error": "Something went wrong",
80
80
  "auth.reset.errors.failed": "Unable to reset password",
81
- "auth.reset.sent": "If an account with that email exists, we sent a reset link. Please check your inbox.",
82
81
  "auth.reset.form.loading": "...",
83
82
  "auth.reset.form.password": "New password",
84
83
  "auth.reset.form.submit": "Update password",
84
+ "auth.reset.sent": "If an account with that email exists, we sent a reset link. Please check your inbox.",
85
85
  "auth.reset.subtitle": "Choose a strong password for your account.",
86
86
  "auth.reset.title": "Set a new password",
87
87
  "auth.resetPassword": "Reset password",
@@ -78,10 +78,10 @@
78
78
  "auth.reset.description": "Ingresa tu correo para recibir un enlace de restablecimiento",
79
79
  "auth.reset.error": "Algo salió mal",
80
80
  "auth.reset.errors.failed": "No se pudo restablecer la contraseña",
81
- "auth.reset.sent": "Si existe una cuenta con ese correo, enviamos un enlace de restablecimiento. Revisa tu bandeja de entrada.",
82
81
  "auth.reset.form.loading": "...",
83
82
  "auth.reset.form.password": "Nueva contraseña",
84
83
  "auth.reset.form.submit": "Actualizar contraseña",
84
+ "auth.reset.sent": "Si existe una cuenta con ese correo, enviamos un enlace de restablecimiento. Revisa tu bandeja de entrada.",
85
85
  "auth.reset.subtitle": "Elige una contraseña segura para tu cuenta.",
86
86
  "auth.reset.title": "Establecer una nueva contraseña",
87
87
  "auth.resetPassword": "Restablecer contraseña",
@@ -78,10 +78,10 @@
78
78
  "auth.reset.description": "Podaj swój adres e-mail, aby otrzymać link do resetowania",
79
79
  "auth.reset.error": "Coś poszło nie tak",
80
80
  "auth.reset.errors.failed": "Nie udało się zresetować hasła",
81
- "auth.reset.sent": "Jeśli konto z tym adresem e-mail istnieje, wysłaliśmy link do resetowania. Sprawdź swoją skrzynkę odbiorczą.",
82
81
  "auth.reset.form.loading": "...",
83
82
  "auth.reset.form.password": "Nowe hasło",
84
83
  "auth.reset.form.submit": "Zaktualizuj hasło",
84
+ "auth.reset.sent": "Jeśli konto z tym adresem e-mail istnieje, wysłaliśmy link do resetowania. Sprawdź swoją skrzynkę odbiorczą.",
85
85
  "auth.reset.subtitle": "Wybierz silne hasło dla swojego konta.",
86
86
  "auth.reset.title": "Ustaw nowe hasło",
87
87
  "auth.resetPassword": "Resetuj hasło",
@@ -1,21 +1,34 @@
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 { buildCustomFieldFiltersFromQuery, extractAllCustomFieldEntries } from '@open-mercato/shared/lib/crud/custom-fields'
6
- import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
7
- import { CatalogProductPrice } from '../../data/entities'
8
- import { priceCreateSchema, priceUpdateSchema } from '../../data/validators'
9
- import { parseScopedCommandInput, resolveCrudRecordId } from '../utils'
10
- import { E } from '#generated/entities.ids.generated'
11
- import * as FP from '#generated/entities/catalog_product_price'
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";
10
+ import {
11
+ CatalogProduct,
12
+ CatalogProductPrice,
13
+ CatalogProductUnitConversion,
14
+ CatalogProductVariant,
15
+ } from "../../data/entities";
16
+ import { priceCreateSchema, priceUpdateSchema } from "../../data/validators";
17
+ import { parseScopedCommandInput, resolveCrudRecordId } from "../utils";
18
+ import { E } from "#generated/entities.ids.generated";
19
+ import * as FP from "#generated/entities/catalog_product_price";
12
20
  import {
13
21
  createCatalogCrudOpenApi,
14
22
  createPagedListResponseSchema,
15
23
  defaultOkResponseSchema,
16
- } from '../openapi'
24
+ } from "../openapi";
25
+ import { toUnitLookupKey } from "../../lib/unitCodes";
26
+ import {
27
+ findWithDecryption,
28
+ findOneWithDecryption,
29
+ } from "@open-mercato/shared/lib/encryption/find";
17
30
 
18
- const rawBodySchema = z.object({}).passthrough()
31
+ const rawBodySchema = z.object({}).passthrough();
19
32
 
20
33
  const listSchema = z
21
34
  .object({
@@ -32,74 +45,154 @@ const listSchema = z
32
45
  userGroupId: z.string().uuid().optional(),
33
46
  customerId: z.string().uuid().optional(),
34
47
  customerGroupId: z.string().uuid().optional(),
48
+ quantity: z.coerce.number().min(1).max(100000).optional(),
49
+ quantityUnit: z.string().trim().max(50).optional(),
35
50
  withDeleted: z.coerce.boolean().optional(),
36
51
  sortField: z.string().optional(),
37
- sortDir: z.enum(['asc', 'desc']).optional(),
52
+ sortDir: z.enum(["asc", "desc"]).optional(),
38
53
  })
39
- .passthrough()
54
+ .passthrough();
40
55
 
41
- type PriceQuery = z.infer<typeof listSchema>
56
+ type PriceQuery = z.infer<typeof listSchema>;
42
57
 
43
58
  const routeMetadata = {
44
- GET: { requireAuth: true, requireFeatures: ['catalog.products.view'] },
45
- POST: { requireAuth: true, requireFeatures: ['catalog.pricing.manage'] },
46
- PUT: { requireAuth: true, requireFeatures: ['catalog.pricing.manage'] },
47
- DELETE: { requireAuth: true, requireFeatures: ['catalog.pricing.manage'] },
48
- }
59
+ GET: { requireAuth: true, requireFeatures: ["catalog.products.view"] },
60
+ POST: { requireAuth: true, requireFeatures: ["catalog.pricing.manage"] },
61
+ PUT: { requireAuth: true, requireFeatures: ["catalog.pricing.manage"] },
62
+ DELETE: { requireAuth: true, requireFeatures: ["catalog.pricing.manage"] },
63
+ };
49
64
 
50
- export const metadata = routeMetadata
65
+ export const metadata = routeMetadata;
66
+
67
+ async function resolveNormalizedQuantityForFilter(params: {
68
+ em: EntityManager;
69
+ organizationId: string | null;
70
+ tenantId: string | null;
71
+ productId?: string;
72
+ variantId?: string;
73
+ quantity: number;
74
+ quantityUnit?: string;
75
+ }): Promise<number> {
76
+ const quantity = params.quantity;
77
+ const quantityUnitKey = toUnitLookupKey(params.quantityUnit);
78
+ if ((!params.productId && !params.variantId) || !quantityUnitKey)
79
+ return quantity;
80
+ if (!params.organizationId || !params.tenantId) return quantity;
81
+ let targetProductId = params.productId;
82
+ if (!targetProductId && params.variantId) {
83
+ const variant = await findOneWithDecryption(
84
+ params.em,
85
+ CatalogProductVariant,
86
+ {
87
+ id: params.variantId,
88
+ organizationId: params.organizationId,
89
+ tenantId: params.tenantId,
90
+ deletedAt: null,
91
+ },
92
+ { fields: ["id", "product"] },
93
+ );
94
+ if (variant) {
95
+ targetProductId =
96
+ typeof variant.product === "string"
97
+ ? variant.product
98
+ : (variant.product?.id ?? null);
99
+ }
100
+ }
101
+ if (!targetProductId) return quantity;
102
+ const product = await findOneWithDecryption(
103
+ params.em,
104
+ CatalogProduct,
105
+ {
106
+ id: targetProductId,
107
+ organizationId: params.organizationId,
108
+ tenantId: params.tenantId,
109
+ deletedAt: null,
110
+ },
111
+ { fields: ["id", "defaultUnit", "uomRoundingScale"] },
112
+ );
113
+ if (!product) return quantity;
114
+ const baseUnitKey = toUnitLookupKey(product.defaultUnit);
115
+ if (!baseUnitKey || baseUnitKey === quantityUnitKey) return quantity;
116
+ const conversions = await findWithDecryption(
117
+ params.em,
118
+ CatalogProductUnitConversion,
119
+ {
120
+ product: product.id,
121
+ organizationId: params.organizationId,
122
+ tenantId: params.tenantId,
123
+ isActive: true,
124
+ deletedAt: null,
125
+ },
126
+ { fields: ["id", "unitCode", "toBaseFactor"] },
127
+ );
128
+ const conversion = conversions.find(
129
+ (entry) => toUnitLookupKey(entry.unitCode) === quantityUnitKey,
130
+ );
131
+ if (!conversion) return quantity;
132
+ const factor = Number(conversion.toBaseFactor);
133
+ if (!Number.isFinite(factor) || factor <= 0) return quantity;
134
+ const rawNormalized = quantity * factor;
135
+ const scale = Number(product.uomRoundingScale ?? 4);
136
+ const pow = Math.pow(10, scale);
137
+ const normalized = Math.round(rawNormalized * pow) / pow;
138
+ return Number.isFinite(normalized) && normalized > 0 ? normalized : quantity;
139
+ }
51
140
 
52
141
  export async function buildPriceFilters(
53
- query: PriceQuery
142
+ query: PriceQuery,
54
143
  ): Promise<Record<string, unknown>> {
55
- const filters: Record<string, unknown> = {}
144
+ const filters: Record<string, unknown> = {};
56
145
  if (query.productId) {
57
- filters.product_id = { $eq: query.productId }
146
+ filters.product_id = { $eq: query.productId };
58
147
  }
59
148
  if (query.variantId) {
60
- filters.variant_id = { $eq: query.variantId }
149
+ filters.variant_id = { $eq: query.variantId };
61
150
  }
62
151
  if (query.offerId) {
63
- filters.offer_id = { $eq: query.offerId }
152
+ filters.offer_id = { $eq: query.offerId };
64
153
  }
65
154
  if (query.channelId) {
66
- filters.channel_id = { $eq: query.channelId }
155
+ filters.channel_id = { $eq: query.channelId };
67
156
  }
68
157
  if (query.currencyCode) {
69
- filters.currency_code = { $eq: query.currencyCode.trim().toUpperCase() }
158
+ filters.currency_code = { $eq: query.currencyCode.trim().toUpperCase() };
70
159
  }
71
160
  if (query.priceKindId) {
72
- filters.price_kind_id = { $eq: query.priceKindId }
161
+ filters.price_kind_id = { $eq: query.priceKindId };
73
162
  }
74
163
  if (query.kind) {
75
- filters.kind = { $eq: query.kind }
164
+ filters.kind = { $eq: query.kind };
76
165
  }
77
- if (query.userId) filters.user_id = { $eq: query.userId }
78
- if (query.userGroupId) filters.user_group_id = { $eq: query.userGroupId }
79
- if (query.customerId) filters.customer_id = { $eq: query.customerId }
80
- if (query.customerGroupId) filters.customer_group_id = { $eq: query.customerGroupId }
81
- return filters
166
+ if (query.userId) filters.user_id = { $eq: query.userId };
167
+ if (query.userGroupId) filters.user_group_id = { $eq: query.userGroupId };
168
+ if (query.customerId) filters.customer_id = { $eq: query.customerId };
169
+ if (query.customerGroupId)
170
+ filters.customer_group_id = { $eq: query.customerGroupId };
171
+ return filters;
82
172
  }
83
173
 
84
174
  const crud = makeCrudRoute({
85
175
  metadata: routeMetadata,
86
176
  orm: {
87
177
  entity: CatalogProductPrice,
88
- idField: 'id',
89
- orgField: 'organizationId',
90
- tenantField: 'tenantId',
178
+ idField: "id",
179
+ orgField: "organizationId",
180
+ tenantField: "tenantId",
91
181
  softDeleteField: null,
92
182
  },
183
+ indexer: {
184
+ entityType: E.catalog.catalog_product_price,
185
+ },
93
186
  list: {
94
187
  schema: listSchema,
95
188
  entityId: E.catalog.catalog_product_price,
96
189
  fields: [
97
190
  FP.id,
98
- 'product_id',
99
- 'variant_id',
100
- 'offer_id',
191
+ "product_id",
192
+ "variant_id",
193
+ "offer_id",
101
194
  FP.currency_code,
102
- 'price_kind_id',
195
+ "price_kind_id",
103
196
  FP.kind,
104
197
  FP.min_quantity,
105
198
  FP.max_quantity,
@@ -120,77 +213,116 @@ const crud = makeCrudRoute({
120
213
  ],
121
214
  sortFieldMap: {
122
215
  currencyCode: FP.currency_code,
123
- priceKindId: 'price_kind_id',
216
+ priceKindId: "price_kind_id",
124
217
  kind: FP.kind,
125
218
  minQuantity: FP.min_quantity,
126
219
  createdAt: FP.created_at,
127
220
  updatedAt: FP.updated_at,
128
221
  },
129
222
  buildFilters: async (query, ctx) => {
130
- const filters = await buildPriceFilters(query)
131
- const tenantId = ctx.auth?.tenantId ?? null
223
+ const filters = await buildPriceFilters(query);
224
+ const tenantId = ctx.auth?.tenantId ?? null;
225
+ const organizationId =
226
+ ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null;
227
+ const em = ctx.container.resolve("em") as EntityManager;
132
228
  try {
133
- const em = ctx.container.resolve('em') as EntityManager
134
229
  const cfFilters = await buildCustomFieldFiltersFromQuery({
135
230
  entityIds: [E.catalog.catalog_product_price],
136
231
  query,
137
232
  em,
138
233
  tenantId,
139
- })
140
- Object.assign(filters, cfFilters)
141
- } catch {
142
- // ignore
234
+ });
235
+ Object.assign(filters, cfFilters);
236
+ } catch (err) {
237
+ // Custom field filter parsing may fail for non-existent or misconfigured fields.
238
+ if (process.env.NODE_ENV === 'development') console.warn('[catalog:prices] custom field filter error', err);
239
+ }
240
+ if (
241
+ typeof query.quantity === "number" &&
242
+ Number.isFinite(query.quantity) &&
243
+ query.quantity > 0
244
+ ) {
245
+ const normalizedQuantity = await resolveNormalizedQuantityForFilter({
246
+ em,
247
+ organizationId,
248
+ tenantId,
249
+ productId: query.productId,
250
+ variantId: query.variantId,
251
+ quantity: query.quantity,
252
+ quantityUnit: query.quantityUnit,
253
+ });
254
+ filters.min_quantity = { $lte: normalizedQuantity };
255
+ filters.$or = [
256
+ { max_quantity: null },
257
+ { max_quantity: { $gte: normalizedQuantity } },
258
+ ];
143
259
  }
144
- return filters
260
+ return filters;
145
261
  },
146
- transformItem: (item: any) => {
147
- if (!item) return item
148
- const normalized = { ...item }
149
- const cfEntries = extractAllCustomFieldEntries(item)
262
+ transformItem: (item: Record<string, unknown> | null | undefined) => {
263
+ if (!item) return item;
264
+ const normalized = { ...item };
265
+ const cfEntries = extractAllCustomFieldEntries(item);
150
266
  for (const key of Object.keys(normalized)) {
151
- if (key.startsWith('cf:')) delete normalized[key]
267
+ if (key.startsWith("cf:")) delete normalized[key];
152
268
  }
153
- return { ...normalized, ...cfEntries }
269
+ return { ...normalized, ...cfEntries };
154
270
  },
155
271
  },
156
272
  actions: {
157
273
  create: {
158
- commandId: 'catalog.prices.create',
274
+ commandId: "catalog.prices.create",
159
275
  schema: rawBodySchema,
160
276
  mapInput: async ({ raw, ctx }) => {
161
- const { translate } = await resolveTranslations()
162
- return parseScopedCommandInput(priceCreateSchema, raw ?? {}, ctx, translate)
277
+ const { translate } = await resolveTranslations();
278
+ return parseScopedCommandInput(
279
+ priceCreateSchema,
280
+ raw ?? {},
281
+ ctx,
282
+ translate,
283
+ );
163
284
  },
164
285
  response: ({ result }) => ({ id: result?.priceId ?? result?.id ?? null }),
165
286
  status: 201,
166
287
  },
167
288
  update: {
168
- commandId: 'catalog.prices.update',
289
+ commandId: "catalog.prices.update",
169
290
  schema: rawBodySchema,
170
291
  mapInput: async ({ raw, ctx }) => {
171
- const { translate } = await resolveTranslations()
172
- return parseScopedCommandInput(priceUpdateSchema, raw ?? {}, ctx, translate)
292
+ const { translate } = await resolveTranslations();
293
+ return parseScopedCommandInput(
294
+ priceUpdateSchema,
295
+ raw ?? {},
296
+ ctx,
297
+ translate,
298
+ );
173
299
  },
174
300
  response: () => ({ ok: true }),
175
301
  },
176
302
  delete: {
177
- commandId: 'catalog.prices.delete',
303
+ commandId: "catalog.prices.delete",
178
304
  schema: rawBodySchema,
179
305
  mapInput: async ({ parsed, ctx }) => {
180
- const { translate } = await resolveTranslations()
181
- const id = resolveCrudRecordId(parsed, ctx, translate)
182
- if (!id) throw new CrudHttpError(400, { error: translate('catalog.errors.id_required', 'Price id is required.') })
183
- return { id }
306
+ const { translate } = await resolveTranslations();
307
+ const id = resolveCrudRecordId(parsed, ctx, translate);
308
+ if (!id)
309
+ throw new CrudHttpError(400, {
310
+ error: translate(
311
+ "catalog.errors.id_required",
312
+ "Price id is required.",
313
+ ),
314
+ });
315
+ return { id };
184
316
  },
185
317
  response: () => ({ ok: true }),
186
318
  },
187
319
  },
188
- })
320
+ });
189
321
 
190
- export const GET = crud.GET
191
- export const POST = crud.POST
192
- export const PUT = crud.PUT
193
- export const DELETE = crud.DELETE
322
+ export const GET = crud.GET;
323
+ export const POST = crud.POST;
324
+ export const PUT = crud.PUT;
325
+ export const DELETE = crud.DELETE;
194
326
 
195
327
  const priceListItemSchema = z.object({
196
328
  id: z.string().uuid(),
@@ -216,25 +348,25 @@ const priceListItemSchema = z.object({
216
348
  ends_at: z.string().nullable().optional(),
217
349
  created_at: z.string().nullable().optional(),
218
350
  updated_at: z.string().nullable().optional(),
219
- })
351
+ });
220
352
 
221
353
  export const openApi = createCatalogCrudOpenApi({
222
- resourceName: 'Price',
223
- pluralName: 'Prices',
354
+ resourceName: "Price",
355
+ pluralName: "Prices",
224
356
  querySchema: listSchema,
225
357
  listResponseSchema: createPagedListResponseSchema(priceListItemSchema),
226
358
  create: {
227
359
  schema: priceCreateSchema,
228
- description: 'Creates a new price entry for a product or variant.',
360
+ description: "Creates a new price entry for a product or variant.",
229
361
  },
230
362
  update: {
231
363
  schema: priceUpdateSchema,
232
364
  responseSchema: defaultOkResponseSchema,
233
- description: 'Updates an existing price by id.',
365
+ description: "Updates an existing price by id.",
234
366
  },
235
367
  del: {
236
368
  schema: z.object({ id: z.string().uuid() }),
237
369
  responseSchema: defaultOkResponseSchema,
238
- description: 'Deletes a price by id.',
370
+ description: "Deletes a price by id.",
239
371
  },
240
- })
372
+ });