@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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/customers/ai-tools/deals-pack.ts"],
4
- "sourcesContent": ["/**\n * `customers.list_deals` + `customers.get_deal` (Phase 1 WS-C, Step 3.9).\n * `customers.update_deal_stage` mutation tool (Phase 3 WS-C, Step 5.13).\n *\n * Phase 3a of `.ai/specs/2026-04-27-ai-tools-api-backed-dry-refactor.md`:\n * `customers.list_deals` is now an API-backed wrapper over\n * `GET /api/customers/deals`. Tool name, schema, requiredFeatures, and output\n * shape are unchanged.\n *\n * Phase 3c of the same spec migrates `customers.get_deal` to the documented\n * aggregate detail route. The handler issues 1 call without `includeRelated`\n * (`GET /customers/deals/<id>`) and 3 bounded calls with `includeRelated`\n * (deal detail + activities + comments by `dealId`). The 3-call cap matches\n * the spec's residual N+1 budget; deeper aggregation can earn a first-class\n * API later without touching the AI surface.\n */\nimport type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'\nimport { z } from 'zod'\nimport { defineApiBackedAiTool } from '@open-mercato/ai-assistant/modules/ai_assistant/lib/api-backed-tool'\nimport {\n createAiApiOperationRunner,\n type AiApiOperationRequest,\n type AiToolExecutionContext,\n} from '@open-mercato/ai-assistant/modules/ai_assistant/lib/ai-api-operation-runner'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n CustomerDeal,\n CustomerPipelineStage,\n} from '../data/entities'\nimport {\n assertTenantScope,\n type CustomersAiToolDefinition,\n type CustomersToolContext,\n type CustomersToolLoadBeforeSingleRecord,\n} from './types'\n\nfunction resolveEm(ctx: CustomersToolContext | AiToolExecutionContext): EntityManager {\n return ctx.container.resolve<EntityManager>('em')\n}\n\nfunction buildScope(ctx: CustomersToolContext | AiToolExecutionContext, tenantId: string) {\n return { tenantId, organizationId: ctx.organizationId }\n}\n\nconst listDealsInput = z\n .object({\n q: z.string().trim().optional().describe('Search text matched against deal title / description. Omit or leave empty to list all.'),\n limit: z.number().int().min(1).max(100).optional().describe('Maximum rows to return (default 50, max 100).'),\n offset: z.number().int().min(0).optional().describe('Number of rows to skip (default 0).'),\n personId: z.string().uuid().optional().describe('Return only deals linked to this person entity id.'),\n companyId: z.string().uuid().optional().describe('Return only deals linked to this company entity id.'),\n pipelineStageId: z.string().uuid().optional().describe('Return only deals at this pipeline stage.'),\n status: z.string().optional().describe('Filter by deal status (e.g. \"open\", \"won\", \"lost\").'),\n })\n .passthrough()\n\ntype ListDealsInput = z.infer<typeof listDealsInput>\n\ntype ListDealsApiItem = {\n id?: string\n title?: string | null\n description?: string | null\n status?: string | null\n pipeline_id?: string | null\n pipelineId?: string | null\n pipeline_stage_id?: string | null\n pipelineStageId?: string | null\n value_amount?: string | number | null\n valueAmount?: string | number | null\n value_currency?: string | null\n valueCurrency?: string | null\n probability?: number | null\n owner_user_id?: string | null\n ownerUserId?: string | null\n expected_close_at?: string | null\n expectedCloseAt?: string | null\n source?: string | null\n organization_id?: string | null\n organizationId?: string | null\n tenant_id?: string | null\n tenantId?: string | null\n created_at?: string | null\n createdAt?: string | null\n}\n\ntype ListDealsApiResponse = {\n items?: ListDealsApiItem[]\n total?: number\n}\n\ntype ListDealsOutput = {\n items: Array<Record<string, unknown>>\n total: number\n limit: number\n offset: number\n}\n\nconst listDealsTool = defineApiBackedAiTool<ListDealsInput, ListDealsApiResponse, ListDealsOutput>({\n name: 'customers.list_deals',\n displayName: 'List deals',\n description:\n 'Search / list deals for the caller tenant + organization. Optional filters include linked person / company / pipeline stage.',\n inputSchema: listDealsInput,\n requiredFeatures: ['customers.deals.view'],\n toOperation: (input, ctx) => {\n assertTenantScope(ctx as unknown as CustomersToolContext)\n const limit = input.limit ?? 50\n const offset = input.offset ?? 0\n const page = Math.floor(offset / limit) + 1\n\n const query: Record<string, string | number | boolean | null | undefined> = {\n page,\n pageSize: limit,\n }\n if (input.q?.trim()) query.search = input.q.trim()\n if (input.personId) query.personId = input.personId\n if (input.companyId) query.companyId = input.companyId\n if (input.pipelineStageId) query.pipelineStageId = input.pipelineStageId\n if (input.status) query.status = input.status\n\n const operation: AiApiOperationRequest = {\n method: 'GET',\n path: '/customers/deals',\n query,\n }\n return operation\n },\n mapResponse: (response, input) => {\n const limit = input.limit ?? 50\n const offset = input.offset ?? 0\n const data = (response.data ?? {}) as ListDealsApiResponse\n const rawItems: ListDealsApiItem[] = Array.isArray(data.items) ? data.items : []\n return {\n items: rawItems.map((row) => {\n const expectedCloseRaw = row.expected_close_at ?? row.expectedCloseAt ?? null\n const expectedCloseAt = expectedCloseRaw ? new Date(String(expectedCloseRaw)).toISOString() : null\n const createdAtRaw = row.created_at ?? row.createdAt ?? null\n const createdAt = createdAtRaw ? new Date(String(createdAtRaw)).toISOString() : null\n return {\n id: row.id,\n title: row.title ?? null,\n description: row.description ?? null,\n status: row.status ?? null,\n pipelineId: row.pipeline_id ?? row.pipelineId ?? null,\n pipelineStageId: row.pipeline_stage_id ?? row.pipelineStageId ?? null,\n valueAmount: row.value_amount ?? row.valueAmount ?? null,\n valueCurrency: row.value_currency ?? row.valueCurrency ?? null,\n probability: row.probability ?? null,\n ownerUserId: row.owner_user_id ?? row.ownerUserId ?? null,\n expectedCloseAt,\n source: row.source ?? null,\n organizationId: row.organization_id ?? row.organizationId ?? null,\n tenantId: row.tenant_id ?? row.tenantId ?? null,\n createdAt,\n }\n }),\n total: typeof data.total === 'number' ? data.total : 0,\n limit,\n offset,\n }\n },\n}) as unknown as CustomersAiToolDefinition\n\nconst getDealInput = z.object({\n dealId: z.string().uuid().describe('Deal id (UUID).'),\n includeRelated: z\n .boolean()\n .optional()\n .describe('When true, include notes, activities, linked people and companies (each capped at 100).'),\n})\n\ntype GetDealInput = z.infer<typeof getDealInput>\n\nfunction toIsoDeal(value: unknown): string | null {\n if (!value) return null\n const dt = value instanceof Date ? value : new Date(String(value))\n if (Number.isNaN(dt.getTime())) return null\n return dt.toISOString()\n}\n\nconst getDealTool: CustomersAiToolDefinition = {\n name: 'customers.get_deal',\n displayName: 'Get deal',\n description:\n 'Fetch a deal by id with fields and (optionally) notes, activities, linked people, and linked companies. Returns { found: false } when outside tenant/org scope.',\n inputSchema: getDealInput,\n requiredFeatures: ['customers.deals.view'],\n tags: ['read', 'customers'],\n handler: async (rawInput, ctx) => {\n const { tenantId: _tenantId } = assertTenantScope(ctx)\n void _tenantId\n const input: GetDealInput = getDealInput.parse(rawInput)\n const includeRelated = !!input.includeRelated\n const runner = createAiApiOperationRunner(ctx as unknown as AiToolExecutionContext)\n\n const detailResponse = await runner.run<Record<string, unknown>>({\n method: 'GET',\n path: `/customers/deals/${input.dealId}`,\n })\n if (!detailResponse.success) {\n if (detailResponse.statusCode === 404 || detailResponse.statusCode === 403) {\n return { found: false as const, dealId: input.dealId }\n }\n throw new Error(detailResponse.error ?? `Failed to fetch deal ${input.dealId}`)\n }\n const detail = (detailResponse.data ?? {}) as Record<string, unknown>\n const dealRow = (detail.deal ?? null) as Record<string, unknown> | null\n if (!dealRow) {\n return { found: false as const, dealId: input.dealId }\n }\n const customFields = (detail.customFields ?? {}) as Record<string, unknown>\n const peopleRows = Array.isArray(detail.people) ? (detail.people as Array<Record<string, unknown>>) : []\n const companiesRows = Array.isArray(detail.companies)\n ? (detail.companies as Array<Record<string, unknown>>)\n : []\n\n let related: Record<string, unknown> | null = null\n if (includeRelated) {\n const [activitiesResponse, commentsResponse] = await Promise.all([\n runner.run<{ items?: Array<Record<string, unknown>>; total?: number }>({\n method: 'GET',\n path: '/customers/activities',\n query: { dealId: input.dealId, page: 1, pageSize: 100, sortField: 'occurredAt', sortDir: 'desc' },\n }),\n runner.run<{ items?: Array<Record<string, unknown>>; total?: number }>({\n method: 'GET',\n path: '/customers/comments',\n query: { dealId: input.dealId, page: 1, pageSize: 100 },\n }),\n ])\n const activities =\n activitiesResponse.success && Array.isArray(activitiesResponse.data?.items)\n ? (activitiesResponse.data!.items as Array<Record<string, unknown>>)\n : []\n const comments =\n commentsResponse.success && Array.isArray(commentsResponse.data?.items)\n ? (commentsResponse.data!.items as Array<Record<string, unknown>>)\n : []\n\n related = {\n activities: activities.map((activity) => ({\n id: activity.id,\n activityType: activity.activityType ?? activity.activity_type ?? null,\n subject: activity.subject ?? null,\n body: activity.body ?? null,\n occurredAt: toIsoDeal(activity.occurredAt ?? activity.occurred_at),\n createdAt: toIsoDeal(activity.createdAt ?? activity.created_at),\n })),\n notes: comments.map((comment) => ({\n id: comment.id,\n body: comment.body,\n authorUserId: comment.authorUserId ?? comment.author_user_id ?? null,\n createdAt: toIsoDeal(comment.createdAt ?? comment.created_at),\n })),\n people: peopleRows\n .map((person) => {\n if (!person || typeof person !== 'object') return null\n const id = typeof person.id === 'string' ? person.id : null\n if (!id) return null\n const subtitle = typeof person.subtitle === 'string' ? person.subtitle : null\n const label = typeof person.label === 'string' ? person.label : ''\n const entry: {\n id: string\n displayName: string\n primaryEmail: string | null\n primaryPhone: string | null\n participantRole: string | null\n } = {\n id,\n displayName: label,\n primaryEmail: subtitle && subtitle.includes('@') ? subtitle : null,\n primaryPhone: subtitle && !subtitle.includes('@') ? subtitle : null,\n participantRole: null as string | null,\n }\n return entry\n })\n .filter(\n (value): value is {\n id: string\n displayName: string\n primaryEmail: string | null\n primaryPhone: string | null\n participantRole: string | null\n } => value !== null,\n ),\n companies: companiesRows\n .map((company) => {\n if (!company || typeof company !== 'object') return null\n const id = typeof company.id === 'string' ? company.id : null\n if (!id) return null\n const label = typeof company.label === 'string' ? company.label : ''\n const entry: {\n id: string\n displayName: string\n primaryEmail: string | null\n primaryPhone: string | null\n } = {\n id,\n displayName: label,\n primaryEmail: null as string | null,\n primaryPhone: null as string | null,\n }\n return entry\n })\n .filter(\n (value): value is {\n id: string\n displayName: string\n primaryEmail: string | null\n primaryPhone: string | null\n } => value !== null,\n ),\n }\n }\n\n return {\n found: true as const,\n deal: {\n id: dealRow.id,\n title: typeof dealRow.title === 'string' ? dealRow.title : '',\n description: dealRow.description ?? null,\n status: dealRow.status ?? null,\n pipelineId: dealRow.pipelineId ?? null,\n pipelineStageId: dealRow.pipelineStageId ?? null,\n valueAmount: dealRow.valueAmount ?? null,\n valueCurrency: dealRow.valueCurrency ?? null,\n probability: dealRow.probability ?? null,\n ownerUserId: dealRow.ownerUserId ?? null,\n expectedCloseAt: toIsoDeal(dealRow.expectedCloseAt),\n source: dealRow.source ?? null,\n organizationId: dealRow.organizationId ?? null,\n tenantId: dealRow.tenantId ?? null,\n createdAt: toIsoDeal(dealRow.createdAt),\n updatedAt: toIsoDeal(dealRow.updatedAt),\n },\n customFields,\n related,\n }\n },\n}\n\n/**\n * Mutation tool: move a deal to a different pipeline stage. Step 5.13 \u2014 first\n * mutation-capable flow on the pending-action contract.\n *\n * Accepts either `toPipelineStageId` (UUID \u2014 preferred, tenant-scoped stage\n * record) or `toStage` (free-form string that maps to `CustomerDeal.status`\n * for pipeline roots like `open`/`won`/`lost`). Exactly one must be provided.\n *\n * The handler delegates to the existing `customers.deals.update` command so\n * all side effects (audit log, `customers.deal.updated` event, query index\n * refresh, notifications) stay identical to a direct API write.\n */\n// LLMs frequently emit `\"\"` for \"not provided\" \u2014 coerce blanks (and surrounding\n// whitespace) to `undefined` BEFORE the per-field validators run so the\n// `.uuid()` check on `toPipelineStageId` does not blow up on an empty string\n// the caller actually meant as \"skip this field\".\nconst blankToUndefined = (value: unknown): unknown => {\n if (typeof value !== 'string') return value\n const trimmed = value.trim()\n return trimmed.length === 0 ? undefined : trimmed\n}\n\nconst updateDealStageInput = z\n .object({\n dealId: z.string().uuid().describe('Deal id (UUID) to update.'),\n toPipelineStageId: z\n .preprocess(blankToUndefined, z.string().uuid().optional())\n .describe('Target pipeline stage id (UUID). Preferred \u2014 tenant-scoped stage record.'),\n toStage: z\n .preprocess(blankToUndefined, z.string().min(1).max(50).optional())\n .describe(\n 'Target status slug (e.g. \"open\", \"won\", \"lost\"). Used when the deal does not belong to a managed pipeline.',\n ),\n })\n .refine(\n (value) => Boolean(value.toPipelineStageId) !== Boolean(value.toStage),\n {\n message: 'Provide exactly one of toPipelineStageId or toStage.',\n path: ['toPipelineStageId'],\n },\n )\n\ntype UpdateDealStageInput = z.infer<typeof updateDealStageInput>\n\nfunction recordVersionFromUpdatedAt(updatedAt: Date | null | undefined): string | null {\n if (!updatedAt) return null\n const value = updatedAt instanceof Date ? updatedAt : new Date(updatedAt)\n if (Number.isNaN(value.getTime())) return null\n return value.toISOString()\n}\n\nfunction titleStatus(value: string | null | undefined): string | undefined {\n if (!value) return undefined\n return value\n .split(/[_\\s-]+/)\n .filter(Boolean)\n .map((part) => part.charAt(0).toUpperCase() + part.slice(1))\n .join(' ')\n}\n\nasync function loadDealWithStage(\n em: EntityManager,\n ctx: CustomersToolContext,\n tenantId: string,\n dealId: string,\n): Promise<CustomerDeal | null> {\n const where: Record<string, unknown> = { id: dealId, tenantId, deletedAt: null }\n if (ctx.organizationId) where.organizationId = ctx.organizationId\n const deal = await findOneWithDecryption<CustomerDeal>(\n em,\n CustomerDeal,\n where as FilterQuery<CustomerDeal>,\n undefined,\n buildScope(ctx, tenantId),\n )\n if (!deal || deal.tenantId !== tenantId) return null\n if (ctx.organizationId && deal.organizationId !== ctx.organizationId) return null\n return deal\n}\n\nasync function loadPipelineStage(\n em: EntityManager,\n ctx: CustomersToolContext,\n tenantId: string,\n stageId: string,\n organizationId: string,\n): Promise<CustomerPipelineStage | null> {\n return findOneWithDecryption<CustomerPipelineStage>(\n em,\n CustomerPipelineStage,\n {\n id: stageId,\n tenantId,\n organizationId,\n },\n undefined,\n buildScope(ctx, tenantId),\n )\n}\n\nconst updateDealStageTool: CustomersAiToolDefinition = {\n name: 'customers.update_deal_stage',\n displayName: 'Update deal stage',\n description:\n 'Move a deal to a different pipeline stage (by stage id) or change its top-level status (e.g. \"open\", \"won\", \"lost\"). Mutation tool \u2014 flows through the AI pending-action approval gate.',\n inputSchema: updateDealStageInput as z.ZodType<unknown>,\n requiredFeatures: ['customers.deals.manage'],\n tags: ['write', 'customers'],\n isMutation: true,\n loadBeforeRecord: async (rawInput, ctx): Promise<CustomersToolLoadBeforeSingleRecord | null> => {\n const { tenantId } = assertTenantScope(ctx)\n const input: UpdateDealStageInput = updateDealStageInput.parse(rawInput)\n const em = resolveEm(ctx)\n const deal = await loadDealWithStage(em, ctx, tenantId, input.dealId)\n if (!deal) return null\n let afterStatus = deal.status ?? null\n let afterPipelineStageId = deal.pipelineStageId ?? null\n let afterPipelineStageLabel = deal.pipelineStage ?? null\n if (input.toPipelineStageId) {\n const organizationId = deal.organizationId ?? ctx.organizationId ?? null\n const stage = organizationId\n ? await loadPipelineStage(em, ctx, tenantId, input.toPipelineStageId, organizationId)\n : null\n afterPipelineStageId = input.toPipelineStageId\n afterPipelineStageLabel = stage?.label ?? input.toPipelineStageId\n } else if (input.toStage) {\n afterStatus = input.toStage\n }\n const beforeStatus = deal.status ?? null\n const beforePipelineStageId = deal.pipelineStageId ?? null\n const beforePipelineStageLabel = deal.pipelineStage ?? beforePipelineStageId\n return {\n recordId: deal.id,\n entityType: 'customers.deal',\n recordVersion: recordVersionFromUpdatedAt(deal.updatedAt),\n before: {\n status: beforeStatus,\n pipelineStageId: beforePipelineStageId,\n },\n after: {\n status: afterStatus,\n pipelineStageId: afterPipelineStageId,\n },\n display: {\n fieldLabels: {\n status: 'Status',\n pipelineStageId: 'Pipeline stage',\n },\n before: {\n ...(beforeStatus ? { status: titleStatus(beforeStatus) } : {}),\n ...(beforePipelineStageLabel ? { pipelineStageId: beforePipelineStageLabel } : {}),\n },\n after: {\n ...(afterStatus ? { status: titleStatus(afterStatus) } : {}),\n ...(afterPipelineStageLabel ? { pipelineStageId: afterPipelineStageLabel } : {}),\n },\n },\n }\n },\n handler: async (rawInput, ctx) => {\n const { tenantId } = assertTenantScope(ctx)\n const input: UpdateDealStageInput = updateDealStageInput.parse(rawInput)\n const em = resolveEm(ctx)\n const deal = await loadDealWithStage(em, ctx, tenantId, input.dealId)\n if (!deal) {\n throw new Error(`Deal \"${input.dealId}\" is not accessible to the caller.`)\n }\n const organizationId = deal.organizationId\n if (!organizationId) {\n throw new Error(`Deal \"${input.dealId}\" has no organization scope.`)\n }\n\n const before = {\n status: deal.status ?? null,\n pipelineStage: deal.pipelineStage ?? null,\n pipelineStageId: deal.pipelineStageId ?? null,\n }\n\n const body: Record<string, unknown> = {\n id: deal.id,\n tenantId,\n organizationId,\n }\n if (input.toPipelineStageId) {\n const stage = await loadPipelineStage(em, ctx, tenantId, input.toPipelineStageId, organizationId)\n if (!stage) {\n throw new Error('Pipeline stage not found.')\n }\n body.pipelineStageId = input.toPipelineStageId\n } else if (input.toStage) {\n body.status = input.toStage\n }\n\n const runner = createAiApiOperationRunner(ctx as unknown as AiToolExecutionContext)\n const response = await runner.run({\n method: 'PUT',\n path: '/customers/deals',\n body,\n })\n if (!response.success) {\n throw new Error(response.error ?? `Failed to update deal \"${deal.id}\"`)\n }\n\n const after = await loadDealWithStage(em, ctx, tenantId, deal.id)\n return {\n recordId: deal.id,\n commandName: 'customers.deals.update',\n before,\n after: after\n ? {\n status: after.status ?? null,\n pipelineStage: after.pipelineStage ?? null,\n pipelineStageId: after.pipelineStageId ?? null,\n }\n : null,\n }\n },\n}\n\nexport const dealsAiTools: CustomersAiToolDefinition[] = [listDealsTool, getDealTool, updateDealStageTool]\n\nexport default dealsAiTools\n"],
4
+ "sourcesContent": ["/**\n * `customers.list_deals` + `customers.get_deal` (Phase 1 WS-C, Step 3.9).\n * `customers.update_deal_stage` mutation tool (Phase 3 WS-C, Step 5.13).\n *\n * Phase 3a of `.ai/specs/implemented/2026-04-27-ai-tools-api-backed-dry-refactor.md`:\n * `customers.list_deals` is now an API-backed wrapper over\n * `GET /api/customers/deals`. Tool name, schema, requiredFeatures, and output\n * shape are unchanged.\n *\n * Phase 3c of the same spec migrates `customers.get_deal` to the documented\n * aggregate detail route. The handler issues 1 call without `includeRelated`\n * (`GET /customers/deals/<id>`) and 3 bounded calls with `includeRelated`\n * (deal detail + activities + comments by `dealId`). The 3-call cap matches\n * the spec's residual N+1 budget; deeper aggregation can earn a first-class\n * API later without touching the AI surface.\n */\nimport type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'\nimport { z } from 'zod'\nimport { defineApiBackedAiTool } from '@open-mercato/ai-assistant/modules/ai_assistant/lib/api-backed-tool'\nimport {\n createAiApiOperationRunner,\n type AiApiOperationRequest,\n type AiToolExecutionContext,\n} from '@open-mercato/ai-assistant/modules/ai_assistant/lib/ai-api-operation-runner'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n CustomerDeal,\n CustomerPipelineStage,\n} from '../data/entities'\nimport {\n assertTenantScope,\n type CustomersAiToolDefinition,\n type CustomersToolContext,\n type CustomersToolLoadBeforeSingleRecord,\n} from './types'\n\nfunction resolveEm(ctx: CustomersToolContext | AiToolExecutionContext): EntityManager {\n return ctx.container.resolve<EntityManager>('em')\n}\n\nfunction buildScope(ctx: CustomersToolContext | AiToolExecutionContext, tenantId: string) {\n return { tenantId, organizationId: ctx.organizationId }\n}\n\nconst listDealsInput = z\n .object({\n q: z.string().trim().optional().describe('Search text matched against deal title / description. Omit or leave empty to list all.'),\n limit: z.number().int().min(1).max(100).optional().describe('Maximum rows to return (default 50, max 100).'),\n offset: z.number().int().min(0).optional().describe('Number of rows to skip (default 0).'),\n personId: z.string().uuid().optional().describe('Return only deals linked to this person entity id.'),\n companyId: z.string().uuid().optional().describe('Return only deals linked to this company entity id.'),\n pipelineStageId: z.string().uuid().optional().describe('Return only deals at this pipeline stage.'),\n status: z.string().optional().describe('Filter by deal status (e.g. \"open\", \"won\", \"lost\").'),\n })\n .passthrough()\n\ntype ListDealsInput = z.infer<typeof listDealsInput>\n\ntype ListDealsApiItem = {\n id?: string\n title?: string | null\n description?: string | null\n status?: string | null\n pipeline_id?: string | null\n pipelineId?: string | null\n pipeline_stage_id?: string | null\n pipelineStageId?: string | null\n value_amount?: string | number | null\n valueAmount?: string | number | null\n value_currency?: string | null\n valueCurrency?: string | null\n probability?: number | null\n owner_user_id?: string | null\n ownerUserId?: string | null\n expected_close_at?: string | null\n expectedCloseAt?: string | null\n source?: string | null\n organization_id?: string | null\n organizationId?: string | null\n tenant_id?: string | null\n tenantId?: string | null\n created_at?: string | null\n createdAt?: string | null\n}\n\ntype ListDealsApiResponse = {\n items?: ListDealsApiItem[]\n total?: number\n}\n\ntype ListDealsOutput = {\n items: Array<Record<string, unknown>>\n total: number\n limit: number\n offset: number\n}\n\nconst listDealsTool = defineApiBackedAiTool<ListDealsInput, ListDealsApiResponse, ListDealsOutput>({\n name: 'customers.list_deals',\n displayName: 'List deals',\n description:\n 'Search / list deals for the caller tenant + organization. Optional filters include linked person / company / pipeline stage.',\n inputSchema: listDealsInput,\n requiredFeatures: ['customers.deals.view'],\n toOperation: (input, ctx) => {\n assertTenantScope(ctx as unknown as CustomersToolContext)\n const limit = input.limit ?? 50\n const offset = input.offset ?? 0\n const page = Math.floor(offset / limit) + 1\n\n const query: Record<string, string | number | boolean | null | undefined> = {\n page,\n pageSize: limit,\n }\n if (input.q?.trim()) query.search = input.q.trim()\n if (input.personId) query.personId = input.personId\n if (input.companyId) query.companyId = input.companyId\n if (input.pipelineStageId) query.pipelineStageId = input.pipelineStageId\n if (input.status) query.status = input.status\n\n const operation: AiApiOperationRequest = {\n method: 'GET',\n path: '/customers/deals',\n query,\n }\n return operation\n },\n mapResponse: (response, input) => {\n const limit = input.limit ?? 50\n const offset = input.offset ?? 0\n const data = (response.data ?? {}) as ListDealsApiResponse\n const rawItems: ListDealsApiItem[] = Array.isArray(data.items) ? data.items : []\n return {\n items: rawItems.map((row) => {\n const expectedCloseRaw = row.expected_close_at ?? row.expectedCloseAt ?? null\n const expectedCloseAt = expectedCloseRaw ? new Date(String(expectedCloseRaw)).toISOString() : null\n const createdAtRaw = row.created_at ?? row.createdAt ?? null\n const createdAt = createdAtRaw ? new Date(String(createdAtRaw)).toISOString() : null\n return {\n id: row.id,\n title: row.title ?? null,\n description: row.description ?? null,\n status: row.status ?? null,\n pipelineId: row.pipeline_id ?? row.pipelineId ?? null,\n pipelineStageId: row.pipeline_stage_id ?? row.pipelineStageId ?? null,\n valueAmount: row.value_amount ?? row.valueAmount ?? null,\n valueCurrency: row.value_currency ?? row.valueCurrency ?? null,\n probability: row.probability ?? null,\n ownerUserId: row.owner_user_id ?? row.ownerUserId ?? null,\n expectedCloseAt,\n source: row.source ?? null,\n organizationId: row.organization_id ?? row.organizationId ?? null,\n tenantId: row.tenant_id ?? row.tenantId ?? null,\n createdAt,\n }\n }),\n total: typeof data.total === 'number' ? data.total : 0,\n limit,\n offset,\n }\n },\n}) as unknown as CustomersAiToolDefinition\n\nconst getDealInput = z.object({\n dealId: z.string().uuid().describe('Deal id (UUID).'),\n includeRelated: z\n .boolean()\n .optional()\n .describe('When true, include notes, activities, linked people and companies (each capped at 100).'),\n})\n\ntype GetDealInput = z.infer<typeof getDealInput>\n\nfunction toIsoDeal(value: unknown): string | null {\n if (!value) return null\n const dt = value instanceof Date ? value : new Date(String(value))\n if (Number.isNaN(dt.getTime())) return null\n return dt.toISOString()\n}\n\nconst getDealTool: CustomersAiToolDefinition = {\n name: 'customers.get_deal',\n displayName: 'Get deal',\n description:\n 'Fetch a deal by id with fields and (optionally) notes, activities, linked people, and linked companies. Returns { found: false } when outside tenant/org scope.',\n inputSchema: getDealInput,\n requiredFeatures: ['customers.deals.view'],\n tags: ['read', 'customers'],\n handler: async (rawInput, ctx) => {\n const { tenantId: _tenantId } = assertTenantScope(ctx)\n void _tenantId\n const input: GetDealInput = getDealInput.parse(rawInput)\n const includeRelated = !!input.includeRelated\n const runner = createAiApiOperationRunner(ctx as unknown as AiToolExecutionContext)\n\n const detailResponse = await runner.run<Record<string, unknown>>({\n method: 'GET',\n path: `/customers/deals/${input.dealId}`,\n })\n if (!detailResponse.success) {\n if (detailResponse.statusCode === 404 || detailResponse.statusCode === 403) {\n return { found: false as const, dealId: input.dealId }\n }\n throw new Error(detailResponse.error ?? `Failed to fetch deal ${input.dealId}`)\n }\n const detail = (detailResponse.data ?? {}) as Record<string, unknown>\n const dealRow = (detail.deal ?? null) as Record<string, unknown> | null\n if (!dealRow) {\n return { found: false as const, dealId: input.dealId }\n }\n const customFields = (detail.customFields ?? {}) as Record<string, unknown>\n const peopleRows = Array.isArray(detail.people) ? (detail.people as Array<Record<string, unknown>>) : []\n const companiesRows = Array.isArray(detail.companies)\n ? (detail.companies as Array<Record<string, unknown>>)\n : []\n\n let related: Record<string, unknown> | null = null\n if (includeRelated) {\n const [activitiesResponse, commentsResponse] = await Promise.all([\n runner.run<{ items?: Array<Record<string, unknown>>; total?: number }>({\n method: 'GET',\n path: '/customers/activities',\n query: { dealId: input.dealId, page: 1, pageSize: 100, sortField: 'occurredAt', sortDir: 'desc' },\n }),\n runner.run<{ items?: Array<Record<string, unknown>>; total?: number }>({\n method: 'GET',\n path: '/customers/comments',\n query: { dealId: input.dealId, page: 1, pageSize: 100 },\n }),\n ])\n const activities =\n activitiesResponse.success && Array.isArray(activitiesResponse.data?.items)\n ? (activitiesResponse.data!.items as Array<Record<string, unknown>>)\n : []\n const comments =\n commentsResponse.success && Array.isArray(commentsResponse.data?.items)\n ? (commentsResponse.data!.items as Array<Record<string, unknown>>)\n : []\n\n related = {\n activities: activities.map((activity) => ({\n id: activity.id,\n activityType: activity.activityType ?? activity.activity_type ?? null,\n subject: activity.subject ?? null,\n body: activity.body ?? null,\n occurredAt: toIsoDeal(activity.occurredAt ?? activity.occurred_at),\n createdAt: toIsoDeal(activity.createdAt ?? activity.created_at),\n })),\n notes: comments.map((comment) => ({\n id: comment.id,\n body: comment.body,\n authorUserId: comment.authorUserId ?? comment.author_user_id ?? null,\n createdAt: toIsoDeal(comment.createdAt ?? comment.created_at),\n })),\n people: peopleRows\n .map((person) => {\n if (!person || typeof person !== 'object') return null\n const id = typeof person.id === 'string' ? person.id : null\n if (!id) return null\n const subtitle = typeof person.subtitle === 'string' ? person.subtitle : null\n const label = typeof person.label === 'string' ? person.label : ''\n const entry: {\n id: string\n displayName: string\n primaryEmail: string | null\n primaryPhone: string | null\n participantRole: string | null\n } = {\n id,\n displayName: label,\n primaryEmail: subtitle && subtitle.includes('@') ? subtitle : null,\n primaryPhone: subtitle && !subtitle.includes('@') ? subtitle : null,\n participantRole: null as string | null,\n }\n return entry\n })\n .filter(\n (value): value is {\n id: string\n displayName: string\n primaryEmail: string | null\n primaryPhone: string | null\n participantRole: string | null\n } => value !== null,\n ),\n companies: companiesRows\n .map((company) => {\n if (!company || typeof company !== 'object') return null\n const id = typeof company.id === 'string' ? company.id : null\n if (!id) return null\n const label = typeof company.label === 'string' ? company.label : ''\n const entry: {\n id: string\n displayName: string\n primaryEmail: string | null\n primaryPhone: string | null\n } = {\n id,\n displayName: label,\n primaryEmail: null as string | null,\n primaryPhone: null as string | null,\n }\n return entry\n })\n .filter(\n (value): value is {\n id: string\n displayName: string\n primaryEmail: string | null\n primaryPhone: string | null\n } => value !== null,\n ),\n }\n }\n\n return {\n found: true as const,\n deal: {\n id: dealRow.id,\n title: typeof dealRow.title === 'string' ? dealRow.title : '',\n description: dealRow.description ?? null,\n status: dealRow.status ?? null,\n pipelineId: dealRow.pipelineId ?? null,\n pipelineStageId: dealRow.pipelineStageId ?? null,\n valueAmount: dealRow.valueAmount ?? null,\n valueCurrency: dealRow.valueCurrency ?? null,\n probability: dealRow.probability ?? null,\n ownerUserId: dealRow.ownerUserId ?? null,\n expectedCloseAt: toIsoDeal(dealRow.expectedCloseAt),\n source: dealRow.source ?? null,\n organizationId: dealRow.organizationId ?? null,\n tenantId: dealRow.tenantId ?? null,\n createdAt: toIsoDeal(dealRow.createdAt),\n updatedAt: toIsoDeal(dealRow.updatedAt),\n },\n customFields,\n related,\n }\n },\n}\n\n/**\n * Mutation tool: move a deal to a different pipeline stage. Step 5.13 \u2014 first\n * mutation-capable flow on the pending-action contract.\n *\n * Accepts either `toPipelineStageId` (UUID \u2014 preferred, tenant-scoped stage\n * record) or `toStage` (free-form string that maps to `CustomerDeal.status`\n * for pipeline roots like `open`/`won`/`lost`). Exactly one must be provided.\n *\n * The handler delegates to the existing `customers.deals.update` command so\n * all side effects (audit log, `customers.deal.updated` event, query index\n * refresh, notifications) stay identical to a direct API write.\n */\n// LLMs frequently emit `\"\"` for \"not provided\" \u2014 coerce blanks (and surrounding\n// whitespace) to `undefined` BEFORE the per-field validators run so the\n// `.uuid()` check on `toPipelineStageId` does not blow up on an empty string\n// the caller actually meant as \"skip this field\".\nconst blankToUndefined = (value: unknown): unknown => {\n if (typeof value !== 'string') return value\n const trimmed = value.trim()\n return trimmed.length === 0 ? undefined : trimmed\n}\n\nconst updateDealStageInput = z\n .object({\n dealId: z.string().uuid().describe('Deal id (UUID) to update.'),\n toPipelineStageId: z\n .preprocess(blankToUndefined, z.string().uuid().optional())\n .describe('Target pipeline stage id (UUID). Preferred \u2014 tenant-scoped stage record.'),\n toStage: z\n .preprocess(blankToUndefined, z.string().min(1).max(50).optional())\n .describe(\n 'Target status slug (e.g. \"open\", \"won\", \"lost\"). Used when the deal does not belong to a managed pipeline.',\n ),\n })\n .refine(\n (value) => Boolean(value.toPipelineStageId) !== Boolean(value.toStage),\n {\n message: 'Provide exactly one of toPipelineStageId or toStage.',\n path: ['toPipelineStageId'],\n },\n )\n\ntype UpdateDealStageInput = z.infer<typeof updateDealStageInput>\n\nfunction recordVersionFromUpdatedAt(updatedAt: Date | null | undefined): string | null {\n if (!updatedAt) return null\n const value = updatedAt instanceof Date ? updatedAt : new Date(updatedAt)\n if (Number.isNaN(value.getTime())) return null\n return value.toISOString()\n}\n\nfunction titleStatus(value: string | null | undefined): string | undefined {\n if (!value) return undefined\n return value\n .split(/[_\\s-]+/)\n .filter(Boolean)\n .map((part) => part.charAt(0).toUpperCase() + part.slice(1))\n .join(' ')\n}\n\nasync function loadDealWithStage(\n em: EntityManager,\n ctx: CustomersToolContext,\n tenantId: string,\n dealId: string,\n): Promise<CustomerDeal | null> {\n const where: Record<string, unknown> = { id: dealId, tenantId, deletedAt: null }\n if (ctx.organizationId) where.organizationId = ctx.organizationId\n const deal = await findOneWithDecryption<CustomerDeal>(\n em,\n CustomerDeal,\n where as FilterQuery<CustomerDeal>,\n undefined,\n buildScope(ctx, tenantId),\n )\n if (!deal || deal.tenantId !== tenantId) return null\n if (ctx.organizationId && deal.organizationId !== ctx.organizationId) return null\n return deal\n}\n\nasync function loadPipelineStage(\n em: EntityManager,\n ctx: CustomersToolContext,\n tenantId: string,\n stageId: string,\n organizationId: string,\n): Promise<CustomerPipelineStage | null> {\n return findOneWithDecryption<CustomerPipelineStage>(\n em,\n CustomerPipelineStage,\n {\n id: stageId,\n tenantId,\n organizationId,\n },\n undefined,\n buildScope(ctx, tenantId),\n )\n}\n\nconst updateDealStageTool: CustomersAiToolDefinition = {\n name: 'customers.update_deal_stage',\n displayName: 'Update deal stage',\n description:\n 'Move a deal to a different pipeline stage (by stage id) or change its top-level status (e.g. \"open\", \"won\", \"lost\"). Mutation tool \u2014 flows through the AI pending-action approval gate.',\n inputSchema: updateDealStageInput as z.ZodType<unknown>,\n requiredFeatures: ['customers.deals.manage'],\n tags: ['write', 'customers'],\n isMutation: true,\n loadBeforeRecord: async (rawInput, ctx): Promise<CustomersToolLoadBeforeSingleRecord | null> => {\n const { tenantId } = assertTenantScope(ctx)\n const input: UpdateDealStageInput = updateDealStageInput.parse(rawInput)\n const em = resolveEm(ctx)\n const deal = await loadDealWithStage(em, ctx, tenantId, input.dealId)\n if (!deal) return null\n let afterStatus = deal.status ?? null\n let afterPipelineStageId = deal.pipelineStageId ?? null\n let afterPipelineStageLabel = deal.pipelineStage ?? null\n if (input.toPipelineStageId) {\n const organizationId = deal.organizationId ?? ctx.organizationId ?? null\n const stage = organizationId\n ? await loadPipelineStage(em, ctx, tenantId, input.toPipelineStageId, organizationId)\n : null\n afterPipelineStageId = input.toPipelineStageId\n afterPipelineStageLabel = stage?.label ?? input.toPipelineStageId\n } else if (input.toStage) {\n afterStatus = input.toStage\n }\n const beforeStatus = deal.status ?? null\n const beforePipelineStageId = deal.pipelineStageId ?? null\n const beforePipelineStageLabel = deal.pipelineStage ?? beforePipelineStageId\n return {\n recordId: deal.id,\n entityType: 'customers.deal',\n recordVersion: recordVersionFromUpdatedAt(deal.updatedAt),\n before: {\n status: beforeStatus,\n pipelineStageId: beforePipelineStageId,\n },\n after: {\n status: afterStatus,\n pipelineStageId: afterPipelineStageId,\n },\n display: {\n fieldLabels: {\n status: 'Status',\n pipelineStageId: 'Pipeline stage',\n },\n before: {\n ...(beforeStatus ? { status: titleStatus(beforeStatus) } : {}),\n ...(beforePipelineStageLabel ? { pipelineStageId: beforePipelineStageLabel } : {}),\n },\n after: {\n ...(afterStatus ? { status: titleStatus(afterStatus) } : {}),\n ...(afterPipelineStageLabel ? { pipelineStageId: afterPipelineStageLabel } : {}),\n },\n },\n }\n },\n handler: async (rawInput, ctx) => {\n const { tenantId } = assertTenantScope(ctx)\n const input: UpdateDealStageInput = updateDealStageInput.parse(rawInput)\n const em = resolveEm(ctx)\n const deal = await loadDealWithStage(em, ctx, tenantId, input.dealId)\n if (!deal) {\n throw new Error(`Deal \"${input.dealId}\" is not accessible to the caller.`)\n }\n const organizationId = deal.organizationId\n if (!organizationId) {\n throw new Error(`Deal \"${input.dealId}\" has no organization scope.`)\n }\n\n const before = {\n status: deal.status ?? null,\n pipelineStage: deal.pipelineStage ?? null,\n pipelineStageId: deal.pipelineStageId ?? null,\n }\n\n const body: Record<string, unknown> = {\n id: deal.id,\n tenantId,\n organizationId,\n }\n if (input.toPipelineStageId) {\n const stage = await loadPipelineStage(em, ctx, tenantId, input.toPipelineStageId, organizationId)\n if (!stage) {\n throw new Error('Pipeline stage not found.')\n }\n body.pipelineStageId = input.toPipelineStageId\n } else if (input.toStage) {\n body.status = input.toStage\n }\n\n const runner = createAiApiOperationRunner(ctx as unknown as AiToolExecutionContext)\n const response = await runner.run({\n method: 'PUT',\n path: '/customers/deals',\n body,\n })\n if (!response.success) {\n throw new Error(response.error ?? `Failed to update deal \"${deal.id}\"`)\n }\n\n const after = await loadDealWithStage(em, ctx, tenantId, deal.id)\n return {\n recordId: deal.id,\n commandName: 'customers.deals.update',\n before,\n after: after\n ? {\n status: after.status ?? null,\n pipelineStage: after.pipelineStage ?? null,\n pipelineStageId: after.pipelineStageId ?? null,\n }\n : null,\n }\n },\n}\n\nexport const dealsAiTools: CustomersAiToolDefinition[] = [listDealsTool, getDealTool, updateDealStageTool]\n\nexport default dealsAiTools\n"],
5
5
  "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/customers/ai-tools/people-pack.ts"],
4
- "sourcesContent": ["/**\n * `customers.list_people` + `customers.get_person` (Phase 1 WS-C, Step 3.9).\n *\n * Read-only tools scoped to `ctx.tenantId` / `ctx.organizationId` that wrap\n * the existing customers query engine + encryption helpers. Mutation tools\n * are deferred to Step 5.13+ under the pending-action contract.\n *\n * Phase 3a of `.ai/specs/2026-04-27-ai-tools-api-backed-dry-refactor.md`:\n * `customers.list_people` is now an API-backed wrapper over\n * `GET /api/customers/people`. The `companyId` AI input has no inclusion\n * equivalent on the route (the route exposes `excludeLinkedCompanyId` only)\n * so it is pre-resolved against `CustomerPersonProfile.company` and threaded\n * through the route's `ids` filter.\n *\n * Phase 3c of the same spec migrates `customers.get_person` to a single\n * in-process call to `GET /api/customers/people/<id>?include=...` (the\n * documented aggregate detail route). Tool name, schema, requiredFeatures,\n * and output shape are unchanged.\n */\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { z } from 'zod'\nimport { defineApiBackedAiTool } from '@open-mercato/ai-assistant/modules/ai_assistant/lib/api-backed-tool'\nimport {\n createAiApiOperationRunner,\n type AiApiOperationRequest,\n type AiToolExecutionContext,\n} from '@open-mercato/ai-assistant/modules/ai_assistant/lib/ai-api-operation-runner'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n CustomerPersonProfile,\n} from '../data/entities'\nimport { assertTenantScope, type CustomersAiToolDefinition, type CustomersToolContext } from './types'\n\nconst NIL_UUID = '00000000-0000-0000-0000-000000000000'\n\nfunction resolveEm(ctx: CustomersToolContext | AiToolExecutionContext): EntityManager {\n return ctx.container.resolve<EntityManager>('em')\n}\n\nfunction buildScope(ctx: CustomersToolContext | AiToolExecutionContext, tenantId: string) {\n return {\n tenantId,\n organizationId: ctx.organizationId,\n }\n}\n\nconst listPeopleInput = z\n .object({\n q: z.string().trim().optional().describe('Optional search text matched against display name / email / phone. Omit or leave empty to list all.'),\n limit: z.number().int().min(1).max(100).optional().describe('Maximum rows to return (default 50, max 100).'),\n offset: z.number().int().min(0).optional().describe('Number of rows to skip (default 0).'),\n tags: z.array(z.string().uuid()).optional().describe('Restrict to persons carrying at least one of these tag ids.'),\n companyId: z.string().uuid().optional().describe('Restrict to persons linked to the given company entity.'),\n })\n .passthrough()\n\ntype ListPeopleInput = z.infer<typeof listPeopleInput>\n\ntype ListPeopleApiItem = {\n id?: string\n display_name?: string | null\n displayName?: string | null\n primary_email?: string | null\n primaryEmail?: string | null\n primary_phone?: string | null\n primaryPhone?: string | null\n status?: string | null\n lifecycle_stage?: string | null\n lifecycleStage?: string | null\n source?: string | null\n owner_user_id?: string | null\n ownerUserId?: string | null\n organization_id?: string | null\n organizationId?: string | null\n tenant_id?: string | null\n tenantId?: string | null\n created_at?: string | null\n createdAt?: string | null\n}\n\ntype ListPeopleApiResponse = {\n items?: ListPeopleApiItem[]\n total?: number\n}\n\ntype ListPeopleOutput = {\n items: Array<Record<string, unknown>>\n total: number\n limit: number\n offset: number\n}\n\nconst listPeopleTool = defineApiBackedAiTool<ListPeopleInput, ListPeopleApiResponse, ListPeopleOutput>({\n name: 'customers.list_people',\n displayName: 'List people',\n description:\n 'Search / list people (CRM persons) for the caller tenant + organization. Returns { items, total, limit, offset }.',\n inputSchema: listPeopleInput,\n requiredFeatures: ['customers.people.view'],\n toOperation: async (input, ctx) => {\n const { tenantId } = assertTenantScope(ctx as unknown as CustomersToolContext)\n const limit = input.limit ?? 50\n const offset = input.offset ?? 0\n const page = Math.floor(offset / limit) + 1\n\n const query: Record<string, string | number | boolean | null | undefined> = {\n page,\n pageSize: limit,\n }\n if (input.q?.trim()) query.search = input.q.trim()\n if (input.tags && input.tags.length > 0) query.tagIds = input.tags.join(',')\n\n if (input.companyId) {\n const em = resolveEm(ctx)\n const profiles = await findWithDecryption<CustomerPersonProfile>(\n em,\n CustomerPersonProfile,\n { tenantId, company: input.companyId } as never,\n undefined,\n buildScope(ctx, tenantId),\n )\n const ids = profiles\n .map((profile) => {\n const entity = (profile as { entity?: unknown }).entity\n if (!entity) return null\n if (typeof entity === 'string') return entity\n const candidate = (entity as { id?: unknown }).id\n return typeof candidate === 'string' ? candidate : null\n })\n .filter((value): value is string => typeof value === 'string' && value.length > 0)\n // Empty match \u2014 feed a non-existent uuid so the route returns\n // { items: [], total: 0 } without us bypassing the API.\n query.ids = ids.length ? ids.join(',') : NIL_UUID\n }\n\n const operation: AiApiOperationRequest = {\n method: 'GET',\n path: '/customers/people',\n query,\n }\n return operation\n },\n mapResponse: (response, input) => {\n const limit = input.limit ?? 50\n const offset = input.offset ?? 0\n const data = (response.data ?? {}) as ListPeopleApiResponse\n const rawItems: ListPeopleApiItem[] = Array.isArray(data.items) ? data.items : []\n return {\n items: rawItems.map((row) => {\n const createdAtRaw = row.created_at ?? row.createdAt ?? null\n const createdAt = createdAtRaw ? new Date(String(createdAtRaw)).toISOString() : null\n return {\n id: row.id,\n displayName: row.display_name ?? row.displayName ?? null,\n primaryEmail: row.primary_email ?? row.primaryEmail ?? null,\n primaryPhone: row.primary_phone ?? row.primaryPhone ?? null,\n status: row.status ?? null,\n lifecycleStage: row.lifecycle_stage ?? row.lifecycleStage ?? null,\n source: row.source ?? null,\n ownerUserId: row.owner_user_id ?? row.ownerUserId ?? null,\n organizationId: row.organization_id ?? row.organizationId ?? null,\n tenantId: row.tenant_id ?? row.tenantId ?? null,\n createdAt,\n }\n }),\n total: typeof data.total === 'number' ? data.total : 0,\n limit,\n offset,\n }\n },\n}) as unknown as CustomersAiToolDefinition\n\nconst getPersonInput = z.object({\n personId: z.string().uuid().describe('Person entity id (UUID).'),\n includeRelated: z\n .boolean()\n .optional()\n .describe('When true, include notes, activities, deals, addresses, tasks, and tags (each capped at 100).'),\n})\n\ntype GetPersonInput = z.infer<typeof getPersonInput>\n\ntype ApiPersonDetailRow = Record<string, unknown> | null | undefined\n\nfunction toIso(value: unknown): string | null {\n if (!value) return null\n const dt = value instanceof Date ? value : new Date(String(value))\n if (Number.isNaN(dt.getTime())) return null\n return dt.toISOString()\n}\n\nconst getPersonTool: CustomersAiToolDefinition = {\n name: 'customers.get_person',\n displayName: 'Get person',\n description:\n 'Fetch a person customer record by id with profile fields and (optionally) notes, activities, deals, addresses, tasks, tags, and custom fields. Returns { found: false } when the record is outside tenant/org scope or missing.',\n inputSchema: getPersonInput,\n requiredFeatures: ['customers.people.view'],\n tags: ['read', 'customers'],\n handler: async (rawInput, ctx) => {\n const { tenantId } = assertTenantScope(ctx)\n const input: GetPersonInput = getPersonInput.parse(rawInput)\n const includeRelated = !!input.includeRelated\n\n const operation: AiApiOperationRequest = {\n method: 'GET',\n path: `/customers/people/${input.personId}`,\n }\n if (includeRelated) {\n operation.query = { include: 'addresses,comments,activities,interactions,deals,todos' }\n }\n\n const runner = createAiApiOperationRunner(ctx as unknown as AiToolExecutionContext)\n const response = await runner.run<Record<string, unknown>>(operation)\n if (!response.success) {\n if (response.statusCode === 404 || response.statusCode === 403) {\n return { found: false as const, personId: input.personId }\n }\n throw new Error(response.error ?? `Failed to fetch person ${input.personId}`)\n }\n const data = (response.data ?? {}) as Record<string, unknown>\n const personRow = (data.person ?? null) as ApiPersonDetailRow\n if (!personRow) {\n return { found: false as const, personId: input.personId }\n }\n const profileRow = (data.profile ?? null) as ApiPersonDetailRow\n const customFields = (data.customFields ?? {}) as Record<string, unknown>\n\n let related: Record<string, unknown> | null = null\n if (includeRelated) {\n const addresses = Array.isArray(data.addresses) ? (data.addresses as Array<Record<string, unknown>>) : []\n const activities = Array.isArray(data.activities) ? (data.activities as Array<Record<string, unknown>>) : []\n const notes = Array.isArray(data.comments) ? (data.comments as Array<Record<string, unknown>>) : []\n const todos = Array.isArray(data.todos) ? (data.todos as Array<Record<string, unknown>>) : []\n const interactions = Array.isArray(data.interactions) ? (data.interactions as Array<Record<string, unknown>>) : []\n const tagsRows = Array.isArray(data.tags) ? (data.tags as Array<Record<string, unknown>>) : []\n const dealsRows = Array.isArray(data.deals) ? (data.deals as Array<Record<string, unknown>>) : []\n related = {\n addresses: addresses.map((address) => ({\n id: address.id,\n name: address.name ?? null,\n purpose: address.purpose ?? null,\n addressLine1: address.addressLine1 ?? null,\n addressLine2: address.addressLine2 ?? null,\n city: address.city ?? null,\n region: address.region ?? null,\n postalCode: address.postalCode ?? null,\n country: address.country ?? null,\n isPrimary: !!address.isPrimary,\n })),\n activities: activities.map((activity) => ({\n id: activity.id,\n activityType: activity.activityType,\n subject: activity.subject ?? null,\n body: activity.body ?? null,\n occurredAt: toIso(activity.occurredAt),\n createdAt: toIso(activity.createdAt),\n })),\n notes: notes.map((comment) => ({\n id: comment.id,\n body: comment.body,\n authorUserId: comment.authorUserId ?? null,\n createdAt: toIso(comment.createdAt),\n })),\n tasks: todos.map((task) => ({\n id: task.id,\n todoId: task.todoId ?? task.id,\n todoSource: task.todoSource ?? null,\n createdAt: toIso(task.createdAt),\n })),\n interactions: interactions.map((interaction) => ({\n id: interaction.id,\n interactionType: interaction.interactionType,\n title: interaction.title ?? null,\n status: interaction.status,\n scheduledAt: toIso(interaction.scheduledAt),\n occurredAt: toIso(interaction.occurredAt),\n })),\n tags: tagsRows\n .map((tag) => {\n if (!tag || typeof tag !== 'object') return null\n const id = typeof tag.id === 'string' ? tag.id : null\n const label = typeof tag.label === 'string' ? tag.label : null\n if (!id || !label) return null\n const slug = typeof tag.slug === 'string' ? tag.slug : label\n const color = typeof tag.color === 'string' ? tag.color : null\n return { id, slug, label, color }\n })\n .filter(\n (entry): entry is { id: string; slug: string; label: string; color: string | null } =>\n entry !== null,\n ),\n deals: dealsRows\n .map((deal) => {\n if (!deal || typeof deal !== 'object') return null\n const id = typeof deal.id === 'string' ? deal.id : null\n if (!id) return null\n return {\n id,\n title: typeof deal.title === 'string' ? deal.title : '',\n status: typeof deal.status === 'string' ? deal.status : null,\n pipelineStageId:\n typeof deal.pipelineStageId === 'string' ? deal.pipelineStageId : null,\n valueAmount:\n typeof deal.valueAmount === 'string'\n ? deal.valueAmount\n : deal.valueAmount === null || deal.valueAmount === undefined\n ? null\n : String(deal.valueAmount),\n valueCurrency:\n typeof deal.valueCurrency === 'string' ? deal.valueCurrency : null,\n }\n })\n .filter(\n (\n value,\n ): value is {\n id: string\n title: string\n status: string | null\n pipelineStageId: string | null\n valueAmount: string | null\n valueCurrency: string | null\n } => value !== null,\n ),\n }\n }\n\n return {\n found: true as const,\n person: {\n id: personRow.id,\n displayName: personRow.displayName ?? null,\n description: personRow.description ?? null,\n primaryEmail: personRow.primaryEmail ?? null,\n primaryPhone: personRow.primaryPhone ?? null,\n status: personRow.status ?? null,\n lifecycleStage: personRow.lifecycleStage ?? null,\n source: personRow.source ?? null,\n ownerUserId: personRow.ownerUserId ?? null,\n organizationId: personRow.organizationId ?? null,\n tenantId: personRow.tenantId ?? null,\n createdAt: toIso(personRow.createdAt),\n updatedAt: toIso(personRow.updatedAt),\n },\n profile: profileRow\n ? {\n id: profileRow.id,\n firstName: profileRow.firstName ?? null,\n lastName: profileRow.lastName ?? null,\n preferredName: profileRow.preferredName ?? null,\n jobTitle: profileRow.jobTitle ?? null,\n department: profileRow.department ?? null,\n seniority: profileRow.seniority ?? null,\n timezone: profileRow.timezone ?? null,\n linkedInUrl: profileRow.linkedInUrl ?? null,\n twitterUrl: profileRow.twitterUrl ?? null,\n companyEntityId: profileRow.companyEntityId ?? null,\n }\n : null,\n customFields,\n related,\n }\n },\n}\n\nexport const peopleAiTools: CustomersAiToolDefinition[] = [listPeopleTool, getPersonTool]\n\nexport default peopleAiTools\n"],
4
+ "sourcesContent": ["/**\n * `customers.list_people` + `customers.get_person` (Phase 1 WS-C, Step 3.9).\n *\n * Read-only tools scoped to `ctx.tenantId` / `ctx.organizationId` that wrap\n * the existing customers query engine + encryption helpers. Mutation tools\n * are deferred to Step 5.13+ under the pending-action contract.\n *\n * Phase 3a of `.ai/specs/implemented/2026-04-27-ai-tools-api-backed-dry-refactor.md`:\n * `customers.list_people` is now an API-backed wrapper over\n * `GET /api/customers/people`. The `companyId` AI input has no inclusion\n * equivalent on the route (the route exposes `excludeLinkedCompanyId` only)\n * so it is pre-resolved against `CustomerPersonProfile.company` and threaded\n * through the route's `ids` filter.\n *\n * Phase 3c of the same spec migrates `customers.get_person` to a single\n * in-process call to `GET /api/customers/people/<id>?include=...` (the\n * documented aggregate detail route). Tool name, schema, requiredFeatures,\n * and output shape are unchanged.\n */\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { z } from 'zod'\nimport { defineApiBackedAiTool } from '@open-mercato/ai-assistant/modules/ai_assistant/lib/api-backed-tool'\nimport {\n createAiApiOperationRunner,\n type AiApiOperationRequest,\n type AiToolExecutionContext,\n} from '@open-mercato/ai-assistant/modules/ai_assistant/lib/ai-api-operation-runner'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n CustomerPersonProfile,\n} from '../data/entities'\nimport { assertTenantScope, type CustomersAiToolDefinition, type CustomersToolContext } from './types'\n\nconst NIL_UUID = '00000000-0000-0000-0000-000000000000'\n\nfunction resolveEm(ctx: CustomersToolContext | AiToolExecutionContext): EntityManager {\n return ctx.container.resolve<EntityManager>('em')\n}\n\nfunction buildScope(ctx: CustomersToolContext | AiToolExecutionContext, tenantId: string) {\n return {\n tenantId,\n organizationId: ctx.organizationId,\n }\n}\n\nconst listPeopleInput = z\n .object({\n q: z.string().trim().optional().describe('Optional search text matched against display name / email / phone. Omit or leave empty to list all.'),\n limit: z.number().int().min(1).max(100).optional().describe('Maximum rows to return (default 50, max 100).'),\n offset: z.number().int().min(0).optional().describe('Number of rows to skip (default 0).'),\n tags: z.array(z.string().uuid()).optional().describe('Restrict to persons carrying at least one of these tag ids.'),\n companyId: z.string().uuid().optional().describe('Restrict to persons linked to the given company entity.'),\n })\n .passthrough()\n\ntype ListPeopleInput = z.infer<typeof listPeopleInput>\n\ntype ListPeopleApiItem = {\n id?: string\n display_name?: string | null\n displayName?: string | null\n primary_email?: string | null\n primaryEmail?: string | null\n primary_phone?: string | null\n primaryPhone?: string | null\n status?: string | null\n lifecycle_stage?: string | null\n lifecycleStage?: string | null\n source?: string | null\n owner_user_id?: string | null\n ownerUserId?: string | null\n organization_id?: string | null\n organizationId?: string | null\n tenant_id?: string | null\n tenantId?: string | null\n created_at?: string | null\n createdAt?: string | null\n}\n\ntype ListPeopleApiResponse = {\n items?: ListPeopleApiItem[]\n total?: number\n}\n\ntype ListPeopleOutput = {\n items: Array<Record<string, unknown>>\n total: number\n limit: number\n offset: number\n}\n\nconst listPeopleTool = defineApiBackedAiTool<ListPeopleInput, ListPeopleApiResponse, ListPeopleOutput>({\n name: 'customers.list_people',\n displayName: 'List people',\n description:\n 'Search / list people (CRM persons) for the caller tenant + organization. Returns { items, total, limit, offset }.',\n inputSchema: listPeopleInput,\n requiredFeatures: ['customers.people.view'],\n toOperation: async (input, ctx) => {\n const { tenantId } = assertTenantScope(ctx as unknown as CustomersToolContext)\n const limit = input.limit ?? 50\n const offset = input.offset ?? 0\n const page = Math.floor(offset / limit) + 1\n\n const query: Record<string, string | number | boolean | null | undefined> = {\n page,\n pageSize: limit,\n }\n if (input.q?.trim()) query.search = input.q.trim()\n if (input.tags && input.tags.length > 0) query.tagIds = input.tags.join(',')\n\n if (input.companyId) {\n const em = resolveEm(ctx)\n const profiles = await findWithDecryption<CustomerPersonProfile>(\n em,\n CustomerPersonProfile,\n { tenantId, company: input.companyId } as never,\n undefined,\n buildScope(ctx, tenantId),\n )\n const ids = profiles\n .map((profile) => {\n const entity = (profile as { entity?: unknown }).entity\n if (!entity) return null\n if (typeof entity === 'string') return entity\n const candidate = (entity as { id?: unknown }).id\n return typeof candidate === 'string' ? candidate : null\n })\n .filter((value): value is string => typeof value === 'string' && value.length > 0)\n // Empty match \u2014 feed a non-existent uuid so the route returns\n // { items: [], total: 0 } without us bypassing the API.\n query.ids = ids.length ? ids.join(',') : NIL_UUID\n }\n\n const operation: AiApiOperationRequest = {\n method: 'GET',\n path: '/customers/people',\n query,\n }\n return operation\n },\n mapResponse: (response, input) => {\n const limit = input.limit ?? 50\n const offset = input.offset ?? 0\n const data = (response.data ?? {}) as ListPeopleApiResponse\n const rawItems: ListPeopleApiItem[] = Array.isArray(data.items) ? data.items : []\n return {\n items: rawItems.map((row) => {\n const createdAtRaw = row.created_at ?? row.createdAt ?? null\n const createdAt = createdAtRaw ? new Date(String(createdAtRaw)).toISOString() : null\n return {\n id: row.id,\n displayName: row.display_name ?? row.displayName ?? null,\n primaryEmail: row.primary_email ?? row.primaryEmail ?? null,\n primaryPhone: row.primary_phone ?? row.primaryPhone ?? null,\n status: row.status ?? null,\n lifecycleStage: row.lifecycle_stage ?? row.lifecycleStage ?? null,\n source: row.source ?? null,\n ownerUserId: row.owner_user_id ?? row.ownerUserId ?? null,\n organizationId: row.organization_id ?? row.organizationId ?? null,\n tenantId: row.tenant_id ?? row.tenantId ?? null,\n createdAt,\n }\n }),\n total: typeof data.total === 'number' ? data.total : 0,\n limit,\n offset,\n }\n },\n}) as unknown as CustomersAiToolDefinition\n\nconst getPersonInput = z.object({\n personId: z.string().uuid().describe('Person entity id (UUID).'),\n includeRelated: z\n .boolean()\n .optional()\n .describe('When true, include notes, activities, deals, addresses, tasks, and tags (each capped at 100).'),\n})\n\ntype GetPersonInput = z.infer<typeof getPersonInput>\n\ntype ApiPersonDetailRow = Record<string, unknown> | null | undefined\n\nfunction toIso(value: unknown): string | null {\n if (!value) return null\n const dt = value instanceof Date ? value : new Date(String(value))\n if (Number.isNaN(dt.getTime())) return null\n return dt.toISOString()\n}\n\nconst getPersonTool: CustomersAiToolDefinition = {\n name: 'customers.get_person',\n displayName: 'Get person',\n description:\n 'Fetch a person customer record by id with profile fields and (optionally) notes, activities, deals, addresses, tasks, tags, and custom fields. Returns { found: false } when the record is outside tenant/org scope or missing.',\n inputSchema: getPersonInput,\n requiredFeatures: ['customers.people.view'],\n tags: ['read', 'customers'],\n handler: async (rawInput, ctx) => {\n const { tenantId } = assertTenantScope(ctx)\n const input: GetPersonInput = getPersonInput.parse(rawInput)\n const includeRelated = !!input.includeRelated\n\n const operation: AiApiOperationRequest = {\n method: 'GET',\n path: `/customers/people/${input.personId}`,\n }\n if (includeRelated) {\n operation.query = { include: 'addresses,comments,activities,interactions,deals,todos' }\n }\n\n const runner = createAiApiOperationRunner(ctx as unknown as AiToolExecutionContext)\n const response = await runner.run<Record<string, unknown>>(operation)\n if (!response.success) {\n if (response.statusCode === 404 || response.statusCode === 403) {\n return { found: false as const, personId: input.personId }\n }\n throw new Error(response.error ?? `Failed to fetch person ${input.personId}`)\n }\n const data = (response.data ?? {}) as Record<string, unknown>\n const personRow = (data.person ?? null) as ApiPersonDetailRow\n if (!personRow) {\n return { found: false as const, personId: input.personId }\n }\n const profileRow = (data.profile ?? null) as ApiPersonDetailRow\n const customFields = (data.customFields ?? {}) as Record<string, unknown>\n\n let related: Record<string, unknown> | null = null\n if (includeRelated) {\n const addresses = Array.isArray(data.addresses) ? (data.addresses as Array<Record<string, unknown>>) : []\n const activities = Array.isArray(data.activities) ? (data.activities as Array<Record<string, unknown>>) : []\n const notes = Array.isArray(data.comments) ? (data.comments as Array<Record<string, unknown>>) : []\n const todos = Array.isArray(data.todos) ? (data.todos as Array<Record<string, unknown>>) : []\n const interactions = Array.isArray(data.interactions) ? (data.interactions as Array<Record<string, unknown>>) : []\n const tagsRows = Array.isArray(data.tags) ? (data.tags as Array<Record<string, unknown>>) : []\n const dealsRows = Array.isArray(data.deals) ? (data.deals as Array<Record<string, unknown>>) : []\n related = {\n addresses: addresses.map((address) => ({\n id: address.id,\n name: address.name ?? null,\n purpose: address.purpose ?? null,\n addressLine1: address.addressLine1 ?? null,\n addressLine2: address.addressLine2 ?? null,\n city: address.city ?? null,\n region: address.region ?? null,\n postalCode: address.postalCode ?? null,\n country: address.country ?? null,\n isPrimary: !!address.isPrimary,\n })),\n activities: activities.map((activity) => ({\n id: activity.id,\n activityType: activity.activityType,\n subject: activity.subject ?? null,\n body: activity.body ?? null,\n occurredAt: toIso(activity.occurredAt),\n createdAt: toIso(activity.createdAt),\n })),\n notes: notes.map((comment) => ({\n id: comment.id,\n body: comment.body,\n authorUserId: comment.authorUserId ?? null,\n createdAt: toIso(comment.createdAt),\n })),\n tasks: todos.map((task) => ({\n id: task.id,\n todoId: task.todoId ?? task.id,\n todoSource: task.todoSource ?? null,\n createdAt: toIso(task.createdAt),\n })),\n interactions: interactions.map((interaction) => ({\n id: interaction.id,\n interactionType: interaction.interactionType,\n title: interaction.title ?? null,\n status: interaction.status,\n scheduledAt: toIso(interaction.scheduledAt),\n occurredAt: toIso(interaction.occurredAt),\n })),\n tags: tagsRows\n .map((tag) => {\n if (!tag || typeof tag !== 'object') return null\n const id = typeof tag.id === 'string' ? tag.id : null\n const label = typeof tag.label === 'string' ? tag.label : null\n if (!id || !label) return null\n const slug = typeof tag.slug === 'string' ? tag.slug : label\n const color = typeof tag.color === 'string' ? tag.color : null\n return { id, slug, label, color }\n })\n .filter(\n (entry): entry is { id: string; slug: string; label: string; color: string | null } =>\n entry !== null,\n ),\n deals: dealsRows\n .map((deal) => {\n if (!deal || typeof deal !== 'object') return null\n const id = typeof deal.id === 'string' ? deal.id : null\n if (!id) return null\n return {\n id,\n title: typeof deal.title === 'string' ? deal.title : '',\n status: typeof deal.status === 'string' ? deal.status : null,\n pipelineStageId:\n typeof deal.pipelineStageId === 'string' ? deal.pipelineStageId : null,\n valueAmount:\n typeof deal.valueAmount === 'string'\n ? deal.valueAmount\n : deal.valueAmount === null || deal.valueAmount === undefined\n ? null\n : String(deal.valueAmount),\n valueCurrency:\n typeof deal.valueCurrency === 'string' ? deal.valueCurrency : null,\n }\n })\n .filter(\n (\n value,\n ): value is {\n id: string\n title: string\n status: string | null\n pipelineStageId: string | null\n valueAmount: string | null\n valueCurrency: string | null\n } => value !== null,\n ),\n }\n }\n\n return {\n found: true as const,\n person: {\n id: personRow.id,\n displayName: personRow.displayName ?? null,\n description: personRow.description ?? null,\n primaryEmail: personRow.primaryEmail ?? null,\n primaryPhone: personRow.primaryPhone ?? null,\n status: personRow.status ?? null,\n lifecycleStage: personRow.lifecycleStage ?? null,\n source: personRow.source ?? null,\n ownerUserId: personRow.ownerUserId ?? null,\n organizationId: personRow.organizationId ?? null,\n tenantId: personRow.tenantId ?? null,\n createdAt: toIso(personRow.createdAt),\n updatedAt: toIso(personRow.updatedAt),\n },\n profile: profileRow\n ? {\n id: profileRow.id,\n firstName: profileRow.firstName ?? null,\n lastName: profileRow.lastName ?? null,\n preferredName: profileRow.preferredName ?? null,\n jobTitle: profileRow.jobTitle ?? null,\n department: profileRow.department ?? null,\n seniority: profileRow.seniority ?? null,\n timezone: profileRow.timezone ?? null,\n linkedInUrl: profileRow.linkedInUrl ?? null,\n twitterUrl: profileRow.twitterUrl ?? null,\n companyEntityId: profileRow.companyEntityId ?? null,\n }\n : null,\n customFields,\n related,\n }\n },\n}\n\nexport const peopleAiTools: CustomersAiToolDefinition[] = [listPeopleTool, getPersonTool]\n\nexport default peopleAiTools\n"],
5
5
  "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -22,7 +22,7 @@ import {
22
22
  extractAllCustomFieldEntries,
23
23
  splitCustomFieldPayload
24
24
  } from "@open-mercato/shared/lib/crud/custom-fields";
25
- import { escapeLikePattern } from "@open-mercato/shared/lib/db/escapeLikePattern";
25
+ import { buildIlikeTerm } from "@open-mercato/shared/lib/db/buildIlikeTerm";
26
26
  import { parseBooleanToken } from "@open-mercato/shared/lib/boolean";
27
27
  import { findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
28
28
  import { consumeAdvancedFilterState, mergeAdvancedFilterTree } from "@open-mercato/shared/lib/crud/advanced-filter-integration";
@@ -168,7 +168,7 @@ const crud = makeCrudRoute({
168
168
  if (matchingIds !== null && matchingIds.length > 0) {
169
169
  applyEntityIdRestriction(filters, matchingIds);
170
170
  } else {
171
- const searchPattern = `%${escapeLikePattern(query.search)}%`;
171
+ const searchPattern = buildIlikeTerm(query.search);
172
172
  filters.$or = [
173
173
  { display_name: { $ilike: searchPattern } },
174
174
  { primary_email: { $ilike: searchPattern } },
@@ -259,9 +259,9 @@ const crud = makeCrudRoute({
259
259
  if (email) {
260
260
  filters.primary_email = { $eq: email };
261
261
  } else if (emailStartsWith) {
262
- filters.primary_email = { $ilike: `${escapeLikePattern(emailStartsWith)}%` };
262
+ filters.primary_email = { $ilike: buildIlikeTerm(emailStartsWith, "startsWith") };
263
263
  } else if (emailContains) {
264
- filters.primary_email = { $ilike: `%${escapeLikePattern(emailContains)}%` };
264
+ filters.primary_email = { $ilike: buildIlikeTerm(emailContains) };
265
265
  }
266
266
  const hasEmail = parseBooleanToken(query.hasEmail);
267
267
  if (!email && !emailStartsWith && !emailContains && hasEmail !== null) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/customers/api/companies/route.ts"],
4
- "sourcesContent": ["import type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'\nimport { z } from 'zod'\nimport { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport {\n CustomerCompanyProfile,\n CustomerDealCompanyLink,\n CustomerEntity,\n CustomerPersonCompanyLink,\n} from '../../data/entities'\nimport { E } from '#generated/entities.ids.generated'\nimport { companyCreateSchema, companyUpdateSchema } from '../../data/validators'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport {\n applyEntityIdExclusion,\n applyEntityIdRestriction,\n findMatchingEntityIdsWithQueryEngine,\n findMatchingEntityIdsBySearchTokensAcrossSources,\n withScopedPayload,\n} from '../utils'\nimport {\n buildCustomFieldFiltersFromQuery,\n extractAllCustomFieldEntries,\n splitCustomFieldPayload,\n} from '@open-mercato/shared/lib/crud/custom-fields'\nimport { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'\nimport { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { consumeAdvancedFilterState, mergeAdvancedFilterTree } from '@open-mercato/shared/lib/crud/advanced-filter-integration'\nimport {\n createCustomersCrudOpenApi,\n createPagedListResponseSchema,\n defaultOkResponseSchema,\n} from '../openapi'\nimport {\n filterActivePersonCompanyLinks,\n withActiveCustomerPersonCompanyLinkFilter,\n withCustomerPersonCompanyLinkScope,\n withScopedCustomerDealLinkWhere,\n} from '../../lib/personCompanyLinkTable'\nimport { normalizeCompanyProfilePayload } from './payload'\n\nconst rawBodySchema = z.object({}).passthrough()\n\nconst listSchema = z\n .object({\n page: z.coerce.number().min(1).default(1),\n pageSize: z.coerce.number().min(1).max(100).default(50),\n search: z.string().optional(),\n email: z.string().optional(),\n emailStartsWith: z.string().optional(),\n emailContains: z.string().optional(),\n sortField: z.string().optional(),\n sortDir: z.enum(['asc', 'desc']).optional(),\n status: z.string().optional(),\n lifecycleStage: z.string().optional(),\n source: z.string().optional(),\n hasEmail: z.string().optional(),\n hasPhone: z.string().optional(),\n hasNextInteraction: z.string().optional(),\n createdFrom: z.string().optional(),\n createdTo: z.string().optional(),\n id: z.string().uuid().optional(),\n tagIds: z.string().optional(),\n tagIdsEmpty: z.string().optional(),\n excludeIds: z.string().optional(),\n excludeLinkedPersonId: z.string().uuid().optional(),\n excludeLinkedCompanyId: z.string().uuid().optional(),\n excludeLinkedDealId: z.string().uuid().optional(),\n })\n .passthrough()\n\nconst routeMetadata = {\n GET: { requireAuth: true, requireFeatures: ['customers.companies.view'] },\n POST: { requireAuth: true, requireFeatures: ['customers.companies.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['customers.companies.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['customers.companies.manage'] },\n}\n\nexport const metadata = routeMetadata\n\nconst crud = makeCrudRoute({\n metadata: routeMetadata,\n orm: {\n entity: CustomerEntity,\n idField: 'id',\n orgField: 'organizationId',\n tenantField: 'tenantId',\n softDeleteField: 'deletedAt',\n },\n indexer: {\n entityType: E.customers.customer_entity,\n },\n list: {\n schema: listSchema,\n entityId: E.customers.customer_entity,\n fields: [\n 'id',\n 'display_name',\n 'description',\n 'owner_user_id',\n 'primary_email',\n 'primary_phone',\n 'status',\n 'lifecycle_stage',\n 'source',\n 'next_interaction_at',\n 'next_interaction_name',\n 'next_interaction_ref_id',\n 'next_interaction_icon',\n 'next_interaction_color',\n 'organization_id',\n 'tenant_id',\n 'kind',\n 'created_at',\n 'updated_at',\n ],\n sortFieldMap: {\n name: 'display_name',\n email: 'primary_email',\n primaryEmail: 'primary_email',\n status: 'status',\n lifecycleStage: 'lifecycle_stage',\n source: 'source',\n nextInteractionAt: 'next_interaction_at',\n createdAt: 'created_at',\n updatedAt: 'updated_at',\n },\n buildFilters: async (query, ctx) => {\n const advancedFilterTree = consumeAdvancedFilterState(query)\n const filters: Record<string, unknown> = { kind: { $eq: 'company' } }\n if (query.id) filters.id = { $eq: query.id }\n if (query.search) {\n const matchingIds = ctx\n ? await findMatchingEntityIdsBySearchTokensAcrossSources({\n ctx,\n query: query.search,\n sources: [\n {\n entityType: E.customers.customer_entity,\n fields: [\n 'display_name',\n 'primary_email',\n 'primary_phone',\n 'description',\n 'status',\n 'lifecycle_stage',\n 'source',\n 'next_interaction_name',\n ],\n },\n {\n entityType: E.customers.customer_company_profile,\n fields: [\n 'display_name',\n 'primary_email',\n 'primary_phone',\n 'description',\n 'status',\n 'lifecycle_stage',\n 'source',\n 'legal_name',\n 'brand_name',\n 'domain',\n 'website_url',\n 'industry',\n 'size_bucket',\n 'annual_revenue',\n ],\n mapToEntityIds: {\n table: 'customer_companies',\n targetColumn: 'entity_id',\n },\n },\n ],\n })\n : null\n if (matchingIds !== null && matchingIds.length > 0) {\n applyEntityIdRestriction(filters, matchingIds)\n } else {\n const searchPattern = `%${escapeLikePattern(query.search)}%`\n filters.$or = [\n { display_name: { $ilike: searchPattern } },\n { primary_email: { $ilike: searchPattern } },\n { primary_phone: { $ilike: searchPattern } },\n { description: { $ilike: searchPattern } },\n { next_interaction_name: { $ilike: searchPattern } },\n ]\n }\n }\n if (query.status) {\n filters.status = { $eq: query.status }\n }\n if (query.lifecycleStage) {\n filters.lifecycle_stage = { $eq: query.lifecycleStage }\n }\n if (query.source) {\n filters.source = { $eq: query.source }\n }\n const tagIdsRaw = typeof query.tagIds === 'string' ? query.tagIds : ''\n const tagIds = tagIdsRaw\n .split(',')\n .map((value: string) => value.trim())\n .filter((value: string) => value.length > 0)\n const tagIdsEmpty = parseBooleanToken(query.tagIdsEmpty) === true\n if (tagIdsEmpty) {\n filters.id = { $eq: '00000000-0000-0000-0000-000000000000' }\n } else if (tagIds.length > 0) {\n filters['tag_assignments.tag_id'] = { $in: tagIds }\n }\n const excludedIds = new Set<string>()\n const excludeIdsRaw = typeof query.excludeIds === 'string' ? query.excludeIds : ''\n excludeIdsRaw\n .split(',')\n .map((value: string) => value.trim())\n .filter((value: string) => value.length > 0)\n .forEach((value: string) => excludedIds.add(value))\n if (ctx && query.excludeLinkedPersonId) {\n try {\n const em = ctx.container.resolve('em') as EntityManager\n const decryptionScope = {\n tenantId: ctx.auth?.tenantId ?? null,\n organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,\n }\n const linkWhere = await withActiveCustomerPersonCompanyLinkFilter(\n em,\n withCustomerPersonCompanyLinkScope({ person: query.excludeLinkedPersonId }, decryptionScope),\n 'customers.companies.GET',\n )\n const links = filterActivePersonCompanyLinks(\n await findWithDecryption(\n em,\n CustomerPersonCompanyLink,\n linkWhere,\n { populate: ['company'] },\n decryptionScope,\n ),\n )\n links.forEach((link) => {\n const companyId = link.company?.id\n if (typeof companyId === 'string' && companyId.length > 0) excludedIds.add(companyId)\n })\n } catch (err) {\n console.warn('[customers.companies.list] exclusion lookup failed; falling back to base result set', err)\n }\n }\n if (typeof query.excludeLinkedCompanyId === 'string' && query.excludeLinkedCompanyId.length > 0) {\n excludedIds.add(query.excludeLinkedCompanyId)\n }\n if (ctx && query.excludeLinkedDealId) {\n try {\n const em = ctx.container.resolve('em') as EntityManager\n const decryptionScope = {\n tenantId: ctx.auth?.tenantId ?? null,\n organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,\n }\n const links = await findWithDecryption(\n em,\n CustomerDealCompanyLink,\n withScopedCustomerDealLinkWhere(query.excludeLinkedDealId, decryptionScope),\n { populate: ['company'] },\n decryptionScope,\n )\n links.forEach((link) => {\n const companyId = link.company?.id\n if (typeof companyId === 'string' && companyId.length > 0) excludedIds.add(companyId)\n })\n } catch (err) {\n console.warn('[customers.companies.list] exclusion lookup failed; falling back to base result set', err)\n }\n }\n applyEntityIdExclusion(filters, Array.from(excludedIds))\n const email = typeof query.email === 'string' ? query.email.trim().toLowerCase() : ''\n const emailStartsWith = typeof query.emailStartsWith === 'string' ? query.emailStartsWith.trim().toLowerCase() : ''\n const emailContains = typeof query.emailContains === 'string' ? query.emailContains.trim().toLowerCase() : ''\n if (email) {\n filters.primary_email = { $eq: email }\n } else if (emailStartsWith) {\n filters.primary_email = { $ilike: `${escapeLikePattern(emailStartsWith)}%` }\n } else if (emailContains) {\n filters.primary_email = { $ilike: `%${escapeLikePattern(emailContains)}%` }\n }\n const hasEmail = parseBooleanToken(query.hasEmail)\n if (!email && !emailStartsWith && !emailContains && hasEmail !== null) {\n filters.primary_email = { $exists: hasEmail }\n }\n const hasPhone = parseBooleanToken(query.hasPhone)\n if (hasPhone !== null) {\n filters.primary_phone = { $exists: hasPhone }\n }\n const hasNextInteraction = parseBooleanToken(query.hasNextInteraction)\n if (hasNextInteraction !== null) {\n filters.next_interaction_at = { $exists: hasNextInteraction }\n }\n const createdRange: Record<string, Date> = {}\n if (query.createdFrom) {\n const from = new Date(query.createdFrom)\n if (!Number.isNaN(from.getTime())) createdRange.$gte = from\n }\n if (query.createdTo) {\n const to = new Date(query.createdTo)\n if (!Number.isNaN(to.getTime())) createdRange.$lte = to\n }\n if (Object.keys(createdRange).length) {\n filters.created_at = createdRange\n }\n if (ctx) {\n try {\n const em = ctx.container.resolve('em') as EntityManager\n const cfFilters = await buildCustomFieldFiltersFromQuery({\n entityIds: [E.customers.customer_entity, E.customers.customer_company_profile],\n query,\n em,\n tenantId: ctx.auth?.tenantId ?? null,\n })\n Object.assign(filters, cfFilters)\n } catch (err) {\n console.warn('[customers.companies.list] custom field filter resolution failed; falling back to base filters', err)\n }\n }\n if (ctx && advancedFilterTree) {\n const advancedFilters = mergeAdvancedFilterTree({ ...filters }, advancedFilterTree)\n const matchedIds = await findMatchingEntityIdsWithQueryEngine({\n ctx,\n entityId: E.customers.customer_entity,\n filters: advancedFilters,\n customFieldSources: [\n {\n entityId: E.customers.customer_company_profile,\n table: 'customer_companies',\n alias: 'company_profile',\n recordIdColumn: 'id',\n join: { fromField: 'id', toField: 'entity_id' },\n },\n ],\n joins: [\n {\n alias: 'tag_assignments',\n table: 'customer_tag_assignments',\n from: { field: 'id' },\n to: { field: 'entity_id' },\n type: 'left',\n },\n ],\n })\n applyEntityIdRestriction(filters, matchedIds)\n }\n return filters\n },\n customFieldSources: [\n {\n entityId: E.customers.customer_company_profile,\n table: 'customer_companies',\n alias: 'company_profile',\n recordIdColumn: 'id',\n join: { fromField: 'id', toField: 'entity_id' },\n },\n ],\n joins: [\n {\n alias: 'tag_assignments',\n table: 'customer_tag_assignments',\n from: { field: 'id' },\n to: { field: 'entity_id' },\n type: 'left',\n },\n ],\n transformItem: (item) => {\n if (!item || typeof item !== 'object') return item\n const record = item as Record<string, unknown>\n const normalized: Record<string, unknown> = { ...record }\n delete normalized.kind\n const cfEntries = extractAllCustomFieldEntries(record)\n for (const key of Object.keys(normalized)) {\n if (key.startsWith('cf:')) {\n delete normalized[key]\n }\n }\n return { ...normalized, ...cfEntries }\n },\n },\n actions: {\n create: {\n commandId: 'customers.companies.create',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n const scoped = withScopedPayload(raw ?? {}, ctx, translate)\n const { base, custom } = splitCustomFieldPayload(scoped)\n const parsed = companyCreateSchema.parse(base)\n return Object.keys(custom).length ? { ...parsed, customFields: custom } : parsed\n },\n response: ({ result }) => ({\n id: result?.entityId ?? result?.id ?? null,\n companyId: result?.companyId ?? null,\n }),\n status: 201,\n },\n update: {\n commandId: 'customers.companies.update',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n const scoped = withScopedPayload(raw ?? {}, ctx, translate)\n const normalized = normalizeCompanyProfilePayload(scoped, translate)\n const { base, custom } = splitCustomFieldPayload(normalized)\n const parsed = companyUpdateSchema.parse(base)\n return Object.keys(custom).length ? { ...parsed, customFields: custom } : parsed\n },\n // Return the freshly-bumped updatedAt so inline-edit detail pages can refresh\n // their optimistic-lock token between sequential saves (avoids a false 409 on\n // the 2nd inline edit now that locking is default-ON). #2055. The factory hands\n // the updated entity here; read defensively in case a `{ result }` wrapper arrives.\n response: (arg: { result?: unknown; updatedAt?: Date | string | null }) => {\n const raw = arg?.updatedAt\n ?? (arg?.result as { updatedAt?: Date | string | null } | null | undefined)?.updatedAt\n ?? null\n return {\n ok: true,\n updatedAt: raw instanceof Date ? raw.toISOString() : (typeof raw === 'string' ? raw : null),\n }\n },\n },\n delete: {\n commandId: 'customers.companies.delete',\n schema: rawBodySchema,\n mapInput: async ({ parsed, ctx }) => {\n const { translate } = await resolveTranslations()\n const id =\n parsed?.body?.id ??\n parsed?.id ??\n parsed?.query?.id ??\n (ctx.request ? new URL(ctx.request.url).searchParams.get('id') : null)\n if (!id) throw new CrudHttpError(400, { error: translate('customers.errors.company_required', 'Company id is required') })\n return { id }\n },\n response: () => ({ ok: true }),\n },\n },\n hooks: {\n afterList: async (payload, ctx) => {\n const items = Array.isArray(payload?.items) ? payload.items : []\n const ids = items\n .map((item: unknown) => (item && typeof item === 'object' && typeof (item as Record<string, unknown>).id === 'string'\n ? (item as Record<string, unknown>).id as string\n : null))\n .filter((id: string | null): id is string => typeof id === 'string' && id.length > 0)\n if (!ids.length) return\n\n const where: Record<string, unknown> = {\n entity: { $in: ids },\n tenantId: ctx.auth?.tenantId ?? null,\n }\n if (ctx.selectedOrganizationId) {\n where.organizationId = ctx.selectedOrganizationId\n }\n\n const profiles = await findWithDecryption(\n ctx.container.resolve('em') as EntityManager,\n CustomerCompanyProfile,\n where as FilterQuery<CustomerCompanyProfile>,\n { populate: ['entity'] },\n {\n tenantId: ctx.auth?.tenantId ?? null,\n organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,\n },\n )\n\n const profilesByEntityId = new Map<string, CustomerCompanyProfile>()\n for (const profile of profiles) {\n const profileEntity = (profile as { entity?: { id?: unknown } }).entity\n const entityId = typeof profileEntity?.id === 'string' ? profileEntity.id : null\n if (entityId) profilesByEntityId.set(entityId, profile)\n }\n\n payload.items = items.map((item: unknown) => {\n if (!item || typeof item !== 'object') return item\n const record = item as Record<string, unknown>\n const profile = typeof record.id === 'string' ? profilesByEntityId.get(record.id) : undefined\n if (!profile) return item\n return {\n ...record,\n legal_name: profile.legalName ?? null,\n brand_name: profile.brandName ?? null,\n domain: profile.domain ?? null,\n website_url: profile.websiteUrl ?? null,\n industry: profile.industry ?? null,\n size_bucket: profile.sizeBucket ?? null,\n annual_revenue: profile.annualRevenue ?? null,\n }\n })\n },\n },\n})\n\nconst { POST, PUT, DELETE } = crud\n\nexport { POST, PUT, DELETE }\nexport const GET = crud.GET\n\nconst companyListItemSchema = z.object({\n id: z.string().uuid(),\n display_name: z.string().optional(),\n description: z.string().nullable().optional(),\n owner_user_id: z.string().uuid().nullable().optional(),\n primary_email: z.string().nullable().optional(),\n primary_phone: z.string().nullable().optional(),\n status: z.string().nullable().optional(),\n lifecycle_stage: z.string().nullable().optional(),\n source: z.string().nullable().optional(),\n next_interaction_at: z.string().nullable().optional(),\n next_interaction_name: z.string().nullable().optional(),\n next_interaction_ref_id: z.string().nullable().optional(),\n next_interaction_icon: z.string().nullable().optional(),\n next_interaction_color: z.string().nullable().optional(),\n organization_id: z.string().uuid().nullable().optional(),\n tenant_id: z.string().uuid().nullable().optional(),\n created_at: z.string().nullable().optional(),\n})\n\nconst companyCreateResponseSchema = z.object({\n id: z.string().uuid().nullable(),\n companyId: z.string().uuid().nullable(),\n})\n\nexport const openApi = createCustomersCrudOpenApi({\n resourceName: 'Company',\n pluralName: 'Companies',\n querySchema: listSchema,\n listResponseSchema: createPagedListResponseSchema(companyListItemSchema),\n create: {\n schema: companyCreateSchema,\n responseSchema: companyCreateResponseSchema,\n description: 'Creates a company record and associated profile data.',\n },\n update: {\n schema: companyUpdateSchema,\n responseSchema: defaultOkResponseSchema,\n description: 'Updates company profile fields, tags, or custom attributes.',\n },\n del: {\n schema: z.object({ id: z.string().uuid() }),\n responseSchema: defaultOkResponseSchema,\n description: 'Deletes a company by id. The identifier can be provided via body or query.',\n errors: [\n {\n status: 422,\n description: 'Company has dependent records (people, deals, or direct staff); unlink or reassign before delete.',\n schema: z.object({\n error: z.string(),\n code: z.literal('COMPANY_HAS_DEPENDENTS'),\n }),\n },\n ],\n },\n})\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'\nimport { z } from 'zod'\nimport { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport {\n CustomerCompanyProfile,\n CustomerDealCompanyLink,\n CustomerEntity,\n CustomerPersonCompanyLink,\n} from '../../data/entities'\nimport { E } from '#generated/entities.ids.generated'\nimport { companyCreateSchema, companyUpdateSchema } from '../../data/validators'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport {\n applyEntityIdExclusion,\n applyEntityIdRestriction,\n findMatchingEntityIdsWithQueryEngine,\n findMatchingEntityIdsBySearchTokensAcrossSources,\n withScopedPayload,\n} from '../utils'\nimport {\n buildCustomFieldFiltersFromQuery,\n extractAllCustomFieldEntries,\n splitCustomFieldPayload,\n} from '@open-mercato/shared/lib/crud/custom-fields'\nimport { buildIlikeTerm } from '@open-mercato/shared/lib/db/buildIlikeTerm'\nimport { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { consumeAdvancedFilterState, mergeAdvancedFilterTree } from '@open-mercato/shared/lib/crud/advanced-filter-integration'\nimport {\n createCustomersCrudOpenApi,\n createPagedListResponseSchema,\n defaultOkResponseSchema,\n} from '../openapi'\nimport {\n filterActivePersonCompanyLinks,\n withActiveCustomerPersonCompanyLinkFilter,\n withCustomerPersonCompanyLinkScope,\n withScopedCustomerDealLinkWhere,\n} from '../../lib/personCompanyLinkTable'\nimport { normalizeCompanyProfilePayload } from './payload'\n\nconst rawBodySchema = z.object({}).passthrough()\n\nconst listSchema = z\n .object({\n page: z.coerce.number().min(1).default(1),\n pageSize: z.coerce.number().min(1).max(100).default(50),\n search: z.string().optional(),\n email: z.string().optional(),\n emailStartsWith: z.string().optional(),\n emailContains: z.string().optional(),\n sortField: z.string().optional(),\n sortDir: z.enum(['asc', 'desc']).optional(),\n status: z.string().optional(),\n lifecycleStage: z.string().optional(),\n source: z.string().optional(),\n hasEmail: z.string().optional(),\n hasPhone: z.string().optional(),\n hasNextInteraction: z.string().optional(),\n createdFrom: z.string().optional(),\n createdTo: z.string().optional(),\n id: z.string().uuid().optional(),\n tagIds: z.string().optional(),\n tagIdsEmpty: z.string().optional(),\n excludeIds: z.string().optional(),\n excludeLinkedPersonId: z.string().uuid().optional(),\n excludeLinkedCompanyId: z.string().uuid().optional(),\n excludeLinkedDealId: z.string().uuid().optional(),\n })\n .passthrough()\n\nconst routeMetadata = {\n GET: { requireAuth: true, requireFeatures: ['customers.companies.view'] },\n POST: { requireAuth: true, requireFeatures: ['customers.companies.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['customers.companies.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['customers.companies.manage'] },\n}\n\nexport const metadata = routeMetadata\n\nconst crud = makeCrudRoute({\n metadata: routeMetadata,\n orm: {\n entity: CustomerEntity,\n idField: 'id',\n orgField: 'organizationId',\n tenantField: 'tenantId',\n softDeleteField: 'deletedAt',\n },\n indexer: {\n entityType: E.customers.customer_entity,\n },\n list: {\n schema: listSchema,\n entityId: E.customers.customer_entity,\n fields: [\n 'id',\n 'display_name',\n 'description',\n 'owner_user_id',\n 'primary_email',\n 'primary_phone',\n 'status',\n 'lifecycle_stage',\n 'source',\n 'next_interaction_at',\n 'next_interaction_name',\n 'next_interaction_ref_id',\n 'next_interaction_icon',\n 'next_interaction_color',\n 'organization_id',\n 'tenant_id',\n 'kind',\n 'created_at',\n 'updated_at',\n ],\n sortFieldMap: {\n name: 'display_name',\n email: 'primary_email',\n primaryEmail: 'primary_email',\n status: 'status',\n lifecycleStage: 'lifecycle_stage',\n source: 'source',\n nextInteractionAt: 'next_interaction_at',\n createdAt: 'created_at',\n updatedAt: 'updated_at',\n },\n buildFilters: async (query, ctx) => {\n const advancedFilterTree = consumeAdvancedFilterState(query)\n const filters: Record<string, unknown> = { kind: { $eq: 'company' } }\n if (query.id) filters.id = { $eq: query.id }\n if (query.search) {\n const matchingIds = ctx\n ? await findMatchingEntityIdsBySearchTokensAcrossSources({\n ctx,\n query: query.search,\n sources: [\n {\n entityType: E.customers.customer_entity,\n fields: [\n 'display_name',\n 'primary_email',\n 'primary_phone',\n 'description',\n 'status',\n 'lifecycle_stage',\n 'source',\n 'next_interaction_name',\n ],\n },\n {\n entityType: E.customers.customer_company_profile,\n fields: [\n 'display_name',\n 'primary_email',\n 'primary_phone',\n 'description',\n 'status',\n 'lifecycle_stage',\n 'source',\n 'legal_name',\n 'brand_name',\n 'domain',\n 'website_url',\n 'industry',\n 'size_bucket',\n 'annual_revenue',\n ],\n mapToEntityIds: {\n table: 'customer_companies',\n targetColumn: 'entity_id',\n },\n },\n ],\n })\n : null\n if (matchingIds !== null && matchingIds.length > 0) {\n applyEntityIdRestriction(filters, matchingIds)\n } else {\n const searchPattern = buildIlikeTerm(query.search)\n filters.$or = [\n { display_name: { $ilike: searchPattern } },\n { primary_email: { $ilike: searchPattern } },\n { primary_phone: { $ilike: searchPattern } },\n { description: { $ilike: searchPattern } },\n { next_interaction_name: { $ilike: searchPattern } },\n ]\n }\n }\n if (query.status) {\n filters.status = { $eq: query.status }\n }\n if (query.lifecycleStage) {\n filters.lifecycle_stage = { $eq: query.lifecycleStage }\n }\n if (query.source) {\n filters.source = { $eq: query.source }\n }\n const tagIdsRaw = typeof query.tagIds === 'string' ? query.tagIds : ''\n const tagIds = tagIdsRaw\n .split(',')\n .map((value: string) => value.trim())\n .filter((value: string) => value.length > 0)\n const tagIdsEmpty = parseBooleanToken(query.tagIdsEmpty) === true\n if (tagIdsEmpty) {\n filters.id = { $eq: '00000000-0000-0000-0000-000000000000' }\n } else if (tagIds.length > 0) {\n filters['tag_assignments.tag_id'] = { $in: tagIds }\n }\n const excludedIds = new Set<string>()\n const excludeIdsRaw = typeof query.excludeIds === 'string' ? query.excludeIds : ''\n excludeIdsRaw\n .split(',')\n .map((value: string) => value.trim())\n .filter((value: string) => value.length > 0)\n .forEach((value: string) => excludedIds.add(value))\n if (ctx && query.excludeLinkedPersonId) {\n try {\n const em = ctx.container.resolve('em') as EntityManager\n const decryptionScope = {\n tenantId: ctx.auth?.tenantId ?? null,\n organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,\n }\n const linkWhere = await withActiveCustomerPersonCompanyLinkFilter(\n em,\n withCustomerPersonCompanyLinkScope({ person: query.excludeLinkedPersonId }, decryptionScope),\n 'customers.companies.GET',\n )\n const links = filterActivePersonCompanyLinks(\n await findWithDecryption(\n em,\n CustomerPersonCompanyLink,\n linkWhere,\n { populate: ['company'] },\n decryptionScope,\n ),\n )\n links.forEach((link) => {\n const companyId = link.company?.id\n if (typeof companyId === 'string' && companyId.length > 0) excludedIds.add(companyId)\n })\n } catch (err) {\n console.warn('[customers.companies.list] exclusion lookup failed; falling back to base result set', err)\n }\n }\n if (typeof query.excludeLinkedCompanyId === 'string' && query.excludeLinkedCompanyId.length > 0) {\n excludedIds.add(query.excludeLinkedCompanyId)\n }\n if (ctx && query.excludeLinkedDealId) {\n try {\n const em = ctx.container.resolve('em') as EntityManager\n const decryptionScope = {\n tenantId: ctx.auth?.tenantId ?? null,\n organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,\n }\n const links = await findWithDecryption(\n em,\n CustomerDealCompanyLink,\n withScopedCustomerDealLinkWhere(query.excludeLinkedDealId, decryptionScope),\n { populate: ['company'] },\n decryptionScope,\n )\n links.forEach((link) => {\n const companyId = link.company?.id\n if (typeof companyId === 'string' && companyId.length > 0) excludedIds.add(companyId)\n })\n } catch (err) {\n console.warn('[customers.companies.list] exclusion lookup failed; falling back to base result set', err)\n }\n }\n applyEntityIdExclusion(filters, Array.from(excludedIds))\n const email = typeof query.email === 'string' ? query.email.trim().toLowerCase() : ''\n const emailStartsWith = typeof query.emailStartsWith === 'string' ? query.emailStartsWith.trim().toLowerCase() : ''\n const emailContains = typeof query.emailContains === 'string' ? query.emailContains.trim().toLowerCase() : ''\n if (email) {\n filters.primary_email = { $eq: email }\n } else if (emailStartsWith) {\n filters.primary_email = { $ilike: buildIlikeTerm(emailStartsWith, 'startsWith') }\n } else if (emailContains) {\n filters.primary_email = { $ilike: buildIlikeTerm(emailContains) }\n }\n const hasEmail = parseBooleanToken(query.hasEmail)\n if (!email && !emailStartsWith && !emailContains && hasEmail !== null) {\n filters.primary_email = { $exists: hasEmail }\n }\n const hasPhone = parseBooleanToken(query.hasPhone)\n if (hasPhone !== null) {\n filters.primary_phone = { $exists: hasPhone }\n }\n const hasNextInteraction = parseBooleanToken(query.hasNextInteraction)\n if (hasNextInteraction !== null) {\n filters.next_interaction_at = { $exists: hasNextInteraction }\n }\n const createdRange: Record<string, Date> = {}\n if (query.createdFrom) {\n const from = new Date(query.createdFrom)\n if (!Number.isNaN(from.getTime())) createdRange.$gte = from\n }\n if (query.createdTo) {\n const to = new Date(query.createdTo)\n if (!Number.isNaN(to.getTime())) createdRange.$lte = to\n }\n if (Object.keys(createdRange).length) {\n filters.created_at = createdRange\n }\n if (ctx) {\n try {\n const em = ctx.container.resolve('em') as EntityManager\n const cfFilters = await buildCustomFieldFiltersFromQuery({\n entityIds: [E.customers.customer_entity, E.customers.customer_company_profile],\n query,\n em,\n tenantId: ctx.auth?.tenantId ?? null,\n })\n Object.assign(filters, cfFilters)\n } catch (err) {\n console.warn('[customers.companies.list] custom field filter resolution failed; falling back to base filters', err)\n }\n }\n if (ctx && advancedFilterTree) {\n const advancedFilters = mergeAdvancedFilterTree({ ...filters }, advancedFilterTree)\n const matchedIds = await findMatchingEntityIdsWithQueryEngine({\n ctx,\n entityId: E.customers.customer_entity,\n filters: advancedFilters,\n customFieldSources: [\n {\n entityId: E.customers.customer_company_profile,\n table: 'customer_companies',\n alias: 'company_profile',\n recordIdColumn: 'id',\n join: { fromField: 'id', toField: 'entity_id' },\n },\n ],\n joins: [\n {\n alias: 'tag_assignments',\n table: 'customer_tag_assignments',\n from: { field: 'id' },\n to: { field: 'entity_id' },\n type: 'left',\n },\n ],\n })\n applyEntityIdRestriction(filters, matchedIds)\n }\n return filters\n },\n customFieldSources: [\n {\n entityId: E.customers.customer_company_profile,\n table: 'customer_companies',\n alias: 'company_profile',\n recordIdColumn: 'id',\n join: { fromField: 'id', toField: 'entity_id' },\n },\n ],\n joins: [\n {\n alias: 'tag_assignments',\n table: 'customer_tag_assignments',\n from: { field: 'id' },\n to: { field: 'entity_id' },\n type: 'left',\n },\n ],\n transformItem: (item) => {\n if (!item || typeof item !== 'object') return item\n const record = item as Record<string, unknown>\n const normalized: Record<string, unknown> = { ...record }\n delete normalized.kind\n const cfEntries = extractAllCustomFieldEntries(record)\n for (const key of Object.keys(normalized)) {\n if (key.startsWith('cf:')) {\n delete normalized[key]\n }\n }\n return { ...normalized, ...cfEntries }\n },\n },\n actions: {\n create: {\n commandId: 'customers.companies.create',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n const scoped = withScopedPayload(raw ?? {}, ctx, translate)\n const { base, custom } = splitCustomFieldPayload(scoped)\n const parsed = companyCreateSchema.parse(base)\n return Object.keys(custom).length ? { ...parsed, customFields: custom } : parsed\n },\n response: ({ result }) => ({\n id: result?.entityId ?? result?.id ?? null,\n companyId: result?.companyId ?? null,\n }),\n status: 201,\n },\n update: {\n commandId: 'customers.companies.update',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n const scoped = withScopedPayload(raw ?? {}, ctx, translate)\n const normalized = normalizeCompanyProfilePayload(scoped, translate)\n const { base, custom } = splitCustomFieldPayload(normalized)\n const parsed = companyUpdateSchema.parse(base)\n return Object.keys(custom).length ? { ...parsed, customFields: custom } : parsed\n },\n // Return the freshly-bumped updatedAt so inline-edit detail pages can refresh\n // their optimistic-lock token between sequential saves (avoids a false 409 on\n // the 2nd inline edit now that locking is default-ON). #2055. The factory hands\n // the updated entity here; read defensively in case a `{ result }` wrapper arrives.\n response: (arg: { result?: unknown; updatedAt?: Date | string | null }) => {\n const raw = arg?.updatedAt\n ?? (arg?.result as { updatedAt?: Date | string | null } | null | undefined)?.updatedAt\n ?? null\n return {\n ok: true,\n updatedAt: raw instanceof Date ? raw.toISOString() : (typeof raw === 'string' ? raw : null),\n }\n },\n },\n delete: {\n commandId: 'customers.companies.delete',\n schema: rawBodySchema,\n mapInput: async ({ parsed, ctx }) => {\n const { translate } = await resolveTranslations()\n const id =\n parsed?.body?.id ??\n parsed?.id ??\n parsed?.query?.id ??\n (ctx.request ? new URL(ctx.request.url).searchParams.get('id') : null)\n if (!id) throw new CrudHttpError(400, { error: translate('customers.errors.company_required', 'Company id is required') })\n return { id }\n },\n response: () => ({ ok: true }),\n },\n },\n hooks: {\n afterList: async (payload, ctx) => {\n const items = Array.isArray(payload?.items) ? payload.items : []\n const ids = items\n .map((item: unknown) => (item && typeof item === 'object' && typeof (item as Record<string, unknown>).id === 'string'\n ? (item as Record<string, unknown>).id as string\n : null))\n .filter((id: string | null): id is string => typeof id === 'string' && id.length > 0)\n if (!ids.length) return\n\n const where: Record<string, unknown> = {\n entity: { $in: ids },\n tenantId: ctx.auth?.tenantId ?? null,\n }\n if (ctx.selectedOrganizationId) {\n where.organizationId = ctx.selectedOrganizationId\n }\n\n const profiles = await findWithDecryption(\n ctx.container.resolve('em') as EntityManager,\n CustomerCompanyProfile,\n where as FilterQuery<CustomerCompanyProfile>,\n { populate: ['entity'] },\n {\n tenantId: ctx.auth?.tenantId ?? null,\n organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,\n },\n )\n\n const profilesByEntityId = new Map<string, CustomerCompanyProfile>()\n for (const profile of profiles) {\n const profileEntity = (profile as { entity?: { id?: unknown } }).entity\n const entityId = typeof profileEntity?.id === 'string' ? profileEntity.id : null\n if (entityId) profilesByEntityId.set(entityId, profile)\n }\n\n payload.items = items.map((item: unknown) => {\n if (!item || typeof item !== 'object') return item\n const record = item as Record<string, unknown>\n const profile = typeof record.id === 'string' ? profilesByEntityId.get(record.id) : undefined\n if (!profile) return item\n return {\n ...record,\n legal_name: profile.legalName ?? null,\n brand_name: profile.brandName ?? null,\n domain: profile.domain ?? null,\n website_url: profile.websiteUrl ?? null,\n industry: profile.industry ?? null,\n size_bucket: profile.sizeBucket ?? null,\n annual_revenue: profile.annualRevenue ?? null,\n }\n })\n },\n },\n})\n\nconst { POST, PUT, DELETE } = crud\n\nexport { POST, PUT, DELETE }\nexport const GET = crud.GET\n\nconst companyListItemSchema = z.object({\n id: z.string().uuid(),\n display_name: z.string().optional(),\n description: z.string().nullable().optional(),\n owner_user_id: z.string().uuid().nullable().optional(),\n primary_email: z.string().nullable().optional(),\n primary_phone: z.string().nullable().optional(),\n status: z.string().nullable().optional(),\n lifecycle_stage: z.string().nullable().optional(),\n source: z.string().nullable().optional(),\n next_interaction_at: z.string().nullable().optional(),\n next_interaction_name: z.string().nullable().optional(),\n next_interaction_ref_id: z.string().nullable().optional(),\n next_interaction_icon: z.string().nullable().optional(),\n next_interaction_color: z.string().nullable().optional(),\n organization_id: z.string().uuid().nullable().optional(),\n tenant_id: z.string().uuid().nullable().optional(),\n created_at: z.string().nullable().optional(),\n})\n\nconst companyCreateResponseSchema = z.object({\n id: z.string().uuid().nullable(),\n companyId: z.string().uuid().nullable(),\n})\n\nexport const openApi = createCustomersCrudOpenApi({\n resourceName: 'Company',\n pluralName: 'Companies',\n querySchema: listSchema,\n listResponseSchema: createPagedListResponseSchema(companyListItemSchema),\n create: {\n schema: companyCreateSchema,\n responseSchema: companyCreateResponseSchema,\n description: 'Creates a company record and associated profile data.',\n },\n update: {\n schema: companyUpdateSchema,\n responseSchema: defaultOkResponseSchema,\n description: 'Updates company profile fields, tags, or custom attributes.',\n },\n del: {\n schema: z.object({ id: z.string().uuid() }),\n responseSchema: defaultOkResponseSchema,\n description: 'Deletes a company by id. The identifier can be provided via body or query.',\n errors: [\n {\n status: 422,\n description: 'Company has dependent records (people, deals, or direct staff); unlink or reassign before delete.',\n schema: z.object({\n error: z.string(),\n code: z.literal('COMPANY_HAS_DEPENDENTS'),\n }),\n },\n ],\n },\n})\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -24,6 +24,7 @@ import { consumeAdvancedFilterState, mergeAdvancedFilterTree } from "@open-merca
24
24
  import { fetchStuckDealIds } from "../../lib/stuckDeals.js";
25
25
  const rawBodySchema = z.object({}).passthrough();
26
26
  const stringOrStringArray = z.union([z.string(), z.array(z.string())]);
27
+ const OPEN_DEAL_STATUSES = ["open", "in_progress"];
27
28
  const booleanQueryParam = z.preprocess((value) => {
28
29
  const parsed = parseBooleanFromUnknown(value);
29
30
  return parsed === null ? value : parsed;
@@ -42,6 +43,7 @@ const dealListQuerySchema = z.object({
42
43
  expectedCloseAtTo: z.string().optional(),
43
44
  isStuck: booleanQueryParam,
44
45
  isOverdue: booleanQueryParam,
46
+ needsAttention: booleanQueryParam,
45
47
  valueCurrency: stringOrStringArray.optional(),
46
48
  sortField: z.string().optional(),
47
49
  sortDir: z.enum(["asc", "desc"]).optional(),
@@ -119,6 +121,36 @@ async function fetchDealIdsMatchingAssociations(em, organizationId, tenantId, pe
119
121
  );
120
122
  return rows.map((row) => row.id);
121
123
  }
124
+ async function fetchNeedAttentionDealIds(em, organizationId, tenantId) {
125
+ const connection = em.getConnection();
126
+ const overdueRows = await connection.execute(
127
+ `SELECT id FROM customer_deals
128
+ WHERE organization_id = ?
129
+ AND tenant_id = ?
130
+ AND deleted_at IS NULL
131
+ AND status = 'open'
132
+ AND expected_close_at IS NOT NULL
133
+ AND expected_close_at < CURRENT_DATE`,
134
+ [organizationId, tenantId]
135
+ );
136
+ const attentionIds = new Set(overdueRows.map((row) => row.id));
137
+ const stuckIds = await fetchStuckDealIds(em, organizationId, tenantId);
138
+ if (stuckIds.length > 0) {
139
+ const idPlaceholders = stuckIds.map(() => "?").join(",");
140
+ const statusPlaceholders = OPEN_DEAL_STATUSES.map(() => "?").join(",");
141
+ const openStuckRows = await connection.execute(
142
+ `SELECT id FROM customer_deals
143
+ WHERE organization_id = ?
144
+ AND tenant_id = ?
145
+ AND deleted_at IS NULL
146
+ AND status IN (${statusPlaceholders})
147
+ AND id IN (${idPlaceholders})`,
148
+ [organizationId, tenantId, ...OPEN_DEAL_STATUSES, ...stuckIds]
149
+ );
150
+ for (const row of openStuckRows) attentionIds.add(row.id);
151
+ }
152
+ return Array.from(attentionIds);
153
+ }
122
154
  function normalizeCurrencyList(value) {
123
155
  const set = /* @__PURE__ */ new Set();
124
156
  const visit = (entry) => {
@@ -233,7 +265,7 @@ async function buildDealListFilters(query, ctx) {
233
265
  if (expectedCloseTo) range.$lte = expectedCloseTo;
234
266
  filters.expected_close_at = range;
235
267
  }
236
- if (query.isOverdue) {
268
+ if (query.isOverdue && !query.needsAttention) {
237
269
  const today = /* @__PURE__ */ new Date();
238
270
  today.setHours(0, 0, 0, 0);
239
271
  if (statusList.length === 0) {
@@ -243,7 +275,7 @@ async function buildDealListFilters(query, ctx) {
243
275
  existingRange.$lt = today;
244
276
  filters.expected_close_at = existingRange;
245
277
  }
246
- if (query.isStuck && ctx) {
278
+ if (query.isStuck && !query.needsAttention && ctx) {
247
279
  const tenantId = ctx.auth?.tenantId;
248
280
  const organizationId = ctx.auth?.orgId;
249
281
  if (typeof tenantId === "string" && typeof organizationId === "string") {
@@ -252,6 +284,15 @@ async function buildDealListFilters(query, ctx) {
252
284
  intersectIds(stuckIds);
253
285
  }
254
286
  }
287
+ if (query.needsAttention && ctx) {
288
+ const tenantId = ctx.auth?.tenantId;
289
+ const organizationId = ctx.auth?.orgId;
290
+ if (typeof tenantId === "string" && typeof organizationId === "string") {
291
+ const em = ctx.container.resolve("em");
292
+ const attentionIds = await fetchNeedAttentionDealIds(em, organizationId, tenantId);
293
+ intersectIds(attentionIds);
294
+ }
295
+ }
255
296
  const url = ctx?.request ? new URL(ctx.request.url) : null;
256
297
  const personCandidates = [query.personId, query.personEntityId];
257
298
  const companyCandidates = [query.companyId, query.companyEntityId];