@open-mercato/core 0.6.4-develop.3996.1.430e257cfc → 0.6.4-develop.4011.1.4f3ed9ae3e
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/auth/api/features.js +24 -2
- package/dist/modules/auth/api/features.js.map +2 -2
- package/dist/modules/auth/backend/users/[id]/edit/page.js +70 -57
- package/dist/modules/auth/backend/users/[id]/edit/page.js.map +2 -2
- package/dist/modules/auth/components/AclDependencyDiagnosticsPanel.js +135 -0
- package/dist/modules/auth/components/AclDependencyDiagnosticsPanel.js.map +7 -0
- package/dist/modules/auth/components/AclEditor.js +10 -0
- package/dist/modules/auth/components/AclEditor.js.map +2 -2
- package/dist/modules/catalog/acl.js +30 -5
- package/dist/modules/catalog/acl.js.map +2 -2
- package/dist/modules/catalog/backend/catalog/products/[id]/page.js +17 -5
- package/dist/modules/catalog/backend/catalog/products/[id]/page.js.map +2 -2
- package/dist/modules/catalog/commands/offers.js +26 -7
- package/dist/modules/catalog/commands/offers.js.map +2 -2
- package/dist/modules/catalog/commands/prices.js +41 -26
- package/dist/modules/catalog/commands/prices.js.map +2 -2
- package/dist/modules/catalog/commands/productUnitConversions.js +7 -1
- package/dist/modules/catalog/commands/productUnitConversions.js.map +2 -2
- package/dist/modules/catalog/commands/products.js +2 -0
- package/dist/modules/catalog/commands/products.js.map +2 -2
- package/dist/modules/catalog/commands/shared.js +58 -11
- package/dist/modules/catalog/commands/shared.js.map +2 -2
- package/dist/modules/catalog/commands/variants.js +18 -5
- package/dist/modules/catalog/commands/variants.js.map +2 -2
- package/dist/modules/customers/acl.js +72 -12
- package/dist/modules/customers/acl.js.map +2 -2
- package/dist/modules/resources/backend/resources/resources/[id]/page.js +17 -2
- package/dist/modules/resources/backend/resources/resources/[id]/page.js.map +2 -2
- package/dist/modules/sales/acl.js +109 -16
- package/dist/modules/sales/acl.js.map +2 -2
- package/dist/modules/sales/backend/sales/documents/[id]/page.js +20 -1
- package/dist/modules/sales/backend/sales/documents/[id]/page.js.map +2 -2
- package/dist/modules/sales/setup.js +2 -0
- package/dist/modules/sales/setup.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/auth/api/features.ts +34 -4
- package/src/modules/auth/backend/users/[id]/edit/page.tsx +28 -6
- package/src/modules/auth/components/AclDependencyDiagnosticsPanel.tsx +173 -0
- package/src/modules/auth/components/AclEditor.tsx +14 -4
- package/src/modules/auth/i18n/de.json +9 -0
- package/src/modules/auth/i18n/en.json +9 -0
- package/src/modules/auth/i18n/es.json +9 -0
- package/src/modules/auth/i18n/pl.json +9 -0
- package/src/modules/catalog/acl.ts +30 -5
- package/src/modules/catalog/backend/catalog/products/[id]/page.tsx +21 -5
- package/src/modules/catalog/commands/offers.ts +26 -7
- package/src/modules/catalog/commands/prices.ts +41 -26
- package/src/modules/catalog/commands/productUnitConversions.ts +7 -1
- package/src/modules/catalog/commands/products.ts +2 -0
- package/src/modules/catalog/commands/shared.ts +70 -6
- package/src/modules/catalog/commands/variants.ts +18 -5
- package/src/modules/catalog/i18n/de.json +1 -0
- package/src/modules/catalog/i18n/en.json +1 -0
- package/src/modules/catalog/i18n/es.json +1 -0
- package/src/modules/catalog/i18n/pl.json +1 -0
- package/src/modules/customers/acl.ts +72 -12
- package/src/modules/resources/backend/resources/resources/[id]/page.tsx +21 -2
- package/src/modules/sales/acl.ts +109 -16
- package/src/modules/sales/backend/sales/documents/[id]/page.tsx +28 -1
- package/src/modules/sales/i18n/de.json +3 -0
- package/src/modules/sales/i18n/en.json +3 -0
- package/src/modules/sales/i18n/es.json +3 -0
- package/src/modules/sales/i18n/pl.json +3 -0
- package/src/modules/sales/setup.ts +2 -0
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from '../data/validators'
|
|
15
15
|
import {
|
|
16
16
|
cloneJson,
|
|
17
|
+
commandActorScope,
|
|
17
18
|
ensureOrganizationScope,
|
|
18
19
|
ensureSameScope,
|
|
19
20
|
ensureTenantScope,
|
|
@@ -98,7 +99,10 @@ const createOfferCommand: CommandHandler<OfferCreateInput, { offerId: string }>
|
|
|
98
99
|
ensureTenantScope(ctx, parsed.tenantId)
|
|
99
100
|
ensureOrganizationScope(ctx, parsed.organizationId)
|
|
100
101
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
101
|
-
const product = await requireProduct(em, parsed.productId
|
|
102
|
+
const product = await requireProduct(em, parsed.productId, {
|
|
103
|
+
tenantId: parsed.tenantId,
|
|
104
|
+
organizationId: parsed.organizationId,
|
|
105
|
+
})
|
|
102
106
|
if (
|
|
103
107
|
product.organizationId !== parsed.organizationId ||
|
|
104
108
|
product.tenantId !== parsed.tenantId
|
|
@@ -224,10 +228,16 @@ const updateOfferCommand: CommandHandler<OfferUpdateInput, { offerId: string }>
|
|
|
224
228
|
ensureOrganizationScope(ctx, record.organizationId)
|
|
225
229
|
let productEntity =
|
|
226
230
|
typeof record.product === 'string'
|
|
227
|
-
? await requireProduct(em, record.product
|
|
231
|
+
? await requireProduct(em, record.product, {
|
|
232
|
+
tenantId: record.tenantId,
|
|
233
|
+
organizationId: record.organizationId,
|
|
234
|
+
})
|
|
228
235
|
: record.product
|
|
229
236
|
if (parsed.productId && parsed.productId !== record.product.id) {
|
|
230
|
-
const nextProduct = await requireProduct(em, parsed.productId
|
|
237
|
+
const nextProduct = await requireProduct(em, parsed.productId, {
|
|
238
|
+
tenantId: record.tenantId,
|
|
239
|
+
organizationId: record.organizationId,
|
|
240
|
+
})
|
|
231
241
|
ensureSameScope(nextProduct, record.organizationId, record.tenantId)
|
|
232
242
|
productEntity = nextProduct
|
|
233
243
|
}
|
|
@@ -322,9 +332,15 @@ const updateOfferCommand: CommandHandler<OfferUpdateInput, { offerId: string }>
|
|
|
322
332
|
const before = payload?.before
|
|
323
333
|
if (!before) return
|
|
324
334
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
325
|
-
const record = await requireOffer(em, before.id
|
|
335
|
+
const record = await requireOffer(em, before.id, {
|
|
336
|
+
tenantId: before.tenantId,
|
|
337
|
+
organizationId: before.organizationId,
|
|
338
|
+
}).catch(() => null)
|
|
326
339
|
if (!record) {
|
|
327
|
-
const product = await requireProduct(em, before.productId
|
|
340
|
+
const product = await requireProduct(em, before.productId, {
|
|
341
|
+
tenantId: before.tenantId,
|
|
342
|
+
organizationId: before.organizationId,
|
|
343
|
+
})
|
|
328
344
|
ensureSameScope(product, before.organizationId, before.tenantId)
|
|
329
345
|
const restored = em.create(CatalogOffer, {
|
|
330
346
|
id: before.id,
|
|
@@ -384,7 +400,7 @@ const deleteOfferCommand: CommandHandler<{ id?: string }, { offerId: string }> =
|
|
|
384
400
|
async execute(input, ctx) {
|
|
385
401
|
const parsed = { id: requireId(input, 'Offer id is required.') }
|
|
386
402
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
387
|
-
const record = await requireOffer(em, parsed.id)
|
|
403
|
+
const record = await requireOffer(em, parsed.id, commandActorScope(ctx))
|
|
388
404
|
ensureTenantScope(ctx, record.tenantId)
|
|
389
405
|
ensureOrganizationScope(ctx, record.organizationId)
|
|
390
406
|
const baseEm = ctx.container.resolve('em') as EntityManager
|
|
@@ -436,7 +452,10 @@ const deleteOfferCommand: CommandHandler<{ id?: string }, { offerId: string }> =
|
|
|
436
452
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
437
453
|
const existing = await em.findOne(CatalogOffer, { id: before.id })
|
|
438
454
|
if (existing) return
|
|
439
|
-
const product = await requireProduct(em, before.productId
|
|
455
|
+
const product = await requireProduct(em, before.productId, {
|
|
456
|
+
tenantId: before.tenantId,
|
|
457
|
+
organizationId: before.organizationId,
|
|
458
|
+
})
|
|
440
459
|
ensureSameScope(product, before.organizationId, before.tenantId)
|
|
441
460
|
const restored = em.create(CatalogOffer, {
|
|
442
461
|
id: before.id,
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
import type { TaxCalculationService } from '@open-mercato/core/modules/sales/services/taxCalculationService'
|
|
17
17
|
import {
|
|
18
18
|
cloneJson,
|
|
19
|
+
commandActorScope,
|
|
19
20
|
ensureOrganizationScope,
|
|
20
21
|
ensureSameScope,
|
|
21
22
|
ensureSameTenant,
|
|
@@ -105,17 +106,18 @@ async function resolveSnapshotAssociations(
|
|
|
105
106
|
product: CatalogProduct
|
|
106
107
|
offer: CatalogOffer | null
|
|
107
108
|
}> {
|
|
109
|
+
const scope = { tenantId: snapshot.tenantId, organizationId: snapshot.organizationId }
|
|
108
110
|
let variant: CatalogProductVariant | null = null
|
|
109
111
|
if (snapshot.variantId) {
|
|
110
|
-
variant = await requireVariant(em, snapshot.variantId)
|
|
112
|
+
variant = await requireVariant(em, snapshot.variantId, scope)
|
|
111
113
|
}
|
|
112
114
|
let product: CatalogProduct | null = null
|
|
113
115
|
if (snapshot.productId) {
|
|
114
|
-
product = await requireProduct(em, snapshot.productId)
|
|
116
|
+
product = await requireProduct(em, snapshot.productId, scope)
|
|
115
117
|
} else if (variant) {
|
|
116
118
|
product =
|
|
117
119
|
typeof variant.product === 'string'
|
|
118
|
-
? await requireProduct(em, variant.product)
|
|
120
|
+
? await requireProduct(em, variant.product, scope)
|
|
119
121
|
: variant.product
|
|
120
122
|
}
|
|
121
123
|
if (!product) {
|
|
@@ -123,7 +125,7 @@ async function resolveSnapshotAssociations(
|
|
|
123
125
|
}
|
|
124
126
|
let offer: CatalogOffer | null = null
|
|
125
127
|
if (snapshot.offerId) {
|
|
126
|
-
offer = await requireOffer(em, snapshot.offerId)
|
|
128
|
+
offer = await requireOffer(em, snapshot.offerId, scope)
|
|
127
129
|
}
|
|
128
130
|
return { variant, product, offer }
|
|
129
131
|
}
|
|
@@ -262,17 +264,21 @@ const createPriceCommand: CommandHandler<PriceCreateInput, { priceId: string }>
|
|
|
262
264
|
async execute(rawInput, ctx) {
|
|
263
265
|
const { parsed, custom } = parseWithCustomFields(priceCreateSchema, rawInput)
|
|
264
266
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
267
|
+
const actorScope = commandActorScope(ctx)
|
|
265
268
|
let variant: CatalogProductVariant | null = null
|
|
266
269
|
let product: CatalogProduct | null = null
|
|
267
270
|
if (parsed.variantId) {
|
|
268
|
-
variant = await requireVariant(em, parsed.variantId)
|
|
271
|
+
variant = await requireVariant(em, parsed.variantId, actorScope)
|
|
269
272
|
product =
|
|
270
273
|
typeof variant.product === 'string'
|
|
271
|
-
? await requireProduct(em, variant.product
|
|
274
|
+
? await requireProduct(em, variant.product, {
|
|
275
|
+
tenantId: variant.tenantId,
|
|
276
|
+
organizationId: variant.organizationId,
|
|
277
|
+
})
|
|
272
278
|
: variant.product
|
|
273
279
|
}
|
|
274
280
|
if (parsed.productId) {
|
|
275
|
-
const explicitProduct = await requireProduct(em, parsed.productId)
|
|
281
|
+
const explicitProduct = await requireProduct(em, parsed.productId, actorScope)
|
|
276
282
|
if (product && explicitProduct.id !== product.id) {
|
|
277
283
|
throw new CrudHttpError(400, { error: 'Variant does not belong to the provided product.' })
|
|
278
284
|
}
|
|
@@ -284,17 +290,24 @@ const createPriceCommand: CommandHandler<PriceCreateInput, { priceId: string }>
|
|
|
284
290
|
const scopeSource = variant ?? product!
|
|
285
291
|
ensureTenantScope(ctx, scopeSource.tenantId)
|
|
286
292
|
ensureOrganizationScope(ctx, scopeSource.organizationId)
|
|
293
|
+
const scopeSourceScope = {
|
|
294
|
+
tenantId: scopeSource.tenantId,
|
|
295
|
+
organizationId: scopeSource.organizationId,
|
|
296
|
+
}
|
|
287
297
|
|
|
288
|
-
const priceKind = await requirePriceKind(em, parsed.priceKindId)
|
|
298
|
+
const priceKind = await requirePriceKind(em, parsed.priceKindId, scopeSourceScope)
|
|
289
299
|
ensureSameTenant(priceKind, scopeSource.tenantId)
|
|
290
300
|
|
|
291
301
|
let offer: CatalogOffer | null = null
|
|
292
302
|
if (parsed.offerId) {
|
|
293
|
-
offer = await requireOffer(em, parsed.offerId)
|
|
303
|
+
offer = await requireOffer(em, parsed.offerId, scopeSourceScope)
|
|
294
304
|
ensureSameScope(offer, scopeSource.organizationId, scopeSource.tenantId)
|
|
295
305
|
const offerProduct =
|
|
296
306
|
typeof offer.product === 'string'
|
|
297
|
-
? await requireProduct(em, offer.product
|
|
307
|
+
? await requireProduct(em, offer.product, {
|
|
308
|
+
tenantId: offer.tenantId,
|
|
309
|
+
organizationId: offer.organizationId,
|
|
310
|
+
})
|
|
298
311
|
: offer.product
|
|
299
312
|
if (product && offerProduct.id !== product.id) {
|
|
300
313
|
throw new CrudHttpError(400, { error: 'Offer does not belong to the selected product.' })
|
|
@@ -445,17 +458,18 @@ const updatePriceCommand: CommandHandler<PriceUpdateInput, { priceId: string }>
|
|
|
445
458
|
{ tenantId: parsed.tenantId, organizationId: parsed.organizationId },
|
|
446
459
|
)
|
|
447
460
|
if (!record) throw new CrudHttpError(404, { error: 'Catalog price not found' })
|
|
461
|
+
const recordScope = { tenantId: record.tenantId, organizationId: record.organizationId }
|
|
448
462
|
const currentVariantRef = record.variant
|
|
449
463
|
let targetVariant: CatalogProductVariant | null = null
|
|
450
464
|
if (typeof currentVariantRef === 'string') {
|
|
451
|
-
targetVariant = await requireVariant(em, currentVariantRef)
|
|
465
|
+
targetVariant = await requireVariant(em, currentVariantRef, recordScope)
|
|
452
466
|
} else if (currentVariantRef) {
|
|
453
467
|
targetVariant = currentVariantRef
|
|
454
468
|
}
|
|
455
469
|
const currentProductRef = record.product ?? (targetVariant ? targetVariant.product : null)
|
|
456
470
|
let targetProduct: CatalogProduct | null = null
|
|
457
471
|
if (typeof currentProductRef === 'string') {
|
|
458
|
-
targetProduct = await requireProduct(em, currentProductRef)
|
|
472
|
+
targetProduct = await requireProduct(em, currentProductRef, recordScope)
|
|
459
473
|
} else if (currentProductRef) {
|
|
460
474
|
targetProduct = currentProductRef
|
|
461
475
|
}
|
|
@@ -464,23 +478,23 @@ const updatePriceCommand: CommandHandler<PriceUpdateInput, { priceId: string }>
|
|
|
464
478
|
if (!parsed.variantId) {
|
|
465
479
|
targetVariant = null
|
|
466
480
|
} else {
|
|
467
|
-
targetVariant = await requireVariant(em, parsed.variantId)
|
|
481
|
+
targetVariant = await requireVariant(em, parsed.variantId, recordScope)
|
|
468
482
|
targetProduct =
|
|
469
483
|
typeof targetVariant.product === 'string'
|
|
470
|
-
? await requireProduct(em, targetVariant.product)
|
|
484
|
+
? await requireProduct(em, targetVariant.product, recordScope)
|
|
471
485
|
: targetVariant.product
|
|
472
486
|
}
|
|
473
487
|
}
|
|
474
488
|
|
|
475
489
|
if (targetVariant && (targetVariant as CatalogProductVariant | null)?.product === undefined) {
|
|
476
|
-
targetVariant = await requireVariant(em, targetVariant.id)
|
|
490
|
+
targetVariant = await requireVariant(em, targetVariant.id, recordScope)
|
|
477
491
|
}
|
|
478
492
|
|
|
479
493
|
if (parsed.productId !== undefined) {
|
|
480
494
|
if (!parsed.productId) {
|
|
481
495
|
targetProduct = null
|
|
482
496
|
} else {
|
|
483
|
-
const explicitProduct = await requireProduct(em, parsed.productId)
|
|
497
|
+
const explicitProduct = await requireProduct(em, parsed.productId, recordScope)
|
|
484
498
|
if (targetVariant) {
|
|
485
499
|
const variantProductId =
|
|
486
500
|
typeof targetVariant.product === 'string'
|
|
@@ -500,7 +514,7 @@ const updatePriceCommand: CommandHandler<PriceUpdateInput, { priceId: string }>
|
|
|
500
514
|
if (!targetProduct && targetVariant) {
|
|
501
515
|
targetProduct =
|
|
502
516
|
typeof targetVariant.product === 'string'
|
|
503
|
-
? await requireProduct(em, targetVariant.product)
|
|
517
|
+
? await requireProduct(em, targetVariant.product, recordScope)
|
|
504
518
|
: targetVariant.product
|
|
505
519
|
}
|
|
506
520
|
if (!targetProduct) {
|
|
@@ -511,14 +525,14 @@ const updatePriceCommand: CommandHandler<PriceUpdateInput, { priceId: string }>
|
|
|
511
525
|
if (record.offer) {
|
|
512
526
|
targetOffer =
|
|
513
527
|
typeof record.offer === 'string'
|
|
514
|
-
? await requireOffer(em, record.offer)
|
|
528
|
+
? await requireOffer(em, record.offer, recordScope)
|
|
515
529
|
: record.offer
|
|
516
530
|
}
|
|
517
531
|
if (parsed.offerId !== undefined) {
|
|
518
532
|
if (!parsed.offerId) {
|
|
519
533
|
targetOffer = null
|
|
520
534
|
} else {
|
|
521
|
-
const explicitOffer = await requireOffer(em, parsed.offerId)
|
|
535
|
+
const explicitOffer = await requireOffer(em, parsed.offerId, recordScope)
|
|
522
536
|
ensureSameScope(explicitOffer, targetProduct.organizationId, targetProduct.tenantId)
|
|
523
537
|
const offerProductId =
|
|
524
538
|
typeof explicitOffer.product === 'string'
|
|
@@ -538,14 +552,14 @@ const updatePriceCommand: CommandHandler<PriceUpdateInput, { priceId: string }>
|
|
|
538
552
|
if (record.priceKind) {
|
|
539
553
|
targetPriceKind =
|
|
540
554
|
typeof record.priceKind === 'string'
|
|
541
|
-
? await requirePriceKind(em, record.priceKind)
|
|
555
|
+
? await requirePriceKind(em, record.priceKind, recordScope)
|
|
542
556
|
: record.priceKind
|
|
543
557
|
}
|
|
544
558
|
if (parsed.priceKindId !== undefined) {
|
|
545
559
|
if (!parsed.priceKindId) {
|
|
546
560
|
throw new CrudHttpError(400, { error: 'Price kind is required.' })
|
|
547
561
|
}
|
|
548
|
-
targetPriceKind = await requirePriceKind(em, parsed.priceKindId)
|
|
562
|
+
targetPriceKind = await requirePriceKind(em, parsed.priceKindId, recordScope)
|
|
549
563
|
}
|
|
550
564
|
if (!targetPriceKind) {
|
|
551
565
|
throw new CrudHttpError(400, { error: 'Price kind is required.' })
|
|
@@ -881,15 +895,16 @@ async function resolvePriceRecordAssociations(
|
|
|
881
895
|
em: EntityManager,
|
|
882
896
|
record: CatalogProductPrice,
|
|
883
897
|
): Promise<{ product: CatalogProduct; variant: CatalogProductVariant | null }> {
|
|
898
|
+
const scope = { tenantId: record.tenantId, organizationId: record.organizationId }
|
|
884
899
|
const variant = record.variant
|
|
885
900
|
? (typeof record.variant === 'string'
|
|
886
|
-
? await requireVariant(em, record.variant)
|
|
901
|
+
? await requireVariant(em, record.variant, scope)
|
|
887
902
|
: record.variant)
|
|
888
903
|
: null
|
|
889
904
|
if (record.product) {
|
|
890
905
|
const product =
|
|
891
906
|
typeof record.product === 'string'
|
|
892
|
-
? await requireProduct(em, record.product)
|
|
907
|
+
? await requireProduct(em, record.product, scope)
|
|
893
908
|
: record.product
|
|
894
909
|
return { product, variant }
|
|
895
910
|
}
|
|
@@ -897,20 +912,20 @@ async function resolvePriceRecordAssociations(
|
|
|
897
912
|
const productRef = variant.product
|
|
898
913
|
const product =
|
|
899
914
|
typeof productRef === 'string'
|
|
900
|
-
? await requireProduct(em, productRef)
|
|
915
|
+
? await requireProduct(em, productRef, scope)
|
|
901
916
|
: productRef
|
|
902
917
|
return { product, variant }
|
|
903
918
|
}
|
|
904
919
|
if (record.offer) {
|
|
905
920
|
const offer =
|
|
906
921
|
typeof record.offer === 'string'
|
|
907
|
-
? await requireOffer(em, record.offer)
|
|
922
|
+
? await requireOffer(em, record.offer, scope)
|
|
908
923
|
: record.offer
|
|
909
924
|
const productRef = offer?.product ?? null
|
|
910
925
|
if (productRef) {
|
|
911
926
|
const product =
|
|
912
927
|
typeof productRef === 'string'
|
|
913
|
-
? await requireProduct(em, productRef)
|
|
928
|
+
? await requireProduct(em, productRef, scope)
|
|
914
929
|
: productRef
|
|
915
930
|
return { product, variant }
|
|
916
931
|
}
|
|
@@ -247,6 +247,7 @@ const createProductUnitConversionCommand: CommandHandler<
|
|
|
247
247
|
const product = await requireProduct(
|
|
248
248
|
em,
|
|
249
249
|
parsed.productId,
|
|
250
|
+
{ tenantId: parsed.tenantId, organizationId: parsed.organizationId },
|
|
250
251
|
translate("catalog.errors.productNotFound", "Catalog product not found"),
|
|
251
252
|
);
|
|
252
253
|
ensureSameScope(product, parsed.organizationId, parsed.tenantId);
|
|
@@ -373,7 +374,12 @@ const updateProductUnitConversionCommand: CommandHandler<
|
|
|
373
374
|
});
|
|
374
375
|
const product =
|
|
375
376
|
typeof record.product === "string"
|
|
376
|
-
? await requireProduct(
|
|
377
|
+
? await requireProduct(
|
|
378
|
+
em,
|
|
379
|
+
record.product,
|
|
380
|
+
{ tenantId: record.tenantId, organizationId: record.organizationId },
|
|
381
|
+
translate("catalog.errors.productNotFound", "Catalog product not found"),
|
|
382
|
+
)
|
|
377
383
|
: record.product;
|
|
378
384
|
ensureTenantScope(ctx, record.tenantId);
|
|
379
385
|
ensureOrganizationScope(ctx, record.organizationId);
|
|
@@ -1288,6 +1288,7 @@ const createProductCommand: CommandHandler<
|
|
|
1288
1288
|
optionSchemaTemplate = await requireOptionSchemaTemplate(
|
|
1289
1289
|
em,
|
|
1290
1290
|
parsed.optionSchemaId,
|
|
1291
|
+
{ tenantId: parsed.tenantId, organizationId: parsed.organizationId },
|
|
1291
1292
|
translate("catalog.errors.optionSchemaNotFound", "Option schema not found"),
|
|
1292
1293
|
);
|
|
1293
1294
|
ensureSameScope(
|
|
@@ -1591,6 +1592,7 @@ const updateProductCommand: CommandHandler<
|
|
|
1591
1592
|
const optionTemplate = await requireOptionSchemaTemplate(
|
|
1592
1593
|
lookupEm,
|
|
1593
1594
|
parsed.optionSchemaId,
|
|
1595
|
+
{ tenantId, organizationId },
|
|
1594
1596
|
translate("catalog.errors.optionSchemaNotFound", "Option schema not found"),
|
|
1595
1597
|
);
|
|
1596
1598
|
ensureSameScope(optionTemplate, organizationId, tenantId);
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
CatalogOptionSchemaTemplate,
|
|
6
6
|
CatalogPriceKind,
|
|
7
7
|
} from '../data/entities'
|
|
8
|
-
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
8
|
+
import type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'
|
|
9
9
|
import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
10
10
|
import type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
|
|
11
11
|
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
@@ -73,12 +73,42 @@ export function toNumericString(value: number | null | undefined): string | null
|
|
|
73
73
|
return value.toString()
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
export type RequireScope = {
|
|
77
|
+
tenantId: string | null
|
|
78
|
+
organizationId: string | null
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Derives the actor's effective tenant/org scope for entry-point lookups, mirroring
|
|
82
|
+
// the bypass semantics of ensureTenantScope/ensureOrganizationScope: tenant is always
|
|
83
|
+
// strict, organization is left unrestricted for super-admins and global-org actors.
|
|
84
|
+
export function commandActorScope(ctx: CommandRuntimeContext): RequireScope {
|
|
85
|
+
const orgUnrestricted = ctx.auth?.isSuperAdmin === true || ctx.organizationScope?.allowedIds === null
|
|
86
|
+
return {
|
|
87
|
+
tenantId: ctx.auth?.tenantId ?? null,
|
|
88
|
+
organizationId: orgUnrestricted ? null : (ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null),
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function applyScopeToWhere(where: Record<string, unknown>, scope: RequireScope): void {
|
|
93
|
+
if (scope.tenantId != null) where.tenantId = scope.tenantId
|
|
94
|
+
if (scope.organizationId != null) where.organizationId = scope.organizationId
|
|
95
|
+
}
|
|
96
|
+
|
|
76
97
|
export async function requireProduct(
|
|
77
98
|
em: EntityManager,
|
|
78
99
|
id: string,
|
|
100
|
+
scope: RequireScope,
|
|
79
101
|
message = 'Catalog product not found'
|
|
80
102
|
): Promise<CatalogProduct> {
|
|
81
|
-
const
|
|
103
|
+
const where: Record<string, unknown> = { id, deletedAt: null }
|
|
104
|
+
applyScopeToWhere(where, scope)
|
|
105
|
+
const product = await findOneWithDecryption(
|
|
106
|
+
em,
|
|
107
|
+
CatalogProduct,
|
|
108
|
+
where as FilterQuery<CatalogProduct>,
|
|
109
|
+
undefined,
|
|
110
|
+
{ tenantId: scope.tenantId, organizationId: scope.organizationId },
|
|
111
|
+
)
|
|
82
112
|
if (!product) throw new CrudHttpError(404, { error: message })
|
|
83
113
|
return product
|
|
84
114
|
}
|
|
@@ -86,13 +116,17 @@ export async function requireProduct(
|
|
|
86
116
|
export async function requireVariant(
|
|
87
117
|
em: EntityManager,
|
|
88
118
|
id: string,
|
|
119
|
+
scope: RequireScope,
|
|
89
120
|
message = 'Catalog variant not found'
|
|
90
121
|
): Promise<CatalogProductVariant> {
|
|
122
|
+
const where: Record<string, unknown> = { id, deletedAt: null }
|
|
123
|
+
applyScopeToWhere(where, scope)
|
|
91
124
|
const variant = await findOneWithDecryption(
|
|
92
125
|
em,
|
|
93
126
|
CatalogProductVariant,
|
|
94
|
-
|
|
127
|
+
where as FilterQuery<CatalogProductVariant>,
|
|
95
128
|
{ populate: ['product'] },
|
|
129
|
+
{ tenantId: scope.tenantId, organizationId: scope.organizationId },
|
|
96
130
|
)
|
|
97
131
|
if (!variant) throw new CrudHttpError(404, { error: message })
|
|
98
132
|
return variant
|
|
@@ -101,9 +135,18 @@ export async function requireVariant(
|
|
|
101
135
|
export async function requireOffer(
|
|
102
136
|
em: EntityManager,
|
|
103
137
|
id: string,
|
|
138
|
+
scope: RequireScope,
|
|
104
139
|
message = 'Catalog offer not found'
|
|
105
140
|
): Promise<CatalogOffer> {
|
|
106
|
-
const
|
|
141
|
+
const where: Record<string, unknown> = { id }
|
|
142
|
+
applyScopeToWhere(where, scope)
|
|
143
|
+
const offer = await findOneWithDecryption(
|
|
144
|
+
em,
|
|
145
|
+
CatalogOffer,
|
|
146
|
+
where as FilterQuery<CatalogOffer>,
|
|
147
|
+
undefined,
|
|
148
|
+
{ tenantId: scope.tenantId, organizationId: scope.organizationId },
|
|
149
|
+
)
|
|
107
150
|
if (!offer) throw new CrudHttpError(404, { error: message })
|
|
108
151
|
return offer
|
|
109
152
|
}
|
|
@@ -111,9 +154,21 @@ export async function requireOffer(
|
|
|
111
154
|
export async function requirePriceKind(
|
|
112
155
|
em: EntityManager,
|
|
113
156
|
id: string,
|
|
157
|
+
scope: RequireScope,
|
|
114
158
|
message = 'Catalog price kind not found'
|
|
115
159
|
): Promise<CatalogPriceKind> {
|
|
116
|
-
|
|
160
|
+
// Price kinds are tenant-global: organization_id is always null and the unique key is
|
|
161
|
+
// (tenant_id, code). Scope by tenant only — applying a concrete org would never match the
|
|
162
|
+
// null row. Tenant scoping still closes the cross-tenant read hole this helper guards.
|
|
163
|
+
const where: Record<string, unknown> = { id, deletedAt: null }
|
|
164
|
+
applyScopeToWhere(where, { tenantId: scope.tenantId, organizationId: null })
|
|
165
|
+
const priceKind = await findOneWithDecryption(
|
|
166
|
+
em,
|
|
167
|
+
CatalogPriceKind,
|
|
168
|
+
where as FilterQuery<CatalogPriceKind>,
|
|
169
|
+
undefined,
|
|
170
|
+
{ tenantId: scope.tenantId, organizationId: null },
|
|
171
|
+
)
|
|
117
172
|
if (!priceKind) throw new CrudHttpError(404, { error: message })
|
|
118
173
|
return priceKind
|
|
119
174
|
}
|
|
@@ -121,9 +176,18 @@ export async function requirePriceKind(
|
|
|
121
176
|
export async function requireOptionSchemaTemplate(
|
|
122
177
|
em: EntityManager,
|
|
123
178
|
id: string,
|
|
179
|
+
scope: RequireScope,
|
|
124
180
|
message = 'Option schema not found'
|
|
125
181
|
): Promise<CatalogOptionSchemaTemplate> {
|
|
126
|
-
const
|
|
182
|
+
const where: Record<string, unknown> = { id, deletedAt: null }
|
|
183
|
+
applyScopeToWhere(where, scope)
|
|
184
|
+
const schema = await findOneWithDecryption(
|
|
185
|
+
em,
|
|
186
|
+
CatalogOptionSchemaTemplate,
|
|
187
|
+
where as FilterQuery<CatalogOptionSchemaTemplate>,
|
|
188
|
+
undefined,
|
|
189
|
+
{ tenantId: scope.tenantId, organizationId: scope.organizationId },
|
|
190
|
+
)
|
|
127
191
|
if (!schema) throw new CrudHttpError(404, { error: message })
|
|
128
192
|
return schema
|
|
129
193
|
}
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
} from '../data/validators'
|
|
26
26
|
import {
|
|
27
27
|
cloneJson,
|
|
28
|
+
commandActorScope,
|
|
28
29
|
ensureOrganizationScope,
|
|
29
30
|
ensureTenantScope,
|
|
30
31
|
emitCatalogQueryIndexEvent,
|
|
@@ -318,7 +319,10 @@ async function restoreVariantPricesFromSnapshots(
|
|
|
318
319
|
if (!snapshots.length) return
|
|
319
320
|
const productRef =
|
|
320
321
|
typeof variant.product === 'string'
|
|
321
|
-
? await requireProduct(em, variant.product
|
|
322
|
+
? await requireProduct(em, variant.product, {
|
|
323
|
+
tenantId: variant.tenantId,
|
|
324
|
+
organizationId: variant.organizationId,
|
|
325
|
+
})
|
|
322
326
|
: variant.product
|
|
323
327
|
for (const snapshot of snapshots) {
|
|
324
328
|
const product =
|
|
@@ -547,7 +551,7 @@ const createVariantCommand: CommandHandler<VariantCreateInput, { variantId: stri
|
|
|
547
551
|
async execute(rawInput, ctx) {
|
|
548
552
|
const { parsed, custom } = parseWithCustomFields(variantCreateSchema, rawInput)
|
|
549
553
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
550
|
-
const product = await requireProduct(em, parsed.productId)
|
|
554
|
+
const product = await requireProduct(em, parsed.productId, commandActorScope(ctx))
|
|
551
555
|
ensureTenantScope(ctx, product.tenantId)
|
|
552
556
|
ensureOrganizationScope(ctx, product.organizationId)
|
|
553
557
|
const { taxRateId, taxRate } = await resolveVariantTaxRate(
|
|
@@ -705,7 +709,10 @@ const updateVariantCommand: CommandHandler<VariantUpdateInput, { variantId: stri
|
|
|
705
709
|
if (!record) throw new CrudHttpError(404, { error: 'Catalog variant not found' })
|
|
706
710
|
ensureTenantScope(ctx, record.tenantId)
|
|
707
711
|
ensureOrganizationScope(ctx, record.organizationId)
|
|
708
|
-
const product = await requireProduct(em, record.product.id
|
|
712
|
+
const product = await requireProduct(em, record.product.id, {
|
|
713
|
+
tenantId: record.tenantId,
|
|
714
|
+
organizationId: record.organizationId,
|
|
715
|
+
})
|
|
709
716
|
|
|
710
717
|
if (!product) throw new CrudHttpError(400, { error: 'Variant product missing' })
|
|
711
718
|
|
|
@@ -827,7 +834,10 @@ const updateVariantCommand: CommandHandler<VariantUpdateInput, { variantId: stri
|
|
|
827
834
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
828
835
|
let record = await em.findOne(CatalogProductVariant, { id: before.id })
|
|
829
836
|
if (!record) {
|
|
830
|
-
const product = await requireProduct(em, before.productId
|
|
837
|
+
const product = await requireProduct(em, before.productId, {
|
|
838
|
+
tenantId: before.tenantId,
|
|
839
|
+
organizationId: before.organizationId,
|
|
840
|
+
})
|
|
831
841
|
record = em.create(CatalogProductVariant, {
|
|
832
842
|
id: before.id,
|
|
833
843
|
product,
|
|
@@ -994,7 +1004,10 @@ const deleteVariantCommand: CommandHandler<
|
|
|
994
1004
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
995
1005
|
let record = await em.findOne(CatalogProductVariant, { id: before.id })
|
|
996
1006
|
if (!record) {
|
|
997
|
-
const product = await requireProduct(em, before.productId
|
|
1007
|
+
const product = await requireProduct(em, before.productId, {
|
|
1008
|
+
tenantId: before.tenantId,
|
|
1009
|
+
organizationId: before.organizationId,
|
|
1010
|
+
})
|
|
998
1011
|
record = em.create(CatalogProductVariant, {
|
|
999
1012
|
id: before.id,
|
|
1000
1013
|
product,
|
|
@@ -361,6 +361,7 @@
|
|
|
361
361
|
"catalog.products.create.variantsBuilder.vatColumn": "Steuerklasse",
|
|
362
362
|
"catalog.products.create.variantsBuilder.vatOptionDefault": "Produkt-Steuerklasse verwenden ({{label}})",
|
|
363
363
|
"catalog.products.create.variantsBuilder.vatOptionNone": "Keine Steuerklasse",
|
|
364
|
+
"catalog.products.edit.actions.backToList": "Zurück zu Produkten",
|
|
364
365
|
"catalog.products.edit.custom.title": "Benutzerdefinierte Attribute",
|
|
365
366
|
"catalog.products.edit.dimensions": "Maße & Gewicht",
|
|
366
367
|
"catalog.products.edit.dimensions.depth": "Tiefe",
|
|
@@ -361,6 +361,7 @@
|
|
|
361
361
|
"catalog.products.create.variantsBuilder.vatColumn": "Tax class",
|
|
362
362
|
"catalog.products.create.variantsBuilder.vatOptionDefault": "Use product tax class ({{label}})",
|
|
363
363
|
"catalog.products.create.variantsBuilder.vatOptionNone": "No tax class",
|
|
364
|
+
"catalog.products.edit.actions.backToList": "Back to products",
|
|
364
365
|
"catalog.products.edit.custom.title": "Custom attributes",
|
|
365
366
|
"catalog.products.edit.dimensions": "Dimensions & weight",
|
|
366
367
|
"catalog.products.edit.dimensions.depth": "Depth",
|
|
@@ -361,6 +361,7 @@
|
|
|
361
361
|
"catalog.products.create.variantsBuilder.vatColumn": "Clase de impuesto",
|
|
362
362
|
"catalog.products.create.variantsBuilder.vatOptionDefault": "Usar clase de impuesto del producto ({{label}})",
|
|
363
363
|
"catalog.products.create.variantsBuilder.vatOptionNone": "Sin clase de impuesto",
|
|
364
|
+
"catalog.products.edit.actions.backToList": "Volver a productos",
|
|
364
365
|
"catalog.products.edit.custom.title": "Atributos personalizados",
|
|
365
366
|
"catalog.products.edit.dimensions": "Dimensiones y peso",
|
|
366
367
|
"catalog.products.edit.dimensions.depth": "Profundidad",
|
|
@@ -361,6 +361,7 @@
|
|
|
361
361
|
"catalog.products.create.variantsBuilder.vatColumn": "Klasa podatkowa",
|
|
362
362
|
"catalog.products.create.variantsBuilder.vatOptionDefault": "Użyj klasy podatkowej produktu ({{label}})",
|
|
363
363
|
"catalog.products.create.variantsBuilder.vatOptionNone": "Brak klasy podatkowej",
|
|
364
|
+
"catalog.products.edit.actions.backToList": "Powrót do listy produktów",
|
|
364
365
|
"catalog.products.edit.custom.title": "Atrybuty niestandardowe",
|
|
365
366
|
"catalog.products.edit.dimensions": "Wymiary i waga",
|
|
366
367
|
"catalog.products.edit.dimensions.depth": "Głębokość",
|