@open-mercato/core 0.6.6-develop.6330.1.a261878aa8 → 0.6.6-develop.6331.1.a33b8e99b7

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 (244) hide show
  1. package/dist/modules/attachments/components/AttachmentPartitionSettings.js +3 -0
  2. package/dist/modules/attachments/components/AttachmentPartitionSettings.js.map +2 -2
  3. package/dist/modules/auth/api/roles/acl/route.js +2 -2
  4. package/dist/modules/auth/api/roles/acl/route.js.map +2 -2
  5. package/dist/modules/auth/api/users/acl/route.js +2 -2
  6. package/dist/modules/auth/api/users/acl/route.js.map +2 -2
  7. package/dist/modules/auth/backend/roles/[id]/edit/page.js +12 -0
  8. package/dist/modules/auth/backend/roles/[id]/edit/page.js.map +2 -2
  9. package/dist/modules/auth/backend/users/[id]/edit/page.js +12 -0
  10. package/dist/modules/auth/backend/users/[id]/edit/page.js.map +2 -2
  11. package/dist/modules/auth/data/entities.js +2 -2
  12. package/dist/modules/auth/data/entities.js.map +2 -2
  13. package/dist/modules/business_rules/api/rules/route.js +3 -3
  14. package/dist/modules/business_rules/api/rules/route.js.map +2 -2
  15. package/dist/modules/business_rules/api/sets/route.js +3 -3
  16. package/dist/modules/business_rules/api/sets/route.js.map +2 -2
  17. package/dist/modules/business_rules/backend/rules/[id]/page.js +12 -1
  18. package/dist/modules/business_rules/backend/rules/[id]/page.js.map +2 -2
  19. package/dist/modules/business_rules/backend/sets/[id]/page.js +12 -1
  20. package/dist/modules/business_rules/backend/sets/[id]/page.js.map +2 -2
  21. package/dist/modules/catalog/api/product-media/route.js.map +2 -2
  22. package/dist/modules/catalog/backend/catalog/categories/[id]/edit/page.js +12 -0
  23. package/dist/modules/catalog/backend/catalog/categories/[id]/edit/page.js.map +2 -2
  24. package/dist/modules/catalog/backend/catalog/products/[id]/page.js +15 -0
  25. package/dist/modules/catalog/backend/catalog/products/[id]/page.js.map +2 -2
  26. package/dist/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.js +12 -1
  27. package/dist/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.js.map +2 -2
  28. package/dist/modules/catalog/components/PriceKindSettings.js +13 -3
  29. package/dist/modules/catalog/components/PriceKindSettings.js.map +2 -2
  30. package/dist/modules/catalog/lib/bulkDelete.js.map +2 -2
  31. package/dist/modules/currencies/backend/currencies/[id]/page.js +15 -3
  32. package/dist/modules/currencies/backend/currencies/[id]/page.js.map +2 -2
  33. package/dist/modules/currencies/backend/currencies/page.js +2 -1
  34. package/dist/modules/currencies/backend/currencies/page.js.map +2 -2
  35. package/dist/modules/currencies/backend/exchange-rates/[id]/page.js +12 -1
  36. package/dist/modules/currencies/backend/exchange-rates/[id]/page.js.map +2 -2
  37. package/dist/modules/currencies/backend/exchange-rates/page.js +3 -2
  38. package/dist/modules/currencies/backend/exchange-rates/page.js.map +2 -2
  39. package/dist/modules/customer_accounts/api/admin/roles/[id].js +3 -3
  40. package/dist/modules/customer_accounts/api/admin/roles/[id].js.map +2 -2
  41. package/dist/modules/customer_accounts/api/admin/users/[id].js +3 -3
  42. package/dist/modules/customer_accounts/api/admin/users/[id].js.map +2 -2
  43. package/dist/modules/customers/api/addresses/route.js +4 -2
  44. package/dist/modules/customers/api/addresses/route.js.map +2 -2
  45. package/dist/modules/customers/api/tags/route.js +8 -3
  46. package/dist/modules/customers/api/tags/route.js.map +2 -2
  47. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +13 -2
  48. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
  49. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.js +24 -9
  50. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.js.map +2 -2
  51. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealClosure.js +53 -23
  52. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealClosure.js.map +2 -2
  53. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealPipeline.js +13 -3
  54. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealPipeline.js.map +2 -2
  55. package/dist/modules/customers/backend/customers/deals/[id]/page.js +19 -2
  56. package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
  57. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +13 -2
  58. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
  59. package/dist/modules/customers/commands/interactions.js +5 -5
  60. package/dist/modules/customers/commands/interactions.js.map +2 -2
  61. package/dist/modules/customers/commands/pipeline-stages.js +24 -2
  62. package/dist/modules/customers/commands/pipeline-stages.js.map +2 -2
  63. package/dist/modules/customers/commands/pipelines.js +24 -2
  64. package/dist/modules/customers/commands/pipelines.js.map +2 -2
  65. package/dist/modules/customers/components/detail/AddressesSection.js +46 -29
  66. package/dist/modules/customers/components/detail/AddressesSection.js.map +2 -2
  67. package/dist/modules/customers/components/detail/DealForm.js +5 -1
  68. package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
  69. package/dist/modules/customers/components/detail/notesAdapter.js +25 -18
  70. package/dist/modules/customers/components/detail/notesAdapter.js.map +2 -2
  71. package/dist/modules/data_sync/api/schedules/[id]/route.js +38 -19
  72. package/dist/modules/data_sync/api/schedules/[id]/route.js.map +2 -2
  73. package/dist/modules/data_sync/api/schedules/route.js +1 -1
  74. package/dist/modules/data_sync/api/schedules/route.js.map +2 -2
  75. package/dist/modules/data_sync/backend/data-sync/page.js +32 -15
  76. package/dist/modules/data_sync/backend/data-sync/page.js.map +2 -2
  77. package/dist/modules/data_sync/lib/sync-schedule-service.js +19 -5
  78. package/dist/modules/data_sync/lib/sync-schedule-service.js.map +2 -2
  79. package/dist/modules/dictionaries/api/[dictionaryId]/entries/[entryId]/route.js +3 -3
  80. package/dist/modules/dictionaries/api/[dictionaryId]/entries/[entryId]/route.js.map +2 -2
  81. package/dist/modules/dictionaries/api/[dictionaryId]/route.js +3 -3
  82. package/dist/modules/dictionaries/api/[dictionaryId]/route.js.map +2 -2
  83. package/dist/modules/dictionaries/components/DictionariesManager.js +12 -1
  84. package/dist/modules/dictionaries/components/DictionariesManager.js.map +2 -2
  85. package/dist/modules/dictionaries/components/DictionaryEntriesEditor.js.map +2 -2
  86. package/dist/modules/directory/backend/directory/organizations/[id]/edit/page.js +12 -0
  87. package/dist/modules/directory/backend/directory/organizations/[id]/edit/page.js.map +2 -2
  88. package/dist/modules/directory/backend/directory/tenants/[id]/edit/page.js +12 -0
  89. package/dist/modules/directory/backend/directory/tenants/[id]/edit/page.js.map +2 -2
  90. package/dist/modules/entities/api/entities.js +2 -2
  91. package/dist/modules/entities/api/entities.js.map +2 -2
  92. package/dist/modules/entities/api/records.js +7 -5
  93. package/dist/modules/entities/api/records.js.map +2 -2
  94. package/dist/modules/feature_toggles/api/overrides/route.js +2 -2
  95. package/dist/modules/feature_toggles/api/overrides/route.js.map +2 -2
  96. package/dist/modules/feature_toggles/backend/feature-toggles/global/[id]/edit/page.js.map +2 -2
  97. package/dist/modules/feature_toggles/data/entities.js +1 -1
  98. package/dist/modules/feature_toggles/data/entities.js.map +2 -2
  99. package/dist/modules/feature_toggles/data/validators.js +2 -1
  100. package/dist/modules/feature_toggles/data/validators.js.map +2 -2
  101. package/dist/modules/feature_toggles/lib/queries.js +2 -1
  102. package/dist/modules/feature_toggles/lib/queries.js.map +2 -2
  103. package/dist/modules/inbox_ops/api/settings/route.js +2 -2
  104. package/dist/modules/inbox_ops/api/settings/route.js.map +2 -2
  105. package/dist/modules/resources/backend/resources/resource-types/[id]/edit/page.js +12 -1
  106. package/dist/modules/resources/backend/resources/resource-types/[id]/edit/page.js.map +2 -2
  107. package/dist/modules/resources/backend/resources/resources/[id]/page.js +12 -1
  108. package/dist/modules/resources/backend/resources/resources/[id]/page.js.map +2 -2
  109. package/dist/modules/resources/components/detail/notesAdapter.js +25 -18
  110. package/dist/modules/resources/components/detail/notesAdapter.js.map +2 -2
  111. package/dist/modules/sales/backend/sales/documents/[id]/page.js +14 -1
  112. package/dist/modules/sales/backend/sales/documents/[id]/page.js.map +2 -2
  113. package/dist/modules/sales/commands/documents.js +11 -10
  114. package/dist/modules/sales/commands/documents.js.map +2 -2
  115. package/dist/modules/sales/commands/payments.js +6 -1
  116. package/dist/modules/sales/commands/payments.js.map +2 -2
  117. package/dist/modules/sales/commands/returns.js +3 -3
  118. package/dist/modules/sales/commands/returns.js.map +2 -2
  119. package/dist/modules/sales/commands/shared.js +3 -3
  120. package/dist/modules/sales/commands/shared.js.map +2 -2
  121. package/dist/modules/sales/commands/shipments.js +6 -1
  122. package/dist/modules/sales/commands/shipments.js.map +2 -2
  123. package/dist/modules/sales/components/documents/PaymentDialog.js +6 -3
  124. package/dist/modules/sales/components/documents/PaymentDialog.js.map +2 -2
  125. package/dist/modules/sales/components/documents/PaymentsSection.js +7 -3
  126. package/dist/modules/sales/components/documents/PaymentsSection.js.map +2 -2
  127. package/dist/modules/sales/components/documents/ShipmentDialog.js +6 -3
  128. package/dist/modules/sales/components/documents/ShipmentDialog.js.map +2 -2
  129. package/dist/modules/sales/components/documents/ShipmentsSection.js +7 -3
  130. package/dist/modules/sales/components/documents/ShipmentsSection.js.map +2 -2
  131. package/dist/modules/sales/di.js +39 -3
  132. package/dist/modules/sales/di.js.map +2 -2
  133. package/dist/modules/sales/setup.js +2 -0
  134. package/dist/modules/sales/setup.js.map +2 -2
  135. package/dist/modules/staff/backend/staff/leave-requests/[id]/page.js +12 -1
  136. package/dist/modules/staff/backend/staff/leave-requests/[id]/page.js.map +2 -2
  137. package/dist/modules/staff/backend/staff/team-members/[id]/page.js +12 -1
  138. package/dist/modules/staff/backend/staff/team-members/[id]/page.js.map +2 -2
  139. package/dist/modules/staff/backend/staff/team-roles/[id]/edit/page.js +12 -1
  140. package/dist/modules/staff/backend/staff/team-roles/[id]/edit/page.js.map +2 -2
  141. package/dist/modules/staff/backend/staff/teams/[id]/edit/page.js +12 -1
  142. package/dist/modules/staff/backend/staff/teams/[id]/edit/page.js.map +2 -2
  143. package/dist/modules/staff/commands/job-histories.js +3 -3
  144. package/dist/modules/staff/commands/job-histories.js.map +2 -2
  145. package/dist/modules/staff/components/detail/addressesAdapter.js +33 -24
  146. package/dist/modules/staff/components/detail/addressesAdapter.js.map +2 -2
  147. package/dist/modules/staff/components/detail/notesAdapter.js +25 -18
  148. package/dist/modules/staff/components/detail/notesAdapter.js.map +2 -2
  149. package/dist/modules/translations/api/[entityType]/[entityId]/route.js +37 -0
  150. package/dist/modules/translations/api/[entityType]/[entityId]/route.js.map +2 -2
  151. package/dist/modules/translations/components/TranslationManager.js +7 -1
  152. package/dist/modules/translations/components/TranslationManager.js.map +2 -2
  153. package/dist/modules/workflows/api/definitions/[id]/route.js +3 -3
  154. package/dist/modules/workflows/api/definitions/[id]/route.js.map +2 -2
  155. package/dist/modules/workflows/backend/definitions/[id]/page.js +12 -1
  156. package/dist/modules/workflows/backend/definitions/[id]/page.js.map +2 -2
  157. package/dist/modules/workflows/backend/definitions/visual-editor/page.js +11 -1
  158. package/dist/modules/workflows/backend/definitions/visual-editor/page.js.map +2 -2
  159. package/package.json +7 -7
  160. package/src/modules/attachments/components/AttachmentPartitionSettings.tsx +3 -0
  161. package/src/modules/auth/api/roles/acl/route.ts +2 -2
  162. package/src/modules/auth/api/users/acl/route.ts +2 -2
  163. package/src/modules/auth/backend/roles/[id]/edit/page.tsx +17 -0
  164. package/src/modules/auth/backend/users/[id]/edit/page.tsx +17 -0
  165. package/src/modules/auth/data/entities.ts +2 -2
  166. package/src/modules/business_rules/api/rules/route.ts +3 -3
  167. package/src/modules/business_rules/api/sets/route.ts +3 -3
  168. package/src/modules/business_rules/backend/rules/[id]/page.tsx +17 -1
  169. package/src/modules/business_rules/backend/sets/[id]/page.tsx +18 -1
  170. package/src/modules/catalog/api/product-media/route.ts +4 -0
  171. package/src/modules/catalog/backend/catalog/categories/[id]/edit/page.tsx +17 -0
  172. package/src/modules/catalog/backend/catalog/products/[id]/page.tsx +21 -0
  173. package/src/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.tsx +16 -1
  174. package/src/modules/catalog/components/PriceKindSettings.tsx +15 -7
  175. package/src/modules/catalog/lib/bulkDelete.ts +6 -0
  176. package/src/modules/currencies/backend/currencies/[id]/page.tsx +20 -3
  177. package/src/modules/currencies/backend/currencies/page.tsx +2 -1
  178. package/src/modules/currencies/backend/exchange-rates/[id]/page.tsx +17 -1
  179. package/src/modules/currencies/backend/exchange-rates/page.tsx +3 -2
  180. package/src/modules/customer_accounts/api/admin/roles/[id].ts +3 -3
  181. package/src/modules/customer_accounts/api/admin/users/[id].ts +3 -3
  182. package/src/modules/customers/api/addresses/route.ts +2 -0
  183. package/src/modules/customers/api/tags/route.ts +6 -1
  184. package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +19 -2
  185. package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.ts +29 -8
  186. package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealClosure.ts +47 -20
  187. package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealPipeline.ts +13 -3
  188. package/src/modules/customers/backend/customers/deals/[id]/page.tsx +21 -1
  189. package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +18 -2
  190. package/src/modules/customers/commands/interactions.ts +5 -5
  191. package/src/modules/customers/commands/pipeline-stages.ts +26 -2
  192. package/src/modules/customers/commands/pipelines.ts +26 -2
  193. package/src/modules/customers/components/detail/AddressesSection.tsx +50 -28
  194. package/src/modules/customers/components/detail/DealForm.tsx +12 -0
  195. package/src/modules/customers/components/detail/notesAdapter.ts +29 -18
  196. package/src/modules/customers/i18n/de.json +2 -0
  197. package/src/modules/customers/i18n/en.json +2 -0
  198. package/src/modules/customers/i18n/es.json +2 -0
  199. package/src/modules/customers/i18n/pl.json +2 -0
  200. package/src/modules/data_sync/api/schedules/[id]/route.ts +38 -20
  201. package/src/modules/data_sync/api/schedules/route.ts +1 -1
  202. package/src/modules/data_sync/backend/data-sync/page.tsx +32 -15
  203. package/src/modules/data_sync/lib/sync-schedule-service.ts +30 -5
  204. package/src/modules/dictionaries/api/[dictionaryId]/entries/[entryId]/route.ts +3 -3
  205. package/src/modules/dictionaries/api/[dictionaryId]/route.ts +3 -3
  206. package/src/modules/dictionaries/components/DictionariesManager.tsx +21 -1
  207. package/src/modules/dictionaries/components/DictionaryEntriesEditor.tsx +2 -0
  208. package/src/modules/directory/backend/directory/organizations/[id]/edit/page.tsx +17 -0
  209. package/src/modules/directory/backend/directory/tenants/[id]/edit/page.tsx +17 -0
  210. package/src/modules/entities/api/entities.ts +2 -2
  211. package/src/modules/entities/api/records.ts +7 -5
  212. package/src/modules/feature_toggles/api/overrides/route.ts +2 -2
  213. package/src/modules/feature_toggles/backend/feature-toggles/global/[id]/edit/page.tsx +12 -0
  214. package/src/modules/feature_toggles/data/entities.ts +1 -1
  215. package/src/modules/feature_toggles/data/validators.ts +1 -0
  216. package/src/modules/feature_toggles/lib/queries.ts +1 -0
  217. package/src/modules/inbox_ops/api/settings/route.ts +2 -2
  218. package/src/modules/resources/backend/resources/resource-types/[id]/edit/page.tsx +17 -1
  219. package/src/modules/resources/backend/resources/resources/[id]/page.tsx +17 -1
  220. package/src/modules/resources/components/detail/notesAdapter.ts +29 -18
  221. package/src/modules/sales/backend/sales/documents/[id]/page.tsx +17 -1
  222. package/src/modules/sales/commands/documents.ts +11 -10
  223. package/src/modules/sales/commands/payments.ts +11 -0
  224. package/src/modules/sales/commands/returns.ts +3 -3
  225. package/src/modules/sales/commands/shared.ts +10 -4
  226. package/src/modules/sales/commands/shipments.ts +14 -0
  227. package/src/modules/sales/components/documents/PaymentDialog.tsx +7 -3
  228. package/src/modules/sales/components/documents/PaymentsSection.tsx +8 -3
  229. package/src/modules/sales/components/documents/ShipmentDialog.tsx +7 -3
  230. package/src/modules/sales/components/documents/ShipmentsSection.tsx +8 -3
  231. package/src/modules/sales/di.ts +68 -9
  232. package/src/modules/sales/setup.ts +2 -0
  233. package/src/modules/staff/backend/staff/leave-requests/[id]/page.tsx +17 -1
  234. package/src/modules/staff/backend/staff/team-members/[id]/page.tsx +17 -1
  235. package/src/modules/staff/backend/staff/team-roles/[id]/edit/page.tsx +17 -1
  236. package/src/modules/staff/backend/staff/teams/[id]/edit/page.tsx +17 -1
  237. package/src/modules/staff/commands/job-histories.ts +3 -3
  238. package/src/modules/staff/components/detail/addressesAdapter.ts +40 -23
  239. package/src/modules/staff/components/detail/notesAdapter.ts +28 -18
  240. package/src/modules/translations/api/[entityType]/[entityId]/route.ts +74 -0
  241. package/src/modules/translations/components/TranslationManager.tsx +14 -1
  242. package/src/modules/workflows/api/definitions/[id]/route.ts +3 -3
  243. package/src/modules/workflows/backend/definitions/[id]/page.tsx +18 -1
  244. package/src/modules/workflows/backend/definitions/visual-editor/page.tsx +19 -1
@@ -24,6 +24,7 @@ import {
24
24
  } from '@open-mercato/ui/primitives/dialog'
25
25
  import { apiCall, readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
26
26
  import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
27
+ import { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'
27
28
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
28
29
  import { raiseCrudError } from '@open-mercato/ui/backend/utils/serverErrors'
29
30
  import { useT } from '@open-mercato/shared/lib/i18n/context'
@@ -257,6 +258,7 @@ export function AttachmentPartitionSettings({ s3Enabled }: AttachmentPartitionSe
257
258
  await loadItems()
258
259
  } catch (err) {
259
260
  console.error('[attachments.partitions] save failed', err)
261
+ if (surfaceRecordConflict(err, t)) return
260
262
  const message =
261
263
  err instanceof Error ? err.message : t('attachments.partitions.errors.save', 'Failed to save partition.')
262
264
  setError(message)
@@ -294,6 +296,7 @@ export function AttachmentPartitionSettings({ s3Enabled }: AttachmentPartitionSe
294
296
  await loadItems()
295
297
  } catch (err) {
296
298
  console.error('[attachments.partitions] delete failed', err)
299
+ if (surfaceRecordConflict(err, t)) return
297
300
  flash(t('attachments.partitions.errors.delete', 'Failed to delete partition.'), 'error')
298
301
  }
299
302
  },
@@ -5,7 +5,7 @@ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
5
5
  import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
6
6
  import { logCrudAccess } from '@open-mercato/shared/lib/crud/factory'
7
7
  import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
8
- import { enforceCommandOptimisticLock } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
8
+ import { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
9
9
  import { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'
10
10
  import { RoleAcl, Role } from '@open-mercato/core/modules/auth/data/entities'
11
11
  import type { EntityManager } from '@mikro-orm/postgresql'
@@ -183,7 +183,7 @@ export async function PUT(req: Request) {
183
183
  // no prior version to conflict with).
184
184
  if (acl) {
185
185
  try {
186
- enforceCommandOptimisticLock({
186
+ await enforceCommandOptimisticLockWithGuards(container, {
187
187
  resourceKind: 'auth.role_acl',
188
188
  resourceId: acl.id,
189
189
  current: acl.updatedAt ?? null,
@@ -5,7 +5,7 @@ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
5
5
  import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
6
6
  import { logCrudAccess } from '@open-mercato/shared/lib/crud/factory'
7
7
  import { forbidden, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
8
- import { enforceCommandOptimisticLock } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
8
+ import { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
9
9
  import { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'
10
10
  import { UserAcl } from '@open-mercato/core/modules/auth/data/entities'
11
11
  import {
@@ -159,7 +159,7 @@ export async function PUT(req: Request) {
159
159
  // the client sends no expected-version header; skipped when no ACL row exists.
160
160
  if (acl) {
161
161
  try {
162
- enforceCommandOptimisticLock({
162
+ await enforceCommandOptimisticLockWithGuards(container, {
163
163
  resourceKind: 'auth.user_acl',
164
164
  resourceId: acl.id,
165
165
  current: acl.updatedAt ?? null,
@@ -1,8 +1,10 @@
1
1
  "use client"
2
2
  import * as React from 'react'
3
+ import { usePathname } from 'next/navigation'
3
4
  import { Page, PageBody } from '@open-mercato/ui/backend/Page'
4
5
  import { CrudForm, type CrudField, type CrudFormGroup } from '@open-mercato/ui/backend/CrudForm'
5
6
  import { apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
7
+ import { buildRecordInjectionContext, useSetCurrentRecordInjectionContext } from '@open-mercato/ui/backend/injection/recordContext'
6
8
  import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
7
9
  import { deleteCrud, updateCrud } from '@open-mercato/ui/backend/utils/crud'
8
10
  import { collectCustomFieldValues } from '@open-mercato/ui/backend/utils/customFieldValues'
@@ -37,6 +39,7 @@ type RoleListResponse = {
37
39
  export default function EditRolePage({ params }: { params?: { id?: string } }) {
38
40
  const id = params?.id
39
41
  const t = useT()
42
+ const pathname = usePathname()
40
43
  const [initial, setInitial] = React.useState<RoleRecord | null>(null)
41
44
  const [loading, setLoading] = React.useState(true)
42
45
  const [aclData, setAclData] = React.useState<AclData>({ isSuperAdmin: false, features: [], organizations: null })
@@ -189,6 +192,20 @@ export default function EditRolePage({ params }: { params?: { id?: string } }) {
189
192
  },
190
193
  ]), [aclData, actorIsSuperAdmin, detailFieldIds, id, initial, loading, selectedTenantId, t])
191
194
 
195
+ // Publish page-load record context to the AppShell-owned `backend:record:current`
196
+ // mount so the enterprise record_locks widget resolves `auth.role` + id explicitly.
197
+ // The resourceKind mirrors the CrudForm `versionHistory` so the held lock matches
198
+ // the save-time conflict surface for the same role.
199
+ useSetCurrentRecordInjectionContext(
200
+ buildRecordInjectionContext({
201
+ resourceKind: 'auth.role',
202
+ resourceId: id || null,
203
+ updatedAt: initial?.updatedAt ?? null,
204
+ data: initial as Record<string, unknown> | null,
205
+ path: pathname,
206
+ }),
207
+ )
208
+
192
209
  if (!id) return null
193
210
  return (
194
211
  <Page>
@@ -1,5 +1,6 @@
1
1
  "use client"
2
2
  import * as React from 'react'
3
+ import { usePathname } from 'next/navigation'
3
4
  import { E } from '#generated/entities.ids.generated'
4
5
  import { Page, PageBody } from '@open-mercato/ui/backend/Page'
5
6
  import { CrudForm, type CrudField, type CrudFormGroup, type CrudFieldOption } from '@open-mercato/ui/backend/CrudForm'
@@ -19,6 +20,7 @@ import { extractCustomFieldEntries } from '@open-mercato/shared/lib/crud/custom-
19
20
  import { formatPasswordRequirements, getPasswordPolicy } from '@open-mercato/shared/lib/auth/passwordPolicy'
20
21
  import { UserConsentsPanel } from '@open-mercato/core/modules/auth/components/UserConsentsPanel'
21
22
  import { RecordNotFoundState, ErrorMessage } from '@open-mercato/ui/backend/detail'
23
+ import { buildRecordInjectionContext, useSetCurrentRecordInjectionContext } from '@open-mercato/ui/backend/injection/recordContext'
22
24
  import { normalizeDisplayNameInput } from '@open-mercato/core/modules/auth/lib/displayName'
23
25
 
24
26
  type EditUserFormValues = {
@@ -123,6 +125,7 @@ function TenantAwareOrganizationSelectInput({
123
125
  export default function EditUserPage({ params }: { params?: { id?: string } }) {
124
126
  const id = params?.id
125
127
  const t = useT()
128
+ const pathname = usePathname()
126
129
  const tRef = React.useRef(t)
127
130
  tRef.current = t
128
131
  const [initialUser, setInitialUser] = React.useState<LoadedUser | null>(null)
@@ -473,6 +476,20 @@ export default function EditUserPage({ params }: { params?: { id?: string } }) {
473
476
  }
474
477
  }, [initialUser, customFieldValues, selectedTenantId])
475
478
 
479
+ // Publish page-load record context to the AppShell-owned `backend:record:current`
480
+ // mount so the enterprise record_locks widget resolves `auth.user` + id explicitly.
481
+ // The resourceKind mirrors the CrudForm `versionHistory` so the held lock matches
482
+ // the save-time conflict surface for the same user.
483
+ useSetCurrentRecordInjectionContext(
484
+ buildRecordInjectionContext({
485
+ resourceKind: 'auth.user',
486
+ resourceId: id || null,
487
+ updatedAt: initialUser?.updatedAt ?? null,
488
+ data: initialUser as Record<string, unknown> | null,
489
+ path: pathname,
490
+ }),
491
+ )
492
+
476
493
  if (isNotFound) {
477
494
  return (
478
495
  <Page>
@@ -273,7 +273,7 @@ export class RoleAcl {
273
273
  @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })
274
274
  createdAt: Date = new Date()
275
275
 
276
- @Property({ name: 'updated_at', type: Date, onUpdate: () => new Date(), nullable: true })
276
+ @Property({ name: 'updated_at', type: Date, onCreate: () => new Date(), onUpdate: () => new Date(), nullable: true })
277
277
  updatedAt?: Date
278
278
 
279
279
  @Property({ name: 'deleted_at', type: Date, nullable: true })
@@ -308,7 +308,7 @@ export class UserAcl {
308
308
  @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })
309
309
  createdAt: Date = new Date()
310
310
 
311
- @Property({ name: 'updated_at', type: Date, onUpdate: () => new Date(), nullable: true })
311
+ @Property({ name: 'updated_at', type: Date, onCreate: () => new Date(), onUpdate: () => new Date(), nullable: true })
312
312
  updatedAt?: Date
313
313
 
314
314
  @Property({ name: 'deleted_at', type: Date, nullable: true })
@@ -5,7 +5,7 @@ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
5
5
  import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
6
6
  import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
7
7
  import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
8
- import { enforceCommandOptimisticLock } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
8
+ import { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
9
9
  import { BusinessRule } from '../../data/entities'
10
10
  import type { EntityManager } from '@mikro-orm/postgresql'
11
11
  import { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'
@@ -280,7 +280,7 @@ export async function PUT(req: Request) {
280
280
  }
281
281
 
282
282
  try {
283
- enforceCommandOptimisticLock({
283
+ await enforceCommandOptimisticLockWithGuards(container, {
284
284
  resourceKind: 'business_rules.rule',
285
285
  resourceId: rule.id,
286
286
  current: rule.updatedAt ?? null,
@@ -338,7 +338,7 @@ export async function DELETE(req: Request) {
338
338
  }
339
339
 
340
340
  try {
341
- enforceCommandOptimisticLock({
341
+ await enforceCommandOptimisticLockWithGuards(container, {
342
342
  resourceKind: 'business_rules.rule',
343
343
  resourceId: rule.id,
344
344
  current: rule.updatedAt ?? null,
@@ -4,7 +4,7 @@ import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
4
4
  import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
5
5
  import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
6
6
  import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
7
- import { enforceCommandOptimisticLock } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
7
+ import { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
8
8
  import { RuleSet } from '../../data/entities'
9
9
  import type { EntityManager } from '@mikro-orm/postgresql'
10
10
  import { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'
@@ -224,7 +224,7 @@ export async function PUT(req: Request) {
224
224
  }
225
225
 
226
226
  try {
227
- enforceCommandOptimisticLock({
227
+ await enforceCommandOptimisticLockWithGuards(container, {
228
228
  resourceKind: 'business_rules.ruleSet',
229
229
  resourceId: ruleSet.id,
230
230
  current: ruleSet.updatedAt ?? null,
@@ -271,7 +271,7 @@ export async function DELETE(req: Request) {
271
271
  }
272
272
 
273
273
  try {
274
- enforceCommandOptimisticLock({
274
+ await enforceCommandOptimisticLockWithGuards(container, {
275
275
  resourceKind: 'business_rules.ruleSet',
276
276
  resourceId: ruleSet.id,
277
277
  current: ruleSet.updatedAt ?? null,
@@ -2,7 +2,7 @@
2
2
 
3
3
  import * as React from 'react'
4
4
  import Link from 'next/link'
5
- import { useRouter, useParams } from 'next/navigation'
5
+ import { useRouter, useParams, usePathname } from 'next/navigation'
6
6
  import { useQuery } from '@tanstack/react-query'
7
7
  import { Page, PageBody } from '@open-mercato/ui/backend/Page'
8
8
  import { CrudForm } from '@open-mercato/ui/backend/CrudForm'
@@ -10,6 +10,7 @@ import { Spinner } from '@open-mercato/ui/primitives/spinner'
10
10
  import { Button } from '@open-mercato/ui/primitives/button'
11
11
  import { apiFetch, withScopedApiHeaders } from '@open-mercato/ui/backend/utils/api'
12
12
  import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
13
+ import { buildRecordInjectionContext, useSetCurrentRecordInjectionContext } from '@open-mercato/ui/backend/injection/recordContext'
13
14
  import { readJsonSafe } from '@open-mercato/ui/backend/utils/serverErrors'
14
15
  import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
15
16
  import { useT } from '@open-mercato/shared/lib/i18n/context'
@@ -27,6 +28,7 @@ import { buildRulePayload, parseRuleToFormValues } from '../../../components/uti
27
28
  export default function EditBusinessRulePage() {
28
29
  const router = useRouter()
29
30
  const params = useParams()
31
+ const pathname = usePathname()
30
32
 
31
33
  // Handle catch-all route: params.slug = ['rules', 'uuid']
32
34
  let ruleId: string | undefined
@@ -103,6 +105,20 @@ export default function EditBusinessRulePage() {
103
105
  [t]
104
106
  )
105
107
 
108
+ // Publish page-load record context to the AppShell-owned `backend:record:current`
109
+ // mount so the enterprise record_locks widget resolves `business_rules.rule` + id
110
+ // explicitly. The resourceKind mirrors the route's `enforceCommandOptimisticLock`
111
+ // call so the held lock matches the save-time conflict surface for the same rule.
112
+ useSetCurrentRecordInjectionContext(
113
+ buildRecordInjectionContext({
114
+ resourceKind: 'business_rules.rule',
115
+ resourceId: ruleId ?? null,
116
+ updatedAt: rule?.updatedAt ?? rule?.updated_at ?? null,
117
+ data: (rule ?? null) as Record<string, unknown> | null,
118
+ path: pathname,
119
+ }),
120
+ )
121
+
106
122
  if (isLoading) {
107
123
  return (
108
124
  <Page>
@@ -2,7 +2,7 @@
2
2
 
3
3
  import * as React from 'react'
4
4
  import Link from 'next/link'
5
- import { useRouter, useParams } from 'next/navigation'
5
+ import { useRouter, useParams, usePathname } from 'next/navigation'
6
6
  import { useQuery, useQueryClient } from '@tanstack/react-query'
7
7
  import { Page, PageBody } from '@open-mercato/ui/backend/Page'
8
8
  import { CrudForm } from '@open-mercato/ui/backend/CrudForm'
@@ -12,6 +12,7 @@ import { Button } from '@open-mercato/ui/primitives/button'
12
12
  import { apiFetch, withScopedApiHeaders } from '@open-mercato/ui/backend/utils/api'
13
13
  import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
14
14
  import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
15
+ import { buildRecordInjectionContext, useSetCurrentRecordInjectionContext } from '@open-mercato/ui/backend/injection/recordContext'
15
16
  import { readJsonSafe } from '@open-mercato/ui/backend/utils/serverErrors'
16
17
  import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
17
18
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
@@ -55,6 +56,7 @@ type RuleSetDetail = {
55
56
  export default function EditRuleSetPage() {
56
57
  const router = useRouter()
57
58
  const params = useParams()
59
+ const pathname = usePathname()
58
60
 
59
61
  // Handle catch-all route: params.slug = ['sets', 'uuid']
60
62
  let setId: string | undefined
@@ -237,6 +239,21 @@ export default function EditRuleSetPage() {
237
239
  ]
238
240
  }, [t, ruleSet, handleAddMember, handleUpdateMember, handleRemoveMember])
239
241
 
242
+ // Publish page-load record context to the AppShell-owned `backend:record:current`
243
+ // mount so the enterprise record_locks widget resolves the rule set + id
244
+ // explicitly. The resourceKind mirrors the route's `enforceCommandOptimisticLock`
245
+ // call (`business_rules.ruleSet`) so the held lock matches the save-time conflict
246
+ // surface for the same rule set.
247
+ useSetCurrentRecordInjectionContext(
248
+ buildRecordInjectionContext({
249
+ resourceKind: 'business_rules.ruleSet',
250
+ resourceId: setId ?? null,
251
+ updatedAt: ruleSet?.updatedAt ?? null,
252
+ data: (ruleSet ?? null) as Record<string, unknown> | null,
253
+ path: pathname,
254
+ }),
255
+ )
256
+
240
257
  if (isLoading) {
241
258
  return (
242
259
  <Page>
@@ -10,6 +10,10 @@ import { CatalogProduct } from '../../data/entities'
10
10
  import { E } from '#generated/entities.ids.generated'
11
11
  import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
12
12
 
13
+ // record_locks decision: this route is read-only (GET only) and projects the
14
+ // attachment side-table for a product. It is EXEMPT from the optimistic-lock /
15
+ // record-lock guard (Phase 4) — there is no mutating method here, and media
16
+ // uploads/deletes are owned by the attachments module's own write paths.
13
17
  export const metadata = {
14
18
  GET: { requireAuth: true, requireFeatures: ['catalog.products.view'] },
15
19
  }
@@ -1,6 +1,7 @@
1
1
  "use client"
2
2
 
3
3
  import * as React from 'react'
4
+ import { usePathname } from 'next/navigation'
4
5
  import { Page, PageBody } from '@open-mercato/ui/backend/Page'
5
6
  import { CrudForm, type CrudField, type CrudFormGroup } from '@open-mercato/ui/backend/CrudForm'
6
7
  import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
@@ -8,6 +9,7 @@ import { updateCrud, deleteCrud } from '@open-mercato/ui/backend/utils/crud'
8
9
  import { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'
9
10
  import { collectCustomFieldValues } from '@open-mercato/ui/backend/utils/customFieldValues'
10
11
  import { ErrorMessage, RecordNotFoundState } from '@open-mercato/ui/backend/detail'
12
+ import { buildRecordInjectionContext, useSetCurrentRecordInjectionContext } from '@open-mercato/ui/backend/injection/recordContext'
11
13
  import { useT } from '@open-mercato/shared/lib/i18n/context'
12
14
  import { extractCustomFieldEntries } from '@open-mercato/shared/lib/crud/custom-fields-client'
13
15
  import { E } from '#generated/entities.ids.generated'
@@ -81,6 +83,7 @@ async function submitCategoryUpdate(
81
83
  export default function EditCatalogCategoryPage({ params }: { params?: { id?: string } }) {
82
84
  const categoryId = params?.id ?? ''
83
85
  const t = useT()
86
+ const pathname = usePathname()
84
87
  const [initialValues, setInitialValues] = React.useState<CategoryFormValues | null>(null)
85
88
  const [pathLabel, setPathLabel] = React.useState<string>('')
86
89
  const [loading, setLoading] = React.useState<boolean>(true)
@@ -195,6 +198,20 @@ export default function EditCatalogCategoryPage({ params }: { params?: { id?: st
195
198
  },
196
199
  ], [t])
197
200
 
201
+ // Publish page-load record context to the AppShell-owned `backend:record:current`
202
+ // mount so the enterprise record_locks widget resolves `catalog.category` + id
203
+ // explicitly. The resourceKind mirrors the CrudForm `versionHistory` so the held
204
+ // lock matches the save-time conflict surface for the same category.
205
+ useSetCurrentRecordInjectionContext(
206
+ buildRecordInjectionContext({
207
+ resourceKind: 'catalog.category',
208
+ resourceId: categoryId || null,
209
+ updatedAt: initialValues?.updatedAt ?? null,
210
+ data: initialValues as Record<string, unknown> | null,
211
+ path: pathname,
212
+ }),
213
+ )
214
+
198
215
  if (!categoryId) {
199
216
  return (
200
217
  <Page>
@@ -2,6 +2,7 @@
2
2
 
3
3
  import * as React from "react";
4
4
  import Link from "next/link";
5
+ import { usePathname } from "next/navigation";
5
6
  import { Page, PageBody } from "@open-mercato/ui/backend/Page";
6
7
  import { ErrorMessage, RecordNotFoundState } from "@open-mercato/ui/backend/detail";
7
8
  import {
@@ -41,6 +42,10 @@ import {
41
42
  } from "@open-mercato/ui/backend/utils/apiCall";
42
43
  import { buildOptimisticLockHeader } from "@open-mercato/ui/backend/utils/optimisticLock";
43
44
  import { surfaceRecordConflict } from "@open-mercato/ui/backend/conflicts";
45
+ import {
46
+ buildRecordInjectionContext,
47
+ useSetCurrentRecordInjectionContext,
48
+ } from "@open-mercato/ui/backend/injection/recordContext";
44
49
  import { useT } from "@open-mercato/shared/lib/i18n/context";
45
50
  import { useConfirmDialog } from "@open-mercato/ui/backend/confirm-dialog";
46
51
  import { E } from "#generated/entities.ids.generated";
@@ -314,6 +319,7 @@ export default function EditCatalogProductPage({
314
319
  }) {
315
320
  const productId = params?.id ? String(params.id) : null;
316
321
  const t = useT();
322
+ const pathname = usePathname();
317
323
  const productSubpathPrefix = productId
318
324
  ? `/backend/catalog/products/${productId}/`
319
325
  : null;
@@ -835,6 +841,21 @@ export default function EditCatalogProductPage({
835
841
  el.scrollIntoView({ behavior: 'smooth', block: 'start' })
836
842
  })
837
843
 
844
+ // Publish page-load record context to the AppShell-owned `backend:record:current`
845
+ // mount so the enterprise record_locks widget resolves `catalog.product` + id
846
+ // explicitly (presence/acquire/heartbeat on load; cleared on unmount). The
847
+ // resourceKind mirrors the CrudForm `versionHistory` so the held lock matches
848
+ // the save-time conflict surface for the same product.
849
+ useSetCurrentRecordInjectionContext(
850
+ buildRecordInjectionContext({
851
+ resourceKind: "catalog.product",
852
+ resourceId: productId,
853
+ updatedAt: initialValues?.updatedAt ?? null,
854
+ data: initialValues as Record<string, unknown> | null,
855
+ path: pathname,
856
+ }),
857
+ );
858
+
838
859
  const handleVariantDeleted = React.useCallback((variantId: string) => {
839
860
  setVariants((prev) => prev.filter((variant) => variant.id !== variantId));
840
861
  }, []);
@@ -1,7 +1,7 @@
1
1
  "use client"
2
2
 
3
3
  import * as React from 'react'
4
- import { useRouter } from 'next/navigation'
4
+ import { useRouter, usePathname } from 'next/navigation'
5
5
  import { Page, PageBody } from '@open-mercato/ui/backend/Page'
6
6
  import { CrudForm, type CrudFormGroup } from '@open-mercato/ui/backend/CrudForm'
7
7
  import { createCrud, updateCrud, deleteCrud } from '@open-mercato/ui/backend/utils/crud'
@@ -10,6 +10,7 @@ import { collectCustomFieldValues } from '@open-mercato/ui/backend/utils/customF
10
10
  import { apiCall, readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
11
11
  import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
12
12
  import { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'
13
+ import { buildRecordInjectionContext, useSetCurrentRecordInjectionContext } from '@open-mercato/ui/backend/injection/recordContext'
13
14
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
14
15
  import { ErrorMessage, RecordNotFoundState } from '@open-mercato/ui/backend/detail'
15
16
  import { useT } from '@open-mercato/shared/lib/i18n/context'
@@ -100,6 +101,7 @@ function resolveVariantPriceLabel(prices: Record<string, VariantPriceDraft> | un
100
101
  export default function EditVariantPage({ params }: { params?: { productId?: string; variantId?: string } }) {
101
102
  const router = useRouter()
102
103
  const t = useT()
104
+ const pathname = usePathname()
103
105
  const productId = params?.productId ? String(params.productId) : null
104
106
  const variantId = params?.variantId ? String(params.variantId) : null
105
107
  const isCreateSentinel = variantId === 'create'
@@ -476,6 +478,19 @@ export default function EditVariantPage({ params }: { params?: { productId?: str
476
478
  return list
477
479
  }, [optionDefinitions, priceKinds, t, taxRates])
478
480
 
481
+ // Publish page-load record context to the AppShell-owned `backend:record:current`
482
+ // mount so the enterprise record_locks widget resolves `catalog.variant` + id
483
+ // explicitly. Skipped on the create-delegation path (no record to lock).
484
+ useSetCurrentRecordInjectionContext(
485
+ buildRecordInjectionContext({
486
+ resourceKind: 'catalog.variant',
487
+ resourceId: isCreateSentinel ? null : variantId,
488
+ updatedAt: initialValues?.updatedAt ?? null,
489
+ data: initialValues as Record<string, unknown> | null,
490
+ path: pathname,
491
+ }),
492
+ )
493
+
479
494
  if (isCreateSentinel) {
480
495
  if (!productId) {
481
496
  return (
@@ -18,7 +18,8 @@ import {
18
18
  } from '@open-mercato/ui/primitives/dialog'
19
19
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
20
20
  import { apiCall, readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
21
- import { buildOptimisticLockHeader, extractOptimisticLockConflict } from '@open-mercato/ui/backend/utils/optimisticLock'
21
+ import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
22
+ import { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'
22
23
  import { raiseCrudError } from '@open-mercato/ui/backend/utils/serverErrors'
23
24
  import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
24
25
  import { useT } from '@open-mercato/shared/lib/i18n/context'
@@ -236,9 +237,15 @@ export function PriceKindSettings() {
236
237
  await loadItems()
237
238
  } catch (err) {
238
239
  console.error('catalog.price-kinds.save failed', err)
239
- const message = extractOptimisticLockConflict(err)
240
- ? t('ui.forms.flash.recordModified', 'This record was modified by someone else. Refresh and try again.')
241
- : err instanceof Error ? err.message : t('catalog.priceKinds.errors.save', 'Failed to save price kind.')
240
+ // Route a concurrent-edit 409 through the single conflict surface (unified
241
+ // conflict bar, or the enterprise merge dialog when its handler is mounted)
242
+ // and close the editor so that surface owns the resolution. Other errors
243
+ // stay inline in the dialog so the user can correct and resubmit.
244
+ if (surfaceRecordConflict(err, t, { onRefresh: () => { void loadItems() } })) {
245
+ closeDialog()
246
+ return
247
+ }
248
+ const message = err instanceof Error ? err.message : t('catalog.priceKinds.errors.save', 'Failed to save price kind.')
242
249
  setError(message)
243
250
  } finally {
244
251
  setSubmitting(false)
@@ -270,9 +277,10 @@ export function PriceKindSettings() {
270
277
  await loadItems()
271
278
  } catch (err) {
272
279
  console.error('catalog.price-kinds.delete failed', err)
273
- const message = extractOptimisticLockConflict(err)
274
- ? t('ui.forms.flash.recordModified', 'This record was modified by someone else. Refresh and try again.')
275
- : err instanceof Error ? err.message : t('catalog.priceKinds.errors.delete', 'Failed to delete price kind.')
280
+ // Route a concurrent-edit 409 through the single conflict surface; fall
281
+ // back to a flash for any other delete failure.
282
+ if (surfaceRecordConflict(err, t, { onRefresh: () => { void loadItems() } })) return
283
+ const message = err instanceof Error ? err.message : t('catalog.priceKinds.errors.delete', 'Failed to delete price kind.')
276
284
  flash(message, 'error')
277
285
  }
278
286
  },
@@ -82,6 +82,12 @@ export async function deleteCatalogProductsWithProgress(params: {
82
82
  let affectedCount = 0
83
83
  const deletedIds = new Set<string>()
84
84
 
85
+ // record_locks decision: bulk product delete is EXEMPT from the optimistic-lock
86
+ // / record-lock guard (Phase 4). It is an explicit destructive operation over a
87
+ // multi-selection, not a collaborative field edit, and the queued job has no
88
+ // per-row expected `updated_at` to compare against — mirroring the customers
89
+ // bulk pattern from Phase 2. Single-product delete on the detail screen still
90
+ // rides the CRUD mutation-guard decorator with the version header.
85
91
  for (const [index, id] of ids.entries()) {
86
92
  await commandBus.execute<{ body?: Record<string, unknown> }, { productId: string }>('catalog.products.delete', {
87
93
  input: { body: { id } },
@@ -1,14 +1,16 @@
1
1
  'use client'
2
2
 
3
3
  import * as React from 'react'
4
- import { useRouter, useParams } from 'next/navigation'
4
+ import { useRouter, useParams, usePathname } from 'next/navigation'
5
5
  import { Page, PageBody } from '@open-mercato/ui/backend/Page'
6
6
  import { CrudForm, type CrudFormGroup } from '@open-mercato/ui/backend/CrudForm'
7
7
  import { updateCrud, deleteCrud } from '@open-mercato/ui/backend/utils/crud'
8
8
  import { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'
9
9
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
10
10
  import { apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
11
- import { buildOptimisticLockHeader, extractOptimisticLockConflict } from '@open-mercato/ui/backend/utils/optimisticLock'
11
+ import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
12
+ import { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'
13
+ import { buildRecordInjectionContext, useSetCurrentRecordInjectionContext } from '@open-mercato/ui/backend/injection/recordContext'
12
14
  import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
13
15
  import { useT } from '@open-mercato/shared/lib/i18n/context'
14
16
  import { SendObjectMessageDialog } from '@open-mercato/ui/backend/messages'
@@ -35,6 +37,7 @@ type CurrencyData = {
35
37
  export default function EditCurrencyPage({ params }: { params?: { id?: string } }) {
36
38
  const t = useT()
37
39
  const router = useRouter()
40
+ const pathname = usePathname()
38
41
  const { confirm: confirmDialog, ConfirmDialogElement } = useConfirmDialog()
39
42
  const mutationContextId = 'currencies-edit:delete'
40
43
  const { runMutation, retryLastMutation } = useGuardedMutation<{
@@ -185,11 +188,25 @@ export default function EditCurrencyPage({ params }: { params?: { id?: string }
185
188
  flash(t('currencies.flash.deleted'), 'success')
186
189
  router.push('/backend/currencies')
187
190
  } catch (error) {
188
- if (extractOptimisticLockConflict(error)) return
191
+ if (surfaceRecordConflict(error, t)) return
189
192
  flash(t('currencies.flash.deleteError'), 'error')
190
193
  }
191
194
  }, [currency, t, router, confirmDialog, mutationContextId, retryLastMutation, runMutation])
192
195
 
196
+ // Publish page-load record context to the AppShell-owned `backend:record:current`
197
+ // mount so the enterprise record_locks widget resolves `currencies.currency` + id
198
+ // explicitly. The resourceKind mirrors the CrudForm `versionHistory` so the held
199
+ // lock matches the save-time conflict surface for the same currency.
200
+ useSetCurrentRecordInjectionContext(
201
+ buildRecordInjectionContext({
202
+ resourceKind: 'currencies.currency',
203
+ resourceId: currency?.id ?? null,
204
+ updatedAt: currency?.updatedAt ?? currency?.updated_at ?? null,
205
+ data: currency as Record<string, unknown> | null,
206
+ path: pathname,
207
+ }),
208
+ )
209
+
193
210
  if (loading) {
194
211
  return (
195
212
  <Page>
@@ -14,6 +14,7 @@ import { Plus, Star } from 'lucide-react'
14
14
  import { useT } from '@open-mercato/shared/lib/i18n/context'
15
15
  import { apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
16
16
  import { buildOptimisticLockHeader, extractOptimisticLockConflict } from '@open-mercato/ui/backend/utils/optimisticLock'
17
+ import { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'
17
18
  import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
18
19
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
19
20
  import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
@@ -188,7 +189,7 @@ export default function CurrenciesPage() {
188
189
  flash(t('currencies.flash.deleted'), 'success')
189
190
  setReloadToken((token) => token + 1)
190
191
  } catch (error) {
191
- if (extractOptimisticLockConflict(error)) return
192
+ if (surfaceRecordConflict(error, t, { onRefresh: () => setReloadToken((token) => token + 1) })) return
192
193
  flash(t('currencies.flash.deleteError'), 'error')
193
194
  }
194
195
  },
@@ -1,13 +1,14 @@
1
1
  'use client'
2
2
 
3
3
  import * as React from 'react'
4
- import { useRouter } from 'next/navigation'
4
+ import { useRouter, usePathname } from 'next/navigation'
5
5
  import { Page, PageBody } from '@open-mercato/ui/backend/Page'
6
6
  import { LoadingMessage, ErrorMessage, RecordNotFoundState } from '@open-mercato/ui/backend/detail'
7
7
  import { CrudForm } from '@open-mercato/ui/backend/CrudForm'
8
8
  import { updateCrud } from '@open-mercato/ui/backend/utils/crud'
9
9
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
10
10
  import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
11
+ import { buildRecordInjectionContext, useSetCurrentRecordInjectionContext } from '@open-mercato/ui/backend/injection/recordContext'
11
12
  import { useT } from '@open-mercato/shared/lib/i18n/context'
12
13
  import {
13
14
  loadCurrencyOptions,
@@ -47,6 +48,7 @@ type ExchangeRateData = {
47
48
  export default function EditExchangeRatePage({ params }: { params?: { id?: string } }) {
48
49
  const t = useT()
49
50
  const router = useRouter()
51
+ const pathname = usePathname()
50
52
 
51
53
  const [exchangeRate, setExchangeRate] = React.useState<ExchangeRateData | null>(null)
52
54
  const [loading, setLoading] = React.useState(true)
@@ -84,6 +86,20 @@ export default function EditExchangeRatePage({ params }: { params?: { id?: strin
84
86
  [t, loadOptions]
85
87
  )
86
88
 
89
+ // Publish page-load record context to the AppShell-owned `backend:record:current`
90
+ // mount so the enterprise record_locks widget resolves `currencies.exchange_rate`
91
+ // + id explicitly. The resourceKind mirrors the CrudForm `versionHistory` so the
92
+ // held lock matches the save-time conflict surface for the same exchange rate.
93
+ useSetCurrentRecordInjectionContext(
94
+ buildRecordInjectionContext({
95
+ resourceKind: 'currencies.exchange_rate',
96
+ resourceId: exchangeRate?.id ?? null,
97
+ updatedAt: exchangeRate?.updatedAt ?? exchangeRate?.updated_at ?? null,
98
+ data: exchangeRate as Record<string, unknown> | null,
99
+ path: pathname,
100
+ }),
101
+ )
102
+
87
103
  if (loading) {
88
104
  return (
89
105
  <Page>