@open-mercato/core 0.6.6-develop.6089.1.a7ed560528 → 0.6.6-develop.6121.1.5797a901b3

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 (320) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/generated/entities/catalog_product/index.js +52 -0
  3. package/dist/generated/entities/catalog_product/index.js.map +2 -2
  4. package/dist/generated/entities/catalog_product_variant/index.js +4 -0
  5. package/dist/generated/entities/catalog_product_variant/index.js.map +2 -2
  6. package/dist/generated/entity-fields-registry.js +28 -0
  7. package/dist/generated/entity-fields-registry.js.map +2 -2
  8. package/dist/modules/catalog/api/products/route.js +103 -33
  9. package/dist/modules/catalog/api/products/route.js.map +2 -2
  10. package/dist/modules/catalog/api/variants/route.js +4 -0
  11. package/dist/modules/catalog/api/variants/route.js.map +2 -2
  12. package/dist/modules/catalog/backend/catalog/products/[id]/page.js +46 -2
  13. package/dist/modules/catalog/backend/catalog/products/[id]/page.js.map +2 -2
  14. package/dist/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.js +4 -0
  15. package/dist/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.js.map +2 -2
  16. package/dist/modules/catalog/backend/catalog/products/[productId]/variants/create/page.js +2 -0
  17. package/dist/modules/catalog/backend/catalog/products/[productId]/variants/create/page.js.map +2 -2
  18. package/dist/modules/catalog/backend/catalog/products/create/page.js +42 -2
  19. package/dist/modules/catalog/backend/catalog/products/create/page.js.map +2 -2
  20. package/dist/modules/catalog/commands/products.js +251 -14
  21. package/dist/modules/catalog/commands/products.js.map +2 -2
  22. package/dist/modules/catalog/commands/variants.js +72 -1
  23. package/dist/modules/catalog/commands/variants.js.map +2 -2
  24. package/dist/modules/catalog/components/products/ProductComplianceSection.js +500 -0
  25. package/dist/modules/catalog/components/products/ProductComplianceSection.js.map +7 -0
  26. package/dist/modules/catalog/components/products/VariantBuilder.js +39 -1
  27. package/dist/modules/catalog/components/products/VariantBuilder.js.map +2 -2
  28. package/dist/modules/catalog/components/products/productForm.js +164 -2
  29. package/dist/modules/catalog/components/products/productForm.js.map +2 -2
  30. package/dist/modules/catalog/components/products/variantForm.js +2 -0
  31. package/dist/modules/catalog/components/products/variantForm.js.map +2 -2
  32. package/dist/modules/catalog/data/entities.js +93 -0
  33. package/dist/modules/catalog/data/entities.js.map +2 -2
  34. package/dist/modules/catalog/data/types.js +28 -0
  35. package/dist/modules/catalog/data/types.js.map +2 -2
  36. package/dist/modules/catalog/data/validators.js +100 -5
  37. package/dist/modules/catalog/data/validators.js.map +2 -2
  38. package/dist/modules/catalog/lib/gtin.js +61 -0
  39. package/dist/modules/catalog/lib/gtin.js.map +7 -0
  40. package/dist/modules/catalog/migrations/Migration20260611090000.js +17 -0
  41. package/dist/modules/catalog/migrations/Migration20260611090000.js.map +7 -0
  42. package/dist/modules/catalog/translations.js +1 -1
  43. package/dist/modules/catalog/translations.js.map +2 -2
  44. package/dist/modules/configs/api/cache/route.js +60 -10
  45. package/dist/modules/configs/api/cache/route.js.map +3 -3
  46. package/dist/modules/configs/components/CachePanel.js +52 -29
  47. package/dist/modules/configs/components/CachePanel.js.map +2 -2
  48. package/dist/modules/currencies/api/currencies/options/route.js +18 -4
  49. package/dist/modules/currencies/api/currencies/options/route.js.map +2 -2
  50. package/dist/modules/currencies/services/rateFetchingService.js +35 -18
  51. package/dist/modules/currencies/services/rateFetchingService.js.map +2 -2
  52. package/dist/modules/customer_accounts/backend/customer_accounts/users/[id]/page.js +16 -16
  53. package/dist/modules/customer_accounts/backend/customer_accounts/users/[id]/page.js.map +2 -2
  54. package/dist/modules/customer_accounts/backend/customer_accounts/users/page.js +1 -1
  55. package/dist/modules/customer_accounts/backend/customer_accounts/users/page.js.map +2 -2
  56. package/dist/modules/customers/backend/calendar/page.js +10 -0
  57. package/dist/modules/customers/backend/calendar/page.js.map +7 -0
  58. package/dist/modules/customers/backend/calendar/page.meta.js +25 -0
  59. package/dist/modules/customers/backend/calendar/page.meta.js.map +7 -0
  60. package/dist/modules/customers/cli.js +1 -0
  61. package/dist/modules/customers/cli.js.map +2 -2
  62. package/dist/modules/customers/components/calendar/AgendaList.js +182 -0
  63. package/dist/modules/customers/components/calendar/AgendaList.js.map +7 -0
  64. package/dist/modules/customers/components/calendar/CalendarEventEditor.js +331 -0
  65. package/dist/modules/customers/components/calendar/CalendarEventEditor.js.map +7 -0
  66. package/dist/modules/customers/components/calendar/CalendarFooter.js +48 -0
  67. package/dist/modules/customers/components/calendar/CalendarFooter.js.map +7 -0
  68. package/dist/modules/customers/components/calendar/CalendarHeader.js +36 -0
  69. package/dist/modules/customers/components/calendar/CalendarHeader.js.map +7 -0
  70. package/dist/modules/customers/components/calendar/CalendarScreen.js +516 -0
  71. package/dist/modules/customers/components/calendar/CalendarScreen.js.map +7 -0
  72. package/dist/modules/customers/components/calendar/CalendarSettingsModal.js +163 -0
  73. package/dist/modules/customers/components/calendar/CalendarSettingsModal.js.map +7 -0
  74. package/dist/modules/customers/components/calendar/CalendarTabs.js +60 -0
  75. package/dist/modules/customers/components/calendar/CalendarTabs.js.map +7 -0
  76. package/dist/modules/customers/components/calendar/CalendarToolbar.js +262 -0
  77. package/dist/modules/customers/components/calendar/CalendarToolbar.js.map +7 -0
  78. package/dist/modules/customers/components/calendar/EventBlock.js +149 -0
  79. package/dist/modules/customers/components/calendar/EventBlock.js.map +7 -0
  80. package/dist/modules/customers/components/calendar/EventPeekPopover.js +72 -0
  81. package/dist/modules/customers/components/calendar/EventPeekPopover.js.map +7 -0
  82. package/dist/modules/customers/components/calendar/MonthGrid.js +214 -0
  83. package/dist/modules/customers/components/calendar/MonthGrid.js.map +7 -0
  84. package/dist/modules/customers/components/calendar/ShortcutsDialog.js +44 -0
  85. package/dist/modules/customers/components/calendar/ShortcutsDialog.js.map +7 -0
  86. package/dist/modules/customers/components/calendar/TimeGrid.js +410 -0
  87. package/dist/modules/customers/components/calendar/TimeGrid.js.map +7 -0
  88. package/dist/modules/customers/components/calendar/UpcomingCards.js +175 -0
  89. package/dist/modules/customers/components/calendar/UpcomingCards.js.map +7 -0
  90. package/dist/modules/customers/components/calendar/editor/CategoryField.js +75 -0
  91. package/dist/modules/customers/components/calendar/editor/CategoryField.js.map +7 -0
  92. package/dist/modules/customers/components/calendar/editor/LocationField.js +30 -0
  93. package/dist/modules/customers/components/calendar/editor/LocationField.js.map +7 -0
  94. package/dist/modules/customers/components/calendar/editor/PeopleField.js +135 -0
  95. package/dist/modules/customers/components/calendar/editor/PeopleField.js.map +7 -0
  96. package/dist/modules/customers/components/calendar/editor/RelatedToField.js +218 -0
  97. package/dist/modules/customers/components/calendar/editor/RelatedToField.js.map +7 -0
  98. package/dist/modules/customers/components/calendar/editor/RepeatField.js +119 -0
  99. package/dist/modules/customers/components/calendar/editor/RepeatField.js.map +7 -0
  100. package/dist/modules/customers/components/calendar/editor/ScheduleSection.js +83 -0
  101. package/dist/modules/customers/components/calendar/editor/ScheduleSection.js.map +7 -0
  102. package/dist/modules/customers/components/calendar/editor/SegmentGroup.js +44 -0
  103. package/dist/modules/customers/components/calendar/editor/SegmentGroup.js.map +7 -0
  104. package/dist/modules/customers/components/calendar/editor/hooks.js +132 -0
  105. package/dist/modules/customers/components/calendar/editor/hooks.js.map +7 -0
  106. package/dist/modules/customers/components/calendar/editor/inputs.js +160 -0
  107. package/dist/modules/customers/components/calendar/editor/inputs.js.map +7 -0
  108. package/dist/modules/customers/components/calendar/editor/lookups.js +107 -0
  109. package/dist/modules/customers/components/calendar/editor/lookups.js.map +7 -0
  110. package/dist/modules/customers/components/calendar/types.js +30 -0
  111. package/dist/modules/customers/components/calendar/types.js.map +7 -0
  112. package/dist/modules/customers/components/calendar/useCalendarItems.js +131 -0
  113. package/dist/modules/customers/components/calendar/useCalendarItems.js.map +7 -0
  114. package/dist/modules/customers/components/calendar/useCalendarPreferences.js +41 -0
  115. package/dist/modules/customers/components/calendar/useCalendarPreferences.js.map +7 -0
  116. package/dist/modules/customers/lib/calendar/categories.js +28 -0
  117. package/dist/modules/customers/lib/calendar/categories.js.map +7 -0
  118. package/dist/modules/customers/lib/calendar/conflicts.js +72 -0
  119. package/dist/modules/customers/lib/calendar/conflicts.js.map +7 -0
  120. package/dist/modules/customers/lib/calendar/editorPayload.js +271 -0
  121. package/dist/modules/customers/lib/calendar/editorPayload.js.map +7 -0
  122. package/dist/modules/customers/lib/calendar/grid.js +48 -0
  123. package/dist/modules/customers/lib/calendar/grid.js.map +7 -0
  124. package/dist/modules/customers/lib/calendar/layout.js +39 -0
  125. package/dist/modules/customers/lib/calendar/layout.js.map +7 -0
  126. package/dist/modules/customers/lib/calendar/mapItem.js +79 -0
  127. package/dist/modules/customers/lib/calendar/mapItem.js.map +7 -0
  128. package/dist/modules/customers/lib/calendar/preferences.js +71 -0
  129. package/dist/modules/customers/lib/calendar/preferences.js.map +7 -0
  130. package/dist/modules/customers/lib/calendar/range.js +46 -0
  131. package/dist/modules/customers/lib/calendar/range.js.map +7 -0
  132. package/dist/modules/customers/lib/calendar/recurrence.js +113 -0
  133. package/dist/modules/customers/lib/calendar/recurrence.js.map +7 -0
  134. package/dist/modules/dashboards/services/widgetDataService.js +5 -5
  135. package/dist/modules/dashboards/services/widgetDataService.js.map +2 -2
  136. package/dist/modules/dictionaries/api/[dictionaryId]/entries/[entryId]/route.js +61 -1
  137. package/dist/modules/dictionaries/api/[dictionaryId]/entries/[entryId]/route.js.map +2 -2
  138. package/dist/modules/dictionaries/api/[dictionaryId]/entries/route.js +33 -1
  139. package/dist/modules/dictionaries/api/[dictionaryId]/entries/route.js.map +2 -2
  140. package/dist/modules/dictionaries/api/[dictionaryId]/route.js +61 -1
  141. package/dist/modules/dictionaries/api/[dictionaryId]/route.js.map +2 -2
  142. package/dist/modules/dictionaries/api/route.js +33 -1
  143. package/dist/modules/dictionaries/api/route.js.map +2 -2
  144. package/dist/modules/feature_toggles/lib/queries.js +36 -52
  145. package/dist/modules/feature_toggles/lib/queries.js.map +2 -2
  146. package/dist/modules/messages/commands/actions.js +81 -9
  147. package/dist/modules/messages/commands/actions.js.map +2 -2
  148. package/dist/modules/perspectives/api/[tableId]/[perspectiveId]/route.js +32 -1
  149. package/dist/modules/perspectives/api/[tableId]/[perspectiveId]/route.js.map +2 -2
  150. package/dist/modules/perspectives/api/[tableId]/roles/[roleId]/route.js +32 -1
  151. package/dist/modules/perspectives/api/[tableId]/roles/[roleId]/route.js.map +2 -2
  152. package/dist/modules/perspectives/api/[tableId]/route.js +73 -2
  153. package/dist/modules/perspectives/api/[tableId]/route.js.map +2 -2
  154. package/dist/modules/perspectives/services/perspectiveService.js +5 -3
  155. package/dist/modules/perspectives/services/perspectiveService.js.map +2 -2
  156. package/dist/modules/planner/api/access.js +9 -0
  157. package/dist/modules/planner/api/access.js.map +2 -2
  158. package/dist/modules/planner/api/availability-date-specific.js +22 -1
  159. package/dist/modules/planner/api/availability-date-specific.js.map +2 -2
  160. package/dist/modules/planner/api/availability-weekly.js +22 -1
  161. package/dist/modules/planner/api/availability-weekly.js.map +2 -2
  162. package/dist/modules/resources/api/resources/tags/assign/route.js +39 -0
  163. package/dist/modules/resources/api/resources/tags/assign/route.js.map +2 -2
  164. package/dist/modules/resources/api/resources/tags/unassign/route.js +39 -0
  165. package/dist/modules/resources/api/resources/tags/unassign/route.js.map +2 -2
  166. package/dist/modules/sales/api/settings/document-numbers/route.js +33 -1
  167. package/dist/modules/sales/api/settings/document-numbers/route.js.map +2 -2
  168. package/dist/modules/sales/api/settings/order-editing/route.js +35 -3
  169. package/dist/modules/sales/api/settings/order-editing/route.js.map +2 -2
  170. package/dist/modules/staff/api/leave-requests/accept/route.js +41 -0
  171. package/dist/modules/staff/api/leave-requests/accept/route.js.map +2 -2
  172. package/dist/modules/staff/api/leave-requests/reject/route.js +41 -0
  173. package/dist/modules/staff/api/leave-requests/reject/route.js.map +2 -2
  174. package/dist/modules/staff/api/team-members/self/route.js +40 -0
  175. package/dist/modules/staff/api/team-members/self/route.js.map +2 -2
  176. package/dist/modules/staff/api/team-members/tags/assign/route.js +41 -0
  177. package/dist/modules/staff/api/team-members/tags/assign/route.js.map +2 -2
  178. package/dist/modules/staff/api/team-members/tags/unassign/route.js +41 -0
  179. package/dist/modules/staff/api/team-members/tags/unassign/route.js.map +2 -2
  180. package/dist/modules/translations/api/[entityType]/[entityId]/route.js +61 -1
  181. package/dist/modules/translations/api/[entityType]/[entityId]/route.js.map +2 -2
  182. package/dist/modules/translations/api/context.js +7 -0
  183. package/dist/modules/translations/api/context.js.map +2 -2
  184. package/dist/modules/translations/api/put/locales.js +33 -1
  185. package/dist/modules/translations/api/put/locales.js.map +2 -2
  186. package/dist/modules/workflows/backend/definitions/visual-editor/page.js +6 -7
  187. package/dist/modules/workflows/backend/definitions/visual-editor/page.js.map +2 -2
  188. package/dist/modules/workflows/backend/instances/[id]/page.js.map +1 -1
  189. package/dist/modules/workflows/components/EdgeEditDialog.js.map +1 -1
  190. package/dist/modules/workflows/components/EdgeEditDialogCrudForm.js.map +1 -1
  191. package/dist/modules/workflows/components/NodeEditDialog.js.map +1 -1
  192. package/dist/modules/workflows/components/NodeEditDialogCrudForm.js.map +1 -1
  193. package/dist/modules/workflows/components/WorkflowGraph.js.map +1 -1
  194. package/dist/modules/workflows/components/WorkflowGraphImpl.js +17 -9
  195. package/dist/modules/workflows/components/WorkflowGraphImpl.js.map +2 -2
  196. package/dist/modules/workflows/components/mobile/MobileVisualEditor.js.map +1 -1
  197. package/dist/modules/workflows/frontend/checkout-demo/deriveCurrentStep.js +8 -0
  198. package/dist/modules/workflows/frontend/checkout-demo/deriveCurrentStep.js.map +7 -0
  199. package/dist/modules/workflows/frontend/checkout-demo/page.js +2 -20
  200. package/dist/modules/workflows/frontend/checkout-demo/page.js.map +2 -2
  201. package/dist/modules/workflows/lib/graph-utils.js +8 -0
  202. package/dist/modules/workflows/lib/graph-utils.js.map +2 -2
  203. package/generated/entities/catalog_product/index.ts +26 -0
  204. package/generated/entities/catalog_product_variant/index.ts +2 -0
  205. package/generated/entity-fields-registry.ts +28 -0
  206. package/package.json +7 -7
  207. package/src/modules/catalog/api/products/route.ts +123 -39
  208. package/src/modules/catalog/api/variants/route.ts +4 -0
  209. package/src/modules/catalog/backend/catalog/products/[id]/page.tsx +43 -0
  210. package/src/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.tsx +14 -0
  211. package/src/modules/catalog/backend/catalog/products/[productId]/variants/create/page.tsx +2 -0
  212. package/src/modules/catalog/backend/catalog/products/create/page.tsx +41 -0
  213. package/src/modules/catalog/commands/products.ts +307 -2
  214. package/src/modules/catalog/commands/variants.ts +90 -1
  215. package/src/modules/catalog/components/products/ProductComplianceSection.tsx +497 -0
  216. package/src/modules/catalog/components/products/VariantBuilder.tsx +43 -0
  217. package/src/modules/catalog/components/products/productForm.ts +262 -0
  218. package/src/modules/catalog/components/products/variantForm.ts +4 -0
  219. package/src/modules/catalog/data/entities.ts +97 -0
  220. package/src/modules/catalog/data/types.ts +36 -0
  221. package/src/modules/catalog/data/validators.ts +159 -3
  222. package/src/modules/catalog/i18n/de.json +64 -0
  223. package/src/modules/catalog/i18n/en.json +64 -0
  224. package/src/modules/catalog/i18n/es.json +64 -0
  225. package/src/modules/catalog/i18n/pl.json +64 -0
  226. package/src/modules/catalog/lib/gtin.ts +66 -0
  227. package/src/modules/catalog/migrations/.snapshot-open-mercato.json +457 -0
  228. package/src/modules/catalog/migrations/Migration20260611090000.ts +17 -0
  229. package/src/modules/catalog/translations.ts +1 -1
  230. package/src/modules/configs/api/cache/route.ts +72 -10
  231. package/src/modules/configs/components/CachePanel.tsx +59 -29
  232. package/src/modules/currencies/api/currencies/options/route.ts +22 -4
  233. package/src/modules/currencies/services/rateFetchingService.ts +63 -26
  234. package/src/modules/customer_accounts/backend/customer_accounts/users/[id]/page.tsx +18 -18
  235. package/src/modules/customer_accounts/backend/customer_accounts/users/page.tsx +1 -1
  236. package/src/modules/customer_accounts/i18n/de.json +1 -1
  237. package/src/modules/customer_accounts/i18n/en.json +1 -1
  238. package/src/modules/customer_accounts/i18n/es.json +1 -1
  239. package/src/modules/customer_accounts/i18n/pl.json +1 -1
  240. package/src/modules/customers/backend/calendar/page.meta.ts +23 -0
  241. package/src/modules/customers/backend/calendar/page.tsx +12 -0
  242. package/src/modules/customers/cli.ts +1 -0
  243. package/src/modules/customers/components/calendar/AgendaList.tsx +219 -0
  244. package/src/modules/customers/components/calendar/CalendarEventEditor.tsx +386 -0
  245. package/src/modules/customers/components/calendar/CalendarFooter.tsx +61 -0
  246. package/src/modules/customers/components/calendar/CalendarHeader.tsx +40 -0
  247. package/src/modules/customers/components/calendar/CalendarScreen.tsx +590 -0
  248. package/src/modules/customers/components/calendar/CalendarSettingsModal.tsx +207 -0
  249. package/src/modules/customers/components/calendar/CalendarTabs.tsx +63 -0
  250. package/src/modules/customers/components/calendar/CalendarToolbar.tsx +295 -0
  251. package/src/modules/customers/components/calendar/EventBlock.tsx +205 -0
  252. package/src/modules/customers/components/calendar/EventPeekPopover.tsx +100 -0
  253. package/src/modules/customers/components/calendar/MonthGrid.tsx +236 -0
  254. package/src/modules/customers/components/calendar/ShortcutsDialog.tsx +57 -0
  255. package/src/modules/customers/components/calendar/TimeGrid.tsx +510 -0
  256. package/src/modules/customers/components/calendar/UpcomingCards.tsx +220 -0
  257. package/src/modules/customers/components/calendar/editor/CategoryField.tsx +79 -0
  258. package/src/modules/customers/components/calendar/editor/LocationField.tsx +37 -0
  259. package/src/modules/customers/components/calendar/editor/PeopleField.tsx +148 -0
  260. package/src/modules/customers/components/calendar/editor/RelatedToField.tsx +242 -0
  261. package/src/modules/customers/components/calendar/editor/RepeatField.tsx +132 -0
  262. package/src/modules/customers/components/calendar/editor/ScheduleSection.tsx +110 -0
  263. package/src/modules/customers/components/calendar/editor/SegmentGroup.tsx +50 -0
  264. package/src/modules/customers/components/calendar/editor/hooks.ts +162 -0
  265. package/src/modules/customers/components/calendar/editor/inputs.tsx +158 -0
  266. package/src/modules/customers/components/calendar/editor/lookups.ts +160 -0
  267. package/src/modules/customers/components/calendar/types.ts +163 -0
  268. package/src/modules/customers/components/calendar/useCalendarItems.ts +162 -0
  269. package/src/modules/customers/components/calendar/useCalendarPreferences.ts +52 -0
  270. package/src/modules/customers/i18n/de.json +195 -0
  271. package/src/modules/customers/i18n/en.json +195 -0
  272. package/src/modules/customers/i18n/es.json +195 -0
  273. package/src/modules/customers/i18n/pl.json +195 -0
  274. package/src/modules/customers/lib/calendar/categories.ts +26 -0
  275. package/src/modules/customers/lib/calendar/conflicts.ts +117 -0
  276. package/src/modules/customers/lib/calendar/editorPayload.ts +388 -0
  277. package/src/modules/customers/lib/calendar/grid.ts +50 -0
  278. package/src/modules/customers/lib/calendar/layout.ts +47 -0
  279. package/src/modules/customers/lib/calendar/mapItem.ts +94 -0
  280. package/src/modules/customers/lib/calendar/preferences.ts +95 -0
  281. package/src/modules/customers/lib/calendar/range.ts +45 -0
  282. package/src/modules/customers/lib/calendar/recurrence.ts +137 -0
  283. package/src/modules/dashboards/services/widgetDataService.ts +8 -6
  284. package/src/modules/dictionaries/api/[dictionaryId]/entries/[entryId]/route.ts +61 -1
  285. package/src/modules/dictionaries/api/[dictionaryId]/entries/route.ts +33 -1
  286. package/src/modules/dictionaries/api/[dictionaryId]/route.ts +65 -1
  287. package/src/modules/dictionaries/api/route.ts +36 -1
  288. package/src/modules/feature_toggles/lib/queries.ts +41 -60
  289. package/src/modules/messages/commands/actions.ts +100 -7
  290. package/src/modules/perspectives/api/[tableId]/[perspectiveId]/route.ts +34 -1
  291. package/src/modules/perspectives/api/[tableId]/roles/[roleId]/route.ts +34 -1
  292. package/src/modules/perspectives/api/[tableId]/route.ts +107 -32
  293. package/src/modules/perspectives/services/perspectiveService.ts +9 -5
  294. package/src/modules/planner/api/access.ts +9 -0
  295. package/src/modules/planner/api/availability-date-specific.ts +22 -1
  296. package/src/modules/planner/api/availability-weekly.ts +22 -1
  297. package/src/modules/resources/api/resources/tags/assign/route.ts +42 -0
  298. package/src/modules/resources/api/resources/tags/unassign/route.ts +42 -0
  299. package/src/modules/sales/api/settings/document-numbers/route.ts +35 -1
  300. package/src/modules/sales/api/settings/order-editing/route.ts +37 -3
  301. package/src/modules/staff/api/leave-requests/accept/route.ts +45 -0
  302. package/src/modules/staff/api/leave-requests/reject/route.ts +45 -0
  303. package/src/modules/staff/api/team-members/self/route.ts +43 -0
  304. package/src/modules/staff/api/team-members/tags/assign/route.ts +45 -0
  305. package/src/modules/staff/api/team-members/tags/unassign/route.ts +45 -0
  306. package/src/modules/translations/api/[entityType]/[entityId]/route.ts +65 -1
  307. package/src/modules/translations/api/context.ts +7 -0
  308. package/src/modules/translations/api/put/locales.ts +35 -1
  309. package/src/modules/workflows/backend/definitions/visual-editor/page.tsx +11 -9
  310. package/src/modules/workflows/backend/instances/[id]/page.tsx +1 -1
  311. package/src/modules/workflows/components/EdgeEditDialog.tsx +1 -1
  312. package/src/modules/workflows/components/EdgeEditDialogCrudForm.tsx +1 -1
  313. package/src/modules/workflows/components/NodeEditDialog.tsx +1 -1
  314. package/src/modules/workflows/components/NodeEditDialogCrudForm.tsx +1 -1
  315. package/src/modules/workflows/components/WorkflowGraph.tsx +2 -2
  316. package/src/modules/workflows/components/WorkflowGraphImpl.tsx +27 -13
  317. package/src/modules/workflows/components/mobile/MobileVisualEditor.tsx +3 -3
  318. package/src/modules/workflows/frontend/checkout-demo/deriveCurrentStep.ts +12 -0
  319. package/src/modules/workflows/frontend/checkout-demo/page.tsx +3 -23
  320. package/src/modules/workflows/lib/graph-utils.ts +21 -1
@@ -15,6 +15,13 @@ import {
15
15
  import { getMessageType } from '../lib/message-types-registry'
16
16
  import { assertOrganizationAccess, type MessageScopeInput } from './shared'
17
17
 
18
+ const actionStateSnapshotSchema = z.object({
19
+ actionTaken: z.string().nullable(),
20
+ actionTakenByUserId: z.string().nullable(),
21
+ actionTakenAt: z.string().nullable(),
22
+ actionResult: z.record(z.string(), z.unknown()).nullable(),
23
+ })
24
+
18
25
  const recordTerminalActionSchema = z.object({
19
26
  messageId: z.string().uuid(),
20
27
  actionId: z.string().min(1),
@@ -22,6 +29,10 @@ const recordTerminalActionSchema = z.object({
22
29
  tenantId: z.string().uuid(),
23
30
  organizationId: z.string().uuid().nullable(),
24
31
  userId: z.string().uuid(),
32
+ // Optional pre-claim snapshot supplied by `messages.actions.execute` so the
33
+ // undo log records the true (un-taken) state even though the terminal action
34
+ // was atomically claimed before this finalizer runs.
35
+ previousState: actionStateSnapshotSchema.optional(),
25
36
  })
26
37
 
27
38
  type RecordTerminalActionInput = z.infer<typeof recordTerminalActionSchema>
@@ -121,12 +132,35 @@ const recordTerminalActionCommand: CommandHandler<unknown, { ok: true }> = {
121
132
  const input = recordTerminalActionSchema.parse(rawInput)
122
133
  const em = (ctx.container.resolve('em') as EntityManager).fork()
123
134
  const message = await requireActionTarget(em, input)
124
- if (message.actionTaken) {
125
- throw new Error('Action already taken')
135
+ const claimedByCaller =
136
+ message.actionTaken === input.actionId && message.actionTakenByUserId === input.userId
137
+ if (!claimedByCaller) {
138
+ if (message.actionTaken) {
139
+ throw new Error('Action already taken')
140
+ }
141
+ // Atomic compare-and-set: only one concurrent request transitions the
142
+ // message out of the un-taken state. nativeUpdate runs as a single
143
+ // `UPDATE ... WHERE action_taken IS NULL` so the loser matches 0 rows.
144
+ const claimedRows = await em.nativeUpdate(
145
+ Message,
146
+ { id: input.messageId, tenantId: input.tenantId, actionTaken: null, deletedAt: null },
147
+ {
148
+ actionTaken: input.actionId,
149
+ actionTakenByUserId: input.userId,
150
+ actionTakenAt: new Date(),
151
+ },
152
+ )
153
+ if (claimedRows === 0) {
154
+ throw new Error('Action already taken')
155
+ }
156
+ message.actionTaken = input.actionId
157
+ message.actionTakenByUserId = input.userId
158
+ }
159
+ if (!message.actionTakenAt) {
160
+ message.actionTakenAt = new Date()
126
161
  }
127
- message.actionTaken = input.actionId
128
- message.actionTakenByUserId = input.userId
129
- message.actionTakenAt = new Date()
162
+ // action_result is an encrypted column, so it must be written through the
163
+ // flush path (the encryption subscriber) rather than nativeUpdate.
130
164
  message.actionResult = input.result
131
165
  await em.flush()
132
166
  return { ok: true }
@@ -139,6 +173,13 @@ const recordTerminalActionCommand: CommandHandler<unknown, { ok: true }> = {
139
173
  },
140
174
  buildLog: async ({ input, snapshots }) => {
141
175
  const parsed = recordTerminalActionSchema.parse(input)
176
+ // When the action was pre-claimed by `messages.actions.execute`, the
177
+ // prepare-captured snapshot already reflects the claimed state, so prefer
178
+ // the explicit pre-claim snapshot for the undo baseline.
179
+ const before =
180
+ (parsed.previousState as ActionStateSnapshot | undefined) ??
181
+ (snapshots.before as ActionStateSnapshot | undefined) ??
182
+ null
142
183
  return {
143
184
  actionLabel: 'Execute message terminal action',
144
185
  resourceKind: 'messages.message',
@@ -147,11 +188,11 @@ const recordTerminalActionCommand: CommandHandler<unknown, { ok: true }> = {
147
188
  organizationId: parsed.organizationId,
148
189
  payload: {
149
190
  undo: {
150
- before: (snapshots.before as ActionStateSnapshot | undefined) ?? null,
191
+ before,
151
192
  after: (snapshots.after as ActionStateSnapshot | undefined) ?? null,
152
193
  } satisfies UndoPayload<ActionStateSnapshot>,
153
194
  },
154
- snapshotBefore: snapshots.before ?? null,
195
+ snapshotBefore: before,
155
196
  snapshotAfter: snapshots.after ?? null,
156
197
  }
157
198
  },
@@ -209,6 +250,52 @@ const executeActionCommand: CommandHandler<
209
250
  }
210
251
  }
211
252
 
253
+ // Capture the un-taken state for the undo log before reserving the action.
254
+ const previousState = snapshotActionState(message)
255
+ let claimedTerminal = false
256
+ const releaseTerminalClaim = async () => {
257
+ if (!claimedTerminal) return
258
+ claimedTerminal = false
259
+ await em.nativeUpdate(
260
+ Message,
261
+ {
262
+ id: message.id,
263
+ tenantId: input.tenantId,
264
+ actionTaken: action.id,
265
+ actionTakenByUserId: input.userId,
266
+ },
267
+ { actionTaken: null, actionTakenByUserId: null, actionTakenAt: null },
268
+ )
269
+ }
270
+
271
+ if (shouldRecordActionTaken) {
272
+ // Atomically reserve the terminal action BEFORE running the target
273
+ // command so concurrent requests cannot both execute it. The losing
274
+ // request matches 0 rows and surfaces the existing 409 response.
275
+ const claimedRows = await em.nativeUpdate(
276
+ Message,
277
+ { id: message.id, tenantId: input.tenantId, actionTaken: null, deletedAt: null },
278
+ {
279
+ actionTaken: action.id,
280
+ actionTakenByUserId: input.userId,
281
+ actionTakenAt: new Date(),
282
+ },
283
+ )
284
+ if (claimedRows === 0) {
285
+ const current = await findOneWithDecryption(
286
+ em,
287
+ Message,
288
+ { id: message.id, tenantId: input.tenantId, deletedAt: null },
289
+ undefined,
290
+ { tenantId: input.tenantId, organizationId: input.organizationId },
291
+ )
292
+ throw Object.assign(new Error('Action already taken'), {
293
+ actionTaken: current?.actionTaken ?? message.actionTaken ?? action.id,
294
+ })
295
+ }
296
+ claimedTerminal = true
297
+ }
298
+
212
299
  const commandBus = ctx.container.resolve('commandBus') as {
213
300
  execute: (
214
301
  commandId: string,
@@ -263,6 +350,9 @@ const executeActionCommand: CommandHandler<
263
350
  operationLogEntry = commandResult.logEntry ?? null
264
351
  } catch (err) {
265
352
  console.error('[messages] executeActionCommand sub-command failed', err)
353
+ // The target command never completed — release the reservation so the
354
+ // action stays retryable, matching the pre-claim failure semantics.
355
+ await releaseTerminalClaim()
266
356
  throw new Error('Action failed')
267
357
  }
268
358
  } else if (action.href) {
@@ -272,10 +362,12 @@ const executeActionCommand: CommandHandler<
272
362
  userId: input.userId,
273
363
  })
274
364
  if (!safeRedirect) {
365
+ await releaseTerminalClaim()
275
366
  throw new Error('Action has an unsafe redirect target')
276
367
  }
277
368
  result = { redirect: safeRedirect }
278
369
  } else {
370
+ await releaseTerminalClaim()
279
371
  throw new Error('Action has no executable target')
280
372
  }
281
373
 
@@ -288,6 +380,7 @@ const executeActionCommand: CommandHandler<
288
380
  tenantId: input.tenantId,
289
381
  organizationId: input.organizationId,
290
382
  userId: input.userId,
383
+ previousState,
291
384
  },
292
385
  ctx: {
293
386
  container: ctx.container,
@@ -2,6 +2,10 @@ import { NextResponse } from 'next/server'
2
2
  import { z } from 'zod'
3
3
  import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
4
4
  import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
5
+ import {
6
+ runCrudMutationGuardAfterSuccess,
7
+ validateCrudMutationGuard,
8
+ } from '@open-mercato/shared/lib/crud/mutation-guard'
5
9
  import { deleteUserPerspective } from '@open-mercato/core/modules/perspectives/services/perspectiveService'
6
10
  import type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
7
11
  import { perspectivesTag, perspectivesErrorSchema, perspectivesSuccessSchema } from '../../openapi'
@@ -40,7 +44,22 @@ export async function DELETE(req: Request, ctx: { params: { tableId: string; per
40
44
  }
41
45
  })()
42
46
 
43
- await deleteUserPerspective(em, cache, {
47
+ const guardResult = await validateCrudMutationGuard(container, {
48
+ tenantId: auth.tenantId ?? '',
49
+ organizationId: auth.orgId ?? null,
50
+ userId: auth.sub,
51
+ resourceKind: 'perspectives.perspective',
52
+ resourceId: perspectiveId,
53
+ operation: 'delete',
54
+ requestMethod: req.method,
55
+ requestHeaders: req.headers,
56
+ mutationPayload: { tableId, perspectiveId },
57
+ })
58
+ if (guardResult && !guardResult.ok) {
59
+ return NextResponse.json(guardResult.body, { status: guardResult.status })
60
+ }
61
+
62
+ const deleted = await deleteUserPerspective(em, cache, {
44
63
  scope: {
45
64
  userId: auth.sub,
46
65
  tenantId: auth.tenantId ?? null,
@@ -50,6 +69,20 @@ export async function DELETE(req: Request, ctx: { params: { tableId: string; per
50
69
  perspectiveId,
51
70
  })
52
71
 
72
+ if (deleted && guardResult?.ok && guardResult.shouldRunAfterSuccess) {
73
+ await runCrudMutationGuardAfterSuccess(container, {
74
+ tenantId: auth.tenantId ?? '',
75
+ organizationId: auth.orgId ?? null,
76
+ userId: auth.sub,
77
+ resourceKind: 'perspectives.perspective',
78
+ resourceId: perspectiveId,
79
+ operation: 'delete',
80
+ requestMethod: req.method,
81
+ requestHeaders: req.headers,
82
+ metadata: guardResult.metadata ?? null,
83
+ })
84
+ }
85
+
53
86
  return NextResponse.json({ success: true })
54
87
  }
55
88
 
@@ -2,6 +2,10 @@ import { NextResponse } from 'next/server'
2
2
  import { z } from 'zod'
3
3
  import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
4
4
  import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
5
+ import {
6
+ runCrudMutationGuardAfterSuccess,
7
+ validateCrudMutationGuard,
8
+ } from '@open-mercato/shared/lib/crud/mutation-guard'
5
9
  import { clearRolePerspectives } from '@open-mercato/core/modules/perspectives/services/perspectiveService'
6
10
  import { Role } from '@open-mercato/core/modules/auth/data/entities'
7
11
  import type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
@@ -48,13 +52,42 @@ export async function DELETE(req: Request, ctx: { params: { tableId: string; rol
48
52
  const role = await em.findOne(Role, { id: roleId, deletedAt: null, ...(scope as any) } as any)
49
53
  if (!role) return NextResponse.json({ error: 'Role not found' }, { status: 404 })
50
54
 
51
- await clearRolePerspectives(em, cache, {
55
+ const guardResult = await validateCrudMutationGuard(container, {
56
+ tenantId: auth.tenantId ?? '',
57
+ organizationId: auth.orgId ?? null,
58
+ userId: auth.sub,
59
+ resourceKind: 'perspectives.role_perspective',
60
+ resourceId: roleId,
61
+ operation: 'delete',
62
+ requestMethod: req.method,
63
+ requestHeaders: req.headers,
64
+ mutationPayload: { tableId, roleId },
65
+ })
66
+ if (guardResult && !guardResult.ok) {
67
+ return NextResponse.json(guardResult.body, { status: guardResult.status })
68
+ }
69
+
70
+ const clearedCount = await clearRolePerspectives(em, cache, {
52
71
  tableId,
53
72
  tenantId: auth.tenantId ?? null,
54
73
  organizationId: auth.orgId ?? null,
55
74
  roleIds: [roleId],
56
75
  })
57
76
 
77
+ if (clearedCount > 0 && guardResult?.ok && guardResult.shouldRunAfterSuccess) {
78
+ await runCrudMutationGuardAfterSuccess(container, {
79
+ tenantId: auth.tenantId ?? '',
80
+ organizationId: auth.orgId ?? null,
81
+ userId: auth.sub,
82
+ resourceKind: 'perspectives.role_perspective',
83
+ resourceId: roleId,
84
+ operation: 'delete',
85
+ requestMethod: req.method,
86
+ requestHeaders: req.headers,
87
+ metadata: guardResult.metadata ?? null,
88
+ })
89
+ }
90
+
58
91
  return NextResponse.json({ success: true })
59
92
  }
60
93
 
@@ -4,6 +4,10 @@ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
4
4
  import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
5
5
  import { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'
6
6
  import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
7
+ import {
8
+ runCrudMutationGuardAfterSuccess,
9
+ validateCrudMutationGuard,
10
+ } from '@open-mercato/shared/lib/crud/mutation-guard'
7
11
  import { perspectiveSaveSchema } from '@open-mercato/core/modules/perspectives/data/validators'
8
12
  import {
9
13
  loadPerspectivesState,
@@ -168,6 +172,7 @@ export async function POST(req: Request, ctx: { params: { tableId: string } }) {
168
172
  const applyToRoles = Array.from(new Set(parsed.data.applyToRoles ?? [])).filter((id) => id.trim().length > 0)
169
173
  const clearRoleIds = Array.from(new Set(parsed.data.clearRoleIds ?? [])).filter((id) => id.trim().length > 0)
170
174
  const hasRoleOps = applyToRoles.length > 0 || clearRoleIds.length > 0
175
+ const targetRoleIds = Array.from(new Set([...applyToRoles, ...clearRoleIds]))
171
176
 
172
177
  if (hasRoleOps) {
173
178
  const canApplyToRoles = await rbac.userHasAllFeatures?.(
@@ -183,7 +188,6 @@ export async function POST(req: Request, ctx: { params: { tableId: string } }) {
183
188
  const roleScope = auth.tenantId
184
189
  ? { $or: [{ tenantId: auth.tenantId }, { tenantId: null }] }
185
190
  : { tenantId: null }
186
- const targetRoleIds = Array.from(new Set([...applyToRoles, ...clearRoleIds]))
187
191
  const roles = await em.find(Role, {
188
192
  id: { $in: targetRoleIds as any },
189
193
  ...(roleScope as any),
@@ -197,44 +201,86 @@ export async function POST(req: Request, ctx: { params: { tableId: string } }) {
197
201
  }
198
202
  }
199
203
 
204
+ const guardResourceId = parsed.data.perspectiveId ?? tableId
205
+ const guardResult = await validateCrudMutationGuard(container, {
206
+ tenantId: auth.tenantId ?? '',
207
+ organizationId: auth.orgId ?? null,
208
+ userId: auth.sub,
209
+ resourceKind: 'perspectives.perspective',
210
+ resourceId: guardResourceId,
211
+ operation: 'custom',
212
+ requestMethod: req.method,
213
+ requestHeaders: req.headers,
214
+ mutationPayload: { ...parsed.data, tableId },
215
+ })
216
+ if (guardResult && !guardResult.ok) {
217
+ return NextResponse.json(guardResult.body, { status: guardResult.status })
218
+ }
219
+
220
+ let roleGuardResult: Awaited<ReturnType<typeof validateCrudMutationGuard>> | null = null
221
+ if (hasRoleOps) {
222
+ roleGuardResult = await validateCrudMutationGuard(container, {
223
+ tenantId: auth.tenantId ?? '',
224
+ organizationId: auth.orgId ?? null,
225
+ userId: auth.sub,
226
+ resourceKind: 'perspectives.role_perspective',
227
+ resourceId: targetRoleIds.join(','),
228
+ operation: 'custom',
229
+ requestMethod: req.method,
230
+ requestHeaders: req.headers,
231
+ mutationPayload: {
232
+ tableId,
233
+ applyToRoles,
234
+ clearRoleIds,
235
+ name: parsed.data.name,
236
+ settings: parsed.data.settings,
237
+ setRoleDefault: parsed.data.setRoleDefault ?? false,
238
+ },
239
+ })
240
+ }
241
+ if (roleGuardResult && !roleGuardResult.ok) {
242
+ return NextResponse.json(roleGuardResult.body, { status: roleGuardResult.status })
243
+ }
244
+
200
245
  let saved: Awaited<ReturnType<typeof saveUserPerspective>> | null = null
201
246
  let updatedRolePerspectives: Awaited<ReturnType<typeof saveRolePerspectives>> | null = null
247
+ let clearedRolePerspectiveCount = 0
202
248
 
203
249
  try {
204
250
  await withAtomicFlush(em, [
205
- async () => {
206
- saved = await saveUserPerspective(em, cache, {
207
- scope,
208
- tableId,
209
- input: parsed.data,
210
- request: req,
211
- })
212
- },
213
- async () => {
214
- if (applyToRoles.length) {
215
- updatedRolePerspectives = await saveRolePerspectives(em, cache, {
216
- tableId,
217
- tenantId: auth.tenantId ?? null,
218
- organizationId: auth.orgId ?? null,
219
- input: {
220
- roleIds: applyToRoles,
221
- name: parsed.data.name,
222
- settings: parsed.data.settings,
223
- setDefault: parsed.data.setRoleDefault ?? false,
224
- },
225
- })
226
- }
227
- },
228
- async () => {
229
- if (clearRoleIds.length) {
230
- await clearRolePerspectives(em, cache, {
251
+ async () => {
252
+ saved = await saveUserPerspective(em, cache, {
253
+ scope,
231
254
  tableId,
232
- tenantId: auth.tenantId ?? null,
233
- organizationId: auth.orgId ?? null,
234
- roleIds: clearRoleIds,
255
+ input: parsed.data,
256
+ request: req,
235
257
  })
236
- }
237
- },
258
+ },
259
+ async () => {
260
+ if (applyToRoles.length) {
261
+ updatedRolePerspectives = await saveRolePerspectives(em, cache, {
262
+ tableId,
263
+ tenantId: auth.tenantId ?? null,
264
+ organizationId: auth.orgId ?? null,
265
+ input: {
266
+ roleIds: applyToRoles,
267
+ name: parsed.data.name,
268
+ settings: parsed.data.settings,
269
+ setDefault: parsed.data.setRoleDefault ?? false,
270
+ },
271
+ })
272
+ }
273
+ },
274
+ async () => {
275
+ if (clearRoleIds.length) {
276
+ clearedRolePerspectiveCount = await clearRolePerspectives(em, cache, {
277
+ tableId,
278
+ tenantId: auth.tenantId ?? null,
279
+ organizationId: auth.orgId ?? null,
280
+ roleIds: clearRoleIds,
281
+ })
282
+ }
283
+ },
238
284
  ], { transaction: true })
239
285
  } catch (err) {
240
286
  if (isCrudHttpError(err)) {
@@ -243,6 +289,35 @@ export async function POST(req: Request, ctx: { params: { tableId: string } }) {
243
289
  throw err
244
290
  }
245
291
 
292
+ if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
293
+ await runCrudMutationGuardAfterSuccess(container, {
294
+ tenantId: auth.tenantId ?? '',
295
+ organizationId: auth.orgId ?? null,
296
+ userId: auth.sub,
297
+ resourceKind: 'perspectives.perspective',
298
+ resourceId: guardResourceId,
299
+ operation: 'custom',
300
+ requestMethod: req.method,
301
+ requestHeaders: req.headers,
302
+ metadata: guardResult.metadata ?? null,
303
+ })
304
+ }
305
+
306
+ const didWriteRolePerspectives = applyToRoles.length > 0 || clearedRolePerspectiveCount > 0
307
+ if (didWriteRolePerspectives && roleGuardResult?.ok && roleGuardResult.shouldRunAfterSuccess) {
308
+ await runCrudMutationGuardAfterSuccess(container, {
309
+ tenantId: auth.tenantId ?? '',
310
+ organizationId: auth.orgId ?? null,
311
+ userId: auth.sub,
312
+ resourceKind: 'perspectives.role_perspective',
313
+ resourceId: targetRoleIds.join(','),
314
+ operation: 'custom',
315
+ requestMethod: req.method,
316
+ requestHeaders: req.headers,
317
+ metadata: roleGuardResult.metadata ?? null,
318
+ })
319
+ }
320
+
246
321
  return NextResponse.json({
247
322
  perspective: saved,
248
323
  rolePerspectives: updatedRolePerspectives ?? [],
@@ -309,7 +309,7 @@ export async function deleteUserPerspective(
309
309
  em: EntityManager,
310
310
  cache: CacheStrategy | null | undefined,
311
311
  options: { scope: PerspectiveScope; tableId: string; perspectiveId: string },
312
- ): Promise<void> {
312
+ ): Promise<boolean> {
313
313
  const { scope, tableId, perspectiveId } = options
314
314
  const tenantId = scope.tenantId ?? null
315
315
  const organizationId = scope.organizationId ?? null
@@ -322,7 +322,7 @@ export async function deleteUserPerspective(
322
322
  tableId,
323
323
  deletedAt: null,
324
324
  })
325
- if (!existing) return
325
+ if (!existing) return false
326
326
 
327
327
  existing.deletedAt = new Date()
328
328
  existing.isDefault = false
@@ -331,6 +331,8 @@ export async function deleteUserPerspective(
331
331
  if (cache?.deleteByTags) {
332
332
  await cache.deleteByTags([userTag(scope, tableId)])
333
333
  }
334
+
335
+ return true
334
336
  }
335
337
 
336
338
  export async function saveRolePerspectives(
@@ -430,13 +432,13 @@ export async function clearRolePerspectives(
430
432
  organizationId?: string | null
431
433
  roleIds: string[]
432
434
  },
433
- ): Promise<void> {
435
+ ): Promise<number> {
434
436
  const { tableId, roleIds } = options
435
437
  const tenantId = options.tenantId ?? null
436
438
  const organizationId = options.organizationId ?? null
437
- if (!roleIds.length) return
439
+ if (!roleIds.length) return 0
438
440
 
439
- await em.nativeUpdate(
441
+ const affected = await em.nativeUpdate(
440
442
  RolePerspective,
441
443
  {
442
444
  roleId: { $in: roleIds as any },
@@ -452,4 +454,6 @@ export async function clearRolePerspectives(
452
454
  const tags = roleIds.map((roleId) => roleTag(roleId, tableId, tenantId))
453
455
  await cache.deleteByTags(tags)
454
456
  }
457
+
458
+ return affected
455
459
  }
@@ -79,3 +79,12 @@ export async function assertAvailabilityWriteAccess(
79
79
  }
80
80
  return access
81
81
  }
82
+
83
+ export function resolveAvailabilityActorId(auth: AuthContext): string {
84
+ if (auth) {
85
+ if (typeof auth.sub === 'string' && auth.sub.trim().length > 0) return auth.sub
86
+ if (typeof auth.userId === 'string' && auth.userId.trim().length > 0) return auth.userId
87
+ if (typeof auth.keyId === 'string' && auth.keyId.trim().length > 0) return auth.keyId
88
+ }
89
+ return 'system'
90
+ }
@@ -11,9 +11,13 @@ import { parseScopedCommandInput } from '@open-mercato/shared/lib/api/scoped'
11
11
  import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
12
12
  import { plannerAvailabilityDateSpecificReplaceSchema } from '../data/validators'
13
13
  import { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'
14
+ import {
15
+ runCrudMutationGuardAfterSuccess,
16
+ validateCrudMutationGuard,
17
+ } from '@open-mercato/shared/lib/crud/mutation-guard'
14
18
  import { PlannerAvailabilityRule } from '../data/entities'
15
19
  import { parseAvailabilityRuleWindow } from '../lib/availabilitySchedule'
16
- import { assertAvailabilityWriteAccess } from './access'
20
+ import { assertAvailabilityWriteAccess, resolveAvailabilityActorId } from './access'
17
21
 
18
22
  export const metadata = {
19
23
  POST: { requireAuth: true },
@@ -93,8 +97,25 @@ export async function POST(req: Request) {
93
97
  }
94
98
  }
95
99
  }
100
+ const guardInput = {
101
+ tenantId: input.tenantId,
102
+ organizationId: input.organizationId,
103
+ userId: resolveAvailabilityActorId(ctx.auth),
104
+ resourceKind: 'planner.availability',
105
+ resourceId: input.subjectId,
106
+ operation: 'custom' as const,
107
+ requestMethod: req.method,
108
+ requestHeaders: req.headers,
109
+ }
110
+ const guardResult = await validateCrudMutationGuard(ctx.container, { ...guardInput, mutationPayload: input })
111
+ if (guardResult && !guardResult.ok) {
112
+ return NextResponse.json(guardResult.body, { status: guardResult.status })
113
+ }
96
114
  const commandBus = ctx.container.resolve('commandBus') as CommandBus
97
115
  const { logEntry } = await commandBus.execute('planner.availability.date-specific.replace', { input, ctx })
116
+ if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
117
+ await runCrudMutationGuardAfterSuccess(ctx.container, { ...guardInput, metadata: guardResult.metadata ?? null })
118
+ }
98
119
  const response = NextResponse.json({ ok: true })
99
120
  if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {
100
121
  response.headers.set(
@@ -9,7 +9,11 @@ import { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/er
9
9
  import { parseScopedCommandInput } from '@open-mercato/shared/lib/api/scoped'
10
10
  import { plannerAvailabilityWeeklyReplaceSchema } from '../data/validators'
11
11
  import { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'
12
- import { assertAvailabilityWriteAccess } from './access'
12
+ import {
13
+ runCrudMutationGuardAfterSuccess,
14
+ validateCrudMutationGuard,
15
+ } from '@open-mercato/shared/lib/crud/mutation-guard'
16
+ import { assertAvailabilityWriteAccess, resolveAvailabilityActorId } from './access'
13
17
 
14
18
  export const metadata = {
15
19
  POST: { requireAuth: true },
@@ -55,8 +59,25 @@ export async function POST(req: Request) {
55
59
  const payload = await req.json().catch(() => ({}))
56
60
  const input = parseScopedCommandInput(plannerAvailabilityWeeklyReplaceSchema, payload, ctx, translate)
57
61
  await assertAvailabilityWriteAccess(ctx, { subjectType: input.subjectType, subjectId: input.subjectId }, translate)
62
+ const guardInput = {
63
+ tenantId: input.tenantId,
64
+ organizationId: input.organizationId,
65
+ userId: resolveAvailabilityActorId(ctx.auth),
66
+ resourceKind: 'planner.availability',
67
+ resourceId: input.subjectId,
68
+ operation: 'custom' as const,
69
+ requestMethod: req.method,
70
+ requestHeaders: req.headers,
71
+ }
72
+ const guardResult = await validateCrudMutationGuard(ctx.container, { ...guardInput, mutationPayload: input })
73
+ if (guardResult && !guardResult.ok) {
74
+ return NextResponse.json(guardResult.body, { status: guardResult.status })
75
+ }
58
76
  const commandBus = ctx.container.resolve('commandBus') as CommandBus
59
77
  const { logEntry } = await commandBus.execute('planner.availability.weekly.replace', { input, ctx })
78
+ if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
79
+ await runCrudMutationGuardAfterSuccess(ctx.container, { ...guardInput, metadata: guardResult.metadata ?? null })
80
+ }
60
81
  const response = NextResponse.json({ ok: true })
61
82
  if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {
62
83
  response.headers.set(
@@ -9,6 +9,10 @@ import { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/er
9
9
  import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
10
10
  import { parseScopedCommandInput } from '@open-mercato/shared/lib/api/scoped'
11
11
  import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
12
+ import {
13
+ runCrudMutationGuardAfterSuccess,
14
+ validateCrudMutationGuard,
15
+ } from '@open-mercato/shared/lib/crud/mutation-guard'
12
16
  import {
13
17
  resourcesResourceTagAssignmentSchema,
14
18
  type ResourcesResourceTagAssignmentInput,
@@ -37,16 +41,54 @@ async function buildContext(
37
41
  return { ctx, translate }
38
42
  }
39
43
 
44
+ function resolveActorId(ctx: CommandRuntimeContext): string {
45
+ const auth = ctx.auth
46
+ if (auth && typeof auth.sub === 'string' && auth.sub.trim().length > 0) return auth.sub
47
+ if (auth && typeof auth.userId === 'string' && auth.userId.trim().length > 0) return auth.userId
48
+ if (auth && typeof auth.keyId === 'string' && auth.keyId.trim().length > 0) return auth.keyId
49
+ return 'system'
50
+ }
51
+
40
52
  export async function POST(req: Request) {
41
53
  try {
42
54
  const { ctx, translate } = await buildContext(req)
43
55
  const body = await req.json().catch(() => ({}))
44
56
  const input = parseScopedCommandInput(resourcesResourceTagAssignmentSchema, body, ctx, translate)
57
+ const actorId = resolveActorId(ctx)
58
+ const guardResult = await validateCrudMutationGuard(ctx.container, {
59
+ tenantId: input.tenantId,
60
+ organizationId: input.organizationId,
61
+ userId: actorId,
62
+ resourceKind: 'resources.resourceTagAssignment',
63
+ resourceId: input.resourceId,
64
+ operation: 'custom',
65
+ requestMethod: req.method,
66
+ requestHeaders: req.headers,
67
+ mutationPayload: input,
68
+ })
69
+ if (guardResult && !guardResult.ok) {
70
+ return NextResponse.json(guardResult.body, { status: guardResult.status })
71
+ }
72
+
45
73
  const commandBus = (ctx.container.resolve('commandBus') as CommandBus)
46
74
  const { result, logEntry } = await commandBus.execute<ResourcesResourceTagAssignmentInput, { assignmentId: string }>(
47
75
  'resources.resourceTags.assign',
48
76
  { input, ctx },
49
77
  )
78
+ if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
79
+ await runCrudMutationGuardAfterSuccess(ctx.container, {
80
+ tenantId: input.tenantId,
81
+ organizationId: input.organizationId,
82
+ userId: actorId,
83
+ resourceKind: 'resources.resourceTagAssignment',
84
+ resourceId: input.resourceId,
85
+ operation: 'custom',
86
+ requestMethod: req.method,
87
+ requestHeaders: req.headers,
88
+ metadata: guardResult.metadata ?? null,
89
+ })
90
+ }
91
+
50
92
  const response = NextResponse.json({ id: result?.assignmentId ?? null }, { status: 201 })
51
93
  if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {
52
94
  response.headers.set(