@open-mercato/core 0.7.0 → 0.7.1-develop.7102.1.b41f7e3e51
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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/helpers/integration/api.js +13 -1
- package/dist/helpers/integration/api.js.map +2 -2
- package/dist/helpers/integration/communicationChannelsFixtures.js +19 -0
- package/dist/helpers/integration/communicationChannelsFixtures.js.map +2 -2
- package/dist/modules/api_keys/api/keys/route.js +1 -1
- package/dist/modules/api_keys/api/keys/route.js.map +2 -2
- package/dist/modules/api_keys/backend/api-keys/page.js +3 -1
- package/dist/modules/api_keys/backend/api-keys/page.js.map +2 -2
- package/dist/modules/api_keys/di.js +9 -0
- package/dist/modules/api_keys/di.js.map +7 -0
- package/dist/modules/api_keys/services/principalService.js +31 -0
- package/dist/modules/api_keys/services/principalService.js.map +7 -0
- package/dist/modules/attachments/api/route.js +2 -2
- package/dist/modules/attachments/api/route.js.map +2 -2
- package/dist/modules/attachments/components/AttachmentLibrary.js +2 -0
- package/dist/modules/attachments/components/AttachmentLibrary.js.map +2 -2
- package/dist/modules/attachments/di.js +11 -1
- package/dist/modules/attachments/di.js.map +2 -2
- package/dist/modules/attachments/index.js.map +1 -1
- package/dist/modules/attachments/lib/attachment-service.js +313 -0
- package/dist/modules/attachments/lib/attachment-service.js.map +7 -0
- package/dist/modules/attachments/lib/scoped-upload-service.js +15 -2
- package/dist/modules/attachments/lib/scoped-upload-service.js.map +2 -2
- package/dist/modules/attachments/lib/upload-limits.js +6 -2
- package/dist/modules/attachments/lib/upload-limits.js.map +2 -2
- package/dist/modules/audit_logs/backend/audit-logs/page.js +6 -0
- package/dist/modules/audit_logs/backend/audit-logs/page.js.map +2 -2
- package/dist/modules/audit_logs/lib/display-helpers.js +19 -11
- package/dist/modules/audit_logs/lib/display-helpers.js.map +2 -2
- package/dist/modules/audit_logs/services/actionLogService.js +57 -12
- package/dist/modules/audit_logs/services/actionLogService.js.map +3 -3
- package/dist/modules/auth/acl.js +30 -5
- package/dist/modules/auth/acl.js.map +2 -2
- package/dist/modules/auth/api/feature-check.js +8 -2
- package/dist/modules/auth/api/feature-check.js.map +2 -2
- package/dist/modules/auth/api/login.js +4 -1
- package/dist/modules/auth/api/login.js.map +2 -2
- package/dist/modules/auth/api/reset/validate.js +55 -0
- package/dist/modules/auth/api/reset/validate.js.map +7 -0
- package/dist/modules/auth/api/roles/acl/route.js +28 -33
- package/dist/modules/auth/api/roles/acl/route.js.map +2 -2
- package/dist/modules/auth/api/session/refresh.js +25 -1
- package/dist/modules/auth/api/session/refresh.js.map +2 -2
- package/dist/modules/auth/api/users/acl/route.js +96 -51
- package/dist/modules/auth/api/users/acl/route.js.map +2 -2
- package/dist/modules/auth/api/users/route.js +13 -5
- package/dist/modules/auth/api/users/route.js.map +2 -2
- package/dist/modules/auth/backend/roles/[id]/edit/page.meta.js +5 -1
- package/dist/modules/auth/backend/roles/[id]/edit/page.meta.js.map +2 -2
- package/dist/modules/auth/backend/roles/page.js +3 -1
- package/dist/modules/auth/backend/roles/page.js.map +2 -2
- package/dist/modules/auth/backend/users/[id]/edit/page.meta.js +5 -1
- package/dist/modules/auth/backend/users/[id]/edit/page.meta.js.map +2 -2
- package/dist/modules/auth/backend/users/page.js +2 -1
- package/dist/modules/auth/backend/users/page.js.map +2 -2
- package/dist/modules/auth/commands/acl.js +254 -0
- package/dist/modules/auth/commands/acl.js.map +7 -0
- package/dist/modules/auth/components/AclEditor.js +39 -30
- package/dist/modules/auth/components/AclEditor.js.map +2 -2
- package/dist/modules/auth/data/entities.js.map +2 -2
- package/dist/modules/auth/data/validators.js +5 -1
- package/dist/modules/auth/data/validators.js.map +2 -2
- package/dist/modules/auth/di.js +35 -9
- package/dist/modules/auth/di.js.map +2 -2
- package/dist/modules/auth/frontend/reset/[token]/page.js +46 -0
- package/dist/modules/auth/frontend/reset/[token]/page.js.map +2 -2
- package/dist/modules/auth/lib/grantChecks.js.map +2 -2
- package/dist/modules/auth/lib/userIdFilter.js +16 -0
- package/dist/modules/auth/lib/userIdFilter.js.map +7 -0
- package/dist/modules/auth/services/authService.js +16 -2
- package/dist/modules/auth/services/authService.js.map +2 -2
- package/dist/modules/auth/services/principalService.js +200 -0
- package/dist/modules/auth/services/principalService.js.map +7 -0
- package/dist/modules/auth/services/rbacService.js +67 -24
- package/dist/modules/auth/services/rbacService.js.map +3 -3
- package/dist/modules/auth/services/roleOrganizationScope.js +41 -0
- package/dist/modules/auth/services/roleOrganizationScope.js.map +7 -0
- package/dist/modules/business_rules/backend/logs/page.js +3 -1
- package/dist/modules/business_rules/backend/logs/page.js.map +2 -2
- package/dist/modules/business_rules/backend/rules/page.js +3 -1
- package/dist/modules/business_rules/backend/rules/page.js.map +2 -2
- package/dist/modules/business_rules/backend/sets/page.js +3 -1
- package/dist/modules/business_rules/backend/sets/page.js.map +2 -2
- package/dist/modules/business_rules/lib/expression-evaluator.js +9 -9
- package/dist/modules/business_rules/lib/expression-evaluator.js.map +2 -2
- package/dist/modules/business_rules/lib/rule-evaluator.js +10 -9
- package/dist/modules/business_rules/lib/rule-evaluator.js.map +2 -2
- package/dist/modules/catalog/ai-agents.js +1 -1
- package/dist/modules/catalog/ai-agents.js.map +1 -1
- package/dist/modules/catalog/ai-tools/merchandising-pack.js +2 -2
- package/dist/modules/catalog/ai-tools/merchandising-pack.js.map +2 -2
- package/dist/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.js +5 -2
- package/dist/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.js.map +2 -2
- package/dist/modules/catalog/components/categories/CategoriesDataTable.js +2 -0
- package/dist/modules/catalog/components/categories/CategoriesDataTable.js.map +2 -2
- package/dist/modules/catalog/components/products/ProductsDataTable.js +7 -1
- package/dist/modules/catalog/components/products/ProductsDataTable.js.map +2 -2
- package/dist/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.client.js +2 -0
- package/dist/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.client.js.map +2 -2
- package/dist/modules/communication_channels/api/post/channels/[id]/test-send/route.js +9 -1
- package/dist/modules/communication_channels/api/post/channels/[id]/test-send/route.js.map +2 -2
- package/dist/modules/communication_channels/api/post/test-seed/route.js +95 -10
- package/dist/modules/communication_channels/api/post/test-seed/route.js.map +2 -2
- package/dist/modules/communication_channels/backend/communication_channels/channels/page.js +3 -0
- package/dist/modules/communication_channels/backend/communication_channels/channels/page.js.map +2 -2
- package/dist/modules/communication_channels/commands/ingest-inbound-message.js +14 -1
- package/dist/modules/communication_channels/commands/ingest-inbound-message.js.map +2 -2
- package/dist/modules/communication_channels/di.js +7 -1
- package/dist/modules/communication_channels/di.js.map +2 -2
- package/dist/modules/communication_channels/lib/email-capabilities.js +4 -1
- package/dist/modules/communication_channels/lib/email-capabilities.js.map +2 -2
- package/dist/modules/communication_channels/lib/outbound-recipient.js +33 -0
- package/dist/modules/communication_channels/lib/outbound-recipient.js.map +7 -0
- package/dist/modules/communication_channels/lib/resolve-channel-type.js +51 -0
- package/dist/modules/communication_channels/lib/resolve-channel-type.js.map +7 -0
- package/dist/modules/communication_channels/lib/test-seed.js +41 -2
- package/dist/modules/communication_channels/lib/test-seed.js.map +2 -2
- package/dist/modules/configs/lib/system-status.js +9 -0
- package/dist/modules/configs/lib/system-status.js.map +2 -2
- package/dist/modules/currencies/backend/currencies/page.js +3 -1
- package/dist/modules/currencies/backend/currencies/page.js.map +2 -2
- package/dist/modules/currencies/backend/exchange-rates/page.js +3 -1
- package/dist/modules/currencies/backend/exchange-rates/page.js.map +2 -2
- package/dist/modules/currencies/services/rateFetchingService.js +6 -1
- package/dist/modules/currencies/services/rateFetchingService.js.map +2 -2
- package/dist/modules/customer_accounts/api/admin/domain-mappings.js +9 -0
- package/dist/modules/customer_accounts/api/admin/domain-mappings.js.map +2 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/roles/page.js +3 -1
- package/dist/modules/customer_accounts/backend/customer_accounts/roles/page.js.map +2 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.js +3 -1
- package/dist/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.js.map +2 -2
- package/dist/modules/customer_accounts/services/domainMappingService.js +10 -0
- package/dist/modules/customer_accounts/services/domainMappingService.js.map +2 -2
- package/dist/modules/customers/ai-tools/companies-pack.js +4 -1
- package/dist/modules/customers/ai-tools/companies-pack.js.map +2 -2
- package/dist/modules/customers/ai-tools/deal-analyzer-pack.js +13 -2
- package/dist/modules/customers/ai-tools/deal-analyzer-pack.js.map +2 -2
- package/dist/modules/customers/ai-tools/deals-pack.js +52 -6
- package/dist/modules/customers/ai-tools/deals-pack.js.map +2 -2
- package/dist/modules/customers/ai-tools/people-pack.js +4 -1
- package/dist/modules/customers/ai-tools/people-pack.js.map +2 -2
- package/dist/modules/customers/api/activities/route.js +48 -6
- package/dist/modules/customers/api/activities/route.js.map +2 -2
- package/dist/modules/customers/api/companies/[id]/people/route.js +20 -64
- package/dist/modules/customers/api/companies/[id]/people/route.js.map +2 -2
- package/dist/modules/customers/api/companies/[id]/route.js +17 -94
- package/dist/modules/customers/api/companies/[id]/route.js.map +2 -2
- package/dist/modules/customers/api/dashboard/widgets/customer-todos/route.js +8 -5
- package/dist/modules/customers/api/dashboard/widgets/customer-todos/route.js.map +2 -2
- package/dist/modules/customers/api/deals/[id]/companies/route.js +1 -1
- package/dist/modules/customers/api/deals/[id]/companies/route.js.map +2 -2
- package/dist/modules/customers/api/deals/[id]/people/route.js +1 -1
- package/dist/modules/customers/api/deals/[id]/people/route.js.map +2 -2
- package/dist/modules/customers/api/deals/[id]/route.js +8 -25
- package/dist/modules/customers/api/deals/[id]/route.js.map +2 -2
- package/dist/modules/customers/api/deals/[id]/stats/route.js +3 -22
- package/dist/modules/customers/api/deals/[id]/stats/route.js.map +2 -2
- package/dist/modules/customers/api/deals/aggregate/route.js +11 -4
- package/dist/modules/customers/api/deals/aggregate/route.js.map +2 -2
- package/dist/modules/customers/api/deals/map/route.js +2 -1
- package/dist/modules/customers/api/deals/map/route.js.map +2 -2
- package/dist/modules/customers/api/deals/route.js +38 -1
- package/dist/modules/customers/api/deals/route.js.map +2 -2
- package/dist/modules/customers/api/entity-roles-factory.js +4 -4
- package/dist/modules/customers/api/entity-roles-factory.js.map +2 -2
- package/dist/modules/customers/api/interactions/route.js +3 -1
- package/dist/modules/customers/api/interactions/route.js.map +2 -2
- package/dist/modules/customers/api/interactions/tasks/route.js +27 -4
- package/dist/modules/customers/api/interactions/tasks/route.js.map +2 -2
- package/dist/modules/customers/api/people/[id]/companies/[linkId]/route.js +4 -3
- package/dist/modules/customers/api/people/[id]/companies/[linkId]/route.js.map +2 -2
- package/dist/modules/customers/api/people/[id]/companies/context.js +1 -1
- package/dist/modules/customers/api/people/[id]/companies/context.js.map +2 -2
- package/dist/modules/customers/api/people/[id]/companies/enriched/route.js +1 -1
- package/dist/modules/customers/api/people/[id]/companies/enriched/route.js.map +2 -2
- package/dist/modules/customers/api/people/[id]/route.js +11 -10
- package/dist/modules/customers/api/people/[id]/route.js.map +2 -2
- package/dist/modules/customers/api/people/check-phone/route.js +51 -9
- package/dist/modules/customers/api/people/check-phone/route.js.map +2 -2
- package/dist/modules/customers/api/todos/route.js +30 -5
- package/dist/modules/customers/api/todos/route.js.map +2 -2
- package/dist/modules/customers/backend/customers/companies/page.js +4 -2
- package/dist/modules/customers/backend/customers/companies/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/page.js +6 -1
- package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/page.js +5 -2
- package/dist/modules/customers/backend/customers/deals/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/components/Lane.js +4 -13
- package/dist/modules/customers/backend/customers/deals/pipeline/components/Lane.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/components/StatusFilterPopover.js +85 -14
- package/dist/modules/customers/backend/customers/deals/pipeline/components/StatusFilterPopover.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/components/toneClasses.js +18 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/toneClasses.js.map +7 -0
- package/dist/modules/customers/backend/customers/people/page.js +4 -2
- package/dist/modules/customers/backend/customers/people/page.js.map +2 -2
- package/dist/modules/customers/backend/hooks/useEmailDuplicateCheck.js +9 -3
- package/dist/modules/customers/backend/hooks/useEmailDuplicateCheck.js.map +2 -2
- package/dist/modules/customers/commands/deals.js +21 -34
- package/dist/modules/customers/commands/deals.js.map +2 -2
- package/dist/modules/customers/commands/interactions.js.map +1 -1
- package/dist/modules/customers/commands/personCompanyLinks.js +131 -2
- package/dist/modules/customers/commands/personCompanyLinks.js.map +2 -2
- package/dist/modules/customers/components/CustomerTodosTable.js +1 -0
- package/dist/modules/customers/components/CustomerTodosTable.js.map +2 -2
- package/dist/modules/customers/components/DealsKpiStrip.js +7 -6
- package/dist/modules/customers/components/DealsKpiStrip.js.map +2 -2
- package/dist/modules/customers/components/calendar/AgendaList.js +2 -2
- package/dist/modules/customers/components/calendar/AgendaList.js.map +2 -2
- package/dist/modules/customers/components/calendar/CalendarEventEditor.js +1 -1
- package/dist/modules/customers/components/calendar/CalendarEventEditor.js.map +2 -2
- package/dist/modules/customers/components/calendar/CalendarScreen.js +1 -1
- package/dist/modules/customers/components/calendar/CalendarScreen.js.map +2 -2
- package/dist/modules/customers/components/calendar/CalendarSettingsModal.js +1 -1
- package/dist/modules/customers/components/calendar/CalendarSettingsModal.js.map +2 -2
- package/dist/modules/customers/components/calendar/EventBlock.js +1 -1
- package/dist/modules/customers/components/calendar/EventBlock.js.map +2 -2
- package/dist/modules/customers/components/calendar/EventPeekPopover.js +3 -2
- package/dist/modules/customers/components/calendar/EventPeekPopover.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/PeopleField.js +40 -5
- package/dist/modules/customers/components/calendar/editor/PeopleField.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/hooks.js +2 -1
- package/dist/modules/customers/components/calendar/editor/hooks.js.map +2 -2
- package/dist/modules/customers/components/calendar/types.js +1 -1
- package/dist/modules/customers/components/calendar/types.js.map +2 -2
- package/dist/modules/customers/components/detail/CompanyKpiBar.js +5 -4
- package/dist/modules/customers/components/detail/CompanyKpiBar.js.map +2 -2
- package/dist/modules/customers/components/detail/CompanyPeopleSection.js +3 -1
- package/dist/modules/customers/components/detail/CompanyPeopleSection.js.map +2 -2
- package/dist/modules/customers/components/detail/DealForm.js +19 -7
- package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
- package/dist/modules/customers/components/detail/DealsSection.js +6 -5
- package/dist/modules/customers/components/detail/DealsSection.js.map +2 -2
- package/dist/modules/customers/components/detail/EntityTagsDialog.js +35 -11
- package/dist/modules/customers/components/detail/EntityTagsDialog.js.map +2 -2
- package/dist/modules/customers/components/detail/PersonCompaniesSection.js +3 -1
- package/dist/modules/customers/components/detail/PersonCompaniesSection.js.map +2 -2
- package/dist/modules/customers/components/detail/hooks/usePersonTasks.js +19 -13
- package/dist/modules/customers/components/detail/hooks/usePersonTasks.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/ParticipantsField.js +4 -4
- package/dist/modules/customers/components/detail/schedule/ParticipantsField.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js +3 -3
- package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js.map +2 -2
- package/dist/modules/customers/components/detail/utils.js +5 -5
- package/dist/modules/customers/components/detail/utils.js.map +2 -2
- package/dist/modules/customers/data/entities.js.map +1 -1
- package/dist/modules/customers/data/validators.js +32 -3
- package/dist/modules/customers/data/validators.js.map +2 -2
- package/dist/modules/customers/events.js +5 -0
- package/dist/modules/customers/events.js.map +2 -2
- package/dist/modules/customers/lib/calendar/conflicts.js +8 -2
- package/dist/modules/customers/lib/calendar/conflicts.js.map +2 -2
- package/dist/modules/customers/lib/calendar/editorPayload.js +3 -2
- package/dist/modules/customers/lib/calendar/editorPayload.js.map +2 -2
- package/dist/modules/customers/lib/calendar/mapItem.js +8 -3
- package/dist/modules/customers/lib/calendar/mapItem.js.map +2 -2
- package/dist/modules/customers/lib/calendar/participantIdentity.js +9 -0
- package/dist/modules/customers/lib/calendar/participantIdentity.js.map +7 -0
- package/dist/modules/customers/lib/closureStage.js +45 -0
- package/dist/modules/customers/lib/closureStage.js.map +7 -0
- package/dist/modules/customers/lib/dealStatus.js +30 -0
- package/dist/modules/customers/lib/dealStatus.js.map +2 -2
- package/dist/modules/customers/lib/detailReadAccess.js +12 -0
- package/dist/modules/customers/lib/detailReadAccess.js.map +7 -0
- package/dist/modules/customers/lib/findPeopleByAddresses.js +1 -0
- package/dist/modules/customers/lib/findPeopleByAddresses.js.map +2 -2
- package/dist/modules/customers/lib/personCompanies.js +100 -2
- package/dist/modules/customers/lib/personCompanies.js.map +2 -2
- package/dist/modules/customers/lib/todoCompatibility.js +12 -0
- package/dist/modules/customers/lib/todoCompatibility.js.map +2 -2
- package/dist/modules/dashboards/lib/widgetScope.js +6 -1
- package/dist/modules/dashboards/lib/widgetScope.js.map +2 -2
- package/dist/modules/data_sync/backend/data-sync/page.js +3 -1
- package/dist/modules/data_sync/backend/data-sync/page.js.map +2 -2
- package/dist/modules/data_sync/lib/abandoned-run.js +19 -0
- package/dist/modules/data_sync/lib/abandoned-run.js.map +7 -0
- package/dist/modules/data_sync/lib/sync-engine.js +72 -6
- package/dist/modules/data_sync/lib/sync-engine.js.map +2 -2
- package/dist/modules/data_sync/workers/sync-export.js +5 -1
- package/dist/modules/data_sync/workers/sync-export.js.map +2 -2
- package/dist/modules/data_sync/workers/sync-import.js +5 -1
- package/dist/modules/data_sync/workers/sync-import.js.map +2 -2
- package/dist/modules/devices/acl.js +9 -1
- package/dist/modules/devices/acl.js.map +2 -2
- package/dist/modules/devices/api/admin/devices/[id]/route.js +5 -32
- package/dist/modules/devices/api/admin/devices/[id]/route.js.map +2 -2
- package/dist/modules/devices/api/admin/devices/route.js +14 -1
- package/dist/modules/devices/api/admin/devices/route.js.map +2 -2
- package/dist/modules/devices/api/deviceList.js +99 -12
- package/dist/modules/devices/api/deviceList.js.map +2 -2
- package/dist/modules/devices/api/deviceSerialization.js +46 -0
- package/dist/modules/devices/api/deviceSerialization.js.map +7 -0
- package/dist/modules/devices/api/openapi.js +3 -1
- package/dist/modules/devices/api/openapi.js.map +2 -2
- package/dist/modules/devices/api/route.js +9 -1
- package/dist/modules/devices/api/route.js.map +2 -2
- package/dist/modules/devices/backend/devices/[id]/page.js +10 -26
- package/dist/modules/devices/backend/devices/[id]/page.js.map +2 -2
- package/dist/modules/devices/backend/devices/create/page.js +17 -1
- package/dist/modules/devices/backend/devices/create/page.js.map +2 -2
- package/dist/modules/devices/backend/devices/page.js +28 -34
- package/dist/modules/devices/backend/devices/page.js.map +2 -2
- package/dist/modules/devices/backend/devices/useDeviceUserLabels.js +34 -0
- package/dist/modules/devices/backend/devices/useDeviceUserLabels.js.map +7 -0
- package/dist/modules/devices/backend/devices/userOptions.js +52 -0
- package/dist/modules/devices/backend/devices/userOptions.js.map +7 -0
- package/dist/modules/devices/setup.js +6 -2
- package/dist/modules/devices/setup.js.map +2 -2
- package/dist/modules/directory/backend/directory/organizations/page.js +2 -1
- package/dist/modules/directory/backend/directory/organizations/page.js.map +2 -2
- package/dist/modules/directory/backend/directory/tenants/page.js +2 -1
- package/dist/modules/directory/backend/directory/tenants/page.js.map +2 -2
- package/dist/modules/directory/di.js +9 -0
- package/dist/modules/directory/di.js.map +2 -2
- package/dist/modules/directory/services/organizationHierarchyService.js +29 -0
- package/dist/modules/directory/services/organizationHierarchyService.js.map +7 -0
- package/dist/modules/directory/services/organizationScopeService.js +56 -0
- package/dist/modules/directory/services/organizationScopeService.js.map +7 -0
- package/dist/modules/directory/utils/organizationScope.js.map +2 -2
- package/dist/modules/entities/api/records.js +4 -2
- package/dist/modules/entities/api/records.js.map +2 -2
- package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js +3 -1
- package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js.map +2 -2
- package/dist/modules/eudr/api/plots/route.js +5 -1
- package/dist/modules/eudr/api/plots/route.js.map +2 -2
- package/dist/modules/eudr/backend/eudr/evidence-submissions/page.js +3 -0
- package/dist/modules/eudr/backend/eudr/evidence-submissions/page.js.map +2 -2
- package/dist/modules/eudr/backend/eudr/plots/page.js +3 -0
- package/dist/modules/eudr/backend/eudr/plots/page.js.map +2 -2
- package/dist/modules/eudr/backend/eudr/product-mappings/page.js +3 -0
- package/dist/modules/eudr/backend/eudr/product-mappings/page.js.map +2 -2
- package/dist/modules/eudr/backend/eudr/risk-assessments/page.js +3 -0
- package/dist/modules/eudr/backend/eudr/risk-assessments/page.js.map +2 -2
- package/dist/modules/eudr/backend/eudr/statements/page.js +3 -0
- package/dist/modules/eudr/backend/eudr/statements/page.js.map +2 -2
- package/dist/modules/eudr/commands/statements.js +0 -3
- package/dist/modules/eudr/commands/statements.js.map +2 -2
- package/dist/modules/eudr/data/validators.js +4 -1
- package/dist/modules/eudr/data/validators.js.map +2 -2
- package/dist/modules/feature_toggles/components/FeatureTogglesTable.js +1 -0
- package/dist/modules/feature_toggles/components/FeatureTogglesTable.js.map +2 -2
- package/dist/modules/feature_toggles/components/OverridesTable.js +1 -0
- package/dist/modules/feature_toggles/components/OverridesTable.js.map +2 -2
- package/dist/modules/feature_toggles/defaults.json +0 -8
- package/dist/modules/inbox_ops/backend/inbox-ops/log/page.js +3 -0
- package/dist/modules/inbox_ops/backend/inbox-ops/log/page.js.map +2 -2
- package/dist/modules/inbox_ops/backend/inbox-ops/page.js +3 -0
- package/dist/modules/inbox_ops/backend/inbox-ops/page.js.map +2 -2
- package/dist/modules/inbox_ops/lib/htmlToPlainText.js +1 -12
- package/dist/modules/inbox_ops/lib/htmlToPlainText.js.map +2 -2
- package/dist/modules/integrations/api/[id]/credentials/route.js +6 -1
- package/dist/modules/integrations/api/[id]/credentials/route.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/[id]/page.js +38 -29
- package/dist/modules/integrations/backend/integrations/[id]/page.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js +36 -9
- package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/credential-secret-fields.js +55 -0
- package/dist/modules/integrations/backend/integrations/credential-secret-fields.js.map +7 -0
- package/dist/modules/integrations/data/validators.js +11 -3
- package/dist/modules/integrations/data/validators.js.map +2 -2
- package/dist/modules/integrations/lib/credentials-masking.js +6 -2
- package/dist/modules/integrations/lib/credentials-masking.js.map +2 -2
- package/dist/modules/messages/api/[id]/route.js +29 -1
- package/dist/modules/messages/api/[id]/route.js.map +2 -2
- package/dist/modules/messages/api/openapi.js +17 -2
- package/dist/modules/messages/api/openapi.js.map +2 -2
- package/dist/modules/messages/api/route.js +17 -2
- package/dist/modules/messages/api/route.js.map +2 -2
- package/dist/modules/messages/components/MessagesInboxPageClient.js +7 -14
- package/dist/modules/messages/components/MessagesInboxPageClient.js.map +2 -2
- package/dist/modules/messages/components/inboxFilters.js +15 -0
- package/dist/modules/messages/components/inboxFilters.js.map +2 -2
- package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsConversation.js +11 -1
- package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsConversation.js.map +2 -2
- package/dist/modules/messages/components/message-detail/panels/MessageHeader.js +3 -1
- package/dist/modules/messages/components/message-detail/panels/MessageHeader.js.map +2 -2
- package/dist/modules/messages/components/message-detail/panels/thread-panel.js +2 -1
- package/dist/modules/messages/components/message-detail/panels/thread-panel.js.map +2 -2
- package/dist/modules/messages/components/messageListLabels.js +14 -2
- package/dist/modules/messages/components/messageListLabels.js.map +2 -2
- package/dist/modules/messages/data/validators.js +18 -4
- package/dist/modules/messages/data/validators.js.map +2 -2
- package/dist/modules/messages/lib/channel-sender-identity.js +26 -0
- package/dist/modules/messages/lib/channel-sender-identity.js.map +7 -0
- package/dist/modules/messages/lib/composeSourceChannelType.js +41 -0
- package/dist/modules/messages/lib/composeSourceChannelType.js.map +7 -0
- package/dist/modules/payment_gateways/api/webhook/[provider]/route.js +4 -6
- package/dist/modules/payment_gateways/api/webhook/[provider]/route.js.map +2 -2
- package/dist/modules/payment_gateways/backend/payment-gateways/page.js +3 -1
- package/dist/modules/payment_gateways/backend/payment-gateways/page.js.map +2 -2
- package/dist/modules/payment_gateways/lib/webhook-processor.js +7 -0
- package/dist/modules/payment_gateways/lib/webhook-processor.js.map +2 -2
- package/dist/modules/planner/backend/planner/availability-rulesets/page.js +3 -0
- package/dist/modules/planner/backend/planner/availability-rulesets/page.js.map +2 -2
- package/dist/modules/push_notifications/backend/push_notifications/page.js +3 -1
- package/dist/modules/push_notifications/backend/push_notifications/page.js.map +2 -2
- package/dist/modules/push_notifications/backend/push_notifications/send/page.js +1 -1
- package/dist/modules/push_notifications/backend/push_notifications/send/page.js.map +2 -2
- package/dist/modules/query_index/lib/engine.js +150 -51
- package/dist/modules/query_index/lib/engine.js.map +2 -2
- package/dist/modules/query_index/lib/search-entity-policy.js +14 -0
- package/dist/modules/query_index/lib/search-entity-policy.js.map +7 -0
- package/dist/modules/query_index/lib/search-tokens.js +79 -3
- package/dist/modules/query_index/lib/search-tokens.js.map +2 -2
- package/dist/modules/resources/backend/resources/resource-types/page.js +3 -1
- package/dist/modules/resources/backend/resources/resource-types/page.js.map +2 -2
- package/dist/modules/resources/backend/resources/resources/page.js +3 -1
- package/dist/modules/resources/backend/resources/resources/page.js.map +2 -2
- package/dist/modules/sales/backend/sales/channels/offers/page.js +3 -0
- package/dist/modules/sales/backend/sales/channels/offers/page.js.map +2 -2
- package/dist/modules/sales/backend/sales/channels/page.js +3 -0
- package/dist/modules/sales/backend/sales/channels/page.js.map +2 -2
- package/dist/modules/sales/backend/sales/documents/[id]/page.js +10 -8
- package/dist/modules/sales/backend/sales/documents/[id]/page.js.map +2 -2
- package/dist/modules/sales/commands/documents.js +79 -1
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/sales/components/PriceWithCurrency.js +7 -5
- package/dist/modules/sales/components/PriceWithCurrency.js.map +2 -2
- package/dist/modules/sales/components/channels/SalesChannelOffersPanel.js +3 -0
- package/dist/modules/sales/components/channels/SalesChannelOffersPanel.js.map +2 -2
- package/dist/modules/sales/components/documents/AdjustmentsSection.js +8 -6
- package/dist/modules/sales/components/documents/AdjustmentsSection.js.map +2 -2
- package/dist/modules/sales/components/documents/ItemsSection.js +51 -22
- package/dist/modules/sales/components/documents/ItemsSection.js.map +2 -2
- package/dist/modules/sales/components/documents/LineItemDialog.js +83 -18
- package/dist/modules/sales/components/documents/LineItemDialog.js.map +2 -2
- package/dist/modules/sales/components/documents/PaymentsSection.js +23 -9
- package/dist/modules/sales/components/documents/PaymentsSection.js.map +2 -2
- package/dist/modules/sales/components/documents/ReturnsSection.js +8 -6
- package/dist/modules/sales/components/documents/ReturnsSection.js.map +2 -2
- package/dist/modules/sales/components/documents/SalesDocumentsTable.js +68 -14
- package/dist/modules/sales/components/documents/SalesDocumentsTable.js.map +2 -2
- package/dist/modules/sales/components/documents/SalesOrderDraftLines.js +6 -5
- package/dist/modules/sales/components/documents/SalesOrderDraftLines.js.map +2 -2
- package/dist/modules/sales/components/documents/ShipmentDialog.js +5 -4
- package/dist/modules/sales/components/documents/ShipmentDialog.js.map +2 -2
- package/dist/modules/sales/components/documents/ShipmentsSection.js +8 -6
- package/dist/modules/sales/components/documents/ShipmentsSection.js.map +2 -2
- package/dist/modules/sales/components/documents/lineItemShipmentLock.js +54 -0
- package/dist/modules/sales/components/documents/lineItemShipmentLock.js.map +7 -0
- package/dist/modules/sales/components/documents/lineItemUtils.js +2 -2
- package/dist/modules/sales/components/documents/lineItemUtils.js.map +2 -2
- package/dist/modules/sales/data/entities.js +8 -2
- package/dist/modules/sales/data/entities.js.map +2 -2
- package/dist/modules/sales/extension-points.js +1 -0
- package/dist/modules/sales/extension-points.js.map +2 -2
- package/dist/modules/sales/lib/salesChannelsToggleSeed.js +33 -0
- package/dist/modules/sales/lib/salesChannelsToggleSeed.js.map +7 -0
- package/dist/modules/sales/migrations/Migration20260821120000_sales_notes_addresses_context_idx.js +32 -0
- package/dist/modules/sales/migrations/Migration20260821120000_sales_notes_addresses_context_idx.js.map +7 -0
- package/dist/modules/sales/setup.js +2 -0
- package/dist/modules/sales/setup.js.map +2 -2
- package/dist/modules/sales/widgets/injection/payment-gateway-status-column/widget.js +10 -2
- package/dist/modules/sales/widgets/injection/payment-gateway-status-column/widget.js.map +2 -2
- package/dist/modules/sales/widgets/injection-table.js +4 -7
- package/dist/modules/sales/widgets/injection-table.js.map +2 -2
- package/dist/modules/staff/backend/staff/leave-requests/page.js +3 -0
- package/dist/modules/staff/backend/staff/leave-requests/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/my-leave-requests/page.js +3 -0
- package/dist/modules/staff/backend/staff/my-leave-requests/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/team-members/page.js +3 -0
- package/dist/modules/staff/backend/staff/team-members/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/team-roles/page.js +3 -0
- package/dist/modules/staff/backend/staff/team-roles/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/teams/[id]/edit/page.js +3 -0
- package/dist/modules/staff/backend/staff/teams/[id]/edit/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/teams/page.js +3 -0
- package/dist/modules/staff/backend/staff/teams/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/timesheets/page.js +29 -18
- package/dist/modules/staff/backend/staff/timesheets/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/timesheets/projects/page.js +3 -0
- package/dist/modules/staff/backend/staff/timesheets/projects/page.js.map +2 -2
- package/dist/modules/sync_excel/lib/adapters/customers.js +1 -0
- package/dist/modules/sync_excel/lib/adapters/customers.js.map +2 -2
- package/dist/modules/warranty_claims/api/assignees/route.js +3 -3
- package/dist/modules/warranty_claims/api/assignees/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/portal/attachments/route.js +2 -2
- package/dist/modules/warranty_claims/api/portal/attachments/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/portal/claims/route.js +12 -9
- package/dist/modules/warranty_claims/api/portal/claims/route.js.map +2 -2
- package/dist/modules/warranty_claims/backend/page.js +3 -0
- package/dist/modules/warranty_claims/backend/page.js.map +2 -2
- package/dist/modules/warranty_claims/backend/warranty_claims/registrations/page.js +3 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/registrations/page.js.map +2 -2
- package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/page.js +3 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/page.js.map +2 -2
- package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/page.js +3 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/page.js.map +2 -2
- package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/page.js +3 -0
- package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/page.js.map +2 -2
- package/dist/modules/wms/api/warehouseSearch.js +21 -0
- package/dist/modules/wms/api/warehouseSearch.js.map +7 -0
- package/dist/modules/wms/api/warehouses/route.js +13 -9
- package/dist/modules/wms/api/warehouses/route.js.map +2 -2
- package/dist/modules/wms/api/zones/route.js +15 -2
- package/dist/modules/wms/api/zones/route.js.map +2 -2
- package/dist/modules/wms/commands/configuration.js +121 -9
- package/dist/modules/wms/commands/configuration.js.map +2 -2
- package/dist/modules/wms/commands/inventory-actions.js +3 -0
- package/dist/modules/wms/commands/inventory-actions.js.map +2 -2
- package/dist/modules/wms/commands/shared.js +9 -1
- package/dist/modules/wms/commands/shared.js.map +2 -2
- package/dist/modules/wms/components/backend/WarehouseEditDialog.js +158 -0
- package/dist/modules/wms/components/backend/WarehouseEditDialog.js.map +7 -0
- package/dist/modules/wms/components/backend/WmsConfigurationPage.js +76 -111
- package/dist/modules/wms/components/backend/WmsConfigurationPage.js.map +3 -3
- package/dist/modules/wms/components/backend/WmsInventoryConsolePage.js +1 -0
- package/dist/modules/wms/components/backend/WmsInventoryConsolePage.js.map +2 -2
- package/dist/modules/wms/components/backend/WmsLotsListPage.js +1 -0
- package/dist/modules/wms/components/backend/WmsLotsListPage.js.map +2 -2
- package/dist/modules/wms/components/backend/inventoryMutationLoaders.js +27 -20
- package/dist/modules/wms/components/backend/inventoryMutationLoaders.js.map +2 -2
- package/dist/modules/wms/components/backend/warehouseFormOptions.js +66 -0
- package/dist/modules/wms/components/backend/warehouseFormOptions.js.map +7 -0
- package/dist/modules/workflows/backend/definitions/page.js +3 -1
- package/dist/modules/workflows/backend/definitions/page.js.map +2 -2
- package/dist/modules/workflows/backend/events/page.js +3 -1
- package/dist/modules/workflows/backend/events/page.js.map +2 -2
- package/dist/modules/workflows/backend/instances/page.js +3 -1
- package/dist/modules/workflows/backend/instances/page.js.map +2 -2
- package/dist/modules/workflows/backend/tasks/page.js +3 -1
- package/dist/modules/workflows/backend/tasks/page.js.map +2 -2
- package/dist/modules/workflows/lib/activity-executor.js +4 -0
- package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
- package/package.json +9 -10
- package/src/helpers/integration/api.ts +33 -1
- package/src/helpers/integration/communicationChannelsFixtures.ts +69 -1
- package/src/modules/api_keys/api/keys/route.ts +3 -1
- package/src/modules/api_keys/backend/api-keys/page.tsx +4 -1
- package/src/modules/api_keys/di.ts +7 -0
- package/src/modules/api_keys/services/principalService.ts +33 -0
- package/src/modules/attachments/AGENTS.md +15 -0
- package/src/modules/attachments/api/route.ts +2 -2
- package/src/modules/attachments/components/AttachmentLibrary.tsx +3 -0
- package/src/modules/attachments/di.ts +16 -0
- package/src/modules/attachments/i18n/de.json +5 -0
- package/src/modules/attachments/i18n/en.json +5 -0
- package/src/modules/attachments/i18n/es.json +5 -0
- package/src/modules/attachments/i18n/ko.json +5 -0
- package/src/modules/attachments/i18n/pl.json +5 -0
- package/src/modules/attachments/index.ts +10 -0
- package/src/modules/attachments/lib/attachment-service.ts +468 -0
- package/src/modules/attachments/lib/scoped-upload-service.ts +46 -0
- package/src/modules/attachments/lib/upload-limits.ts +6 -2
- package/src/modules/audit_logs/backend/audit-logs/page.tsx +8 -0
- package/src/modules/audit_logs/lib/display-helpers.tsx +42 -32
- package/src/modules/audit_logs/services/actionLogService.ts +74 -11
- package/src/modules/auth/acl.ts +30 -6
- package/src/modules/auth/api/feature-check.ts +10 -3
- package/src/modules/auth/api/login.ts +9 -1
- package/src/modules/auth/api/reset/validate.ts +57 -0
- package/src/modules/auth/api/roles/acl/route.ts +42 -39
- package/src/modules/auth/api/session/refresh.ts +32 -1
- package/src/modules/auth/api/users/acl/route.ts +177 -53
- package/src/modules/auth/api/users/route.ts +18 -5
- package/src/modules/auth/backend/roles/[id]/edit/page.meta.ts +5 -2
- package/src/modules/auth/backend/roles/page.tsx +4 -2
- package/src/modules/auth/backend/users/[id]/edit/page.meta.ts +5 -2
- package/src/modules/auth/backend/users/page.tsx +3 -2
- package/src/modules/auth/commands/acl.ts +495 -0
- package/src/modules/auth/components/AclEditor.tsx +63 -33
- package/src/modules/auth/data/entities.ts +4 -1
- package/src/modules/auth/data/validators.ts +5 -0
- package/src/modules/auth/di.ts +46 -16
- package/src/modules/auth/frontend/reset/[token]/page.tsx +74 -0
- package/src/modules/auth/i18n/de.json +389 -0
- package/src/modules/auth/i18n/en.json +389 -0
- package/src/modules/auth/i18n/es.json +389 -0
- package/src/modules/auth/i18n/ko.json +389 -0
- package/src/modules/auth/i18n/pl.json +391 -2
- package/src/modules/auth/lib/grantChecks.ts +6 -0
- package/src/modules/auth/lib/userIdFilter.ts +31 -0
- package/src/modules/auth/services/authService.ts +18 -2
- package/src/modules/auth/services/principalService.ts +266 -0
- package/src/modules/auth/services/rbacService.ts +110 -24
- package/src/modules/auth/services/roleOrganizationScope.ts +70 -0
- package/src/modules/business_rules/backend/logs/page.tsx +4 -1
- package/src/modules/business_rules/backend/rules/page.tsx +4 -1
- package/src/modules/business_rules/backend/sets/page.tsx +4 -1
- package/src/modules/business_rules/lib/expression-evaluator.ts +9 -9
- package/src/modules/business_rules/lib/rule-evaluator.ts +10 -9
- package/src/modules/catalog/ai-agents.ts +1 -1
- package/src/modules/catalog/ai-tools/merchandising-pack.ts +2 -2
- package/src/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.tsx +12 -5
- package/src/modules/catalog/components/categories/CategoriesDataTable.tsx +3 -0
- package/src/modules/catalog/components/products/ProductsDataTable.tsx +7 -0
- package/src/modules/catalog/i18n/de.json +1 -0
- package/src/modules/catalog/i18n/en.json +1 -0
- package/src/modules/catalog/i18n/es.json +1 -0
- package/src/modules/catalog/i18n/ko.json +1 -0
- package/src/modules/catalog/i18n/pl.json +1 -0
- package/src/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.client.tsx +4 -0
- package/src/modules/communication_channels/api/post/channels/[id]/test-send/route.ts +17 -1
- package/src/modules/communication_channels/api/post/test-seed/route.ts +155 -16
- package/src/modules/communication_channels/backend/communication_channels/channels/page.tsx +4 -0
- package/src/modules/communication_channels/commands/ingest-inbound-message.ts +38 -1
- package/src/modules/communication_channels/di.ts +7 -0
- package/src/modules/communication_channels/lib/adapter.ts +14 -0
- package/src/modules/communication_channels/lib/email-capabilities.ts +4 -0
- package/src/modules/communication_channels/lib/outbound-recipient.ts +71 -0
- package/src/modules/communication_channels/lib/resolve-channel-type.ts +99 -0
- package/src/modules/communication_channels/lib/test-seed.ts +64 -4
- package/src/modules/configs/i18n/de.json +2 -0
- package/src/modules/configs/i18n/en.json +2 -0
- package/src/modules/configs/i18n/es.json +2 -0
- package/src/modules/configs/i18n/ko.json +2 -0
- package/src/modules/configs/i18n/pl.json +2 -0
- package/src/modules/configs/lib/system-status.ts +9 -0
- package/src/modules/currencies/AGENTS.md +3 -0
- package/src/modules/currencies/backend/currencies/page.tsx +4 -1
- package/src/modules/currencies/backend/exchange-rates/page.tsx +4 -1
- package/src/modules/currencies/services/README.md +45 -0
- package/src/modules/currencies/services/rateFetchingService.ts +6 -1
- package/src/modules/customer_accounts/api/admin/domain-mappings.ts +7 -0
- package/src/modules/customer_accounts/backend/customer_accounts/roles/page.tsx +4 -1
- package/src/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.tsx +4 -1
- package/src/modules/customer_accounts/services/domainMappingService.ts +10 -0
- package/src/modules/customers/ai-tools/companies-pack.ts +4 -1
- package/src/modules/customers/ai-tools/deal-analyzer-pack.ts +17 -2
- package/src/modules/customers/ai-tools/deals-pack.ts +66 -1
- package/src/modules/customers/ai-tools/people-pack.ts +4 -1
- package/src/modules/customers/api/activities/route.ts +45 -0
- package/src/modules/customers/api/companies/[id]/people/route.ts +24 -73
- package/src/modules/customers/api/companies/[id]/route.ts +24 -107
- package/src/modules/customers/api/dashboard/widgets/customer-todos/route.ts +16 -8
- package/src/modules/customers/api/deals/[id]/companies/route.ts +4 -1
- package/src/modules/customers/api/deals/[id]/people/route.ts +4 -1
- package/src/modules/customers/api/deals/[id]/route.ts +17 -28
- package/src/modules/customers/api/deals/[id]/stats/route.ts +10 -25
- package/src/modules/customers/api/deals/aggregate/route.ts +14 -4
- package/src/modules/customers/api/deals/map/route.ts +1 -0
- package/src/modules/customers/api/deals/route.ts +50 -1
- package/src/modules/customers/api/entity-roles-factory.ts +8 -4
- package/src/modules/customers/api/interactions/route.ts +8 -2
- package/src/modules/customers/api/interactions/tasks/route.ts +24 -1
- package/src/modules/customers/api/people/[id]/companies/[linkId]/route.ts +12 -4
- package/src/modules/customers/api/people/[id]/companies/context.ts +4 -1
- package/src/modules/customers/api/people/[id]/companies/enriched/route.ts +4 -1
- package/src/modules/customers/api/people/[id]/route.ts +16 -11
- package/src/modules/customers/api/people/check-phone/route.ts +75 -10
- package/src/modules/customers/api/todos/route.ts +27 -2
- package/src/modules/customers/backend/customers/companies/page.tsx +5 -2
- package/src/modules/customers/backend/customers/deals/[id]/page.tsx +7 -2
- package/src/modules/customers/backend/customers/deals/page.tsx +8 -2
- package/src/modules/customers/backend/customers/deals/pipeline/components/Lane.tsx +5 -14
- package/src/modules/customers/backend/customers/deals/pipeline/components/StatusFilterPopover.tsx +109 -27
- package/src/modules/customers/backend/customers/deals/pipeline/components/toneClasses.ts +21 -0
- package/src/modules/customers/backend/customers/people/page.tsx +5 -2
- package/src/modules/customers/backend/hooks/useEmailDuplicateCheck.ts +12 -4
- package/src/modules/customers/commands/deals.ts +34 -59
- package/src/modules/customers/commands/interactions.ts +1 -1
- package/src/modules/customers/commands/personCompanyLinks.ts +224 -8
- package/src/modules/customers/components/CustomerTodosTable.tsx +2 -0
- package/src/modules/customers/components/DealsKpiStrip.tsx +7 -7
- package/src/modules/customers/components/calendar/AgendaList.tsx +3 -3
- package/src/modules/customers/components/calendar/CalendarEventEditor.tsx +1 -1
- package/src/modules/customers/components/calendar/CalendarScreen.tsx +1 -1
- package/src/modules/customers/components/calendar/CalendarSettingsModal.tsx +1 -1
- package/src/modules/customers/components/calendar/EventBlock.tsx +2 -2
- package/src/modules/customers/components/calendar/EventPeekPopover.tsx +4 -2
- package/src/modules/customers/components/calendar/editor/PeopleField.tsx +61 -5
- package/src/modules/customers/components/calendar/editor/hooks.ts +7 -1
- package/src/modules/customers/components/calendar/types.ts +2 -2
- package/src/modules/customers/components/detail/CompanyKpiBar.tsx +5 -4
- package/src/modules/customers/components/detail/CompanyPeopleSection.tsx +8 -1
- package/src/modules/customers/components/detail/DealForm.tsx +22 -2
- package/src/modules/customers/components/detail/DealsSection.tsx +11 -5
- package/src/modules/customers/components/detail/EntityTagsDialog.tsx +58 -12
- package/src/modules/customers/components/detail/PersonCompaniesSection.tsx +8 -1
- package/src/modules/customers/components/detail/hooks/usePersonTasks.ts +23 -14
- package/src/modules/customers/components/detail/schedule/ParticipantsField.tsx +5 -5
- package/src/modules/customers/components/detail/schedule/useScheduleFormState.ts +5 -5
- package/src/modules/customers/components/detail/types.ts +1 -1
- package/src/modules/customers/components/detail/utils.ts +15 -5
- package/src/modules/customers/data/entities.ts +1 -1
- package/src/modules/customers/data/validators.ts +50 -9
- package/src/modules/customers/events.ts +5 -0
- package/src/modules/customers/i18n/de.json +9 -1
- package/src/modules/customers/i18n/en.json +9 -1
- package/src/modules/customers/i18n/es.json +9 -1
- package/src/modules/customers/i18n/ko.json +9 -1
- package/src/modules/customers/i18n/pl.json +9 -1
- package/src/modules/customers/lib/calendar/conflicts.ts +10 -2
- package/src/modules/customers/lib/calendar/editorPayload.ts +4 -3
- package/src/modules/customers/lib/calendar/mapItem.ts +8 -3
- package/src/modules/customers/lib/calendar/participantIdentity.ts +17 -0
- package/src/modules/customers/lib/closureStage.ts +76 -0
- package/src/modules/customers/lib/dealStatus.ts +42 -0
- package/src/modules/customers/lib/detailReadAccess.ts +35 -0
- package/src/modules/customers/lib/findPeopleByAddresses.ts +8 -6
- package/src/modules/customers/lib/personCompanies.ts +136 -1
- package/src/modules/customers/lib/todoCompatibility.ts +19 -1
- package/src/modules/dashboards/i18n/es.json +4 -4
- package/src/modules/dashboards/i18n/pl.json +7 -7
- package/src/modules/dashboards/lib/widgetScope.ts +6 -0
- package/src/modules/data_sync/AGENTS.md +1 -1
- package/src/modules/data_sync/backend/data-sync/page.tsx +4 -1
- package/src/modules/data_sync/lib/abandoned-run.ts +56 -0
- package/src/modules/data_sync/lib/adapter.ts +36 -0
- package/src/modules/data_sync/lib/sync-engine.ts +113 -5
- package/src/modules/data_sync/workers/sync-export.ts +4 -0
- package/src/modules/data_sync/workers/sync-import.ts +4 -0
- package/src/modules/devices/AGENTS.md +20 -1
- package/src/modules/devices/acl.ts +9 -1
- package/src/modules/devices/api/admin/devices/[id]/route.ts +5 -34
- package/src/modules/devices/api/admin/devices/route.ts +14 -1
- package/src/modules/devices/api/deviceList.ts +135 -11
- package/src/modules/devices/api/deviceSerialization.ts +50 -0
- package/src/modules/devices/api/openapi.ts +9 -1
- package/src/modules/devices/api/route.ts +9 -1
- package/src/modules/devices/backend/devices/[id]/page.tsx +18 -32
- package/src/modules/devices/backend/devices/create/page.tsx +17 -1
- package/src/modules/devices/backend/devices/page.tsx +44 -45
- package/src/modules/devices/backend/devices/useDeviceUserLabels.ts +45 -0
- package/src/modules/devices/backend/devices/userOptions.ts +99 -0
- package/src/modules/devices/i18n/de.json +3 -2
- package/src/modules/devices/i18n/en.json +3 -2
- package/src/modules/devices/i18n/es.json +3 -2
- package/src/modules/devices/i18n/ko.json +3 -2
- package/src/modules/devices/i18n/pl.json +3 -2
- package/src/modules/devices/setup.ts +6 -2
- package/src/modules/directory/backend/directory/organizations/page.tsx +3 -1
- package/src/modules/directory/backend/directory/tenants/page.tsx +3 -1
- package/src/modules/directory/di.ts +14 -2
- package/src/modules/directory/services/organizationHierarchyService.ts +34 -0
- package/src/modules/directory/services/organizationScopeService.ts +85 -0
- package/src/modules/directory/utils/organizationScope.ts +4 -15
- package/src/modules/entities/api/records.ts +2 -0
- package/src/modules/entities/backend/entities/user/[entityId]/records/page.tsx +4 -1
- package/src/modules/eudr/api/plots/route.ts +4 -0
- package/src/modules/eudr/backend/eudr/evidence-submissions/page.tsx +4 -0
- package/src/modules/eudr/backend/eudr/plots/page.tsx +4 -0
- package/src/modules/eudr/backend/eudr/product-mappings/page.tsx +4 -0
- package/src/modules/eudr/backend/eudr/risk-assessments/page.tsx +4 -0
- package/src/modules/eudr/backend/eudr/statements/page.tsx +4 -0
- package/src/modules/eudr/commands/statements.ts +0 -3
- package/src/modules/eudr/data/validators.ts +16 -1
- package/src/modules/feature_toggles/components/FeatureTogglesTable.tsx +2 -1
- package/src/modules/feature_toggles/components/OverridesTable.tsx +2 -1
- package/src/modules/feature_toggles/defaults.json +0 -8
- package/src/modules/inbox_ops/backend/inbox-ops/log/page.tsx +4 -0
- package/src/modules/inbox_ops/backend/inbox-ops/page.tsx +4 -0
- package/src/modules/inbox_ops/lib/htmlToPlainText.ts +1 -17
- package/src/modules/integrations/api/[id]/credentials/route.ts +6 -4
- package/src/modules/integrations/backend/integrations/[id]/page.tsx +61 -31
- package/src/modules/integrations/backend/integrations/bundle/[id]/page.tsx +44 -9
- package/src/modules/integrations/backend/integrations/credential-secret-fields.ts +84 -0
- package/src/modules/integrations/data/validators.ts +12 -2
- package/src/modules/integrations/i18n/de.json +1 -0
- package/src/modules/integrations/i18n/en.json +1 -0
- package/src/modules/integrations/i18n/es.json +1 -0
- package/src/modules/integrations/i18n/ko.json +1 -0
- package/src/modules/integrations/i18n/pl.json +1 -0
- package/src/modules/integrations/lib/credentials-masking.ts +10 -6
- package/src/modules/messages/api/[id]/route.ts +55 -1
- package/src/modules/messages/api/openapi.ts +20 -0
- package/src/modules/messages/api/route.ts +31 -2
- package/src/modules/messages/components/MessagesInboxPageClient.tsx +9 -16
- package/src/modules/messages/components/inboxFilters.ts +36 -0
- package/src/modules/messages/components/message-detail/hooks/useMessageDetailsConversation.ts +14 -1
- package/src/modules/messages/components/message-detail/panels/MessageHeader.tsx +3 -1
- package/src/modules/messages/components/message-detail/panels/thread-panel.tsx +2 -1
- package/src/modules/messages/components/message-detail/types.ts +3 -0
- package/src/modules/messages/components/messageListLabels.ts +52 -3
- package/src/modules/messages/data/validators.ts +43 -4
- package/src/modules/messages/lib/channel-sender-identity.ts +55 -0
- package/src/modules/messages/lib/composeSourceChannelType.ts +95 -0
- package/src/modules/payment_gateways/api/webhook/[provider]/route.ts +4 -6
- package/src/modules/payment_gateways/backend/payment-gateways/page.tsx +4 -1
- package/src/modules/payment_gateways/lib/webhook-processor.ts +11 -0
- package/src/modules/planner/backend/planner/availability-rulesets/page.tsx +4 -0
- package/src/modules/push_notifications/backend/push_notifications/page.tsx +4 -1
- package/src/modules/push_notifications/backend/push_notifications/send/page.tsx +4 -4
- package/src/modules/query_index/lib/engine.ts +249 -61
- package/src/modules/query_index/lib/search-entity-policy.ts +46 -0
- package/src/modules/query_index/lib/search-tokens.ts +135 -3
- package/src/modules/resources/backend/resources/resource-types/page.tsx +4 -1
- package/src/modules/resources/backend/resources/resources/page.tsx +4 -1
- package/src/modules/sales/backend/sales/channels/offers/page.tsx +4 -0
- package/src/modules/sales/backend/sales/channels/page.tsx +4 -0
- package/src/modules/sales/backend/sales/documents/[id]/page.tsx +14 -8
- package/src/modules/sales/commands/documents.ts +93 -0
- package/src/modules/sales/components/PriceWithCurrency.tsx +17 -5
- package/src/modules/sales/components/channels/SalesChannelOffersPanel.tsx +4 -0
- package/src/modules/sales/components/documents/AdjustmentsSection.tsx +6 -5
- package/src/modules/sales/components/documents/ItemsSection.tsx +56 -18
- package/src/modules/sales/components/documents/LineItemDialog.tsx +137 -20
- package/src/modules/sales/components/documents/PaymentsSection.tsx +20 -9
- package/src/modules/sales/components/documents/ReturnsSection.tsx +6 -5
- package/src/modules/sales/components/documents/SalesDocumentsTable.tsx +106 -14
- package/src/modules/sales/components/documents/SalesOrderDraftLines.tsx +6 -5
- package/src/modules/sales/components/documents/ShipmentDialog.tsx +5 -4
- package/src/modules/sales/components/documents/ShipmentsSection.tsx +6 -5
- package/src/modules/sales/components/documents/lineItemShipmentLock.ts +74 -0
- package/src/modules/sales/components/documents/lineItemUtils.ts +7 -2
- package/src/modules/sales/data/entities.ts +6 -0
- package/src/modules/sales/extension-points.ts +1 -0
- package/src/modules/sales/i18n/de.json +8 -0
- package/src/modules/sales/i18n/en.json +8 -0
- package/src/modules/sales/i18n/es.json +8 -0
- package/src/modules/sales/i18n/ko.json +8 -0
- package/src/modules/sales/i18n/pl.json +8 -0
- package/src/modules/sales/lib/salesChannelsToggleSeed.ts +42 -0
- package/src/modules/sales/migrations/.snapshot-open-mercato.json +60 -0
- package/src/modules/sales/migrations/Migration20260821120000_sales_notes_addresses_context_idx.ts +87 -0
- package/src/modules/sales/setup.ts +2 -0
- package/src/modules/sales/widgets/injection/payment-gateway-status-column/widget.ts +9 -1
- package/src/modules/sales/widgets/injection-table.ts +4 -7
- package/src/modules/staff/backend/staff/leave-requests/page.tsx +4 -0
- package/src/modules/staff/backend/staff/my-leave-requests/page.tsx +4 -0
- package/src/modules/staff/backend/staff/team-members/page.tsx +6 -0
- package/src/modules/staff/backend/staff/team-roles/page.tsx +4 -0
- package/src/modules/staff/backend/staff/teams/[id]/edit/page.tsx +4 -0
- package/src/modules/staff/backend/staff/teams/page.tsx +4 -0
- package/src/modules/staff/backend/staff/timesheets/page.tsx +12 -1
- package/src/modules/staff/backend/staff/timesheets/projects/page.tsx +4 -0
- package/src/modules/sync_excel/lib/adapters/customers.ts +5 -0
- package/src/modules/warranty_claims/api/assignees/route.ts +3 -3
- package/src/modules/warranty_claims/api/portal/attachments/route.ts +2 -2
- package/src/modules/warranty_claims/api/portal/claims/route.ts +12 -9
- package/src/modules/warranty_claims/backend/page.tsx +4 -0
- package/src/modules/warranty_claims/backend/warranty_claims/registrations/page.tsx +4 -0
- package/src/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/page.tsx +4 -0
- package/src/modules/warranty_claims/backend/warranty_claims/vendor-policies/page.tsx +4 -0
- package/src/modules/warranty_claims/frontend/[orgSlug]/portal/claims/page.tsx +4 -0
- package/src/modules/wms/api/warehouseSearch.ts +23 -0
- package/src/modules/wms/api/warehouses/route.ts +13 -9
- package/src/modules/wms/api/zones/route.ts +16 -0
- package/src/modules/wms/commands/configuration.ts +159 -10
- package/src/modules/wms/commands/inventory-actions.ts +3 -0
- package/src/modules/wms/commands/shared.ts +17 -0
- package/src/modules/wms/components/backend/WarehouseEditDialog.tsx +216 -0
- package/src/modules/wms/components/backend/WmsConfigurationPage.tsx +95 -141
- package/src/modules/wms/components/backend/WmsInventoryConsolePage.tsx +2 -0
- package/src/modules/wms/components/backend/WmsLotsListPage.tsx +2 -0
- package/src/modules/wms/components/backend/inventoryMutationLoaders.ts +36 -20
- package/src/modules/wms/components/backend/warehouseFormOptions.ts +72 -0
- package/src/modules/wms/i18n/de.json +2 -0
- package/src/modules/wms/i18n/en.json +2 -0
- package/src/modules/wms/i18n/es.json +2 -0
- package/src/modules/wms/i18n/ko.json +2 -0
- package/src/modules/wms/i18n/pl.json +2 -0
- package/src/modules/workflows/backend/definitions/page.tsx +4 -1
- package/src/modules/workflows/backend/events/page.tsx +4 -1
- package/src/modules/workflows/backend/instances/page.tsx +4 -1
- package/src/modules/workflows/backend/tasks/page.tsx +4 -1
- package/src/modules/workflows/lib/activity-executor.ts +9 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/customers/ai-tools/companies-pack.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * `customers.list_companies` + `customers.get_company` (Phase 1 WS-C, Step 3.9).\n *\n * Phase 3a of `.ai/specs/implemented/2026-04-27-ai-tools-api-backed-dry-refactor.md`:\n * `customers.list_companies` is now an API-backed wrapper over\n * `GET /api/customers/companies`. Tool name, schema, requiredFeatures, and\n * output shape are unchanged.\n *\n * Phase 3c of the same spec migrates `customers.get_company` to a single\n * in-process call to `GET /api/customers/companies/<id>?include=...` over the\n * documented aggregate detail route. Tool name, schema, requiredFeatures, and\n * output shape are unchanged.\n */\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 { assertTenantScope, type CustomersAiToolDefinition, type CustomersToolContext } from './types'\nimport {\n buildRelatedRecords,\n toCustomerListSummary,\n toIso,\n type CustomerRelatedRecords,\n} from './_shared'\n\nconst listCompaniesInput = z\n .object({\n q: z.string().trim().optional().describe('Search text matched against display name / email / domain. 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 companies carrying at least one of these tag ids.'),\n })\n .passthrough()\n\ntype ListCompaniesInput = z.infer<typeof listCompaniesInput>\n\ntype ListCompaniesApiItem = {\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 domain?: string | null\n website_url?: string | null\n websiteUrl?: string | null\n industry?: string | null\n size_bucket?: string | null\n sizeBucket?: string | null\n created_at?: string | null\n createdAt?: string | null\n}\n\ntype ListCompaniesApiResponse = {\n items?: ListCompaniesApiItem[]\n total?: number\n}\n\ntype ListCompaniesOutput = {\n items: Array<Record<string, unknown>>\n total: number\n limit: number\n offset: number\n}\n\nconst listCompaniesTool = defineApiBackedAiTool<\n ListCompaniesInput,\n ListCompaniesApiResponse,\n ListCompaniesOutput\n>({\n name: 'customers.list_companies',\n displayName: 'List companies',\n description:\n 'Search / list companies for the caller tenant + organization. Returns { items, total, limit, offset }.',\n inputSchema: listCompaniesInput,\n requiredFeatures: ['customers.companies.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.tags && input.tags.length > 0) query.tagIds = input.tags.join(',')\n\n const operation: AiApiOperationRequest = {\n method: 'GET',\n path: '/customers/companies',\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 ListCompaniesApiResponse\n const rawItems: ListCompaniesApiItem[] = Array.isArray(data.items) ? data.items : []\n return {\n items: rawItems.map((row) => ({\n ...toCustomerListSummary(row),\n domain: row.domain ?? null,\n websiteUrl: row.website_url ?? row.websiteUrl ?? null,\n industry: row.industry ?? null,\n sizeBucket: row.size_bucket ?? row.sizeBucket ?? null,\n })),\n total: typeof data.total === 'number' ? data.total : 0,\n limit,\n offset,\n }\n },\n}) as unknown as CustomersAiToolDefinition\n\nconst getCompanyInput = z.object({\n companyId: z.string().uuid().describe('Company entity id (UUID).'),\n includeRelated: z\n .boolean()\n .optional()\n .describe('When true, include notes, activities, deals, people, addresses, tasks, and tags (each capped at 100).'),\n})\n\ntype GetCompanyInput = z.infer<typeof getCompanyInput>\n\nconst getCompanyTool: CustomersAiToolDefinition = {\n name: 'customers.get_company',\n displayName: 'Get company',\n description:\n 'Fetch a company customer record by id with profile fields and (optionally) notes, activities, deals, people, addresses, tasks, tags, and custom fields. Returns { found: false } when outside tenant/org scope.',\n inputSchema: getCompanyInput,\n requiredFeatures: ['customers.companies.view'],\n tags: ['read', 'customers'],\n handler: async (rawInput, ctx) => {\n const { tenantId: _tenantId } = assertTenantScope(ctx)\n void _tenantId\n const input: GetCompanyInput = getCompanyInput.parse(rawInput)\n const includeRelated = !!input.includeRelated\n\n const operation: AiApiOperationRequest = {\n method: 'GET',\n path: `/customers/companies/${input.companyId}`,\n }\n if (includeRelated) {\n operation.query = {\n include: 'addresses,comments,activities,interactions,deals,todos,people',\n }\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, companyId: input.companyId }\n }\n throw new Error(response.error ?? `Failed to fetch company ${input.companyId}`)\n }\n const data = (response.data ?? {}) as Record<string, unknown>\n const companyRow = (data.company ?? null) as Record<string, unknown> | null\n if (!companyRow) {\n return { found: false as const, companyId: input.companyId }\n }\n const profileRow = (data.profile ?? null) as Record<string, unknown> | null\n const customFields = (data.customFields ?? {}) as Record<string, unknown>\n\n let related: CustomerRelatedRecords | null = null\n if (includeRelated) {\n related = buildRelatedRecords(data, { includePeople: true })\n }\n return {\n found: true as const,\n company: {\n id: companyRow.id,\n displayName: companyRow.displayName ?? null,\n description: companyRow.description ?? null,\n primaryEmail: companyRow.primaryEmail ?? null,\n primaryPhone: companyRow.primaryPhone ?? null,\n status: companyRow.status ?? null,\n lifecycleStage: companyRow.lifecycleStage ?? null,\n source: companyRow.source ?? null,\n ownerUserId: companyRow.ownerUserId ?? null,\n organizationId: companyRow.organizationId ?? null,\n tenantId: companyRow.tenantId ?? null,\n createdAt: toIso(companyRow.createdAt),\n updatedAt: toIso(companyRow.updatedAt),\n },\n profile: profileRow\n ? {\n id: profileRow.id,\n legalName: profileRow.legalName ?? null,\n brandName: profileRow.brandName ?? null,\n domain: profileRow.domain ?? null,\n websiteUrl: profileRow.websiteUrl ?? null,\n industry: profileRow.industry ?? null,\n sizeBucket: profileRow.sizeBucket ?? null,\n annualRevenue: profileRow.annualRevenue ?? null,\n }\n : null,\n customFields,\n related,\n }\n },\n}\n\nexport const companiesAiTools: CustomersAiToolDefinition[] = [listCompaniesTool, getCompanyTool]\n\nexport default companiesAiTools\n"],
|
|
5
|
-
"mappings": "AAaA,SAAS,SAAS;AAClB,SAAS,6BAA6B;AACtC;AAAA,EACE;AAAA,OAGK;AACP,SAAS,yBAAoF;AAC7F;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AAEP,MAAM,qBAAqB,EACxB,OAAO;AAAA,EACN,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,6FAA6F;AAAA,EACtI,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,SAAS,EAAE,SAAS,+CAA+C;AAAA,EAC3G,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,SAAS,EAAE,SAAS,qCAAqC;AAAA,EACzF,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,SAAS,EAAE,SAAS,+DAA+D;AACtH,CAAC,EACA,YAAY;AA4Cf,MAAM,oBAAoB,sBAIxB;AAAA,EACA,MAAM;AAAA,EACN,aAAa;AAAA,EACb,aACE;AAAA,EACF,aAAa;AAAA,EACb,kBAAkB,CAAC,0BAA0B;AAAA,EAC7C,aAAa,CAAC,OAAO,QAAQ;AAC3B,sBAAkB,GAAsC;AACxD,UAAM,QAAQ,MAAM,SAAS;AAC7B,UAAM,SAAS,MAAM,UAAU;AAC/B,UAAM,OAAO,KAAK,MAAM,SAAS,KAAK,IAAI;AAE1C,UAAM,QAAsE;AAAA,MAC1E;AAAA,MACA,UAAU;AAAA,IACZ;AACA,QAAI,MAAM,GAAG,KAAK,EAAG,OAAM,SAAS,MAAM,EAAE,KAAK;AACjD,QAAI,MAAM,QAAQ,MAAM,KAAK,SAAS,EAAG,OAAM,SAAS,MAAM,KAAK,KAAK,GAAG;AAE3E,UAAM,YAAmC;AAAA,MACvC,QAAQ;AAAA,MACR,MAAM;AAAA,MACN;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAAA,EACA,aAAa,CAAC,UAAU,UAAU;AAChC,UAAM,QAAQ,MAAM,SAAS;AAC7B,UAAM,SAAS,MAAM,UAAU;AAC/B,UAAM,OAAQ,SAAS,QAAQ,CAAC;AAChC,UAAM,WAAmC,MAAM,QAAQ,KAAK,KAAK,IAAI,KAAK,QAAQ,CAAC;AACnF,WAAO;AAAA,MACL,OAAO,SAAS,IAAI,CAAC,SAAS;AAAA,QAC5B,GAAG,sBAAsB,GAAG;AAAA,QAC5B,QAAQ,IAAI,UAAU;AAAA,QACtB,YAAY,IAAI,eAAe,IAAI,cAAc;AAAA,QACjD,UAAU,IAAI,YAAY;AAAA,QAC1B,YAAY,IAAI,eAAe,IAAI,cAAc;AAAA,MACnD,EAAE;AAAA,MACF,OAAO,OAAO,KAAK,UAAU,WAAW,KAAK,QAAQ;AAAA,
|
|
4
|
+
"sourcesContent": ["/**\n * `customers.list_companies` + `customers.get_company` (Phase 1 WS-C, Step 3.9).\n *\n * Phase 3a of `.ai/specs/implemented/2026-04-27-ai-tools-api-backed-dry-refactor.md`:\n * `customers.list_companies` is now an API-backed wrapper over\n * `GET /api/customers/companies`. Tool name, schema, requiredFeatures, and\n * output shape are unchanged.\n *\n * Phase 3c of the same spec migrates `customers.get_company` to a single\n * in-process call to `GET /api/customers/companies/<id>?include=...` over the\n * documented aggregate detail route. Tool name, schema, requiredFeatures, and\n * output shape are unchanged.\n */\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 { assertTenantScope, type CustomersAiToolDefinition, type CustomersToolContext } from './types'\nimport {\n buildRelatedRecords,\n toCustomerListSummary,\n toIso,\n type CustomerRelatedRecords,\n} from './_shared'\n\nconst listCompaniesInput = z\n .object({\n q: z.string().trim().optional().describe('Search text matched against display name / email / domain. 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 companies carrying at least one of these tag ids.'),\n })\n .passthrough()\n\ntype ListCompaniesInput = z.infer<typeof listCompaniesInput>\n\ntype ListCompaniesApiItem = {\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 domain?: string | null\n website_url?: string | null\n websiteUrl?: string | null\n industry?: string | null\n size_bucket?: string | null\n sizeBucket?: string | null\n created_at?: string | null\n createdAt?: string | null\n}\n\ntype ListCompaniesApiResponse = {\n items?: ListCompaniesApiItem[]\n total?: number\n}\n\ntype ListCompaniesOutput = {\n items: Array<Record<string, unknown>>\n total: number\n limit: number\n offset: number\n}\n\nconst listCompaniesTool = defineApiBackedAiTool<\n ListCompaniesInput,\n ListCompaniesApiResponse,\n ListCompaniesOutput\n>({\n name: 'customers.list_companies',\n displayName: 'List companies',\n description:\n 'Search / list companies for the caller tenant + organization. Returns { items, total, totalIsCapped, limit, offset }. When totalIsCapped is true, total is a floor (\"at least N\", render it as \"N+\"), and pagination is exhausted only when a page returns fewer than limit items \u2014 never when offset reaches total.',\n inputSchema: listCompaniesInput,\n requiredFeatures: ['customers.companies.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.tags && input.tags.length > 0) query.tagIds = input.tags.join(',')\n\n const operation: AiApiOperationRequest = {\n method: 'GET',\n path: '/customers/companies',\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 ListCompaniesApiResponse\n const rawItems: ListCompaniesApiItem[] = Array.isArray(data.items) ? data.items : []\n return {\n items: rawItems.map((row) => ({\n ...toCustomerListSummary(row),\n domain: row.domain ?? null,\n websiteUrl: row.website_url ?? row.websiteUrl ?? null,\n industry: row.industry ?? null,\n sizeBucket: row.size_bucket ?? row.sizeBucket ?? null,\n })),\n total: typeof data.total === 'number' ? data.total : 0,\n // A capped count reports a floor: phrase the total as \"at least N\" and\n // never treat it as proof that pagination is exhausted.\n totalIsCapped: (data as { totalIsCapped?: boolean }).totalIsCapped === true,\n limit,\n offset,\n }\n },\n}) as unknown as CustomersAiToolDefinition\n\nconst getCompanyInput = z.object({\n companyId: z.string().uuid().describe('Company entity id (UUID).'),\n includeRelated: z\n .boolean()\n .optional()\n .describe('When true, include notes, activities, deals, people, addresses, tasks, and tags (each capped at 100).'),\n})\n\ntype GetCompanyInput = z.infer<typeof getCompanyInput>\n\nconst getCompanyTool: CustomersAiToolDefinition = {\n name: 'customers.get_company',\n displayName: 'Get company',\n description:\n 'Fetch a company customer record by id with profile fields and (optionally) notes, activities, deals, people, addresses, tasks, tags, and custom fields. Returns { found: false } when outside tenant/org scope.',\n inputSchema: getCompanyInput,\n requiredFeatures: ['customers.companies.view'],\n tags: ['read', 'customers'],\n handler: async (rawInput, ctx) => {\n const { tenantId: _tenantId } = assertTenantScope(ctx)\n void _tenantId\n const input: GetCompanyInput = getCompanyInput.parse(rawInput)\n const includeRelated = !!input.includeRelated\n\n const operation: AiApiOperationRequest = {\n method: 'GET',\n path: `/customers/companies/${input.companyId}`,\n }\n if (includeRelated) {\n operation.query = {\n include: 'addresses,comments,activities,interactions,deals,todos,people',\n }\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, companyId: input.companyId }\n }\n throw new Error(response.error ?? `Failed to fetch company ${input.companyId}`)\n }\n const data = (response.data ?? {}) as Record<string, unknown>\n const companyRow = (data.company ?? null) as Record<string, unknown> | null\n if (!companyRow) {\n return { found: false as const, companyId: input.companyId }\n }\n const profileRow = (data.profile ?? null) as Record<string, unknown> | null\n const customFields = (data.customFields ?? {}) as Record<string, unknown>\n\n let related: CustomerRelatedRecords | null = null\n if (includeRelated) {\n related = buildRelatedRecords(data, { includePeople: true })\n }\n return {\n found: true as const,\n company: {\n id: companyRow.id,\n displayName: companyRow.displayName ?? null,\n description: companyRow.description ?? null,\n primaryEmail: companyRow.primaryEmail ?? null,\n primaryPhone: companyRow.primaryPhone ?? null,\n status: companyRow.status ?? null,\n lifecycleStage: companyRow.lifecycleStage ?? null,\n source: companyRow.source ?? null,\n ownerUserId: companyRow.ownerUserId ?? null,\n organizationId: companyRow.organizationId ?? null,\n tenantId: companyRow.tenantId ?? null,\n createdAt: toIso(companyRow.createdAt),\n updatedAt: toIso(companyRow.updatedAt),\n },\n profile: profileRow\n ? {\n id: profileRow.id,\n legalName: profileRow.legalName ?? null,\n brandName: profileRow.brandName ?? null,\n domain: profileRow.domain ?? null,\n websiteUrl: profileRow.websiteUrl ?? null,\n industry: profileRow.industry ?? null,\n sizeBucket: profileRow.sizeBucket ?? null,\n annualRevenue: profileRow.annualRevenue ?? null,\n }\n : null,\n customFields,\n related,\n }\n },\n}\n\nexport const companiesAiTools: CustomersAiToolDefinition[] = [listCompaniesTool, getCompanyTool]\n\nexport default companiesAiTools\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -106,14 +106,25 @@ const analyzeDealsToolDefinition = {
|
|
|
106
106
|
lastActivityByDeal.set(refId, activityDate);
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
|
+
const dealById = new Map(deals.map((deal) => [deal.id, deal]));
|
|
110
|
+
const scopedDealIds = Array.from(dealById.keys());
|
|
109
111
|
const personLinkWhere = {
|
|
110
|
-
deal: { $in:
|
|
112
|
+
deal: { $in: scopedDealIds }
|
|
111
113
|
};
|
|
112
|
-
const
|
|
114
|
+
const personLinksRaw = await em.find(
|
|
113
115
|
CustomerDealPersonLink,
|
|
114
116
|
personLinkWhere,
|
|
115
117
|
{ limit: 500 }
|
|
116
118
|
);
|
|
119
|
+
const personLinks = personLinksRaw.filter((link) => {
|
|
120
|
+
const dealRefId = refIdOf(link.deal);
|
|
121
|
+
if (!dealRefId) return false;
|
|
122
|
+
const scopedDeal = dealById.get(dealRefId);
|
|
123
|
+
if (!scopedDeal) return false;
|
|
124
|
+
if (scopedDeal.tenantId !== tenantId) return false;
|
|
125
|
+
if (ctx.organizationId && scopedDeal.organizationId !== ctx.organizationId) return false;
|
|
126
|
+
return true;
|
|
127
|
+
});
|
|
117
128
|
const linksByDeal = /* @__PURE__ */ new Map();
|
|
118
129
|
for (const link of personLinks) {
|
|
119
130
|
const dealRefId = refIdOf(link.deal);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/customers/ai-tools/deal-analyzer-pack.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * `customers.analyze_deals` \u2014 analytical read-only tool for the deal_analyzer\n * agent (Step d1 of the deal-analyzer-demo).\n *\n * Reads deals scoped to the caller tenant/org, enriches each deal with the\n * most recent activity timestamp (days-since-last-activity), computes a simple\n * healthScore, and returns a ranked list. The handler issues two bounded DB\n * reads (deals + activities) \u2014 no N+1.\n *\n * `customers.update_deal_stage` already exists in deals-pack.ts and is\n * imported and re-exported here so the deal_analyzer agent's `allowedTools`\n * list can reference both under a single pack. The tool itself is NOT\n * redeclared here.\n */\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { z } from 'zod'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n CustomerDeal,\n CustomerActivity,\n CustomerDealPersonLink,\n CustomerEntity,\n} from '../data/entities'\nimport {\n assertTenantScope,\n type CustomersAiToolDefinition,\n type CustomersToolContext,\n} from './types'\n\nfunction refIdOf(ref: unknown): string | undefined {\n if (!ref) return undefined\n if (typeof ref === 'string') return ref\n if (typeof ref === 'object' && typeof (ref as { id?: unknown }).id === 'string') {\n return (ref as { id: string }).id\n }\n return undefined\n}\n\nfunction resolveEm(ctx: CustomersToolContext): EntityManager {\n return ctx.container.resolve<EntityManager>('em')\n}\n\nfunction clamp(value: number, min: number, max: number): number {\n return Math.min(Math.max(value, min), max)\n}\n\n/**\n * Compute a health score for a deal:\n * healthScore = clamp((30 - daysSinceLastActivity) / 30 * 100, 0, 100)\n * A deal with activity within the last 30 days scores > 0. Deals with no\n * activity default to daysSinceLastActivity = activityWindow so they score 0.\n * Exported for unit testing.\n */\nexport function computeHealthScore(daysSinceLastActivity: number): number {\n return clamp(((30 - daysSinceLastActivity) / 30) * 100, 0, 100)\n}\n\nconst analyzeDealsInput = z.object({\n dealStageFilter: z\n .string()\n .optional()\n .describe(\n 'Optional pipeline stage name or status slug to restrict results (e.g. \"open\", \"qualification\", \"negotiation\").',\n ),\n daysOfActivityWindow: z\n .number()\n .int()\n .min(1)\n .max(365)\n .default(30)\n .describe(\n 'Number of days to look back for \"last activity\" calculation. Deals with no activity in this window are flagged as stalled. Default 30.',\n ),\n limit: z\n .number()\n .int()\n .min(1)\n .max(100)\n .default(25)\n .describe('Maximum number of deals to return, ranked by healthScore ascending (most at-risk first). Default 25.'),\n})\n\ntype AnalyzeDealsInput = z.infer<typeof analyzeDealsInput>\n\ntype DealAnalysisItem = {\n id: string\n title: string\n value: number | null\n valueCurrency: string | null\n stage: string | null\n status: string\n daysSinceLastActivity: number\n primaryContact: string | null\n healthScore: number\n}\n\ntype AnalyzeDealsOutput = {\n deals: DealAnalysisItem[]\n totalAnalyzed: number\n stalledCount: number\n windowDays: number\n}\n\nexport function msTodays(ms: number): number {\n return Math.floor(ms / (1000 * 60 * 60 * 24))\n}\n\nconst analyzeDealsToolDefinition: CustomersAiToolDefinition<AnalyzeDealsInput, AnalyzeDealsOutput> = {\n name: 'customers.analyze_deals',\n displayName: 'Analyze deals',\n description:\n 'Fetch and analyze open deals for the caller tenant. Returns each deal with its days-since-last-activity and a health score (0\u2013100, lower = more stalled). Results are ranked most-at-risk first.',\n inputSchema: analyzeDealsInput as z.ZodType<AnalyzeDealsInput>,\n requiredFeatures: ['customers.deals.view'],\n tags: ['read', 'customers', 'analytics'],\n isMutation: false,\n handler: async (rawInput, ctx): Promise<AnalyzeDealsOutput> => {\n const { tenantId } = assertTenantScope(ctx)\n const input = analyzeDealsInput.parse(rawInput)\n const em = resolveEm(ctx)\n const scope = { tenantId, organizationId: ctx.organizationId }\n\n // Build deal filter\n const dealWhere: Record<string, unknown> = {\n tenantId,\n deletedAt: null,\n }\n if (ctx.organizationId) dealWhere.organizationId = ctx.organizationId\n // Apply stage/status filter when provided. `$or` is composed with the\n // outer `deletedAt: null` filter \u2014 both must hold \u2014 so the caller-supplied\n // value matches either the canonical status slug or the stage label.\n if (input.dealStageFilter) {\n const filterValue = input.dealStageFilter.trim()\n if (filterValue.length) {\n dealWhere.$or = [\n { status: filterValue },\n { pipelineStage: filterValue },\n ]\n }\n }\n\n const deals = await findWithDecryption<CustomerDeal>(\n em,\n CustomerDeal,\n dealWhere as any,\n {\n orderBy: { updatedAt: 'desc' },\n limit: Math.min(input.limit * 4, 400), // Fetch extra so we can rank properly\n },\n scope,\n )\n\n if (!deals.length) {\n return { deals: [], totalAnalyzed: 0, stalledCount: 0, windowDays: input.daysOfActivityWindow }\n }\n\n const dealIds = deals.map((d) => d.id)\n\n // Fetch most recent activity per deal within the window in one batch.\n // CustomerActivity has encrypted columns (subject, body); use the\n // tenant-aware helper even though this query only reads occurredAt/deal,\n // so future readers cannot accidentally surface ciphertext.\n const activityWhere: Record<string, unknown> = {\n deal: { $in: dealIds },\n tenantId,\n }\n if (ctx.organizationId) activityWhere.organizationId = ctx.organizationId\n const activities = await findWithDecryption<CustomerActivity>(\n em,\n CustomerActivity,\n activityWhere as any,\n {\n orderBy: { occurredAt: 'desc' },\n limit: 1000,\n },\n scope,\n )\n\n const lastActivityByDeal = new Map<string, Date>()\n for (const activity of activities) {\n const refId = refIdOf((activity as { deal?: unknown }).deal)\n if (!refId) continue\n const activityDate = activity.occurredAt ?? activity.createdAt\n if (!activityDate) continue\n const existing = lastActivityByDeal.get(refId)\n if (!existing || activityDate > existing) {\n lastActivityByDeal.set(refId, activityDate)\n }\n }\n\n // Fetch primary contacts for deals in one batch. CustomerDealPersonLink\n // has no encrypted columns, but the linked CustomerEntity.display_name is\n // encrypted, so resolve names via findWithDecryption rather than\n // populating the relation through raw em.find.\n const personLinkWhere: Record<string, unknown> = {\n deal: { $in: dealIds },\n }\n const personLinks = await em.find(\n CustomerDealPersonLink,\n personLinkWhere as any,\n { limit: 500 },\n )\n\n const linksByDeal = new Map<string, string>() // dealId \u2192 personId (first link wins)\n for (const link of personLinks) {\n const dealRefId = refIdOf((link as { deal?: unknown }).deal)\n const personRefId = refIdOf((link as { person?: unknown }).person)\n if (!dealRefId || !personRefId) continue\n if (!linksByDeal.has(dealRefId)) linksByDeal.set(dealRefId, personRefId)\n }\n\n const primaryContactByDeal = new Map<string, string>()\n const personIds = Array.from(new Set(linksByDeal.values()))\n if (personIds.length) {\n const personWhere: Record<string, unknown> = {\n id: { $in: personIds },\n tenantId,\n }\n if (ctx.organizationId) personWhere.organizationId = ctx.organizationId\n const persons = await findWithDecryption<CustomerEntity>(\n em,\n CustomerEntity,\n personWhere as any,\n undefined,\n scope,\n )\n const nameByPersonId = new Map<string, string>()\n for (const person of persons) {\n const name = person.displayName\n if (typeof name === 'string' && name.length) {\n nameByPersonId.set(person.id, name)\n }\n }\n for (const [dealId, personId] of linksByDeal) {\n const name = nameByPersonId.get(personId)\n if (name) primaryContactByDeal.set(dealId, name)\n }\n }\n\n const now = Date.now()\n const analyzed: DealAnalysisItem[] = deals.map((deal) => {\n const lastActivity = lastActivityByDeal.get(deal.id)\n const daysSinceLastActivity = lastActivity\n ? msTodays(now - lastActivity.getTime())\n : input.daysOfActivityWindow // Treat as fully stalled when no activity\n const value = deal.valueAmount ? parseFloat(String(deal.valueAmount)) : null\n return {\n id: deal.id,\n title: deal.title,\n value: Number.isFinite(value) ? value : null,\n valueCurrency: deal.valueCurrency ?? null,\n stage: deal.pipelineStage ?? deal.status ?? null,\n status: deal.status ?? 'open',\n daysSinceLastActivity,\n primaryContact: primaryContactByDeal.get(deal.id) ?? null,\n healthScore: Math.round(computeHealthScore(daysSinceLastActivity)),\n }\n })\n\n // Sort most-at-risk first (lowest health score first, then by value desc)\n analyzed.sort((a, b) => {\n if (a.healthScore !== b.healthScore) return a.healthScore - b.healthScore\n const aVal = a.value ?? 0\n const bVal = b.value ?? 0\n return bVal - aVal\n })\n\n const truncated = analyzed.slice(0, input.limit)\n const stalledCount = truncated.filter(\n (d) => d.daysSinceLastActivity >= input.daysOfActivityWindow,\n ).length\n\n return {\n deals: truncated,\n totalAnalyzed: deals.length,\n stalledCount,\n windowDays: input.daysOfActivityWindow,\n }\n },\n}\n\nexport const dealAnalyzerAiTools: CustomersAiToolDefinition[] = [\n analyzeDealsToolDefinition as unknown as CustomersAiToolDefinition,\n]\n\nexport default dealAnalyzerAiTools\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["/**\n * `customers.analyze_deals` \u2014 analytical read-only tool for the deal_analyzer\n * agent (Step d1 of the deal-analyzer-demo).\n *\n * Reads deals scoped to the caller tenant/org, enriches each deal with the\n * most recent activity timestamp (days-since-last-activity), computes a simple\n * healthScore, and returns a ranked list. The handler issues two bounded DB\n * reads (deals + activities) \u2014 no N+1.\n *\n * `customers.update_deal_stage` already exists in deals-pack.ts and is\n * imported and re-exported here so the deal_analyzer agent's `allowedTools`\n * list can reference both under a single pack. The tool itself is NOT\n * redeclared here.\n */\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { z } from 'zod'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n CustomerDeal,\n CustomerActivity,\n CustomerDealPersonLink,\n CustomerEntity,\n} from '../data/entities'\nimport {\n assertTenantScope,\n type CustomersAiToolDefinition,\n type CustomersToolContext,\n} from './types'\n\nfunction refIdOf(ref: unknown): string | undefined {\n if (!ref) return undefined\n if (typeof ref === 'string') return ref\n if (typeof ref === 'object' && typeof (ref as { id?: unknown }).id === 'string') {\n return (ref as { id: string }).id\n }\n return undefined\n}\n\nfunction resolveEm(ctx: CustomersToolContext): EntityManager {\n return ctx.container.resolve<EntityManager>('em')\n}\n\nfunction clamp(value: number, min: number, max: number): number {\n return Math.min(Math.max(value, min), max)\n}\n\n/**\n * Compute a health score for a deal:\n * healthScore = clamp((30 - daysSinceLastActivity) / 30 * 100, 0, 100)\n * A deal with activity within the last 30 days scores > 0. Deals with no\n * activity default to daysSinceLastActivity = activityWindow so they score 0.\n * Exported for unit testing.\n */\nexport function computeHealthScore(daysSinceLastActivity: number): number {\n return clamp(((30 - daysSinceLastActivity) / 30) * 100, 0, 100)\n}\n\nconst analyzeDealsInput = z.object({\n dealStageFilter: z\n .string()\n .optional()\n .describe(\n 'Optional pipeline stage name or status slug to restrict results (e.g. \"open\", \"qualification\", \"negotiation\").',\n ),\n daysOfActivityWindow: z\n .number()\n .int()\n .min(1)\n .max(365)\n .default(30)\n .describe(\n 'Number of days to look back for \"last activity\" calculation. Deals with no activity in this window are flagged as stalled. Default 30.',\n ),\n limit: z\n .number()\n .int()\n .min(1)\n .max(100)\n .default(25)\n .describe('Maximum number of deals to return, ranked by healthScore ascending (most at-risk first). Default 25.'),\n})\n\ntype AnalyzeDealsInput = z.infer<typeof analyzeDealsInput>\n\ntype DealAnalysisItem = {\n id: string\n title: string\n value: number | null\n valueCurrency: string | null\n stage: string | null\n status: string\n daysSinceLastActivity: number\n primaryContact: string | null\n healthScore: number\n}\n\ntype AnalyzeDealsOutput = {\n deals: DealAnalysisItem[]\n totalAnalyzed: number\n stalledCount: number\n windowDays: number\n}\n\nexport function msTodays(ms: number): number {\n return Math.floor(ms / (1000 * 60 * 60 * 24))\n}\n\nconst analyzeDealsToolDefinition: CustomersAiToolDefinition<AnalyzeDealsInput, AnalyzeDealsOutput> = {\n name: 'customers.analyze_deals',\n displayName: 'Analyze deals',\n description:\n 'Fetch and analyze open deals for the caller tenant. Returns each deal with its days-since-last-activity and a health score (0\u2013100, lower = more stalled). Results are ranked most-at-risk first.',\n inputSchema: analyzeDealsInput as z.ZodType<AnalyzeDealsInput>,\n requiredFeatures: ['customers.deals.view'],\n tags: ['read', 'customers', 'analytics'],\n isMutation: false,\n handler: async (rawInput, ctx): Promise<AnalyzeDealsOutput> => {\n const { tenantId } = assertTenantScope(ctx)\n const input = analyzeDealsInput.parse(rawInput)\n const em = resolveEm(ctx)\n const scope = { tenantId, organizationId: ctx.organizationId }\n\n // Build deal filter\n const dealWhere: Record<string, unknown> = {\n tenantId,\n deletedAt: null,\n }\n if (ctx.organizationId) dealWhere.organizationId = ctx.organizationId\n // Apply stage/status filter when provided. `$or` is composed with the\n // outer `deletedAt: null` filter \u2014 both must hold \u2014 so the caller-supplied\n // value matches either the canonical status slug or the stage label.\n if (input.dealStageFilter) {\n const filterValue = input.dealStageFilter.trim()\n if (filterValue.length) {\n dealWhere.$or = [\n { status: filterValue },\n { pipelineStage: filterValue },\n ]\n }\n }\n\n const deals = await findWithDecryption<CustomerDeal>(\n em,\n CustomerDeal,\n dealWhere as any,\n {\n orderBy: { updatedAt: 'desc' },\n limit: Math.min(input.limit * 4, 400), // Fetch extra so we can rank properly\n },\n scope,\n )\n\n if (!deals.length) {\n return { deals: [], totalAnalyzed: 0, stalledCount: 0, windowDays: input.daysOfActivityWindow }\n }\n\n const dealIds = deals.map((d) => d.id)\n\n // Fetch most recent activity per deal within the window in one batch.\n // CustomerActivity has encrypted columns (subject, body); use the\n // tenant-aware helper even though this query only reads occurredAt/deal,\n // so future readers cannot accidentally surface ciphertext.\n const activityWhere: Record<string, unknown> = {\n deal: { $in: dealIds },\n tenantId,\n }\n if (ctx.organizationId) activityWhere.organizationId = ctx.organizationId\n const activities = await findWithDecryption<CustomerActivity>(\n em,\n CustomerActivity,\n activityWhere as any,\n {\n orderBy: { occurredAt: 'desc' },\n limit: 1000,\n },\n scope,\n )\n\n const lastActivityByDeal = new Map<string, Date>()\n for (const activity of activities) {\n const refId = refIdOf((activity as { deal?: unknown }).deal)\n if (!refId) continue\n const activityDate = activity.occurredAt ?? activity.createdAt\n if (!activityDate) continue\n const existing = lastActivityByDeal.get(refId)\n if (!existing || activityDate > existing) {\n lastActivityByDeal.set(refId, activityDate)\n }\n }\n\n // Fetch primary contacts for deals in one batch. CustomerDealPersonLink\n // has no encrypted columns, but the linked CustomerEntity.display_name is\n // encrypted, so resolve names via findWithDecryption rather than\n // populating the relation through raw em.find.\n // Defense-in-depth: `customer_deal_people` carries no tenant/org columns,\n // so DB-level scoping is via the tenant-filtered `dealIds` set. Filter the\n // returned rows against the caller-scoped deals map so a future refactor\n // that widens `dealIds` cannot surface cross-tenant links.\n const dealById = new Map<string, CustomerDeal>(deals.map((deal) => [deal.id, deal]))\n const scopedDealIds = Array.from(dealById.keys())\n const personLinkWhere: Record<string, unknown> = {\n deal: { $in: scopedDealIds },\n }\n const personLinksRaw = await em.find(\n CustomerDealPersonLink,\n personLinkWhere as any,\n { limit: 500 },\n )\n const personLinks = personLinksRaw.filter((link) => {\n const dealRefId = refIdOf((link as { deal?: unknown }).deal)\n if (!dealRefId) return false\n const scopedDeal = dealById.get(dealRefId)\n if (!scopedDeal) return false\n if (scopedDeal.tenantId !== tenantId) return false\n if (ctx.organizationId && scopedDeal.organizationId !== ctx.organizationId) return false\n return true\n })\n\n const linksByDeal = new Map<string, string>() // dealId \u2192 personId (first link wins)\n for (const link of personLinks) {\n const dealRefId = refIdOf((link as { deal?: unknown }).deal)\n const personRefId = refIdOf((link as { person?: unknown }).person)\n if (!dealRefId || !personRefId) continue\n if (!linksByDeal.has(dealRefId)) linksByDeal.set(dealRefId, personRefId)\n }\n\n const primaryContactByDeal = new Map<string, string>()\n const personIds = Array.from(new Set(linksByDeal.values()))\n if (personIds.length) {\n const personWhere: Record<string, unknown> = {\n id: { $in: personIds },\n tenantId,\n }\n if (ctx.organizationId) personWhere.organizationId = ctx.organizationId\n const persons = await findWithDecryption<CustomerEntity>(\n em,\n CustomerEntity,\n personWhere as any,\n undefined,\n scope,\n )\n const nameByPersonId = new Map<string, string>()\n for (const person of persons) {\n const name = person.displayName\n if (typeof name === 'string' && name.length) {\n nameByPersonId.set(person.id, name)\n }\n }\n for (const [dealId, personId] of linksByDeal) {\n const name = nameByPersonId.get(personId)\n if (name) primaryContactByDeal.set(dealId, name)\n }\n }\n\n const now = Date.now()\n const analyzed: DealAnalysisItem[] = deals.map((deal) => {\n const lastActivity = lastActivityByDeal.get(deal.id)\n const daysSinceLastActivity = lastActivity\n ? msTodays(now - lastActivity.getTime())\n : input.daysOfActivityWindow // Treat as fully stalled when no activity\n const value = deal.valueAmount ? parseFloat(String(deal.valueAmount)) : null\n return {\n id: deal.id,\n title: deal.title,\n value: Number.isFinite(value) ? value : null,\n valueCurrency: deal.valueCurrency ?? null,\n stage: deal.pipelineStage ?? deal.status ?? null,\n status: deal.status ?? 'open',\n daysSinceLastActivity,\n primaryContact: primaryContactByDeal.get(deal.id) ?? null,\n healthScore: Math.round(computeHealthScore(daysSinceLastActivity)),\n }\n })\n\n // Sort most-at-risk first (lowest health score first, then by value desc)\n analyzed.sort((a, b) => {\n if (a.healthScore !== b.healthScore) return a.healthScore - b.healthScore\n const aVal = a.value ?? 0\n const bVal = b.value ?? 0\n return bVal - aVal\n })\n\n const truncated = analyzed.slice(0, input.limit)\n const stalledCount = truncated.filter(\n (d) => d.daysSinceLastActivity >= input.daysOfActivityWindow,\n ).length\n\n return {\n deals: truncated,\n totalAnalyzed: deals.length,\n stalledCount,\n windowDays: input.daysOfActivityWindow,\n }\n },\n}\n\nexport const dealAnalyzerAiTools: CustomersAiToolDefinition[] = [\n analyzeDealsToolDefinition as unknown as CustomersAiToolDefinition,\n]\n\nexport default dealAnalyzerAiTools\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -8,6 +8,11 @@ import {
|
|
|
8
8
|
CustomerDeal,
|
|
9
9
|
CustomerPipelineStage
|
|
10
10
|
} from "../data/entities.js";
|
|
11
|
+
import {
|
|
12
|
+
dealClosureOutcomeFromStatus,
|
|
13
|
+
loadClosurePipelineStageSnapshot
|
|
14
|
+
} from "../lib/closureStage.js";
|
|
15
|
+
import { canonicalDealStatus, isClosedDealStatus } from "../lib/dealStatus.js";
|
|
11
16
|
import {
|
|
12
17
|
assertTenantScope
|
|
13
18
|
} from "./types.js";
|
|
@@ -29,7 +34,7 @@ const listDealsInput = z.object({
|
|
|
29
34
|
const listDealsTool = defineApiBackedAiTool({
|
|
30
35
|
name: "customers.list_deals",
|
|
31
36
|
displayName: "List deals",
|
|
32
|
-
description:
|
|
37
|
+
description: 'Search / list deals for the caller tenant + organization. Optional filters include linked person / company / pipeline stage. Returns { items, total, totalIsCapped, limit, offset }. When totalIsCapped is true, total is a floor ("at least N", render it as "N+"), and pagination is exhausted only when a page returns fewer than limit items \u2014 never when offset reaches total.',
|
|
33
38
|
inputSchema: listDealsInput,
|
|
34
39
|
requiredFeatures: ["customers.deals.view"],
|
|
35
40
|
toOperation: (input, ctx) => {
|
|
@@ -83,6 +88,9 @@ const listDealsTool = defineApiBackedAiTool({
|
|
|
83
88
|
};
|
|
84
89
|
}),
|
|
85
90
|
total: typeof data.total === "number" ? data.total : 0,
|
|
91
|
+
// A capped count reports a floor: phrase the total as "at least N" and
|
|
92
|
+
// never treat it as proof that pagination is exhausted.
|
|
93
|
+
totalIsCapped: data.totalIsCapped === true,
|
|
86
94
|
limit,
|
|
87
95
|
offset
|
|
88
96
|
};
|
|
@@ -298,34 +306,72 @@ const updateDealStageTool = {
|
|
|
298
306
|
afterPipelineStageLabel = stage?.label ?? input.toPipelineStageId;
|
|
299
307
|
} else if (input.toStage) {
|
|
300
308
|
afterStatus = input.toStage;
|
|
309
|
+
const organizationId = deal.organizationId ?? ctx.organizationId ?? null;
|
|
310
|
+
const outcome = dealClosureOutcomeFromStatus(input.toStage);
|
|
311
|
+
if (outcome && organizationId) {
|
|
312
|
+
const terminalStage = await loadClosurePipelineStageSnapshot(em, {
|
|
313
|
+
pipelineId: deal.pipelineId ?? null,
|
|
314
|
+
closureOutcome: outcome,
|
|
315
|
+
tenantId,
|
|
316
|
+
organizationId
|
|
317
|
+
});
|
|
318
|
+
if (terminalStage) {
|
|
319
|
+
afterPipelineStageId = terminalStage.id;
|
|
320
|
+
afterPipelineStageLabel = terminalStage.label;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
301
323
|
}
|
|
302
324
|
const beforeStatus = deal.status ?? null;
|
|
303
325
|
const beforePipelineStageId = deal.pipelineStageId ?? null;
|
|
304
326
|
const beforePipelineStageLabel = deal.pipelineStage ?? beforePipelineStageId;
|
|
327
|
+
const beforeClosureOutcome = deal.closureOutcome ?? null;
|
|
328
|
+
const beforeLossReasonId = deal.lossReasonId ?? null;
|
|
329
|
+
const beforeLossNotes = deal.lossNotes ?? null;
|
|
330
|
+
const requestedOutcome = dealClosureOutcomeFromStatus(input.toStage);
|
|
331
|
+
const clearsClosure = input.toPipelineStageId === void 0 && input.toStage !== void 0 && !requestedOutcome && !isClosedDealStatus(canonicalDealStatus(input.toStage));
|
|
332
|
+
const afterClosureOutcome = clearsClosure ? null : requestedOutcome ?? beforeClosureOutcome;
|
|
333
|
+
const closureOutcomeCleared = beforeClosureOutcome !== null && afterClosureOutcome === null;
|
|
334
|
+
const afterLossReasonId = closureOutcomeCleared ? null : beforeLossReasonId;
|
|
335
|
+
const afterLossNotes = closureOutcomeCleared ? null : beforeLossNotes;
|
|
305
336
|
return {
|
|
306
337
|
recordId: deal.id,
|
|
307
338
|
entityType: "customers.deal",
|
|
308
339
|
recordVersion: recordVersionFromUpdatedAt(deal.updatedAt),
|
|
309
340
|
before: {
|
|
310
341
|
status: beforeStatus,
|
|
311
|
-
pipelineStageId: beforePipelineStageId
|
|
342
|
+
pipelineStageId: beforePipelineStageId,
|
|
343
|
+
closureOutcome: beforeClosureOutcome,
|
|
344
|
+
lossReasonId: beforeLossReasonId,
|
|
345
|
+
lossNotes: beforeLossNotes
|
|
312
346
|
},
|
|
313
347
|
after: {
|
|
314
348
|
status: afterStatus,
|
|
315
|
-
pipelineStageId: afterPipelineStageId
|
|
349
|
+
pipelineStageId: afterPipelineStageId,
|
|
350
|
+
closureOutcome: afterClosureOutcome,
|
|
351
|
+
lossReasonId: afterLossReasonId,
|
|
352
|
+
lossNotes: afterLossNotes
|
|
316
353
|
},
|
|
317
354
|
display: {
|
|
318
355
|
fieldLabels: {
|
|
319
356
|
status: "Status",
|
|
320
|
-
pipelineStageId: "Pipeline stage"
|
|
357
|
+
pipelineStageId: "Pipeline stage",
|
|
358
|
+
closureOutcome: "Closure outcome",
|
|
359
|
+
lossReasonId: "Loss reason",
|
|
360
|
+
lossNotes: "Loss notes"
|
|
321
361
|
},
|
|
322
362
|
before: {
|
|
323
363
|
...beforeStatus ? { status: titleStatus(beforeStatus) } : {},
|
|
324
|
-
...beforePipelineStageLabel ? { pipelineStageId: beforePipelineStageLabel } : {}
|
|
364
|
+
...beforePipelineStageLabel ? { pipelineStageId: beforePipelineStageLabel } : {},
|
|
365
|
+
...beforeClosureOutcome ? { closureOutcome: titleStatus(beforeClosureOutcome) } : {},
|
|
366
|
+
...beforeLossReasonId ? { lossReasonId: beforeLossReasonId } : {},
|
|
367
|
+
...beforeLossNotes ? { lossNotes: beforeLossNotes } : {}
|
|
325
368
|
},
|
|
326
369
|
after: {
|
|
327
370
|
...afterStatus ? { status: titleStatus(afterStatus) } : {},
|
|
328
|
-
...afterPipelineStageLabel ? { pipelineStageId: afterPipelineStageLabel } : {}
|
|
371
|
+
...afterPipelineStageLabel ? { pipelineStageId: afterPipelineStageLabel } : {},
|
|
372
|
+
...afterClosureOutcome ? { closureOutcome: titleStatus(afterClosureOutcome) } : closureOutcomeCleared ? { closureOutcome: "\u2014" } : {},
|
|
373
|
+
...afterLossReasonId ? { lossReasonId: afterLossReasonId } : {},
|
|
374
|
+
...afterLossNotes ? { lossNotes: afterLossNotes } : {}
|
|
329
375
|
}
|
|
330
376
|
}
|
|
331
377
|
};
|
|
@@ -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/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
|
-
"mappings": "AAiBA,SAAS,SAAS;AAClB,SAAS,6BAA6B;AACtC;AAAA,EACE;AAAA,OAGK;AACP,SAAS,6BAA6B;AACtC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,OAIK;AAEP,SAAS,UAAU,KAAmE;AACpF,SAAO,IAAI,UAAU,QAAuB,IAAI;AAClD;AAEA,SAAS,WAAW,KAAoD,UAAkB;AACxF,SAAO,EAAE,UAAU,gBAAgB,IAAI,eAAe;AACxD;AAEA,MAAM,iBAAiB,EACpB,OAAO;AAAA,EACN,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,wFAAwF;AAAA,EACjI,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,SAAS,EAAE,SAAS,+CAA+C;AAAA,EAC3G,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,SAAS,EAAE,SAAS,qCAAqC;AAAA,EACzF,UAAU,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,oDAAoD;AAAA,EACpG,WAAW,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,qDAAqD;AAAA,EACtG,iBAAiB,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,2CAA2C;AAAA,EAClG,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,qDAAqD;AAC9F,CAAC,EACA,YAAY;AA2Cf,MAAM,gBAAgB,sBAA6E;AAAA,EACjG,MAAM;AAAA,EACN,aAAa;AAAA,EACb,aACE;AAAA,EACF,aAAa;AAAA,EACb,kBAAkB,CAAC,sBAAsB;AAAA,EACzC,aAAa,CAAC,OAAO,QAAQ;AAC3B,sBAAkB,GAAsC;AACxD,UAAM,QAAQ,MAAM,SAAS;AAC7B,UAAM,SAAS,MAAM,UAAU;AAC/B,UAAM,OAAO,KAAK,MAAM,SAAS,KAAK,IAAI;AAE1C,UAAM,QAAsE;AAAA,MAC1E;AAAA,MACA,UAAU;AAAA,IACZ;AACA,QAAI,MAAM,GAAG,KAAK,EAAG,OAAM,SAAS,MAAM,EAAE,KAAK;AACjD,QAAI,MAAM,SAAU,OAAM,WAAW,MAAM;AAC3C,QAAI,MAAM,UAAW,OAAM,YAAY,MAAM;AAC7C,QAAI,MAAM,gBAAiB,OAAM,kBAAkB,MAAM;AACzD,QAAI,MAAM,OAAQ,OAAM,SAAS,MAAM;AAEvC,UAAM,YAAmC;AAAA,MACvC,QAAQ;AAAA,MACR,MAAM;AAAA,MACN;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAAA,EACA,aAAa,CAAC,UAAU,UAAU;AAChC,UAAM,QAAQ,MAAM,SAAS;AAC7B,UAAM,SAAS,MAAM,UAAU;AAC/B,UAAM,OAAQ,SAAS,QAAQ,CAAC;AAChC,UAAM,WAA+B,MAAM,QAAQ,KAAK,KAAK,IAAI,KAAK,QAAQ,CAAC;AAC/E,WAAO;AAAA,MACL,OAAO,SAAS,IAAI,CAAC,QAAQ;AAC3B,cAAM,mBAAmB,IAAI,qBAAqB,IAAI,mBAAmB;AACzE,cAAM,kBAAkB,mBAAmB,IAAI,KAAK,OAAO,gBAAgB,CAAC,EAAE,YAAY,IAAI;AAC9F,cAAM,eAAe,IAAI,cAAc,IAAI,aAAa;AACxD,cAAM,YAAY,eAAe,IAAI,KAAK,OAAO,YAAY,CAAC,EAAE,YAAY,IAAI;AAChF,eAAO;AAAA,UACL,IAAI,IAAI;AAAA,UACR,OAAO,IAAI,SAAS;AAAA,UACpB,aAAa,IAAI,eAAe;AAAA,UAChC,QAAQ,IAAI,UAAU;AAAA,UACtB,YAAY,IAAI,eAAe,IAAI,cAAc;AAAA,UACjD,iBAAiB,IAAI,qBAAqB,IAAI,mBAAmB;AAAA,UACjE,aAAa,IAAI,gBAAgB,IAAI,eAAe;AAAA,UACpD,eAAe,IAAI,kBAAkB,IAAI,iBAAiB;AAAA,UAC1D,aAAa,IAAI,eAAe;AAAA,UAChC,aAAa,IAAI,iBAAiB,IAAI,eAAe;AAAA,UACrD;AAAA,UACA,QAAQ,IAAI,UAAU;AAAA,UACtB,gBAAgB,IAAI,mBAAmB,IAAI,kBAAkB;AAAA,UAC7D,UAAU,IAAI,aAAa,IAAI,YAAY;AAAA,UAC3C;AAAA,QACF;AAAA,MACF,CAAC;AAAA,MACD,OAAO,OAAO,KAAK,UAAU,WAAW,KAAK,QAAQ;AAAA,MACrD;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACF,CAAC;AAED,MAAM,eAAe,EAAE,OAAO;AAAA,EAC5B,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,iBAAiB;AAAA,EACpD,gBAAgB,EACb,QAAQ,EACR,SAAS,EACT,SAAS,yFAAyF;AACvG,CAAC;AAID,SAAS,UAAU,OAA+B;AAChD,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,KAAK,iBAAiB,OAAO,QAAQ,IAAI,KAAK,OAAO,KAAK,CAAC;AACjE,MAAI,OAAO,MAAM,GAAG,QAAQ,CAAC,EAAG,QAAO;AACvC,SAAO,GAAG,YAAY;AACxB;AAEA,MAAM,cAAyC;AAAA,EAC7C,MAAM;AAAA,EACN,aAAa;AAAA,EACb,aACE;AAAA,EACF,aAAa;AAAA,EACb,kBAAkB,CAAC,sBAAsB;AAAA,EACzC,MAAM,CAAC,QAAQ,WAAW;AAAA,EAC1B,SAAS,OAAO,UAAU,QAAQ;AAChC,UAAM,EAAE,UAAU,UAAU,IAAI,kBAAkB,GAAG;AACrD,SAAK;AACL,UAAM,QAAsB,aAAa,MAAM,QAAQ;AACvD,UAAM,iBAAiB,CAAC,CAAC,MAAM;AAC/B,UAAM,SAAS,2BAA2B,GAAwC;AAElF,UAAM,iBAAiB,MAAM,OAAO,IAA6B;AAAA,MAC/D,QAAQ;AAAA,MACR,MAAM,oBAAoB,MAAM,MAAM;AAAA,IACxC,CAAC;AACD,QAAI,CAAC,eAAe,SAAS;AAC3B,UAAI,eAAe,eAAe,OAAO,eAAe,eAAe,KAAK;AAC1E,eAAO,EAAE,OAAO,OAAgB,QAAQ,MAAM,OAAO;AAAA,MACvD;AACA,YAAM,IAAI,MAAM,eAAe,SAAS,wBAAwB,MAAM,MAAM,EAAE;AAAA,IAChF;AACA,UAAM,SAAU,eAAe,QAAQ,CAAC;AACxC,UAAM,UAAW,OAAO,QAAQ;AAChC,QAAI,CAAC,SAAS;AACZ,aAAO,EAAE,OAAO,OAAgB,QAAQ,MAAM,OAAO;AAAA,IACvD;AACA,UAAM,eAAgB,OAAO,gBAAgB,CAAC;AAC9C,UAAM,aAAa,MAAM,QAAQ,OAAO,MAAM,IAAK,OAAO,SAA4C,CAAC;AACvG,UAAM,gBAAgB,MAAM,QAAQ,OAAO,SAAS,IAC/C,OAAO,YACR,CAAC;AAEL,QAAI,UAA0C;AAC9C,QAAI,gBAAgB;AAClB,YAAM,CAAC,oBAAoB,gBAAgB,IAAI,MAAM,QAAQ,IAAI;AAAA,QAC/D,OAAO,IAAgE;AAAA,UACrE,QAAQ;AAAA,UACR,MAAM;AAAA,UACN,OAAO,EAAE,QAAQ,MAAM,QAAQ,MAAM,GAAG,UAAU,KAAK,WAAW,cAAc,SAAS,OAAO;AAAA,QAClG,CAAC;AAAA,QACD,OAAO,IAAgE;AAAA,UACrE,QAAQ;AAAA,UACR,MAAM;AAAA,UACN,OAAO,EAAE,QAAQ,MAAM,QAAQ,MAAM,GAAG,UAAU,IAAI;AAAA,QACxD,CAAC;AAAA,MACH,CAAC;AACD,YAAM,aACJ,mBAAmB,WAAW,MAAM,QAAQ,mBAAmB,MAAM,KAAK,IACrE,mBAAmB,KAAM,QAC1B,CAAC;AACP,YAAM,WACJ,iBAAiB,WAAW,MAAM,QAAQ,iBAAiB,MAAM,KAAK,IACjE,iBAAiB,KAAM,QACxB,CAAC;AAEP,gBAAU;AAAA,QACR,YAAY,WAAW,IAAI,CAAC,cAAc;AAAA,UACxC,IAAI,SAAS;AAAA,UACb,cAAc,SAAS,gBAAgB,SAAS,iBAAiB;AAAA,UACjE,SAAS,SAAS,WAAW;AAAA,UAC7B,MAAM,SAAS,QAAQ;AAAA,UACvB,YAAY,UAAU,SAAS,cAAc,SAAS,WAAW;AAAA,UACjE,WAAW,UAAU,SAAS,aAAa,SAAS,UAAU;AAAA,QAChE,EAAE;AAAA,QACF,OAAO,SAAS,IAAI,CAAC,aAAa;AAAA,UAChC,IAAI,QAAQ;AAAA,UACZ,MAAM,QAAQ;AAAA,UACd,cAAc,QAAQ,gBAAgB,QAAQ,kBAAkB;AAAA,UAChE,WAAW,UAAU,QAAQ,aAAa,QAAQ,UAAU;AAAA,QAC9D,EAAE;AAAA,QACF,QAAQ,WACL,IAAI,CAAC,WAAW;AACf,cAAI,CAAC,UAAU,OAAO,WAAW,SAAU,QAAO;AAClD,gBAAM,KAAK,OAAO,OAAO,OAAO,WAAW,OAAO,KAAK;AACvD,cAAI,CAAC,GAAI,QAAO;AAChB,gBAAM,WAAW,OAAO,OAAO,aAAa,WAAW,OAAO,WAAW;AACzE,gBAAM,QAAQ,OAAO,OAAO,UAAU,WAAW,OAAO,QAAQ;AAChE,gBAAM,QAMF;AAAA,YACF;AAAA,YACA,aAAa;AAAA,YACb,cAAc,YAAY,SAAS,SAAS,GAAG,IAAI,WAAW;AAAA,YAC9D,cAAc,YAAY,CAAC,SAAS,SAAS,GAAG,IAAI,WAAW;AAAA,YAC/D,iBAAiB;AAAA,UACnB;AACA,iBAAO;AAAA,QACT,CAAC,EACA;AAAA,UACC,CAAC,UAMI,UAAU;AAAA,QACjB;AAAA,QACF,WAAW,cACR,IAAI,CAAC,YAAY;AAChB,cAAI,CAAC,WAAW,OAAO,YAAY,SAAU,QAAO;AACpD,gBAAM,KAAK,OAAO,QAAQ,OAAO,WAAW,QAAQ,KAAK;AACzD,cAAI,CAAC,GAAI,QAAO;AAChB,gBAAM,QAAQ,OAAO,QAAQ,UAAU,WAAW,QAAQ,QAAQ;AAClE,gBAAM,QAKF;AAAA,YACF;AAAA,YACA,aAAa;AAAA,YACb,cAAc;AAAA,YACd,cAAc;AAAA,UAChB;AACA,iBAAO;AAAA,QACT,CAAC,EACA;AAAA,UACC,CAAC,UAKI,UAAU;AAAA,QACjB;AAAA,MACJ;AAAA,IACF;AAEA,WAAO;AAAA,MACL,OAAO;AAAA,MACP,MAAM;AAAA,QACJ,IAAI,QAAQ;AAAA,QACZ,OAAO,OAAO,QAAQ,UAAU,WAAW,QAAQ,QAAQ;AAAA,QAC3D,aAAa,QAAQ,eAAe;AAAA,QACpC,QAAQ,QAAQ,UAAU;AAAA,QAC1B,YAAY,QAAQ,cAAc;AAAA,QAClC,iBAAiB,QAAQ,mBAAmB;AAAA,QAC5C,aAAa,QAAQ,eAAe;AAAA,QACpC,eAAe,QAAQ,iBAAiB;AAAA,QACxC,aAAa,QAAQ,eAAe;AAAA,QACpC,aAAa,QAAQ,eAAe;AAAA,QACpC,iBAAiB,UAAU,QAAQ,eAAe;AAAA,QAClD,QAAQ,QAAQ,UAAU;AAAA,QAC1B,gBAAgB,QAAQ,kBAAkB;AAAA,QAC1C,UAAU,QAAQ,YAAY;AAAA,QAC9B,WAAW,UAAU,QAAQ,SAAS;AAAA,QACtC,WAAW,UAAU,QAAQ,SAAS;AAAA,MACxC;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACF;AAkBA,MAAM,mBAAmB,CAAC,UAA4B;AACpD,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,QAAM,UAAU,MAAM,KAAK;AAC3B,SAAO,QAAQ,WAAW,IAAI,SAAY;AAC5C;AAEA,MAAM,uBAAuB,EAC1B,OAAO;AAAA,EACN,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,2BAA2B;AAAA,EAC9D,mBAAmB,EAChB,WAAW,kBAAkB,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,CAAC,EACzD,SAAS,+EAA0E;AAAA,EACtF,SAAS,EACN,WAAW,kBAAkB,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,EAAE,EAAE,SAAS,CAAC,EACjE;AAAA,IACC;AAAA,EACF;AACJ,CAAC,EACA;AAAA,EACC,CAAC,UAAU,QAAQ,MAAM,iBAAiB,MAAM,QAAQ,MAAM,OAAO;AAAA,EACrE;AAAA,IACE,SAAS;AAAA,IACT,MAAM,CAAC,mBAAmB;AAAA,EAC5B;AACF;AAIF,SAAS,2BAA2B,WAAmD;AACrF,MAAI,CAAC,UAAW,QAAO;AACvB,QAAM,QAAQ,qBAAqB,OAAO,YAAY,IAAI,KAAK,SAAS;AACxE,MAAI,OAAO,MAAM,MAAM,QAAQ,CAAC,EAAG,QAAO;AAC1C,SAAO,MAAM,YAAY;AAC3B;AAEA,SAAS,YAAY,OAAsD;AACzE,MAAI,CAAC,MAAO,QAAO;AACnB,SAAO,MACJ,MAAM,SAAS,EACf,OAAO,OAAO,EACd,IAAI,CAAC,SAAS,KAAK,OAAO,CAAC,EAAE,YAAY,IAAI,KAAK,MAAM,CAAC,CAAC,EAC1D,KAAK,GAAG;AACb;AAEA,eAAe,kBACb,IACA,KACA,UACA,QAC8B;AAC9B,QAAM,QAAiC,EAAE,IAAI,QAAQ,UAAU,WAAW,KAAK;AAC/E,MAAI,IAAI,eAAgB,OAAM,iBAAiB,IAAI;AACnD,QAAM,OAAO,MAAM;AAAA,IACjB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,WAAW,KAAK,QAAQ;AAAA,EAC1B;AACA,MAAI,CAAC,QAAQ,KAAK,aAAa,SAAU,QAAO;AAChD,MAAI,IAAI,kBAAkB,KAAK,mBAAmB,IAAI,eAAgB,QAAO;AAC7E,SAAO;AACT;AAEA,eAAe,kBACb,IACA,KACA,UACA,SACA,gBACuC;AACvC,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,MACE,IAAI;AAAA,MACJ;AAAA,MACA;AAAA,IACF;AAAA,IACA;AAAA,IACA,WAAW,KAAK,QAAQ;AAAA,EAC1B;AACF;AAEA,MAAM,sBAAiD;AAAA,EACrD,MAAM;AAAA,EACN,aAAa;AAAA,EACb,aACE;AAAA,EACF,aAAa;AAAA,EACb,kBAAkB,CAAC,wBAAwB;AAAA,EAC3C,MAAM,CAAC,SAAS,WAAW;AAAA,EAC3B,YAAY;AAAA,EACZ,kBAAkB,OAAO,UAAU,QAA6D;AAC9F,UAAM,EAAE,SAAS,IAAI,kBAAkB,GAAG;AAC1C,UAAM,QAA8B,qBAAqB,MAAM,QAAQ;AACvE,UAAM,KAAK,UAAU,GAAG;AACxB,UAAM,OAAO,MAAM,kBAAkB,IAAI,KAAK,UAAU,MAAM,MAAM;AACpE,QAAI,CAAC,KAAM,QAAO;AAClB,QAAI,cAAc,KAAK,UAAU;AACjC,QAAI,uBAAuB,KAAK,mBAAmB;AACnD,QAAI,0BAA0B,KAAK,iBAAiB;AACpD,QAAI,MAAM,mBAAmB;AAC3B,YAAM,iBAAiB,KAAK,kBAAkB,IAAI,kBAAkB;AACpE,YAAM,QAAQ,iBACV,MAAM,kBAAkB,IAAI,KAAK,UAAU,MAAM,mBAAmB,cAAc,IAClF;AACJ,6BAAuB,MAAM;AAC7B,gCAA0B,OAAO,SAAS,MAAM;AAAA,IAClD,WAAW,MAAM,SAAS;AACxB,oBAAc,MAAM;AAAA,IACtB;AACA,UAAM,eAAe,KAAK,UAAU;AACpC,UAAM,wBAAwB,KAAK,mBAAmB;AACtD,UAAM,2BAA2B,KAAK,iBAAiB;AACvD,WAAO;AAAA,MACL,UAAU,KAAK;AAAA,MACf,YAAY;AAAA,MACZ,eAAe,2BAA2B,KAAK,SAAS;AAAA,MACxD,QAAQ;AAAA,QACN,QAAQ;AAAA,QACR,iBAAiB;AAAA,MACnB;AAAA,MACA,OAAO;AAAA,QACL,QAAQ;AAAA,QACR,iBAAiB;AAAA,MACnB;AAAA,MACA,SAAS;AAAA,QACP,aAAa;AAAA,UACX,QAAQ;AAAA,UACR,iBAAiB;AAAA,QACnB;AAAA,QACA,QAAQ;AAAA,UACN,GAAI,eAAe,EAAE,QAAQ,YAAY,YAAY,EAAE,IAAI,CAAC;AAAA,UAC5D,GAAI,2BAA2B,EAAE,iBAAiB,yBAAyB,IAAI,CAAC;AAAA,QAClF;AAAA,QACA,OAAO;AAAA,UACL,GAAI,cAAc,EAAE,QAAQ,YAAY,WAAW,EAAE,IAAI,CAAC;AAAA,UAC1D,GAAI,0BAA0B,EAAE,iBAAiB,wBAAwB,IAAI,CAAC;AAAA,QAChF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EACA,SAAS,OAAO,UAAU,QAAQ;AAChC,UAAM,EAAE,SAAS,IAAI,kBAAkB,GAAG;AAC1C,UAAM,QAA8B,qBAAqB,MAAM,QAAQ;AACvE,UAAM,KAAK,UAAU,GAAG;AACxB,UAAM,OAAO,MAAM,kBAAkB,IAAI,KAAK,UAAU,MAAM,MAAM;AACpE,QAAI,CAAC,MAAM;AACT,YAAM,IAAI,MAAM,SAAS,MAAM,MAAM,oCAAoC;AAAA,IAC3E;AACA,UAAM,iBAAiB,KAAK;AAC5B,QAAI,CAAC,gBAAgB;AACnB,YAAM,IAAI,MAAM,SAAS,MAAM,MAAM,8BAA8B;AAAA,IACrE;AAEA,UAAM,SAAS;AAAA,MACb,QAAQ,KAAK,UAAU;AAAA,MACvB,eAAe,KAAK,iBAAiB;AAAA,MACrC,iBAAiB,KAAK,mBAAmB;AAAA,IAC3C;AAEA,UAAM,OAAgC;AAAA,MACpC,IAAI,KAAK;AAAA,MACT;AAAA,MACA;AAAA,IACF;AACA,QAAI,MAAM,mBAAmB;AAC3B,YAAM,QAAQ,MAAM,kBAAkB,IAAI,KAAK,UAAU,MAAM,mBAAmB,cAAc;AAChG,UAAI,CAAC,OAAO;AACV,cAAM,IAAI,MAAM,2BAA2B;AAAA,MAC7C;AACA,WAAK,kBAAkB,MAAM;AAAA,IAC/B,WAAW,MAAM,SAAS;AACxB,WAAK,SAAS,MAAM;AAAA,IACtB;AAEA,UAAM,SAAS,2BAA2B,GAAwC;AAClF,UAAM,WAAW,MAAM,OAAO,IAAI;AAAA,MAChC,QAAQ;AAAA,MACR,MAAM;AAAA,MACN;AAAA,IACF,CAAC;AACD,QAAI,CAAC,SAAS,SAAS;AACrB,YAAM,IAAI,MAAM,SAAS,SAAS,0BAA0B,KAAK,EAAE,GAAG;AAAA,IACxE;AAEA,UAAM,QAAQ,MAAM,kBAAkB,IAAI,KAAK,UAAU,KAAK,EAAE;AAChE,WAAO;AAAA,MACL,UAAU,KAAK;AAAA,MACf,aAAa;AAAA,MACb;AAAA,MACA,OAAO,QACH;AAAA,QACE,QAAQ,MAAM,UAAU;AAAA,QACxB,eAAe,MAAM,iBAAiB;AAAA,QACtC,iBAAiB,MAAM,mBAAmB;AAAA,MAC5C,IACA;AAAA,IACN;AAAA,EACF;AACF;AAEO,MAAM,eAA4C,CAAC,eAAe,aAAa,mBAAmB;AAEzG,IAAO,qBAAQ;",
|
|
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 dealClosureOutcomeFromStatus,\n loadClosurePipelineStageSnapshot,\n} from '../lib/closureStage'\nimport { canonicalDealStatus, isClosedDealStatus } from '../lib/dealStatus'\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. Returns { items, total, totalIsCapped, limit, offset }. When totalIsCapped is true, total is a floor (\"at least N\", render it as \"N+\"), and pagination is exhausted only when a page returns fewer than limit items \u2014 never when offset reaches total.',\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 // A capped count reports a floor: phrase the total as \"at least N\" and\n // never treat it as proof that pagination is exhausted.\n totalIsCapped: (data as { totalIsCapped?: boolean }).totalIsCapped === true,\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 // The update command derives a closure outcome from terminal status spellings and\n // relocates the deal to the pipeline's terminal stage (#5107) \u2014 preview that same\n // projection so the approval card states the full blast radius.\n const organizationId = deal.organizationId ?? ctx.organizationId ?? null\n const outcome = dealClosureOutcomeFromStatus(input.toStage)\n if (outcome && organizationId) {\n const terminalStage = await loadClosurePipelineStageSnapshot(em, {\n pipelineId: deal.pipelineId ?? null,\n closureOutcome: outcome,\n tenantId,\n organizationId,\n })\n if (terminalStage) {\n afterPipelineStageId = terminalStage.id\n afterPipelineStageLabel = terminalStage.label\n }\n }\n }\n const beforeStatus = deal.status ?? null\n const beforePipelineStageId = deal.pipelineStageId ?? null\n const beforePipelineStageLabel = deal.pipelineStage ?? beforePipelineStageId\n const beforeClosureOutcome = deal.closureOutcome ?? null\n const beforeLossReasonId = deal.lossReasonId ?? null\n const beforeLossNotes = deal.lossNotes ?? null\n // Mirror the update command exactly (#5107): a status-only write derives the closure\n // outcome for terminal spellings, clears outcome plus loss columns for non-closed\n // non-terminal ones (reopen), and leaves `closed` and stage-only writes untouched.\n // `toStage` is free-form model text, so canonicalize before the `closed` check \u2014 the\n // command does the same, and the two must stay in lockstep or the approval card would\n // preview a different write than the one that lands.\n const requestedOutcome = dealClosureOutcomeFromStatus(input.toStage)\n const clearsClosure =\n input.toPipelineStageId === undefined &&\n input.toStage !== undefined &&\n !requestedOutcome &&\n !isClosedDealStatus(canonicalDealStatus(input.toStage))\n const afterClosureOutcome = clearsClosure\n ? null\n : requestedOutcome ?? beforeClosureOutcome\n const closureOutcomeCleared = beforeClosureOutcome !== null && afterClosureOutcome === null\n const afterLossReasonId = closureOutcomeCleared ? null : beforeLossReasonId\n const afterLossNotes = closureOutcomeCleared ? null : beforeLossNotes\n return {\n recordId: deal.id,\n entityType: 'customers.deal',\n recordVersion: recordVersionFromUpdatedAt(deal.updatedAt),\n before: {\n status: beforeStatus,\n pipelineStageId: beforePipelineStageId,\n closureOutcome: beforeClosureOutcome,\n lossReasonId: beforeLossReasonId,\n lossNotes: beforeLossNotes,\n },\n after: {\n status: afterStatus,\n pipelineStageId: afterPipelineStageId,\n closureOutcome: afterClosureOutcome,\n lossReasonId: afterLossReasonId,\n lossNotes: afterLossNotes,\n },\n display: {\n fieldLabels: {\n status: 'Status',\n pipelineStageId: 'Pipeline stage',\n closureOutcome: 'Closure outcome',\n lossReasonId: 'Loss reason',\n lossNotes: 'Loss notes',\n },\n before: {\n ...(beforeStatus ? { status: titleStatus(beforeStatus) } : {}),\n ...(beforePipelineStageLabel ? { pipelineStageId: beforePipelineStageLabel } : {}),\n ...(beforeClosureOutcome ? { closureOutcome: titleStatus(beforeClosureOutcome) } : {}),\n ...(beforeLossReasonId ? { lossReasonId: beforeLossReasonId } : {}),\n ...(beforeLossNotes ? { lossNotes: beforeLossNotes } : {}),\n },\n after: {\n ...(afterStatus ? { status: titleStatus(afterStatus) } : {}),\n ...(afterPipelineStageLabel ? { pipelineStageId: afterPipelineStageLabel } : {}),\n ...(afterClosureOutcome\n ? { closureOutcome: titleStatus(afterClosureOutcome) }\n : closureOutcomeCleared\n ? { closureOutcome: '\u2014' }\n : {}),\n ...(afterLossReasonId ? { lossReasonId: afterLossReasonId } : {}),\n ...(afterLossNotes ? { lossNotes: afterLossNotes } : {}),\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
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -26,7 +26,7 @@ const listPeopleInput = z.object({
|
|
|
26
26
|
const listPeopleTool = defineApiBackedAiTool({
|
|
27
27
|
name: "customers.list_people",
|
|
28
28
|
displayName: "List people",
|
|
29
|
-
description:
|
|
29
|
+
description: 'Search / list people (CRM persons) for the caller tenant + organization. Returns { items, total, totalIsCapped, limit, offset }. When totalIsCapped is true, total is a floor ("at least N", render it as "N+"), and pagination is exhausted only when a page returns fewer than limit items \u2014 never when offset reaches total.',
|
|
30
30
|
inputSchema: listPeopleInput,
|
|
31
31
|
requiredFeatures: ["customers.people.view"],
|
|
32
32
|
toOperation: async (input, ctx) => {
|
|
@@ -73,6 +73,9 @@ const listPeopleTool = defineApiBackedAiTool({
|
|
|
73
73
|
return {
|
|
74
74
|
items: rawItems.map((row) => toCustomerListSummary(row)),
|
|
75
75
|
total: typeof data.total === "number" ? data.total : 0,
|
|
76
|
+
// A capped count reports a floor: phrase the total as "at least N" and
|
|
77
|
+
// never treat it as proof that pagination is exhausted.
|
|
78
|
+
totalIsCapped: data.totalIsCapped === true,
|
|
76
79
|
limit,
|
|
77
80
|
offset
|
|
78
81
|
};
|
|
@@ -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/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 { 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'\nimport {\n buildRelatedRecords,\n buildScope,\n resolveEm,\n toCustomerListSummary,\n toIso,\n type CustomerRelatedRecords,\n} from './_shared'\n\nconst NIL_UUID = '00000000-0000-0000-0000-000000000000'\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) => toCustomerListSummary(row)),\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\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: CustomerRelatedRecords | null = null\n if (includeRelated) {\n related = buildRelatedRecords(data)\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
|
-
"mappings": "AAmBA,SAAS,SAAS;AAClB,SAAS,6BAA6B;AACtC;AAAA,EACE;AAAA,OAGK;AACP,SAAS,0BAA0B;AACnC;AAAA,EACE;AAAA,OACK;AACP,SAAS,yBAAoF;AAC7F;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AAEP,MAAM,WAAW;AAEjB,MAAM,kBAAkB,EACrB,OAAO;AAAA,EACN,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,qGAAqG;AAAA,EAC9I,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,SAAS,EAAE,SAAS,+CAA+C;AAAA,EAC3G,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,SAAS,EAAE,SAAS,qCAAqC;AAAA,EACzF,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,SAAS,EAAE,SAAS,6DAA6D;AAAA,EAClH,WAAW,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,yDAAyD;AAC5G,CAAC,EACA,YAAY;AAsCf,MAAM,iBAAiB,sBAAgF;AAAA,EACrG,MAAM;AAAA,EACN,aAAa;AAAA,EACb,aACE;AAAA,EACF,aAAa;AAAA,EACb,kBAAkB,CAAC,uBAAuB;AAAA,EAC1C,aAAa,OAAO,OAAO,QAAQ;AACjC,UAAM,EAAE,SAAS,IAAI,kBAAkB,GAAsC;AAC7E,UAAM,QAAQ,MAAM,SAAS;AAC7B,UAAM,SAAS,MAAM,UAAU;AAC/B,UAAM,OAAO,KAAK,MAAM,SAAS,KAAK,IAAI;AAE1C,UAAM,QAAsE;AAAA,MAC1E;AAAA,MACA,UAAU;AAAA,IACZ;AACA,QAAI,MAAM,GAAG,KAAK,EAAG,OAAM,SAAS,MAAM,EAAE,KAAK;AACjD,QAAI,MAAM,QAAQ,MAAM,KAAK,SAAS,EAAG,OAAM,SAAS,MAAM,KAAK,KAAK,GAAG;AAE3E,QAAI,MAAM,WAAW;AACnB,YAAM,KAAK,UAAU,GAAG;AACxB,YAAM,WAAW,MAAM;AAAA,QACrB;AAAA,QACA;AAAA,QACA,EAAE,UAAU,SAAS,MAAM,UAAU;AAAA,QACrC;AAAA,QACA,WAAW,KAAK,QAAQ;AAAA,MAC1B;AACA,YAAM,MAAM,SACT,IAAI,CAAC,YAAY;AAChB,cAAM,SAAU,QAAiC;AACjD,YAAI,CAAC,OAAQ,QAAO;AACpB,YAAI,OAAO,WAAW,SAAU,QAAO;AACvC,cAAM,YAAa,OAA4B;AAC/C,eAAO,OAAO,cAAc,WAAW,YAAY;AAAA,MACrD,CAAC,EACA,OAAO,CAAC,UAA2B,OAAO,UAAU,YAAY,MAAM,SAAS,CAAC;AAGnF,YAAM,MAAM,IAAI,SAAS,IAAI,KAAK,GAAG,IAAI;AAAA,IAC3C;AAEA,UAAM,YAAmC;AAAA,MACvC,QAAQ;AAAA,MACR,MAAM;AAAA,MACN;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAAA,EACA,aAAa,CAAC,UAAU,UAAU;AAChC,UAAM,QAAQ,MAAM,SAAS;AAC7B,UAAM,SAAS,MAAM,UAAU;AAC/B,UAAM,OAAQ,SAAS,QAAQ,CAAC;AAChC,UAAM,WAAgC,MAAM,QAAQ,KAAK,KAAK,IAAI,KAAK,QAAQ,CAAC;AAChF,WAAO;AAAA,MACL,OAAO,SAAS,IAAI,CAAC,QAAQ,sBAAsB,GAAG,CAAC;AAAA,MACvD,OAAO,OAAO,KAAK,UAAU,WAAW,KAAK,QAAQ;AAAA,
|
|
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 { 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'\nimport {\n buildRelatedRecords,\n buildScope,\n resolveEm,\n toCustomerListSummary,\n toIso,\n type CustomerRelatedRecords,\n} from './_shared'\n\nconst NIL_UUID = '00000000-0000-0000-0000-000000000000'\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, totalIsCapped, limit, offset }. When totalIsCapped is true, total is a floor (\"at least N\", render it as \"N+\"), and pagination is exhausted only when a page returns fewer than limit items \u2014 never when offset reaches total.',\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) => toCustomerListSummary(row)),\n total: typeof data.total === 'number' ? data.total : 0,\n // A capped count reports a floor: phrase the total as \"at least N\" and\n // never treat it as proof that pagination is exhausted.\n totalIsCapped: (data as { totalIsCapped?: boolean }).totalIsCapped === true,\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\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: CustomerRelatedRecords | null = null\n if (includeRelated) {\n related = buildRelatedRecords(data)\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
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|