@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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/catalog/backend/catalog/prices/[id]/edit/page.js +166 -0
- package/dist/modules/catalog/backend/catalog/prices/[id]/edit/page.js.map +7 -0
- package/dist/modules/catalog/backend/catalog/prices/[id]/edit/page.meta.js +16 -0
- package/dist/modules/catalog/backend/catalog/prices/[id]/edit/page.meta.js.map +7 -0
- package/dist/modules/catalog/backend/catalog/prices/create/page.js +68 -0
- package/dist/modules/catalog/backend/catalog/prices/create/page.js.map +7 -0
- package/dist/modules/catalog/backend/catalog/prices/create/page.meta.js +25 -0
- package/dist/modules/catalog/backend/catalog/prices/create/page.meta.js.map +7 -0
- package/dist/modules/catalog/backend/catalog/prices/page.js +11 -0
- package/dist/modules/catalog/backend/catalog/prices/page.js.map +7 -0
- package/dist/modules/catalog/backend/catalog/prices/page.meta.js +16 -0
- package/dist/modules/catalog/backend/catalog/prices/page.meta.js.map +7 -0
- package/dist/modules/catalog/components/prices/PriceScopeSelectors.js +201 -0
- package/dist/modules/catalog/components/prices/PriceScopeSelectors.js.map +7 -0
- package/dist/modules/catalog/components/prices/PricesDataTable.js +273 -0
- package/dist/modules/catalog/components/prices/PricesDataTable.js.map +7 -0
- package/dist/modules/catalog/components/prices/normalizePriceRecord.js +43 -0
- package/dist/modules/catalog/components/prices/normalizePriceRecord.js.map +7 -0
- package/dist/modules/catalog/components/prices/priceFormFields.js +195 -0
- package/dist/modules/catalog/components/prices/priceFormFields.js.map +7 -0
- package/dist/modules/catalog/components/prices/useBatchLabels.js +42 -0
- package/dist/modules/catalog/components/prices/useBatchLabels.js.map +7 -0
- package/dist/modules/catalog/data/validators.js +21 -2
- package/dist/modules/catalog/data/validators.js.map +2 -2
- package/dist/modules/catalog/extension-points.js +2 -1
- package/dist/modules/catalog/extension-points.js.map +2 -2
- package/dist/modules/catalog/lib/pricing.js +37 -8
- package/dist/modules/catalog/lib/pricing.js.map +2 -2
- package/dist/modules/communication_channels/api/post/channels/[id]/import-history/route.js +5 -1
- package/dist/modules/communication_channels/api/post/channels/[id]/import-history/route.js.map +2 -2
- package/dist/modules/communication_channels/backend/profile/communication-channels/ConnectChannelMenu.js +88 -0
- package/dist/modules/communication_channels/backend/profile/communication-channels/ConnectChannelMenu.js.map +7 -0
- package/dist/modules/communication_channels/backend/profile/communication-channels/page.js +17 -19
- package/dist/modules/communication_channels/backend/profile/communication-channels/page.js.map +2 -2
- package/dist/modules/communication_channels/commands/connect-credential-channel.js +7 -2
- package/dist/modules/communication_channels/commands/connect-credential-channel.js.map +2 -2
- package/dist/modules/communication_channels/commands/ingest-inbound-message.js +11 -2
- package/dist/modules/communication_channels/commands/ingest-inbound-message.js.map +2 -2
- package/dist/modules/communication_channels/commands/queue-import-history.js +40 -7
- package/dist/modules/communication_channels/commands/queue-import-history.js.map +2 -2
- package/dist/modules/communication_channels/extension-points.js +1 -1
- package/dist/modules/communication_channels/extension-points.js.map +1 -1
- package/dist/modules/communication_channels/lib/connect-channel.js +33 -0
- package/dist/modules/communication_channels/lib/connect-channel.js.map +2 -2
- package/dist/modules/communication_channels/lib/import-history-limits.js +69 -0
- package/dist/modules/communication_channels/lib/import-history-limits.js.map +7 -0
- package/dist/modules/communication_channels/workers/channel-import-history.js +36 -2
- package/dist/modules/communication_channels/workers/channel-import-history.js.map +2 -2
- package/dist/modules/messages/api/route.js +5 -1
- package/dist/modules/messages/api/route.js.map +2 -2
- package/dist/modules/messages/commands/messages.js +3 -2
- package/dist/modules/messages/commands/messages.js.map +2 -2
- package/dist/modules/messages/data/validators.js +12 -2
- package/dist/modules/messages/data/validators.js.map +2 -2
- package/dist/modules/messages/subscribers/message-notification.js +4 -1
- package/dist/modules/messages/subscribers/message-notification.js.map +2 -2
- package/dist/modules/query_index/lib/engine.js +19 -1
- package/dist/modules/query_index/lib/engine.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/catalog/AGENTS.md +6 -2
- package/src/modules/catalog/backend/catalog/prices/[id]/edit/page.meta.ts +12 -0
- package/src/modules/catalog/backend/catalog/prices/[id]/edit/page.tsx +198 -0
- package/src/modules/catalog/backend/catalog/prices/create/page.meta.ts +23 -0
- package/src/modules/catalog/backend/catalog/prices/create/page.tsx +70 -0
- package/src/modules/catalog/backend/catalog/prices/page.meta.ts +12 -0
- package/src/modules/catalog/backend/catalog/prices/page.tsx +14 -0
- package/src/modules/catalog/components/prices/PriceScopeSelectors.tsx +243 -0
- package/src/modules/catalog/components/prices/PricesDataTable.tsx +320 -0
- package/src/modules/catalog/components/prices/normalizePriceRecord.ts +71 -0
- package/src/modules/catalog/components/prices/priceFormFields.tsx +228 -0
- package/src/modules/catalog/components/prices/useBatchLabels.ts +57 -0
- package/src/modules/catalog/data/validators.ts +43 -2
- package/src/modules/catalog/extension-points.ts +1 -0
- package/src/modules/catalog/i18n/de.json +72 -0
- package/src/modules/catalog/i18n/en.json +72 -0
- package/src/modules/catalog/i18n/es.json +72 -0
- package/src/modules/catalog/i18n/ko.json +72 -0
- package/src/modules/catalog/i18n/pl.json +72 -0
- package/src/modules/catalog/lib/pricing.ts +103 -9
- package/src/modules/communication_channels/api/post/channels/[id]/import-history/route.ts +6 -1
- package/src/modules/communication_channels/backend/profile/communication-channels/ConnectChannelMenu.tsx +120 -0
- package/src/modules/communication_channels/backend/profile/communication-channels/page.tsx +20 -17
- package/src/modules/communication_channels/commands/connect-credential-channel.ts +9 -0
- package/src/modules/communication_channels/commands/ingest-inbound-message.ts +17 -2
- package/src/modules/communication_channels/commands/queue-import-history.ts +59 -11
- package/src/modules/communication_channels/extension-points.ts +1 -1
- package/src/modules/communication_channels/i18n/de.json +4 -3
- package/src/modules/communication_channels/i18n/en.json +4 -3
- package/src/modules/communication_channels/i18n/es.json +4 -3
- package/src/modules/communication_channels/i18n/ko.json +4 -3
- package/src/modules/communication_channels/i18n/pl.json +4 -3
- package/src/modules/communication_channels/lib/adapter.ts +16 -2
- package/src/modules/communication_channels/lib/connect-channel.ts +49 -0
- package/src/modules/communication_channels/lib/import-history-limits.ts +109 -0
- package/src/modules/communication_channels/workers/channel-import-history.ts +46 -5
- package/src/modules/messages/api/route.ts +8 -2
- package/src/modules/messages/commands/messages.ts +17 -2
- package/src/modules/messages/data/validators.ts +26 -11
- package/src/modules/messages/subscribers/message-notification.ts +17 -5
- package/src/modules/query_index/lib/engine.ts +23 -1
- 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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
132
|
-
|
|
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
|
-
|
|
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
|
|
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
|