@open-mercato/core 0.6.6-develop.6330.1.a261878aa8 → 0.6.6-develop.6332.1.6e73f0f55b

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 (253) 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/logout.js +2 -3
  4. package/dist/modules/auth/api/logout.js.map +2 -2
  5. package/dist/modules/auth/api/roles/acl/route.js +2 -2
  6. package/dist/modules/auth/api/roles/acl/route.js.map +2 -2
  7. package/dist/modules/auth/api/session/refresh.js +5 -6
  8. package/dist/modules/auth/api/session/refresh.js.map +2 -2
  9. package/dist/modules/auth/api/users/acl/route.js +2 -2
  10. package/dist/modules/auth/api/users/acl/route.js.map +2 -2
  11. package/dist/modules/auth/backend/roles/[id]/edit/page.js +12 -0
  12. package/dist/modules/auth/backend/roles/[id]/edit/page.js.map +2 -2
  13. package/dist/modules/auth/backend/users/[id]/edit/page.js +12 -0
  14. package/dist/modules/auth/backend/users/[id]/edit/page.js.map +2 -2
  15. package/dist/modules/auth/data/entities.js +2 -2
  16. package/dist/modules/auth/data/entities.js.map +2 -2
  17. package/dist/modules/auth/lib/requestRedirect.js +31 -3
  18. package/dist/modules/auth/lib/requestRedirect.js.map +2 -2
  19. package/dist/modules/business_rules/api/rules/route.js +3 -3
  20. package/dist/modules/business_rules/api/rules/route.js.map +2 -2
  21. package/dist/modules/business_rules/api/sets/route.js +3 -3
  22. package/dist/modules/business_rules/api/sets/route.js.map +2 -2
  23. package/dist/modules/business_rules/backend/rules/[id]/page.js +12 -1
  24. package/dist/modules/business_rules/backend/rules/[id]/page.js.map +2 -2
  25. package/dist/modules/business_rules/backend/sets/[id]/page.js +12 -1
  26. package/dist/modules/business_rules/backend/sets/[id]/page.js.map +2 -2
  27. package/dist/modules/catalog/api/product-media/route.js.map +2 -2
  28. package/dist/modules/catalog/backend/catalog/categories/[id]/edit/page.js +12 -0
  29. package/dist/modules/catalog/backend/catalog/categories/[id]/edit/page.js.map +2 -2
  30. package/dist/modules/catalog/backend/catalog/products/[id]/page.js +15 -0
  31. package/dist/modules/catalog/backend/catalog/products/[id]/page.js.map +2 -2
  32. package/dist/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.js +12 -1
  33. package/dist/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.js.map +2 -2
  34. package/dist/modules/catalog/components/PriceKindSettings.js +13 -3
  35. package/dist/modules/catalog/components/PriceKindSettings.js.map +2 -2
  36. package/dist/modules/catalog/lib/bulkDelete.js.map +2 -2
  37. package/dist/modules/currencies/backend/currencies/[id]/page.js +15 -3
  38. package/dist/modules/currencies/backend/currencies/[id]/page.js.map +2 -2
  39. package/dist/modules/currencies/backend/currencies/page.js +2 -1
  40. package/dist/modules/currencies/backend/currencies/page.js.map +2 -2
  41. package/dist/modules/currencies/backend/exchange-rates/[id]/page.js +12 -1
  42. package/dist/modules/currencies/backend/exchange-rates/[id]/page.js.map +2 -2
  43. package/dist/modules/currencies/backend/exchange-rates/page.js +3 -2
  44. package/dist/modules/currencies/backend/exchange-rates/page.js.map +2 -2
  45. package/dist/modules/customer_accounts/api/admin/roles/[id].js +3 -3
  46. package/dist/modules/customer_accounts/api/admin/roles/[id].js.map +2 -2
  47. package/dist/modules/customer_accounts/api/admin/users/[id].js +3 -3
  48. package/dist/modules/customer_accounts/api/admin/users/[id].js.map +2 -2
  49. package/dist/modules/customers/api/addresses/route.js +4 -2
  50. package/dist/modules/customers/api/addresses/route.js.map +2 -2
  51. package/dist/modules/customers/api/tags/route.js +8 -3
  52. package/dist/modules/customers/api/tags/route.js.map +2 -2
  53. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +13 -2
  54. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
  55. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.js +24 -9
  56. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.js.map +2 -2
  57. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealClosure.js +53 -23
  58. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealClosure.js.map +2 -2
  59. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealPipeline.js +13 -3
  60. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealPipeline.js.map +2 -2
  61. package/dist/modules/customers/backend/customers/deals/[id]/page.js +19 -2
  62. package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
  63. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +13 -2
  64. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
  65. package/dist/modules/customers/commands/interactions.js +5 -5
  66. package/dist/modules/customers/commands/interactions.js.map +2 -2
  67. package/dist/modules/customers/commands/pipeline-stages.js +24 -2
  68. package/dist/modules/customers/commands/pipeline-stages.js.map +2 -2
  69. package/dist/modules/customers/commands/pipelines.js +24 -2
  70. package/dist/modules/customers/commands/pipelines.js.map +2 -2
  71. package/dist/modules/customers/components/detail/AddressesSection.js +46 -29
  72. package/dist/modules/customers/components/detail/AddressesSection.js.map +2 -2
  73. package/dist/modules/customers/components/detail/DealForm.js +5 -1
  74. package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
  75. package/dist/modules/customers/components/detail/notesAdapter.js +25 -18
  76. package/dist/modules/customers/components/detail/notesAdapter.js.map +2 -2
  77. package/dist/modules/data_sync/api/schedules/[id]/route.js +38 -19
  78. package/dist/modules/data_sync/api/schedules/[id]/route.js.map +2 -2
  79. package/dist/modules/data_sync/api/schedules/route.js +1 -1
  80. package/dist/modules/data_sync/api/schedules/route.js.map +2 -2
  81. package/dist/modules/data_sync/backend/data-sync/page.js +32 -15
  82. package/dist/modules/data_sync/backend/data-sync/page.js.map +2 -2
  83. package/dist/modules/data_sync/lib/sync-schedule-service.js +19 -5
  84. package/dist/modules/data_sync/lib/sync-schedule-service.js.map +2 -2
  85. package/dist/modules/dictionaries/api/[dictionaryId]/entries/[entryId]/route.js +3 -3
  86. package/dist/modules/dictionaries/api/[dictionaryId]/entries/[entryId]/route.js.map +2 -2
  87. package/dist/modules/dictionaries/api/[dictionaryId]/route.js +3 -3
  88. package/dist/modules/dictionaries/api/[dictionaryId]/route.js.map +2 -2
  89. package/dist/modules/dictionaries/components/DictionariesManager.js +12 -1
  90. package/dist/modules/dictionaries/components/DictionariesManager.js.map +2 -2
  91. package/dist/modules/dictionaries/components/DictionaryEntriesEditor.js.map +2 -2
  92. package/dist/modules/directory/backend/directory/organizations/[id]/edit/page.js +12 -0
  93. package/dist/modules/directory/backend/directory/organizations/[id]/edit/page.js.map +2 -2
  94. package/dist/modules/directory/backend/directory/tenants/[id]/edit/page.js +12 -0
  95. package/dist/modules/directory/backend/directory/tenants/[id]/edit/page.js.map +2 -2
  96. package/dist/modules/entities/api/entities.js +2 -2
  97. package/dist/modules/entities/api/entities.js.map +2 -2
  98. package/dist/modules/entities/api/records.js +7 -5
  99. package/dist/modules/entities/api/records.js.map +2 -2
  100. package/dist/modules/feature_toggles/api/overrides/route.js +2 -2
  101. package/dist/modules/feature_toggles/api/overrides/route.js.map +2 -2
  102. package/dist/modules/feature_toggles/backend/feature-toggles/global/[id]/edit/page.js.map +2 -2
  103. package/dist/modules/feature_toggles/data/entities.js +1 -1
  104. package/dist/modules/feature_toggles/data/entities.js.map +2 -2
  105. package/dist/modules/feature_toggles/data/validators.js +2 -1
  106. package/dist/modules/feature_toggles/data/validators.js.map +2 -2
  107. package/dist/modules/feature_toggles/lib/queries.js +2 -1
  108. package/dist/modules/feature_toggles/lib/queries.js.map +2 -2
  109. package/dist/modules/inbox_ops/api/settings/route.js +2 -2
  110. package/dist/modules/inbox_ops/api/settings/route.js.map +2 -2
  111. package/dist/modules/resources/backend/resources/resource-types/[id]/edit/page.js +12 -1
  112. package/dist/modules/resources/backend/resources/resource-types/[id]/edit/page.js.map +2 -2
  113. package/dist/modules/resources/backend/resources/resources/[id]/page.js +12 -1
  114. package/dist/modules/resources/backend/resources/resources/[id]/page.js.map +2 -2
  115. package/dist/modules/resources/components/detail/notesAdapter.js +25 -18
  116. package/dist/modules/resources/components/detail/notesAdapter.js.map +2 -2
  117. package/dist/modules/sales/backend/sales/documents/[id]/page.js +14 -1
  118. package/dist/modules/sales/backend/sales/documents/[id]/page.js.map +2 -2
  119. package/dist/modules/sales/commands/documents.js +11 -10
  120. package/dist/modules/sales/commands/documents.js.map +2 -2
  121. package/dist/modules/sales/commands/payments.js +6 -1
  122. package/dist/modules/sales/commands/payments.js.map +2 -2
  123. package/dist/modules/sales/commands/returns.js +3 -3
  124. package/dist/modules/sales/commands/returns.js.map +2 -2
  125. package/dist/modules/sales/commands/shared.js +3 -3
  126. package/dist/modules/sales/commands/shared.js.map +2 -2
  127. package/dist/modules/sales/commands/shipments.js +6 -1
  128. package/dist/modules/sales/commands/shipments.js.map +2 -2
  129. package/dist/modules/sales/components/documents/PaymentDialog.js +6 -3
  130. package/dist/modules/sales/components/documents/PaymentDialog.js.map +2 -2
  131. package/dist/modules/sales/components/documents/PaymentsSection.js +7 -3
  132. package/dist/modules/sales/components/documents/PaymentsSection.js.map +2 -2
  133. package/dist/modules/sales/components/documents/ShipmentDialog.js +6 -3
  134. package/dist/modules/sales/components/documents/ShipmentDialog.js.map +2 -2
  135. package/dist/modules/sales/components/documents/ShipmentsSection.js +7 -3
  136. package/dist/modules/sales/components/documents/ShipmentsSection.js.map +2 -2
  137. package/dist/modules/sales/di.js +39 -3
  138. package/dist/modules/sales/di.js.map +2 -2
  139. package/dist/modules/sales/setup.js +2 -0
  140. package/dist/modules/sales/setup.js.map +2 -2
  141. package/dist/modules/staff/backend/staff/leave-requests/[id]/page.js +12 -1
  142. package/dist/modules/staff/backend/staff/leave-requests/[id]/page.js.map +2 -2
  143. package/dist/modules/staff/backend/staff/team-members/[id]/page.js +12 -1
  144. package/dist/modules/staff/backend/staff/team-members/[id]/page.js.map +2 -2
  145. package/dist/modules/staff/backend/staff/team-roles/[id]/edit/page.js +12 -1
  146. package/dist/modules/staff/backend/staff/team-roles/[id]/edit/page.js.map +2 -2
  147. package/dist/modules/staff/backend/staff/teams/[id]/edit/page.js +12 -1
  148. package/dist/modules/staff/backend/staff/teams/[id]/edit/page.js.map +2 -2
  149. package/dist/modules/staff/commands/job-histories.js +3 -3
  150. package/dist/modules/staff/commands/job-histories.js.map +2 -2
  151. package/dist/modules/staff/components/detail/addressesAdapter.js +33 -24
  152. package/dist/modules/staff/components/detail/addressesAdapter.js.map +2 -2
  153. package/dist/modules/staff/components/detail/notesAdapter.js +25 -18
  154. package/dist/modules/staff/components/detail/notesAdapter.js.map +2 -2
  155. package/dist/modules/translations/api/[entityType]/[entityId]/route.js +37 -0
  156. package/dist/modules/translations/api/[entityType]/[entityId]/route.js.map +2 -2
  157. package/dist/modules/translations/components/TranslationManager.js +7 -1
  158. package/dist/modules/translations/components/TranslationManager.js.map +2 -2
  159. package/dist/modules/workflows/api/definitions/[id]/route.js +3 -3
  160. package/dist/modules/workflows/api/definitions/[id]/route.js.map +2 -2
  161. package/dist/modules/workflows/backend/definitions/[id]/page.js +12 -1
  162. package/dist/modules/workflows/backend/definitions/[id]/page.js.map +2 -2
  163. package/dist/modules/workflows/backend/definitions/visual-editor/page.js +11 -1
  164. package/dist/modules/workflows/backend/definitions/visual-editor/page.js.map +2 -2
  165. package/package.json +7 -7
  166. package/src/modules/attachments/components/AttachmentPartitionSettings.tsx +3 -0
  167. package/src/modules/auth/api/logout.ts +2 -3
  168. package/src/modules/auth/api/roles/acl/route.ts +2 -2
  169. package/src/modules/auth/api/session/refresh.ts +5 -6
  170. package/src/modules/auth/api/users/acl/route.ts +2 -2
  171. package/src/modules/auth/backend/roles/[id]/edit/page.tsx +17 -0
  172. package/src/modules/auth/backend/users/[id]/edit/page.tsx +17 -0
  173. package/src/modules/auth/data/entities.ts +2 -2
  174. package/src/modules/auth/lib/requestRedirect.ts +44 -2
  175. package/src/modules/business_rules/api/rules/route.ts +3 -3
  176. package/src/modules/business_rules/api/sets/route.ts +3 -3
  177. package/src/modules/business_rules/backend/rules/[id]/page.tsx +17 -1
  178. package/src/modules/business_rules/backend/sets/[id]/page.tsx +18 -1
  179. package/src/modules/catalog/api/product-media/route.ts +4 -0
  180. package/src/modules/catalog/backend/catalog/categories/[id]/edit/page.tsx +17 -0
  181. package/src/modules/catalog/backend/catalog/products/[id]/page.tsx +21 -0
  182. package/src/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.tsx +16 -1
  183. package/src/modules/catalog/components/PriceKindSettings.tsx +15 -7
  184. package/src/modules/catalog/lib/bulkDelete.ts +6 -0
  185. package/src/modules/currencies/backend/currencies/[id]/page.tsx +20 -3
  186. package/src/modules/currencies/backend/currencies/page.tsx +2 -1
  187. package/src/modules/currencies/backend/exchange-rates/[id]/page.tsx +17 -1
  188. package/src/modules/currencies/backend/exchange-rates/page.tsx +3 -2
  189. package/src/modules/customer_accounts/api/admin/roles/[id].ts +3 -3
  190. package/src/modules/customer_accounts/api/admin/users/[id].ts +3 -3
  191. package/src/modules/customers/api/addresses/route.ts +2 -0
  192. package/src/modules/customers/api/tags/route.ts +6 -1
  193. package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +19 -2
  194. package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.ts +29 -8
  195. package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealClosure.ts +47 -20
  196. package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealPipeline.ts +13 -3
  197. package/src/modules/customers/backend/customers/deals/[id]/page.tsx +21 -1
  198. package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +18 -2
  199. package/src/modules/customers/commands/interactions.ts +5 -5
  200. package/src/modules/customers/commands/pipeline-stages.ts +26 -2
  201. package/src/modules/customers/commands/pipelines.ts +26 -2
  202. package/src/modules/customers/components/detail/AddressesSection.tsx +50 -28
  203. package/src/modules/customers/components/detail/DealForm.tsx +12 -0
  204. package/src/modules/customers/components/detail/notesAdapter.ts +29 -18
  205. package/src/modules/customers/i18n/de.json +2 -0
  206. package/src/modules/customers/i18n/en.json +2 -0
  207. package/src/modules/customers/i18n/es.json +2 -0
  208. package/src/modules/customers/i18n/pl.json +2 -0
  209. package/src/modules/data_sync/api/schedules/[id]/route.ts +38 -20
  210. package/src/modules/data_sync/api/schedules/route.ts +1 -1
  211. package/src/modules/data_sync/backend/data-sync/page.tsx +32 -15
  212. package/src/modules/data_sync/lib/sync-schedule-service.ts +30 -5
  213. package/src/modules/dictionaries/api/[dictionaryId]/entries/[entryId]/route.ts +3 -3
  214. package/src/modules/dictionaries/api/[dictionaryId]/route.ts +3 -3
  215. package/src/modules/dictionaries/components/DictionariesManager.tsx +21 -1
  216. package/src/modules/dictionaries/components/DictionaryEntriesEditor.tsx +2 -0
  217. package/src/modules/directory/backend/directory/organizations/[id]/edit/page.tsx +17 -0
  218. package/src/modules/directory/backend/directory/tenants/[id]/edit/page.tsx +17 -0
  219. package/src/modules/entities/api/entities.ts +2 -2
  220. package/src/modules/entities/api/records.ts +7 -5
  221. package/src/modules/feature_toggles/api/overrides/route.ts +2 -2
  222. package/src/modules/feature_toggles/backend/feature-toggles/global/[id]/edit/page.tsx +12 -0
  223. package/src/modules/feature_toggles/data/entities.ts +1 -1
  224. package/src/modules/feature_toggles/data/validators.ts +1 -0
  225. package/src/modules/feature_toggles/lib/queries.ts +1 -0
  226. package/src/modules/inbox_ops/api/settings/route.ts +2 -2
  227. package/src/modules/resources/backend/resources/resource-types/[id]/edit/page.tsx +17 -1
  228. package/src/modules/resources/backend/resources/resources/[id]/page.tsx +17 -1
  229. package/src/modules/resources/components/detail/notesAdapter.ts +29 -18
  230. package/src/modules/sales/backend/sales/documents/[id]/page.tsx +17 -1
  231. package/src/modules/sales/commands/documents.ts +11 -10
  232. package/src/modules/sales/commands/payments.ts +11 -0
  233. package/src/modules/sales/commands/returns.ts +3 -3
  234. package/src/modules/sales/commands/shared.ts +10 -4
  235. package/src/modules/sales/commands/shipments.ts +14 -0
  236. package/src/modules/sales/components/documents/PaymentDialog.tsx +7 -3
  237. package/src/modules/sales/components/documents/PaymentsSection.tsx +8 -3
  238. package/src/modules/sales/components/documents/ShipmentDialog.tsx +7 -3
  239. package/src/modules/sales/components/documents/ShipmentsSection.tsx +8 -3
  240. package/src/modules/sales/di.ts +68 -9
  241. package/src/modules/sales/setup.ts +2 -0
  242. package/src/modules/staff/backend/staff/leave-requests/[id]/page.tsx +17 -1
  243. package/src/modules/staff/backend/staff/team-members/[id]/page.tsx +17 -1
  244. package/src/modules/staff/backend/staff/team-roles/[id]/edit/page.tsx +17 -1
  245. package/src/modules/staff/backend/staff/teams/[id]/edit/page.tsx +17 -1
  246. package/src/modules/staff/commands/job-histories.ts +3 -3
  247. package/src/modules/staff/components/detail/addressesAdapter.ts +40 -23
  248. package/src/modules/staff/components/detail/notesAdapter.ts +28 -18
  249. package/src/modules/translations/api/[entityType]/[entityId]/route.ts +74 -0
  250. package/src/modules/translations/components/TranslationManager.tsx +14 -1
  251. package/src/modules/workflows/api/definitions/[id]/route.ts +3 -3
  252. package/src/modules/workflows/backend/definitions/[id]/page.tsx +18 -1
  253. package/src/modules/workflows/backend/definitions/visual-editor/page.tsx +19 -1
@@ -1,7 +1,8 @@
1
1
  import { randomUUID } from 'node:crypto'
2
+ import type { AwilixContainer } from 'awilix'
2
3
  import type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'
3
4
  import { findAndCountWithDecryption, findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
4
- import { enforceCommandOptimisticLock } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
5
+ import { enforceCommandOptimisticLockWithGuards, enforceRecordGoneIsConflict } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
5
6
  import { SyncSchedule } from '../data/entities'
6
7
 
7
8
  type SyncScope = {
@@ -131,18 +132,28 @@ export function createSyncScheduleService(em: EntityManager, schedulerService?:
131
132
  fullSync: boolean
132
133
  isEnabled: boolean
133
134
  expectedUpdatedAt?: string | null
134
- }, scope: SyncScope): Promise<SyncSchedule> {
135
+ }, scope: SyncScope, container?: AwilixContainer): Promise<SyncSchedule> {
135
136
  const existing = input.id
136
137
  ? await getById(input.id, scope)
137
138
  : await getByKey(input.integrationId, input.entityType, input.direction, scope)
138
139
 
139
- if (existing) {
140
- enforceCommandOptimisticLock({
140
+ if (existing && container) {
141
+ await enforceCommandOptimisticLockWithGuards(container, {
141
142
  resourceKind: 'data_sync.schedule',
142
143
  resourceId: existing.id,
143
144
  current: existing.updatedAt ?? null,
144
145
  expected: input.expectedUpdatedAt ?? null,
145
146
  })
147
+ } else if (!existing && input.expectedUpdatedAt) {
148
+ // Concurrent delete-then-edit race: the client edited a schedule that was
149
+ // removed before this keyed upsert ran. Surface the unified conflict
150
+ // instead of silently re-creating it. No-op when no expected version was
151
+ // sent (a genuine create) or when OM_OPTIMISTIC_LOCK is off.
152
+ enforceRecordGoneIsConflict({
153
+ resourceKind: 'data_sync.schedule',
154
+ resourceId: input.id ?? `${input.integrationId}:${input.entityType}:${input.direction}`,
155
+ expected: input.expectedUpdatedAt,
156
+ })
146
157
  }
147
158
 
148
159
  const row = existing ?? em.create(SyncSchedule, {
@@ -200,10 +211,24 @@ export function createSyncScheduleService(em: EntityManager, schedulerService?:
200
211
  return row
201
212
  },
202
213
 
203
- async deleteSchedule(id: string, scope: SyncScope): Promise<boolean> {
214
+ async deleteSchedule(
215
+ id: string,
216
+ scope: SyncScope,
217
+ container?: AwilixContainer,
218
+ expectedUpdatedAt?: string | null,
219
+ ): Promise<boolean> {
204
220
  const row = await getById(id, scope)
205
221
  if (!row) return false
206
222
 
223
+ if (container) {
224
+ await enforceCommandOptimisticLockWithGuards(container, {
225
+ resourceKind: 'data_sync.schedule',
226
+ resourceId: row.id,
227
+ current: row.updatedAt ?? null,
228
+ expected: expectedUpdatedAt ?? null,
229
+ })
230
+ }
231
+
207
232
  const scheduledJobId = row.scheduledJobId ?? row.id
208
233
  await requireScheduler().unregister(scheduledJobId)
209
234
 
@@ -4,7 +4,7 @@ import { Dictionary, DictionaryEntry } from '@open-mercato/core/modules/dictiona
4
4
  import { resolveDictionariesRouteContext, resolveDictionaryActorId } from '@open-mercato/core/modules/dictionaries/api/context'
5
5
  import { updateDictionaryEntrySchema } from '@open-mercato/core/modules/dictionaries/data/validators'
6
6
  import { CrudHttpError, 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 {
9
9
  runCrudMutationGuardAfterSuccess,
10
10
  validateCrudMutationGuard,
@@ -76,7 +76,7 @@ export async function PATCH(req: Request, ctx: { params?: { dictionaryId?: strin
76
76
  })
77
77
  const dictionary = await loadDictionary(context, dictionaryId)
78
78
  const entry = await loadEntry(context, dictionary, entryId)
79
- enforceCommandOptimisticLock({
79
+ await enforceCommandOptimisticLockWithGuards(context.container, {
80
80
  resourceKind: 'dictionaries.entry',
81
81
  resourceId: entry.id,
82
82
  current: entry.updatedAt ?? null,
@@ -178,7 +178,7 @@ export async function DELETE(req: Request, ctx: { params?: { dictionaryId?: stri
178
178
  })
179
179
  const dictionary = await loadDictionary(context, dictionaryId)
180
180
  const entry = await loadEntry(context, dictionary, entryId)
181
- enforceCommandOptimisticLock({
181
+ await enforceCommandOptimisticLockWithGuards(context.container, {
182
182
  resourceKind: 'dictionaries.entry',
183
183
  resourceId: entry.id,
184
184
  current: entry.updatedAt ?? null,
@@ -3,7 +3,7 @@ import { z } from 'zod'
3
3
  import { Dictionary } from '@open-mercato/core/modules/dictionaries/data/entities'
4
4
  import { resolveDictionariesRouteContext, resolveDictionaryActorId } from '@open-mercato/core/modules/dictionaries/api/context'
5
5
  import { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
6
- import { enforceCommandOptimisticLock } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
6
+ import { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
7
7
  import {
8
8
  runCrudMutationGuardAfterSuccess,
9
9
  validateCrudMutationGuard,
@@ -115,7 +115,7 @@ export async function PATCH(req: Request, ctx: { params?: { dictionaryId?: strin
115
115
  const payload = updateSchema.parse(await req.json().catch(() => ({})))
116
116
  const dictionary = await loadDictionary(context, dictionaryId)
117
117
 
118
- enforceCommandOptimisticLock({
118
+ await enforceCommandOptimisticLockWithGuards(context.container, {
119
119
  resourceKind: 'dictionaries.dictionary',
120
120
  resourceId: dictionary.id,
121
121
  current: dictionary.updatedAt ?? null,
@@ -236,7 +236,7 @@ export async function DELETE(req: Request, ctx: { params?: { dictionaryId?: stri
236
236
  const { dictionaryId } = paramsSchema.parse({ dictionaryId: ctx.params?.dictionaryId })
237
237
  const dictionary = await loadDictionary(context, dictionaryId)
238
238
 
239
- enforceCommandOptimisticLock({
239
+ await enforceCommandOptimisticLockWithGuards(context.container, {
240
240
  resourceKind: 'dictionaries.dictionary',
241
241
  resourceId: dictionary.id,
242
242
  current: dictionary.updatedAt ?? null,
@@ -1,7 +1,7 @@
1
1
  "use client"
2
2
 
3
3
  import * as React from 'react'
4
- import { useSearchParams } from 'next/navigation'
4
+ import { useSearchParams, usePathname } from 'next/navigation'
5
5
  import { Book, Plus, Pencil, Trash2 } from 'lucide-react'
6
6
  import { EmptyState } from '@open-mercato/ui/primitives/empty-state'
7
7
  import { Button } from '@open-mercato/ui/primitives/button'
@@ -18,6 +18,7 @@ import { flash } from '@open-mercato/ui/backend/FlashMessages'
18
18
  import { apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
19
19
  import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
20
20
  import { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'
21
+ import { buildRecordInjectionContext, useSetCurrentRecordInjectionContext } from '@open-mercato/ui/backend/injection/recordContext'
21
22
  import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
22
23
  import { useT } from '@open-mercato/shared/lib/i18n/context'
23
24
  import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
@@ -58,6 +59,7 @@ export function DictionariesManager() {
58
59
  const t = useT()
59
60
  const { confirm, ConfirmDialogElement } = useConfirmDialog()
60
61
  const searchParams = useSearchParams()
62
+ const pathname = usePathname()
61
63
  const [items, setItems] = React.useState<DictionarySummary[]>([])
62
64
  const [selectedId, setSelectedId] = React.useState<string | null>(null)
63
65
  const [loading, setLoading] = React.useState(true)
@@ -366,6 +368,24 @@ export function DictionariesManager() {
366
368
 
367
369
  const selectedDictionary = items.find((item) => item.id === selectedId) ?? null
368
370
 
371
+ // Publish the open dictionary's record context to the AppShell-owned
372
+ // `backend:record:current` mount so the enterprise record_locks widget resolves
373
+ // `dictionaries.dictionary` + id while the manager is open. Editing here is
374
+ // inline/dialog-driven (not a `[id]` detail route or a CrudForm with a stable
375
+ // `injectionSpotId`), so mounting presence on the manager keyed to the selected
376
+ // dictionary is the simplest place to hold the lock — it mirrors the
377
+ // `dictionaries.dictionary` resourceKind the dictionary PATCH route guards on.
378
+ // Cleared automatically when no dictionary is selected or on unmount.
379
+ useSetCurrentRecordInjectionContext(
380
+ buildRecordInjectionContext({
381
+ resourceKind: 'dictionaries.dictionary',
382
+ resourceId: selectedDictionary?.id ?? null,
383
+ updatedAt: selectedDictionary?.updatedAt ?? null,
384
+ data: (selectedDictionary ?? null) as Record<string, unknown> | null,
385
+ path: pathname,
386
+ }),
387
+ )
388
+
369
389
  return (
370
390
  <div className="grid gap-6 lg:grid-cols-[320px_1fr]">
371
391
  <div className="rounded-lg border bg-card p-4 shadow-sm">
@@ -276,6 +276,8 @@ export function DictionaryEntriesEditor({ dictionaryId, dictionaryName, readOnly
276
276
  await invalidateDictionaryEntries(queryClient, dictionaryId)
277
277
  flash(t('dictionaries.config.entries.success.delete', 'Dictionary entry deleted.'), 'success')
278
278
  } catch (err) {
279
+ // Route a concurrent-edit 409 through the single conflict surface (matching
280
+ // the entry save path); fall back to a flash for any other delete failure.
279
281
  if (surfaceRecordConflict(err, t)) {
280
282
  return
281
283
  }
@@ -1,5 +1,6 @@
1
1
  "use client"
2
2
  import * as React from 'react'
3
+ import { usePathname } from 'next/navigation'
3
4
  import { useT } from '@open-mercato/shared/lib/i18n/context'
4
5
  import { extractCustomFieldEntries } from '@open-mercato/shared/lib/crud/custom-fields-client'
5
6
  import { E } from '#generated/entities.ids.generated'
@@ -17,6 +18,7 @@ import { deleteCrud, updateCrud } from '@open-mercato/ui/backend/utils/crud'
17
18
  import { collectCustomFieldValues } from '@open-mercato/ui/backend/utils/customFieldValues'
18
19
  import { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'
19
20
  import { ErrorMessage, RecordNotFoundState } from '@open-mercato/ui/backend/detail'
21
+ import { buildRecordInjectionContext, useSetCurrentRecordInjectionContext } from '@open-mercato/ui/backend/injection/recordContext'
20
22
 
21
23
  type TreeResponse = {
22
24
  items: OrganizationTreeNode[]
@@ -46,6 +48,7 @@ const TREE_PADDING = 12
46
48
  export default function EditOrganizationPage({ params }: { params?: { id?: string } }) {
47
49
  const orgId = params?.id
48
50
  const t = useT()
51
+ const pathname = usePathname()
49
52
  const [initialValues, setInitialValues] = React.useState<Record<string, unknown> | null>(null)
50
53
  const [pathLabel, setPathLabel] = React.useState<string>('')
51
54
  const [tenantId, setTenantId] = React.useState<string | null>(null)
@@ -270,6 +273,20 @@ export default function EditOrganizationPage({ params }: { params?: { id?: strin
270
273
  { id: 'custom', title: t('directory.organizations.form.group.customFields', 'Custom Data'), column: 2, kind: 'customFields' },
271
274
  ]), [detailFields, t])
272
275
 
276
+ // Publish page-load record context to the AppShell-owned `backend:record:current`
277
+ // mount so the enterprise record_locks widget resolves `directory.organization` + id
278
+ // explicitly. The resourceKind mirrors the CrudForm `versionHistory` so the held
279
+ // lock matches the save-time conflict surface for the same organization.
280
+ useSetCurrentRecordInjectionContext(
281
+ buildRecordInjectionContext({
282
+ resourceKind: 'directory.organization',
283
+ resourceId: orgId || null,
284
+ updatedAt: typeof initialValues?.updatedAt === 'string' ? initialValues.updatedAt : null,
285
+ data: initialValues,
286
+ path: pathname,
287
+ }),
288
+ )
289
+
273
290
  if (!orgId) {
274
291
  return (
275
292
  <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 } from '@open-mercato/ui/backend/CrudForm'
@@ -9,6 +10,7 @@ import { raiseCrudError } from '@open-mercato/ui/backend/utils/serverErrors'
9
10
  import { readApiResultOrThrow, apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
10
11
  import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
11
12
  import { ErrorMessage, RecordNotFoundState } from '@open-mercato/ui/backend/detail'
13
+ import { buildRecordInjectionContext, useSetCurrentRecordInjectionContext } from '@open-mercato/ui/backend/injection/recordContext'
12
14
  import { useT } from '@open-mercato/shared/lib/i18n/context'
13
15
  import { extractCustomFieldEntries } from '@open-mercato/shared/lib/crud/custom-fields-client'
14
16
 
@@ -21,6 +23,7 @@ type TenantFormValues = {
21
23
 
22
24
  export default function EditTenantPage({ params }: { params?: { id?: string } }) {
23
25
  const tenantId = params?.id
26
+ const pathname = usePathname()
24
27
  const [initial, setInitial] = React.useState<TenantFormValues | null>(null)
25
28
  const [loading, setLoading] = React.useState(true)
26
29
  const [error, setError] = React.useState<string | null>(null)
@@ -84,6 +87,20 @@ export default function EditTenantPage({ params }: { params?: { id?: string } })
84
87
  return () => { cancelled = true }
85
88
  }, [tenantId, t])
86
89
 
90
+ // Publish page-load record context to the AppShell-owned `backend:record:current`
91
+ // mount so the enterprise record_locks widget resolves `directory.tenant` + id
92
+ // explicitly. The resourceKind mirrors the CrudForm `versionHistory` so the held
93
+ // lock matches the save-time conflict surface for the same tenant.
94
+ useSetCurrentRecordInjectionContext(
95
+ buildRecordInjectionContext({
96
+ resourceKind: 'directory.tenant',
97
+ resourceId: tenantId || null,
98
+ updatedAt: initial?.updatedAt ?? null,
99
+ data: initial as Record<string, unknown> | null,
100
+ path: pathname,
101
+ }),
102
+ )
103
+
87
104
  if (!tenantId) return null
88
105
 
89
106
  if (isNotFound) {
@@ -8,7 +8,7 @@ import { upsertCustomEntitySchema } from '@open-mercato/core/modules/entities/da
8
8
  import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
9
9
  import { isSystemEntitySelectable } from '@open-mercato/shared/lib/entities/system-entities'
10
10
  import { SYSTEM_ENTITY_RECORDS_BLOCKED_CODE, isOrmBackedSystemEntityId } from '@open-mercato/shared/lib/data/engine'
11
- import { enforceCommandOptimisticLock } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
11
+ import { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
12
12
  import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
13
13
  import {
14
14
  beginEntitiesMutationGuard,
@@ -132,7 +132,7 @@ export async function POST(req: Request) {
132
132
  let ent = await em.findOne(CustomEntity, where)
133
133
  if (ent) {
134
134
  try {
135
- enforceCommandOptimisticLock({
135
+ await enforceCommandOptimisticLockWithGuards(container, {
136
136
  resourceKind: CUSTOM_ENTITY_DEFINITION_RESOURCE_KIND,
137
137
  resourceId: ent.id,
138
138
  current: ent.updatedAt ?? null,
@@ -11,7 +11,7 @@ import { parseBooleanToken, parseBooleanWithDefault } from '@open-mercato/shared
11
11
  import { setRecordCustomFields } from '../lib/helpers'
12
12
  import { CustomFieldValue } from '../data/entities'
13
13
  import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
14
- import { enforceCommandOptimisticLock } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
14
+ import { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
15
15
  import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
16
16
  import { getModules } from '@open-mercato/shared/lib/i18n/server'
17
17
  import { assertEntityAclForRequest } from '../lib/entityAcl'
@@ -460,7 +460,8 @@ export async function PUT(req: Request) {
460
460
  const { entityId, recordId, values } = parsed.data
461
461
 
462
462
  try {
463
- const { resolve } = await createRequestContainer()
463
+ const container = await createRequestContainer()
464
+ const { resolve } = container
464
465
  const de = resolve('dataEngine') as any
465
466
  const em = resolve('em') as any
466
467
  const rbac = resolve('rbacService') as RbacService
@@ -510,7 +511,7 @@ export async function PUT(req: Request) {
510
511
  entityId: rid,
511
512
  organizationId: targetOrgId,
512
513
  })
513
- enforceCommandOptimisticLock({
514
+ await enforceCommandOptimisticLockWithGuards(container, {
514
515
  resourceKind: CUSTOM_ENTITY_RECORD_RESOURCE_KIND,
515
516
  resourceId: rid,
516
517
  current: currentUpdatedAt,
@@ -558,7 +559,8 @@ export async function DELETE(req: Request) {
558
559
  const { entityId, recordId } = parsed.data
559
560
 
560
561
  try {
561
- const { resolve } = await createRequestContainer()
562
+ const container = await createRequestContainer()
563
+ const { resolve } = container
562
564
  const de = resolve('dataEngine') as any
563
565
  const em = resolve('em') as any
564
566
  const rbac = resolve('rbacService') as RbacService
@@ -576,7 +578,7 @@ export async function DELETE(req: Request) {
576
578
  entityId: recordId,
577
579
  organizationId: targetOrgId,
578
580
  })
579
- enforceCommandOptimisticLock({
581
+ await enforceCommandOptimisticLockWithGuards(container, {
580
582
  resourceKind: CUSTOM_ENTITY_RECORD_RESOURCE_KIND,
581
583
  resourceId: recordId,
582
584
  current: currentUpdatedAt,
@@ -5,7 +5,7 @@ import type { CommandBus } from '@open-mercato/shared/lib/commands'
5
5
  import { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'
6
6
  import { getOverrides } from '../../lib/queries'
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 {
10
10
  runCrudMutationGuardAfterSuccess,
11
11
  validateCrudMutationGuard,
@@ -100,7 +100,7 @@ export async function PUT(req: Request) {
100
100
  toggle: { id: parsed.data.toggleId },
101
101
  })
102
102
  if (existingOverride) {
103
- enforceCommandOptimisticLock({
103
+ await enforceCommandOptimisticLockWithGuards(ctx.container, {
104
104
  resourceKind: 'feature_toggles.feature_toggle_override',
105
105
  resourceId: existingOverride.id,
106
106
  current: existingOverride.updatedAt ?? null,
@@ -57,6 +57,18 @@ export default function EditFeatureTogglePage({ params }: { params?: { id?: stri
57
57
  )
58
58
  }
59
59
 
60
+ // record_locks scope decision (Phase 6): the global feature toggle is a
61
+ // NON-TENANT, superadmin-only entity. record_locks enrichment (presence,
62
+ // acquire/heartbeat, conflict upgrade) is tenant-scoped — its decorator skips
63
+ // enrichment when no tenant/resource resolves — so a global (null-tenant) record
64
+ // cannot participate in record_locks. This surface therefore stays OSS-floor
65
+ // guarded ONLY: the CrudForm auto-derives the optimistic-lock header from
66
+ // `optimisticLockUpdatedAt`, the route enforces it via the OSS floor, and a stale
67
+ // save surfaces the unified conflict bar. No `backend:record:current` presence is
68
+ // mounted: it would never resolve a tenant-scoped lock, and this is a
69
+ // single-superadmin surface with no concurrent-editor expectation. Per-tenant
70
+ // override values (`api/global/[id]/override`) ARE tenant-scoped and are enabled
71
+ // in Phase 6b.
60
72
  return (
61
73
  <Page>
62
74
  <PageBody>
@@ -67,7 +67,7 @@ export class FeatureToggleOverride {
67
67
  @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })
68
68
  createdAt: Date = new Date()
69
69
 
70
- @Property({ name: 'updated_at', type: Date, onUpdate: () => new Date() })
70
+ @Property({ name: 'updated_at', type: Date, onCreate: () => new Date(), onUpdate: () => new Date() })
71
71
  updatedAt: Date = new Date()
72
72
 
73
73
  @Property({ name: 'value', type: 'jsonb' })
@@ -104,6 +104,7 @@ export const overrideListResponseSchema = z.object({
104
104
  name: z.string(),
105
105
  category: z.string(),
106
106
  isOverride: z.boolean(),
107
+ updatedAt: z.string().nullable().optional(),
107
108
  });
108
109
 
109
110
  export type OverrideListResponse = z.infer<typeof overrideListResponseSchema>
@@ -100,6 +100,7 @@ export async function getOverrides(
100
100
  name: toggle.name,
101
101
  category: toggle.category ?? '',
102
102
  isOverride: Boolean(override),
103
+ updatedAt: override?.updatedAt instanceof Date ? override.updatedAt.toISOString() : null,
103
104
  }
104
105
  })
105
106
 
@@ -2,7 +2,7 @@ import { NextResponse } from 'next/server'
2
2
  import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
3
3
  import { runWithCacheTenant } from '@open-mercato/cache'
4
4
  import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
5
- import { enforceCommandOptimisticLock } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
5
+ import { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
6
6
  import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
7
7
  import { InboxSettings } from '../../data/entities'
8
8
  import { updateSettingsSchema } from '../../data/validators'
@@ -104,7 +104,7 @@ export async function PATCH(req: Request) {
104
104
  // Optimistic lock: refuse a stale overwrite when two tabs edit the same inbox
105
105
  // settings record. Strictly additive — a no-op without the expected-version header.
106
106
  try {
107
- enforceCommandOptimisticLock({
107
+ await enforceCommandOptimisticLockWithGuards(ctx.container, {
108
108
  resourceKind: 'inbox_ops.settings',
109
109
  resourceId: settings.id,
110
110
  current: settings.updatedAt ?? null,
@@ -1,9 +1,10 @@
1
1
  "use client"
2
2
 
3
3
  import * as React from 'react'
4
- import { useRouter } from 'next/navigation'
4
+ import { usePathname, useRouter } from 'next/navigation'
5
5
  import { Page, PageBody } from '@open-mercato/ui/backend/Page'
6
6
  import { ErrorMessage, RecordNotFoundState } from '@open-mercato/ui/backend/detail'
7
+ import { buildRecordInjectionContext, useSetCurrentRecordInjectionContext } from '@open-mercato/ui/backend/injection/recordContext'
7
8
  import { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
8
9
  import { updateCrud, deleteCrud } from '@open-mercato/ui/backend/utils/crud'
9
10
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
@@ -19,6 +20,7 @@ export default function ResourcesResourceTypeEditPage({ params }: { params?: { i
19
20
  const resourceTypeId = params?.id ?? ''
20
21
  const t = useT()
21
22
  const router = useRouter()
23
+ const pathname = usePathname()
22
24
  const [initialValues, setInitialValues] = React.useState<ResourceTypeFormValues | null>(null)
23
25
  const [loading, setLoading] = React.useState(true)
24
26
  const [error, setError] = React.useState<string | null>(null)
@@ -108,6 +110,20 @@ export default function ResourcesResourceTypeEditPage({ params }: { params?: { i
108
110
  router.push('/backend/resources/resource-types')
109
111
  }, [resourceCount, resourceTypeId, router, t])
110
112
 
113
+ // Publish page-load record context to the AppShell-owned `backend:record:current`
114
+ // mount so the enterprise record_locks widget resolves `resources.resourceType` + id
115
+ // explicitly. The resourceKind mirrors the ResourceTypeCrudForm `versionHistory` so the
116
+ // held lock matches the save-time conflict surface for the same resource type.
117
+ useSetCurrentRecordInjectionContext(
118
+ buildRecordInjectionContext({
119
+ resourceKind: 'resources.resourceType',
120
+ resourceId: resourceTypeId || null,
121
+ updatedAt: initialValues?.updatedAt ?? null,
122
+ data: initialValues as Record<string, unknown> | null,
123
+ path: pathname,
124
+ }),
125
+ )
126
+
111
127
  if (isNotFound) {
112
128
  return (
113
129
  <Page>
@@ -1,7 +1,7 @@
1
1
  "use client"
2
2
 
3
3
  import * as React from 'react'
4
- import { useRouter, useSearchParams } from 'next/navigation'
4
+ import { usePathname, useRouter, useSearchParams } from 'next/navigation'
5
5
  import { Page, PageBody } from '@open-mercato/ui/backend/Page'
6
6
  import { FormHeader } from '@open-mercato/ui/backend/forms'
7
7
  import { Button } from '@open-mercato/ui/primitives/button'
@@ -12,6 +12,7 @@ import { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors
12
12
  import { updateCrud, deleteCrud } from '@open-mercato/ui/backend/utils/crud'
13
13
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
14
14
  import { ActivitiesSection, NotesSection, RecordNotFoundState, type SectionAction, type TagOption } from '@open-mercato/ui/backend/detail'
15
+ import { buildRecordInjectionContext, useSetCurrentRecordInjectionContext } from '@open-mercato/ui/backend/injection/recordContext'
15
16
  import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
16
17
  import { VersionHistoryAction } from '@open-mercato/ui/backend/version-history'
17
18
  import { SendObjectMessageDialog } from '@open-mercato/ui/backend/messages'
@@ -91,6 +92,7 @@ export default function ResourcesResourceDetailPage({ params }: { params?: { id?
91
92
  const t = useT()
92
93
  const detailTranslator = React.useMemo(() => createTranslatorWithFallback(t), [t])
93
94
  const router = useRouter()
95
+ const pathname = usePathname()
94
96
  const searchParams = useSearchParams()
95
97
  const [initialValues, setInitialValues] = React.useState<Record<string, unknown> | null>(null)
96
98
  const [isNotFound, setIsNotFound] = React.useState(false)
@@ -602,6 +604,20 @@ export default function ResourcesResourceDetailPage({ params }: { params?: { id?
602
604
  ? initialValues.name.trim()
603
605
  : t('resources.resources.detail.untitled', 'Unnamed resource')
604
606
 
607
+ // Publish page-load record context to the AppShell-owned `backend:record:current`
608
+ // mount so the enterprise record_locks widget resolves `resources.resource` + id
609
+ // explicitly. The resourceKind mirrors the VersionHistoryAction config / ResourceCrudForm
610
+ // `versionHistory` so the held lock matches the save-time conflict surface for the resource.
611
+ useSetCurrentRecordInjectionContext(
612
+ buildRecordInjectionContext({
613
+ resourceKind: 'resources.resource',
614
+ resourceId: resourceId || null,
615
+ updatedAt: typeof initialValues?.updatedAt === 'string' ? initialValues.updatedAt : null,
616
+ data: initialValues,
617
+ path: pathname,
618
+ }),
619
+ )
620
+
605
621
  if (isNotFound) {
606
622
  return (
607
623
  <Page>
@@ -1,6 +1,7 @@
1
1
  "use client"
2
2
 
3
- import { apiCallOrThrow, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
3
+ import { apiCallOrThrow, readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
4
+ import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
4
5
  import { mapCommentSummary, type NotesDataAdapter } from '@open-mercato/ui/backend/detail/NotesSection'
5
6
 
6
7
  type Translator = (key: string, fallback?: string, params?: Record<string, string | number>) => string
@@ -61,33 +62,43 @@ export function createResourceNotesAdapter(
61
62
  )
62
63
  return response.result ?? {}
63
64
  },
64
- update: async ({ id, patch }) => {
65
+ update: async ({ id, patch, updatedAt }) => {
65
66
  const payload: Record<string, unknown> = { id }
66
67
  if (patch.body !== undefined) payload.body = patch.body
67
68
  if (patch.appearanceIcon !== undefined) payload.appearanceIcon = patch.appearanceIcon
68
69
  if (patch.appearanceColor !== undefined) payload.appearanceColor = patch.appearanceColor
70
+ // Run through the guarded-mutation runner AND send the optimistic-lock header
71
+ // (note's loaded updatedAt) so a stale edit fails with a 409; the shared
72
+ // NotesSection host surfaces it through surfaceRecordConflict instead of
73
+ // silently overwriting.
69
74
  await runWrite(
70
- () => apiCallOrThrow(
71
- '/api/resources/comments',
72
- {
73
- method: 'PUT',
74
- headers: { 'content-type': 'application/json' },
75
- body: JSON.stringify(payload),
76
- },
77
- { errorMessage: translator('resources.resources.detail.notes.updateError', 'Failed to update note.') },
75
+ () => withScopedApiRequestHeaders(
76
+ buildOptimisticLockHeader(updatedAt ?? null),
77
+ () => apiCallOrThrow(
78
+ '/api/resources/comments',
79
+ {
80
+ method: 'PUT',
81
+ headers: { 'content-type': 'application/json' },
82
+ body: JSON.stringify(payload),
83
+ },
84
+ { errorMessage: translator('resources.resources.detail.notes.updateError', 'Failed to update note.') },
85
+ ),
78
86
  ),
79
87
  { operation: 'updateNote', id },
80
88
  )
81
89
  },
82
- delete: async ({ id }) => {
90
+ delete: async ({ id, updatedAt }) => {
83
91
  await runWrite(
84
- () => apiCallOrThrow(
85
- `/api/resources/comments?id=${encodeURIComponent(id)}`,
86
- {
87
- method: 'DELETE',
88
- headers: { 'content-type': 'application/json' },
89
- },
90
- { errorMessage: translator('resources.resources.detail.notes.deleteError', 'Failed to delete note') },
92
+ () => withScopedApiRequestHeaders(
93
+ buildOptimisticLockHeader(updatedAt ?? null),
94
+ () => apiCallOrThrow(
95
+ `/api/resources/comments?id=${encodeURIComponent(id)}`,
96
+ {
97
+ method: 'DELETE',
98
+ headers: { 'content-type': 'application/json' },
99
+ },
100
+ { errorMessage: translator('resources.resources.detail.notes.deleteError', 'Failed to delete note') },
101
+ ),
91
102
  ),
92
103
  { operation: 'deleteNote', id },
93
104
  )
@@ -3,7 +3,7 @@
3
3
  "use client"
4
4
 
5
5
  import * as React from 'react'
6
- import { useSearchParams, useRouter } from 'next/navigation'
6
+ import { useSearchParams, useRouter, usePathname } from 'next/navigation'
7
7
  import { Page, PageBody } from '@open-mercato/ui/backend/Page'
8
8
  import {
9
9
  CustomDataSection,
@@ -68,6 +68,7 @@ import { ICON_SUGGESTIONS } from '@open-mercato/core/modules/customers/lib/dicti
68
68
  import { readMarkdownPreferenceCookie, writeMarkdownPreferenceCookie } from '@open-mercato/core/modules/customers/lib/markdownPreference'
69
69
  import { InjectionSpot, useInjectionWidgets } from '@open-mercato/ui/backend/injection/InjectionSpot'
70
70
  import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
71
+ import { buildRecordInjectionContext, useSetCurrentRecordInjectionContext } from '@open-mercato/ui/backend/injection/recordContext'
71
72
 
72
73
  function formatMessageAmount(amount: number | null | undefined, currency: string | null | undefined): string | null {
73
74
  if (typeof amount !== 'number' || !Number.isFinite(amount)) return null
@@ -1892,6 +1893,7 @@ export default function SalesDocumentDetailPage({
1892
1893
  const t = useT()
1893
1894
  const router = useRouter()
1894
1895
  const searchParams = useSearchParams()
1896
+ const pathname = usePathname()
1895
1897
  const { confirm, ConfirmDialogElement } = useConfirmDialog()
1896
1898
  const [loading, setLoading] = React.useState(true)
1897
1899
  const [isNotFound, setIsNotFound] = React.useState(false)
@@ -1972,6 +1974,18 @@ export default function SalesDocumentDetailPage({
1972
1974
  },
1973
1975
  [detailInjectionContext, runMutation],
1974
1976
  )
1977
+ // Publish page-load record context to the AppShell-owned `backend:record:current`
1978
+ // mount so record_locks gets presence + the action-log base (updatedAt/data) for
1979
+ // this order/quote. Sub-resource sections inherit this context — no second mount.
1980
+ useSetCurrentRecordInjectionContext(
1981
+ buildRecordInjectionContext({
1982
+ resourceKind: `sales.${kind}`,
1983
+ resourceId: record?.id ?? null,
1984
+ updatedAt: record?.updatedAt ?? null,
1985
+ data: record as Record<string, unknown> | null,
1986
+ path: pathname,
1987
+ }),
1988
+ )
1975
1989
  const clearCustomerError = React.useCallback(() => setCustomerError(null), [])
1976
1990
  const { data: currencyDictionary } = useCurrencyDictionary()
1977
1991
  const scopeVersion = useOrganizationScopeVersion()
@@ -4244,6 +4258,7 @@ export default function SalesDocumentDetailPage({
4244
4258
  currencyCode={record.currencyCode ?? null}
4245
4259
  organizationId={(record as any)?.organizationId ?? (record as any)?.organization_id ?? null}
4246
4260
  tenantId={(record as any)?.tenantId ?? (record as any)?.tenant_id ?? null}
4261
+ documentUpdatedAt={record.updatedAt ?? null}
4247
4262
  shippingAddressSnapshot={shippingSnapshot ?? null}
4248
4263
  onActionChange={handleSectionActionChange}
4249
4264
  onAddComment={appendShipmentComment}
@@ -4300,6 +4315,7 @@ export default function SalesDocumentDetailPage({
4300
4315
  currencyCode={record.currencyCode ?? null}
4301
4316
  organizationId={(record as any)?.organizationId ?? (record as any)?.organization_id ?? null}
4302
4317
  tenantId={(record as any)?.tenantId ?? (record as any)?.tenant_id ?? null}
4318
+ documentUpdatedAt={record.updatedAt ?? null}
4303
4319
  onActionChange={handleSectionActionChange}
4304
4320
  onPaymentsChange={(payments) => setHasPayments(payments.length > 0)}
4305
4321
  onTotalsChange={() => {