@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
@@ -21,7 +21,7 @@ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
21
21
  import { resolveRedoSnapshot } from '@open-mercato/shared/lib/commands/redo'
22
22
  import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
23
23
  import {
24
- enforceCommandOptimisticLock,
24
+ enforceCommandOptimisticLockWithGuards,
25
25
  enforceRecordGoneIsConflict,
26
26
  } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
27
27
  import type { CrudIndexerConfig } from '@open-mercato/shared/lib/crud/types'
@@ -218,7 +218,7 @@ const updateJobHistoryCommand: CommandHandler<StaffTeamMemberJobHistoryUpdateInp
218
218
  }
219
219
  ensureTenantScope(ctx, record.tenantId)
220
220
  ensureOrganizationScope(ctx, record.organizationId)
221
- enforceCommandOptimisticLock({
221
+ await enforceCommandOptimisticLockWithGuards(ctx.container, {
222
222
  resourceKind: JOB_HISTORY_LOCK_RESOURCE_KIND,
223
223
  resourceId: record.id,
224
224
  current: record.updatedAt ?? null,
@@ -366,7 +366,7 @@ const deleteJobHistoryCommand: CommandHandler<{ id?: string; updatedAt?: string;
366
366
  }
367
367
  ensureTenantScope(ctx, record.tenantId)
368
368
  ensureOrganizationScope(ctx, record.organizationId)
369
- enforceCommandOptimisticLock({
369
+ await enforceCommandOptimisticLockWithGuards(ctx.container, {
370
370
  resourceKind: JOB_HISTORY_LOCK_RESOURCE_KIND,
371
371
  resourceId: record.id,
372
372
  current: record.updatedAt ?? null,
@@ -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 type { AddressDataAdapter } from '@open-mercato/ui/backend/detail'
5
6
  import type { AddressTypesAdapter } from '@open-mercato/ui/backend/detail'
6
7
  import { loadStaffDictionaryEntries, createStaffDictionaryEntry } from './dictionaries'
@@ -77,6 +78,12 @@ export function createStaffAddressAdapter(translator: Translator): AddressDataAd
77
78
  : typeof record.isPrimary === 'boolean'
78
79
  ? record.isPrimary
79
80
  : false,
81
+ updatedAt:
82
+ typeof record.updated_at === 'string'
83
+ ? record.updated_at
84
+ : typeof record.updatedAt === 'string'
85
+ ? record.updatedAt
86
+ : null,
80
87
  }
81
88
  })
82
89
  .filter((value): value is NonNullable<typeof value> => value !== null)
@@ -85,6 +92,7 @@ export function createStaffAddressAdapter(translator: Translator): AddressDataAd
85
92
  const response = await apiCallOrThrow<Record<string, unknown>>(
86
93
  '/api/staff/addresses',
87
94
  {
95
+ // optimistic-lock-exempt: address create-only (no prior version to compare)
88
96
  method: 'POST',
89
97
  headers: { 'content-type': 'application/json' },
90
98
  body: JSON.stringify({
@@ -97,30 +105,39 @@ export function createStaffAddressAdapter(translator: Translator): AddressDataAd
97
105
  )
98
106
  return response.result ?? {}
99
107
  },
100
- update: async ({ id, payload }) => {
101
- await apiCallOrThrow(
102
- '/api/staff/addresses',
103
- {
104
- method: 'PUT',
105
- headers: { 'content-type': 'application/json' },
106
- body: JSON.stringify({
107
- id,
108
- ...payload,
109
- country: payload.country ? payload.country.toUpperCase() : undefined,
110
- }),
111
- },
112
- { errorMessage: translator('staff.teamMembers.detail.addresses.error', 'Failed to save address.') },
108
+ update: async ({ id, payload, updatedAt }) => {
109
+ // Send the optimistic-lock header (address's loaded updatedAt) so a stale
110
+ // edit fails with a 409; the shared AddressesSection host surfaces it through
111
+ // surfaceRecordConflict instead of silently overwriting.
112
+ await withScopedApiRequestHeaders(
113
+ buildOptimisticLockHeader(updatedAt ?? null),
114
+ () => apiCallOrThrow(
115
+ '/api/staff/addresses',
116
+ {
117
+ method: 'PUT',
118
+ headers: { 'content-type': 'application/json' },
119
+ body: JSON.stringify({
120
+ id,
121
+ ...payload,
122
+ country: payload.country ? payload.country.toUpperCase() : undefined,
123
+ }),
124
+ },
125
+ { errorMessage: translator('staff.teamMembers.detail.addresses.error', 'Failed to save address.') },
126
+ ),
113
127
  )
114
128
  },
115
- delete: async ({ id }) => {
116
- await apiCallOrThrow(
117
- '/api/staff/addresses',
118
- {
119
- method: 'DELETE',
120
- headers: { 'content-type': 'application/json' },
121
- body: JSON.stringify({ id }),
122
- },
123
- { errorMessage: translator('staff.teamMembers.detail.addresses.error', 'Failed to delete address.') },
129
+ delete: async ({ id, updatedAt }) => {
130
+ await withScopedApiRequestHeaders(
131
+ buildOptimisticLockHeader(updatedAt ?? null),
132
+ () => apiCallOrThrow(
133
+ '/api/staff/addresses',
134
+ {
135
+ method: 'DELETE',
136
+ headers: { 'content-type': 'application/json' },
137
+ body: JSON.stringify({ id }),
138
+ },
139
+ { errorMessage: translator('staff.teamMembers.detail.addresses.error', 'Failed to delete address.') },
140
+ ),
124
141
  )
125
142
  },
126
143
  }
@@ -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
@@ -35,29 +36,38 @@ export function createStaffNotesAdapter(translator: Translator): NotesDataAdapte
35
36
  )
36
37
  return response.result ?? {}
37
38
  },
38
- update: async ({ id, patch }) => {
39
+ update: async ({ id, patch, updatedAt }) => {
39
40
  const payload: Record<string, unknown> = { id }
40
41
  if (patch.body !== undefined) payload.body = patch.body
41
42
  if (patch.appearanceIcon !== undefined) payload.appearanceIcon = patch.appearanceIcon
42
43
  if (patch.appearanceColor !== undefined) payload.appearanceColor = patch.appearanceColor
43
- await apiCallOrThrow(
44
- '/api/staff/comments',
45
- {
46
- method: 'PUT',
47
- headers: { 'content-type': 'application/json' },
48
- body: JSON.stringify(payload),
49
- },
50
- { errorMessage: translator('staff.teamMembers.detail.notes.updateError', 'Failed to update note.') },
44
+ // Send the optimistic-lock header (note's loaded updatedAt) so a stale edit
45
+ // fails with a 409; the shared NotesSection host surfaces it through
46
+ // surfaceRecordConflict instead of silently overwriting.
47
+ await withScopedApiRequestHeaders(
48
+ buildOptimisticLockHeader(updatedAt ?? null),
49
+ () => apiCallOrThrow(
50
+ '/api/staff/comments',
51
+ {
52
+ method: 'PUT',
53
+ headers: { 'content-type': 'application/json' },
54
+ body: JSON.stringify(payload),
55
+ },
56
+ { errorMessage: translator('staff.teamMembers.detail.notes.updateError', 'Failed to update note.') },
57
+ ),
51
58
  )
52
59
  },
53
- delete: async ({ id }) => {
54
- await apiCallOrThrow(
55
- `/api/staff/comments?id=${encodeURIComponent(id)}`,
56
- {
57
- method: 'DELETE',
58
- headers: { 'content-type': 'application/json' },
59
- },
60
- { errorMessage: translator('staff.teamMembers.detail.notes.deleteError', 'Failed to delete note') },
60
+ delete: async ({ id, updatedAt }) => {
61
+ await withScopedApiRequestHeaders(
62
+ buildOptimisticLockHeader(updatedAt ?? null),
63
+ () => apiCallOrThrow(
64
+ `/api/staff/comments?id=${encodeURIComponent(id)}`,
65
+ {
66
+ method: 'DELETE',
67
+ headers: { 'content-type': 'application/json' },
68
+ },
69
+ { errorMessage: translator('staff.teamMembers.detail.notes.deleteError', 'Failed to delete note') },
70
+ ),
61
71
  )
62
72
  },
63
73
  }
@@ -10,8 +10,44 @@ import {
10
10
  } from '@open-mercato/shared/lib/crud/mutation-guard'
11
11
  import { CommandBus } from '@open-mercato/shared/lib/commands'
12
12
  import { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'
13
+ import { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
13
14
  import type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
14
15
 
16
+ const TRANSLATION_RESOURCE_KIND = 'translations.translation'
17
+
18
+ /**
19
+ * Load the existing translation row's primary key + `updated_at` (the row's own
20
+ * version), scoped to tenant/org. Returns `null` when no row exists yet.
21
+ *
22
+ * Optimistic locking enforces against the TRANSLATION ROW'S OWN version, not the
23
+ * host entity's: the host's EAV `entityType` (`module:entity`) cannot be cleanly
24
+ * mapped to a registered optimistic-lock reader key (those are derived from the
25
+ * host module's ORM entity name / events config, e.g. `CatalogProduct` →
26
+ * `catalog.product`, which does not equal `canonicalizeResourceTag('catalog:catalog_product')`),
27
+ * so there is no reliable server-side path to resolve the host's current version
28
+ * for an arbitrary `entityType`. Guarding the translation row's own `updated_at`
29
+ * closes the no-lock hole with real server-side enforcement and no cross-module
30
+ * coupling (mirrors the hand-written `auth.role_acl` route).
31
+ */
32
+ async function loadTranslationRowVersion(
33
+ db: any,
34
+ entityType: string,
35
+ entityId: string,
36
+ tenantId: string,
37
+ organizationId: string | null,
38
+ ): Promise<{ id: string; updatedAt: Date | string | null } | null> {
39
+ const row = await db
40
+ .selectFrom('entity_translations')
41
+ .select(['id', 'updated_at'])
42
+ .where('entity_type', '=', entityType)
43
+ .where('entity_id', '=', entityId)
44
+ .where(sql<boolean>`tenant_id is not distinct from ${tenantId}`)
45
+ .where(sql<boolean>`organization_id is not distinct from ${organizationId}`)
46
+ .executeTakeFirst() as { id: string; updated_at: Date | string | null } | undefined
47
+ if (!row) return null
48
+ return { id: row.id, updatedAt: row.updated_at ?? null }
49
+ }
50
+
15
51
  const paramsSchema = z.object({
16
52
  entityType: entityTypeParamSchema,
17
53
  entityId: entityIdParamSchema,
@@ -100,6 +136,26 @@ export async function PUT(req: Request, ctx: { params?: { entityType?: string; e
100
136
  return NextResponse.json(guardResult.body, { status: guardResult.status })
101
137
  }
102
138
 
139
+ // Optimistic lock: refuse a stale standalone PUT so a translation save that
140
+ // started from an out-of-date view cannot silently clobber a concurrent edit.
141
+ // Strictly additive — no expected-version header → no-op. Skipped when no row
142
+ // exists yet (first save has no prior version to conflict with).
143
+ const existingVersion = await loadTranslationRowVersion(
144
+ context.db as any,
145
+ entityType,
146
+ entityId,
147
+ context.tenantId,
148
+ context.organizationId,
149
+ )
150
+ if (existingVersion) {
151
+ await enforceCommandOptimisticLockWithGuards(context.container, {
152
+ resourceKind: TRANSLATION_RESOURCE_KIND,
153
+ resourceId: existingVersion.id,
154
+ current: existingVersion.updatedAt,
155
+ request: req,
156
+ })
157
+ }
158
+
103
159
  const commandBus = context.container.resolve('commandBus') as CommandBus
104
160
  const { result, logEntry } = await commandBus.execute<
105
161
  { entityType: string; entityId: string; translations: typeof translations; organizationId: string | null; tenantId: string },
@@ -200,6 +256,24 @@ export async function DELETE(req: Request, ctx: { params?: { entityType?: string
200
256
  return NextResponse.json(guardResult.body, { status: guardResult.status })
201
257
  }
202
258
 
259
+ // Optimistic lock: refuse a stale standalone DELETE (same hole as PUT).
260
+ // Additive — no header → no-op; skipped when no row exists.
261
+ const existingVersion = await loadTranslationRowVersion(
262
+ context.db as any,
263
+ entityType,
264
+ entityId,
265
+ context.tenantId,
266
+ context.organizationId,
267
+ )
268
+ if (existingVersion) {
269
+ await enforceCommandOptimisticLockWithGuards(context.container, {
270
+ resourceKind: TRANSLATION_RESOURCE_KIND,
271
+ resourceId: existingVersion.id,
272
+ current: existingVersion.updatedAt,
273
+ request: req,
274
+ })
275
+ }
276
+
203
277
  const commandBus = context.container.resolve('commandBus') as CommandBus
204
278
  const { logEntry } = await commandBus.execute<
205
279
  { entityType: string; entityId: string; organizationId: string | null; tenantId: string },
@@ -11,6 +11,7 @@ import { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail'
11
11
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
12
12
  import { apiCall, readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
13
13
  import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
14
+ import { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'
14
15
  import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
15
16
  import { useCustomFieldDefs } from '@open-mercato/ui/backend/utils/customFieldDefs'
16
17
  import { Save, Plus, X } from 'lucide-react'
@@ -194,6 +195,17 @@ export function TranslationManager({
194
195
  },
195
196
  })
196
197
 
198
+ // Optimistic lock keys off the TRANSLATION ROW'S OWN version (`updatedAt` from
199
+ // the GET response), not the host entity's: the host's EAV `entityType`
200
+ // (`module:entity`) has no reliable server-side mapping to a registered
201
+ // optimistic-lock reader, so the route enforces against the translation row's
202
+ // own `updated_at`. `null` for a brand-new translation (no existing row → the
203
+ // header is omitted and the route enforces nothing on insert).
204
+ const translationRowUpdatedAt = React.useMemo(() => {
205
+ const value = translationData?.updatedAt
206
+ return typeof value === 'string' && value.trim().length > 0 ? value : null
207
+ }, [translationData])
208
+
197
209
  const translationSignature = React.useMemo(() => JSON.stringify(translationData ?? null), [translationData])
198
210
  const lastTranslationSignatureRef = React.useRef<string | null>(null)
199
211
 
@@ -258,7 +270,7 @@ export function TranslationManager({
258
270
  return runMutation({
259
271
  operation: async () => {
260
272
  const res = await withScopedApiRequestHeaders(
261
- buildOptimisticLockHeader(translationData?.updatedAt),
273
+ buildOptimisticLockHeader(translationRowUpdatedAt),
262
274
  () => apiCall(
263
275
  `/api/translations/${encodeURIComponent(entityType)}/${encodeURIComponent(recordId)}`,
264
276
  {
@@ -292,6 +304,7 @@ export function TranslationManager({
292
304
  void refetchTranslation()
293
305
  },
294
306
  onError: (err: unknown) => {
307
+ if (surfaceRecordConflict(err, t)) return
295
308
  const message = err instanceof Error ? err.message : t('translations.manager.errors.save', 'Failed to save translations')
296
309
  flash(message, 'error')
297
310
  },
@@ -24,7 +24,7 @@ import { getCodeWorkflow } from '../../../lib/code-registry'
24
24
  import { createGenericOptimisticLockReader } from '@open-mercato/shared/lib/crud/optimistic-lock'
25
25
  import { registerOptimisticLockReaderIfAbsent } from '@open-mercato/shared/lib/crud/optimistic-lock-store'
26
26
  import { validateCrudMutationGuard, runCrudMutationGuardAfterSuccess } from '@open-mercato/shared/lib/crud/mutation-guard'
27
- import { enforceCommandOptimisticLock } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
27
+ import { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
28
28
  import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
29
29
 
30
30
  registerOptimisticLockReaderIfAbsent({
@@ -201,7 +201,7 @@ export async function PUT(
201
201
  let savedOverride: WorkflowDefinition
202
202
  if (existingOverride) {
203
203
  try {
204
- enforceCommandOptimisticLock({
204
+ await enforceCommandOptimisticLockWithGuards(container, {
205
205
  resourceKind: 'workflows.definition',
206
206
  resourceId: existingOverride.id,
207
207
  current: existingOverride.updatedAt ?? null,
@@ -308,7 +308,7 @@ export async function PUT(
308
308
  }
309
309
 
310
310
  try {
311
- enforceCommandOptimisticLock({
311
+ await enforceCommandOptimisticLockWithGuards(container, {
312
312
  resourceKind: 'workflows.definition',
313
313
  resourceId: definition.id,
314
314
  current: definition.updatedAt ?? null,
@@ -1,7 +1,7 @@
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 { useQuery } from '@tanstack/react-query'
6
6
  import { Page, PageBody } from '@open-mercato/ui/backend/Page'
7
7
  import { CrudForm } from '@open-mercato/ui/backend/CrudForm'
@@ -10,6 +10,7 @@ import { Button } from '@open-mercato/ui/primitives/button'
10
10
  import { Alert, AlertDescription } from '@open-mercato/ui/primitives/alert'
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 { ErrorMessage, RecordNotFoundState } from '@open-mercato/ui/backend/detail'
14
15
  import { readJsonSafe } from '@open-mercato/ui/backend/utils/serverErrors'
15
16
  import { formatWorkflowValidationError } from '../../../lib/format-validation-error'
@@ -35,6 +36,7 @@ import type { WorkflowDefinitionTrigger } from '../../../data/entities'
35
36
  export default function EditWorkflowDefinitionPage() {
36
37
  const router = useRouter()
37
38
  const params = useParams()
39
+ const pathname = usePathname()
38
40
  const t = useT()
39
41
  const isMobile = useIsMobile()
40
42
 
@@ -221,6 +223,21 @@ export default function EditWorkflowDefinitionPage() {
221
223
  router.push(`/backend/definitions/visual-editor?id=${definitionId}`)
222
224
  }, [router, definitionId])
223
225
 
226
+ // Publish page-load record context to the AppShell-owned `backend:record:current`
227
+ // mount so the enterprise record_locks widget resolves `workflows.definition` + id
228
+ // explicitly. The resourceKind mirrors the save-time `useGuardedMutation` context
229
+ // and server guard so the held lock matches the conflict surface for the same
230
+ // definition across both the form and visual editors.
231
+ useSetCurrentRecordInjectionContext(
232
+ buildRecordInjectionContext({
233
+ resourceKind: 'workflows.definition',
234
+ resourceId: definitionId ?? null,
235
+ updatedAt: typeof definition?.updatedAt === 'string' ? definition.updatedAt : null,
236
+ data: (definition ?? null) as Record<string, unknown> | null,
237
+ path: pathname,
238
+ }),
239
+ )
240
+
224
241
  if (isLoading) {
225
242
  return (
226
243
  <Page>
@@ -8,7 +8,7 @@ import { NodeEditDialogCrudForm } from '../../../components/NodeEditDialogCrudFo
8
8
  import { EdgeEditDialogCrudForm } from '../../../components/EdgeEditDialogCrudForm'
9
9
  import type { Node, Edge, Connection } from '@xyflow/react'
10
10
  import { useState, useCallback, useEffect } from 'react'
11
- import { useRouter, useSearchParams } from 'next/navigation'
11
+ import { useRouter, useSearchParams, usePathname } from 'next/navigation'
12
12
  import { graphToDefinition, definitionToGraph, validateWorkflowGraph, generateStepId, generateTransitionId, appendWorkflowEdge, ValidationError } from '../../../lib/graph-utils'
13
13
  import { performDeleteEdgeFlow, performDeleteNodeFlow } from '../../../lib/visual-editor-delete-flow'
14
14
  import { workflowDefinitionDataSchema } from '../../../data/validators'
@@ -35,6 +35,7 @@ import { FormHeader } from '@open-mercato/ui/backend/forms'
35
35
  import { apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
36
36
  import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
37
37
  import { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'
38
+ import { buildRecordInjectionContext, useSetCurrentRecordInjectionContext } from '@open-mercato/ui/backend/injection/recordContext'
38
39
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
39
40
  import { CircleQuestionMark, PanelTopClose, PanelTopOpen, Play, Save, Trash2 } from 'lucide-react'
40
41
  import { NODE_TYPE_ICONS, NODE_TYPE_COLORS, NODE_TYPE_LABELS } from '../../../lib/node-type-icons'
@@ -61,6 +62,7 @@ export default function VisualEditorPage() {
61
62
  const t = useT()
62
63
  const router = useRouter()
63
64
  const searchParams = useSearchParams()
65
+ const pathname = usePathname()
64
66
  const definitionId = searchParams.get('id')
65
67
  const isMobile = useIsMobile()
66
68
 
@@ -617,6 +619,22 @@ export default function VisualEditorPage() {
617
619
  flash('Canvas cleared', 'success')
618
620
  }, [])
619
621
 
622
+ // Publish page-load record context to the AppShell-owned `backend:record:current`
623
+ // mount so the enterprise record_locks widget resolves `workflows.definition` + id
624
+ // explicitly. This is the highest-value record_locks target (long-lived visual
625
+ // edits): presence holds the lock while the graph is open, and the raw `apiCall`
626
+ // save already routes its 409 through `surfaceRecordConflict`. Cleared on create
627
+ // (no `definitionId`) and on unmount. Mirrors the form edit page's resourceKind so
628
+ // a lock held in either editor surfaces in the other.
629
+ useSetCurrentRecordInjectionContext(
630
+ buildRecordInjectionContext({
631
+ resourceKind: 'workflows.definition',
632
+ resourceId: definitionId,
633
+ updatedAt,
634
+ path: pathname,
635
+ }),
636
+ )
637
+
620
638
  // Show loading spinner while loading definition
621
639
  if (isLoading) {
622
640
  return (