@open-mercato/core 0.6.5-develop.5337.1.534b781eac → 0.6.5

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 (350) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +1 -1
  3. package/dist/bootstrap.js +46 -6
  4. package/dist/bootstrap.js.map +2 -2
  5. package/dist/generated/entities/organization/index.js +2 -0
  6. package/dist/generated/entities/organization/index.js.map +2 -2
  7. package/dist/generated/entity-fields-registry.js +1 -0
  8. package/dist/generated/entity-fields-registry.js.map +2 -2
  9. package/dist/helpers/integration/crmFixtures.js +4 -0
  10. package/dist/helpers/integration/crmFixtures.js.map +2 -2
  11. package/dist/modules/attachments/api/library/route.js +2 -2
  12. package/dist/modules/attachments/api/library/route.js.map +2 -2
  13. package/dist/modules/attachments/api/route.js +2 -0
  14. package/dist/modules/attachments/api/route.js.map +2 -2
  15. package/dist/modules/attachments/components/AttachmentContentPreview.js +9 -5
  16. package/dist/modules/attachments/components/AttachmentContentPreview.js.map +2 -2
  17. package/dist/modules/attachments/lib/access.js +18 -0
  18. package/dist/modules/attachments/lib/access.js.map +2 -2
  19. package/dist/modules/audit_logs/api/audit-logs/actions/redo/route.js +3 -2
  20. package/dist/modules/audit_logs/api/audit-logs/actions/redo/route.js.map +2 -2
  21. package/dist/modules/audit_logs/data/entities.js +2 -1
  22. package/dist/modules/audit_logs/data/entities.js.map +2 -2
  23. package/dist/modules/audit_logs/migrations/Migration20260611104500.js +13 -0
  24. package/dist/modules/audit_logs/migrations/Migration20260611104500.js.map +7 -0
  25. package/dist/modules/audit_logs/services/accessLogService.js +10 -0
  26. package/dist/modules/audit_logs/services/accessLogService.js.map +2 -2
  27. package/dist/modules/auth/api/admin/nav.js +9 -0
  28. package/dist/modules/auth/api/admin/nav.js.map +2 -2
  29. package/dist/modules/auth/api/login.js +4 -13
  30. package/dist/modules/auth/api/login.js.map +2 -2
  31. package/dist/modules/auth/commands/users.js +20 -14
  32. package/dist/modules/auth/commands/users.js.map +2 -2
  33. package/dist/modules/auth/data/entities.js +4 -2
  34. package/dist/modules/auth/data/entities.js.map +2 -2
  35. package/dist/modules/auth/lib/backendChrome.js +35 -2
  36. package/dist/modules/auth/lib/backendChrome.js.map +2 -2
  37. package/dist/modules/auth/lib/consentIntegrity.js +3 -3
  38. package/dist/modules/auth/lib/consentIntegrity.js.map +2 -2
  39. package/dist/modules/auth/migrations/Migration20260610120000.js +30 -0
  40. package/dist/modules/auth/migrations/Migration20260610120000.js.map +7 -0
  41. package/dist/modules/auth/migrations/Migration20260611103000.js +15 -0
  42. package/dist/modules/auth/migrations/Migration20260611103000.js.map +7 -0
  43. package/dist/modules/auth/services/authService.js +5 -3
  44. package/dist/modules/auth/services/authService.js.map +2 -2
  45. package/dist/modules/auth/services/rbacService.js +3 -2
  46. package/dist/modules/auth/services/rbacService.js.map +2 -2
  47. package/dist/modules/catalog/ai-tools/configuration-pack.js.map +1 -1
  48. package/dist/modules/catalog/ai-tools/prices-offers-pack.js.map +1 -1
  49. package/dist/modules/catalog/ai-tools/products-pack.js.map +1 -1
  50. package/dist/modules/catalog/ai-tools/variants-pack.js.map +1 -1
  51. package/dist/modules/communication_channels/data/entities.js.map +1 -1
  52. package/dist/modules/communication_channels/encryption.js.map +1 -1
  53. package/dist/modules/communication_channels/lib/thread-matcher.js.map +1 -1
  54. package/dist/modules/communication_channels/lib/thread-token.js.map +1 -1
  55. package/dist/modules/currencies/api/currencies/route.js +4 -3
  56. package/dist/modules/currencies/api/currencies/route.js.map +2 -2
  57. package/dist/modules/customer_accounts/api/admin/roles.js +2 -1
  58. package/dist/modules/customer_accounts/api/admin/roles.js.map +2 -2
  59. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.js +0 -3
  60. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.js.map +2 -2
  61. package/dist/modules/customer_accounts/events.js +1 -1
  62. package/dist/modules/customer_accounts/events.js.map +1 -1
  63. package/dist/modules/customer_accounts/lib/resolveTenantContext.js.map +1 -1
  64. package/dist/modules/customers/acl.js +1 -1
  65. package/dist/modules/customers/acl.js.map +1 -1
  66. package/dist/modules/customers/ai-tools/companies-pack.js.map +1 -1
  67. package/dist/modules/customers/ai-tools/deals-pack.js.map +1 -1
  68. package/dist/modules/customers/ai-tools/people-pack.js.map +1 -1
  69. package/dist/modules/customers/api/companies/route.js +4 -4
  70. package/dist/modules/customers/api/companies/route.js.map +2 -2
  71. package/dist/modules/customers/api/deals/route.js +43 -2
  72. package/dist/modules/customers/api/deals/route.js.map +2 -2
  73. package/dist/modules/customers/api/deals/summary/route.js +402 -0
  74. package/dist/modules/customers/api/deals/summary/route.js.map +7 -0
  75. package/dist/modules/customers/api/people/route.js +4 -4
  76. package/dist/modules/customers/api/people/route.js.map +2 -2
  77. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealActivities.js +16 -5
  78. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealActivities.js.map +2 -2
  79. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealData.js +22 -5
  80. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealData.js.map +2 -2
  81. package/dist/modules/customers/backend/customers/deals/[id]/page.js +12 -2
  82. package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
  83. package/dist/modules/customers/backend/customers/deals/page.js +221 -56
  84. package/dist/modules/customers/backend/customers/deals/page.js.map +3 -3
  85. package/dist/modules/customers/backend/customers/deals/pipeline/page.js +1 -1
  86. package/dist/modules/customers/backend/customers/deals/pipeline/page.js.map +2 -2
  87. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +18 -0
  88. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
  89. package/dist/modules/customers/cli.js +15 -9
  90. package/dist/modules/customers/cli.js.map +2 -2
  91. package/dist/modules/customers/commands/addresses.js +5 -5
  92. package/dist/modules/customers/commands/addresses.js.map +2 -2
  93. package/dist/modules/customers/commands/comments.js +5 -5
  94. package/dist/modules/customers/commands/comments.js.map +2 -2
  95. package/dist/modules/customers/commands/deals.js +2 -2
  96. package/dist/modules/customers/commands/deals.js.map +2 -2
  97. package/dist/modules/customers/commands/entity-roles.js +2 -1
  98. package/dist/modules/customers/commands/entity-roles.js.map +2 -2
  99. package/dist/modules/customers/commands/interactions.js +8 -5
  100. package/dist/modules/customers/commands/interactions.js.map +2 -2
  101. package/dist/modules/customers/commands/shared.js +21 -6
  102. package/dist/modules/customers/commands/shared.js.map +2 -2
  103. package/dist/modules/customers/commands/tags.js +3 -3
  104. package/dist/modules/customers/commands/tags.js.map +2 -2
  105. package/dist/modules/customers/components/DealsKpiStrip.js +282 -0
  106. package/dist/modules/customers/components/DealsKpiStrip.js.map +7 -0
  107. package/dist/modules/customers/components/detail/ConfirmDealLostDialog.js +0 -1
  108. package/dist/modules/customers/components/detail/ConfirmDealLostDialog.js.map +2 -2
  109. package/dist/modules/customers/components/detail/DealForm.js +100 -17
  110. package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
  111. package/dist/modules/customers/components/detail/PersonDetailTabs.js +11 -3
  112. package/dist/modules/customers/components/detail/PersonDetailTabs.js.map +2 -2
  113. package/dist/modules/customers/components/detail/ScheduleActivityDialog.js +1 -2
  114. package/dist/modules/customers/components/detail/ScheduleActivityDialog.js.map +2 -2
  115. package/dist/modules/customers/components/detail/assignableStaff.js +21 -8
  116. package/dist/modules/customers/components/detail/assignableStaff.js.map +2 -2
  117. package/dist/modules/customers/components/kpi/PipelineStageBar.js +63 -0
  118. package/dist/modules/customers/components/kpi/PipelineStageBar.js.map +7 -0
  119. package/dist/modules/customers/lib/dealsMetrics.js +82 -0
  120. package/dist/modules/customers/lib/dealsMetrics.js.map +7 -0
  121. package/dist/modules/customers/migrations/Migration20260519120000_pipeline_stage_color_tones.js.map +1 -1
  122. package/dist/modules/data_sync/api/run.js +1 -1
  123. package/dist/modules/data_sync/api/run.js.map +2 -2
  124. package/dist/modules/directory/api/organization-branding/route.js +214 -0
  125. package/dist/modules/directory/api/organization-branding/route.js.map +7 -0
  126. package/dist/modules/directory/api/organizations/route.js +7 -0
  127. package/dist/modules/directory/api/organizations/route.js.map +3 -3
  128. package/dist/modules/directory/backend/directory/branding/page.js +214 -0
  129. package/dist/modules/directory/backend/directory/branding/page.js.map +7 -0
  130. package/dist/modules/directory/backend/directory/branding/page.meta.js +26 -0
  131. package/dist/modules/directory/backend/directory/branding/page.meta.js.map +7 -0
  132. package/dist/modules/directory/commands/organizations.js +8 -1
  133. package/dist/modules/directory/commands/organizations.js.map +2 -2
  134. package/dist/modules/directory/data/entities.js +3 -0
  135. package/dist/modules/directory/data/entities.js.map +2 -2
  136. package/dist/modules/directory/data/validators.js +9 -0
  137. package/dist/modules/directory/data/validators.js.map +2 -2
  138. package/dist/modules/directory/migrations/Migration20260607222259_directory.js +13 -0
  139. package/dist/modules/directory/migrations/Migration20260607222259_directory.js.map +7 -0
  140. package/dist/modules/directory/subscribers/invalidateOrgScopeCache.js +2 -1
  141. package/dist/modules/directory/subscribers/invalidateOrgScopeCache.js.map +2 -2
  142. package/dist/modules/directory/utils/organizationScope.js +59 -27
  143. package/dist/modules/directory/utils/organizationScope.js.map +2 -2
  144. package/dist/modules/entities/api/definitions.batch.js +2 -1
  145. package/dist/modules/entities/api/definitions.batch.js.map +2 -2
  146. package/dist/modules/entities/api/entities.js +7 -0
  147. package/dist/modules/entities/api/entities.js.map +2 -2
  148. package/dist/modules/entities/api/records.js +26 -15
  149. package/dist/modules/entities/api/records.js.map +2 -2
  150. package/dist/modules/entities/backend/entities/user/[entityId]/records/[recordId]/page.js +14 -0
  151. package/dist/modules/entities/backend/entities/user/[entityId]/records/[recordId]/page.js.map +2 -2
  152. package/dist/modules/entities/backend/entities/user/[entityId]/records/create/page.js +14 -0
  153. package/dist/modules/entities/backend/entities/user/[entityId]/records/create/page.js.map +2 -2
  154. package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js +12 -0
  155. package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js.map +2 -2
  156. package/dist/modules/entities/components/useRecordsEntityGuard.js +30 -0
  157. package/dist/modules/entities/components/useRecordsEntityGuard.js.map +7 -0
  158. package/dist/modules/payment_gateways/api/transactions/route.js +2 -4
  159. package/dist/modules/payment_gateways/api/transactions/route.js.map +2 -2
  160. package/dist/modules/progress/api/jobs/[id]/route.js +7 -2
  161. package/dist/modules/progress/api/jobs/[id]/route.js.map +2 -2
  162. package/dist/modules/progress/api/jobs/route.js +1 -1
  163. package/dist/modules/progress/api/jobs/route.js.map +2 -2
  164. package/dist/modules/progress/lib/progressServiceImpl.js +8 -2
  165. package/dist/modules/progress/lib/progressServiceImpl.js.map +2 -2
  166. package/dist/modules/query_index/data/entities.js +2 -1
  167. package/dist/modules/query_index/data/entities.js.map +2 -2
  168. package/dist/modules/query_index/lib/engine.js +4 -2
  169. package/dist/modules/query_index/lib/engine.js.map +2 -2
  170. package/dist/modules/query_index/migrations/Migration20260611103000_query_index.js +16 -0
  171. package/dist/modules/query_index/migrations/Migration20260611103000_query_index.js.map +7 -0
  172. package/dist/modules/resources/api/resources.js +2 -3
  173. package/dist/modules/resources/api/resources.js.map +2 -2
  174. package/dist/modules/sales/api/documents/factory.js +2 -2
  175. package/dist/modules/sales/api/documents/factory.js.map +2 -2
  176. package/dist/modules/sales/commands/documents.js +7 -5
  177. package/dist/modules/sales/commands/documents.js.map +2 -2
  178. package/dist/modules/sales/components/documents/SalesDocumentsTable.js +2 -1
  179. package/dist/modules/sales/components/documents/SalesDocumentsTable.js.map +2 -2
  180. package/dist/modules/sales/components/documents/salesDocumentsColumns.js +10 -0
  181. package/dist/modules/sales/components/documents/salesDocumentsColumns.js.map +7 -0
  182. package/dist/modules/staff/api/team-members.js +9 -2
  183. package/dist/modules/staff/api/team-members.js.map +2 -2
  184. package/dist/modules/staff/api/timesheets/time-entries/[id]/timer-start/route.js +24 -1
  185. package/dist/modules/staff/api/timesheets/time-entries/[id]/timer-start/route.js.map +2 -2
  186. package/dist/modules/staff/backend/staff/team-members/[id]/page.js +11 -6
  187. package/dist/modules/staff/backend/staff/team-members/[id]/page.js.map +2 -2
  188. package/dist/modules/staff/commands/team-members.js +1 -1
  189. package/dist/modules/staff/commands/team-members.js.map +2 -2
  190. package/dist/modules/staff/components/TeamMemberForm.js +1 -1
  191. package/dist/modules/staff/components/TeamMemberForm.js.map +2 -2
  192. package/dist/modules/staff/lib/scheduleSwitch.js +23 -0
  193. package/dist/modules/staff/lib/scheduleSwitch.js.map +7 -0
  194. package/dist/modules/sync_excel/api/import/route.js +1 -1
  195. package/dist/modules/sync_excel/api/import/route.js.map +2 -2
  196. package/dist/modules/workflows/api/definitions/route.js +3 -2
  197. package/dist/modules/workflows/api/definitions/route.js.map +2 -2
  198. package/dist/modules/workflows/backend/definitions/create/page.js +1 -2
  199. package/dist/modules/workflows/backend/definitions/create/page.js.map +2 -2
  200. package/dist/modules/workflows/backend/definitions/visual-editor/page.js +1 -2
  201. package/dist/modules/workflows/backend/definitions/visual-editor/page.js.map +2 -2
  202. package/dist/modules/workflows/components/DefinitionTriggersEditor.js +1 -2
  203. package/dist/modules/workflows/components/DefinitionTriggersEditor.js.map +2 -2
  204. package/dist/modules/workflows/components/NodeEditDialog.js +4 -13
  205. package/dist/modules/workflows/components/NodeEditDialog.js.map +2 -2
  206. package/dist/modules/workflows/components/NodeEditDialogCrudForm.js +4 -13
  207. package/dist/modules/workflows/components/NodeEditDialogCrudForm.js.map +2 -2
  208. package/dist/modules/workflows/components/WorkflowGraphImpl.js +1 -4
  209. package/dist/modules/workflows/components/WorkflowGraphImpl.js.map +2 -2
  210. package/dist/modules/workflows/components/fields/FormFieldArrayEditor.js +2 -5
  211. package/dist/modules/workflows/components/fields/FormFieldArrayEditor.js.map +2 -2
  212. package/generated/entities/organization/index.ts +1 -0
  213. package/generated/entity-fields-registry.ts +1 -0
  214. package/package.json +11 -12
  215. package/src/bootstrap.ts +65 -7
  216. package/src/helpers/integration/crmFixtures.ts +21 -1
  217. package/src/modules/attachments/AGENTS.md +79 -0
  218. package/src/modules/attachments/api/library/route.ts +2 -2
  219. package/src/modules/attachments/api/route.ts +2 -0
  220. package/src/modules/attachments/components/AttachmentContentPreview.tsx +6 -6
  221. package/src/modules/attachments/lib/access.ts +36 -0
  222. package/src/modules/audit_logs/api/audit-logs/actions/redo/route.ts +14 -2
  223. package/src/modules/audit_logs/data/entities.ts +1 -0
  224. package/src/modules/audit_logs/migrations/.snapshot-open-mercato.json +10 -0
  225. package/src/modules/audit_logs/migrations/Migration20260611104500.ts +13 -0
  226. package/src/modules/audit_logs/services/accessLogService.ts +15 -0
  227. package/src/modules/auth/api/admin/nav.ts +9 -0
  228. package/src/modules/auth/api/login.ts +13 -13
  229. package/src/modules/auth/commands/users.ts +32 -15
  230. package/src/modules/auth/data/entities.ts +13 -1
  231. package/src/modules/auth/i18n/de.json +0 -1
  232. package/src/modules/auth/i18n/en.json +0 -1
  233. package/src/modules/auth/i18n/es.json +0 -1
  234. package/src/modules/auth/i18n/pl.json +0 -1
  235. package/src/modules/auth/lib/backendChrome.tsx +37 -1
  236. package/src/modules/auth/lib/consentIntegrity.ts +6 -3
  237. package/src/modules/auth/migrations/.snapshot-open-mercato.json +20 -10
  238. package/src/modules/auth/migrations/Migration20260610120000.ts +53 -0
  239. package/src/modules/auth/migrations/Migration20260611103000.ts +21 -0
  240. package/src/modules/auth/services/authService.ts +24 -4
  241. package/src/modules/auth/services/rbacService.ts +11 -2
  242. package/src/modules/catalog/ai-tools/configuration-pack.ts +1 -1
  243. package/src/modules/catalog/ai-tools/prices-offers-pack.ts +1 -1
  244. package/src/modules/catalog/ai-tools/products-pack.ts +1 -1
  245. package/src/modules/catalog/ai-tools/variants-pack.ts +1 -1
  246. package/src/modules/communication_channels/data/entities.ts +2 -2
  247. package/src/modules/communication_channels/encryption.ts +1 -1
  248. package/src/modules/communication_channels/lib/adapter.ts +1 -1
  249. package/src/modules/communication_channels/lib/thread-matcher.ts +1 -1
  250. package/src/modules/communication_channels/lib/thread-token.ts +1 -1
  251. package/src/modules/currencies/api/currencies/route.ts +4 -3
  252. package/src/modules/customer_accounts/api/admin/roles.ts +2 -1
  253. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.tsx +0 -3
  254. package/src/modules/customer_accounts/events.ts +1 -1
  255. package/src/modules/customer_accounts/lib/resolveTenantContext.ts +2 -2
  256. package/src/modules/customers/acl.ts +1 -1
  257. package/src/modules/customers/ai-tools/companies-pack.ts +1 -1
  258. package/src/modules/customers/ai-tools/deals-pack.ts +1 -1
  259. package/src/modules/customers/ai-tools/people-pack.ts +1 -1
  260. package/src/modules/customers/api/companies/route.ts +4 -4
  261. package/src/modules/customers/api/deals/route.ts +51 -2
  262. package/src/modules/customers/api/deals/summary/route.ts +496 -0
  263. package/src/modules/customers/api/people/route.ts +4 -4
  264. package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealActivities.ts +28 -6
  265. package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealData.ts +33 -6
  266. package/src/modules/customers/backend/customers/deals/[id]/page.tsx +17 -2
  267. package/src/modules/customers/backend/customers/deals/page.tsx +254 -66
  268. package/src/modules/customers/backend/customers/deals/pipeline/page.tsx +1 -2
  269. package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +18 -0
  270. package/src/modules/customers/cli.ts +15 -15
  271. package/src/modules/customers/commands/addresses.ts +5 -5
  272. package/src/modules/customers/commands/comments.ts +5 -5
  273. package/src/modules/customers/commands/deals.ts +2 -2
  274. package/src/modules/customers/commands/entity-roles.ts +2 -1
  275. package/src/modules/customers/commands/interactions.ts +8 -5
  276. package/src/modules/customers/commands/shared.ts +26 -4
  277. package/src/modules/customers/commands/tags.ts +3 -3
  278. package/src/modules/customers/components/DealsKpiStrip.tsx +389 -0
  279. package/src/modules/customers/components/detail/ConfirmDealLostDialog.tsx +0 -1
  280. package/src/modules/customers/components/detail/DealForm.tsx +121 -19
  281. package/src/modules/customers/components/detail/PersonDetailTabs.tsx +12 -2
  282. package/src/modules/customers/components/detail/ScheduleActivityDialog.tsx +1 -2
  283. package/src/modules/customers/components/detail/assignableStaff.ts +32 -8
  284. package/src/modules/customers/components/kpi/PipelineStageBar.tsx +77 -0
  285. package/src/modules/customers/i18n/de.json +43 -0
  286. package/src/modules/customers/i18n/en.json +43 -0
  287. package/src/modules/customers/i18n/es.json +43 -0
  288. package/src/modules/customers/i18n/pl.json +43 -0
  289. package/src/modules/customers/lib/dealsMetrics.ts +159 -0
  290. package/src/modules/customers/migrations/Migration20260519120000_pipeline_stage_color_tones.ts +1 -1
  291. package/src/modules/data_sync/api/run.ts +1 -1
  292. package/src/modules/directory/api/organization-branding/route.ts +238 -0
  293. package/src/modules/directory/api/organizations/route.ts +7 -0
  294. package/src/modules/directory/backend/directory/branding/page.meta.ts +24 -0
  295. package/src/modules/directory/backend/directory/branding/page.tsx +248 -0
  296. package/src/modules/directory/commands/organizations.ts +9 -1
  297. package/src/modules/directory/data/entities.ts +3 -0
  298. package/src/modules/directory/data/validators.ts +12 -0
  299. package/src/modules/directory/i18n/de.json +21 -0
  300. package/src/modules/directory/i18n/en.json +21 -0
  301. package/src/modules/directory/i18n/es.json +21 -0
  302. package/src/modules/directory/i18n/pl.json +21 -0
  303. package/src/modules/directory/migrations/.snapshot-open-mercato.json +40 -0
  304. package/src/modules/directory/migrations/Migration20260607222259_directory.ts +13 -0
  305. package/src/modules/directory/subscribers/invalidateOrgScopeCache.ts +3 -1
  306. package/src/modules/directory/utils/organizationScope.ts +85 -30
  307. package/src/modules/entities/api/definitions.batch.ts +11 -7
  308. package/src/modules/entities/api/entities.ts +11 -0
  309. package/src/modules/entities/api/records.ts +46 -25
  310. package/src/modules/entities/backend/entities/user/[entityId]/records/[recordId]/page.tsx +15 -0
  311. package/src/modules/entities/backend/entities/user/[entityId]/records/create/page.tsx +15 -0
  312. package/src/modules/entities/backend/entities/user/[entityId]/records/page.tsx +23 -0
  313. package/src/modules/entities/components/useRecordsEntityGuard.ts +41 -0
  314. package/src/modules/entities/i18n/de.json +1 -0
  315. package/src/modules/entities/i18n/en.json +1 -0
  316. package/src/modules/entities/i18n/es.json +1 -0
  317. package/src/modules/entities/i18n/pl.json +1 -0
  318. package/src/modules/payment_gateways/api/transactions/route.ts +2 -5
  319. package/src/modules/progress/api/jobs/[id]/route.ts +6 -1
  320. package/src/modules/progress/api/jobs/route.ts +1 -1
  321. package/src/modules/progress/lib/progressServiceImpl.ts +7 -1
  322. package/src/modules/query_index/data/entities.ts +1 -0
  323. package/src/modules/query_index/lib/engine.ts +11 -5
  324. package/src/modules/query_index/migrations/.snapshot-open-mercato.json +11 -0
  325. package/src/modules/query_index/migrations/Migration20260611103000_query_index.ts +29 -0
  326. package/src/modules/resources/api/resources.ts +2 -3
  327. package/src/modules/sales/api/documents/factory.ts +2 -2
  328. package/src/modules/sales/commands/documents.ts +7 -5
  329. package/src/modules/sales/components/documents/SalesDocumentsTable.tsx +2 -1
  330. package/src/modules/sales/components/documents/salesDocumentsColumns.ts +6 -0
  331. package/src/modules/staff/AGENTS.md +1 -1
  332. package/src/modules/staff/api/team-members.ts +9 -2
  333. package/src/modules/staff/api/timesheets/time-entries/[id]/timer-start/route.ts +31 -1
  334. package/src/modules/staff/backend/staff/team-members/[id]/page.tsx +18 -8
  335. package/src/modules/staff/commands/team-members.ts +5 -2
  336. package/src/modules/staff/components/TeamMemberForm.tsx +4 -1
  337. package/src/modules/staff/i18n/de.json +1 -0
  338. package/src/modules/staff/i18n/en.json +1 -0
  339. package/src/modules/staff/i18n/es.json +1 -0
  340. package/src/modules/staff/i18n/pl.json +1 -0
  341. package/src/modules/staff/lib/scheduleSwitch.ts +46 -0
  342. package/src/modules/sync_excel/api/import/route.ts +1 -1
  343. package/src/modules/workflows/api/definitions/route.ts +3 -2
  344. package/src/modules/workflows/backend/definitions/create/page.tsx +1 -2
  345. package/src/modules/workflows/backend/definitions/visual-editor/page.tsx +1 -2
  346. package/src/modules/workflows/components/DefinitionTriggersEditor.tsx +1 -2
  347. package/src/modules/workflows/components/NodeEditDialog.tsx +1 -4
  348. package/src/modules/workflows/components/NodeEditDialogCrudForm.tsx +4 -7
  349. package/src/modules/workflows/components/WorkflowGraphImpl.tsx +1 -2
  350. package/src/modules/workflows/components/fields/FormFieldArrayEditor.tsx +2 -3
@@ -0,0 +1,53 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+
3
+ // #2934: User email uniqueness must be per-tenant, not global. The original
4
+ // `users_email_unique` constraint (unique on `email` across all tenants) contradicts the
5
+ // multi-tenant login flow — which resolves the same email across tenants via
6
+ // `findUsersByEmail` — and leaks cross-tenant account existence / enables registration
7
+ // squatting. Replace it with a partial unique index scoped per-tenant over live rows.
8
+ //
9
+ // The index is keyed on `email_hash`, NOT `email`: `email` is encrypted at rest with a
10
+ // per-row IV (auth/encryption.ts -> shared aes.ts), so its ciphertext is non-deterministic
11
+ // and a unique index on it would never detect duplicates under the default (encryption-on)
12
+ // configuration. `email_hash` is the deterministic lookup hash the application already
13
+ // de-dupes on, so the constraint is effective in both encryption-on and encryption-off
14
+ // modes. This mirrors `customer_users_tenant_email_hash_uniq` in customer_accounts.
15
+ //
16
+ // `WHERE deleted_at IS NULL` lets a soft-deleted user's email be reused (the old non-partial
17
+ // constraint blocked this); `AND email_hash IS NOT NULL` skips the rare legacy/bootstrap rows
18
+ // that predate hash population (encryption-off `setup-app` users) — those remain protected by
19
+ // the tenant-scoped application duplicate check.
20
+ //
21
+ // Before creating the index, soft-delete any pre-existing duplicate live rows per
22
+ // (tenant_id, email_hash), keeping the most-recently-updated one. Under encryption the old
23
+ // `email` constraint never fired, so same-tenant duplicates were only blocked by the
24
+ // application check and a historical race could have slipped one through; the dedupe makes
25
+ // the index creation safe on such data (no-op when there are none).
26
+ export class Migration20260610120000 extends Migration {
27
+
28
+ override up(): void | Promise<void> {
29
+ this.addSql(`
30
+ with ranked as (
31
+ select id,
32
+ row_number() over (
33
+ partition by tenant_id, email_hash
34
+ order by coalesce(updated_at, created_at) desc, created_at desc, id desc
35
+ ) as rn
36
+ from users
37
+ where deleted_at is null and email_hash is not null
38
+ )
39
+ update users
40
+ set deleted_at = now()
41
+ from ranked
42
+ where users.id = ranked.id and ranked.rn > 1;
43
+ `);
44
+ this.addSql(`alter table "users" drop constraint if exists "users_email_unique";`);
45
+ this.addSql(`create unique index if not exists "users_tenant_email_hash_uniq" on "users" ("tenant_id", "email_hash") where "deleted_at" is null and "email_hash" is not null;`);
46
+ }
47
+
48
+ override down(): void | Promise<void> {
49
+ this.addSql(`drop index if exists "users_tenant_email_hash_uniq";`);
50
+ this.addSql(`alter table "users" add constraint "users_email_unique" unique ("email");`);
51
+ }
52
+
53
+ }
@@ -0,0 +1,21 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+
3
+ // #2966: user_roles carries only its FK constraints and Postgres does not
4
+ // auto-index FK columns, so RBAC scans it sequentially by user_id on every
5
+ // ACL cache miss (rbacService super-admin check + ACL aggregation) and by
6
+ // role_id on user-list filtering and role rename/delete guards. Index both
7
+ // FK columns so these hot paths become index scans. The table is small
8
+ // relative to search_tokens, so a plain (transactional) build is safe.
9
+ export class Migration20260611103000 extends Migration {
10
+
11
+ override up(): void | Promise<void> {
12
+ this.addSql(`create index if not exists "user_roles_user_id_idx" on "user_roles" ("user_id");`);
13
+ this.addSql(`create index if not exists "user_roles_role_id_idx" on "user_roles" ("role_id");`);
14
+ }
15
+
16
+ override down(): void | Promise<void> {
17
+ this.addSql(`drop index if exists "user_roles_user_id_idx";`);
18
+ this.addSql(`drop index if exists "user_roles_role_id_idx";`);
19
+ }
20
+
21
+ }
@@ -5,6 +5,13 @@ import { emailHashLookupValues } from '@open-mercato/core/modules/auth/lib/email
5
5
  import { generateAuthToken, hashAuthToken } from '@open-mercato/core/modules/auth/lib/tokenHash'
6
6
  import { findWithDecryption, findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
7
7
 
8
+ // A fixed, valid bcrypt hash (cost 10) of a throwaway value no real password
9
+ // can match. verifyPassword compares against it whenever the user is missing or
10
+ // has no password hash, so a failed login spends the same bcrypt CPU time
11
+ // regardless of whether the account exists — closing the timing side channel
12
+ // for account enumeration (issue #2242).
13
+ const TIMING_EQUALIZER_PASSWORD_HASH = '$2b$10$OcZrhmZpIzJOjkfwUrk7d.Nl0eHNzOvalBcBlt5Ran.4lj8R3HZg6'
14
+
8
15
  export class AuthService {
9
16
  constructor(private em: EntityManager) {}
10
17
 
@@ -48,9 +55,13 @@ export class AuthService {
48
55
  )
49
56
  }
50
57
 
51
- async verifyPassword(user: User, password: string) {
52
- if (!user.passwordHash) return false
53
- return compare(password, user.passwordHash)
58
+ async verifyPassword(user: User | null, password: string) {
59
+ const storedHash = user?.passwordHash ?? null
60
+ // Always run a bcrypt comparison — against a fixed dummy hash when the user
61
+ // is absent or has no password — so login latency does not reveal whether
62
+ // the account exists (timing-based enumeration, issue #2242).
63
+ const matched = await compare(password, storedHash ?? TIMING_EQUALIZER_PASSWORD_HASH)
64
+ return storedHash !== null && matched
54
65
  }
55
66
 
56
67
  async updateLastLoginAt(user: User) {
@@ -70,7 +81,16 @@ export class AuthService {
70
81
  { populate: ['role'] },
71
82
  { tenantId: resolvedTenantId, organizationId: user.organizationId ?? null },
72
83
  )
73
- return links.map((l) => l.role.name)
84
+ // A populated `role` can still be null when the link points at a soft-deleted
85
+ // role (the Role soft-delete filter suppresses hydration), e.g. an admin link
86
+ // orphaned by a re-seed during interrupted-provisioning recovery. Dropping such
87
+ // links keeps role resolution from throwing on the login / session-refresh hot
88
+ // path, mirroring resolveCanonicalStaffAuthContext in lib/sessionIntegrity.ts.
89
+ return links
90
+ .map((l) => l.role)
91
+ .filter((role): role is Role => !!role)
92
+ .map((role) => role.name)
93
+ .filter((name): name is string => typeof name === 'string' && name.trim().length > 0)
74
94
  }
75
95
 
76
96
 
@@ -4,6 +4,7 @@ import { getCurrentCacheTenant, runWithCacheTenant } from '@open-mercato/cache'
4
4
  import { UserAcl, RoleAcl, User, UserRole } from '@open-mercato/core/modules/auth/data/entities'
5
5
  import { ApiKey } from '@open-mercato/core/modules/api_keys/data/entities'
6
6
  import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
7
+ import { buildOrgScopeUserCacheTag, buildOrgScopeTenantCacheTag } from '@open-mercato/core/modules/directory/utils/organizationScope'
7
8
  import { matchFeature as sharedMatchFeature, hasAllFeatures as sharedHasAllFeatures } from '@open-mercato/shared/lib/auth/featureMatch'
8
9
  import { filterGrantsByEnabledModules, getOwningModuleId, getEnabledModuleIds } from '@open-mercato/shared/security/enabledModulesRegistry'
9
10
 
@@ -130,7 +131,12 @@ export class RbacService {
130
131
  */
131
132
  async invalidateUserCache(userId: string): Promise<void> {
132
133
  this.globalSuperAdminCache.delete(userId)
133
- await this.deleteCacheByTags([this.getUserTag(userId)])
134
+ // Also drop the directory OrganizationScope cache for this user. That scope's
135
+ // accessible-org set is derived from this user's ACL/role grants, so any
136
+ // permission change that invalidates the RBAC cache must invalidate the
137
+ // resolved scope too. This is the missing `org-scope:user:*` caller required
138
+ // before the cross-request scope TTL can be safely enabled (issue #2259).
139
+ await this.deleteCacheByTags([this.getUserTag(userId), buildOrgScopeUserCacheTag(userId)])
134
140
  }
135
141
 
136
142
  /**
@@ -142,7 +148,10 @@ export class RbacService {
142
148
  */
143
149
  async invalidateTenantCache(tenantId: string): Promise<void> {
144
150
  this.globalSuperAdminCache.clear()
145
- await this.deleteCacheByTags([this.getTenantTag(tenantId)], [tenantId])
151
+ // Role ACL changes invalidate every user in the tenant; the resolved
152
+ // OrganizationScope for those users derives from the same grants, so drop
153
+ // the tenant-tagged scope entries alongside the RBAC ones (issue #2259).
154
+ await this.deleteCacheByTags([this.getTenantTag(tenantId), buildOrgScopeTenantCacheTag(tenantId)], [tenantId])
146
155
  }
147
156
 
148
157
  /**
@@ -5,7 +5,7 @@
5
5
  * Product-configuration surface: option schemas (variant axes) and unit
6
6
  * conversions (UoM factors).
7
7
  *
8
- * Phase 3c of `.ai/specs/2026-04-27-ai-tools-api-backed-dry-refactor.md`:
8
+ * Phase 3c of `.ai/specs/implemented/2026-04-27-ai-tools-api-backed-dry-refactor.md`:
9
9
  * both tools are now API-backed wrappers over the documented CRUD list
10
10
  * routes (`GET /api/catalog/option-schemas` and
11
11
  * `GET /api/catalog/product-unit-conversions`). Tool names, schemas,
@@ -11,7 +11,7 @@
11
11
  * names available so the D18 tool can layer merchandising-specific shape
12
12
  * over the base enumerator.
13
13
  *
14
- * Phase 3b of `.ai/specs/2026-04-27-ai-tools-api-backed-dry-refactor.md`:
14
+ * Phase 3b of `.ai/specs/implemented/2026-04-27-ai-tools-api-backed-dry-refactor.md`:
15
15
  * `catalog.list_prices` and `catalog.list_offers` are now API-backed wrappers
16
16
  * over `GET /api/catalog/prices` and `GET /api/catalog/offers`. Tool names,
17
17
  * schemas, requiredFeatures, and output shapes are unchanged. The offers
@@ -4,7 +4,7 @@
4
4
  * Read-only tools scoped to `ctx.tenantId` + `ctx.organizationId`. Mutation
5
5
  * tools are deferred to Step 5.14 under the pending-action contract.
6
6
  *
7
- * Phase 3b of `.ai/specs/2026-04-27-ai-tools-api-backed-dry-refactor.md`:
7
+ * Phase 3b of `.ai/specs/implemented/2026-04-27-ai-tools-api-backed-dry-refactor.md`:
8
8
  * `catalog.list_products` is now an API-backed wrapper over
9
9
  * `GET /api/catalog/products`. Tool name, schema, requiredFeatures, and
10
10
  * output shape are unchanged.
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Enumerate variants for a single product with option values + media refs.
5
5
  *
6
- * Phase 3b of `.ai/specs/2026-04-27-ai-tools-api-backed-dry-refactor.md`:
6
+ * Phase 3b of `.ai/specs/implemented/2026-04-27-ai-tools-api-backed-dry-refactor.md`:
7
7
  * `catalog.list_variants` is now an API-backed wrapper over
8
8
  * `GET /api/catalog/variants`. Tool name, schema, requiredFeatures, and
9
9
  * output shape are unchanged.
@@ -440,7 +440,7 @@ export class MessageReaction {
440
440
  * forged inbound messages: tokens that don't HMAC-verify never reach the DB
441
441
  * lookup.
442
442
  *
443
- * See `.ai/specs/2026-05-27-email-integration-inbound-reliability-and-threading.md`.
443
+ * See `.ai/specs/implemented/2026-05-27-email-integration-inbound-reliability-and-threading.md`.
444
444
  */
445
445
  @Entity({ tableName: 'channel_thread_tokens' })
446
446
  // One token row per (tenant, thread): the matcher resolves every reply to the
@@ -495,7 +495,7 @@ export class ChannelThreadToken {
495
495
  * `raw_body` is encrypted at rest via the module's `encryption.ts`
496
496
  * `defaultEncryptionMaps` entry (MIME bodies may contain PII).
497
497
  *
498
- * See `.ai/specs/2026-05-27-email-integration-inbound-reliability-and-threading.md`
498
+ * See `.ai/specs/implemented/2026-05-27-email-integration-inbound-reliability-and-threading.md`
499
499
  * (§ 3 Data Model).
500
500
  */
501
501
  @Entity({ tableName: 'channel_ingest_dead_letters' })
@@ -31,7 +31,7 @@ import type { ModuleEncryptionMap } from '@open-mercato/shared/modules/encryptio
31
31
  * contact resolution looks addresses up by value (see the address blind-index
32
32
  * follow-up in customers/lib/findPeopleByAddresses.ts).
33
33
  *
34
- * See `.ai/specs/2026-05-27-email-integration-inbound-reliability-and-threading.md`
34
+ * See `.ai/specs/implemented/2026-05-27-email-integration-inbound-reliability-and-threading.md`
35
35
  * (§ 3 Encryption posture).
36
36
  */
37
37
  export const defaultEncryptionMaps: ModuleEncryptionMap[] = [
@@ -445,7 +445,7 @@ export interface ExchangeOAuthCodeResult {
445
445
  * `RefreshCredentialsInput`. Adapters without OAuth refresh (IMAP, WhatsApp
446
446
  * Business API) ignore it.
447
447
  *
448
- * See `.ai/specs/2026-05-27-email-integration-inbound-reliability-and-threading.md`.
448
+ * See `.ai/specs/implemented/2026-05-27-email-integration-inbound-reliability-and-threading.md`.
449
449
  */
450
450
  export interface OAuthClientConfig {
451
451
  clientId: string
@@ -19,7 +19,7 @@ import { extractTokenFromBody, extractTokenFromHeaders } from './thread-token'
19
19
  * `UPDATE` that bumps a matched token's `last_seen_at` (a future-GC hint),
20
20
  * which does not touch the caller's pending entities.
21
21
  *
22
- * See `.ai/specs/2026-05-27-email-integration-inbound-reliability-and-threading.md`
22
+ * See `.ai/specs/implemented/2026-05-27-email-integration-inbound-reliability-and-threading.md`
23
23
  * § 4 Threading Algorithm.
24
24
  */
25
25
 
@@ -17,7 +17,7 @@ import { isUniqueViolation } from './pg-errors'
17
17
  * `(tenantId, token)` so that even if the HMAC key leaked, tenant
18
18
  * isolation still holds at the DB layer.
19
19
  *
20
- * See `.ai/specs/2026-05-27-email-integration-inbound-reliability-and-threading.md`.
20
+ * See `.ai/specs/implemented/2026-05-27-email-integration-inbound-reliability-and-threading.md`.
21
21
  */
22
22
 
23
23
  const TOKEN_PREFIX = 'om_'
@@ -6,6 +6,7 @@ import type { EntityManager } from '@mikro-orm/postgresql'
6
6
  import type { FilterQuery } from '@mikro-orm/core'
7
7
  import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
8
8
  import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
9
+ import { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'
9
10
  import { currencyCreateSchema, currencyUpdateSchema } from '../../data/validators'
10
11
  import {
11
12
  createCurrenciesCrudOpenApi,
@@ -148,9 +149,9 @@ export async function GET(req: Request) {
148
149
  if (code) filter.code = code
149
150
  if (search) {
150
151
  filter.$or = [
151
- { code: { $ilike: `%${search}%` } },
152
- { name: { $ilike: `%${search}%` } },
153
- { symbol: { $ilike: `%${search}%` } },
152
+ { code: { $ilike: `%${escapeLikePattern(search)}%` } },
153
+ { name: { $ilike: `%${escapeLikePattern(search)}%` } },
154
+ { symbol: { $ilike: `%${escapeLikePattern(search)}%` } },
154
155
  ]
155
156
  }
156
157
  if (isBase === 'true') filter.isBase = true
@@ -3,6 +3,7 @@ import { z } from 'zod'
3
3
  import type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi'
4
4
  import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
5
5
  import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
6
+ import { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'
6
7
  import { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
7
8
  import { CustomerRole, CustomerRoleAcl } from '@open-mercato/core/modules/customer_accounts/data/entities'
8
9
  import { createRoleSchema } from '@open-mercato/core/modules/customer_accounts/data/validators'
@@ -37,7 +38,7 @@ export async function GET(req: Request) {
37
38
  }
38
39
 
39
40
  if (search) {
40
- const escapedSearch = search.replace(/[%_\\]/g, '\\$&')
41
+ const escapedSearch = escapeLikePattern(search)
41
42
  where.$or = [
42
43
  { name: { $ilike: `%${escapedSearch}%` } },
43
44
  { slug: { $ilike: `%${escapedSearch}%` } },
@@ -1,7 +1,6 @@
1
1
  "use client"
2
2
 
3
3
  import * as React from 'react'
4
- import { AlertTriangle } from 'lucide-react'
5
4
  import { Alert, AlertTitle, AlertDescription } from '@open-mercato/ui/primitives/alert'
6
5
  import { useT } from '@open-mercato/shared/lib/i18n/context'
7
6
  import type { DomainMappingRow } from './types'
@@ -15,7 +14,6 @@ export function DnsDiagnostics({ mapping }: DiagnosticsProps) {
15
14
  if (mapping.status !== 'dns_failed') return null
16
15
  return (
17
16
  <Alert variant="destructive">
18
- <AlertTriangle className="h-4 w-4" aria-hidden />
19
17
  <AlertTitle>
20
18
  {t('customer_accounts.domainMapping.dns.diagnostics.title', 'DNS configuration issue')}
21
19
  </AlertTitle>
@@ -41,7 +39,6 @@ export function TlsDiagnostics({ mapping }: DiagnosticsProps) {
41
39
  if (mapping.status !== 'tls_failed') return null
42
40
  return (
43
41
  <Alert variant="warning">
44
- <AlertTriangle className="h-4 w-4" aria-hidden />
45
42
  <AlertTitle>
46
43
  {t('customer_accounts.domainMapping.tls.diagnostics.title', 'SSL certificate issue')}
47
44
  </AlertTitle>
@@ -18,7 +18,7 @@ const events = [
18
18
  { id: 'customer_accounts.role.deleted', label: 'Customer Role Deleted', entity: 'role', category: 'crud' },
19
19
  { id: 'customer_accounts.invitation.accepted', label: 'Customer Invitation Accepted', category: 'lifecycle', clientBroadcast: true },
20
20
  { id: 'customer_accounts.password_reset.requested', label: 'Customer Password Reset Requested', category: 'lifecycle' },
21
- // Custom domain mapping lifecycle (see .ai/specs/2026-04-08-portal-custom-domain-routing.md)
21
+ // Custom domain mapping lifecycle (see .ai/specs/implemented/2026-04-08-portal-custom-domain-routing.md)
22
22
  { id: 'customer_accounts.domain_mapping.created', label: 'Custom Domain Registered', entity: 'domain_mapping', category: 'crud', clientBroadcast: true },
23
23
  { id: 'customer_accounts.domain_mapping.verified', label: 'Custom Domain DNS Verified', entity: 'domain_mapping', category: 'lifecycle', clientBroadcast: true },
24
24
  { id: 'customer_accounts.domain_mapping.activated', label: 'Custom Domain Active', entity: 'domain_mapping', category: 'lifecycle', clientBroadcast: true },
@@ -12,8 +12,8 @@
12
12
  * signup, magic-link, password-reset) so they all behave consistently when
13
13
  * the request arrives on a tenant's branded URL.
14
14
  *
15
- * See `.ai/specs/2026-04-08-portal-custom-domain-routing.md` Phase 1.5 and
16
- * `.ai/specs/2026-06-05-tenant-ownership-and-module-acl-authorization.md` § C.
15
+ * See `.ai/specs/implemented/2026-04-08-portal-custom-domain-routing.md` Phase 1.5 and
16
+ * `.ai/specs/implemented/2026-06-05-tenant-ownership-and-module-acl-authorization.md` § C.
17
17
  */
18
18
 
19
19
  import { tryNormalizeHostname } from '@open-mercato/core/modules/customer_accounts/lib/hostname'
@@ -89,7 +89,7 @@ export const features = [
89
89
  // privacy model is strict owner-only with NO admin bypass, so this feature is
90
90
  // declared but INERT — granting it does not unlock other users' private emails
91
91
  // (the visibility filter and the visibility-change gate ignore it). See
92
- // .ai/specs/2026-05-27-crm-email-integration.md (v1 strict owner-only).
92
+ // .ai/specs/implemented/2026-05-27-crm-email-integration.md (v1 strict owner-only).
93
93
  {
94
94
  id: 'customers.email.view_private',
95
95
  title: 'View other users\' private emails (reserved — inert in v1)',
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * `customers.list_companies` + `customers.get_company` (Phase 1 WS-C, Step 3.9).
3
3
  *
4
- * Phase 3a of `.ai/specs/2026-04-27-ai-tools-api-backed-dry-refactor.md`:
4
+ * Phase 3a of `.ai/specs/implemented/2026-04-27-ai-tools-api-backed-dry-refactor.md`:
5
5
  * `customers.list_companies` is now an API-backed wrapper over
6
6
  * `GET /api/customers/companies`. Tool name, schema, requiredFeatures, and
7
7
  * output shape are unchanged.
@@ -2,7 +2,7 @@
2
2
  * `customers.list_deals` + `customers.get_deal` (Phase 1 WS-C, Step 3.9).
3
3
  * `customers.update_deal_stage` mutation tool (Phase 3 WS-C, Step 5.13).
4
4
  *
5
- * Phase 3a of `.ai/specs/2026-04-27-ai-tools-api-backed-dry-refactor.md`:
5
+ * Phase 3a of `.ai/specs/implemented/2026-04-27-ai-tools-api-backed-dry-refactor.md`:
6
6
  * `customers.list_deals` is now an API-backed wrapper over
7
7
  * `GET /api/customers/deals`. Tool name, schema, requiredFeatures, and output
8
8
  * shape are unchanged.
@@ -5,7 +5,7 @@
5
5
  * the existing customers query engine + encryption helpers. Mutation tools
6
6
  * are deferred to Step 5.13+ under the pending-action contract.
7
7
  *
8
- * Phase 3a of `.ai/specs/2026-04-27-ai-tools-api-backed-dry-refactor.md`:
8
+ * Phase 3a of `.ai/specs/implemented/2026-04-27-ai-tools-api-backed-dry-refactor.md`:
9
9
  * `customers.list_people` is now an API-backed wrapper over
10
10
  * `GET /api/customers/people`. The `companyId` AI input has no inclusion
11
11
  * equivalent on the route (the route exposes `excludeLinkedCompanyId` only)
@@ -23,7 +23,7 @@ import {
23
23
  extractAllCustomFieldEntries,
24
24
  splitCustomFieldPayload,
25
25
  } from '@open-mercato/shared/lib/crud/custom-fields'
26
- import { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'
26
+ import { buildIlikeTerm } from '@open-mercato/shared/lib/db/buildIlikeTerm'
27
27
  import { parseBooleanToken } from '@open-mercato/shared/lib/boolean'
28
28
  import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
29
29
  import { consumeAdvancedFilterState, mergeAdvancedFilterTree } from '@open-mercato/shared/lib/crud/advanced-filter-integration'
@@ -178,7 +178,7 @@ const crud = makeCrudRoute({
178
178
  if (matchingIds !== null && matchingIds.length > 0) {
179
179
  applyEntityIdRestriction(filters, matchingIds)
180
180
  } else {
181
- const searchPattern = `%${escapeLikePattern(query.search)}%`
181
+ const searchPattern = buildIlikeTerm(query.search)
182
182
  filters.$or = [
183
183
  { display_name: { $ilike: searchPattern } },
184
184
  { primary_email: { $ilike: searchPattern } },
@@ -276,9 +276,9 @@ const crud = makeCrudRoute({
276
276
  if (email) {
277
277
  filters.primary_email = { $eq: email }
278
278
  } else if (emailStartsWith) {
279
- filters.primary_email = { $ilike: `${escapeLikePattern(emailStartsWith)}%` }
279
+ filters.primary_email = { $ilike: buildIlikeTerm(emailStartsWith, 'startsWith') }
280
280
  } else if (emailContains) {
281
- filters.primary_email = { $ilike: `%${escapeLikePattern(emailContains)}%` }
281
+ filters.primary_email = { $ilike: buildIlikeTerm(emailContains) }
282
282
  }
283
283
  const hasEmail = parseBooleanToken(query.hasEmail)
284
284
  if (!email && !emailStartsWith && !emailContains && hasEmail !== null) {
@@ -27,6 +27,7 @@ import { fetchStuckDealIds } from '../../lib/stuckDeals'
27
27
  const rawBodySchema = z.object({}).passthrough()
28
28
 
29
29
  const stringOrStringArray = z.union([z.string(), z.array(z.string())])
30
+ const OPEN_DEAL_STATUSES = ['open', 'in_progress'] as const
30
31
  const booleanQueryParam = z.preprocess((value) => {
31
32
  const parsed = parseBooleanFromUnknown(value)
32
33
  return parsed === null ? value : parsed
@@ -47,6 +48,7 @@ export const dealListQuerySchema = z
47
48
  expectedCloseAtTo: z.string().optional(),
48
49
  isStuck: booleanQueryParam,
49
50
  isOverdue: booleanQueryParam,
51
+ needsAttention: booleanQueryParam,
50
52
  valueCurrency: stringOrStringArray.optional(),
51
53
  sortField: z.string().optional(),
52
54
  sortDir: z.enum(['asc', 'desc']).optional(),
@@ -156,6 +158,43 @@ async function fetchDealIdsMatchingAssociations(
156
158
  return rows.map((row) => row.id)
157
159
  }
158
160
 
161
+ async function fetchNeedAttentionDealIds(
162
+ em: EntityManager,
163
+ organizationId: string,
164
+ tenantId: string,
165
+ ): Promise<string[]> {
166
+ const connection = em.getConnection()
167
+ const overdueRows = await connection.execute<Array<{ id: string }>>(
168
+ `SELECT id FROM customer_deals
169
+ WHERE organization_id = ?
170
+ AND tenant_id = ?
171
+ AND deleted_at IS NULL
172
+ AND status = 'open'
173
+ AND expected_close_at IS NOT NULL
174
+ AND expected_close_at < CURRENT_DATE`,
175
+ [organizationId, tenantId],
176
+ )
177
+
178
+ const attentionIds = new Set(overdueRows.map((row) => row.id))
179
+ const stuckIds = await fetchStuckDealIds(em, organizationId, tenantId)
180
+ if (stuckIds.length > 0) {
181
+ const idPlaceholders = stuckIds.map(() => '?').join(',')
182
+ const statusPlaceholders = OPEN_DEAL_STATUSES.map(() => '?').join(',')
183
+ const openStuckRows = await connection.execute<Array<{ id: string }>>(
184
+ `SELECT id FROM customer_deals
185
+ WHERE organization_id = ?
186
+ AND tenant_id = ?
187
+ AND deleted_at IS NULL
188
+ AND status IN (${statusPlaceholders})
189
+ AND id IN (${idPlaceholders})`,
190
+ [organizationId, tenantId, ...OPEN_DEAL_STATUSES, ...stuckIds],
191
+ )
192
+ for (const row of openStuckRows) attentionIds.add(row.id)
193
+ }
194
+
195
+ return Array.from(attentionIds)
196
+ }
197
+
159
198
  function normalizeCurrencyList(value: unknown): string[] {
160
199
  const set = new Set<string>()
161
200
  const visit = (entry: unknown) => {
@@ -302,7 +341,7 @@ export async function buildDealListFilters(query: DealListQuery, ctx?: import('@
302
341
  filters.expected_close_at = range
303
342
  }
304
343
 
305
- if (query.isOverdue) {
344
+ if (query.isOverdue && !query.needsAttention) {
306
345
  const today = new Date()
307
346
  today.setHours(0, 0, 0, 0)
308
347
  if (statusList.length === 0) {
@@ -316,7 +355,7 @@ export async function buildDealListFilters(query: DealListQuery, ctx?: import('@
316
355
  filters.expected_close_at = existingRange
317
356
  }
318
357
 
319
- if (query.isStuck && ctx) {
358
+ if (query.isStuck && !query.needsAttention && ctx) {
320
359
  const tenantId = ctx.auth?.tenantId
321
360
  // CrudCtx.auth carries `orgId` (not `organizationId`). The previous code referenced
322
361
  // `organizationId` which is always `undefined`, so the typeof check below silently
@@ -329,6 +368,16 @@ export async function buildDealListFilters(query: DealListQuery, ctx?: import('@
329
368
  }
330
369
  }
331
370
 
371
+ if (query.needsAttention && ctx) {
372
+ const tenantId = ctx.auth?.tenantId
373
+ const organizationId = ctx.auth?.orgId
374
+ if (typeof tenantId === 'string' && typeof organizationId === 'string') {
375
+ const em = ctx.container.resolve<EntityManager>('em')
376
+ const attentionIds = await fetchNeedAttentionDealIds(em, organizationId, tenantId)
377
+ intersectIds(attentionIds)
378
+ }
379
+ }
380
+
332
381
  // Pre-pagination association filter. Must run on the FULL dataset (before pagination),
333
382
  // otherwise matching deals on later pages disappear and `total` would be wrong. Read the
334
383
  // raw URL too so legacy `?personEntityId=` / `?companyEntityId=` keep working alongside the