@open-mercato/core 0.10.1-develop.7717.1.2befe9c634 → 0.10.1-develop.7722.1.901c72b9e9
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 +2 -2
- package/dist/generated/entities/staff_support_routing_rule/index.js +21 -0
- package/dist/generated/entities/staff_support_routing_rule/index.js.map +7 -0
- package/dist/generated/entities/staff_support_ticket/index.js +37 -0
- package/dist/generated/entities/staff_support_ticket/index.js.map +7 -0
- package/dist/generated/entities/staff_time_project/index.js +2 -0
- package/dist/generated/entities/staff_time_project/index.js.map +2 -2
- package/dist/generated/entities/staff_time_project_member/index.js +2 -0
- package/dist/generated/entities/staff_time_project_member/index.js.map +2 -2
- package/dist/generated/entities/staff_time_task/index.js +4 -0
- package/dist/generated/entities/staff_time_task/index.js.map +2 -2
- package/dist/generated/entities/staff_time_task_comment/index.js +9 -1
- package/dist/generated/entities/staff_time_task_comment/index.js.map +2 -2
- package/dist/generated/entities.ids.generated.js +2 -0
- package/dist/generated/entities.ids.generated.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +38 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/helpers/integration/auth.js +1 -0
- package/dist/helpers/integration/auth.js.map +2 -2
- package/dist/helpers/integration/communicationChannelsFixtures.js +20 -0
- package/dist/helpers/integration/communicationChannelsFixtures.js.map +2 -2
- package/dist/modules/catalog/api/bulk-delete/route.js +12 -2
- package/dist/modules/catalog/api/bulk-delete/route.js.map +2 -2
- package/dist/modules/communication_channels/api/post/test-seed/route.js +17 -3
- package/dist/modules/communication_channels/api/post/test-seed/route.js.map +2 -2
- package/dist/modules/communication_channels/lib/test-seed.js +38 -5
- package/dist/modules/communication_channels/lib/test-seed.js.map +2 -2
- package/dist/modules/messages/commands/messages.js +2 -3
- package/dist/modules/messages/commands/messages.js.map +2 -2
- package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/page.js +2 -2
- package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/page.js.map +2 -2
- package/dist/modules/staff/api/portal/support-tickets/[id]/attachments/route.js +299 -0
- package/dist/modules/staff/api/portal/support-tickets/[id]/attachments/route.js.map +7 -0
- package/dist/modules/staff/api/portal/support-tickets/[id]/messages/route.js +91 -0
- package/dist/modules/staff/api/portal/support-tickets/[id]/messages/route.js.map +7 -0
- package/dist/modules/staff/api/portal/support-tickets/[id]/route.js +146 -0
- package/dist/modules/staff/api/portal/support-tickets/[id]/route.js.map +7 -0
- package/dist/modules/staff/api/portal/support-tickets/options/route.js +60 -0
- package/dist/modules/staff/api/portal/support-tickets/options/route.js.map +7 -0
- package/dist/modules/staff/api/portal/support-tickets/route.js +220 -0
- package/dist/modules/staff/api/portal/support-tickets/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/settings/route.js +7 -1
- package/dist/modules/staff/api/timesheets/settings/route.js.map +2 -2
- package/dist/modules/staff/api/timesheets/support-routing-rules/fallback-project/route.js +122 -0
- package/dist/modules/staff/api/timesheets/support-routing-rules/fallback-project/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/support-routing-rules/preview/route.js +180 -0
- package/dist/modules/staff/api/timesheets/support-routing-rules/preview/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/support-routing-rules/route.js +147 -0
- package/dist/modules/staff/api/timesheets/support-routing-rules/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/support-tickets/options/route.js +133 -0
- package/dist/modules/staff/api/timesheets/support-tickets/options/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/tasks/[id]/claim/route.js +33 -0
- package/dist/modules/staff/api/timesheets/tasks/[id]/claim/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/tasks/[id]/comments/route.js +25 -7
- package/dist/modules/staff/api/timesheets/tasks/[id]/comments/route.js.map +2 -2
- package/dist/modules/staff/api/timesheets/tasks/[id]/release/route.js +32 -0
- package/dist/modules/staff/api/timesheets/tasks/[id]/release/route.js.map +7 -0
- package/dist/modules/staff/api/timesheets/tasks/route.js +94 -47
- package/dist/modules/staff/api/timesheets/tasks/route.js.map +2 -2
- package/dist/modules/staff/api/timesheets/time-projects/[id]/employees/route.js +3 -0
- package/dist/modules/staff/api/timesheets/time-projects/[id]/employees/route.js.map +2 -2
- package/dist/modules/staff/api/timesheets/time-projects/route.js +3 -0
- package/dist/modules/staff/api/timesheets/time-projects/route.js.map +2 -2
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.js +1 -0
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js +54 -4
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/time-tracking/projects/projectFormConfig.js +21 -5
- package/dist/modules/staff/backend/staff/time-tracking/projects/projectFormConfig.js.map +2 -2
- package/dist/modules/staff/backend/staff/time-tracking/settings/page.js +319 -254
- package/dist/modules/staff/backend/staff/time-tracking/settings/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/time-tracking/support/page.js +363 -0
- package/dist/modules/staff/backend/staff/time-tracking/support/page.js.map +7 -0
- package/dist/modules/staff/backend/staff/time-tracking/support/page.meta.js +18 -0
- package/dist/modules/staff/backend/staff/time-tracking/support/page.meta.js.map +7 -0
- package/dist/modules/staff/cli.js +27 -0
- package/dist/modules/staff/cli.js.map +2 -2
- package/dist/modules/staff/commands/index.js +3 -0
- package/dist/modules/staff/commands/index.js.map +2 -2
- package/dist/modules/staff/commands/shared.js +10 -1
- package/dist/modules/staff/commands/shared.js.map +2 -2
- package/dist/modules/staff/commands/support-routing-rules.js +505 -0
- package/dist/modules/staff/commands/support-routing-rules.js.map +7 -0
- package/dist/modules/staff/commands/support-tickets.js +513 -0
- package/dist/modules/staff/commands/support-tickets.js.map +7 -0
- package/dist/modules/staff/commands/timesheets-projects.js +72 -14
- package/dist/modules/staff/commands/timesheets-projects.js.map +2 -2
- package/dist/modules/staff/commands/timesheets-task-assignment.js +270 -0
- package/dist/modules/staff/commands/timesheets-task-assignment.js.map +7 -0
- package/dist/modules/staff/commands/timesheets-task-comments.js +58 -12
- package/dist/modules/staff/commands/timesheets-task-comments.js.map +2 -2
- package/dist/modules/staff/commands/timesheets-tasks.js +91 -9
- package/dist/modules/staff/commands/timesheets-tasks.js.map +3 -3
- package/dist/modules/staff/data/enrichers.js +141 -0
- package/dist/modules/staff/data/enrichers.js.map +2 -2
- package/dist/modules/staff/data/entities.js +143 -2
- package/dist/modules/staff/data/entities.js.map +2 -2
- package/dist/modules/staff/data/extensions.js +8 -0
- package/dist/modules/staff/data/extensions.js.map +2 -2
- package/dist/modules/staff/data/validators.js +34 -5
- package/dist/modules/staff/data/validators.js.map +2 -2
- package/dist/modules/staff/encryption.js +6 -0
- package/dist/modules/staff/encryption.js.map +2 -2
- package/dist/modules/staff/events.js +25 -0
- package/dist/modules/staff/events.js.map +2 -2
- package/dist/modules/staff/events.payloads.js +54 -0
- package/dist/modules/staff/events.payloads.js.map +2 -2
- package/dist/modules/staff/extension-points.js +1 -0
- package/dist/modules/staff/extension-points.js.map +2 -2
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.js +468 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.js.map +7 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.meta.js +12 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.meta.js.map +7 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/new/page.js +316 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/new/page.js.map +7 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/new/page.meta.js +12 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/new/page.meta.js.map +7 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/page.js +190 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/page.js.map +7 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/page.meta.js +19 -0
- package/dist/modules/staff/frontend/[orgSlug]/portal/support/page.meta.js.map +7 -0
- package/dist/modules/staff/index.js +1 -1
- package/dist/modules/staff/index.js.map +2 -2
- package/dist/modules/staff/lib/crud.js +2 -0
- package/dist/modules/staff/lib/crud.js.map +2 -2
- package/dist/modules/staff/lib/support/attachments.js +36 -0
- package/dist/modules/staff/lib/support/attachments.js.map +7 -0
- package/dist/modules/staff/lib/support/dictionaries.js +87 -0
- package/dist/modules/staff/lib/support/dictionaries.js.map +7 -0
- package/dist/modules/staff/lib/support/notifications.js +111 -0
- package/dist/modules/staff/lib/support/notifications.js.map +7 -0
- package/dist/modules/staff/lib/support/portalAttachments.js +100 -0
- package/dist/modules/staff/lib/support/portalAttachments.js.map +7 -0
- package/dist/modules/staff/lib/support/portalBroadcast.js +117 -0
- package/dist/modules/staff/lib/support/portalBroadcast.js.map +7 -0
- package/dist/modules/staff/lib/support/portalRoute.js +99 -0
- package/dist/modules/staff/lib/support/portalRoute.js.map +7 -0
- package/dist/modules/staff/lib/support/portalStatus.js +12 -0
- package/dist/modules/staff/lib/support/portalStatus.js.map +7 -0
- package/dist/modules/staff/lib/support/portalTickets.js +148 -0
- package/dist/modules/staff/lib/support/portalTickets.js.map +7 -0
- package/dist/modules/staff/lib/support/routing.js +81 -0
- package/dist/modules/staff/lib/support/routing.js.map +7 -0
- package/dist/modules/staff/lib/support/routingRuleDraft.js +25 -0
- package/dist/modules/staff/lib/support/routingRuleDraft.js.map +7 -0
- package/dist/modules/staff/lib/support/seedDefaults.js +114 -0
- package/dist/modules/staff/lib/support/seedDefaults.js.map +7 -0
- package/dist/modules/staff/lib/support/supportActivity.js +26 -0
- package/dist/modules/staff/lib/support/supportActivity.js.map +7 -0
- package/dist/modules/staff/lib/support-ui/PortalSupportNotLinked.js +23 -0
- package/dist/modules/staff/lib/support-ui/PortalSupportNotLinked.js.map +7 -0
- package/dist/modules/staff/lib/support-ui/portalAttachmentValidation.js +112 -0
- package/dist/modules/staff/lib/support-ui/portalAttachmentValidation.js.map +7 -0
- package/dist/modules/staff/lib/support-ui/portalPendingUploads.js +45 -0
- package/dist/modules/staff/lib/support-ui/portalPendingUploads.js.map +7 -0
- package/dist/modules/staff/lib/support-ui/portalSupport.js +51 -0
- package/dist/modules/staff/lib/support-ui/portalSupport.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking/access.js +3 -39
- package/dist/modules/staff/lib/time-tracking/access.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking/listProjectAccess.js +54 -0
- package/dist/modules/staff/lib/time-tracking/listProjectAccess.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking/portalRecipients.js +92 -2
- package/dist/modules/staff/lib/time-tracking/portalRecipients.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking/projectAssignment.js +112 -0
- package/dist/modules/staff/lib/time-tracking/projectAssignment.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking/settingKeys.js +14 -0
- package/dist/modules/staff/lib/time-tracking/settingKeys.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking/settings.js +28 -1
- package/dist/modules/staff/lib/time-tracking/settings.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking/taskAssignmentRoute.js +143 -0
- package/dist/modules/staff/lib/time-tracking/taskAssignmentRoute.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking/workRecipients.js +92 -0
- package/dist/modules/staff/lib/time-tracking/workRecipients.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/BoardFilterChips.js +32 -3
- package/dist/modules/staff/lib/time-tracking-ui/BoardFilterChips.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/KanbanBoard.js +29 -0
- package/dist/modules/staff/lib/time-tracking-ui/KanbanBoard.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/KanbanCard.js +4 -0
- package/dist/modules/staff/lib/time-tracking-ui/KanbanCard.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/ProjectFormSections.js +141 -1
- package/dist/modules/staff/lib/time-tracking-ui/ProjectFormSections.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/SupportAttachmentsSection.js +153 -0
- package/dist/modules/staff/lib/time-tracking-ui/SupportAttachmentsSection.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/SupportRequestPanel.js +67 -0
- package/dist/modules/staff/lib/time-tracking-ui/SupportRequestPanel.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/TaskBoardScreen.js +3 -1
- package/dist/modules/staff/lib/time-tracking-ui/TaskBoardScreen.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/TaskCommentThread.js +80 -17
- package/dist/modules/staff/lib/time-tracking-ui/TaskCommentThread.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/TaskDrawer.js +230 -41
- package/dist/modules/staff/lib/time-tracking-ui/TaskDrawer.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/boardDirectory.js +21 -1
- package/dist/modules/staff/lib/time-tracking-ui/boardDirectory.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/boardFilters.js +16 -4
- package/dist/modules/staff/lib/time-tracking-ui/boardFilters.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/kanbanBoardData.js +3 -1
- package/dist/modules/staff/lib/time-tracking-ui/kanbanBoardData.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/projectDefaultAssignee.js +68 -0
- package/dist/modules/staff/lib/time-tracking-ui/projectDefaultAssignee.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/settingsTabs.js +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/settingsTabs.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/supportTicketData.js +50 -0
- package/dist/modules/staff/lib/time-tracking-ui/supportTicketData.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/taskDrawerData.js +13 -1
- package/dist/modules/staff/lib/time-tracking-ui/taskDrawerData.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/timeTrackingSettingsForm.js +36 -2
- package/dist/modules/staff/lib/time-tracking-ui/timeTrackingSettingsForm.js.map +2 -2
- package/dist/modules/staff/migrations/Migration20261009180543_staff.js +42 -0
- package/dist/modules/staff/migrations/Migration20261009180543_staff.js.map +7 -0
- package/dist/modules/staff/notifications.js +92 -0
- package/dist/modules/staff/notifications.js.map +2 -2
- package/dist/modules/staff/setup.js +6 -2
- package/dist/modules/staff/setup.js.map +2 -2
- package/dist/modules/staff/subscribers/support-portal-comment-created.js +26 -0
- package/dist/modules/staff/subscribers/support-portal-comment-created.js.map +7 -0
- package/dist/modules/staff/subscribers/support-portal-comment-deleted.js +26 -0
- package/dist/modules/staff/subscribers/support-portal-comment-deleted.js.map +7 -0
- package/dist/modules/staff/subscribers/support-portal-comment-updated.js +26 -0
- package/dist/modules/staff/subscribers/support-portal-comment-updated.js.map +7 -0
- package/dist/modules/staff/subscribers/support-portal-status-column-updated.js +24 -0
- package/dist/modules/staff/subscribers/support-portal-status-column-updated.js.map +7 -0
- package/dist/modules/staff/subscribers/support-portal-task-status-changed.js +25 -0
- package/dist/modules/staff/subscribers/support-portal-task-status-changed.js.map +7 -0
- package/dist/modules/staff/subscribers/support-portal-task-updated.js +25 -0
- package/dist/modules/staff/subscribers/support-portal-task-updated.js.map +7 -0
- package/dist/modules/staff/subscribers/support-ticket-created-notification.js +25 -0
- package/dist/modules/staff/subscribers/support-ticket-created-notification.js.map +7 -0
- package/dist/modules/staff/subscribers/support-ticket-customer-replied-notification.js +25 -0
- package/dist/modules/staff/subscribers/support-ticket-customer-replied-notification.js.map +7 -0
- package/dist/modules/staff/subscribers/support-ticket-unnotified-clear.js +26 -0
- package/dist/modules/staff/subscribers/support-ticket-unnotified-clear.js.map +7 -0
- package/dist/modules/staff/subscribers/time-task-assignee-notification.js +83 -0
- package/dist/modules/staff/subscribers/time-task-assignee-notification.js.map +7 -0
- package/dist/modules/staff/widgets/injection/portal-support-tickets/widget.client.js +112 -0
- package/dist/modules/staff/widgets/injection/portal-support-tickets/widget.client.js.map +7 -0
- package/dist/modules/staff/widgets/injection/portal-support-tickets/widget.js +17 -0
- package/dist/modules/staff/widgets/injection/portal-support-tickets/widget.js.map +7 -0
- package/dist/modules/staff/widgets/injection/time-tracking-settings-support/widget.client.js +870 -0
- package/dist/modules/staff/widgets/injection/time-tracking-settings-support/widget.client.js.map +7 -0
- package/dist/modules/staff/widgets/injection/time-tracking-settings-support/widget.js +17 -0
- package/dist/modules/staff/widgets/injection/time-tracking-settings-support/widget.js.map +7 -0
- package/dist/modules/staff/widgets/injection-table.js +16 -1
- package/dist/modules/staff/widgets/injection-table.js.map +2 -2
- package/dist/modules/wms/components/backend/WmsLocationDetailPage.js +4 -18
- package/dist/modules/wms/components/backend/WmsLocationDetailPage.js.map +2 -2
- package/dist/modules/wms/components/backend/WmsLotDetailPage.js +4 -18
- package/dist/modules/wms/components/backend/WmsLotDetailPage.js.map +2 -2
- package/dist/modules/wms/components/backend/WmsSkuDetailPage.js +4 -18
- package/dist/modules/wms/components/backend/WmsSkuDetailPage.js.map +2 -2
- package/dist/modules/wms/lib/inventoryCsvExport.js +19 -0
- package/dist/modules/wms/lib/inventoryCsvExport.js.map +7 -0
- package/generated/entities/staff_support_routing_rule/index.ts +9 -0
- package/generated/entities/staff_support_ticket/index.ts +17 -0
- package/generated/entities/staff_time_project/index.ts +1 -0
- package/generated/entities/staff_time_project_member/index.ts +1 -0
- package/generated/entities/staff_time_task/index.ts +2 -0
- package/generated/entities/staff_time_task_comment/index.ts +4 -0
- package/generated/entities.ids.generated.ts +2 -0
- package/generated/entity-fields-registry.ts +38 -0
- package/package.json +8 -8
- package/src/helpers/integration/auth.ts +1 -1
- package/src/helpers/integration/communicationChannelsFixtures.ts +60 -2
- package/src/modules/catalog/api/bulk-delete/route.ts +12 -2
- package/src/modules/communication_channels/api/post/test-seed/route.ts +29 -12
- package/src/modules/communication_channels/lib/test-seed.ts +49 -10
- package/src/modules/messages/commands/messages.ts +8 -10
- package/src/modules/portal/frontend/[orgSlug]/portal/dashboard/page.tsx +2 -2
- package/src/modules/staff/AGENTS.md +22 -6
- package/src/modules/staff/api/portal/support-tickets/[id]/attachments/route.ts +327 -0
- package/src/modules/staff/api/portal/support-tickets/[id]/messages/route.ts +94 -0
- package/src/modules/staff/api/portal/support-tickets/[id]/route.ts +160 -0
- package/src/modules/staff/api/portal/support-tickets/options/route.ts +60 -0
- package/src/modules/staff/api/portal/support-tickets/route.ts +253 -0
- package/src/modules/staff/api/timesheets/settings/route.ts +7 -1
- package/src/modules/staff/api/timesheets/support-routing-rules/fallback-project/route.ts +131 -0
- package/src/modules/staff/api/timesheets/support-routing-rules/preview/route.ts +248 -0
- package/src/modules/staff/api/timesheets/support-routing-rules/route.ts +150 -0
- package/src/modules/staff/api/timesheets/support-tickets/options/route.ts +181 -0
- package/src/modules/staff/api/timesheets/tasks/[id]/claim/route.ts +31 -0
- package/src/modules/staff/api/timesheets/tasks/[id]/comments/route.ts +25 -7
- package/src/modules/staff/api/timesheets/tasks/[id]/release/route.ts +30 -0
- package/src/modules/staff/api/timesheets/tasks/route.ts +119 -67
- package/src/modules/staff/api/timesheets/time-projects/[id]/employees/route.ts +3 -0
- package/src/modules/staff/api/timesheets/time-projects/route.ts +3 -0
- package/src/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.tsx +1 -0
- package/src/modules/staff/backend/staff/time-tracking/projects/[id]/page.tsx +67 -2
- package/src/modules/staff/backend/staff/time-tracking/projects/projectFormConfig.ts +22 -3
- package/src/modules/staff/backend/staff/time-tracking/settings/page.tsx +405 -308
- package/src/modules/staff/backend/staff/time-tracking/support/page.meta.ts +14 -0
- package/src/modules/staff/backend/staff/time-tracking/support/page.tsx +428 -0
- package/src/modules/staff/cli.ts +28 -0
- package/src/modules/staff/commands/index.ts +3 -0
- package/src/modules/staff/commands/shared.ts +22 -1
- package/src/modules/staff/commands/support-routing-rules.ts +595 -0
- package/src/modules/staff/commands/support-tickets.ts +580 -0
- package/src/modules/staff/commands/timesheets-projects.ts +84 -13
- package/src/modules/staff/commands/timesheets-task-assignment.ts +319 -0
- package/src/modules/staff/commands/timesheets-task-comments.ts +70 -15
- package/src/modules/staff/commands/timesheets-tasks.ts +106 -15
- package/src/modules/staff/data/enrichers.ts +203 -0
- package/src/modules/staff/data/entities.ts +133 -2
- package/src/modules/staff/data/extensions.ts +9 -1
- package/src/modules/staff/data/validators.ts +36 -0
- package/src/modules/staff/encryption.ts +6 -0
- package/src/modules/staff/events.payloads.ts +87 -1
- package/src/modules/staff/events.ts +26 -0
- package/src/modules/staff/extension-points.ts +1 -0
- package/src/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.meta.ts +10 -0
- package/src/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.tsx +553 -0
- package/src/modules/staff/frontend/[orgSlug]/portal/support/new/page.meta.ts +10 -0
- package/src/modules/staff/frontend/[orgSlug]/portal/support/new/page.tsx +346 -0
- package/src/modules/staff/frontend/[orgSlug]/portal/support/page.meta.ts +17 -0
- package/src/modules/staff/frontend/[orgSlug]/portal/support/page.tsx +253 -0
- package/src/modules/staff/i18n/de.json +291 -0
- package/src/modules/staff/i18n/en.json +291 -0
- package/src/modules/staff/i18n/es.json +291 -0
- package/src/modules/staff/i18n/ko.json +291 -0
- package/src/modules/staff/i18n/pl.json +291 -0
- package/src/modules/staff/index.ts +1 -1
- package/src/modules/staff/lib/crud.ts +2 -1
- package/src/modules/staff/lib/support/attachments.ts +43 -0
- package/src/modules/staff/lib/support/dictionaries.ts +120 -0
- package/src/modules/staff/lib/support/notifications.ts +138 -0
- package/src/modules/staff/lib/support/portalAttachments.ts +140 -0
- package/src/modules/staff/lib/support/portalBroadcast.ts +161 -0
- package/src/modules/staff/lib/support/portalRoute.ts +108 -0
- package/src/modules/staff/lib/support/portalStatus.ts +15 -0
- package/src/modules/staff/lib/support/portalTickets.ts +233 -0
- package/src/modules/staff/lib/support/routing.ts +117 -0
- package/src/modules/staff/lib/support/routingRuleDraft.ts +45 -0
- package/src/modules/staff/lib/support/seedDefaults.ts +151 -0
- package/src/modules/staff/lib/support/supportActivity.ts +39 -0
- package/src/modules/staff/lib/support-ui/PortalSupportNotLinked.tsx +20 -0
- package/src/modules/staff/lib/support-ui/portalAttachmentValidation.ts +125 -0
- package/src/modules/staff/lib/support-ui/portalPendingUploads.ts +49 -0
- package/src/modules/staff/lib/support-ui/portalSupport.ts +116 -0
- package/src/modules/staff/lib/time-tracking/access.ts +2 -62
- package/src/modules/staff/lib/time-tracking/listProjectAccess.ts +78 -0
- package/src/modules/staff/lib/time-tracking/portalRecipients.ts +173 -5
- package/src/modules/staff/lib/time-tracking/projectAssignment.ts +172 -0
- package/src/modules/staff/lib/time-tracking/settingKeys.ts +11 -0
- package/src/modules/staff/lib/time-tracking/settings.ts +46 -1
- package/src/modules/staff/lib/time-tracking/taskAssignmentRoute.ts +165 -0
- package/src/modules/staff/lib/time-tracking/workRecipients.ts +183 -0
- package/src/modules/staff/lib/time-tracking-ui/BoardFilterChips.tsx +36 -1
- package/src/modules/staff/lib/time-tracking-ui/KanbanBoard.tsx +30 -0
- package/src/modules/staff/lib/time-tracking-ui/KanbanCard.tsx +9 -0
- package/src/modules/staff/lib/time-tracking-ui/ProjectFormSections.tsx +185 -6
- package/src/modules/staff/lib/time-tracking-ui/SupportAttachmentsSection.tsx +188 -0
- package/src/modules/staff/lib/time-tracking-ui/SupportRequestPanel.tsx +102 -0
- package/src/modules/staff/lib/time-tracking-ui/TaskBoardScreen.tsx +2 -0
- package/src/modules/staff/lib/time-tracking-ui/TaskCommentThread.tsx +105 -22
- package/src/modules/staff/lib/time-tracking-ui/TaskDrawer.tsx +278 -43
- package/src/modules/staff/lib/time-tracking-ui/boardDirectory.ts +21 -0
- package/src/modules/staff/lib/time-tracking-ui/boardFilters.ts +16 -2
- package/src/modules/staff/lib/time-tracking-ui/kanbanBoardData.ts +4 -0
- package/src/modules/staff/lib/time-tracking-ui/projectDefaultAssignee.ts +117 -0
- package/src/modules/staff/lib/time-tracking-ui/settingsTabs.ts +17 -0
- package/src/modules/staff/lib/time-tracking-ui/supportTicketData.ts +71 -0
- package/src/modules/staff/lib/time-tracking-ui/taskDrawerData.ts +15 -0
- package/src/modules/staff/lib/time-tracking-ui/timeTrackingSettingsForm.ts +48 -1
- package/src/modules/staff/migrations/.snapshot-open-mercato.json +714 -0
- package/src/modules/staff/migrations/Migration20261009180543_staff.ts +50 -0
- package/src/modules/staff/notifications.ts +92 -0
- package/src/modules/staff/setup.ts +6 -2
- package/src/modules/staff/subscribers/support-portal-comment-created.ts +35 -0
- package/src/modules/staff/subscribers/support-portal-comment-deleted.ts +35 -0
- package/src/modules/staff/subscribers/support-portal-comment-updated.ts +35 -0
- package/src/modules/staff/subscribers/support-portal-status-column-updated.ts +36 -0
- package/src/modules/staff/subscribers/support-portal-task-status-changed.ts +34 -0
- package/src/modules/staff/subscribers/support-portal-task-updated.ts +34 -0
- package/src/modules/staff/subscribers/support-ticket-created-notification.ts +27 -0
- package/src/modules/staff/subscribers/support-ticket-customer-replied-notification.ts +27 -0
- package/src/modules/staff/subscribers/support-ticket-unnotified-clear.ts +29 -0
- package/src/modules/staff/subscribers/time-task-assignee-notification.ts +102 -0
- package/src/modules/staff/widgets/injection/portal-support-tickets/widget.client.tsx +151 -0
- package/src/modules/staff/widgets/injection/portal-support-tickets/widget.ts +16 -0
- package/src/modules/staff/widgets/injection/time-tracking-settings-support/widget.client.tsx +1058 -0
- package/src/modules/staff/widgets/injection/time-tracking-settings-support/widget.ts +17 -0
- package/src/modules/staff/widgets/injection-table.ts +15 -0
- package/src/modules/wms/components/backend/WmsLocationDetailPage.tsx +4 -20
- package/src/modules/wms/components/backend/WmsLotDetailPage.tsx +4 -20
- package/src/modules/wms/components/backend/WmsSkuDetailPage.tsx +4 -19
- package/src/modules/wms/lib/inventoryCsvExport.ts +18 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/helpers/integration/auth.ts"],
|
|
4
|
-
"sourcesContent": ["import { type Page, type Response } from '@playwright/test';\nimport { readFileSync } from 'fs';\nimport { resolve } from 'path';\n\nfunction loadEnvFileContent(): string | null {\n const candidatePaths = [\n resolve(process.cwd(), 'apps/mercato/.env'),\n resolve(process.cwd(), '.env'),\n ];\n\n for (const envPath of candidatePaths) {\n try {\n const content = readFileSync(envPath, 'utf-8');\n if (content.trim().length > 0) {\n return content;\n }\n } catch {\n continue;\n }\n }\n\n return null;\n}\n\nfunction loadEnvValue(key: string): string | undefined {\n if (process.env[key]) return process.env[key];\n const content = loadEnvFileContent();\n if (!content) return undefined;\n const match = content.match(new RegExp(`^${key}=(.+)$`, 'm'));\n return match?.[1]?.trim();\n}\n\nexport const DEFAULT_CREDENTIALS: Record<string, { email: string; password: string }> = {\n superadmin: {\n email: loadEnvValue('OM_INIT_SUPERADMIN_EMAIL') || 'superadmin@acme.com',\n password: loadEnvValue('OM_INIT_SUPERADMIN_PASSWORD') || 'secret',\n },\n admin: { email: 'admin@acme.com', password: 'secret' },\n employee: { email: 'employee@acme.com', password: 'secret' },\n};\n\nexport type Role = 'superadmin' | 'admin' | 'employee';\n\nfunction decodeJwtClaims(token: string): { tenantId?: string; orgId?: string | null } | null {\n const parts = token.split('.');\n if (parts.length < 2) return null;\n try {\n const normalized = parts[1].replace(/-/g, '+').replace(/_/g, '/');\n const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, '=');\n const payload = JSON.parse(Buffer.from(padded, 'base64').toString('utf8')) as {\n tenantId?: string;\n orgId?: string | null;\n };\n return payload;\n } catch {\n return null;\n }\n}\n\nasync function acknowledgeGlobalNotices(page: Page): Promise<void> {\n const baseUrl = process.env.BASE_URL || 'http://localhost:3000';\n await page.context().addCookies([\n {\n name: 'om_demo_notice_ack',\n value: 'ack',\n url: baseUrl,\n sameSite: 'Lax',\n },\n {\n name: 'om_cookie_notice_ack',\n value: 'ack',\n url: baseUrl,\n sameSite: 'Lax',\n },\n {\n name: 'om_feedback_suppress',\n value: '1',\n url: baseUrl,\n sameSite: 'Lax',\n },\n {\n name: 'om_feedback_shown',\n value: new Date().toISOString().slice(0, 10),\n url: baseUrl,\n sameSite: 'Lax',\n },\n ]);\n}\n\nasync function dismissGlobalNoticesIfPresent(page: Page): Promise<void> {\n const cookieAcceptButton = page.getByRole('button', { name: /accept cookies/i }).first();\n if (await cookieAcceptButton.isVisible().catch(() => false)) {\n await cookieAcceptButton.click();\n }\n\n const demoNotice = page.getByText(/this instance is provided for demo purposes only/i).first();\n if (await demoNotice.isVisible().catch(() => false)) {\n const noticeContainer = demoNotice.locator('xpath=ancestor::div[contains(@class,\"pointer-events-auto\")]').first();\n const dismissButton = noticeContainer.locator('button').first();\n if (await dismissButton.isVisible().catch(() => false)) {\n await dismissButton.click();\n }\n }\n\n const feedbackDialog = page.getByRole('dialog', { name: /Talk to Open Mercato team/i }).first();\n if (await feedbackDialog.isVisible().catch(() => false)) {\n const closeButton = feedbackDialog.getByRole('button', { name: /close/i }).first();\n if (await closeButton.isVisible().catch(() => false)) {\n await closeButton.click().catch(() => {});\n } else {\n await page.keyboard.press('Escape').catch(() => {});\n }\n }\n}\n\nasync function recoverClientSideErrorPageIfPresent(page: Page): Promise<void> {\n const clientErrorHeading = page\n .getByRole('heading', { name: /Application error: a client-side exception has occurred/i })\n .first();\n if (!(await clientErrorHeading.isVisible().catch(() => false))) return;\n await page.reload({ waitUntil: 'domcontentloaded' });\n await dismissGlobalNoticesIfPresent(page);\n}\n\nasync function recoverGenericErrorPageIfPresent(page: Page): Promise<void> {\n const errorHeading = page.getByRole('heading', { name: /^Something went wrong$/i }).first();\n if (!(await errorHeading.isVisible().catch(() => false))) return;\n const retryButton = page.getByRole('button', { name: /Try again/i }).first();\n if (await retryButton.isVisible().catch(() => false)) {\n await retryButton.click().catch(() => {});\n await page.waitForLoadState('domcontentloaded').catch(() => {});\n await page.waitForTimeout(500).catch(() => {});\n } else {\n await page.reload({ waitUntil: 'domcontentloaded' });\n }\n await dismissGlobalNoticesIfPresent(page);\n}\n\ntype AuthNavigationTraceEntry = {\n path: string;\n status: number;\n location: string | null;\n};\n\nfunction toPathOnly(rawUrl: string): string {\n try {\n const url = new URL(rawUrl);\n return `${url.pathname}${url.search}`;\n } catch {\n return '[unparseable-url]';\n }\n}\n\nfunction toRedactedLocation(rawLocation: string | undefined): string | null {\n if (!rawLocation) return null;\n try {\n const url = new URL(rawLocation, 'http://integration.local');\n return `${url.pathname}${url.search}`;\n } catch {\n return '[unparseable-location]';\n }\n}\n\nfunction createAuthNavigationTrace(page: Page): {\n entries: AuthNavigationTraceEntry[];\n stop: () => void;\n} {\n const entries: AuthNavigationTraceEntry[] = [];\n const onResponse = (response: Response) => {\n const path = toPathOnly(response.url());\n if (path === '/backend' || path.startsWith('/api/auth/session/refresh')) {\n entries.push({\n path,\n status: response.status(),\n location: toRedactedLocation(response.headers().location),\n });\n }\n };\n page.on('response', onResponse);\n return {\n entries,\n stop: () => page.off('response', onResponse),\n };\n}\n\nasync function formatLoginDiagnostics(\n page: Page,\n role: Role,\n entries: AuthNavigationTraceEntry[],\n): Promise<string> {\n const cookieNames = Array.from(\n new Set((await page.context().cookies()).map((cookie) => cookie.name)),\n ).sort((left, right) => left.localeCompare(right));\n const traceLines = entries.length\n ? entries.map((entry) => {\n const location = entry.location ? ` -> ${entry.location}` : '';\n return `${entry.status} ${entry.path}${location}`;\n })\n : ['none'];\n\n return [\n `Login diagnostics for role \"${role}\":`,\n `current URL: ${toPathOnly(page.url())}`,\n `browser cookie names: ${cookieNames.length ? cookieNames.join(', ') : 'none'}`,\n 'backend/refresh trace:',\n ...traceLines,\n ].join('\\n');\n}\n\nasync function throwLoginErrorWithDiagnostics(\n page: Page,\n role: Role,\n entries: AuthNavigationTraceEntry[],\n error: unknown,\n): Promise<never> {\n const message = error instanceof Error ? error.message : String(error);\n throw new Error([\n `Login did not reach backend for role: ${role}.`,\n await formatLoginDiagnostics(page, role, entries),\n `Original error: ${message}`,\n ].join('\\n'));\n}\n\nexport async function login(page: Page, role: Role = 'admin'): Promise<void> {\n const authTrace = createAuthNavigationTrace(page);\n try {\n const creds = DEFAULT_CREDENTIALS[role];\n const loginReadySelector = 'form[data-auth-ready=\"1\"]';\n const hasBackendUrl = (): boolean => /\\/backend(?:\\/.*)?$/.test(page.url());\n const waitForBackend = async (timeout: number): Promise<boolean> => {\n try {\n await page.waitForURL(/\\/backend(?:\\/.*)?$/, { timeout });\n return true;\n } catch {\n return hasBackendUrl();\n }\n };\n\n await acknowledgeGlobalNotices(page);\n const apiLoginForm = new URLSearchParams();\n apiLoginForm.set('email', creds.email);\n apiLoginForm.set('password', creds.password);\n // Retry-on-429 against the auth rate limit (5/60s per email). Capped\n // exponential backoff: 1s, 2s, 4s \u2014 worst-case ~7s.\n let apiLoginResponse: Awaited<ReturnType<typeof page.request.post>> | null = null;\n for (let retry = 0; retry < 4; retry += 1) {\n apiLoginResponse = await page.request.post('/api/auth/login', {\n headers: {\n 'content-type': 'application/x-www-form-urlencoded',\n },\n data: apiLoginForm.toString(),\n }).catch(() => null);\n if (!apiLoginResponse || apiLoginResponse.status() !== 429) break;\n await new Promise((resolve) => setTimeout(resolve, 1000 * 2 ** retry));\n }\n if (apiLoginResponse?.ok()) {\n const apiLoginBody = (await apiLoginResponse.json().catch(() => null)) as { token?: string } | null;\n const claims = typeof apiLoginBody?.token === 'string' ? decodeJwtClaims(apiLoginBody.token) : null;\n const baseUrl = process.env.BASE_URL || 'http://localhost:3000';\n const cookies = [];\n if (claims?.tenantId) {\n cookies.push({\n name: 'om_selected_tenant',\n value: claims.tenantId,\n url: baseUrl,\n sameSite: 'Lax' as const,\n });\n }\n if (claims?.orgId) {\n cookies.push({\n name: 'om_selected_org',\n value: claims.orgId,\n url: baseUrl,\n sameSite: 'Lax' as const,\n });\n }\n if (cookies.length > 0) {\n await page.context().addCookies(cookies);\n }\n await page.goto('/backend', { waitUntil: 'domcontentloaded' });\n if (await waitForBackend(8_000)) return;\n }\n\n for (let attempt = 0; attempt < 4; attempt += 1) {\n await page.goto('/login', { waitUntil: 'domcontentloaded' });\n await dismissGlobalNoticesIfPresent(page);\n await recoverClientSideErrorPageIfPresent(page);\n await recoverGenericErrorPageIfPresent(page);\n await page.waitForSelector(loginReadySelector, { state: 'visible', timeout: 3_000 }).catch(() => null);\n if (await page.getByLabel('Email').isVisible().catch(() => false)) break;\n if (attempt === 3) {\n throw new Error(`Login form is unavailable for role: ${role}; current URL: ${page.url()}`);\n }\n }\n await page.getByLabel('Email').fill(creds.email);\n\n const passwordInput = page.getByLabel('Password', { exact: true }).first();\n if (await passwordInput.isVisible().catch(() => false)) {\n await passwordInput.fill(creds.password);\n await passwordInput.press('Enter');\n } else {\n const submitButton = page.getByRole('button', { name: /login|sign in|continue with sso/i }).first();\n await submitButton.click();\n }\n\n if (await waitForBackend(7_000)) return;\n\n const loginForm = page.locator('form').first();\n if (await loginForm.isVisible().catch(() => false)) {\n await loginForm.evaluate((element) => {\n const form = element as HTMLFormElement\n form.requestSubmit()\n }).catch(() => {})\n }\n if (await waitForBackend(5_000)) return;\n\n const loginButton = page.getByRole('button', { name: /login|sign in|continue with sso/i }).first();\n if (await loginButton.isVisible().catch(() => false)) {\n await loginButton.click({ force: true });\n }\n if (await waitForBackend(8_000)) return;\n\n throw new Error(`Login did not reach backend for role: ${role}; current URL: ${page.url()}`);\n } catch (error) {\n await throwLoginErrorWithDiagnostics(page, role, authTrace.entries, error);\n } finally {\n authTrace.stop();\n }\n}\n"],
|
|
5
|
-
"mappings": "AACA,SAAS,oBAAoB;AAC7B,SAAS,eAAe;AAExB,SAAS,qBAAoC;AAC3C,QAAM,iBAAiB;AAAA,IACrB,QAAQ,QAAQ,IAAI,GAAG,mBAAmB;AAAA,IAC1C,QAAQ,QAAQ,IAAI,GAAG,MAAM;AAAA,EAC/B;AAEA,aAAW,WAAW,gBAAgB;AACpC,QAAI;AACF,YAAM,UAAU,aAAa,SAAS,OAAO;AAC7C,UAAI,QAAQ,KAAK,EAAE,SAAS,GAAG;AAC7B,eAAO;AAAA,MACT;AAAA,IACF,QAAQ;AACN;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;AAEA,SAAS,aAAa,KAAiC;AACrD,MAAI,QAAQ,IAAI,GAAG,EAAG,QAAO,QAAQ,IAAI,GAAG;AAC5C,QAAM,UAAU,mBAAmB;AACnC,MAAI,CAAC,QAAS,QAAO;AACrB,QAAM,QAAQ,QAAQ,MAAM,IAAI,OAAO,IAAI,GAAG,UAAU,GAAG,CAAC;AAC5D,SAAO,QAAQ,CAAC,GAAG,KAAK;AAC1B;AAEO,MAAM,sBAA2E;AAAA,EACtF,YAAY;AAAA,IACV,OAAO,aAAa,0BAA0B,KAAK;AAAA,IACnD,UAAU,aAAa,6BAA6B,KAAK;AAAA,EAC3D;AAAA,EACA,OAAO,EAAE,OAAO,kBAAkB,UAAU,SAAS;AAAA,EACrD,UAAU,EAAE,OAAO,qBAAqB,UAAU,SAAS;AAC7D;AAIA,SAAS,gBAAgB,OAAoE;AAC3F,QAAM,QAAQ,MAAM,MAAM,GAAG;AAC7B,MAAI,MAAM,SAAS,EAAG,QAAO;AAC7B,MAAI;AACF,UAAM,aAAa,MAAM,CAAC,EAAE,QAAQ,MAAM,GAAG,EAAE,QAAQ,MAAM,GAAG;AAChE,UAAM,SAAS,WAAW,OAAO,KAAK,KAAK,WAAW,SAAS,CAAC,IAAI,GAAG,GAAG;AAC1E,UAAM,UAAU,KAAK,MAAM,OAAO,KAAK,QAAQ,QAAQ,EAAE,SAAS,MAAM,CAAC;AAIzE,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,
|
|
4
|
+
"sourcesContent": ["import { type Page, type Response } from '@playwright/test';\nimport { readFileSync } from 'fs';\nimport { resolve } from 'path';\n\nfunction loadEnvFileContent(): string | null {\n const candidatePaths = [\n resolve(process.cwd(), 'apps/mercato/.env'),\n resolve(process.cwd(), '.env'),\n ];\n\n for (const envPath of candidatePaths) {\n try {\n const content = readFileSync(envPath, 'utf-8');\n if (content.trim().length > 0) {\n return content;\n }\n } catch {\n continue;\n }\n }\n\n return null;\n}\n\nfunction loadEnvValue(key: string): string | undefined {\n if (process.env[key]) return process.env[key];\n const content = loadEnvFileContent();\n if (!content) return undefined;\n const match = content.match(new RegExp(`^${key}=(.+)$`, 'm'));\n return match?.[1]?.trim();\n}\n\nexport const DEFAULT_CREDENTIALS: Record<string, { email: string; password: string }> = {\n superadmin: {\n email: loadEnvValue('OM_INIT_SUPERADMIN_EMAIL') || 'superadmin@acme.com',\n password: loadEnvValue('OM_INIT_SUPERADMIN_PASSWORD') || 'secret',\n },\n admin: { email: 'admin@acme.com', password: 'secret' },\n employee: { email: 'employee@acme.com', password: 'secret' },\n};\n\nexport type Role = 'superadmin' | 'admin' | 'employee';\n\nfunction decodeJwtClaims(token: string): { tenantId?: string; orgId?: string | null } | null {\n const parts = token.split('.');\n if (parts.length < 2) return null;\n try {\n const normalized = parts[1].replace(/-/g, '+').replace(/_/g, '/');\n const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, '=');\n const payload = JSON.parse(Buffer.from(padded, 'base64').toString('utf8')) as {\n tenantId?: string;\n orgId?: string | null;\n };\n return payload;\n } catch {\n return null;\n }\n}\n\nexport async function acknowledgeGlobalNotices(page: Page): Promise<void> {\n const baseUrl = process.env.BASE_URL || 'http://localhost:3000';\n await page.context().addCookies([\n {\n name: 'om_demo_notice_ack',\n value: 'ack',\n url: baseUrl,\n sameSite: 'Lax',\n },\n {\n name: 'om_cookie_notice_ack',\n value: 'ack',\n url: baseUrl,\n sameSite: 'Lax',\n },\n {\n name: 'om_feedback_suppress',\n value: '1',\n url: baseUrl,\n sameSite: 'Lax',\n },\n {\n name: 'om_feedback_shown',\n value: new Date().toISOString().slice(0, 10),\n url: baseUrl,\n sameSite: 'Lax',\n },\n ]);\n}\n\nasync function dismissGlobalNoticesIfPresent(page: Page): Promise<void> {\n const cookieAcceptButton = page.getByRole('button', { name: /accept cookies/i }).first();\n if (await cookieAcceptButton.isVisible().catch(() => false)) {\n await cookieAcceptButton.click();\n }\n\n const demoNotice = page.getByText(/this instance is provided for demo purposes only/i).first();\n if (await demoNotice.isVisible().catch(() => false)) {\n const noticeContainer = demoNotice.locator('xpath=ancestor::div[contains(@class,\"pointer-events-auto\")]').first();\n const dismissButton = noticeContainer.locator('button').first();\n if (await dismissButton.isVisible().catch(() => false)) {\n await dismissButton.click();\n }\n }\n\n const feedbackDialog = page.getByRole('dialog', { name: /Talk to Open Mercato team/i }).first();\n if (await feedbackDialog.isVisible().catch(() => false)) {\n const closeButton = feedbackDialog.getByRole('button', { name: /close/i }).first();\n if (await closeButton.isVisible().catch(() => false)) {\n await closeButton.click().catch(() => {});\n } else {\n await page.keyboard.press('Escape').catch(() => {});\n }\n }\n}\n\nasync function recoverClientSideErrorPageIfPresent(page: Page): Promise<void> {\n const clientErrorHeading = page\n .getByRole('heading', { name: /Application error: a client-side exception has occurred/i })\n .first();\n if (!(await clientErrorHeading.isVisible().catch(() => false))) return;\n await page.reload({ waitUntil: 'domcontentloaded' });\n await dismissGlobalNoticesIfPresent(page);\n}\n\nasync function recoverGenericErrorPageIfPresent(page: Page): Promise<void> {\n const errorHeading = page.getByRole('heading', { name: /^Something went wrong$/i }).first();\n if (!(await errorHeading.isVisible().catch(() => false))) return;\n const retryButton = page.getByRole('button', { name: /Try again/i }).first();\n if (await retryButton.isVisible().catch(() => false)) {\n await retryButton.click().catch(() => {});\n await page.waitForLoadState('domcontentloaded').catch(() => {});\n await page.waitForTimeout(500).catch(() => {});\n } else {\n await page.reload({ waitUntil: 'domcontentloaded' });\n }\n await dismissGlobalNoticesIfPresent(page);\n}\n\ntype AuthNavigationTraceEntry = {\n path: string;\n status: number;\n location: string | null;\n};\n\nfunction toPathOnly(rawUrl: string): string {\n try {\n const url = new URL(rawUrl);\n return `${url.pathname}${url.search}`;\n } catch {\n return '[unparseable-url]';\n }\n}\n\nfunction toRedactedLocation(rawLocation: string | undefined): string | null {\n if (!rawLocation) return null;\n try {\n const url = new URL(rawLocation, 'http://integration.local');\n return `${url.pathname}${url.search}`;\n } catch {\n return '[unparseable-location]';\n }\n}\n\nfunction createAuthNavigationTrace(page: Page): {\n entries: AuthNavigationTraceEntry[];\n stop: () => void;\n} {\n const entries: AuthNavigationTraceEntry[] = [];\n const onResponse = (response: Response) => {\n const path = toPathOnly(response.url());\n if (path === '/backend' || path.startsWith('/api/auth/session/refresh')) {\n entries.push({\n path,\n status: response.status(),\n location: toRedactedLocation(response.headers().location),\n });\n }\n };\n page.on('response', onResponse);\n return {\n entries,\n stop: () => page.off('response', onResponse),\n };\n}\n\nasync function formatLoginDiagnostics(\n page: Page,\n role: Role,\n entries: AuthNavigationTraceEntry[],\n): Promise<string> {\n const cookieNames = Array.from(\n new Set((await page.context().cookies()).map((cookie) => cookie.name)),\n ).sort((left, right) => left.localeCompare(right));\n const traceLines = entries.length\n ? entries.map((entry) => {\n const location = entry.location ? ` -> ${entry.location}` : '';\n return `${entry.status} ${entry.path}${location}`;\n })\n : ['none'];\n\n return [\n `Login diagnostics for role \"${role}\":`,\n `current URL: ${toPathOnly(page.url())}`,\n `browser cookie names: ${cookieNames.length ? cookieNames.join(', ') : 'none'}`,\n 'backend/refresh trace:',\n ...traceLines,\n ].join('\\n');\n}\n\nasync function throwLoginErrorWithDiagnostics(\n page: Page,\n role: Role,\n entries: AuthNavigationTraceEntry[],\n error: unknown,\n): Promise<never> {\n const message = error instanceof Error ? error.message : String(error);\n throw new Error([\n `Login did not reach backend for role: ${role}.`,\n await formatLoginDiagnostics(page, role, entries),\n `Original error: ${message}`,\n ].join('\\n'));\n}\n\nexport async function login(page: Page, role: Role = 'admin'): Promise<void> {\n const authTrace = createAuthNavigationTrace(page);\n try {\n const creds = DEFAULT_CREDENTIALS[role];\n const loginReadySelector = 'form[data-auth-ready=\"1\"]';\n const hasBackendUrl = (): boolean => /\\/backend(?:\\/.*)?$/.test(page.url());\n const waitForBackend = async (timeout: number): Promise<boolean> => {\n try {\n await page.waitForURL(/\\/backend(?:\\/.*)?$/, { timeout });\n return true;\n } catch {\n return hasBackendUrl();\n }\n };\n\n await acknowledgeGlobalNotices(page);\n const apiLoginForm = new URLSearchParams();\n apiLoginForm.set('email', creds.email);\n apiLoginForm.set('password', creds.password);\n // Retry-on-429 against the auth rate limit (5/60s per email). Capped\n // exponential backoff: 1s, 2s, 4s \u2014 worst-case ~7s.\n let apiLoginResponse: Awaited<ReturnType<typeof page.request.post>> | null = null;\n for (let retry = 0; retry < 4; retry += 1) {\n apiLoginResponse = await page.request.post('/api/auth/login', {\n headers: {\n 'content-type': 'application/x-www-form-urlencoded',\n },\n data: apiLoginForm.toString(),\n }).catch(() => null);\n if (!apiLoginResponse || apiLoginResponse.status() !== 429) break;\n await new Promise((resolve) => setTimeout(resolve, 1000 * 2 ** retry));\n }\n if (apiLoginResponse?.ok()) {\n const apiLoginBody = (await apiLoginResponse.json().catch(() => null)) as { token?: string } | null;\n const claims = typeof apiLoginBody?.token === 'string' ? decodeJwtClaims(apiLoginBody.token) : null;\n const baseUrl = process.env.BASE_URL || 'http://localhost:3000';\n const cookies = [];\n if (claims?.tenantId) {\n cookies.push({\n name: 'om_selected_tenant',\n value: claims.tenantId,\n url: baseUrl,\n sameSite: 'Lax' as const,\n });\n }\n if (claims?.orgId) {\n cookies.push({\n name: 'om_selected_org',\n value: claims.orgId,\n url: baseUrl,\n sameSite: 'Lax' as const,\n });\n }\n if (cookies.length > 0) {\n await page.context().addCookies(cookies);\n }\n await page.goto('/backend', { waitUntil: 'domcontentloaded' });\n if (await waitForBackend(8_000)) return;\n }\n\n for (let attempt = 0; attempt < 4; attempt += 1) {\n await page.goto('/login', { waitUntil: 'domcontentloaded' });\n await dismissGlobalNoticesIfPresent(page);\n await recoverClientSideErrorPageIfPresent(page);\n await recoverGenericErrorPageIfPresent(page);\n await page.waitForSelector(loginReadySelector, { state: 'visible', timeout: 3_000 }).catch(() => null);\n if (await page.getByLabel('Email').isVisible().catch(() => false)) break;\n if (attempt === 3) {\n throw new Error(`Login form is unavailable for role: ${role}; current URL: ${page.url()}`);\n }\n }\n await page.getByLabel('Email').fill(creds.email);\n\n const passwordInput = page.getByLabel('Password', { exact: true }).first();\n if (await passwordInput.isVisible().catch(() => false)) {\n await passwordInput.fill(creds.password);\n await passwordInput.press('Enter');\n } else {\n const submitButton = page.getByRole('button', { name: /login|sign in|continue with sso/i }).first();\n await submitButton.click();\n }\n\n if (await waitForBackend(7_000)) return;\n\n const loginForm = page.locator('form').first();\n if (await loginForm.isVisible().catch(() => false)) {\n await loginForm.evaluate((element) => {\n const form = element as HTMLFormElement\n form.requestSubmit()\n }).catch(() => {})\n }\n if (await waitForBackend(5_000)) return;\n\n const loginButton = page.getByRole('button', { name: /login|sign in|continue with sso/i }).first();\n if (await loginButton.isVisible().catch(() => false)) {\n await loginButton.click({ force: true });\n }\n if (await waitForBackend(8_000)) return;\n\n throw new Error(`Login did not reach backend for role: ${role}; current URL: ${page.url()}`);\n } catch (error) {\n await throwLoginErrorWithDiagnostics(page, role, authTrace.entries, error);\n } finally {\n authTrace.stop();\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["resolve"]
|
|
7
7
|
}
|
|
@@ -137,6 +137,25 @@ async function ingestInboundChatMessage(request, token, input) {
|
|
|
137
137
|
channelType: body?.channelType ?? null
|
|
138
138
|
};
|
|
139
139
|
}
|
|
140
|
+
async function ingestInboundEmailMessage(request, token, input) {
|
|
141
|
+
const { senderAddress, ...rest } = input;
|
|
142
|
+
const response = await apiRequest(request, "POST", TEST_SEED_PATH, {
|
|
143
|
+
token,
|
|
144
|
+
data: { action: "ingest-inbound", senderIdentifier: senderAddress, ...rest }
|
|
145
|
+
});
|
|
146
|
+
expect(
|
|
147
|
+
response.status(),
|
|
148
|
+
"POST /api/communication_channels/test-seed (ingest-inbound, email) should return 201"
|
|
149
|
+
).toBe(201);
|
|
150
|
+
const body = await readJsonSafe(response);
|
|
151
|
+
return {
|
|
152
|
+
status: body?.status ?? "unknown",
|
|
153
|
+
messageId: body?.messageId ?? null,
|
|
154
|
+
conversationId: body?.conversationId ?? null,
|
|
155
|
+
channelLinkId: body?.channelLinkId ?? null,
|
|
156
|
+
channelType: body?.channelType ?? null
|
|
157
|
+
};
|
|
158
|
+
}
|
|
140
159
|
async function inspectMessageChannelLinks(request, token, messageId) {
|
|
141
160
|
const response = await apiRequest(request, "POST", TEST_SEED_PATH, {
|
|
142
161
|
token,
|
|
@@ -165,6 +184,7 @@ export {
|
|
|
165
184
|
clearCapturedSystemEmails,
|
|
166
185
|
deleteChannelIfExists,
|
|
167
186
|
ingestInboundChatMessage,
|
|
187
|
+
ingestInboundEmailMessage,
|
|
168
188
|
inspectMessageChannelLinks,
|
|
169
189
|
isChannelSeedingAvailable,
|
|
170
190
|
listCapturedSystemEmails,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/helpers/integration/communicationChannelsFixtures.ts"],
|
|
4
|
-
"sourcesContent": ["import { expect, type APIRequestContext } from '@playwright/test';\nimport { apiRequest } from './api';\nimport { expectId, readJsonSafe } from './generalFixtures';\n\n/**\n * Communication-channels integration fixtures.\n *\n * These drive the TEST-ONLY seed endpoint `POST /api/communication_channels/test-seed`,\n * which is gated by `OM_ENABLE_TEST_CHANNEL_SEEDING` (inert/404 in production). Use\n * {@link isChannelSeedingAvailable} to skip tests when the gate is off rather than\n * failing them.\n */\n\nexport type SeedAddressField =\n | string\n | { address: string; name?: string }\n | Array<string | { address: string; name?: string }>;\n\nexport type CapturedTestSeedEmail = {\n capturedAt: string;\n externalMessageId: string;\n conversationId?: string;\n content: {\n text?: string;\n html?: string;\n bodyFormat?: 'text' | 'markdown' | 'html';\n attachments?: Array<{ url: string; mimeType: string; fileName: string; fileSize?: number; inline?: boolean }>;\n raw?: Record<string, unknown>;\n };\n credentials: Record<string, unknown>;\n scope: {\n tenantId: string;\n organizationId: string;\n };\n metadata?: Record<string, unknown>;\n};\n\nconst TEST_SEED_PATH = '/api/communication_channels/test-seed';\n\nfunction resolveSystemCaptureCredentials(): {\n accessToken: string;\n captureCorrelationToken: string;\n} {\n const accessToken = process.env.OM_TEST_EMAIL_CAPTURE_ACCESS_TOKEN?.trim();\n const captureCorrelationToken = process.env.OM_TEST_EMAIL_CAPTURE_CORRELATION_TOKEN?.trim();\n if (!accessToken || !captureCorrelationToken) {\n throw new Error('[internal] System email capture credentials are not configured by the integration harness');\n }\n return { accessToken, captureCorrelationToken };\n}\n\n/**\n * Probe whether the env-gated test-seed endpoint is enabled in the target app.\n * Returns false when the route answers 404 (flag off) so callers can `test.skip`.\n * The caller's token must hold `communication_channels.connect_user_channel`.\n */\nexport async function isChannelSeedingAvailable(\n request: APIRequestContext,\n token: string,\n): Promise<boolean> {\n // A malformed body returns 422 when the gate is ON and 404 when it is OFF.\n const response = await apiRequest(request, 'POST', TEST_SEED_PATH, {\n token,\n data: { action: '__probe__' },\n });\n return response.status() !== 404;\n}\n\n/**\n * Seed a connected, network-free `__test_seed__` channel owned by the caller.\n * Returns the new channel id. Tear down with {@link deleteChannelIfExists}.\n */\nexport async function seedConnectedChannel(\n request: APIRequestContext,\n token: string,\n input: {\n displayName?: string;\n externalIdentifier?: string;\n /**\n * `chat` connects the non-email stub instead: a channel whose senders carry\n * an opaque handle and no address, and whose `externalIdentifier` is NULL \u2014\n * the shape a real Discord/Slack channel has. Use it for anything asserting\n * the hub's sender-identity contract (#4975); the default `email` flavor\n * cannot prove it, because it can only ever supply email-shaped data.\n */\n providerFlavor?: 'email' | 'chat';\n /**\n * Relabel the connected stub channel's `provider_key`. Provider packages own\n * routes that filter on their own key, and a real channel for those providers\n * cannot be connected in CI (the Discord adapter validates its bot token\n * against the live API), so without this a provider-scoped listing can only\n * be asserted against an empty result \u2014 the assertion that stayed green\n * through #5602.\n *\n * The row is relabelled, nothing is registered: do not drive an outbound send\n * through a relabelled channel, because delivery would resolve the real\n * provider's adapter.\n */\n labelAsProviderKey?: string;\n } = {},\n): Promise<string> {\n const response = await apiRequest(request, 'POST', TEST_SEED_PATH, {\n token,\n data: { action: 'connect-channel', ...input },\n });\n expect(\n response.status(),\n 'POST /api/communication_channels/test-seed (connect-channel) should return 201',\n ).toBe(201);\n const body = await readJsonSafe<{ channelId?: string }>(response);\n return expectId(body?.channelId, 'connect-channel response should include channelId');\n}\n\nexport async function seedSystemEmailChannel(\n request: APIRequestContext,\n token: string,\n input: { displayName?: string; externalIdentifier?: string } = {},\n): Promise<string> {\n const response = await apiRequest(request, 'POST', TEST_SEED_PATH, {\n token,\n data: { action: 'seed-system-channel', ...input },\n });\n expect(\n response.status(),\n 'POST /api/communication_channels/test-seed (seed-system-channel) should return 201',\n ).toBe(201);\n const body = await readJsonSafe<{ channelId?: string }>(response);\n return expectId(body?.channelId, 'seed-system-channel response should include channelId');\n}\n\nexport async function clearCapturedSystemEmails(\n request: APIRequestContext,\n token: string,\n options: { systemRecipient?: string } = {},\n): Promise<void> {\n const captureCredentials = options.systemRecipient ? resolveSystemCaptureCredentials() : null;\n const response = await apiRequest(request, 'POST', TEST_SEED_PATH, {\n token,\n data: {\n action: 'clear-capture',\n ...options,\n ...(captureCredentials\n ? { captureCorrelationToken: captureCredentials.captureCorrelationToken }\n : {}),\n },\n headers: captureCredentials\n ? { 'x-om-test-email-capture-access-token': captureCredentials.accessToken }\n : undefined,\n });\n expect(response.ok(), 'POST /api/communication_channels/test-seed (clear-capture) should succeed').toBeTruthy();\n}\n\nexport async function listCapturedSystemEmails(\n request: APIRequestContext,\n token: string,\n options: { systemRecipient?: string } = {},\n): Promise<CapturedTestSeedEmail[]> {\n const captureCredentials = options.systemRecipient ? resolveSystemCaptureCredentials() : null;\n const response = await apiRequest(request, 'POST', TEST_SEED_PATH, {\n token,\n data: {\n action: 'list-capture',\n ...options,\n ...(captureCredentials\n ? { captureCorrelationToken: captureCredentials.captureCorrelationToken }\n : {}),\n },\n headers: captureCredentials\n ? { 'x-om-test-email-capture-access-token': captureCredentials.accessToken }\n : undefined,\n });\n expect(response.ok(), 'POST /api/communication_channels/test-seed (list-capture) should succeed').toBeTruthy();\n const body = await readJsonSafe<{ items?: CapturedTestSeedEmail[] }>(response);\n return Array.isArray(body?.items) ? body.items : [];\n}\n\nexport async function waitForCapturedSystemEmail(\n request: APIRequestContext,\n token: string,\n predicate: (email: CapturedTestSeedEmail) => boolean,\n options: {\n timeoutMs?: number;\n intervalMs?: number;\n description?: string;\n systemRecipient?: string;\n } = {},\n): Promise<CapturedTestSeedEmail> {\n const timeoutMs = options.timeoutMs ?? 20_000;\n const intervalMs = options.intervalMs ?? 1_000;\n const deadline = Date.now() + timeoutMs;\n let lastItems: CapturedTestSeedEmail[] = [];\n while (Date.now() < deadline) {\n lastItems = await listCapturedSystemEmails(request, token, {\n systemRecipient: options.systemRecipient,\n });\n const found = lastItems.find(predicate);\n if (found) return found;\n await new Promise((resolve) => setTimeout(resolve, intervalMs));\n }\n const summary = lastItems.map((item) => ({\n to: item.metadata?.to,\n from: item.metadata?.from,\n subject: item.metadata?.subject,\n scope: item.scope,\n bodyFormat: item.content?.bodyFormat,\n }));\n throw new Error(\n `Timed out waiting for captured system email${options.description ? `: ${options.description}` : ''}. Captured ${lastItems.length} message(s): ${JSON.stringify(summary)}`,\n );\n}\n\n/**\n * Seed an inbound `MessageChannelLink` for `channelId` and emit\n * `communication_channels.message.received` through the real event bus. The\n * persistent customers link-channel-message-received subscriber is enqueued to\n * the `events` queue \u2014 drain it with `drainIntegrationQueue('events')`.\n *\n * Returns the created link + message ids (the message id is the platform\n * `messages.message` id, usable as `messageThreadId` to thread a follow-up).\n *\n * Pass `createThreadMapping: true` to also seed a `ChannelThreadMapping` for the\n * thread \u2014 required before exercising the reaction (`/messages/[id]/reactions`)\n * and thread-assign (`/threads/[id]/assign`) routes, which resolve the owning\n * channel through that mapping.\n */\nexport async function seedInboundMessage(\n request: APIRequestContext,\n token: string,\n input: {\n channelId: string;\n from?: SeedAddressField;\n to?: SeedAddressField;\n cc?: SeedAddressField;\n subject?: string;\n bodyText?: string;\n messageId?: string;\n inReplyTo?: string;\n references?: string[];\n messageThreadId?: string;\n providerKey?: string;\n createThreadMapping?: boolean;\n },\n): Promise<{ channelLinkId: string; messageId: string; conversationId: string }> {\n const response = await apiRequest(request, 'POST', TEST_SEED_PATH, {\n token,\n data: { action: 'emit-inbound', ...input },\n });\n expect(\n response.status(),\n 'POST /api/communication_channels/test-seed (emit-inbound) should return 201',\n ).toBe(201);\n const body = await readJsonSafe<{\n channelLinkId?: string;\n messageId?: string;\n conversationId?: string;\n }>(response);\n return {\n channelLinkId: expectId(body?.channelLinkId, 'emit-inbound response should include channelLinkId'),\n messageId: expectId(body?.messageId, 'emit-inbound response should include messageId'),\n conversationId: expectId(\n body?.conversationId,\n 'emit-inbound response should include conversationId',\n ),\n };\n}\n\n/**\n * Best-effort hard delete of the rows one {@link seedInboundMessage} call created\n * (thread mapping, channel link, conversation and message). Run it before\n * {@link deleteChannelIfExists}: the purge is authorized against the live channel,\n * and disconnecting a channel deliberately retains its conversations and messages.\n * Safe to call with null ids in `finally`.\n */\nexport async function purgeSeededInboundMessage(\n request: APIRequestContext,\n token: string | null,\n channelId: string | null,\n seeded: { channelLinkId: string; messageId: string; conversationId: string } | null,\n): Promise<void> {\n if (!token || !channelId || !seeded) return;\n await apiRequest(request, 'POST', TEST_SEED_PATH, {\n token,\n data: { action: 'purge-inbound', channelId, ...seeded },\n }).catch(() => undefined);\n}\n\n/**\n * Ingest an inbound chat message through the REAL `ingest_inbound_message`\n * command: the stub adapter normalizes the frame and the hub composes the\n * platform message via `messages.messages.compose`.\n *\n * Unlike {@link seedInboundMessage}, nothing here is short-circuited \u2014 no row is\n * inserted behind the compose path \u2014 so a hub contract the provider cannot\n * satisfy fails the test instead of hiding behind seeded data. That bypass is\n * exactly why CI was green while every real inbound Discord message was rejected\n * (#4975).\n *\n * The sender is identified only by `senderIdentifier` (a Discord snowflake, a\n * Slack member id, \u2026). There is deliberately no way to pass an address.\n */\nexport async function ingestInboundChatMessage(\n request: APIRequestContext,\n token: string,\n input: {\n channelId: string;\n senderIdentifier: string;\n senderDisplayName?: string;\n body?: string;\n externalMessageId: string;\n externalConversationId: string;\n },\n): Promise<{\n status: string;\n messageId: string | null;\n conversationId: string | null;\n channelLinkId: string | null;\n channelType: string | null;\n}> {\n const response = await apiRequest(request, 'POST', TEST_SEED_PATH, {\n token,\n data: { action: 'ingest-inbound', ...input },\n });\n expect(\n response.status(),\n 'POST /api/communication_channels/test-seed (ingest-inbound) should return 201 \u2014 ' +\n 'a non-201 here means the hub rejected a sender with no email address',\n ).toBe(201);\n const body = await readJsonSafe<{\n status?: string;\n messageId?: string | null;\n conversationId?: string | null;\n channelLinkId?: string | null;\n channelType?: string | null;\n }>(response);\n return {\n status: body?.status ?? 'unknown',\n messageId: body?.messageId ?? null,\n conversationId: body?.conversationId ?? null,\n channelLinkId: body?.channelLinkId ?? null,\n channelType: body?.channelType ?? null,\n };\n}\n\nexport type InspectedMessageLinks = {\n links: Array<{\n id: string;\n direction: string;\n providerKey: string;\n channelType: string;\n deliveryStatus: string;\n externalConversationId: string | null;\n }>;\n threadMappings: Array<{\n id: string;\n messageThreadId: string;\n channelId: string;\n externalConversationId: string;\n }>;\n};\n\n/**\n * Report the `MessageChannelLink` rows and `ChannelThreadMapping` rows a\n * platform message carries. No product API exposes them, so this is the only way\n * to assert that a send really was routed through the hub rather than the\n * platform sender.\n */\nexport async function inspectMessageChannelLinks(\n request: APIRequestContext,\n token: string,\n messageId: string,\n): Promise<InspectedMessageLinks> {\n const response = await apiRequest(request, 'POST', TEST_SEED_PATH, {\n token,\n data: { action: 'inspect-message-links', messageId },\n });\n expect(\n response.status(),\n 'POST /api/communication_channels/test-seed (inspect-message-links) should return 200',\n ).toBe(200);\n const body = await readJsonSafe<InspectedMessageLinks>(response);\n return {\n links: Array.isArray(body?.links) ? body.links : [],\n threadMappings: Array.isArray(body?.threadMappings) ? body.threadMappings : [],\n };\n}\n\n/**\n * Best-effort delete of a seeded channel via the owner-scoped DELETE route.\n * Safe to call with a null id in `finally`.\n */\nexport async function deleteChannelIfExists(\n request: APIRequestContext,\n token: string | null,\n channelId: string | null,\n): Promise<void> {\n if (!token || !channelId) return;\n await apiRequest(\n request,\n 'DELETE',\n `/api/communication_channels/channels/${encodeURIComponent(channelId)}`,\n { token },\n ).catch(() => undefined);\n}\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["import { expect, type APIRequestContext } from '@playwright/test';\nimport { apiRequest } from './api';\nimport { expectId, readJsonSafe } from './generalFixtures';\n\n/**\n * Communication-channels integration fixtures.\n *\n * These drive the TEST-ONLY seed endpoint `POST /api/communication_channels/test-seed`,\n * which is gated by `OM_ENABLE_TEST_CHANNEL_SEEDING` (inert/404 in production). Use\n * {@link isChannelSeedingAvailable} to skip tests when the gate is off rather than\n * failing them.\n */\n\nexport type SeedAddressField =\n | string\n | { address: string; name?: string }\n | Array<string | { address: string; name?: string }>;\n\nexport type CapturedTestSeedEmail = {\n capturedAt: string;\n externalMessageId: string;\n conversationId?: string;\n content: {\n text?: string;\n html?: string;\n bodyFormat?: 'text' | 'markdown' | 'html';\n attachments?: Array<{ url: string; mimeType: string; fileName: string; fileSize?: number; inline?: boolean }>;\n raw?: Record<string, unknown>;\n };\n credentials: Record<string, unknown>;\n scope: {\n tenantId: string;\n organizationId: string;\n };\n metadata?: Record<string, unknown>;\n};\n\nconst TEST_SEED_PATH = '/api/communication_channels/test-seed';\n\nfunction resolveSystemCaptureCredentials(): {\n accessToken: string;\n captureCorrelationToken: string;\n} {\n const accessToken = process.env.OM_TEST_EMAIL_CAPTURE_ACCESS_TOKEN?.trim();\n const captureCorrelationToken = process.env.OM_TEST_EMAIL_CAPTURE_CORRELATION_TOKEN?.trim();\n if (!accessToken || !captureCorrelationToken) {\n throw new Error('[internal] System email capture credentials are not configured by the integration harness');\n }\n return { accessToken, captureCorrelationToken };\n}\n\n/**\n * Probe whether the env-gated test-seed endpoint is enabled in the target app.\n * Returns false when the route answers 404 (flag off) so callers can `test.skip`.\n * The caller's token must hold `communication_channels.connect_user_channel`.\n */\nexport async function isChannelSeedingAvailable(\n request: APIRequestContext,\n token: string,\n): Promise<boolean> {\n // A malformed body returns 422 when the gate is ON and 404 when it is OFF.\n const response = await apiRequest(request, 'POST', TEST_SEED_PATH, {\n token,\n data: { action: '__probe__' },\n });\n return response.status() !== 404;\n}\n\n/**\n * Seed a connected, network-free `__test_seed__` channel owned by the caller.\n * Returns the new channel id. Tear down with {@link deleteChannelIfExists}.\n */\nexport async function seedConnectedChannel(\n request: APIRequestContext,\n token: string,\n input: {\n displayName?: string;\n externalIdentifier?: string;\n /**\n * `chat` connects the non-email stub instead: a channel whose senders carry\n * an opaque handle and no address, and whose `externalIdentifier` is NULL \u2014\n * the shape a real Discord/Slack channel has. Use it for anything asserting\n * the hub's sender-identity contract (#4975); the default `email` flavor\n * cannot prove it, because it can only ever supply email-shaped data.\n */\n providerFlavor?: 'email' | 'chat';\n /**\n * Relabel the connected stub channel's `provider_key`. Provider packages own\n * routes that filter on their own key, and a real channel for those providers\n * cannot be connected in CI (the Discord adapter validates its bot token\n * against the live API), so without this a provider-scoped listing can only\n * be asserted against an empty result \u2014 the assertion that stayed green\n * through #5602.\n *\n * The row is relabelled, nothing is registered: do not drive an outbound send\n * through a relabelled channel, because delivery would resolve the real\n * provider's adapter.\n */\n labelAsProviderKey?: string;\n } = {},\n): Promise<string> {\n const response = await apiRequest(request, 'POST', TEST_SEED_PATH, {\n token,\n data: { action: 'connect-channel', ...input },\n });\n expect(\n response.status(),\n 'POST /api/communication_channels/test-seed (connect-channel) should return 201',\n ).toBe(201);\n const body = await readJsonSafe<{ channelId?: string }>(response);\n return expectId(body?.channelId, 'connect-channel response should include channelId');\n}\n\nexport async function seedSystemEmailChannel(\n request: APIRequestContext,\n token: string,\n input: { displayName?: string; externalIdentifier?: string } = {},\n): Promise<string> {\n const response = await apiRequest(request, 'POST', TEST_SEED_PATH, {\n token,\n data: { action: 'seed-system-channel', ...input },\n });\n expect(\n response.status(),\n 'POST /api/communication_channels/test-seed (seed-system-channel) should return 201',\n ).toBe(201);\n const body = await readJsonSafe<{ channelId?: string }>(response);\n return expectId(body?.channelId, 'seed-system-channel response should include channelId');\n}\n\nexport async function clearCapturedSystemEmails(\n request: APIRequestContext,\n token: string,\n options: { systemRecipient?: string } = {},\n): Promise<void> {\n const captureCredentials = options.systemRecipient ? resolveSystemCaptureCredentials() : null;\n const response = await apiRequest(request, 'POST', TEST_SEED_PATH, {\n token,\n data: {\n action: 'clear-capture',\n ...options,\n ...(captureCredentials\n ? { captureCorrelationToken: captureCredentials.captureCorrelationToken }\n : {}),\n },\n headers: captureCredentials\n ? { 'x-om-test-email-capture-access-token': captureCredentials.accessToken }\n : undefined,\n });\n expect(response.ok(), 'POST /api/communication_channels/test-seed (clear-capture) should succeed').toBeTruthy();\n}\n\nexport async function listCapturedSystemEmails(\n request: APIRequestContext,\n token: string,\n options: { systemRecipient?: string } = {},\n): Promise<CapturedTestSeedEmail[]> {\n const captureCredentials = options.systemRecipient ? resolveSystemCaptureCredentials() : null;\n const response = await apiRequest(request, 'POST', TEST_SEED_PATH, {\n token,\n data: {\n action: 'list-capture',\n ...options,\n ...(captureCredentials\n ? { captureCorrelationToken: captureCredentials.captureCorrelationToken }\n : {}),\n },\n headers: captureCredentials\n ? { 'x-om-test-email-capture-access-token': captureCredentials.accessToken }\n : undefined,\n });\n expect(response.ok(), 'POST /api/communication_channels/test-seed (list-capture) should succeed').toBeTruthy();\n const body = await readJsonSafe<{ items?: CapturedTestSeedEmail[] }>(response);\n return Array.isArray(body?.items) ? body.items : [];\n}\n\nexport async function waitForCapturedSystemEmail(\n request: APIRequestContext,\n token: string,\n predicate: (email: CapturedTestSeedEmail) => boolean,\n options: {\n timeoutMs?: number;\n intervalMs?: number;\n description?: string;\n systemRecipient?: string;\n } = {},\n): Promise<CapturedTestSeedEmail> {\n const timeoutMs = options.timeoutMs ?? 20_000;\n const intervalMs = options.intervalMs ?? 1_000;\n const deadline = Date.now() + timeoutMs;\n let lastItems: CapturedTestSeedEmail[] = [];\n while (Date.now() < deadline) {\n lastItems = await listCapturedSystemEmails(request, token, {\n systemRecipient: options.systemRecipient,\n });\n const found = lastItems.find(predicate);\n if (found) return found;\n await new Promise((resolve) => setTimeout(resolve, intervalMs));\n }\n const summary = lastItems.map((item) => ({\n to: item.metadata?.to,\n from: item.metadata?.from,\n subject: item.metadata?.subject,\n scope: item.scope,\n bodyFormat: item.content?.bodyFormat,\n }));\n throw new Error(\n `Timed out waiting for captured system email${options.description ? `: ${options.description}` : ''}. Captured ${lastItems.length} message(s): ${JSON.stringify(summary)}`,\n );\n}\n\n/**\n * Seed an inbound `MessageChannelLink` for `channelId` and emit\n * `communication_channels.message.received` through the real event bus. The\n * persistent customers link-channel-message-received subscriber is enqueued to\n * the `events` queue \u2014 drain it with `drainIntegrationQueue('events')`.\n *\n * Returns the created link + message ids (the message id is the platform\n * `messages.message` id, usable as `messageThreadId` to thread a follow-up).\n *\n * Pass `createThreadMapping: true` to also seed a `ChannelThreadMapping` for the\n * thread \u2014 required before exercising the reaction (`/messages/[id]/reactions`)\n * and thread-assign (`/threads/[id]/assign`) routes, which resolve the owning\n * channel through that mapping.\n */\nexport async function seedInboundMessage(\n request: APIRequestContext,\n token: string,\n input: {\n channelId: string;\n from?: SeedAddressField;\n to?: SeedAddressField;\n cc?: SeedAddressField;\n subject?: string;\n bodyText?: string;\n messageId?: string;\n inReplyTo?: string;\n references?: string[];\n messageThreadId?: string;\n providerKey?: string;\n createThreadMapping?: boolean;\n },\n): Promise<{ channelLinkId: string; messageId: string; conversationId: string }> {\n const response = await apiRequest(request, 'POST', TEST_SEED_PATH, {\n token,\n data: { action: 'emit-inbound', ...input },\n });\n expect(\n response.status(),\n 'POST /api/communication_channels/test-seed (emit-inbound) should return 201',\n ).toBe(201);\n const body = await readJsonSafe<{\n channelLinkId?: string;\n messageId?: string;\n conversationId?: string;\n }>(response);\n return {\n channelLinkId: expectId(body?.channelLinkId, 'emit-inbound response should include channelLinkId'),\n messageId: expectId(body?.messageId, 'emit-inbound response should include messageId'),\n conversationId: expectId(\n body?.conversationId,\n 'emit-inbound response should include conversationId',\n ),\n };\n}\n\n/**\n * Best-effort hard delete of the rows one seeded or test-ingested inbound message\n * created (thread mapping, channel link, external message, conversation and\n * platform message). Run it before\n * {@link deleteChannelIfExists}: the purge is authorized against the live channel,\n * and disconnecting a channel deliberately retains its conversations and messages.\n * Safe to call with null ids in `finally`.\n */\nexport async function purgeSeededInboundMessage(\n request: APIRequestContext,\n token: string | null,\n channelId: string | null,\n seeded: { channelLinkId: string; messageId: string; conversationId: string } | null,\n): Promise<void> {\n if (!token || !channelId || !seeded) return;\n await apiRequest(request, 'POST', TEST_SEED_PATH, {\n token,\n data: { action: 'purge-inbound', channelId, ...seeded },\n }).catch(() => undefined);\n}\n\n/**\n * Ingest an inbound chat message through the REAL `ingest_inbound_message`\n * command: the stub adapter normalizes the frame and the hub composes the\n * platform message via `messages.messages.compose`.\n *\n * Unlike {@link seedInboundMessage}, nothing here is short-circuited \u2014 no row is\n * inserted behind the compose path \u2014 so a hub contract the provider cannot\n * satisfy fails the test instead of hiding behind seeded data. That bypass is\n * exactly why CI was green while every real inbound Discord message was rejected\n * (#4975).\n *\n * The sender is identified only by `senderIdentifier` (a Discord snowflake, a\n * Slack member id, \u2026). There is deliberately no way to pass an address.\n */\nexport async function ingestInboundChatMessage(\n request: APIRequestContext,\n token: string,\n input: {\n channelId: string;\n senderIdentifier: string;\n senderDisplayName?: string;\n body?: string;\n externalMessageId: string;\n externalConversationId: string;\n },\n): Promise<{\n status: string;\n messageId: string | null;\n conversationId: string | null;\n channelLinkId: string | null;\n channelType: string | null;\n}> {\n const response = await apiRequest(request, 'POST', TEST_SEED_PATH, {\n token,\n data: { action: 'ingest-inbound', ...input },\n });\n expect(\n response.status(),\n 'POST /api/communication_channels/test-seed (ingest-inbound) should return 201 \u2014 ' +\n 'a non-201 here means the hub rejected a sender with no email address',\n ).toBe(201);\n const body = await readJsonSafe<{\n status?: string;\n messageId?: string | null;\n conversationId?: string | null;\n channelLinkId?: string | null;\n channelType?: string | null;\n }>(response);\n return {\n status: body?.status ?? 'unknown',\n messageId: body?.messageId ?? null,\n conversationId: body?.conversationId ?? null,\n channelLinkId: body?.channelLinkId ?? null,\n channelType: body?.channelType ?? null,\n };\n}\n\n/**\n * Ingest an inbound email through the real `ingest_inbound_message` command on a\n * channel connected with the default `email` flavor. The stub adapter turns the\n * frame into an email-shaped inbound message whose sender is `senderAddress`, so\n * the hub derives `externalEmail` from it and composes the platform message via\n * `messages.messages.compose` exactly as a real IMAP/Gmail/Graph adapter would.\n *\n * Use this \u2014 not {@link seedInboundMessage}, which bypasses compose \u2014 for any\n * assertion about what the hub does with an inbound email after ingest (for\n * example that it is not echoed back to its sender, #6089). Prefix the supplied\n * external conversation id with `inbound-seed:` when the test will use\n * {@link purgeSeededInboundMessage} for teardown.\n */\nexport async function ingestInboundEmailMessage(\n request: APIRequestContext,\n token: string,\n input: {\n channelId: string;\n senderAddress: string;\n senderDisplayName?: string;\n subject?: string;\n body?: string;\n externalMessageId: string;\n externalConversationId: string;\n },\n): Promise<{\n status: string;\n messageId: string | null;\n conversationId: string | null;\n channelLinkId: string | null;\n channelType: string | null;\n}> {\n const { senderAddress, ...rest } = input;\n const response = await apiRequest(request, 'POST', TEST_SEED_PATH, {\n token,\n data: { action: 'ingest-inbound', senderIdentifier: senderAddress, ...rest },\n });\n expect(\n response.status(),\n 'POST /api/communication_channels/test-seed (ingest-inbound, email) should return 201',\n ).toBe(201);\n const body = await readJsonSafe<{\n status?: string;\n messageId?: string | null;\n conversationId?: string | null;\n channelLinkId?: string | null;\n channelType?: string | null;\n }>(response);\n return {\n status: body?.status ?? 'unknown',\n messageId: body?.messageId ?? null,\n conversationId: body?.conversationId ?? null,\n channelLinkId: body?.channelLinkId ?? null,\n channelType: body?.channelType ?? null,\n };\n}\n\nexport type InspectedMessageLinks = {\n links: Array<{\n id: string;\n direction: string;\n providerKey: string;\n channelType: string;\n deliveryStatus: string;\n externalConversationId: string | null;\n }>;\n threadMappings: Array<{\n id: string;\n messageThreadId: string;\n channelId: string;\n externalConversationId: string;\n }>;\n};\n\n/**\n * Report the `MessageChannelLink` rows and `ChannelThreadMapping` rows a\n * platform message carries. No product API exposes them, so this is the only way\n * to assert that a send really was routed through the hub rather than the\n * platform sender.\n */\nexport async function inspectMessageChannelLinks(\n request: APIRequestContext,\n token: string,\n messageId: string,\n): Promise<InspectedMessageLinks> {\n const response = await apiRequest(request, 'POST', TEST_SEED_PATH, {\n token,\n data: { action: 'inspect-message-links', messageId },\n });\n expect(\n response.status(),\n 'POST /api/communication_channels/test-seed (inspect-message-links) should return 200',\n ).toBe(200);\n const body = await readJsonSafe<InspectedMessageLinks>(response);\n return {\n links: Array.isArray(body?.links) ? body.links : [],\n threadMappings: Array.isArray(body?.threadMappings) ? body.threadMappings : [],\n };\n}\n\n/**\n * Best-effort delete of a seeded channel via the owner-scoped DELETE route.\n * Safe to call with a null id in `finally`.\n */\nexport async function deleteChannelIfExists(\n request: APIRequestContext,\n token: string | null,\n channelId: string | null,\n): Promise<void> {\n if (!token || !channelId) return;\n await apiRequest(\n request,\n 'DELETE',\n `/api/communication_channels/channels/${encodeURIComponent(channelId)}`,\n { token },\n ).catch(() => undefined);\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -2,6 +2,7 @@ import { NextResponse } from "next/server";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
4
4
|
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
5
|
+
import { resolveOrganizationScopeForRequest } from "@open-mercato/core/modules/directory/utils/organizationScope";
|
|
5
6
|
import { readJsonSafe } from "@open-mercato/shared/lib/http/readJsonSafe";
|
|
6
7
|
import {
|
|
7
8
|
CATALOG_PRODUCT_BULK_DELETE_QUEUE,
|
|
@@ -44,6 +45,15 @@ async function POST(req) {
|
|
|
44
45
|
const ids = Array.from(new Set(parsed.data.ids));
|
|
45
46
|
const container = await createRequestContainer();
|
|
46
47
|
const progressService = container.resolve("progressService");
|
|
48
|
+
const orgScope = await resolveOrganizationScopeForRequest({ container, auth, request: req });
|
|
49
|
+
if (orgScope?.selectionRejected) {
|
|
50
|
+
return NextResponse.json(responseSchema.parse({
|
|
51
|
+
ok: false,
|
|
52
|
+
progressJobId: null,
|
|
53
|
+
message: "Your selected organization is no longer available. Please re-select an organization and try again."
|
|
54
|
+
}), { status: 422 });
|
|
55
|
+
}
|
|
56
|
+
const organizationId = orgScope?.selectedId ?? auth.orgId;
|
|
47
57
|
const progressJob = await progressService.createJob(
|
|
48
58
|
{
|
|
49
59
|
jobType: "catalog.products.bulk_delete",
|
|
@@ -58,7 +68,7 @@ async function POST(req) {
|
|
|
58
68
|
},
|
|
59
69
|
{
|
|
60
70
|
tenantId: auth.tenantId,
|
|
61
|
-
organizationId
|
|
71
|
+
organizationId,
|
|
62
72
|
userId: auth.sub
|
|
63
73
|
}
|
|
64
74
|
);
|
|
@@ -67,7 +77,7 @@ async function POST(req) {
|
|
|
67
77
|
progressJobId: progressJob.id,
|
|
68
78
|
ids,
|
|
69
79
|
scope: {
|
|
70
|
-
organizationId
|
|
80
|
+
organizationId,
|
|
71
81
|
tenantId: auth.tenantId,
|
|
72
82
|
userId: auth.sub
|
|
73
83
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/catalog/api/bulk-delete/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport type { ProgressService } from '../../../progress/lib/progressService'\nimport {\n CATALOG_PRODUCT_BULK_DELETE_QUEUE,\n getCatalogQueue,\n} from '../../lib/bulkDelete'\n\nconst requestSchema = z.object({\n confirm: z.literal(true),\n ids: z.array(z.string().uuid()).min(1).max(10000),\n scope: z.enum(['selected', 'filtered']),\n})\n\nconst responseSchema = z.object({\n ok: z.boolean(),\n progressJobId: z.string().uuid().nullable(),\n message: z.string(),\n})\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['catalog.products.manage'] },\n}\n\nexport const openApi = {\n tags: ['Catalog'],\n summary: 'Start bulk deleting catalog products',\n}\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId || !auth.orgId) {\n return NextResponse.json(responseSchema.parse({\n ok: false,\n progressJobId: null,\n message: 'Unauthorized',\n }), { status: 401 })\n }\n\n const parsed = requestSchema.safeParse(await readJsonSafe(req))\n if (!parsed.success) {\n return NextResponse.json(responseSchema.parse({\n ok: false,\n progressJobId: null,\n message: 'Invalid payload',\n }), { status: 400 })\n }\n\n const ids = Array.from(new Set(parsed.data.ids))\n const container = await createRequestContainer()\n const progressService = container.resolve('progressService') as ProgressService\n\n const progressJob = await progressService.createJob(\n {\n jobType: 'catalog.products.bulk_delete',\n name: parsed.data.scope === 'filtered'\n ? 'Delete filtered products'\n : 'Delete selected products',\n description: `${ids.length} catalog products queued for deletion`,\n totalCount: ids.length,\n cancellable: false,\n meta: {\n source: 'catalog.bulk-delete',\n scope: parsed.data.scope,\n },\n },\n {\n tenantId: auth.tenantId,\n organizationId
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AACvC,SAAS,oBAAoB;AAE7B;AAAA,EACE;AAAA,EACA;AAAA,OACK;AAEP,MAAM,gBAAgB,EAAE,OAAO;AAAA,EAC7B,SAAS,EAAE,QAAQ,IAAI;AAAA,EACvB,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,IAAI,CAAC,EAAE,IAAI,GAAK;AAAA,EAChD,OAAO,EAAE,KAAK,CAAC,YAAY,UAAU,CAAC;AACxC,CAAC;AAED,MAAM,iBAAiB,EAAE,OAAO;AAAA,EAC9B,IAAI,EAAE,QAAQ;AAAA,EACd,eAAe,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AAAA,EAC1C,SAAS,EAAE,OAAO;AACpB,CAAC;AAEM,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,yBAAyB,EAAE;AAC1E;AAEO,MAAM,UAAU;AAAA,EACrB,MAAM,CAAC,SAAS;AAAA,EAChB,SAAS;AACX;AAEA,eAAsB,KAAK,KAAc;AACvC,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,MAAM,YAAY,CAAC,KAAK,OAAO;AAClC,WAAO,aAAa,KAAK,eAAe,MAAM;AAAA,MAC5C,IAAI;AAAA,MACJ,eAAe;AAAA,MACf,SAAS;AAAA,IACX,CAAC,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACrB;AAEA,QAAM,SAAS,cAAc,UAAU,MAAM,aAAa,GAAG,CAAC;AAC9D,MAAI,CAAC,OAAO,SAAS;AACnB,WAAO,aAAa,KAAK,eAAe,MAAM;AAAA,MAC5C,IAAI;AAAA,MACJ,eAAe;AAAA,MACf,SAAS;AAAA,IACX,CAAC,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACrB;AAEA,QAAM,MAAM,MAAM,KAAK,IAAI,IAAI,OAAO,KAAK,GAAG,CAAC;AAC/C,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,kBAAkB,UAAU,QAAQ,iBAAiB;
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport type { ProgressService } from '../../../progress/lib/progressService'\nimport {\n CATALOG_PRODUCT_BULK_DELETE_QUEUE,\n getCatalogQueue,\n} from '../../lib/bulkDelete'\n\nconst requestSchema = z.object({\n confirm: z.literal(true),\n ids: z.array(z.string().uuid()).min(1).max(10000),\n scope: z.enum(['selected', 'filtered']),\n})\n\nconst responseSchema = z.object({\n ok: z.boolean(),\n progressJobId: z.string().uuid().nullable(),\n message: z.string(),\n})\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['catalog.products.manage'] },\n}\n\nexport const openApi = {\n tags: ['Catalog'],\n summary: 'Start bulk deleting catalog products',\n}\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId || !auth.orgId) {\n return NextResponse.json(responseSchema.parse({\n ok: false,\n progressJobId: null,\n message: 'Unauthorized',\n }), { status: 401 })\n }\n\n const parsed = requestSchema.safeParse(await readJsonSafe(req))\n if (!parsed.success) {\n return NextResponse.json(responseSchema.parse({\n ok: false,\n progressJobId: null,\n message: 'Invalid payload',\n }), { status: 400 })\n }\n\n const ids = Array.from(new Set(parsed.data.ids))\n const container = await createRequestContainer()\n const progressService = container.resolve('progressService') as ProgressService\n const orgScope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n if (orgScope?.selectionRejected) {\n return NextResponse.json(responseSchema.parse({\n ok: false,\n progressJobId: null,\n message: 'Your selected organization is no longer available. Please re-select an organization and try again.',\n }), { status: 422 })\n }\n const organizationId = orgScope?.selectedId ?? auth.orgId\n\n const progressJob = await progressService.createJob(\n {\n jobType: 'catalog.products.bulk_delete',\n name: parsed.data.scope === 'filtered'\n ? 'Delete filtered products'\n : 'Delete selected products',\n description: `${ids.length} catalog products queued for deletion`,\n totalCount: ids.length,\n cancellable: false,\n meta: {\n source: 'catalog.bulk-delete',\n scope: parsed.data.scope,\n },\n },\n {\n tenantId: auth.tenantId,\n organizationId,\n userId: auth.sub,\n },\n )\n\n const queue = getCatalogQueue(CATALOG_PRODUCT_BULK_DELETE_QUEUE)\n await queue.enqueue({\n progressJobId: progressJob.id,\n ids,\n scope: {\n organizationId,\n tenantId: auth.tenantId,\n userId: auth.sub,\n },\n })\n\n return NextResponse.json(responseSchema.parse({\n ok: true,\n progressJobId: progressJob.id,\n message: 'Bulk delete started.',\n }), { status: 202 })\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
ChannelThreadMapping,
|
|
12
12
|
CommunicationChannel,
|
|
13
13
|
ExternalConversation,
|
|
14
|
+
ExternalMessage,
|
|
14
15
|
MessageChannelLink
|
|
15
16
|
} from "../../../data/entities.js";
|
|
16
17
|
import { ChannelAccessDeniedError, assertCanManageChannel } from "../../../lib/access-control.js";
|
|
@@ -76,9 +77,15 @@ const connectChannelSchema = z.object({
|
|
|
76
77
|
const ingestInboundSchema = z.object({
|
|
77
78
|
action: z.literal("ingest-inbound"),
|
|
78
79
|
channelId: z.string().uuid(),
|
|
79
|
-
/**
|
|
80
|
+
/**
|
|
81
|
+
* Opaque sender handle — a Discord snowflake, a Slack member id, etc. For a
|
|
82
|
+
* channel connected with the default `email` flavor this is the sender's
|
|
83
|
+
* address, and the email stub normalizes it into an email-shaped frame.
|
|
84
|
+
*/
|
|
80
85
|
senderIdentifier: z.string().min(1).max(255),
|
|
81
86
|
senderDisplayName: z.string().max(255).optional(),
|
|
87
|
+
/** Honored by the email stub only; chat frames carry no subject. */
|
|
88
|
+
subject: z.string().max(500).optional(),
|
|
82
89
|
body: z.string().max(5e4).optional(),
|
|
83
90
|
externalMessageId: z.string().min(1).max(255),
|
|
84
91
|
externalConversationId: z.string().min(1).max(255)
|
|
@@ -410,6 +417,11 @@ async function POST(req) {
|
|
|
410
417
|
externalConversationId: body.conversationId,
|
|
411
418
|
...rowScope
|
|
412
419
|
});
|
|
420
|
+
await em.nativeDelete(ExternalMessage, {
|
|
421
|
+
conversationId: body.conversationId,
|
|
422
|
+
channelId: body.channelId,
|
|
423
|
+
...rowScope
|
|
424
|
+
});
|
|
413
425
|
await em.nativeDelete(ExternalConversation, {
|
|
414
426
|
id: body.conversationId,
|
|
415
427
|
channelId: body.channelId,
|
|
@@ -418,16 +430,17 @@ async function POST(req) {
|
|
|
418
430
|
await deleteTestSeedPlatformMessage(em, {
|
|
419
431
|
messageId: body.messageId,
|
|
420
432
|
channelId: body.channelId,
|
|
433
|
+
conversationId: body.conversationId,
|
|
421
434
|
...rowScope
|
|
422
435
|
});
|
|
423
436
|
return NextResponse.json({ ok: true });
|
|
424
437
|
}
|
|
425
438
|
if (body.action === "ingest-inbound") {
|
|
426
439
|
const channelProviderKey = ownedChannel.providerKey;
|
|
427
|
-
if (channelProviderKey !== TEST_SEED_CHAT_PROVIDER_KEY) {
|
|
440
|
+
if (channelProviderKey !== TEST_SEED_CHAT_PROVIDER_KEY && channelProviderKey !== TEST_SEED_PROVIDER_KEY) {
|
|
428
441
|
return NextResponse.json(
|
|
429
442
|
{
|
|
430
|
-
error: `ingest-inbound requires a channel connected
|
|
443
|
+
error: `ingest-inbound requires a channel connected through the test-seed stub; channel ${body.channelId} is '${channelProviderKey}'`
|
|
431
444
|
},
|
|
432
445
|
{ status: 422 }
|
|
433
446
|
);
|
|
@@ -449,6 +462,7 @@ async function POST(req) {
|
|
|
449
462
|
externalConversationId: body.externalConversationId,
|
|
450
463
|
senderIdentifier: body.senderIdentifier,
|
|
451
464
|
senderDisplayName: body.senderDisplayName,
|
|
465
|
+
...body.subject !== void 0 ? { subject: body.subject } : {},
|
|
452
466
|
body: body.body ?? ""
|
|
453
467
|
},
|
|
454
468
|
eventType: "message",
|