@open-mercato/core 0.6.6 → 0.6.7-develop.6553.1.2ec7246e04

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 (271) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/dist/generated/entities/gateway_session_initialization/index.js +23 -0
  3. package/dist/generated/entities/gateway_session_initialization/index.js.map +7 -0
  4. package/dist/generated/entities.ids.generated.js +1 -0
  5. package/dist/generated/entities.ids.generated.js.map +2 -2
  6. package/dist/generated/entity-fields-registry.js +12 -0
  7. package/dist/generated/entity-fields-registry.js.map +2 -2
  8. package/dist/modules/api_keys/api/keys/route.js +30 -13
  9. package/dist/modules/api_keys/api/keys/route.js.map +2 -2
  10. package/dist/modules/api_keys/cli.js +2 -1
  11. package/dist/modules/api_keys/cli.js.map +2 -2
  12. package/dist/modules/attachments/api/file/[id]/route.js +9 -7
  13. package/dist/modules/attachments/api/file/[id]/route.js.map +2 -2
  14. package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js +9 -7
  15. package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js.map +2 -2
  16. package/dist/modules/attachments/api/route.js +21 -17
  17. package/dist/modules/attachments/api/route.js.map +2 -2
  18. package/dist/modules/attachments/components/AttachmentContentPreview.js +6 -35
  19. package/dist/modules/attachments/components/AttachmentContentPreview.js.map +2 -2
  20. package/dist/modules/attachments/lib/reconcileOrganization.js +110 -0
  21. package/dist/modules/attachments/lib/reconcileOrganization.js.map +7 -0
  22. package/dist/modules/attachments/lib/requestScope.js +10 -0
  23. package/dist/modules/attachments/lib/requestScope.js.map +7 -0
  24. package/dist/modules/audit_logs/backend/audit-logs/page.js +14 -24
  25. package/dist/modules/audit_logs/backend/audit-logs/page.js.map +2 -2
  26. package/dist/modules/auth/api/autologin.js +85 -0
  27. package/dist/modules/auth/api/autologin.js.map +7 -0
  28. package/dist/modules/auth/api/users/route.js +6 -2
  29. package/dist/modules/auth/api/users/route.js.map +2 -2
  30. package/dist/modules/auth/cli.js +3 -2
  31. package/dist/modules/auth/cli.js.map +2 -2
  32. package/dist/modules/auth/lib/autologin.js +19 -0
  33. package/dist/modules/auth/lib/autologin.js.map +7 -0
  34. package/dist/modules/configs/lib/upgrade-actions.js +22 -0
  35. package/dist/modules/configs/lib/upgrade-actions.js.map +2 -2
  36. package/dist/modules/customer_accounts/api/admin/roles/[id]/acl.js +1 -0
  37. package/dist/modules/customer_accounts/api/admin/roles/[id]/acl.js.map +2 -2
  38. package/dist/modules/customer_accounts/api/admin/roles/[id].js +17 -3
  39. package/dist/modules/customer_accounts/api/admin/roles/[id].js.map +2 -2
  40. package/dist/modules/customer_accounts/api/portal/users/[id]/roles.js +6 -1
  41. package/dist/modules/customer_accounts/api/portal/users/[id]/roles.js.map +2 -2
  42. package/dist/modules/customer_accounts/api/portal/users-invite.js +6 -1
  43. package/dist/modules/customer_accounts/api/portal/users-invite.js.map +2 -2
  44. package/dist/modules/customer_accounts/services/customerInvitationService.js +1 -0
  45. package/dist/modules/customer_accounts/services/customerInvitationService.js.map +2 -2
  46. package/dist/modules/customer_accounts/services/customerRbacService.js +6 -1
  47. package/dist/modules/customer_accounts/services/customerRbacService.js.map +2 -2
  48. package/dist/modules/customer_accounts/widgets/injection-table.js +0 -2
  49. package/dist/modules/customer_accounts/widgets/injection-table.js.map +2 -2
  50. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +7 -1
  51. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
  52. package/dist/modules/customers/backend/customers/deals/pipeline/components/ViewTabsRow.js +2 -2
  53. package/dist/modules/customers/backend/customers/deals/pipeline/components/ViewTabsRow.js.map +2 -2
  54. package/dist/modules/customers/components/detail/CompanyDetailTabs.js +37 -43
  55. package/dist/modules/customers/components/detail/CompanyDetailTabs.js.map +2 -2
  56. package/dist/modules/customers/components/detail/DealDetailTabs.js +23 -35
  57. package/dist/modules/customers/components/detail/DealDetailTabs.js.map +2 -2
  58. package/dist/modules/customers/components/detail/DetailTabsLayout.js +6 -21
  59. package/dist/modules/customers/components/detail/DetailTabsLayout.js.map +2 -2
  60. package/dist/modules/customers/components/detail/MobilePersonDetail.js +26 -32
  61. package/dist/modules/customers/components/detail/MobilePersonDetail.js.map +2 -2
  62. package/dist/modules/customers/components/detail/PersonDetailTabs.js +29 -39
  63. package/dist/modules/customers/components/detail/PersonDetailTabs.js.map +2 -2
  64. package/dist/modules/customers/components/detail/useDealsAccess.js +16 -0
  65. package/dist/modules/customers/components/detail/useDealsAccess.js.map +7 -0
  66. package/dist/modules/dashboards/api/roles/widgets/route.js +22 -3
  67. package/dist/modules/dashboards/api/roles/widgets/route.js.map +2 -2
  68. package/dist/modules/dashboards/api/users/widgets/route.js +22 -3
  69. package/dist/modules/dashboards/api/users/widgets/route.js.map +2 -2
  70. package/dist/modules/dashboards/cli.js +4 -3
  71. package/dist/modules/dashboards/cli.js.map +2 -2
  72. package/dist/modules/dashboards/lib/widgetAssignmentScope.js +9 -1
  73. package/dist/modules/dashboards/lib/widgetAssignmentScope.js.map +2 -2
  74. package/dist/modules/dictionaries/commands/entries.js +7 -14
  75. package/dist/modules/dictionaries/commands/entries.js.map +2 -2
  76. package/dist/modules/dictionaries/commands/entry-operations.js +7 -16
  77. package/dist/modules/dictionaries/commands/entry-operations.js.map +2 -2
  78. package/dist/modules/dictionaries/commands/factory.js +9 -3
  79. package/dist/modules/dictionaries/commands/factory.js.map +2 -2
  80. package/dist/modules/dictionaries/fields/dictionary.js +56 -41
  81. package/dist/modules/dictionaries/fields/dictionary.js.map +2 -2
  82. package/dist/modules/directory/utils/organizationScope.js +10 -3
  83. package/dist/modules/directory/utils/organizationScope.js.map +2 -2
  84. package/dist/modules/entities/api/records.js +5 -4
  85. package/dist/modules/entities/api/records.js.map +2 -2
  86. package/dist/modules/entities/backend/entities/system/[entityId]/page.meta.js +1 -0
  87. package/dist/modules/entities/backend/entities/system/[entityId]/page.meta.js.map +2 -2
  88. package/dist/modules/entities/backend/entities/user/[entityId]/page.js +49 -49
  89. package/dist/modules/entities/backend/entities/user/[entityId]/page.js.map +2 -2
  90. package/dist/modules/entities/backend/entities/user/[entityId]/page.meta.js +1 -0
  91. package/dist/modules/entities/backend/entities/user/[entityId]/page.meta.js.map +2 -2
  92. package/dist/modules/entities/cli.js +2 -1
  93. package/dist/modules/entities/cli.js.map +2 -2
  94. package/dist/modules/feature_toggles/api/check/boolean/route.js +0 -14
  95. package/dist/modules/feature_toggles/api/check/boolean/route.js.map +2 -2
  96. package/dist/modules/feature_toggles/defaults.json +8 -0
  97. package/dist/modules/integrations/backend/integrations/[id]/page.js +9 -80
  98. package/dist/modules/integrations/backend/integrations/[id]/page.js.map +2 -2
  99. package/dist/modules/payment_gateways/data/entities.js +45 -0
  100. package/dist/modules/payment_gateways/data/entities.js.map +2 -2
  101. package/dist/modules/payment_gateways/lib/gateway-service.js +173 -38
  102. package/dist/modules/payment_gateways/lib/gateway-service.js.map +3 -3
  103. package/dist/modules/payment_gateways/lib/session-idempotency.js +104 -0
  104. package/dist/modules/payment_gateways/lib/session-idempotency.js.map +7 -0
  105. package/dist/modules/payment_gateways/migrations/Migration20260709220938_payment_gateways.js +11 -0
  106. package/dist/modules/payment_gateways/migrations/Migration20260709220938_payment_gateways.js.map +7 -0
  107. package/dist/modules/planner/backend/planner/availability-rulesets/[id]/page.js +16 -14
  108. package/dist/modules/planner/backend/planner/availability-rulesets/[id]/page.js.map +2 -2
  109. package/dist/modules/planner/components/AvailabilityRulesEditor.js +18 -30
  110. package/dist/modules/planner/components/AvailabilityRulesEditor.js.map +2 -2
  111. package/dist/modules/query_index/api/status.js +2 -8
  112. package/dist/modules/query_index/api/status.js.map +2 -2
  113. package/dist/modules/resources/backend/resources/resources/[id]/page.js +24 -24
  114. package/dist/modules/resources/backend/resources/resources/[id]/page.js.map +2 -2
  115. package/dist/modules/sales/backend/sales/channels/create/page.js +6 -0
  116. package/dist/modules/sales/backend/sales/channels/create/page.js.map +2 -2
  117. package/dist/modules/sales/backend/sales/channels/create/page.meta.js +2 -0
  118. package/dist/modules/sales/backend/sales/channels/create/page.meta.js.map +2 -2
  119. package/dist/modules/sales/backend/sales/channels/offers/page.js +6 -0
  120. package/dist/modules/sales/backend/sales/channels/offers/page.js.map +2 -2
  121. package/dist/modules/sales/backend/sales/channels/offers/page.meta.js +2 -0
  122. package/dist/modules/sales/backend/sales/channels/offers/page.meta.js.map +2 -2
  123. package/dist/modules/sales/backend/sales/channels/page.js +6 -0
  124. package/dist/modules/sales/backend/sales/channels/page.js.map +2 -2
  125. package/dist/modules/sales/backend/sales/channels/page.meta.js +2 -0
  126. package/dist/modules/sales/backend/sales/channels/page.meta.js.map +2 -2
  127. package/dist/modules/sales/backend/sales/documents/[id]/page.js +6 -4
  128. package/dist/modules/sales/backend/sales/documents/[id]/page.js.map +2 -2
  129. package/dist/modules/sales/commands/documents.js +44 -0
  130. package/dist/modules/sales/commands/documents.js.map +2 -2
  131. package/dist/modules/sales/commands/returns.js +2 -27
  132. package/dist/modules/sales/commands/returns.js.map +2 -2
  133. package/dist/modules/sales/components/SalesChannelsDisabledNotice.js +15 -0
  134. package/dist/modules/sales/components/SalesChannelsDisabledNotice.js.map +7 -0
  135. package/dist/modules/sales/components/documents/AddressesSection.js +6 -2
  136. package/dist/modules/sales/components/documents/AddressesSection.js.map +2 -2
  137. package/dist/modules/sales/components/documents/ItemsSection.js +1 -0
  138. package/dist/modules/sales/components/documents/ItemsSection.js.map +2 -2
  139. package/dist/modules/sales/components/documents/LineItemDialog.js +9 -0
  140. package/dist/modules/sales/components/documents/LineItemDialog.js.map +2 -2
  141. package/dist/modules/sales/components/documents/SalesDocumentForm.js +51 -3
  142. package/dist/modules/sales/components/documents/SalesDocumentForm.js.map +2 -2
  143. package/dist/modules/sales/components/documents/SalesDocumentsTable.js +10 -8
  144. package/dist/modules/sales/components/documents/SalesDocumentsTable.js.map +2 -2
  145. package/dist/modules/sales/components/documents/ShipmentDialog.js +59 -38
  146. package/dist/modules/sales/components/documents/ShipmentDialog.js.map +2 -2
  147. package/dist/modules/sales/components/useSalesChannelsEnabled.js +44 -0
  148. package/dist/modules/sales/components/useSalesChannelsEnabled.js.map +7 -0
  149. package/dist/modules/sales/lib/calculations.js +6 -0
  150. package/dist/modules/sales/lib/calculations.js.map +2 -2
  151. package/dist/modules/sales/lib/salesChannelsToggle.js +18 -0
  152. package/dist/modules/sales/lib/salesChannelsToggle.js.map +7 -0
  153. package/dist/modules/sales/lib/salesChannelsToggleId.js +5 -0
  154. package/dist/modules/sales/lib/salesChannelsToggleId.js.map +7 -0
  155. package/dist/modules/sales/lib/shipments/snapshots.js +27 -1
  156. package/dist/modules/sales/lib/shipments/snapshots.js.map +2 -2
  157. package/dist/modules/staff/backend/staff/team-members/[id]/page.js +22 -25
  158. package/dist/modules/staff/backend/staff/team-members/[id]/page.js.map +2 -2
  159. package/dist/modules/staff/backend/staff/teams/[id]/edit/page.js +12 -14
  160. package/dist/modules/staff/backend/staff/teams/[id]/edit/page.js.map +2 -2
  161. package/dist/modules/staff/lib/timesheets-projects-ui/SavedViewTabs.js +10 -29
  162. package/dist/modules/staff/lib/timesheets-projects-ui/SavedViewTabs.js.map +2 -2
  163. package/dist/modules/sync_excel/lib/adapters/customers.js +19 -20
  164. package/dist/modules/sync_excel/lib/adapters/customers.js.map +2 -2
  165. package/dist/modules/sync_excel/lib/parser.js +185 -0
  166. package/dist/modules/sync_excel/lib/parser.js.map +2 -2
  167. package/dist/modules/sync_excel/lib/upload-storage.js +20 -1
  168. package/dist/modules/sync_excel/lib/upload-storage.js.map +2 -2
  169. package/generated/entities/gateway_session_initialization/index.ts +10 -0
  170. package/generated/entities.ids.generated.ts +1 -0
  171. package/generated/entity-fields-registry.ts +12 -0
  172. package/package.json +9 -8
  173. package/src/modules/api_keys/api/keys/route.ts +36 -13
  174. package/src/modules/api_keys/cli.ts +2 -3
  175. package/src/modules/attachments/api/file/[id]/route.ts +11 -7
  176. package/src/modules/attachments/api/image/[id]/[[...slug]]/route.ts +11 -7
  177. package/src/modules/attachments/api/route.ts +26 -21
  178. package/src/modules/attachments/components/AttachmentContentPreview.tsx +9 -44
  179. package/src/modules/attachments/lib/reconcileOrganization.ts +188 -0
  180. package/src/modules/attachments/lib/requestScope.ts +27 -0
  181. package/src/modules/audit_logs/backend/audit-logs/page.tsx +12 -22
  182. package/src/modules/auth/api/autologin.ts +100 -0
  183. package/src/modules/auth/api/users/route.ts +11 -2
  184. package/src/modules/auth/cli.ts +3 -2
  185. package/src/modules/auth/lib/autologin.ts +36 -0
  186. package/src/modules/configs/i18n/de.json +5 -1
  187. package/src/modules/configs/i18n/en.json +5 -1
  188. package/src/modules/configs/i18n/es.json +5 -1
  189. package/src/modules/configs/i18n/pl.json +5 -1
  190. package/src/modules/configs/lib/upgrade-actions.ts +24 -0
  191. package/src/modules/customer_accounts/AGENTS.md +8 -6
  192. package/src/modules/customer_accounts/api/admin/roles/[id]/acl.ts +1 -0
  193. package/src/modules/customer_accounts/api/admin/roles/[id].ts +17 -3
  194. package/src/modules/customer_accounts/api/portal/users/[id]/roles.ts +6 -1
  195. package/src/modules/customer_accounts/api/portal/users-invite.ts +6 -1
  196. package/src/modules/customer_accounts/services/customerInvitationService.ts +1 -0
  197. package/src/modules/customer_accounts/services/customerRbacService.ts +9 -2
  198. package/src/modules/customer_accounts/widgets/injection-table.ts +0 -2
  199. package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +11 -1
  200. package/src/modules/customers/backend/customers/deals/pipeline/components/ViewTabsRow.tsx +6 -2
  201. package/src/modules/customers/components/detail/CompanyDetailTabs.tsx +39 -55
  202. package/src/modules/customers/components/detail/DealDetailTabs.tsx +24 -49
  203. package/src/modules/customers/components/detail/DetailTabsLayout.tsx +13 -24
  204. package/src/modules/customers/components/detail/MobilePersonDetail.tsx +25 -31
  205. package/src/modules/customers/components/detail/PersonDetailTabs.tsx +29 -50
  206. package/src/modules/customers/components/detail/useDealsAccess.ts +19 -0
  207. package/src/modules/dashboards/api/roles/widgets/route.ts +22 -3
  208. package/src/modules/dashboards/api/users/widgets/route.ts +22 -3
  209. package/src/modules/dashboards/cli.ts +4 -9
  210. package/src/modules/dashboards/lib/widgetAssignmentScope.ts +18 -0
  211. package/src/modules/dictionaries/commands/entries.ts +7 -16
  212. package/src/modules/dictionaries/commands/entry-operations.ts +8 -18
  213. package/src/modules/dictionaries/commands/factory.ts +8 -2
  214. package/src/modules/dictionaries/fields/dictionary.tsx +55 -32
  215. package/src/modules/dictionaries/i18n/de.json +2 -0
  216. package/src/modules/dictionaries/i18n/en.json +2 -0
  217. package/src/modules/dictionaries/i18n/es.json +2 -0
  218. package/src/modules/dictionaries/i18n/pl.json +2 -0
  219. package/src/modules/directory/utils/organizationScope.ts +36 -2
  220. package/src/modules/entities/api/records.ts +5 -7
  221. package/src/modules/entities/backend/entities/system/[entityId]/page.meta.ts +1 -0
  222. package/src/modules/entities/backend/entities/user/[entityId]/page.meta.ts +1 -0
  223. package/src/modules/entities/backend/entities/user/[entityId]/page.tsx +51 -49
  224. package/src/modules/entities/cli.ts +2 -1
  225. package/src/modules/entities/i18n/de.json +34 -0
  226. package/src/modules/entities/i18n/en.json +34 -0
  227. package/src/modules/entities/i18n/es.json +34 -0
  228. package/src/modules/entities/i18n/pl.json +34 -0
  229. package/src/modules/feature_toggles/api/check/boolean/route.ts +0 -15
  230. package/src/modules/feature_toggles/defaults.json +8 -0
  231. package/src/modules/integrations/backend/integrations/[id]/page.tsx +16 -59
  232. package/src/modules/payment_gateways/data/entities.ts +39 -0
  233. package/src/modules/payment_gateways/lib/gateway-service.ts +211 -40
  234. package/src/modules/payment_gateways/lib/session-idempotency.ts +142 -0
  235. package/src/modules/payment_gateways/migrations/.snapshot-open-mercato.json +236 -0
  236. package/src/modules/payment_gateways/migrations/Migration20260709220938_payment_gateways.ts +10 -0
  237. package/src/modules/planner/backend/planner/availability-rulesets/[id]/page.tsx +14 -20
  238. package/src/modules/planner/components/AvailabilityRulesEditor.tsx +16 -34
  239. package/src/modules/query_index/api/status.ts +2 -8
  240. package/src/modules/resources/backend/resources/resources/[id]/page.tsx +27 -37
  241. package/src/modules/sales/backend/sales/channels/create/page.meta.ts +3 -0
  242. package/src/modules/sales/backend/sales/channels/create/page.tsx +7 -0
  243. package/src/modules/sales/backend/sales/channels/offers/page.meta.ts +3 -0
  244. package/src/modules/sales/backend/sales/channels/offers/page.tsx +7 -0
  245. package/src/modules/sales/backend/sales/channels/page.meta.ts +4 -0
  246. package/src/modules/sales/backend/sales/channels/page.tsx +7 -0
  247. package/src/modules/sales/backend/sales/documents/[id]/page.tsx +7 -5
  248. package/src/modules/sales/commands/documents.ts +65 -0
  249. package/src/modules/sales/commands/returns.ts +2 -32
  250. package/src/modules/sales/components/SalesChannelsDisabledNotice.tsx +21 -0
  251. package/src/modules/sales/components/documents/AddressesSection.tsx +6 -2
  252. package/src/modules/sales/components/documents/ItemsSection.tsx +5 -0
  253. package/src/modules/sales/components/documents/LineItemDialog.tsx +10 -0
  254. package/src/modules/sales/components/documents/SalesDocumentForm.tsx +53 -3
  255. package/src/modules/sales/components/documents/SalesDocumentsTable.tsx +10 -8
  256. package/src/modules/sales/components/documents/ShipmentDialog.tsx +65 -40
  257. package/src/modules/sales/components/useSalesChannelsEnabled.ts +59 -0
  258. package/src/modules/sales/i18n/de.json +5 -0
  259. package/src/modules/sales/i18n/en.json +5 -0
  260. package/src/modules/sales/i18n/es.json +5 -0
  261. package/src/modules/sales/i18n/pl.json +5 -0
  262. package/src/modules/sales/lib/calculations.ts +15 -0
  263. package/src/modules/sales/lib/salesChannelsToggle.ts +26 -0
  264. package/src/modules/sales/lib/salesChannelsToggleId.ts +1 -0
  265. package/src/modules/sales/lib/shipments/snapshots.ts +30 -0
  266. package/src/modules/staff/backend/staff/team-members/[id]/page.tsx +25 -34
  267. package/src/modules/staff/backend/staff/teams/[id]/edit/page.tsx +11 -17
  268. package/src/modules/staff/lib/timesheets-projects-ui/SavedViewTabs.tsx +11 -28
  269. package/src/modules/sync_excel/lib/adapters/customers.ts +20 -22
  270. package/src/modules/sync_excel/lib/parser.ts +245 -0
  271. package/src/modules/sync_excel/lib/upload-storage.ts +23 -1
@@ -6,6 +6,7 @@ import { Role } from '@open-mercato/core/modules/auth/data/entities'
6
6
  import { loadAllWidgets } from '@open-mercato/core/modules/dashboards/lib/widgets'
7
7
  import { appendWidgetsToRoles, resolveAnalyticsWidgetIds } from '@open-mercato/core/modules/dashboards/lib/role-widgets'
8
8
  import { seedAnalyticsData } from './seed/analytics'
9
+ import { parseCommaSeparatedList } from '@open-mercato/shared/lib/string'
9
10
 
10
11
  type Args = Record<string, string>
11
12
 
@@ -108,10 +109,7 @@ const seedDefaults: ModuleCli = {
108
109
  return
109
110
  }
110
111
 
111
- const roleNames = roleCsv
112
- .split(',')
113
- .map((name) => name.trim())
114
- .filter(Boolean)
112
+ const roleNames = parseCommaSeparatedList(roleCsv)
115
113
 
116
114
  if (!roleNames.length) {
117
115
  console.log('No roles provided, nothing to seed.')
@@ -125,7 +123,7 @@ const seedDefaults: ModuleCli = {
125
123
  tenantId,
126
124
  organizationId,
127
125
  roleNames,
128
- widgetIds: widgetCsv ? widgetCsv.split(',').map((id) => id.trim()).filter(Boolean) : undefined,
126
+ widgetIds: widgetCsv ? parseCommaSeparatedList(widgetCsv) : undefined,
129
127
  logger: (message) => console.log(message),
130
128
  })
131
129
  },
@@ -143,10 +141,7 @@ const enableAnalyticsWidgets: ModuleCli = {
143
141
  return
144
142
  }
145
143
 
146
- const roleNames = roleCsv
147
- .split(',')
148
- .map((name) => name.trim())
149
- .filter(Boolean)
144
+ const roleNames = parseCommaSeparatedList(roleCsv)
150
145
 
151
146
  if (!roleNames.length) {
152
147
  console.log('No roles provided, nothing to update.')
@@ -8,6 +8,24 @@ export type ResolvedAssignmentScope = {
8
8
  organizationId: string | null
9
9
  }
10
10
 
11
+ export type WidgetAssignmentTarget = {
12
+ tenantId?: string | null
13
+ }
14
+
15
+ export type WidgetAssignmentTargetAccess = 'allowed' | 'forbidden' | 'not-found'
16
+
17
+ export function resolveWidgetAssignmentTargetAccess(params: {
18
+ isSuperAdmin: boolean
19
+ scopeTenantId: string | null
20
+ target: WidgetAssignmentTarget | null | undefined
21
+ }): WidgetAssignmentTargetAccess {
22
+ const { isSuperAdmin, scopeTenantId, target } = params
23
+ if (!isSuperAdmin && !scopeTenantId) return 'forbidden'
24
+ if (!target) return 'not-found'
25
+ if (scopeTenantId && (target.tenantId ?? null) !== scopeTenantId) return 'not-found'
26
+ return 'allowed'
27
+ }
28
+
11
29
  export function resolveWidgetAssignmentReadScope(params: {
12
30
  auth: AuthScopeInput
13
31
  isSuperAdmin: boolean
@@ -4,22 +4,13 @@ import {
4
4
  dictionaryEntryCommandCreateSchema,
5
5
  dictionaryEntryCommandUpdateSchema,
6
6
  } from '@open-mercato/core/modules/dictionaries/data/validators'
7
- import { registerDictionaryEntryCommands } from '@open-mercato/core/modules/dictionaries/commands/factory'
8
- import type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
7
+ import {
8
+ ensureDictionaryEntryScope,
9
+ registerDictionaryEntryCommands,
10
+ } from '@open-mercato/core/modules/dictionaries/commands/factory'
9
11
  import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
10
12
  import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
11
13
 
12
- function ensureScope(ctx: CommandRuntimeContext, scope: { tenantId: string; organizationId: string }): void {
13
- const tenantId = ctx.auth?.tenantId ?? null
14
- if (tenantId && tenantId !== scope.tenantId) {
15
- throw new CrudHttpError(403, { error: 'Forbidden' })
16
- }
17
- const organizationId = ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null
18
- if (organizationId && organizationId !== scope.organizationId) {
19
- throw new CrudHttpError(403, { error: 'Forbidden' })
20
- }
21
- }
22
-
23
14
  async function ensureDictionary(em: EntityManager, id: string): Promise<Dictionary> {
24
15
  const dictionary = await em.findOne(Dictionary, {
25
16
  id,
@@ -37,12 +28,12 @@ registerDictionaryEntryCommands({
37
28
  translationKeyPrefix: 'dictionaries.entries',
38
29
  createSchema: dictionaryEntryCommandCreateSchema,
39
30
  updateSchema: dictionaryEntryCommandUpdateSchema,
40
- ensureScope,
31
+ ensureScope: ensureDictionaryEntryScope,
41
32
  duplicateError: 'An entry with this value already exists.',
42
33
  resolveDictionaryForCreate: async ({ em, ctx, parsed }) => {
43
34
  const dictionary = await ensureDictionary(em, parsed.dictionaryId)
44
35
  const scope = { tenantId: dictionary.tenantId, organizationId: dictionary.organizationId }
45
- ensureScope(ctx, scope)
36
+ ensureDictionaryEntryScope(ctx, scope)
46
37
  return { dictionary, scope }
47
38
  },
48
39
  resolveEntry: async ({ em, ctx, id }) => {
@@ -54,7 +45,7 @@ registerDictionaryEntryCommands({
54
45
  throw new CrudHttpError(404, { error: 'Dictionary not found' })
55
46
  }
56
47
  const scope = { tenantId: entry.tenantId, organizationId: entry.organizationId }
57
- ensureScope(ctx, scope)
48
+ ensureDictionaryEntryScope(ctx, scope)
58
49
  return { entry, dictionary: entry.dictionary, scope }
59
50
  },
60
51
  ensureDictionaryForUndo: async ({ em, snapshot }) => {
@@ -6,7 +6,8 @@ import {
6
6
  type ReorderDictionaryEntriesCommandInput,
7
7
  type SetDefaultDictionaryEntryCommandInput,
8
8
  } from '@open-mercato/core/modules/dictionaries/data/validators'
9
- import { registerCommand, type CommandHandler, type CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
9
+ import { ensureDictionaryEntryScope } from '@open-mercato/core/modules/dictionaries/commands/factory'
10
+ import { registerCommand, type CommandHandler } from '@open-mercato/shared/lib/commands'
10
11
  import { extractUndoPayload } from '@open-mercato/shared/lib/commands/undo'
11
12
  import { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'
12
13
  import { emitCrudSideEffects, emitCrudUndoSideEffects } from '@open-mercato/shared/lib/commands/helpers'
@@ -36,17 +37,6 @@ const dictionaryCrudEvents: CrudEventsConfig = {
36
37
  }),
37
38
  }
38
39
 
39
- function ensureScope(ctx: CommandRuntimeContext, scope: DictionaryScope): void {
40
- const tenantId = ctx.auth?.tenantId ?? null
41
- if (tenantId && tenantId !== scope.tenantId) {
42
- throw new CrudHttpError(403, { error: 'Forbidden' })
43
- }
44
- const organizationId = ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null
45
- if (organizationId && organizationId !== scope.organizationId) {
46
- throw new CrudHttpError(403, { error: 'Forbidden' })
47
- }
48
- }
49
-
50
40
  async function requireDictionary(
51
41
  em: EntityManager,
52
42
  id: string,
@@ -84,7 +74,7 @@ const reorderDictionaryEntriesCommand: CommandHandler<
84
74
  async prepare(rawInput, ctx) {
85
75
  const parsed = reorderDictionaryEntriesCommandSchema.parse(rawInput)
86
76
  const scope: DictionaryScope = { tenantId: parsed.tenantId, organizationId: parsed.organizationId }
87
- ensureScope(ctx, scope)
77
+ ensureDictionaryEntryScope(ctx, scope)
88
78
  const em = (ctx.container.resolve('em') as EntityManager).fork()
89
79
  const ids = parsed.entries.map((e) => e.id)
90
80
  const existing = await findWithDecryption(
@@ -108,7 +98,7 @@ const reorderDictionaryEntriesCommand: CommandHandler<
108
98
  async execute(rawInput, ctx) {
109
99
  const parsed = reorderDictionaryEntriesCommandSchema.parse(rawInput)
110
100
  const scope: DictionaryScope = { tenantId: parsed.tenantId, organizationId: parsed.organizationId }
111
- ensureScope(ctx, scope)
101
+ ensureDictionaryEntryScope(ctx, scope)
112
102
 
113
103
  const em = (ctx.container.resolve('em') as EntityManager).fork()
114
104
  const dictionary = await requireDictionary(em, parsed.dictionaryId, scope)
@@ -201,7 +191,7 @@ const reorderDictionaryEntriesCommand: CommandHandler<
201
191
  const undo = extractUndoPayload<ReorderUndoPayload>(logEntry)
202
192
  const before = undo?.before ?? (logEntry?.snapshotBefore as ReorderSnapshot | null | undefined) ?? null
203
193
  if (!before) return
204
- ensureScope(ctx, before.scope)
194
+ ensureDictionaryEntryScope(ctx, before.scope)
205
195
 
206
196
  const em = (ctx.container.resolve('em') as EntityManager).fork()
207
197
  const ids = before.positions.map((p) => p.id)
@@ -265,7 +255,7 @@ const setDefaultDictionaryEntryCommand: CommandHandler<
265
255
  async prepare(rawInput, ctx) {
266
256
  const parsed = setDefaultDictionaryEntryCommandSchema.parse(rawInput)
267
257
  const scope: DictionaryScope = { tenantId: parsed.tenantId, organizationId: parsed.organizationId }
268
- ensureScope(ctx, scope)
258
+ ensureDictionaryEntryScope(ctx, scope)
269
259
  const em = (ctx.container.resolve('em') as EntityManager).fork()
270
260
  const dictionary = await requireDictionary(em, parsed.dictionaryId, scope)
271
261
  const previousDefaults = await findWithDecryption(
@@ -291,7 +281,7 @@ const setDefaultDictionaryEntryCommand: CommandHandler<
291
281
  async execute(rawInput, ctx) {
292
282
  const parsed = setDefaultDictionaryEntryCommandSchema.parse(rawInput)
293
283
  const scope: DictionaryScope = { tenantId: parsed.tenantId, organizationId: parsed.organizationId }
294
- ensureScope(ctx, scope)
284
+ ensureDictionaryEntryScope(ctx, scope)
295
285
 
296
286
  const em = (ctx.container.resolve('em') as EntityManager).fork()
297
287
  const dictionary = await requireDictionary(em, parsed.dictionaryId, scope)
@@ -399,7 +389,7 @@ const setDefaultDictionaryEntryCommand: CommandHandler<
399
389
  const undo = extractUndoPayload<DefaultUndoPayload>(logEntry)
400
390
  const before = undo?.before ?? (logEntry?.snapshotBefore as DefaultSnapshot | null | undefined) ?? null
401
391
  if (!before) return
402
- ensureScope(ctx, before.scope)
392
+ ensureDictionaryEntryScope(ctx, before.scope)
403
393
 
404
394
  const em = (ctx.container.resolve('em') as EntityManager).fork()
405
395
  const dictionary = await requireDictionary(em, before.dictionaryId, before.scope)
@@ -12,6 +12,7 @@ import {
12
12
  } from '@open-mercato/shared/lib/commands'
13
13
  import { extractUndoPayload } from '@open-mercato/shared/lib/commands/undo'
14
14
  import { buildChanges, requireId } from '@open-mercato/shared/lib/commands/helpers'
15
+ import { ensureOrganizationScope, ensureTenantScope } from '@open-mercato/shared/lib/commands/scope'
15
16
  import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
16
17
  import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
17
18
  import type { z } from 'zod'
@@ -22,6 +23,11 @@ type DictionaryScope = {
22
23
  organizationId: string
23
24
  }
24
25
 
26
+ export function ensureDictionaryEntryScope(ctx: CommandRuntimeContext, scope: DictionaryScope): void {
27
+ ensureTenantScope(ctx, scope.tenantId)
28
+ ensureOrganizationScope(ctx, scope.organizationId)
29
+ }
30
+
25
31
  export type DictionaryEntrySnapshot = {
26
32
  id: string
27
33
  dictionaryId: string
@@ -87,8 +93,8 @@ export type DictionaryEntryCommandConfig<TCreate, TUpdate> = {
87
93
  labels?: DictionaryEntryCommandLabels
88
94
  }
89
95
 
90
- function toScopeEnsurer(fn: EnsureScopeFn | undefined): EnsureScopeFn {
91
- return typeof fn === 'function' ? fn : () => {}
96
+ export function toScopeEnsurer(fn: EnsureScopeFn | undefined): EnsureScopeFn {
97
+ return typeof fn === 'function' ? fn : ensureDictionaryEntryScope
92
98
  }
93
99
 
94
100
  async function loadSnapshot(em: EntityManager, id: string): Promise<DictionaryEntrySnapshot | null> {
@@ -90,9 +90,12 @@ function DictionaryFieldDefEditor({ def, onChange }: { def: { configJson?: Dicti
90
90
  const [error, setError] = React.useState<string | null>(null)
91
91
  const multiId = React.useId()
92
92
  const inlineCreateId = React.useId()
93
+ const inlineCreateHintId = React.useId()
94
+ const defaultValueHintId = React.useId()
93
95
  const selectedId = typeof def?.configJson?.dictionaryId === 'string' ? def?.configJson?.dictionaryId : ''
94
96
  const inlineCreate = def?.configJson?.dictionaryInlineCreate !== false
95
97
  const isMulti = def?.configJson?.multi === true
98
+ const inlineCreateDisabled = !selectedId || isMulti
96
99
  const errorLoadLabel = t('dictionaries.customFields.errorLoad', 'Failed to load dictionaries.')
97
100
 
98
101
  React.useEffect(() => {
@@ -181,45 +184,65 @@ function DictionaryFieldDefEditor({ def, onChange }: { def: { configJson?: Dicti
181
184
  </a>
182
185
  </div>
183
186
  ) : null}
184
- <div className="inline-flex items-center gap-2 text-xs">
185
- <Checkbox
186
- id={multiId}
187
- checked={isMulti}
188
- onCheckedChange={(checked) => {
189
- const enabled = checked === true
190
- onChange({ multi: enabled, defaultValue: enabled ? undefined : def?.configJson?.defaultValue })
191
- }}
192
- disabled={!selectedId}
193
- />
194
- <label
195
- htmlFor={multiId}
196
- className={selectedId ? 'cursor-pointer select-none' : 'cursor-not-allowed opacity-60'}
197
- >
198
- {t('dictionaries.customFields.allowMultiple', 'Allow selecting multiple entries')}
199
- </label>
200
- </div>
201
- {!isMulti ? (
202
- <div className="inline-flex items-center gap-2 text-xs">
203
- <Checkbox
204
- id={inlineCreateId}
205
- checked={inlineCreate}
206
- onCheckedChange={(checked) => onChange({ dictionaryInlineCreate: checked === true })}
207
- disabled={!selectedId}
208
- />
209
- <label
210
- htmlFor={inlineCreateId}
211
- className={selectedId ? 'cursor-pointer select-none' : 'cursor-not-allowed opacity-60'}
212
- >
213
- {t('dictionaries.customFields.allowInlineCreate', 'Allow inline creation inside forms')}
214
- </label>
187
+ <div className="space-y-2">
188
+ <div className="flex flex-col gap-2 sm:flex-row sm:flex-wrap sm:items-center sm:gap-x-4">
189
+ <div className="inline-flex items-center gap-2 text-xs">
190
+ <Checkbox
191
+ id={multiId}
192
+ checked={isMulti}
193
+ onCheckedChange={(checked) => {
194
+ const enabled = checked === true
195
+ onChange({ multi: enabled, defaultValue: enabled ? undefined : def?.configJson?.defaultValue })
196
+ }}
197
+ disabled={!selectedId}
198
+ />
199
+ <label
200
+ htmlFor={multiId}
201
+ className={selectedId ? 'cursor-pointer select-none' : 'cursor-not-allowed opacity-60'}
202
+ >
203
+ {t('dictionaries.customFields.allowMultiple', 'Allow selecting multiple entries')}
204
+ </label>
205
+ </div>
206
+ <div className="inline-flex items-center gap-2 text-xs">
207
+ <Checkbox
208
+ id={inlineCreateId}
209
+ checked={!isMulti && inlineCreate}
210
+ onCheckedChange={(checked) => onChange({ dictionaryInlineCreate: checked === true })}
211
+ disabled={inlineCreateDisabled}
212
+ aria-describedby={isMulti ? inlineCreateHintId : undefined}
213
+ />
214
+ <label
215
+ htmlFor={inlineCreateId}
216
+ className={!inlineCreateDisabled ? 'cursor-pointer select-none' : 'cursor-not-allowed opacity-60'}
217
+ >
218
+ {t('dictionaries.customFields.allowInlineCreate', 'Allow inline creation inside forms')}
219
+ </label>
220
+ </div>
215
221
  </div>
216
- ) : null}
222
+ {isMulti ? (
223
+ <p id={inlineCreateHintId} className="text-xs text-muted-foreground">
224
+ {t(
225
+ 'dictionaries.customFields.inlineCreateSingleOnly',
226
+ 'Inline creation is available for single-entry dictionary fields only.',
227
+ )}
228
+ </p>
229
+ ) : null}
230
+ </div>
217
231
  {selectedId && !isMulti ? (
218
232
  <DictionaryDefaultSelector
219
233
  dictionaryId={selectedId}
220
234
  defaultValue={typeof def?.configJson?.defaultValue === 'string' ? def.configJson.defaultValue : ''}
221
235
  onChange={(value) => onChange({ defaultValue: value || undefined })}
222
236
  />
237
+ ) : selectedId && isMulti ? (
238
+ <div className="inline-flex items-center gap-2 text-xs">
239
+ <p id={defaultValueHintId} className="text-xs text-muted-foreground">
240
+ {t(
241
+ 'dictionaries.customFields.defaultValueMultiUnavailable',
242
+ 'Default values are not available for multi-select dictionary fields.',
243
+ )}
244
+ </p>
245
+ </div>
223
246
  ) : null}
224
247
  </div>
225
248
  )
@@ -82,6 +82,7 @@
82
82
  "dictionaries.customFields.allowInlineCreate": "Inline-Erstellung in Formularen erlauben",
83
83
  "dictionaries.customFields.allowMultiple": "Mehrere Einträge auswählen erlauben",
84
84
  "dictionaries.customFields.defaultValue": "Standardwert",
85
+ "dictionaries.customFields.defaultValueMultiUnavailable": "Standardwerte sind für Wörterbuchfelder mit Mehrfachauswahl nicht verfügbar.",
85
86
  "dictionaries.customFields.defaultValueNone": "Kein Standardwert",
86
87
  "dictionaries.customFields.defaultValueStale": "Standardeintrag nicht gefunden — er wurde möglicherweise gelöscht oder umbenannt.",
87
88
  "dictionaries.customFields.dictionaryLabel": "Quelle des Wörterbuchs",
@@ -89,6 +90,7 @@
89
90
  "dictionaries.customFields.empty": "Noch keine Wörterbücher verfügbar. Erstellen Sie zuerst eines.",
90
91
  "dictionaries.customFields.errorLoad": "Wörterbücher konnten nicht geladen werden.",
91
92
  "dictionaries.customFields.inactive": "inaktiv",
93
+ "dictionaries.customFields.inlineCreateSingleOnly": "Inline-Erstellung ist nur für Wörterbuchfelder mit Einzelauswahl verfügbar.",
92
94
  "dictionaries.customFields.loading": "Wörterbücher werden geladen…",
93
95
  "dictionaries.customFields.manageLink": "Wörterbücher verwalten",
94
96
  "dictionaries.customFields.selectedHint": "Einträge aus diesem Wörterbuch füllen das Feld.",
@@ -82,6 +82,7 @@
82
82
  "dictionaries.customFields.allowInlineCreate": "Allow inline creation inside forms",
83
83
  "dictionaries.customFields.allowMultiple": "Allow selecting multiple entries",
84
84
  "dictionaries.customFields.defaultValue": "Default value",
85
+ "dictionaries.customFields.defaultValueMultiUnavailable": "Default values are not available for multi-select dictionary fields.",
85
86
  "dictionaries.customFields.defaultValueNone": "No default",
86
87
  "dictionaries.customFields.defaultValueStale": "Default entry not found — it may have been deleted or renamed.",
87
88
  "dictionaries.customFields.dictionaryLabel": "Dictionary source",
@@ -89,6 +90,7 @@
89
90
  "dictionaries.customFields.empty": "No dictionaries available yet. Create one first.",
90
91
  "dictionaries.customFields.errorLoad": "Failed to load dictionaries.",
91
92
  "dictionaries.customFields.inactive": "inactive",
93
+ "dictionaries.customFields.inlineCreateSingleOnly": "Inline creation is available for single-entry dictionary fields only.",
92
94
  "dictionaries.customFields.loading": "Loading dictionaries…",
93
95
  "dictionaries.customFields.manageLink": "Manage dictionaries",
94
96
  "dictionaries.customFields.selectedHint": "Entries from this dictionary populate the field.",
@@ -82,6 +82,7 @@
82
82
  "dictionaries.customFields.allowInlineCreate": "Permitir creación en línea dentro de los formularios",
83
83
  "dictionaries.customFields.allowMultiple": "Permitir seleccionar varias entradas",
84
84
  "dictionaries.customFields.defaultValue": "Valor predeterminado",
85
+ "dictionaries.customFields.defaultValueMultiUnavailable": "Los valores predeterminados no están disponibles para campos de diccionario con selección múltiple.",
85
86
  "dictionaries.customFields.defaultValueNone": "Sin valor predeterminado",
86
87
  "dictionaries.customFields.defaultValueStale": "No se encontró la entrada predeterminada; puede haber sido eliminada o renombrada.",
87
88
  "dictionaries.customFields.dictionaryLabel": "Fuente del diccionario",
@@ -89,6 +90,7 @@
89
90
  "dictionaries.customFields.empty": "Aún no hay diccionarios disponibles. Crea uno primero.",
90
91
  "dictionaries.customFields.errorLoad": "No se pudieron cargar los diccionarios.",
91
92
  "dictionaries.customFields.inactive": "inactivo",
93
+ "dictionaries.customFields.inlineCreateSingleOnly": "La creación en línea solo está disponible para campos de diccionario de una sola entrada.",
92
94
  "dictionaries.customFields.loading": "Cargando diccionarios…",
93
95
  "dictionaries.customFields.manageLink": "Administrar diccionarios",
94
96
  "dictionaries.customFields.selectedHint": "Las entradas de este diccionario rellenarán el campo.",
@@ -82,6 +82,7 @@
82
82
  "dictionaries.customFields.allowInlineCreate": "Zezwól na tworzenie w formularzach",
83
83
  "dictionaries.customFields.allowMultiple": "Zezwól na wybór wielu wpisów",
84
84
  "dictionaries.customFields.defaultValue": "Wartość domyślna",
85
+ "dictionaries.customFields.defaultValueMultiUnavailable": "Wartości domyślne nie są dostępne dla wielokrotnego wyboru wpisów słownika.",
85
86
  "dictionaries.customFields.defaultValueNone": "Brak wartości domyślnej",
86
87
  "dictionaries.customFields.defaultValueStale": "Nie znaleziono domyślnego wpisu — mógł zostać usunięty albo przemianowany.",
87
88
  "dictionaries.customFields.dictionaryLabel": "Źródło słownika",
@@ -89,6 +90,7 @@
89
90
  "dictionaries.customFields.empty": "Brak dostępnych słowników. Najpierw utwórz jeden.",
90
91
  "dictionaries.customFields.errorLoad": "Nie udało się wczytać słowników.",
91
92
  "dictionaries.customFields.inactive": "nieaktywne",
93
+ "dictionaries.customFields.inlineCreateSingleOnly": "Tworzenie wpisów w formularzu jest dostępne tylko dla pól z pojedynczym wyborem.",
92
94
  "dictionaries.customFields.loading": "Ładowanie słowników…",
93
95
  "dictionaries.customFields.manageLink": "Zarządzaj słownikami",
94
96
  "dictionaries.customFields.selectedHint": "Wpisy z tego słownika uzupełnią pole.",
@@ -18,6 +18,13 @@ export type OrganizationScope = {
18
18
  filterIds: string[] | null
19
19
  allowedIds: string[] | null
20
20
  tenantId: string | null
21
+ // True when the caller explicitly selected a concrete organization (cookie /
22
+ // selectedId param) that could not be honored — it does not exist for the
23
+ // tenant or is not accessible. Reads degrade gracefully (filterIds/selectedId
24
+ // fall back to the caller's accessible orgs), but writes MUST fail loudly on
25
+ // this so a record never lands under an org the caller did not intend. Absent
26
+ // (undefined) means the selection — if any — was honored.
27
+ selectionRejected?: boolean
21
28
  }
22
29
 
23
30
  // Phase 4 — short-TTL cache for resolveOrganizationScopeForRequest.
@@ -79,7 +86,8 @@ function isValidCachedScope(value: unknown): value is OrganizationScope {
79
86
  const record = value as Partial<OrganizationScope>
80
87
  const idOk = (v: unknown) => v === null || typeof v === 'string'
81
88
  const arrOk = (v: unknown) => v === null || (Array.isArray(v) && v.every((entry) => typeof entry === 'string'))
82
- return idOk(record.selectedId) && idOk(record.tenantId) && arrOk(record.filterIds) && arrOk(record.allowedIds)
89
+ const flagOk = record.selectionRejected === undefined || typeof record.selectionRejected === 'boolean'
90
+ return idOk(record.selectedId) && idOk(record.tenantId) && arrOk(record.filterIds) && arrOk(record.allowedIds) && flagOk
83
91
  }
84
92
 
85
93
  function resolveCacheFromContainer(container: AwilixContainer | null | undefined): CacheStrategy | null {
@@ -320,9 +328,18 @@ export async function resolveOrganizationScope({
320
328
  const initialSelected =
321
329
  normalizedSelectedId
322
330
  ?? (widenToAllOrgs ? null : accountOrgId ?? null)
331
+ // A selection is only honored when it resolves to a real, non-deleted org for
332
+ // this tenant. `orgDescendants` holds exactly the existing orgs among the
333
+ // candidate ids, so a stale/dead id (e.g. a selected-org cookie or JWT org
334
+ // that no longer resolves after a DB reset) has no entry and is dropped here.
335
+ // Without the existence guard an unrestricted (all-orgs) principal — whose
336
+ // `allowedSet` is null — would accept the dead id as `effectiveSelected`, and
337
+ // writes derived from `selectedId` would land under an org the read scope
338
+ // (`filterIds`) never filters to, silently orphaning the record.
339
+ const selectionResolvesToOrg = (id: string): boolean => orgDescendants.has(id)
323
340
  let effectiveSelected: string | null = null
324
341
  if (initialSelected) {
325
- if (allowedSet === null || allowedSet.has(initialSelected)) {
342
+ if ((allowedSet === null || allowedSet.has(initialSelected)) && selectionResolvesToOrg(initialSelected)) {
326
343
  effectiveSelected = initialSelected
327
344
  }
328
345
  }
@@ -336,6 +353,13 @@ export async function resolveOrganizationScope({
336
353
  filterSet = null
337
354
  } else if (auth.orgId) {
338
355
  filterSet = loadFallbackSet()
356
+ // Keep the write target (`selectedId`) aligned with the read scope
357
+ // (`filterIds`): when an unrestricted principal's requested selection was
358
+ // dropped above, fall the selection back to the account org too so a record
359
+ // created here is always readable back by the same caller.
360
+ if (!effectiveSelected && fallbackOrgId && filterSet && filterSet.size > 0) {
361
+ effectiveSelected = fallbackOrgId
362
+ }
339
363
  }
340
364
 
341
365
  if ((!filterSet || filterSet.size === 0) && fallbackOrgId && !widenToAllOrgs) {
@@ -348,11 +372,21 @@ export async function resolveOrganizationScope({
348
372
  }
349
373
  }
350
374
 
375
+ // A concrete organization was explicitly requested (`normalizedSelectedId` is
376
+ // null for both "no selection" and the "all organizations" token) but the
377
+ // resolver could not honor it — it was dropped as non-existent/inaccessible
378
+ // and the effective selection fell back to something else. Surface this so the
379
+ // write layer can reject the request instead of silently creating the record
380
+ // under the fallback org. Only set the field when true to keep the scope shape
381
+ // unchanged for the common (honored) case.
382
+ const selectionRejected = normalizedSelectedId !== null && effectiveSelected !== normalizedSelectedId
383
+
351
384
  return {
352
385
  selectedId: effectiveSelected,
353
386
  filterIds: filterSet ? Array.from(filterSet) : null,
354
387
  allowedIds: allowedSet ? Array.from(allowedSet) : null,
355
388
  tenantId,
389
+ ...(selectionRejected ? { selectionRejected: true } : {}),
356
390
  }
357
391
  }
358
392
 
@@ -8,6 +8,7 @@ import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacS
8
8
  import { resolveOrganizationScope, getSelectedOrganizationFromRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'
9
9
  import { SYSTEM_ENTITY_RECORDS_BLOCKED_CODE, isOrmBackedSystemEntityId } from '@open-mercato/shared/lib/data/engine'
10
10
  import { parseBooleanToken, parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'
11
+ import { parseCommaSeparatedList } from '@open-mercato/shared/lib/string'
11
12
  import { setRecordCustomFields } from '../lib/helpers'
12
13
  import { CustomFieldValue } from '../data/entities'
13
14
  import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
@@ -151,10 +152,7 @@ export async function GET(req: Request) {
151
152
  const sortDir = (url.searchParams.get('sortDir') || 'asc').toLowerCase() === 'desc' ? 'desc' : 'asc'
152
153
  const withDeleted = parseBooleanWithDefault(url.searchParams.get('withDeleted'), false)
153
154
  const searchTerm = (url.searchParams.get('search') || '').trim()
154
- const searchFields = (url.searchParams.get('searchFields') || '')
155
- .split(',')
156
- .map((field) => field.trim())
157
- .filter(Boolean)
155
+ const searchFields = parseCommaSeparatedList(url.searchParams.get('searchFields'))
158
156
 
159
157
  const qpEntries: Array<[string, string]> = []
160
158
  for (const [key, val] of url.searchParams.entries()) {
@@ -211,11 +209,11 @@ export async function GET(req: Request) {
211
209
  if (key.startsWith('cf_')) {
212
210
  if (key.endsWith('In')) {
213
211
  const base = key.slice(0, -2)
214
- const values = val.split(',').map((s) => s.trim()).filter(Boolean)
212
+ const values = parseCommaSeparatedList(val)
215
213
  ;(filtersObj as any)[base] = { $in: values }
216
214
  } else {
217
215
  if (val.includes(',')) {
218
- const values = val.split(',').map((s) => s.trim()).filter(Boolean)
216
+ const values = parseCommaSeparatedList(val)
219
217
  ;(filtersObj as any)[key] = { $in: values }
220
218
  } else {
221
219
  const parsed = parseBooleanToken(val)
@@ -224,7 +222,7 @@ export async function GET(req: Request) {
224
222
  }
225
223
  } else if (allowAnyKey) {
226
224
  if (val.includes(',')) {
227
- const values = val.split(',').map((s) => s.trim()).filter(Boolean)
225
+ const values = parseCommaSeparatedList(val)
228
226
  ;(filtersObj as any)[key] = { $in: values }
229
227
  } else {
230
228
  const parsed = parseBooleanToken(val)
@@ -2,6 +2,7 @@ export const metadata = {
2
2
  requireAuth: true,
3
3
  requireFeatures: ['entities.definitions.manage'],
4
4
  pageTitle: 'Edit Definitions',
5
+ pageTitleKey: 'entities.userEntities.edit.pageTitle',
5
6
  pageGroup: 'Data designer',
6
7
  navHidden: true,
7
8
  }
@@ -2,6 +2,7 @@ export const metadata = {
2
2
  requireAuth: true,
3
3
  requireFeatures: ['entities.definitions.manage'],
4
4
  pageTitle: 'Edit Definitions',
5
+ pageTitleKey: 'entities.userEntities.edit.pageTitle',
5
6
  pageGroup: 'Data designer',
6
7
  navHidden: true,
7
8
  }