@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.
Files changed (89) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/audit_logs/components/AuditLogsActions.js +1 -1
  3. package/dist/modules/audit_logs/components/AuditLogsActions.js.map +2 -2
  4. package/dist/modules/auth/frontend/reset.js +7 -3
  5. package/dist/modules/auth/frontend/reset.js.map +2 -2
  6. package/dist/modules/business_rules/api/rules/route.js +12 -1
  7. package/dist/modules/business_rules/api/rules/route.js.map +2 -2
  8. package/dist/modules/catalog/backend/catalog/products/[id]/page.js +17 -2
  9. package/dist/modules/catalog/backend/catalog/products/[id]/page.js.map +2 -2
  10. package/dist/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.js +3 -2
  11. package/dist/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.js.map +2 -2
  12. package/dist/modules/catalog/backend/catalog/products/create/page.js +1 -0
  13. package/dist/modules/catalog/backend/catalog/products/create/page.js.map +2 -2
  14. package/dist/modules/catalog/components/products/productFormUtils.js +9 -0
  15. package/dist/modules/catalog/components/products/productFormUtils.js.map +2 -2
  16. package/dist/modules/customers/backend/customers/companies/page.js +32 -32
  17. package/dist/modules/customers/backend/customers/companies/page.js.map +2 -2
  18. package/dist/modules/customers/backend/customers/deals/page.js +22 -22
  19. package/dist/modules/customers/backend/customers/deals/page.js.map +2 -2
  20. package/dist/modules/customers/backend/customers/people/page.js +34 -34
  21. package/dist/modules/customers/backend/customers/people/page.js.map +2 -2
  22. package/dist/modules/ecommerce/search.js +1 -2
  23. package/dist/modules/ecommerce/search.js.map +2 -2
  24. package/dist/modules/shipping_carriers/api/webhook/[provider]/route.js +4 -7
  25. package/dist/modules/shipping_carriers/api/webhook/[provider]/route.js.map +2 -2
  26. package/dist/modules/shipping_carriers/lib/shipping-service.js +29 -11
  27. package/dist/modules/shipping_carriers/lib/shipping-service.js.map +2 -2
  28. package/dist/modules/shipping_carriers/lib/status-sync.js +2 -2
  29. package/dist/modules/shipping_carriers/lib/status-sync.js.map +2 -2
  30. package/dist/modules/shipping_carriers/workers/webhook-processor.js +60 -33
  31. package/dist/modules/shipping_carriers/workers/webhook-processor.js.map +3 -3
  32. package/dist/modules/staff/backend/staff/time-tracking/entries/page.js +10 -7
  33. package/dist/modules/staff/backend/staff/time-tracking/entries/page.js.map +2 -2
  34. package/dist/modules/warranty_claims/commands/claim-lines.js +89 -68
  35. package/dist/modules/warranty_claims/commands/claim-lines.js.map +2 -2
  36. package/dist/modules/warranty_claims/commands/claims.js +14 -13
  37. package/dist/modules/warranty_claims/commands/claims.js.map +2 -2
  38. package/dist/modules/warranty_claims/commands/shared.js +8 -2
  39. package/dist/modules/warranty_claims/commands/shared.js.map +2 -2
  40. package/dist/modules/wms/api/interceptors.js +3 -0
  41. package/dist/modules/wms/api/interceptors.js.map +2 -2
  42. package/dist/modules/wms/commands/configuration.js +16 -16
  43. package/dist/modules/wms/commands/configuration.js.map +2 -2
  44. package/dist/modules/wms/events.js +1 -0
  45. package/dist/modules/wms/events.js.map +2 -2
  46. package/dist/modules/wms/lib/catalogInventoryProfileIntent.js +9 -0
  47. package/dist/modules/wms/lib/catalogInventoryProfileIntent.js.map +2 -2
  48. package/dist/modules/wms/widgets/injection/catalog-inventory-profile/widget.js +11 -3
  49. package/dist/modules/wms/widgets/injection/catalog-inventory-profile/widget.js.map +2 -2
  50. package/package.json +7 -7
  51. package/src/modules/audit_logs/components/AuditLogsActions.tsx +1 -1
  52. package/src/modules/auth/frontend/reset.tsx +8 -3
  53. package/src/modules/business_rules/api/rules/route.ts +11 -0
  54. package/src/modules/business_rules/i18n/de.json +1 -0
  55. package/src/modules/business_rules/i18n/en.json +1 -0
  56. package/src/modules/business_rules/i18n/es.json +1 -0
  57. package/src/modules/business_rules/i18n/ko.json +1 -0
  58. package/src/modules/business_rules/i18n/pl.json +1 -0
  59. package/src/modules/catalog/backend/catalog/products/[id]/page.tsx +20 -1
  60. package/src/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.tsx +2 -1
  61. package/src/modules/catalog/backend/catalog/products/create/page.tsx +1 -0
  62. package/src/modules/catalog/components/products/productFormUtils.ts +9 -0
  63. package/src/modules/customers/backend/customers/companies/page.tsx +32 -32
  64. package/src/modules/customers/backend/customers/deals/page.tsx +22 -22
  65. package/src/modules/customers/backend/customers/people/page.tsx +34 -34
  66. package/src/modules/customers/i18n/de.json +12 -0
  67. package/src/modules/customers/i18n/en.json +12 -0
  68. package/src/modules/customers/i18n/es.json +12 -0
  69. package/src/modules/customers/i18n/ko.json +12 -0
  70. package/src/modules/customers/i18n/pl.json +12 -0
  71. package/src/modules/ecommerce/i18n/de.json +2 -1
  72. package/src/modules/ecommerce/i18n/en.json +2 -1
  73. package/src/modules/ecommerce/i18n/es.json +2 -1
  74. package/src/modules/ecommerce/i18n/ko.json +2 -1
  75. package/src/modules/ecommerce/i18n/pl.json +2 -1
  76. package/src/modules/ecommerce/search.ts +0 -1
  77. package/src/modules/shipping_carriers/api/webhook/[provider]/route.ts +4 -7
  78. package/src/modules/shipping_carriers/lib/shipping-service.ts +29 -11
  79. package/src/modules/shipping_carriers/lib/status-sync.ts +2 -2
  80. package/src/modules/shipping_carriers/workers/webhook-processor.ts +71 -39
  81. package/src/modules/staff/backend/staff/time-tracking/entries/page.tsx +20 -14
  82. package/src/modules/warranty_claims/commands/claim-lines.ts +107 -66
  83. package/src/modules/warranty_claims/commands/claims.ts +14 -13
  84. package/src/modules/warranty_claims/commands/shared.ts +7 -1
  85. package/src/modules/wms/api/interceptors.ts +9 -0
  86. package/src/modules/wms/commands/configuration.ts +28 -16
  87. package/src/modules/wms/events.ts +1 -0
  88. package/src/modules/wms/lib/catalogInventoryProfileIntent.ts +9 -0
  89. 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
- throw new CrudHttpError(409, { error: 'warranty_claims.errors.conflict' })
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
- void emitWmsEvent('wms.inventory_profile.created', {
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
- void emitWmsEvent('wms.inventory_profile.updated', {
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 intent = buildIntent(source)
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: {