@open-mercato/core 0.9.1-develop.7499.1.fcec456e4e → 0.9.1-develop.7515.1.e2712c8b00
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/audit_logs/components/AuditLogsActions.js +1 -1
- package/dist/modules/audit_logs/components/AuditLogsActions.js.map +2 -2
- package/dist/modules/auth/frontend/reset.js +7 -3
- package/dist/modules/auth/frontend/reset.js.map +2 -2
- package/dist/modules/business_rules/api/rules/route.js +12 -1
- package/dist/modules/business_rules/api/rules/route.js.map +2 -2
- package/dist/modules/catalog/backend/catalog/products/[id]/page.js +17 -2
- package/dist/modules/catalog/backend/catalog/products/[id]/page.js.map +2 -2
- package/dist/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.js +3 -2
- package/dist/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.js.map +2 -2
- package/dist/modules/catalog/backend/catalog/products/create/page.js +1 -0
- package/dist/modules/catalog/backend/catalog/products/create/page.js.map +2 -2
- package/dist/modules/catalog/components/products/productFormUtils.js +9 -0
- package/dist/modules/catalog/components/products/productFormUtils.js.map +2 -2
- package/dist/modules/customers/backend/customers/companies/page.js +32 -32
- package/dist/modules/customers/backend/customers/companies/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/page.js +22 -22
- package/dist/modules/customers/backend/customers/deals/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/people/page.js +34 -34
- package/dist/modules/customers/backend/customers/people/page.js.map +2 -2
- package/dist/modules/ecommerce/search.js +1 -2
- package/dist/modules/ecommerce/search.js.map +2 -2
- package/dist/modules/shipping_carriers/api/webhook/[provider]/route.js +4 -7
- package/dist/modules/shipping_carriers/api/webhook/[provider]/route.js.map +2 -2
- package/dist/modules/shipping_carriers/lib/shipping-service.js +29 -11
- package/dist/modules/shipping_carriers/lib/shipping-service.js.map +2 -2
- package/dist/modules/shipping_carriers/lib/status-sync.js +2 -2
- package/dist/modules/shipping_carriers/lib/status-sync.js.map +2 -2
- package/dist/modules/shipping_carriers/workers/webhook-processor.js +60 -33
- package/dist/modules/shipping_carriers/workers/webhook-processor.js.map +3 -3
- package/dist/modules/staff/backend/staff/time-tracking/entries/page.js +10 -7
- package/dist/modules/staff/backend/staff/time-tracking/entries/page.js.map +2 -2
- package/dist/modules/warranty_claims/commands/claim-lines.js +89 -68
- package/dist/modules/warranty_claims/commands/claim-lines.js.map +2 -2
- package/dist/modules/warranty_claims/commands/claims.js +14 -13
- package/dist/modules/warranty_claims/commands/claims.js.map +2 -2
- package/dist/modules/warranty_claims/commands/shared.js +8 -2
- package/dist/modules/warranty_claims/commands/shared.js.map +2 -2
- package/dist/modules/wms/api/interceptors.js +3 -0
- package/dist/modules/wms/api/interceptors.js.map +2 -2
- package/dist/modules/wms/commands/configuration.js +16 -16
- package/dist/modules/wms/commands/configuration.js.map +2 -2
- package/dist/modules/wms/events.js +1 -0
- package/dist/modules/wms/events.js.map +2 -2
- package/dist/modules/wms/lib/catalogInventoryProfileIntent.js +9 -0
- package/dist/modules/wms/lib/catalogInventoryProfileIntent.js.map +2 -2
- package/dist/modules/wms/widgets/injection/catalog-inventory-profile/widget.js +11 -3
- package/dist/modules/wms/widgets/injection/catalog-inventory-profile/widget.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/audit_logs/components/AuditLogsActions.tsx +1 -1
- package/src/modules/auth/frontend/reset.tsx +8 -3
- package/src/modules/business_rules/api/rules/route.ts +11 -0
- package/src/modules/business_rules/i18n/de.json +1 -0
- package/src/modules/business_rules/i18n/en.json +1 -0
- package/src/modules/business_rules/i18n/es.json +1 -0
- package/src/modules/business_rules/i18n/ko.json +1 -0
- package/src/modules/business_rules/i18n/pl.json +1 -0
- package/src/modules/catalog/backend/catalog/products/[id]/page.tsx +20 -1
- package/src/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.tsx +2 -1
- package/src/modules/catalog/backend/catalog/products/create/page.tsx +1 -0
- package/src/modules/catalog/components/products/productFormUtils.ts +9 -0
- package/src/modules/customers/backend/customers/companies/page.tsx +32 -32
- package/src/modules/customers/backend/customers/deals/page.tsx +22 -22
- package/src/modules/customers/backend/customers/people/page.tsx +34 -34
- package/src/modules/customers/i18n/de.json +12 -0
- package/src/modules/customers/i18n/en.json +12 -0
- package/src/modules/customers/i18n/es.json +12 -0
- package/src/modules/customers/i18n/ko.json +12 -0
- package/src/modules/customers/i18n/pl.json +12 -0
- package/src/modules/ecommerce/i18n/de.json +2 -1
- package/src/modules/ecommerce/i18n/en.json +2 -1
- package/src/modules/ecommerce/i18n/es.json +2 -1
- package/src/modules/ecommerce/i18n/ko.json +2 -1
- package/src/modules/ecommerce/i18n/pl.json +2 -1
- package/src/modules/ecommerce/search.ts +0 -1
- package/src/modules/shipping_carriers/api/webhook/[provider]/route.ts +4 -7
- package/src/modules/shipping_carriers/lib/shipping-service.ts +29 -11
- package/src/modules/shipping_carriers/lib/status-sync.ts +2 -2
- package/src/modules/shipping_carriers/workers/webhook-processor.ts +71 -39
- package/src/modules/staff/backend/staff/time-tracking/entries/page.tsx +20 -14
- package/src/modules/warranty_claims/commands/claim-lines.ts +107 -66
- package/src/modules/warranty_claims/commands/claims.ts +14 -13
- package/src/modules/warranty_claims/commands/shared.ts +7 -1
- package/src/modules/wms/api/interceptors.ts +9 -0
- package/src/modules/wms/commands/configuration.ts +28 -16
- package/src/modules/wms/events.ts +1 -0
- package/src/modules/wms/lib/catalogInventoryProfileIntent.ts +9 -0
- package/src/modules/wms/widgets/injection/catalog-inventory-profile/widget.ts +13 -4
|
@@ -68,6 +68,7 @@ import {
|
|
|
68
68
|
ensureTenantScope,
|
|
69
69
|
extractUndoPayload,
|
|
70
70
|
requireScopedClaim,
|
|
71
|
+
throwUndoRefusal,
|
|
71
72
|
type WarrantyClaimScope,
|
|
72
73
|
} from './shared'
|
|
73
74
|
|
|
@@ -628,8 +629,8 @@ function restoreLineDelta(line: WarrantyClaimLine, before: ClaimLineSnapshot, af
|
|
|
628
629
|
line.updatedAt = new Date()
|
|
629
630
|
}
|
|
630
631
|
|
|
631
|
-
function throwClaimUndoConflict(): never {
|
|
632
|
-
|
|
632
|
+
function throwClaimUndoConflict(): Promise<never> {
|
|
633
|
+
return throwUndoRefusal('warranty_claims.errors.conflict', 'This claim changed while you were editing it.')
|
|
633
634
|
}
|
|
634
635
|
|
|
635
636
|
async function requireCurrentUndoSnapshot(
|
|
@@ -645,7 +646,7 @@ async function requireCurrentUndoSnapshot(
|
|
|
645
646
|
scope,
|
|
646
647
|
)
|
|
647
648
|
const expectedClaimUpdatedAt = toIso(after.updatedAt)
|
|
648
|
-
if (!claim || !expectedClaimUpdatedAt || toIso(claim.updatedAt) !== expectedClaimUpdatedAt) throwClaimUndoConflict()
|
|
649
|
+
if (!claim || !expectedClaimUpdatedAt || toIso(claim.updatedAt) !== expectedClaimUpdatedAt) return throwClaimUndoConflict()
|
|
649
650
|
const lines = await findWithDecryption(
|
|
650
651
|
em,
|
|
651
652
|
WarrantyClaimLine,
|
|
@@ -654,12 +655,12 @@ async function requireCurrentUndoSnapshot(
|
|
|
654
655
|
scope,
|
|
655
656
|
)
|
|
656
657
|
const afterLines = Array.isArray(after.lines) ? after.lines : []
|
|
657
|
-
if (lines.length !== afterLines.length) throwClaimUndoConflict()
|
|
658
|
+
if (lines.length !== afterLines.length) return throwClaimUndoConflict()
|
|
658
659
|
const afterLinesById = new Map(afterLines.map((line) => [line.id, line]))
|
|
659
660
|
for (const line of lines) {
|
|
660
661
|
const expected = afterLinesById.get(line.id)
|
|
661
662
|
const expectedLineUpdatedAt = toIso(expected?.updatedAt)
|
|
662
|
-
if (!expectedLineUpdatedAt || toIso(line.updatedAt) !== expectedLineUpdatedAt) throwClaimUndoConflict()
|
|
663
|
+
if (!expectedLineUpdatedAt || toIso(line.updatedAt) !== expectedLineUpdatedAt) return throwClaimUndoConflict()
|
|
663
664
|
}
|
|
664
665
|
return { claim, lines }
|
|
665
666
|
}
|
|
@@ -679,7 +680,7 @@ async function restoreSnapshot(em: EntityManager, before: ClaimSnapshot, after:
|
|
|
679
680
|
for (const line of lines) {
|
|
680
681
|
const beforeLine = beforeLinesById.get(line.id)
|
|
681
682
|
const afterLine = afterLinesById.get(line.id)
|
|
682
|
-
if (!beforeLine || !afterLine) throwClaimUndoConflict()
|
|
683
|
+
if (!beforeLine || !afterLine) return throwClaimUndoConflict()
|
|
683
684
|
if (snapshotValueChanged(beforeLine, afterLine)) restoreLineDelta(line, beforeLine, afterLine)
|
|
684
685
|
}
|
|
685
686
|
return claim
|
|
@@ -1690,7 +1691,7 @@ const updateClaimCommand: CommandHandler<ClaimUpdateInput, { claimId: string }>
|
|
|
1690
1691
|
const before = payload?.before
|
|
1691
1692
|
if (!before) return
|
|
1692
1693
|
const after = payload?.after
|
|
1693
|
-
if (!after) throwClaimUndoConflict()
|
|
1694
|
+
if (!after) return throwClaimUndoConflict()
|
|
1694
1695
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
1695
1696
|
let claim!: WarrantyClaim
|
|
1696
1697
|
await withAtomicFlush(em, [
|
|
@@ -1754,7 +1755,7 @@ const deleteClaimCommand: CommandHandler<ClaimDeleteInput, { claimId: string }>
|
|
|
1754
1755
|
const before = payload?.before
|
|
1755
1756
|
if (!before) return
|
|
1756
1757
|
const after = payload?.after
|
|
1757
|
-
if (!after) throwClaimUndoConflict()
|
|
1758
|
+
if (!after) return throwClaimUndoConflict()
|
|
1758
1759
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
1759
1760
|
let claim!: WarrantyClaim
|
|
1760
1761
|
await withAtomicFlush(em, [
|
|
@@ -1939,7 +1940,7 @@ const transitionClaimCommand: CommandHandler<TransitionClaimInput, { claimId: st
|
|
|
1939
1940
|
const before = payload?.before
|
|
1940
1941
|
if (!before) return
|
|
1941
1942
|
const after = payload?.after
|
|
1942
|
-
if (!after) throwClaimUndoConflict()
|
|
1943
|
+
if (!after) return throwClaimUndoConflict()
|
|
1943
1944
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
1944
1945
|
let claim!: WarrantyClaim
|
|
1945
1946
|
await withAtomicFlush(em, [
|
|
@@ -2043,7 +2044,7 @@ const setReturnLabelCommand: CommandHandler<ClaimSetReturnLabelInput, ClaimSetRe
|
|
|
2043
2044
|
const before = payload?.before
|
|
2044
2045
|
if (!before) return
|
|
2045
2046
|
const after = payload?.after
|
|
2046
|
-
if (!after) throwClaimUndoConflict()
|
|
2047
|
+
if (!after) return throwClaimUndoConflict()
|
|
2047
2048
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
2048
2049
|
let claim!: WarrantyClaim
|
|
2049
2050
|
await withAtomicFlush(em, [
|
|
@@ -2522,7 +2523,7 @@ const createSalesReturnCommand: CommandHandler<ClaimCreateSalesReturnInput, Clai
|
|
|
2522
2523
|
const before = payload?.before
|
|
2523
2524
|
if (!before) return
|
|
2524
2525
|
const after = payload?.after
|
|
2525
|
-
if (!after) throwClaimUndoConflict()
|
|
2526
|
+
if (!after) return throwClaimUndoConflict()
|
|
2526
2527
|
await assertUndoSnapshotCurrent(ctx, after)
|
|
2527
2528
|
const scope: WarrantyClaimScope = { tenantId: before.tenantId, organizationId: before.organizationId }
|
|
2528
2529
|
const salesReturn = payload?.salesReturn ?? null
|
|
@@ -2911,7 +2912,7 @@ const createCreditMemoCommand: CommandHandler<ClaimCreateCreditMemoInput, ClaimC
|
|
|
2911
2912
|
const before = payload?.before
|
|
2912
2913
|
if (!before) return
|
|
2913
2914
|
const after = payload?.after
|
|
2914
|
-
if (!after) throwClaimUndoConflict()
|
|
2915
|
+
if (!after) return throwClaimUndoConflict()
|
|
2915
2916
|
await assertUndoSnapshotCurrent(ctx, after)
|
|
2916
2917
|
const scope: WarrantyClaimScope = { tenantId: before.tenantId, organizationId: before.organizationId }
|
|
2917
2918
|
const creditMemo = payload?.creditMemo ?? null
|
|
@@ -3275,7 +3276,7 @@ const createReplacementOrderCommand: CommandHandler<ClaimCreateReplacementOrderI
|
|
|
3275
3276
|
const before = payload?.before
|
|
3276
3277
|
if (!before) return
|
|
3277
3278
|
const after = payload?.after
|
|
3278
|
-
if (!after) throwClaimUndoConflict()
|
|
3279
|
+
if (!after) return throwClaimUndoConflict()
|
|
3279
3280
|
await assertUndoSnapshotCurrent(ctx, after)
|
|
3280
3281
|
const scope: WarrantyClaimScope = { tenantId: before.tenantId, organizationId: before.organizationId }
|
|
3281
3282
|
const replacementOrder = payload?.replacementOrder ?? null
|
|
@@ -2,13 +2,14 @@ import { randomUUID } from 'crypto'
|
|
|
2
2
|
import type { FindOneOptions } from '@mikro-orm/core'
|
|
3
3
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
4
4
|
import type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
|
|
5
|
-
import { assertFound } from '@open-mercato/shared/lib/crud/errors'
|
|
5
|
+
import { CrudHttpError, assertFound } from '@open-mercato/shared/lib/crud/errors'
|
|
6
6
|
import { invalidateCrudCache } from '@open-mercato/shared/lib/crud/cache'
|
|
7
7
|
import { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
|
|
8
8
|
import { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'
|
|
9
9
|
import { emitCrudSideEffects, flushCrudSideEffects } from '@open-mercato/shared/lib/commands/helpers'
|
|
10
10
|
import type { DataEngine } from '@open-mercato/shared/lib/data/engine'
|
|
11
11
|
import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
12
|
+
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
12
13
|
import {
|
|
13
14
|
WarrantyClaim,
|
|
14
15
|
WarrantyClaimEvent,
|
|
@@ -65,6 +66,11 @@ export async function enforceWarrantyClaimOptimisticLock(
|
|
|
65
66
|
})
|
|
66
67
|
}
|
|
67
68
|
|
|
69
|
+
export async function throwUndoRefusal(key: string, fallback: string): Promise<never> {
|
|
70
|
+
const { translate } = await resolveTranslations()
|
|
71
|
+
throw new CrudHttpError(409, { error: translate(key, fallback), code: key })
|
|
72
|
+
}
|
|
73
|
+
|
|
68
74
|
export async function loadScopedClaim(
|
|
69
75
|
em: EntityManager,
|
|
70
76
|
id: string,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ApiInterceptor } from '@open-mercato/shared/lib/crud/api-interceptor'
|
|
2
|
+
import { E } from '#generated/entities.ids.generated'
|
|
2
3
|
import {
|
|
3
4
|
decodeCatalogInventoryProfileIntent,
|
|
4
5
|
WMS_CATALOG_PROFILE_HEADER,
|
|
@@ -35,6 +36,12 @@ export const interceptors: ApiInterceptor[] = [
|
|
|
35
36
|
if (!rawHeader) return { ok: true }
|
|
36
37
|
|
|
37
38
|
const intent = decodeCatalogInventoryProfileIntent(rawHeader)
|
|
39
|
+
// CrudForm scopes `onBeforeSave` request headers around the whole
|
|
40
|
+
// `onSubmit` callback, so a page whose submit handler also creates
|
|
41
|
+
// other records (e.g. the product-create wizard implicitly creating
|
|
42
|
+
// its default variant) can leak this header onto those unrelated
|
|
43
|
+
// requests. Only honor an intent the product form itself computed.
|
|
44
|
+
if (intent?.sourceEntityId !== E.catalog.catalog_product) return { ok: true }
|
|
38
45
|
return {
|
|
39
46
|
ok: true,
|
|
40
47
|
metadata: { intent, target: 'product' },
|
|
@@ -74,6 +81,8 @@ export const interceptors: ApiInterceptor[] = [
|
|
|
74
81
|
if (!rawHeader) return { ok: true }
|
|
75
82
|
|
|
76
83
|
const intent = decodeCatalogInventoryProfileIntent(rawHeader)
|
|
84
|
+
// See the product interceptor's `before` for why this check exists.
|
|
85
|
+
if (intent?.sourceEntityId !== E.catalog.catalog_product_variant) return { ok: true }
|
|
77
86
|
return {
|
|
78
87
|
ok: true,
|
|
79
88
|
metadata: { intent, target: 'variant' },
|
|
@@ -1532,6 +1532,28 @@ function restoreInventoryProfileFromSnapshot(em: EntityManager, before: ProductI
|
|
|
1532
1532
|
record.deletedAt = null
|
|
1533
1533
|
}
|
|
1534
1534
|
|
|
1535
|
+
type InventoryProfileEventSource = {
|
|
1536
|
+
id: string
|
|
1537
|
+
catalogProductId: string
|
|
1538
|
+
catalogVariantId?: string | null
|
|
1539
|
+
tenantId: string
|
|
1540
|
+
organizationId: string
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
function emitInventoryProfileEvent(
|
|
1544
|
+
eventId: 'wms.inventory_profile.created' | 'wms.inventory_profile.updated' | 'wms.inventory_profile.deleted',
|
|
1545
|
+
profile: InventoryProfileEventSource,
|
|
1546
|
+
): void {
|
|
1547
|
+
void emitWmsEvent(eventId, {
|
|
1548
|
+
id: profile.id,
|
|
1549
|
+
profileId: profile.id,
|
|
1550
|
+
catalogProductId: profile.catalogProductId,
|
|
1551
|
+
catalogVariantId: profile.catalogVariantId ?? null,
|
|
1552
|
+
tenantId: profile.tenantId,
|
|
1553
|
+
organizationId: profile.organizationId,
|
|
1554
|
+
}).catch(() => undefined)
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1535
1557
|
const createProductInventoryProfileCommand: CommandHandler<ProductInventoryProfileCreateInput, { profileId: string }> = {
|
|
1536
1558
|
id: 'wms.inventoryProfiles.create',
|
|
1537
1559
|
async execute(rawInput, ctx) {
|
|
@@ -1555,14 +1577,7 @@ const createProductInventoryProfileCommand: CommandHandler<ProductInventoryProfi
|
|
|
1555
1577
|
metadata: toJsonValue(parsed.metadata),
|
|
1556
1578
|
})
|
|
1557
1579
|
await em.persist(profile).flush()
|
|
1558
|
-
|
|
1559
|
-
id: profile.id,
|
|
1560
|
-
profileId: profile.id,
|
|
1561
|
-
catalogProductId: profile.catalogProductId,
|
|
1562
|
-
catalogVariantId: profile.catalogVariantId ?? null,
|
|
1563
|
-
tenantId: profile.tenantId,
|
|
1564
|
-
organizationId: profile.organizationId,
|
|
1565
|
-
}).catch(() => undefined)
|
|
1580
|
+
emitInventoryProfileEvent('wms.inventory_profile.created', profile)
|
|
1566
1581
|
return { profileId: profile.id }
|
|
1567
1582
|
},
|
|
1568
1583
|
captureAfter: async (_input, result, ctx) => {
|
|
@@ -1591,6 +1606,7 @@ const createProductInventoryProfileCommand: CommandHandler<ProductInventoryProfi
|
|
|
1591
1606
|
ensureOrganizationScope(ctx, record.organizationId)
|
|
1592
1607
|
record.deletedAt = new Date()
|
|
1593
1608
|
await em.flush()
|
|
1609
|
+
emitInventoryProfileEvent('wms.inventory_profile.deleted', record)
|
|
1594
1610
|
},
|
|
1595
1611
|
}
|
|
1596
1612
|
|
|
@@ -1629,14 +1645,7 @@ const updateProductInventoryProfileCommand: CommandHandler<ProductInventoryProfi
|
|
|
1629
1645
|
if (parsed.safetyStock !== undefined) profile.safetyStock = toNumericString(parsed.safetyStock)
|
|
1630
1646
|
if (parsed.metadata !== undefined) profile.metadata = toJsonValue(parsed.metadata)
|
|
1631
1647
|
await em.flush()
|
|
1632
|
-
|
|
1633
|
-
id: profile.id,
|
|
1634
|
-
profileId: profile.id,
|
|
1635
|
-
catalogProductId: profile.catalogProductId,
|
|
1636
|
-
catalogVariantId: profile.catalogVariantId ?? null,
|
|
1637
|
-
tenantId: profile.tenantId,
|
|
1638
|
-
organizationId: profile.organizationId,
|
|
1639
|
-
}).catch(() => undefined)
|
|
1648
|
+
emitInventoryProfileEvent('wms.inventory_profile.updated', profile)
|
|
1640
1649
|
return { profileId: profile.id }
|
|
1641
1650
|
},
|
|
1642
1651
|
captureAfter: async (_input, result, ctx) => {
|
|
@@ -1686,6 +1695,7 @@ const updateProductInventoryProfileCommand: CommandHandler<ProductInventoryProfi
|
|
|
1686
1695
|
restoreInventoryProfileFromSnapshot(em, before, record)
|
|
1687
1696
|
}
|
|
1688
1697
|
await em.flush()
|
|
1698
|
+
emitInventoryProfileEvent('wms.inventory_profile.updated', before)
|
|
1689
1699
|
},
|
|
1690
1700
|
}
|
|
1691
1701
|
|
|
@@ -1703,6 +1713,7 @@ const deleteProductInventoryProfileCommand: CommandHandler<{ id?: string }, { pr
|
|
|
1703
1713
|
const profile = await loadProfile(em, ctx, profileId)
|
|
1704
1714
|
profile.deletedAt = new Date()
|
|
1705
1715
|
await em.flush()
|
|
1716
|
+
emitInventoryProfileEvent('wms.inventory_profile.deleted', profile)
|
|
1706
1717
|
return { profileId: profile.id }
|
|
1707
1718
|
},
|
|
1708
1719
|
buildLog: async ({ input, result, ctx, snapshots }) => {
|
|
@@ -1747,6 +1758,7 @@ const deleteProductInventoryProfileCommand: CommandHandler<{ id?: string }, { pr
|
|
|
1747
1758
|
restoreInventoryProfileFromSnapshot(em, before, record)
|
|
1748
1759
|
}
|
|
1749
1760
|
await em.flush()
|
|
1761
|
+
emitInventoryProfileEvent('wms.inventory_profile.created', before)
|
|
1750
1762
|
},
|
|
1751
1763
|
}
|
|
1752
1764
|
|
|
@@ -9,6 +9,7 @@ const events = [
|
|
|
9
9
|
{ id: 'wms.location.updated', label: 'Warehouse Location Updated', entity: 'location', category: 'crud' },
|
|
10
10
|
{ id: 'wms.inventory_profile.created', label: 'Inventory Profile Created', entity: 'inventory_profile', category: 'crud' },
|
|
11
11
|
{ id: 'wms.inventory_profile.updated', label: 'Inventory Profile Updated', entity: 'inventory_profile', category: 'crud' },
|
|
12
|
+
{ id: 'wms.inventory_profile.deleted', label: 'Inventory Profile Deleted', entity: 'inventory_profile', category: 'crud' },
|
|
12
13
|
{ id: 'wms.inventory_balance.created', label: 'Inventory Balance Created', entity: 'inventory_balance', category: 'crud' },
|
|
13
14
|
{ id: 'wms.inventory_balance.updated', label: 'Inventory Balance Updated', entity: 'inventory_balance', category: 'crud' },
|
|
14
15
|
{ id: 'wms.inventory_balance.deleted', label: 'Inventory Balance Deleted', entity: 'inventory_balance', category: 'crud' },
|
|
@@ -3,6 +3,15 @@ import { z } from 'zod'
|
|
|
3
3
|
const inventoryStrategySchema = z.enum(['fifo', 'lifo', 'fefo'])
|
|
4
4
|
|
|
5
5
|
export const catalogInventoryProfileIntentSchema = z.object({
|
|
6
|
+
// Identifies which host CrudForm (product vs. variant) computed this intent.
|
|
7
|
+
// CrudForm scopes `onBeforeSave` request headers around the whole `onSubmit`
|
|
8
|
+
// callback, not around a single HTTP call, so a page whose submit handler
|
|
9
|
+
// creates secondary records (e.g. the product-create wizard implicitly
|
|
10
|
+
// creating its default variant) can leak this header onto those unrelated
|
|
11
|
+
// requests. Each interceptor only acts when this matches its own target
|
|
12
|
+
// entity, so a leaked intent is ignored rather than creating a profile the
|
|
13
|
+
// user never asked for on that other entity.
|
|
14
|
+
sourceEntityId: z.string(),
|
|
6
15
|
manageInventory: z.boolean(),
|
|
7
16
|
defaultUom: z.string().trim().max(32).nullable().optional(),
|
|
8
17
|
defaultStrategy: inventoryStrategySchema.optional(),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { InjectionFieldWidget } from '@open-mercato/shared/modules/widgets/injection'
|
|
1
|
+
import type { InjectionContext, InjectionFieldWidget } from '@open-mercato/shared/modules/widgets/injection'
|
|
2
2
|
import {
|
|
3
3
|
encodeCatalogInventoryProfileIntent,
|
|
4
4
|
WMS_CATALOG_PROFILE_HEADER,
|
|
@@ -29,8 +29,9 @@ function readNumber(value: unknown): number | undefined {
|
|
|
29
29
|
return undefined
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
function buildIntent(data: RecordLike): CatalogInventoryProfileIntent {
|
|
32
|
+
function buildIntent(data: RecordLike, sourceEntityId: string): CatalogInventoryProfileIntent {
|
|
33
33
|
return {
|
|
34
|
+
sourceEntityId,
|
|
34
35
|
manageInventory: readBoolean(data['wms.manageInventory']),
|
|
35
36
|
defaultUom: readString(data['wms.defaultUom']).trim() || null,
|
|
36
37
|
defaultStrategy: (() => {
|
|
@@ -47,6 +48,10 @@ function buildIntent(data: RecordLike): CatalogInventoryProfileIntent {
|
|
|
47
48
|
}
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
function hasLoadedInventoryProfile(data: RecordLike): boolean {
|
|
52
|
+
return Boolean(asRecord(asRecord(data._wms)?.inventoryProfile))
|
|
53
|
+
}
|
|
54
|
+
|
|
50
55
|
function buildDisplayState(data: RecordLike): RecordLike {
|
|
51
56
|
const wms = asRecord(data._wms)
|
|
52
57
|
const inventoryProfile = asRecord(wms?.inventoryProfile)
|
|
@@ -170,12 +175,16 @@ const widget: InjectionFieldWidget = {
|
|
|
170
175
|
if (!source) return data
|
|
171
176
|
return buildDisplayState(source) as typeof data
|
|
172
177
|
},
|
|
173
|
-
async onBeforeSave(data) {
|
|
178
|
+
async onBeforeSave(data, context: InjectionContext) {
|
|
174
179
|
const source = asRecord(data)
|
|
175
180
|
if (!source) return { ok: true }
|
|
176
181
|
|
|
177
|
-
const
|
|
182
|
+
const sourceEntityId = readString(context?.entityId)
|
|
183
|
+
const intent = buildIntent(source, sourceEntityId)
|
|
178
184
|
if (!intent.manageInventory) {
|
|
185
|
+
if (source['wms.manageInventory'] !== false || !hasLoadedInventoryProfile(source)) {
|
|
186
|
+
return { ok: true }
|
|
187
|
+
}
|
|
179
188
|
return {
|
|
180
189
|
ok: true,
|
|
181
190
|
requestHeaders: {
|