@open-mercato/core 0.6.6-develop.6205.1.109e4b6a84 → 0.6.6-develop.6228.1.cf483c68b5

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 (205) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/currencies/backend/currencies/[id]/page.js +33 -8
  3. package/dist/modules/currencies/backend/currencies/[id]/page.js.map +2 -2
  4. package/dist/modules/currencies/backend/currencies/page.js +63 -27
  5. package/dist/modules/currencies/backend/currencies/page.js.map +2 -2
  6. package/dist/modules/currencies/backend/exchange-rates/page.js +35 -14
  7. package/dist/modules/currencies/backend/exchange-rates/page.js.map +2 -2
  8. package/dist/modules/currencies/components/CurrencyFetchingConfig.js +104 -41
  9. package/dist/modules/currencies/components/CurrencyFetchingConfig.js.map +2 -2
  10. package/dist/modules/customer_accounts/api/admin/roles/[id]/acl.js +57 -8
  11. package/dist/modules/customer_accounts/api/admin/roles/[id]/acl.js.map +2 -2
  12. package/dist/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.js +5 -2
  13. package/dist/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.js.map +2 -2
  14. package/dist/modules/customer_accounts/backend/customer_accounts/roles/page.js +30 -14
  15. package/dist/modules/customer_accounts/backend/customer_accounts/roles/page.js.map +2 -2
  16. package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js +29 -19
  17. package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js.map +2 -2
  18. package/dist/modules/customers/backend/config/customers/pipeline-stages/page.js +166 -64
  19. package/dist/modules/customers/backend/config/customers/pipeline-stages/page.js.map +2 -2
  20. package/dist/modules/customers/components/AddressFormatSettings.js +31 -16
  21. package/dist/modules/customers/components/AddressFormatSettings.js.map +2 -2
  22. package/dist/modules/customers/components/DictionarySettings.js +57 -31
  23. package/dist/modules/customers/components/DictionarySettings.js.map +2 -2
  24. package/dist/modules/customers/components/PipelineSettings.js +156 -76
  25. package/dist/modules/customers/components/PipelineSettings.js.map +2 -2
  26. package/dist/modules/customers/components/calendar/AgendaList.js +10 -4
  27. package/dist/modules/customers/components/calendar/AgendaList.js.map +2 -2
  28. package/dist/modules/customers/components/calendar/CalendarSettingsModal.js +18 -2
  29. package/dist/modules/customers/components/calendar/CalendarSettingsModal.js.map +2 -2
  30. package/dist/modules/customers/components/calendar/MonthGrid.js +5 -2
  31. package/dist/modules/customers/components/calendar/MonthGrid.js.map +2 -2
  32. package/dist/modules/customers/components/calendar/editor/PeopleField.js +6 -0
  33. package/dist/modules/customers/components/calendar/editor/PeopleField.js.map +2 -2
  34. package/dist/modules/customers/components/calendar/editor/RelatedToField.js +2 -0
  35. package/dist/modules/customers/components/calendar/editor/RelatedToField.js.map +2 -2
  36. package/dist/modules/customers/components/calendar/editor/ScheduleSection.js +4 -1
  37. package/dist/modules/customers/components/calendar/editor/ScheduleSection.js.map +2 -2
  38. package/dist/modules/customers/lib/calendar/labels.js +33 -0
  39. package/dist/modules/customers/lib/calendar/labels.js.map +7 -0
  40. package/dist/modules/dashboards/components/WidgetVisibilityEditor.js +32 -11
  41. package/dist/modules/dashboards/components/WidgetVisibilityEditor.js.map +2 -2
  42. package/dist/modules/data_sync/api/mappings/[id]/route.js +59 -0
  43. package/dist/modules/data_sync/api/mappings/[id]/route.js.map +2 -2
  44. package/dist/modules/data_sync/api/mappings/route.js +44 -0
  45. package/dist/modules/data_sync/api/mappings/route.js.map +2 -2
  46. package/dist/modules/data_sync/api/run.js +31 -0
  47. package/dist/modules/data_sync/api/run.js.map +2 -2
  48. package/dist/modules/data_sync/api/runs/[id]/cancel.js +31 -0
  49. package/dist/modules/data_sync/api/runs/[id]/cancel.js.map +2 -2
  50. package/dist/modules/data_sync/api/runs/[id]/retry.js +31 -0
  51. package/dist/modules/data_sync/api/runs/[id]/retry.js.map +2 -2
  52. package/dist/modules/data_sync/api/schedules/[id]/route.js +59 -3
  53. package/dist/modules/data_sync/api/schedules/[id]/route.js.map +2 -2
  54. package/dist/modules/data_sync/api/schedules/route.js +33 -4
  55. package/dist/modules/data_sync/api/schedules/route.js.map +2 -2
  56. package/dist/modules/dictionaries/fields/dictionary.js +29 -4
  57. package/dist/modules/dictionaries/fields/dictionary.js.map +2 -2
  58. package/dist/modules/directory/api/organization-branding/route.js +54 -2
  59. package/dist/modules/directory/api/organization-branding/route.js.map +2 -2
  60. package/dist/modules/directory/backend/directory/branding/page.js +15 -9
  61. package/dist/modules/directory/backend/directory/branding/page.js.map +2 -2
  62. package/dist/modules/directory/backend/directory/organizations/page.js +28 -9
  63. package/dist/modules/directory/backend/directory/organizations/page.js.map +2 -2
  64. package/dist/modules/directory/backend/directory/tenants/page.js +29 -13
  65. package/dist/modules/directory/backend/directory/tenants/page.js.map +2 -2
  66. package/dist/modules/entities/api/definitions.batch.js +15 -0
  67. package/dist/modules/entities/api/definitions.batch.js.map +2 -2
  68. package/dist/modules/entities/api/definitions.js +26 -0
  69. package/dist/modules/entities/api/definitions.js.map +2 -2
  70. package/dist/modules/entities/api/definitions.mutation-guard.js +57 -0
  71. package/dist/modules/entities/api/definitions.mutation-guard.js.map +7 -0
  72. package/dist/modules/entities/api/definitions.restore.js +15 -0
  73. package/dist/modules/entities/api/definitions.restore.js.map +2 -2
  74. package/dist/modules/entities/api/entities.js +31 -3
  75. package/dist/modules/entities/api/entities.js.map +2 -2
  76. package/dist/modules/entities/api/records.js +18 -0
  77. package/dist/modules/entities/api/records.js.map +2 -2
  78. package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js +28 -10
  79. package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js.map +2 -2
  80. package/dist/modules/feature_toggles/api/overrides/route.js +38 -1
  81. package/dist/modules/feature_toggles/api/overrides/route.js.map +2 -2
  82. package/dist/modules/inbox_ops/api/emails/[id]/route.js +30 -0
  83. package/dist/modules/inbox_ops/api/emails/[id]/route.js.map +2 -2
  84. package/dist/modules/inbox_ops/api/proposals/[id]/accept-all/route.js +30 -0
  85. package/dist/modules/inbox_ops/api/proposals/[id]/accept-all/route.js.map +2 -2
  86. package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/route.js +31 -0
  87. package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/route.js.map +2 -2
  88. package/dist/modules/inbox_ops/api/proposals/[id]/categorize/route.js +31 -0
  89. package/dist/modules/inbox_ops/api/proposals/[id]/categorize/route.js.map +2 -2
  90. package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsActions.js +68 -39
  91. package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsActions.js.map +2 -2
  92. package/dist/modules/notifications/frontend/NotificationSettingsPageClient.js +14 -4
  93. package/dist/modules/notifications/frontend/NotificationSettingsPageClient.js.map +2 -2
  94. package/dist/modules/payment_gateways/api/cancel/route.js +37 -0
  95. package/dist/modules/payment_gateways/api/cancel/route.js.map +2 -2
  96. package/dist/modules/payment_gateways/api/capture/route.js +37 -0
  97. package/dist/modules/payment_gateways/api/capture/route.js.map +2 -2
  98. package/dist/modules/payment_gateways/api/guards.js +31 -0
  99. package/dist/modules/payment_gateways/api/guards.js.map +7 -0
  100. package/dist/modules/payment_gateways/api/refund/route.js +37 -0
  101. package/dist/modules/payment_gateways/api/refund/route.js.map +2 -2
  102. package/dist/modules/payment_gateways/api/sessions/route.js +37 -0
  103. package/dist/modules/payment_gateways/api/sessions/route.js.map +2 -2
  104. package/dist/modules/planner/api/availability-date-specific.js +11 -1
  105. package/dist/modules/planner/api/availability-date-specific.js.map +2 -2
  106. package/dist/modules/planner/backend/planner/availability-rulesets/page.js +20 -3
  107. package/dist/modules/planner/backend/planner/availability-rulesets/page.js.map +2 -2
  108. package/dist/modules/planner/commands/availability-date-specific.js +16 -0
  109. package/dist/modules/planner/commands/availability-date-specific.js.map +2 -2
  110. package/dist/modules/planner/components/AvailabilityRulesEditor.js +109 -40
  111. package/dist/modules/planner/components/AvailabilityRulesEditor.js.map +2 -2
  112. package/dist/modules/query_index/api/purge.js +35 -3
  113. package/dist/modules/query_index/api/purge.js.map +2 -2
  114. package/dist/modules/query_index/api/reindex.js +41 -3
  115. package/dist/modules/query_index/api/reindex.js.map +2 -2
  116. package/dist/modules/query_index/components/QueryIndexesTable.js +57 -24
  117. package/dist/modules/query_index/components/QueryIndexesTable.js.map +2 -2
  118. package/dist/modules/shipping_carriers/api/shipments/route.js +31 -0
  119. package/dist/modules/shipping_carriers/api/shipments/route.js.map +2 -2
  120. package/dist/modules/shipping_carriers/api/tracking/refresh/route.js +55 -0
  121. package/dist/modules/shipping_carriers/api/tracking/refresh/route.js.map +7 -0
  122. package/dist/modules/shipping_carriers/data/validators.js +6 -1
  123. package/dist/modules/shipping_carriers/data/validators.js.map +2 -2
  124. package/dist/modules/shipping_carriers/lib/shipment-wizard/hooks/useShipmentWizard.js +28 -8
  125. package/dist/modules/shipping_carriers/lib/shipment-wizard/hooks/useShipmentWizard.js.map +2 -2
  126. package/dist/modules/shipping_carriers/lib/shipping-service.js +37 -7
  127. package/dist/modules/shipping_carriers/lib/shipping-service.js.map +2 -2
  128. package/dist/modules/shipping_carriers/workers/status-poller.js +1 -1
  129. package/dist/modules/shipping_carriers/workers/status-poller.js.map +2 -2
  130. package/dist/modules/translations/components/TranslationManager.js +49 -20
  131. package/dist/modules/translations/components/TranslationManager.js.map +2 -2
  132. package/package.json +7 -7
  133. package/src/modules/currencies/backend/currencies/[id]/page.tsx +40 -10
  134. package/src/modules/currencies/backend/currencies/page.tsx +68 -29
  135. package/src/modules/currencies/backend/exchange-rates/page.tsx +40 -15
  136. package/src/modules/currencies/components/CurrencyFetchingConfig.tsx +110 -41
  137. package/src/modules/customer_accounts/api/admin/roles/[id]/acl.ts +69 -7
  138. package/src/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.tsx +16 -8
  139. package/src/modules/customer_accounts/backend/customer_accounts/roles/page.tsx +32 -14
  140. package/src/modules/customer_accounts/widgets/injection/account-status/widget.client.tsx +32 -20
  141. package/src/modules/customers/backend/config/customers/pipeline-stages/page.tsx +171 -64
  142. package/src/modules/customers/components/AddressFormatSettings.tsx +39 -19
  143. package/src/modules/customers/components/DictionarySettings.tsx +63 -29
  144. package/src/modules/customers/components/PipelineSettings.tsx +165 -80
  145. package/src/modules/customers/components/calendar/AgendaList.tsx +13 -8
  146. package/src/modules/customers/components/calendar/CalendarSettingsModal.tsx +16 -2
  147. package/src/modules/customers/components/calendar/MonthGrid.tsx +5 -2
  148. package/src/modules/customers/components/calendar/editor/PeopleField.tsx +6 -0
  149. package/src/modules/customers/components/calendar/editor/RelatedToField.tsx +2 -0
  150. package/src/modules/customers/components/calendar/editor/ScheduleSection.tsx +7 -0
  151. package/src/modules/customers/i18n/de.json +11 -7
  152. package/src/modules/customers/i18n/en.json +6 -2
  153. package/src/modules/customers/i18n/es.json +11 -7
  154. package/src/modules/customers/i18n/pl.json +11 -7
  155. package/src/modules/customers/lib/calendar/labels.ts +42 -0
  156. package/src/modules/dashboards/components/WidgetVisibilityEditor.tsx +39 -11
  157. package/src/modules/data_sync/api/mappings/[id]/route.ts +63 -0
  158. package/src/modules/data_sync/api/mappings/route.ts +48 -0
  159. package/src/modules/data_sync/api/run.ts +33 -0
  160. package/src/modules/data_sync/api/runs/[id]/cancel.ts +34 -0
  161. package/src/modules/data_sync/api/runs/[id]/retry.ts +33 -0
  162. package/src/modules/data_sync/api/schedules/[id]/route.ts +64 -2
  163. package/src/modules/data_sync/api/schedules/route.ts +36 -4
  164. package/src/modules/dictionaries/fields/dictionary.tsx +31 -8
  165. package/src/modules/dictionaries/i18n/de.json +1 -0
  166. package/src/modules/dictionaries/i18n/en.json +1 -0
  167. package/src/modules/dictionaries/i18n/es.json +1 -0
  168. package/src/modules/dictionaries/i18n/pl.json +1 -0
  169. package/src/modules/directory/api/organization-branding/route.ts +61 -0
  170. package/src/modules/directory/backend/directory/branding/page.tsx +16 -10
  171. package/src/modules/directory/backend/directory/organizations/page.tsx +35 -8
  172. package/src/modules/directory/backend/directory/tenants/page.tsx +37 -13
  173. package/src/modules/entities/api/definitions.batch.ts +17 -0
  174. package/src/modules/entities/api/definitions.mutation-guard.ts +80 -0
  175. package/src/modules/entities/api/definitions.restore.ts +17 -0
  176. package/src/modules/entities/api/definitions.ts +30 -0
  177. package/src/modules/entities/api/entities.ts +35 -3
  178. package/src/modules/entities/api/records.ts +20 -0
  179. package/src/modules/entities/backend/entities/user/[entityId]/records/page.tsx +33 -10
  180. package/src/modules/feature_toggles/api/overrides/route.ts +44 -1
  181. package/src/modules/inbox_ops/api/emails/[id]/route.ts +32 -0
  182. package/src/modules/inbox_ops/api/proposals/[id]/accept-all/route.ts +32 -0
  183. package/src/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/route.ts +33 -0
  184. package/src/modules/inbox_ops/api/proposals/[id]/categorize/route.ts +33 -0
  185. package/src/modules/messages/components/message-detail/hooks/useMessageDetailsActions.ts +80 -42
  186. package/src/modules/notifications/frontend/NotificationSettingsPageClient.tsx +21 -4
  187. package/src/modules/payment_gateways/api/cancel/route.ts +39 -0
  188. package/src/modules/payment_gateways/api/capture/route.ts +39 -0
  189. package/src/modules/payment_gateways/api/guards.ts +59 -0
  190. package/src/modules/payment_gateways/api/refund/route.ts +39 -0
  191. package/src/modules/payment_gateways/api/sessions/route.ts +40 -0
  192. package/src/modules/planner/api/availability-date-specific.ts +10 -0
  193. package/src/modules/planner/backend/planner/availability-rulesets/page.tsx +26 -5
  194. package/src/modules/planner/commands/availability-date-specific.ts +24 -0
  195. package/src/modules/planner/components/AvailabilityRulesEditor.tsx +122 -41
  196. package/src/modules/query_index/api/purge.ts +37 -3
  197. package/src/modules/query_index/api/reindex.ts +43 -3
  198. package/src/modules/query_index/components/QueryIndexesTable.tsx +66 -24
  199. package/src/modules/shipping_carriers/api/shipments/route.ts +31 -0
  200. package/src/modules/shipping_carriers/api/tracking/refresh/route.ts +53 -0
  201. package/src/modules/shipping_carriers/data/validators.ts +5 -0
  202. package/src/modules/shipping_carriers/lib/shipment-wizard/hooks/useShipmentWizard.ts +29 -8
  203. package/src/modules/shipping_carriers/lib/shipping-service.ts +43 -7
  204. package/src/modules/shipping_carriers/workers/status-poller.ts +1 -1
  205. package/src/modules/translations/components/TranslationManager.tsx +65 -21
@@ -16,6 +16,7 @@ import {
16
16
  import { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail'
17
17
  import { apiCall, apiCallOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
18
18
  import { createCrud, deleteCrud, updateCrud } from '@open-mercato/ui/backend/utils/crud'
19
+ import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
19
20
  import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
20
21
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
21
22
  import { Spinner } from '@open-mercato/ui/primitives/spinner'
@@ -29,6 +30,7 @@ import {
29
30
  import { useT } from '@open-mercato/shared/lib/i18n/context'
30
31
  import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
31
32
  import { normalizeCrudServerError } from '@open-mercato/ui/backend/utils/serverErrors'
33
+ import { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'
32
34
  import { parseAvailabilityRuleWindow } from '@open-mercato/core/modules/planner/lib/availabilitySchedule'
33
35
  import { deleteAvailabilityRuleSet } from '@open-mercato/core/modules/planner/lib/deleteAvailabilityRuleSet'
34
36
  import { CrudForm, type CrudField } from '@open-mercato/ui/backend/CrudForm'
@@ -108,6 +110,30 @@ function withOptimisticLockForRuleSet<T>(ruleSet: AvailabilityRuleSet | null | u
108
110
  return withScopedApiRequestHeaders(buildOptimisticLockHeader(ruleSet?.updatedAt ?? ruleSet?.updated_at ?? null), mutation)
109
111
  }
110
112
 
113
+ /**
114
+ * The date-specific replace endpoint mutates a subject/date aggregate that has
115
+ * no single parent row, so its lock version is the latest `updated_at` of the
116
+ * one-off rules being replaced (mirrors the command-side derivation). Returns
117
+ * null when no existing rules back the date so the header is omitted (additive).
118
+ */
119
+ function resolveDateSpecificLockToken(rules: AvailabilityRule[]): string | null {
120
+ let latestMs = Number.NEGATIVE_INFINITY
121
+ let latestIso: string | null = null
122
+ for (const rule of rules) {
123
+ const raw = rule.updatedAt ?? rule.updated_at ?? null
124
+ if (!raw) continue
125
+ const ms = Date.parse(raw)
126
+ if (!Number.isFinite(ms) || ms <= latestMs) continue
127
+ latestMs = ms
128
+ latestIso = raw
129
+ }
130
+ return latestIso
131
+ }
132
+
133
+ function withOptimisticLockForDateSpecific<T>(rules: AvailabilityRule[], mutation: () => Promise<T>): Promise<T> {
134
+ return withScopedApiRequestHeaders(buildOptimisticLockHeader(resolveDateSpecificLockToken(rules)), mutation)
135
+ }
136
+
111
137
  const DAY_LABELS = [
112
138
  { code: 'SU', short: 'S', nameKey: 'schedule.weekday.sunday', fallback: 'Sunday' },
113
139
  { code: 'MO', short: 'M', nameKey: 'schedule.weekday.monday', fallback: 'Monday' },
@@ -381,6 +407,23 @@ export function AvailabilityRulesEditor({
381
407
  }: AvailabilityRulesEditorProps) {
382
408
  const t = useT()
383
409
  const { confirm, ConfirmDialogElement } = useConfirmDialog()
410
+ const mutationContextId = 'planner-availability-rules-editor'
411
+ const { runMutation, retryLastMutation } = useGuardedMutation<{
412
+ formId: string
413
+ resourceKind: string
414
+ retryLastMutation: () => Promise<boolean>
415
+ }>({
416
+ contextId: mutationContextId,
417
+ blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),
418
+ })
419
+ const mutationContext = React.useMemo(
420
+ () => ({
421
+ formId: mutationContextId,
422
+ resourceKind: 'planner.availability',
423
+ retryLastMutation,
424
+ }),
425
+ [mutationContextId, retryLastMutation],
426
+ )
384
427
  const isReadOnly = Boolean(readOnly)
385
428
  const canManageUnavailability = allowUnavailability ?? true
386
429
  const dialogRef = React.useRef<HTMLDivElement | null>(null)
@@ -818,16 +861,20 @@ export function AvailabilityRulesEditor({
818
861
  setIsWeeklyAutoSaving(options?.silentSuccess === true)
819
862
  try {
820
863
  const windows = buildWeeklyPayload(normalizeWeeklyWindows(weeklyWindowsRef.current))
821
- await apiCallOrThrow('/api/planner/availability-weekly', {
822
- method: 'POST',
823
- headers: { 'Content-Type': 'application/json' },
824
- body: JSON.stringify({
825
- subjectType: subjectForRules,
826
- subjectId: subjectIdForRules,
827
- timezone,
828
- windows,
829
- }),
830
- }, { errorMessage: listLabels.saveWeeklyError })
864
+ await runMutation({
865
+ operation: () => apiCallOrThrow('/api/planner/availability-weekly', {
866
+ method: 'POST',
867
+ headers: { 'Content-Type': 'application/json' },
868
+ body: JSON.stringify({
869
+ subjectType: subjectForRules,
870
+ subjectId: subjectIdForRules,
871
+ timezone,
872
+ windows,
873
+ }),
874
+ }, { errorMessage: listLabels.saveWeeklyError }),
875
+ context: mutationContext,
876
+ mutationPayload: { action: 'save-weekly', subjectType: subjectForRules, subjectId: subjectIdForRules },
877
+ })
831
878
  lastSavedWeeklyKeyRef.current = weeklyKey
832
879
  weeklyDirtyRef.current = false
833
880
  if (!options?.silentSuccess) {
@@ -856,6 +903,8 @@ export function AvailabilityRulesEditor({
856
903
  weeklyHasErrors,
857
904
  weeklyKey,
858
905
  isReadOnly,
906
+ mutationContext,
907
+ runMutation,
859
908
  ])
860
909
 
861
910
  React.useEffect(() => {
@@ -929,7 +978,11 @@ export function AvailabilityRulesEditor({
929
978
  )))
930
979
  }
931
980
  if (updates.length) {
932
- await Promise.all(updates)
981
+ await runMutation({
982
+ operation: () => Promise.all(updates),
983
+ context: mutationContext,
984
+ mutationPayload: { action: 'update-timezone', timezone: trimmedTimezone },
985
+ })
933
986
  await refreshAvailability()
934
987
  await refreshRuleSetRules()
935
988
  }
@@ -951,6 +1004,8 @@ export function AvailabilityRulesEditor({
951
1004
  subjectId,
952
1005
  subjectType,
953
1006
  isReadOnly,
1007
+ mutationContext,
1008
+ runMutation,
954
1009
  ])
955
1010
 
956
1011
  React.useEffect(() => {
@@ -990,14 +1045,18 @@ export function AvailabilityRulesEditor({
990
1045
  kind: rule.kind ?? 'availability',
991
1046
  note: rule.note ?? null,
992
1047
  }))
993
- await Promise.all(creations)
1048
+ await runMutation({
1049
+ operation: () => Promise.all(creations),
1050
+ context: mutationContext,
1051
+ mutationPayload: { action: 'customize', count: creations.length },
1052
+ })
994
1053
  setCustomOverridesEnabled(true)
995
1054
  await refreshAvailability()
996
1055
  } catch (error) {
997
1056
  const message = error instanceof Error ? error.message : listLabels.saveWeeklyError
998
1057
  flash(message, 'error')
999
1058
  }
1000
- }, [effectiveRulesetId, listLabels.saveWeeklyError, refreshAvailability, rulesetRules, subjectId, subjectType, isReadOnly])
1059
+ }, [effectiveRulesetId, listLabels.saveWeeklyError, refreshAvailability, rulesetRules, subjectId, subjectType, isReadOnly, mutationContext, runMutation])
1001
1060
 
1002
1061
  const handleResetToRuleSet = React.useCallback(async () => {
1003
1062
  if (isReadOnly) return
@@ -1012,21 +1071,25 @@ export function AvailabilityRulesEditor({
1012
1071
  try {
1013
1072
  const idsToDelete = selectCustomRuleIdsToDelete('reset', availabilityRules)
1014
1073
  const idsToDeleteSet = new Set(idsToDelete)
1015
- await Promise.all(
1016
- availabilityRules
1017
- .filter((rule) => idsToDeleteSet.has(rule.id))
1018
- .map((rule) => withOptimisticLockForRule(
1019
- rule,
1020
- () => deleteCrud('planner/availability', rule.id, { errorMessage: listLabels.saveWeeklyError }),
1021
- )),
1022
- )
1074
+ await runMutation({
1075
+ operation: () => Promise.all(
1076
+ availabilityRules
1077
+ .filter((rule) => idsToDeleteSet.has(rule.id))
1078
+ .map((rule) => withOptimisticLockForRule(
1079
+ rule,
1080
+ () => deleteCrud('planner/availability', rule.id, { errorMessage: listLabels.saveWeeklyError }),
1081
+ )),
1082
+ ),
1083
+ context: mutationContext,
1084
+ mutationPayload: { action: 'reset-to-ruleset', ids: idsToDelete },
1085
+ })
1023
1086
  setCustomOverridesEnabled(false)
1024
1087
  await refreshAvailability()
1025
1088
  } catch (error) {
1026
1089
  const message = error instanceof Error ? error.message : listLabels.saveWeeklyError
1027
1090
  flash(message, 'error')
1028
1091
  }
1029
- }, [availabilityRules, confirm, effectiveRulesetId, listLabels.ruleSetConfirm, listLabels.saveWeeklyError, refreshAvailability, isReadOnly])
1092
+ }, [availabilityRules, confirm, effectiveRulesetId, listLabels.ruleSetConfirm, listLabels.saveWeeklyError, refreshAvailability, isReadOnly, mutationContext, runMutation])
1030
1093
 
1031
1094
  const handleRuleSetChange = React.useCallback(async (nextId: string | null) => {
1032
1095
  if (isReadOnly) return
@@ -1035,14 +1098,18 @@ export function AvailabilityRulesEditor({
1035
1098
  const idsToDelete = selectCustomRuleIdsToDelete('switch', availabilityRules)
1036
1099
  if (idsToDelete.length) {
1037
1100
  const idsToDeleteSet = new Set(idsToDelete)
1038
- await Promise.all(
1039
- availabilityRules
1040
- .filter((rule) => idsToDeleteSet.has(rule.id))
1041
- .map((rule) => withOptimisticLockForRule(
1042
- rule,
1043
- () => deleteCrud('planner/availability', rule.id, { errorMessage: listLabels.saveWeeklyError }),
1044
- )),
1045
- )
1101
+ await runMutation({
1102
+ operation: () => Promise.all(
1103
+ availabilityRules
1104
+ .filter((rule) => idsToDeleteSet.has(rule.id))
1105
+ .map((rule) => withOptimisticLockForRule(
1106
+ rule,
1107
+ () => deleteCrud('planner/availability', rule.id, { errorMessage: listLabels.saveWeeklyError }),
1108
+ )),
1109
+ ),
1110
+ context: mutationContext,
1111
+ mutationPayload: { action: 'switch-ruleset', ids: idsToDelete },
1112
+ })
1046
1113
  }
1047
1114
  setSelectedRulesetId(nextId)
1048
1115
  setCustomOverridesEnabled(false)
@@ -1060,6 +1127,8 @@ export function AvailabilityRulesEditor({
1060
1127
  onRulesetChange,
1061
1128
  refreshAvailability,
1062
1129
  isReadOnly,
1130
+ mutationContext,
1131
+ runMutation,
1063
1132
  ])
1064
1133
 
1065
1134
  const handleDeleteRuleSet = React.useCallback(async () => {
@@ -1076,9 +1145,13 @@ export function AvailabilityRulesEditor({
1076
1145
  deleteRuleSet: async (id) => {
1077
1146
  // Version-check the schedule delete (the list page already does; the editor
1078
1147
  // previously sent no header → last-write-wins on delete). #2055 round-5.
1079
- await withOptimisticLockForRuleSet(selected, () => (
1080
- deleteCrud('planner/availability-rule-sets', id, { errorMessage: listLabels.ruleSetDeleteError })
1081
- ))
1148
+ await runMutation({
1149
+ operation: () => withOptimisticLockForRuleSet(selected, () => (
1150
+ deleteCrud('planner/availability-rule-sets', id, { errorMessage: listLabels.ruleSetDeleteError })
1151
+ )),
1152
+ context: mutationContext,
1153
+ mutationPayload: { action: 'delete-ruleset', id },
1154
+ })
1082
1155
  },
1083
1156
  clearAssignment: async () => {
1084
1157
  setCustomOverridesEnabled(false)
@@ -1105,6 +1178,8 @@ export function AvailabilityRulesEditor({
1105
1178
  refreshRuleSets,
1106
1179
  ruleSets,
1107
1180
  rulesetId,
1181
+ mutationContext,
1182
+ runMutation,
1108
1183
  ])
1109
1184
 
1110
1185
  const ruleSetFormSchema = React.useMemo(
@@ -1299,11 +1374,11 @@ export function AvailabilityRulesEditor({
1299
1374
  unavailabilityReasonEntryId: editorUnavailable ? reasonEntryId : null,
1300
1375
  unavailabilityReasonValue: editorUnavailable ? reasonValue : null,
1301
1376
  }
1302
- await apiCallOrThrow('/api/planner/availability-date-specific', {
1377
+ await withOptimisticLockForDateSpecific(editorRules, () => apiCallOrThrow('/api/planner/availability-date-specific', {
1303
1378
  method: 'POST',
1304
1379
  headers: { 'Content-Type': 'application/json' },
1305
1380
  body: JSON.stringify(payload),
1306
- }, { errorMessage: listLabels.saveDateError })
1381
+ }, { errorMessage: listLabels.saveDateError }))
1307
1382
  } else {
1308
1383
  const rulesToDelete = editorRules
1309
1384
  const uniqueRulesById = new Map(rulesToDelete.map((rule) => [rule.id, rule]))
@@ -1338,6 +1413,7 @@ export function AvailabilityRulesEditor({
1338
1413
  await refreshAvailability()
1339
1414
  await refreshRuleSetRules()
1340
1415
  } catch (error) {
1416
+ if (surfaceRecordConflict(error, t)) return
1341
1417
  const message = error instanceof Error ? error.message : listLabels.saveDateError
1342
1418
  flash(message, 'error')
1343
1419
  }
@@ -1364,6 +1440,7 @@ export function AvailabilityRulesEditor({
1364
1440
  usingRuleSet,
1365
1441
  canManageUnavailability,
1366
1442
  isReadOnly,
1443
+ t,
1367
1444
  ])
1368
1445
 
1369
1446
  const handleSlotClick = React.useCallback((slot: ScheduleSlot) => {
@@ -1658,12 +1735,16 @@ export function AvailabilityRulesEditor({
1658
1735
  variant="outline"
1659
1736
  size="icon"
1660
1737
  onClick={async () => {
1661
- await Promise.all(
1662
- rules.map((rule) => withOptimisticLockForRule(
1663
- rule,
1664
- () => deleteCrud('planner/availability', rule.id),
1665
- )),
1666
- )
1738
+ await runMutation({
1739
+ operation: () => Promise.all(
1740
+ rules.map((rule) => withOptimisticLockForRule(
1741
+ rule,
1742
+ () => deleteCrud('planner/availability', rule.id),
1743
+ )),
1744
+ ),
1745
+ context: mutationContext,
1746
+ mutationPayload: { action: 'delete-date-rules', count: rules.length },
1747
+ })
1667
1748
  await refreshAvailability()
1668
1749
  await refreshRuleSetRules()
1669
1750
  }}
@@ -4,6 +4,10 @@ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
4
4
  import type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
5
5
  import { queryIndexTag, queryIndexErrorSchema, queryIndexOkSchema, queryIndexPurgeRequestSchema } from './openapi'
6
6
  import { recordIndexerLog } from '@open-mercato/shared/lib/indexers/status-log'
7
+ import {
8
+ runCrudMutationGuardAfterSuccess,
9
+ validateCrudMutationGuard,
10
+ } from '@open-mercato/shared/lib/crud/mutation-guard'
7
11
 
8
12
  export const metadata = {
9
13
  POST: { requireAuth: true, requireFeatures: ['query_index.purge'] },
@@ -16,12 +20,29 @@ export async function POST(req: Request) {
16
20
  const entityType = String(body?.entityType || '')
17
21
  if (!entityType) return NextResponse.json({ error: 'Missing entityType' }, { status: 400 })
18
22
 
19
- const { resolve } = await createRequestContainer()
23
+ const container = await createRequestContainer()
20
24
  let em: any | null = null
21
25
  try {
22
- em = resolve('em')
26
+ em = container.resolve('em')
23
27
  } catch {}
24
- const bus = resolve('eventBus') as any
28
+ const bus = container.resolve('eventBus') as any
29
+
30
+ const guardUserId = typeof auth.sub === 'string' ? auth.sub : ''
31
+ const guardResult = await validateCrudMutationGuard(container, {
32
+ tenantId: auth.tenantId,
33
+ organizationId: auth.orgId,
34
+ userId: guardUserId,
35
+ resourceKind: 'query_index',
36
+ resourceId: entityType,
37
+ operation: 'custom',
38
+ requestMethod: req.method,
39
+ requestHeaders: req.headers,
40
+ mutationPayload: { entityType },
41
+ })
42
+ if (guardResult && !guardResult.ok) {
43
+ return NextResponse.json(guardResult.body, { status: guardResult.status })
44
+ }
45
+
25
46
  await recordIndexerLog(
26
47
  { em: em ?? undefined },
27
48
  {
@@ -50,6 +71,19 @@ export async function POST(req: Request) {
50
71
  organizationId: auth.orgId ?? null,
51
72
  },
52
73
  ).catch(() => undefined)
74
+ if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
75
+ await runCrudMutationGuardAfterSuccess(container, {
76
+ tenantId: auth.tenantId,
77
+ organizationId: auth.orgId,
78
+ userId: guardUserId,
79
+ resourceKind: 'query_index',
80
+ resourceId: entityType,
81
+ operation: 'custom',
82
+ requestMethod: req.method,
83
+ requestHeaders: req.headers,
84
+ metadata: guardResult.metadata ?? null,
85
+ })
86
+ }
53
87
  } catch (error) {
54
88
  await recordIndexerLog(
55
89
  { em: em ?? undefined },
@@ -5,6 +5,10 @@ import type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib
5
5
  import { queryIndexTag, queryIndexErrorSchema, queryIndexOkSchema, queryIndexReindexRequestSchema } from './openapi'
6
6
  import { recordIndexerLog } from '@open-mercato/shared/lib/indexers/status-log'
7
7
  import { isValidEntityIdShape } from '@open-mercato/shared/lib/query/engine'
8
+ import {
9
+ runCrudMutationGuardAfterSuccess,
10
+ validateCrudMutationGuard,
11
+ } from '@open-mercato/shared/lib/crud/mutation-guard'
8
12
 
9
13
  export const metadata = {
10
14
  POST: { requireAuth: true, requireFeatures: ['query_index.reindex'] },
@@ -31,16 +35,39 @@ export async function POST(req: Request) {
31
35
  return NextResponse.json({ error: 'partitionIndex must be < partitionCount' }, { status: 400 })
32
36
  }
33
37
 
34
- const { resolve } = await createRequestContainer()
38
+ const container = await createRequestContainer()
35
39
  let em: any | null = null
36
40
  try {
37
- em = resolve('em')
41
+ em = container.resolve('em')
38
42
  } catch {}
39
- const bus = resolve('eventBus') as any
43
+ const bus = container.resolve('eventBus') as any
40
44
  const partitions = partitionIndex !== undefined
41
45
  ? [partitionIndex]
42
46
  : Array.from({ length: partitionCount }, (_, idx) => idx)
43
47
  const firstPartition = partitions[0] ?? 0
48
+
49
+ const guardUserId = typeof auth.sub === 'string' ? auth.sub : ''
50
+ const guardResult = await validateCrudMutationGuard(container, {
51
+ tenantId: auth.tenantId,
52
+ organizationId: auth.orgId,
53
+ userId: guardUserId,
54
+ resourceKind: 'query_index',
55
+ resourceId: entityType,
56
+ operation: 'custom',
57
+ requestMethod: req.method,
58
+ requestHeaders: req.headers,
59
+ mutationPayload: {
60
+ entityType,
61
+ force,
62
+ batchSize: batchSize ?? null,
63
+ partitionCount,
64
+ partitionIndex: partitionIndex ?? null,
65
+ },
66
+ })
67
+ if (guardResult && !guardResult.ok) {
68
+ return NextResponse.json(guardResult.body, { status: guardResult.status })
69
+ }
70
+
44
71
  await recordIndexerLog(
45
72
  { em: em ?? undefined },
46
73
  {
@@ -102,6 +129,19 @@ export async function POST(req: Request) {
102
129
  },
103
130
  },
104
131
  ).catch(() => undefined)
132
+ if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
133
+ await runCrudMutationGuardAfterSuccess(container, {
134
+ tenantId: auth.tenantId,
135
+ organizationId: auth.orgId,
136
+ userId: guardUserId,
137
+ resourceKind: 'query_index',
138
+ resourceId: entityType,
139
+ operation: 'custom',
140
+ requestMethod: req.method,
141
+ requestHeaders: req.headers,
142
+ metadata: guardResult.metadata ?? null,
143
+ })
144
+ }
105
145
  } catch (error) {
106
146
  await recordIndexerLog(
107
147
  { em: em ?? undefined },
@@ -10,6 +10,9 @@ import { flash } from '@open-mercato/ui/backend/FlashMessages'
10
10
  import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
11
11
  import { useT } from '@open-mercato/shared/lib/i18n/context'
12
12
  import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
13
+ import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
14
+
15
+ const MUTATION_CONTEXT_ID = 'query_index.status.list:actions'
13
16
 
14
17
  type Translator = (key: string, params?: Record<string, string | number>) => string
15
18
 
@@ -227,6 +230,15 @@ export default function QueryIndexesTable() {
227
230
  const t = useT()
228
231
  const { confirm, ConfirmDialogElement } = useConfirmDialog()
229
232
  const columns = React.useMemo(() => createColumns(t), [t])
233
+ const { runMutation, retryLastMutation } = useGuardedMutation<{
234
+ formId: string
235
+ resourceKind: string
236
+ resourceId: string
237
+ retryLastMutation: () => Promise<boolean>
238
+ }>({
239
+ contextId: MUTATION_CONTEXT_ID,
240
+ blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),
241
+ })
230
242
 
231
243
  const { data, isLoading } = useQuery<Resp>({
232
244
  queryKey: ['query-index-status', scopeVersion, refreshSeq],
@@ -257,11 +269,21 @@ export default function QueryIndexesTable() {
257
269
  action === 'purge' ? t('query_index.table.actions.purge') : t('query_index.table.actions.reindex')
258
270
  const errorMessage = t('query_index.table.errors.actionFailed', { action: actionLabel })
259
271
  try {
260
- await apiCallOrThrow(`/api/query_index/${action}`, {
261
- method: 'POST',
262
- headers: { 'content-type': 'application/json' },
263
- body: JSON.stringify(body),
264
- }, { errorMessage })
272
+ await runMutation({
273
+ operation: () =>
274
+ apiCallOrThrow(`/api/query_index/${action}`, {
275
+ method: 'POST',
276
+ headers: { 'content-type': 'application/json' },
277
+ body: JSON.stringify(body),
278
+ }, { errorMessage }),
279
+ context: {
280
+ formId: MUTATION_CONTEXT_ID,
281
+ resourceKind: 'query_index',
282
+ resourceId: entityId,
283
+ retryLastMutation,
284
+ },
285
+ mutationPayload: { action, entityType: entityId, force: Boolean(opts?.force) },
286
+ })
265
287
  } catch (err) {
266
288
  // Expected operational failures (e.g. a 503 when a search backend is not
267
289
  // configured) — surface a flash toast, not an alert or an error-level
@@ -272,7 +294,7 @@ export default function QueryIndexesTable() {
272
294
  }
273
295
  qc.invalidateQueries({ queryKey: ['query-index-status'] })
274
296
  },
275
- [qc, t],
297
+ [qc, t, runMutation, retryLastMutation],
276
298
  )
277
299
 
278
300
  const triggerVector = React.useCallback(
@@ -290,14 +312,24 @@ export default function QueryIndexesTable() {
290
312
  : t('query_index.table.actions.vectorReindex')
291
313
  const errorMessage = t('query_index.table.errors.actionFailed', { action: actionLabel })
292
314
  try {
293
- await apiCallOrThrow('/api/search/embeddings/reindex', {
294
- method: 'POST',
295
- headers: { 'content-type': 'application/json' },
296
- body: JSON.stringify({
297
- entityId,
298
- purgeFirst: action === 'purge',
299
- }),
300
- }, { errorMessage })
315
+ await runMutation({
316
+ operation: () =>
317
+ apiCallOrThrow('/api/search/embeddings/reindex', {
318
+ method: 'POST',
319
+ headers: { 'content-type': 'application/json' },
320
+ body: JSON.stringify({
321
+ entityId,
322
+ purgeFirst: action === 'purge',
323
+ }),
324
+ }, { errorMessage }),
325
+ context: {
326
+ formId: MUTATION_CONTEXT_ID,
327
+ resourceKind: 'query_index.vector',
328
+ resourceId: entityId,
329
+ retryLastMutation,
330
+ },
331
+ mutationPayload: { action, entityId, purgeFirst: action === 'purge' },
332
+ })
301
333
  } catch (err) {
302
334
  const message = err instanceof Error && err.message ? err.message : errorMessage
303
335
  console.warn('[query_index] vector action failed', message)
@@ -305,7 +337,7 @@ export default function QueryIndexesTable() {
305
337
  }
306
338
  qc.invalidateQueries({ queryKey: ['query-index-status'] })
307
339
  },
308
- [confirm, qc, t],
340
+ [confirm, qc, t, runMutation, retryLastMutation],
309
341
  )
310
342
 
311
343
  const triggerFulltext = React.useCallback(
@@ -323,14 +355,24 @@ export default function QueryIndexesTable() {
323
355
  : t('query_index.table.actions.fulltextReindex')
324
356
  const errorMessage = t('query_index.table.errors.actionFailed', { action: actionLabel })
325
357
  try {
326
- await apiCallOrThrow('/api/search/reindex', {
327
- method: 'POST',
328
- headers: { 'content-type': 'application/json' },
329
- body: JSON.stringify({
330
- action: action === 'purge' ? 'clear' : 'reindex',
331
- entityId,
332
- }),
333
- }, { errorMessage })
358
+ await runMutation({
359
+ operation: () =>
360
+ apiCallOrThrow('/api/search/reindex', {
361
+ method: 'POST',
362
+ headers: { 'content-type': 'application/json' },
363
+ body: JSON.stringify({
364
+ action: action === 'purge' ? 'clear' : 'reindex',
365
+ entityId,
366
+ }),
367
+ }, { errorMessage }),
368
+ context: {
369
+ formId: MUTATION_CONTEXT_ID,
370
+ resourceKind: 'query_index.fulltext',
371
+ resourceId: entityId,
372
+ retryLastMutation,
373
+ },
374
+ mutationPayload: { action, entityId },
375
+ })
334
376
  } catch (err) {
335
377
  const message = err instanceof Error && err.message ? err.message : errorMessage
336
378
  console.warn('[query_index] fulltext action failed', message)
@@ -338,7 +380,7 @@ export default function QueryIndexesTable() {
338
380
  }
339
381
  qc.invalidateQueries({ queryKey: ['query-index-status'] })
340
382
  },
341
- [confirm, qc, t],
383
+ [confirm, qc, t, runMutation, retryLastMutation],
342
384
  )
343
385
 
344
386
  return (
@@ -2,6 +2,10 @@ import { NextResponse } from 'next/server'
2
2
  import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
3
3
  import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
4
4
  import { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'
5
+ import {
6
+ runCrudMutationGuardAfterSuccess,
7
+ validateCrudMutationGuard,
8
+ } from '@open-mercato/shared/lib/crud/mutation-guard'
5
9
  import type { ShippingCarrierService } from '../../lib/shipping-service'
6
10
  import { createShipmentSchema } from '../../data/validators'
7
11
  import { shippingCarriersTag } from '../openapi'
@@ -23,12 +27,39 @@ export async function POST(req: Request) {
23
27
  }
24
28
  const container = await createRequestContainer()
25
29
  const service = container.resolve('shippingCarrierService') as ShippingCarrierService
30
+ const guardResult = await validateCrudMutationGuard(container, {
31
+ tenantId: auth.tenantId,
32
+ organizationId: auth.orgId,
33
+ userId: auth.sub ?? '',
34
+ resourceKind: 'shipping_carriers.shipment',
35
+ resourceId: parsed.data.orderId,
36
+ operation: 'create',
37
+ requestMethod: req.method,
38
+ requestHeaders: req.headers,
39
+ mutationPayload: parsed.data as unknown as Record<string, unknown>,
40
+ })
41
+ if (guardResult && !guardResult.ok) {
42
+ return NextResponse.json(guardResult.body, { status: guardResult.status })
43
+ }
26
44
  try {
27
45
  const shipment = await service.createShipment({
28
46
  ...parsed.data,
29
47
  organizationId: auth.orgId as string,
30
48
  tenantId: auth.tenantId,
31
49
  })
50
+ if (guardResult?.shouldRunAfterSuccess) {
51
+ await runCrudMutationGuardAfterSuccess(container, {
52
+ tenantId: auth.tenantId,
53
+ organizationId: auth.orgId,
54
+ userId: auth.sub ?? '',
55
+ resourceKind: 'shipping_carriers.shipment',
56
+ resourceId: shipment.id,
57
+ operation: 'create',
58
+ requestMethod: req.method,
59
+ requestHeaders: req.headers,
60
+ metadata: guardResult.metadata ?? null,
61
+ })
62
+ }
32
63
  return NextResponse.json({
33
64
  shipmentId: shipment.id,
34
65
  carrierShipmentId: shipment.carrierShipmentId,