@open-mercato/core 0.8.1-develop.7221.1.ece7ca874e → 0.8.1-develop.7227.1.9248c14599

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 (102) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/catalog/backend/catalog/prices/[id]/edit/page.js +166 -0
  3. package/dist/modules/catalog/backend/catalog/prices/[id]/edit/page.js.map +7 -0
  4. package/dist/modules/catalog/backend/catalog/prices/[id]/edit/page.meta.js +16 -0
  5. package/dist/modules/catalog/backend/catalog/prices/[id]/edit/page.meta.js.map +7 -0
  6. package/dist/modules/catalog/backend/catalog/prices/create/page.js +68 -0
  7. package/dist/modules/catalog/backend/catalog/prices/create/page.js.map +7 -0
  8. package/dist/modules/catalog/backend/catalog/prices/create/page.meta.js +25 -0
  9. package/dist/modules/catalog/backend/catalog/prices/create/page.meta.js.map +7 -0
  10. package/dist/modules/catalog/backend/catalog/prices/page.js +11 -0
  11. package/dist/modules/catalog/backend/catalog/prices/page.js.map +7 -0
  12. package/dist/modules/catalog/backend/catalog/prices/page.meta.js +16 -0
  13. package/dist/modules/catalog/backend/catalog/prices/page.meta.js.map +7 -0
  14. package/dist/modules/catalog/components/prices/PriceScopeSelectors.js +201 -0
  15. package/dist/modules/catalog/components/prices/PriceScopeSelectors.js.map +7 -0
  16. package/dist/modules/catalog/components/prices/PricesDataTable.js +273 -0
  17. package/dist/modules/catalog/components/prices/PricesDataTable.js.map +7 -0
  18. package/dist/modules/catalog/components/prices/normalizePriceRecord.js +43 -0
  19. package/dist/modules/catalog/components/prices/normalizePriceRecord.js.map +7 -0
  20. package/dist/modules/catalog/components/prices/priceFormFields.js +195 -0
  21. package/dist/modules/catalog/components/prices/priceFormFields.js.map +7 -0
  22. package/dist/modules/catalog/components/prices/useBatchLabels.js +42 -0
  23. package/dist/modules/catalog/components/prices/useBatchLabels.js.map +7 -0
  24. package/dist/modules/catalog/data/validators.js +21 -2
  25. package/dist/modules/catalog/data/validators.js.map +2 -2
  26. package/dist/modules/catalog/extension-points.js +2 -1
  27. package/dist/modules/catalog/extension-points.js.map +2 -2
  28. package/dist/modules/catalog/lib/pricing.js +37 -8
  29. package/dist/modules/catalog/lib/pricing.js.map +2 -2
  30. package/dist/modules/communication_channels/api/post/channels/[id]/import-history/route.js +5 -1
  31. package/dist/modules/communication_channels/api/post/channels/[id]/import-history/route.js.map +2 -2
  32. package/dist/modules/communication_channels/backend/profile/communication-channels/ConnectChannelMenu.js +88 -0
  33. package/dist/modules/communication_channels/backend/profile/communication-channels/ConnectChannelMenu.js.map +7 -0
  34. package/dist/modules/communication_channels/backend/profile/communication-channels/page.js +17 -19
  35. package/dist/modules/communication_channels/backend/profile/communication-channels/page.js.map +2 -2
  36. package/dist/modules/communication_channels/commands/connect-credential-channel.js +7 -2
  37. package/dist/modules/communication_channels/commands/connect-credential-channel.js.map +2 -2
  38. package/dist/modules/communication_channels/commands/ingest-inbound-message.js +11 -2
  39. package/dist/modules/communication_channels/commands/ingest-inbound-message.js.map +2 -2
  40. package/dist/modules/communication_channels/commands/queue-import-history.js +40 -7
  41. package/dist/modules/communication_channels/commands/queue-import-history.js.map +2 -2
  42. package/dist/modules/communication_channels/extension-points.js +1 -1
  43. package/dist/modules/communication_channels/extension-points.js.map +1 -1
  44. package/dist/modules/communication_channels/lib/connect-channel.js +33 -0
  45. package/dist/modules/communication_channels/lib/connect-channel.js.map +2 -2
  46. package/dist/modules/communication_channels/lib/import-history-limits.js +69 -0
  47. package/dist/modules/communication_channels/lib/import-history-limits.js.map +7 -0
  48. package/dist/modules/communication_channels/workers/channel-import-history.js +36 -2
  49. package/dist/modules/communication_channels/workers/channel-import-history.js.map +2 -2
  50. package/dist/modules/messages/api/route.js +5 -1
  51. package/dist/modules/messages/api/route.js.map +2 -2
  52. package/dist/modules/messages/commands/messages.js +3 -2
  53. package/dist/modules/messages/commands/messages.js.map +2 -2
  54. package/dist/modules/messages/data/validators.js +12 -2
  55. package/dist/modules/messages/data/validators.js.map +2 -2
  56. package/dist/modules/messages/subscribers/message-notification.js +4 -1
  57. package/dist/modules/messages/subscribers/message-notification.js.map +2 -2
  58. package/dist/modules/query_index/lib/engine.js +19 -1
  59. package/dist/modules/query_index/lib/engine.js.map +2 -2
  60. package/package.json +7 -7
  61. package/src/modules/catalog/AGENTS.md +6 -2
  62. package/src/modules/catalog/backend/catalog/prices/[id]/edit/page.meta.ts +12 -0
  63. package/src/modules/catalog/backend/catalog/prices/[id]/edit/page.tsx +198 -0
  64. package/src/modules/catalog/backend/catalog/prices/create/page.meta.ts +23 -0
  65. package/src/modules/catalog/backend/catalog/prices/create/page.tsx +70 -0
  66. package/src/modules/catalog/backend/catalog/prices/page.meta.ts +12 -0
  67. package/src/modules/catalog/backend/catalog/prices/page.tsx +14 -0
  68. package/src/modules/catalog/components/prices/PriceScopeSelectors.tsx +243 -0
  69. package/src/modules/catalog/components/prices/PricesDataTable.tsx +320 -0
  70. package/src/modules/catalog/components/prices/normalizePriceRecord.ts +71 -0
  71. package/src/modules/catalog/components/prices/priceFormFields.tsx +228 -0
  72. package/src/modules/catalog/components/prices/useBatchLabels.ts +57 -0
  73. package/src/modules/catalog/data/validators.ts +43 -2
  74. package/src/modules/catalog/extension-points.ts +1 -0
  75. package/src/modules/catalog/i18n/de.json +72 -0
  76. package/src/modules/catalog/i18n/en.json +72 -0
  77. package/src/modules/catalog/i18n/es.json +72 -0
  78. package/src/modules/catalog/i18n/ko.json +72 -0
  79. package/src/modules/catalog/i18n/pl.json +72 -0
  80. package/src/modules/catalog/lib/pricing.ts +103 -9
  81. package/src/modules/communication_channels/api/post/channels/[id]/import-history/route.ts +6 -1
  82. package/src/modules/communication_channels/backend/profile/communication-channels/ConnectChannelMenu.tsx +120 -0
  83. package/src/modules/communication_channels/backend/profile/communication-channels/page.tsx +20 -17
  84. package/src/modules/communication_channels/commands/connect-credential-channel.ts +9 -0
  85. package/src/modules/communication_channels/commands/ingest-inbound-message.ts +17 -2
  86. package/src/modules/communication_channels/commands/queue-import-history.ts +59 -11
  87. package/src/modules/communication_channels/extension-points.ts +1 -1
  88. package/src/modules/communication_channels/i18n/de.json +4 -3
  89. package/src/modules/communication_channels/i18n/en.json +4 -3
  90. package/src/modules/communication_channels/i18n/es.json +4 -3
  91. package/src/modules/communication_channels/i18n/ko.json +4 -3
  92. package/src/modules/communication_channels/i18n/pl.json +4 -3
  93. package/src/modules/communication_channels/lib/adapter.ts +16 -2
  94. package/src/modules/communication_channels/lib/connect-channel.ts +49 -0
  95. package/src/modules/communication_channels/lib/import-history-limits.ts +109 -0
  96. package/src/modules/communication_channels/workers/channel-import-history.ts +46 -5
  97. package/src/modules/messages/api/route.ts +8 -2
  98. package/src/modules/messages/commands/messages.ts +17 -2
  99. package/src/modules/messages/data/validators.ts +26 -11
  100. package/src/modules/messages/subscribers/message-notification.ts +17 -5
  101. package/src/modules/query_index/lib/engine.ts +23 -1
  102. package/src/modules/sales/AGENTS.md +2 -3
@@ -459,7 +459,8 @@ export const priceKindUpdateSchema = z
459
459
  })
460
460
  .merge(priceKindCreateSchema.partial())
461
461
 
462
- export const priceCreateSchema = scoped.extend({
462
+ // Base schema without refinements (used for .partial() in the update schema).
463
+ const priceBaseSchema = scoped.extend({
463
464
  variantId: uuid().optional(),
464
465
  productId: uuid().optional(),
465
466
  offerId: uuid().optional(),
@@ -481,11 +482,51 @@ export const priceCreateSchema = scoped.extend({
481
482
  endsAt: z.coerce.date().optional(),
482
483
  })
483
484
 
485
+ // Both checks only fire when both sides of the comparison are present in this
486
+ // payload — a partial update touching only one side is not re-validated against
487
+ // the stored record here (same limitation the GTIN cross-field check documents
488
+ // above: zod cannot see the merged state, only the command layer can).
489
+ const priceQuantityRangeRefinement = (
490
+ input: { minQuantity?: number; maxQuantity?: number },
491
+ ctx: z.RefinementCtx,
492
+ ) => {
493
+ if (
494
+ input.minQuantity != null &&
495
+ input.maxQuantity != null &&
496
+ input.maxQuantity < input.minQuantity
497
+ ) {
498
+ ctx.addIssue({
499
+ code: z.ZodIssueCode.custom,
500
+ path: ['maxQuantity'],
501
+ message: 'catalog.prices.validation.quantityRange',
502
+ })
503
+ }
504
+ }
505
+
506
+ const priceValidityWindowRefinement = (
507
+ input: { startsAt?: Date; endsAt?: Date },
508
+ ctx: z.RefinementCtx,
509
+ ) => {
510
+ if (input.startsAt && input.endsAt && input.endsAt < input.startsAt) {
511
+ ctx.addIssue({
512
+ code: z.ZodIssueCode.custom,
513
+ path: ['endsAt'],
514
+ message: 'catalog.prices.validation.validityWindow',
515
+ })
516
+ }
517
+ }
518
+
519
+ export const priceCreateSchema = priceBaseSchema
520
+ .superRefine(priceQuantityRangeRefinement)
521
+ .superRefine(priceValidityWindowRefinement)
522
+
484
523
  export const priceUpdateSchema = z
485
524
  .object({
486
525
  id: uuid(),
487
526
  })
488
- .merge(priceCreateSchema.partial())
527
+ .merge(priceBaseSchema.partial())
528
+ .superRefine(priceQuantityRangeRefinement)
529
+ .superRefine(priceValidityWindowRefinement)
489
530
 
490
531
  export const categoryCreateSchema = scoped.extend({
491
532
  name: z.string().trim().min(1).max(255),
@@ -18,6 +18,7 @@ export const extensionPoints = defineModuleExtensionPoints({
18
18
  source: 'components/products/ProductsDataTable.tsx',
19
19
  }),
20
20
  categoriesTable: dataTableExtensionHost({ tableId: 'catalog.categories.list', source: 'components/categories/CategoriesDataTable.tsx' }),
21
+ pricesTable: dataTableExtensionHost({ tableId: 'catalog.prices.list', source: 'components/prices/PricesDataTable.tsx' }),
21
22
  },
22
23
  })
23
24
 
@@ -186,6 +186,78 @@
186
186
  "catalog.priceKinds.table.promotionYes": "Ja",
187
187
  "catalog.priceKinds.table.title": "Titel",
188
188
  "catalog.priceKinds.title": "Preisarten",
189
+ "catalog.prices.flash.created": "Preisregel erstellt",
190
+ "catalog.prices.flash.deleted": "Preisregel gelöscht",
191
+ "catalog.prices.flash.updated": "Preisregel aktualisiert",
192
+ "catalog.prices.form.action.save": "Speichern",
193
+ "catalog.prices.form.actions.backToList": "Zurück zu den Preisregeln",
194
+ "catalog.prices.form.createTitle": "Preisregel erstellen",
195
+ "catalog.prices.form.editTitle": "Preisregel bearbeiten",
196
+ "catalog.prices.form.errors.delete": "Preisregel konnte nicht gelöscht werden",
197
+ "catalog.prices.form.errors.idRequired": "Die Kennung der Preisregel ist erforderlich.",
198
+ "catalog.prices.form.errors.load": "Preisregel konnte nicht geladen werden",
199
+ "catalog.prices.form.errors.notFound": "Preisregel nicht gefunden",
200
+ "catalog.prices.form.errors.productOrVariantRequired": "Wählen Sie ein Produkt oder eine Variante.",
201
+ "catalog.prices.form.field.channel": "Kanal",
202
+ "catalog.prices.form.field.currency": "Währung",
203
+ "catalog.prices.form.field.customer": "Kunde",
204
+ "catalog.prices.form.field.customerGroupId": "Kundengruppen-ID",
205
+ "catalog.prices.form.field.customerGroupIdHint": "Rohe Kundengruppen-Kennung — noch kein Kundengruppenverzeichnis.",
206
+ "catalog.prices.form.field.endsAt": "Endet am",
207
+ "catalog.prices.form.field.maxQuantity": "Maximale Menge",
208
+ "catalog.prices.form.field.maxQuantityHint": "Leer lassen für „und mehr“",
209
+ "catalog.prices.form.field.metadata": "Metadaten",
210
+ "catalog.prices.form.field.minQuantity": "Mindestmenge",
211
+ "catalog.prices.form.field.offer": "Angebot",
212
+ "catalog.prices.form.field.priceKind": "Preisart",
213
+ "catalog.prices.form.field.product": "Produkt",
214
+ "catalog.prices.form.field.startsAt": "Beginnt am",
215
+ "catalog.prices.form.field.taxRate": "Steuersatz",
216
+ "catalog.prices.form.field.unitPriceGross": "Stückpreis (brutto)",
217
+ "catalog.prices.form.field.unitPriceNet": "Stückpreis (netto)",
218
+ "catalog.prices.form.field.userGroupId": "Benutzergruppen-ID",
219
+ "catalog.prices.form.field.userId": "Benutzer-ID",
220
+ "catalog.prices.form.field.variant": "Variante",
221
+ "catalog.prices.form.group.advanced": "Erweiterter Geltungsbereich",
222
+ "catalog.prices.form.group.pricing": "Preisgestaltung",
223
+ "catalog.prices.form.group.product": "Produkt",
224
+ "catalog.prices.form.group.quantity": "Mengenstaffel",
225
+ "catalog.prices.form.group.scope": "Geltungsbereich",
226
+ "catalog.prices.form.group.validity": "Gültigkeitszeitraum",
227
+ "catalog.prices.form.loading": "Preisregel wird geladen...",
228
+ "catalog.prices.list.actions.create": "Erstellen",
229
+ "catalog.prices.list.actions.delete": "Löschen",
230
+ "catalog.prices.list.actions.edit": "Bearbeiten",
231
+ "catalog.prices.list.badge.expired": "Abgelaufen",
232
+ "catalog.prices.list.columns.amount": "Betrag",
233
+ "catalog.prices.list.columns.kind": "Art",
234
+ "catalog.prices.list.columns.product": "Produkt",
235
+ "catalog.prices.list.columns.quantity": "Menge",
236
+ "catalog.prices.list.columns.scope": "Geltungsbereich",
237
+ "catalog.prices.list.columns.validity": "Gültigkeit",
238
+ "catalog.prices.list.confirmDelete": "Diese Preisregel löschen?",
239
+ "catalog.prices.list.empty": "Noch keine Preisregeln — fügen Sie eine hinzu, um dieses Produkt jenseits des Grundpreises zu bepreisen.",
240
+ "catalog.prices.list.error.delete": "Preisregel konnte nicht gelöscht werden",
241
+ "catalog.prices.list.error.load": "Preisregeln konnten nicht geladen werden",
242
+ "catalog.prices.list.quantity.min": "{{min}}+",
243
+ "catalog.prices.list.quantity.range": "{{min}}–{{max}}",
244
+ "catalog.prices.list.scope.channel": "Kanal",
245
+ "catalog.prices.list.scope.customer": "Kunde",
246
+ "catalog.prices.list.scope.customerGroup": "Kundengruppe",
247
+ "catalog.prices.list.searchPlaceholder": "Preisregeln suchen",
248
+ "catalog.prices.list.title": "Preisregeln",
249
+ "catalog.prices.list.validity.always": "Immer",
250
+ "catalog.prices.list.validity.from": "Ab {{date}}",
251
+ "catalog.prices.list.validity.range": "{{from}} – {{until}}",
252
+ "catalog.prices.list.validity.until": "Bis {{date}}",
253
+ "catalog.prices.page.title": "Preisregeln",
254
+ "catalog.prices.select.channel.placeholder": "Kanal auswählen…",
255
+ "catalog.prices.select.currency.placeholder": "Währung auswählen…",
256
+ "catalog.prices.select.customer.placeholder": "Kunden suchen…",
257
+ "catalog.prices.select.priceKind.placeholder": "Preisart auswählen…",
258
+ "catalog.prices.select.product.placeholder": "Produkte suchen…",
259
+ "catalog.prices.validation.quantityRange": "Die maximale Menge muss größer oder gleich der Mindestmenge sein.",
260
+ "catalog.prices.validation.validityWindow": "Das Enddatum muss nach dem Startdatum liegen.",
189
261
  "catalog.pricing.list.title": "Preislisten",
190
262
  "catalog.products.actions.create": "Erstellen",
191
263
  "catalog.products.actions.refresh": "Aktualisieren",
@@ -186,6 +186,78 @@
186
186
  "catalog.priceKinds.table.promotionYes": "Yes",
187
187
  "catalog.priceKinds.table.title": "Title",
188
188
  "catalog.priceKinds.title": "Price kinds",
189
+ "catalog.prices.flash.created": "Price rule created",
190
+ "catalog.prices.flash.deleted": "Price rule deleted",
191
+ "catalog.prices.flash.updated": "Price rule updated",
192
+ "catalog.prices.form.action.save": "Save",
193
+ "catalog.prices.form.actions.backToList": "Back to price rules",
194
+ "catalog.prices.form.createTitle": "Create price rule",
195
+ "catalog.prices.form.editTitle": "Edit price rule",
196
+ "catalog.prices.form.errors.delete": "Failed to delete price rule",
197
+ "catalog.prices.form.errors.idRequired": "Price rule identifier is required.",
198
+ "catalog.prices.form.errors.load": "Failed to load price rule",
199
+ "catalog.prices.form.errors.notFound": "Price rule not found",
200
+ "catalog.prices.form.errors.productOrVariantRequired": "Choose a product or a variant.",
201
+ "catalog.prices.form.field.channel": "Channel",
202
+ "catalog.prices.form.field.currency": "Currency",
203
+ "catalog.prices.form.field.customer": "Customer",
204
+ "catalog.prices.form.field.customerGroupId": "Customer group ID",
205
+ "catalog.prices.form.field.customerGroupIdHint": "Raw customer-group identifier — no customer-group directory yet.",
206
+ "catalog.prices.form.field.endsAt": "Ends at",
207
+ "catalog.prices.form.field.maxQuantity": "Maximum quantity",
208
+ "catalog.prices.form.field.maxQuantityHint": "Leave empty for “and above”",
209
+ "catalog.prices.form.field.metadata": "Metadata",
210
+ "catalog.prices.form.field.minQuantity": "Minimum quantity",
211
+ "catalog.prices.form.field.offer": "Offer",
212
+ "catalog.prices.form.field.priceKind": "Price kind",
213
+ "catalog.prices.form.field.product": "Product",
214
+ "catalog.prices.form.field.startsAt": "Starts at",
215
+ "catalog.prices.form.field.taxRate": "Tax rate",
216
+ "catalog.prices.form.field.unitPriceGross": "Unit price (gross)",
217
+ "catalog.prices.form.field.unitPriceNet": "Unit price (net)",
218
+ "catalog.prices.form.field.userGroupId": "User group ID",
219
+ "catalog.prices.form.field.userId": "User ID",
220
+ "catalog.prices.form.field.variant": "Variant",
221
+ "catalog.prices.form.group.advanced": "Advanced scope",
222
+ "catalog.prices.form.group.pricing": "Pricing",
223
+ "catalog.prices.form.group.product": "Product",
224
+ "catalog.prices.form.group.quantity": "Quantity tier",
225
+ "catalog.prices.form.group.scope": "Scope",
226
+ "catalog.prices.form.group.validity": "Validity window",
227
+ "catalog.prices.form.loading": "Loading price rule...",
228
+ "catalog.prices.list.actions.create": "Create",
229
+ "catalog.prices.list.actions.delete": "Delete",
230
+ "catalog.prices.list.actions.edit": "Edit",
231
+ "catalog.prices.list.badge.expired": "Expired",
232
+ "catalog.prices.list.columns.amount": "Amount",
233
+ "catalog.prices.list.columns.kind": "Kind",
234
+ "catalog.prices.list.columns.product": "Product",
235
+ "catalog.prices.list.columns.quantity": "Quantity",
236
+ "catalog.prices.list.columns.scope": "Scope",
237
+ "catalog.prices.list.columns.validity": "Validity",
238
+ "catalog.prices.list.confirmDelete": "Delete this price rule?",
239
+ "catalog.prices.list.empty": "No price rules yet — add one to price this product beyond its base price.",
240
+ "catalog.prices.list.error.delete": "Failed to delete price rule",
241
+ "catalog.prices.list.error.load": "Failed to load price rules",
242
+ "catalog.prices.list.quantity.min": "{{min}}+",
243
+ "catalog.prices.list.quantity.range": "{{min}}–{{max}}",
244
+ "catalog.prices.list.scope.channel": "Channel",
245
+ "catalog.prices.list.scope.customer": "Customer",
246
+ "catalog.prices.list.scope.customerGroup": "Customer group",
247
+ "catalog.prices.list.searchPlaceholder": "Search price rules",
248
+ "catalog.prices.list.title": "Price rules",
249
+ "catalog.prices.list.validity.always": "Always",
250
+ "catalog.prices.list.validity.from": "From {{date}}",
251
+ "catalog.prices.list.validity.range": "{{from}} – {{until}}",
252
+ "catalog.prices.list.validity.until": "Until {{date}}",
253
+ "catalog.prices.page.title": "Price rules",
254
+ "catalog.prices.select.channel.placeholder": "Select a channel…",
255
+ "catalog.prices.select.currency.placeholder": "Select a currency…",
256
+ "catalog.prices.select.customer.placeholder": "Search customers…",
257
+ "catalog.prices.select.priceKind.placeholder": "Select a price kind…",
258
+ "catalog.prices.select.product.placeholder": "Search products…",
259
+ "catalog.prices.validation.quantityRange": "Maximum quantity must be greater than or equal to the minimum quantity.",
260
+ "catalog.prices.validation.validityWindow": "End date must be after the start date.",
189
261
  "catalog.pricing.list.title": "Price Lists",
190
262
  "catalog.products.actions.create": "Create",
191
263
  "catalog.products.actions.refresh": "Refresh",
@@ -186,6 +186,78 @@
186
186
  "catalog.priceKinds.table.promotionYes": "Sí",
187
187
  "catalog.priceKinds.table.title": "Título",
188
188
  "catalog.priceKinds.title": "Tipos de precio",
189
+ "catalog.prices.flash.created": "Regla de precio creada",
190
+ "catalog.prices.flash.deleted": "Regla de precio eliminada",
191
+ "catalog.prices.flash.updated": "Regla de precio actualizada",
192
+ "catalog.prices.form.action.save": "Guardar",
193
+ "catalog.prices.form.actions.backToList": "Volver a las reglas de precio",
194
+ "catalog.prices.form.createTitle": "Crear regla de precio",
195
+ "catalog.prices.form.editTitle": "Editar regla de precio",
196
+ "catalog.prices.form.errors.delete": "No se pudo eliminar la regla de precio",
197
+ "catalog.prices.form.errors.idRequired": "Se requiere el identificador de la regla de precio.",
198
+ "catalog.prices.form.errors.load": "No se pudo cargar la regla de precio",
199
+ "catalog.prices.form.errors.notFound": "Regla de precio no encontrada",
200
+ "catalog.prices.form.errors.productOrVariantRequired": "Elija un producto o una variante.",
201
+ "catalog.prices.form.field.channel": "Canal",
202
+ "catalog.prices.form.field.currency": "Moneda",
203
+ "catalog.prices.form.field.customer": "Cliente",
204
+ "catalog.prices.form.field.customerGroupId": "ID de grupo de clientes",
205
+ "catalog.prices.form.field.customerGroupIdHint": "Identificador sin resolver del grupo de clientes — todavía no hay un directorio de grupos de clientes.",
206
+ "catalog.prices.form.field.endsAt": "Finaliza el",
207
+ "catalog.prices.form.field.maxQuantity": "Cantidad máxima",
208
+ "catalog.prices.form.field.maxQuantityHint": "Deje vacío para «y más»",
209
+ "catalog.prices.form.field.metadata": "Metadatos",
210
+ "catalog.prices.form.field.minQuantity": "Cantidad mínima",
211
+ "catalog.prices.form.field.offer": "Oferta",
212
+ "catalog.prices.form.field.priceKind": "Tipo de precio",
213
+ "catalog.prices.form.field.product": "Producto",
214
+ "catalog.prices.form.field.startsAt": "Comienza el",
215
+ "catalog.prices.form.field.taxRate": "Tasa de impuesto",
216
+ "catalog.prices.form.field.unitPriceGross": "Precio unitario (bruto)",
217
+ "catalog.prices.form.field.unitPriceNet": "Precio unitario (neto)",
218
+ "catalog.prices.form.field.userGroupId": "ID de grupo de usuarios",
219
+ "catalog.prices.form.field.userId": "ID de usuario",
220
+ "catalog.prices.form.field.variant": "Variante",
221
+ "catalog.prices.form.group.advanced": "Alcance avanzado",
222
+ "catalog.prices.form.group.pricing": "Precios",
223
+ "catalog.prices.form.group.product": "Producto",
224
+ "catalog.prices.form.group.quantity": "Escalado por cantidad",
225
+ "catalog.prices.form.group.scope": "Alcance",
226
+ "catalog.prices.form.group.validity": "Periodo de validez",
227
+ "catalog.prices.form.loading": "Cargando regla de precio...",
228
+ "catalog.prices.list.actions.create": "Crear",
229
+ "catalog.prices.list.actions.delete": "Eliminar",
230
+ "catalog.prices.list.actions.edit": "Editar",
231
+ "catalog.prices.list.badge.expired": "Caducada",
232
+ "catalog.prices.list.columns.amount": "Importe",
233
+ "catalog.prices.list.columns.kind": "Tipo",
234
+ "catalog.prices.list.columns.product": "Producto",
235
+ "catalog.prices.list.columns.quantity": "Cantidad",
236
+ "catalog.prices.list.columns.scope": "Alcance",
237
+ "catalog.prices.list.columns.validity": "Validez",
238
+ "catalog.prices.list.confirmDelete": "¿Eliminar esta regla de precio?",
239
+ "catalog.prices.list.empty": "Aún no hay reglas de precio — agregue una para fijar el precio de este producto más allá de su precio base.",
240
+ "catalog.prices.list.error.delete": "No se pudo eliminar la regla de precio",
241
+ "catalog.prices.list.error.load": "No se pudieron cargar las reglas de precio",
242
+ "catalog.prices.list.quantity.min": "{{min}}+",
243
+ "catalog.prices.list.quantity.range": "{{min}}–{{max}}",
244
+ "catalog.prices.list.scope.channel": "Canal",
245
+ "catalog.prices.list.scope.customer": "Cliente",
246
+ "catalog.prices.list.scope.customerGroup": "Grupo de clientes",
247
+ "catalog.prices.list.searchPlaceholder": "Buscar reglas de precio",
248
+ "catalog.prices.list.title": "Reglas de precio",
249
+ "catalog.prices.list.validity.always": "Siempre",
250
+ "catalog.prices.list.validity.from": "Desde {{date}}",
251
+ "catalog.prices.list.validity.range": "{{from}} – {{until}}",
252
+ "catalog.prices.list.validity.until": "Hasta {{date}}",
253
+ "catalog.prices.page.title": "Reglas de precio",
254
+ "catalog.prices.select.channel.placeholder": "Seleccionar un canal…",
255
+ "catalog.prices.select.currency.placeholder": "Seleccionar una moneda…",
256
+ "catalog.prices.select.customer.placeholder": "Buscar clientes…",
257
+ "catalog.prices.select.priceKind.placeholder": "Seleccionar un tipo de precio…",
258
+ "catalog.prices.select.product.placeholder": "Buscar productos…",
259
+ "catalog.prices.validation.quantityRange": "La cantidad máxima debe ser mayor o igual que la cantidad mínima.",
260
+ "catalog.prices.validation.validityWindow": "La fecha de finalización debe ser posterior a la fecha de inicio.",
189
261
  "catalog.pricing.list.title": "Listas de precios",
190
262
  "catalog.products.actions.create": "Crear",
191
263
  "catalog.products.actions.refresh": "Actualizar",
@@ -186,6 +186,78 @@
186
186
  "catalog.priceKinds.table.promotionYes": "예",
187
187
  "catalog.priceKinds.table.title": "제목",
188
188
  "catalog.priceKinds.title": "가격 종류",
189
+ "catalog.prices.flash.created": "가격 규칙이 생성되었습니다",
190
+ "catalog.prices.flash.deleted": "가격 규칙이 삭제되었습니다",
191
+ "catalog.prices.flash.updated": "가격 규칙이 업데이트되었습니다",
192
+ "catalog.prices.form.action.save": "저장",
193
+ "catalog.prices.form.actions.backToList": "가격 규칙 목록으로 돌아가기",
194
+ "catalog.prices.form.createTitle": "가격 규칙 생성",
195
+ "catalog.prices.form.editTitle": "가격 규칙 수정",
196
+ "catalog.prices.form.errors.delete": "가격 규칙을 삭제하지 못했습니다",
197
+ "catalog.prices.form.errors.idRequired": "가격 규칙 식별자가 필요합니다.",
198
+ "catalog.prices.form.errors.load": "가격 규칙을 불러오지 못했습니다",
199
+ "catalog.prices.form.errors.notFound": "가격 규칙을 찾을 수 없습니다",
200
+ "catalog.prices.form.errors.productOrVariantRequired": "제품 또는 변형을 선택하세요.",
201
+ "catalog.prices.form.field.channel": "채널",
202
+ "catalog.prices.form.field.currency": "통화",
203
+ "catalog.prices.form.field.customer": "고객",
204
+ "catalog.prices.form.field.customerGroupId": "고객 그룹 ID",
205
+ "catalog.prices.form.field.customerGroupIdHint": "원본 고객 그룹 식별자입니다 — 아직 고객 그룹 디렉토리가 없습니다.",
206
+ "catalog.prices.form.field.endsAt": "종료일",
207
+ "catalog.prices.form.field.maxQuantity": "최대 수량",
208
+ "catalog.prices.form.field.maxQuantityHint": "이상을 표시하려면 비워두세요",
209
+ "catalog.prices.form.field.metadata": "메타데이터",
210
+ "catalog.prices.form.field.minQuantity": "최소 수량",
211
+ "catalog.prices.form.field.offer": "오퍼",
212
+ "catalog.prices.form.field.priceKind": "가격 종류",
213
+ "catalog.prices.form.field.product": "제품",
214
+ "catalog.prices.form.field.startsAt": "시작일",
215
+ "catalog.prices.form.field.taxRate": "세율",
216
+ "catalog.prices.form.field.unitPriceGross": "단가 (세금 포함)",
217
+ "catalog.prices.form.field.unitPriceNet": "단가 (세금 제외)",
218
+ "catalog.prices.form.field.userGroupId": "사용자 그룹 ID",
219
+ "catalog.prices.form.field.userId": "사용자 ID",
220
+ "catalog.prices.form.field.variant": "변형",
221
+ "catalog.prices.form.group.advanced": "고급 범위",
222
+ "catalog.prices.form.group.pricing": "가격 책정",
223
+ "catalog.prices.form.group.product": "제품",
224
+ "catalog.prices.form.group.quantity": "수량 단계",
225
+ "catalog.prices.form.group.scope": "범위",
226
+ "catalog.prices.form.group.validity": "유효 기간",
227
+ "catalog.prices.form.loading": "가격 규칙을 불러오는 중...",
228
+ "catalog.prices.list.actions.create": "생성",
229
+ "catalog.prices.list.actions.delete": "삭제",
230
+ "catalog.prices.list.actions.edit": "수정",
231
+ "catalog.prices.list.badge.expired": "만료됨",
232
+ "catalog.prices.list.columns.amount": "금액",
233
+ "catalog.prices.list.columns.kind": "종류",
234
+ "catalog.prices.list.columns.product": "제품",
235
+ "catalog.prices.list.columns.quantity": "수량",
236
+ "catalog.prices.list.columns.scope": "범위",
237
+ "catalog.prices.list.columns.validity": "유효기간",
238
+ "catalog.prices.list.confirmDelete": "이 가격 규칙을 삭제하시겠습니까?",
239
+ "catalog.prices.list.empty": "아직 가격 규칙이 없습니다 — 기본 가격 외에 이 제품의 가격을 책정하려면 하나를 추가하세요.",
240
+ "catalog.prices.list.error.delete": "가격 규칙을 삭제하지 못했습니다",
241
+ "catalog.prices.list.error.load": "가격 규칙을 불러오지 못했습니다",
242
+ "catalog.prices.list.quantity.min": "{{min}}+",
243
+ "catalog.prices.list.quantity.range": "{{min}}–{{max}}",
244
+ "catalog.prices.list.scope.channel": "채널",
245
+ "catalog.prices.list.scope.customer": "고객",
246
+ "catalog.prices.list.scope.customerGroup": "고객 그룹",
247
+ "catalog.prices.list.searchPlaceholder": "가격 규칙 검색",
248
+ "catalog.prices.list.title": "가격 규칙",
249
+ "catalog.prices.list.validity.always": "항상",
250
+ "catalog.prices.list.validity.from": "{{date}}부터",
251
+ "catalog.prices.list.validity.range": "{{from}} – {{until}}",
252
+ "catalog.prices.list.validity.until": "{{date}}까지",
253
+ "catalog.prices.page.title": "가격 규칙",
254
+ "catalog.prices.select.channel.placeholder": "채널 선택…",
255
+ "catalog.prices.select.currency.placeholder": "통화 선택…",
256
+ "catalog.prices.select.customer.placeholder": "고객 검색…",
257
+ "catalog.prices.select.priceKind.placeholder": "가격 종류 선택…",
258
+ "catalog.prices.select.product.placeholder": "제품 검색…",
259
+ "catalog.prices.validation.quantityRange": "최대 수량은 최소 수량보다 크거나 같아야 합니다.",
260
+ "catalog.prices.validation.validityWindow": "종료일은 시작일보다 이후여야 합니다.",
189
261
  "catalog.pricing.list.title": "가격표",
190
262
  "catalog.products.actions.create": "생성",
191
263
  "catalog.products.actions.refresh": "새로고침",
@@ -186,6 +186,78 @@
186
186
  "catalog.priceKinds.table.promotionYes": "Tak",
187
187
  "catalog.priceKinds.table.title": "Tytuł",
188
188
  "catalog.priceKinds.title": "Rodzaje cen",
189
+ "catalog.prices.flash.created": "Utworzono regułę cenową",
190
+ "catalog.prices.flash.deleted": "Usunięto regułę cenową",
191
+ "catalog.prices.flash.updated": "Zaktualizowano regułę cenową",
192
+ "catalog.prices.form.action.save": "Zapisz",
193
+ "catalog.prices.form.actions.backToList": "Wróć do reguł cenowych",
194
+ "catalog.prices.form.createTitle": "Utwórz regułę cenową",
195
+ "catalog.prices.form.editTitle": "Edytuj regułę cenową",
196
+ "catalog.prices.form.errors.delete": "Nie udało się usunąć reguły cenowej",
197
+ "catalog.prices.form.errors.idRequired": "Identyfikator reguły cenowej jest wymagany.",
198
+ "catalog.prices.form.errors.load": "Nie udało się wczytać reguły cenowej",
199
+ "catalog.prices.form.errors.notFound": "Nie znaleziono reguły cenowej",
200
+ "catalog.prices.form.errors.productOrVariantRequired": "Wybierz produkt lub wariant.",
201
+ "catalog.prices.form.field.channel": "Kanał",
202
+ "catalog.prices.form.field.currency": "Waluta",
203
+ "catalog.prices.form.field.customer": "Klient",
204
+ "catalog.prices.form.field.customerGroupId": "ID grupy klientów",
205
+ "catalog.prices.form.field.customerGroupIdHint": "Surowy identyfikator grupy klientów — katalog grup klientów jeszcze nie istnieje.",
206
+ "catalog.prices.form.field.endsAt": "Kończy się",
207
+ "catalog.prices.form.field.maxQuantity": "Maksymalna ilość",
208
+ "catalog.prices.form.field.maxQuantityHint": "Pozostaw puste dla „i więcej”",
209
+ "catalog.prices.form.field.metadata": "Metadane",
210
+ "catalog.prices.form.field.minQuantity": "Minimalna ilość",
211
+ "catalog.prices.form.field.offer": "Oferta",
212
+ "catalog.prices.form.field.priceKind": "Rodzaj ceny",
213
+ "catalog.prices.form.field.product": "Produkt",
214
+ "catalog.prices.form.field.startsAt": "Zaczyna się",
215
+ "catalog.prices.form.field.taxRate": "Stawka podatku",
216
+ "catalog.prices.form.field.unitPriceGross": "Cena jednostkowa (brutto)",
217
+ "catalog.prices.form.field.unitPriceNet": "Cena jednostkowa (netto)",
218
+ "catalog.prices.form.field.userGroupId": "ID grupy użytkowników",
219
+ "catalog.prices.form.field.userId": "ID użytkownika",
220
+ "catalog.prices.form.field.variant": "Wariant",
221
+ "catalog.prices.form.group.advanced": "Zaawansowany zakres",
222
+ "catalog.prices.form.group.pricing": "Cennik",
223
+ "catalog.prices.form.group.product": "Produkt",
224
+ "catalog.prices.form.group.quantity": "Próg ilościowy",
225
+ "catalog.prices.form.group.scope": "Zakres",
226
+ "catalog.prices.form.group.validity": "Okres ważności",
227
+ "catalog.prices.form.loading": "Wczytywanie reguły cenowej...",
228
+ "catalog.prices.list.actions.create": "Utwórz",
229
+ "catalog.prices.list.actions.delete": "Usuń",
230
+ "catalog.prices.list.actions.edit": "Edytuj",
231
+ "catalog.prices.list.badge.expired": "Wygasła",
232
+ "catalog.prices.list.columns.amount": "Kwota",
233
+ "catalog.prices.list.columns.kind": "Rodzaj",
234
+ "catalog.prices.list.columns.product": "Produkt",
235
+ "catalog.prices.list.columns.quantity": "Ilość",
236
+ "catalog.prices.list.columns.scope": "Zakres",
237
+ "catalog.prices.list.columns.validity": "Ważność",
238
+ "catalog.prices.list.confirmDelete": "Usunąć tę regułę cenową?",
239
+ "catalog.prices.list.empty": "Brak reguł cenowych — dodaj jedną, aby wycenić ten produkt poza ceną bazową.",
240
+ "catalog.prices.list.error.delete": "Nie udało się usunąć reguły cenowej",
241
+ "catalog.prices.list.error.load": "Nie udało się wczytać reguł cenowych",
242
+ "catalog.prices.list.quantity.min": "{{min}}+",
243
+ "catalog.prices.list.quantity.range": "{{min}}–{{max}}",
244
+ "catalog.prices.list.scope.channel": "Kanał",
245
+ "catalog.prices.list.scope.customer": "Klient",
246
+ "catalog.prices.list.scope.customerGroup": "Grupa klientów",
247
+ "catalog.prices.list.searchPlaceholder": "Szukaj reguł cenowych",
248
+ "catalog.prices.list.title": "Reguły cenowe",
249
+ "catalog.prices.list.validity.always": "Zawsze",
250
+ "catalog.prices.list.validity.from": "Od {{date}}",
251
+ "catalog.prices.list.validity.range": "{{from}} – {{until}}",
252
+ "catalog.prices.list.validity.until": "Do {{date}}",
253
+ "catalog.prices.page.title": "Reguły cenowe",
254
+ "catalog.prices.select.channel.placeholder": "Wybierz kanał…",
255
+ "catalog.prices.select.currency.placeholder": "Wybierz walutę…",
256
+ "catalog.prices.select.customer.placeholder": "Szukaj klientów…",
257
+ "catalog.prices.select.priceKind.placeholder": "Wybierz rodzaj ceny…",
258
+ "catalog.prices.select.product.placeholder": "Szukaj produktów…",
259
+ "catalog.prices.validation.quantityRange": "Maksymalna ilość musi być większa lub równa ilości minimalnej.",
260
+ "catalog.prices.validation.validityWindow": "Data zakończenia musi być późniejsza niż data rozpoczęcia.",
189
261
  "catalog.pricing.list.title": "Cenniki",
190
262
  "catalog.products.actions.create": "Utwórz",
191
263
  "catalog.products.actions.refresh": "Odśwież",
@@ -1,4 +1,5 @@
1
1
  import type { EventBus } from '@open-mercato/events'
2
+ import type { FilterQuery } from '@mikro-orm/postgresql'
2
3
  import type {
3
4
  CatalogOffer,
4
5
  CatalogPriceKind,
@@ -13,7 +14,12 @@ export type PricingContext = {
13
14
  userId?: string | null
14
15
  userGroupId?: string | null
15
16
  customerId?: string | null
17
+ /** @deprecated use `customerGroupIds` — kept for backward compatibility, read as a one-element set when `customerGroupIds` is absent. */
16
18
  customerGroupId?: string | null
19
+ /** Set membership: a row's `customerGroupId` matches when it appears in this list. Falls back to `customerGroupId` (above) when omitted. */
20
+ customerGroupIds?: string[]
21
+ /** When set, only rows in this currency match. Omitted (the default): no currency filtering, unchanged legacy behavior. */
22
+ currencyCode?: string | null
17
23
  quantity: number
18
24
  date: Date
19
25
  }
@@ -63,11 +69,64 @@ function matchesContext(row: PriceRow, ctx: PricingContext): boolean {
63
69
  if (row.userId && ctx.userId !== row.userId) return false
64
70
  if (row.userGroupId && ctx.userGroupId !== row.userGroupId) return false
65
71
  if (row.customerId && ctx.customerId !== row.customerId) return false
66
- if (row.customerGroupId && ctx.customerGroupId !== row.customerGroupId) return false
72
+ if (row.customerGroupId) {
73
+ const candidateGroupIds = ctx.customerGroupIds ?? (ctx.customerGroupId ? [ctx.customerGroupId] : [])
74
+ if (!candidateGroupIds.includes(row.customerGroupId)) return false
75
+ }
76
+ if (ctx.currencyCode && row.currencyCode !== ctx.currencyCode) return false
67
77
  if (ctx.offerId && resolvePriceOfferId(row) && resolvePriceOfferId(row) !== ctx.offerId) return false
68
78
  return true
69
79
  }
70
80
 
81
+ /**
82
+ * The narrowing half of `matchesContext` expressed as a MikroORM filter, for
83
+ * callers that fetch `CatalogProductPrice` rows by product/variant today and
84
+ * would otherwise load every contracted customer's rows just to discard
85
+ * almost all of them (per-customer pricing at scale — see
86
+ * `.ai/specs/2026-08-21-pricing-engine.md` § Row narrowing).
87
+ *
88
+ * Covers only the dimensions that are plain column comparisons: customer,
89
+ * customer-group, user, user-group, channel, currency. Quantity bounds,
90
+ * validity windows, and offer-derived channel resolution stay in
91
+ * `matchesContext` — they are cheap over an already-narrowed set and are not
92
+ * expressible as one column predicate (offer's own `channelId` lives on a
93
+ * different table).
94
+ *
95
+ * **Invariant (one-directional, this is the whole point of the helper):**
96
+ * `matchesContext(row, ctx)` implies this filter admits `row`. The filter
97
+ * MAY admit rows `matchesContext` later rejects (a slightly wide fetch) —
98
+ * it MUST NEVER exclude a row `matchesContext` would have accepted, because
99
+ * that is a silently wrong price with no error anywhere. Verified by the
100
+ * property-based test in `lib/__tests__/buildPriceRowFilter.property.test.ts`.
101
+ */
102
+ export function buildPriceRowFilter(ctx: PricingContext): FilterQuery<CatalogProductPrice> {
103
+ const customerGroupIds = ctx.customerGroupIds ?? (ctx.customerGroupId ? [ctx.customerGroupId] : [])
104
+
105
+ const clauses: FilterQuery<CatalogProductPrice>[] = [
106
+ ctx.customerId
107
+ ? { $or: [{ customerId: null }, { customerId: ctx.customerId }] }
108
+ : { customerId: null },
109
+ customerGroupIds.length
110
+ ? { $or: [{ customerGroupId: null }, { customerGroupId: { $in: customerGroupIds } }] }
111
+ : { customerGroupId: null },
112
+ ctx.userId
113
+ ? { $or: [{ userId: null }, { userId: ctx.userId }] }
114
+ : { userId: null },
115
+ ctx.userGroupId
116
+ ? { $or: [{ userGroupId: null }, { userGroupId: ctx.userGroupId }] }
117
+ : { userGroupId: null },
118
+ ctx.channelId
119
+ ? { $or: [{ channelId: null }, { channelId: ctx.channelId }] }
120
+ : { channelId: null },
121
+ ]
122
+
123
+ if (ctx.currencyCode) {
124
+ clauses.push({ currencyCode: ctx.currencyCode })
125
+ }
126
+
127
+ return { $and: clauses } as FilterQuery<CatalogProductPrice>
128
+ }
129
+
71
130
  function scorePrice(row: PriceRow): number {
72
131
  const resolvedKind = resolvePriceKindCode(row)
73
132
  let score = 0
@@ -114,26 +173,61 @@ export type CatalogPricingResolver = (
114
173
  ) => PriceRow | null | undefined | Promise<PriceRow | null | undefined>
115
174
 
116
175
  type RegisteredResolver = {
176
+ id?: string
117
177
  resolver: CatalogPricingResolver
118
178
  priority: number
119
179
  }
120
180
 
121
- const pricingResolvers: RegisteredResolver[] = []
181
+ type PricingRegistryState = {
182
+ resolvers: RegisteredResolver[]
183
+ }
184
+
185
+ // `globalThis`-keyed so the registry survives duplicated module instances
186
+ // (a standalone app built from this monorepo, or any dev/build setup that
187
+ // loads `catalog` through more than one chunk) — the same failure class
188
+ // already fixed once for the ORM entity registry
189
+ // (see `packages/shared/src/modules/integrations/types.ts` for the identical
190
+ // pattern). A module-local array is invisible across instances: a resolver
191
+ // registered from one instance would silently never run for resolution
192
+ // happening in another.
193
+ const GLOBAL_PRICING_REGISTRY_KEY = '__openMercatoCatalogPricingRegistry__' as const
194
+
195
+ type GlobalPricingRegistry = typeof globalThis & {
196
+ [GLOBAL_PRICING_REGISTRY_KEY]?: PricingRegistryState
197
+ }
198
+
199
+ function getPricingRegistryState(): PricingRegistryState {
200
+ const globalRegistry = globalThis as GlobalPricingRegistry
201
+ if (!globalRegistry[GLOBAL_PRICING_REGISTRY_KEY]) {
202
+ globalRegistry[GLOBAL_PRICING_REGISTRY_KEY] = { resolvers: [] }
203
+ }
204
+ return globalRegistry[GLOBAL_PRICING_REGISTRY_KEY]
205
+ }
122
206
 
123
- function sortResolvers(): void {
124
- pricingResolvers.sort((a, b) => b.priority - a.priority)
207
+ function sortResolvers(state: PricingRegistryState): void {
208
+ // `Array.prototype.sort` is stable (ES2019+): resolvers registered at the
209
+ // same priority keep their registration order. This is the documented,
210
+ // tested same-priority tie-break — see `catalog/AGENTS.md` § Price
211
+ // selection order and the registry test in `lib/__tests__/pricing.test.ts`.
212
+ state.resolvers.sort((a, b) => b.priority - a.priority)
125
213
  }
126
214
 
127
215
  export function registerCatalogPricingResolver(
128
216
  resolver: CatalogPricingResolver,
129
- options?: { priority?: number }
217
+ options?: { priority?: number; id?: string }
130
218
  ): void {
131
- pricingResolvers.push({ resolver, priority: options?.priority ?? 0 })
132
- sortResolvers()
219
+ const state = getPricingRegistryState()
220
+ const id = options?.id
221
+ // Dedupe by id so a documented extension point stays HMR-safe for every
222
+ // future registrant, not just one caller: re-registering the same id is a
223
+ // no-op instead of appending a duplicate entry on every module reload.
224
+ if (id && state.resolvers.some((entry) => entry.id === id)) return
225
+ state.resolvers.push({ id, resolver, priority: options?.priority ?? 0 })
226
+ sortResolvers(state)
133
227
  }
134
228
 
135
229
  export function resetCatalogPricingResolvers(): void {
136
- pricingResolvers.splice(0, pricingResolvers.length)
230
+ getPricingRegistryState().resolvers.splice(0)
137
231
  }
138
232
 
139
233
  export async function resolveCatalogPrice(
@@ -163,7 +257,7 @@ export async function resolveCatalogPrice(
163
257
  if (resolved !== undefined) return resolved
164
258
  }
165
259
 
166
- for (const { resolver } of pricingResolvers) {
260
+ for (const { resolver } of getPricingRegistryState().resolvers) {
167
261
  const result = await resolver(workingRows, workingContext)
168
262
  if (result !== undefined) {
169
263
  resolved = result ?? null