@open-mercato/core 0.10.1-develop.7717.1.2befe9c634 → 0.10.1-develop.7721.1.b5c4a00a68

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (384) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/dist/generated/entities/staff_support_routing_rule/index.js +21 -0
  3. package/dist/generated/entities/staff_support_routing_rule/index.js.map +7 -0
  4. package/dist/generated/entities/staff_support_ticket/index.js +37 -0
  5. package/dist/generated/entities/staff_support_ticket/index.js.map +7 -0
  6. package/dist/generated/entities/staff_time_project/index.js +2 -0
  7. package/dist/generated/entities/staff_time_project/index.js.map +2 -2
  8. package/dist/generated/entities/staff_time_project_member/index.js +2 -0
  9. package/dist/generated/entities/staff_time_project_member/index.js.map +2 -2
  10. package/dist/generated/entities/staff_time_task/index.js +4 -0
  11. package/dist/generated/entities/staff_time_task/index.js.map +2 -2
  12. package/dist/generated/entities/staff_time_task_comment/index.js +9 -1
  13. package/dist/generated/entities/staff_time_task_comment/index.js.map +2 -2
  14. package/dist/generated/entities.ids.generated.js +2 -0
  15. package/dist/generated/entities.ids.generated.js.map +2 -2
  16. package/dist/generated/entity-fields-registry.js +38 -0
  17. package/dist/generated/entity-fields-registry.js.map +2 -2
  18. package/dist/helpers/integration/auth.js +1 -0
  19. package/dist/helpers/integration/auth.js.map +2 -2
  20. package/dist/helpers/integration/communicationChannelsFixtures.js +20 -0
  21. package/dist/helpers/integration/communicationChannelsFixtures.js.map +2 -2
  22. package/dist/modules/catalog/api/bulk-delete/route.js +12 -2
  23. package/dist/modules/catalog/api/bulk-delete/route.js.map +2 -2
  24. package/dist/modules/communication_channels/api/post/test-seed/route.js +17 -3
  25. package/dist/modules/communication_channels/api/post/test-seed/route.js.map +2 -2
  26. package/dist/modules/communication_channels/lib/test-seed.js +38 -5
  27. package/dist/modules/communication_channels/lib/test-seed.js.map +2 -2
  28. package/dist/modules/messages/commands/messages.js +2 -3
  29. package/dist/modules/messages/commands/messages.js.map +2 -2
  30. package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/page.js +2 -2
  31. package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/page.js.map +2 -2
  32. package/dist/modules/staff/api/portal/support-tickets/[id]/attachments/route.js +299 -0
  33. package/dist/modules/staff/api/portal/support-tickets/[id]/attachments/route.js.map +7 -0
  34. package/dist/modules/staff/api/portal/support-tickets/[id]/messages/route.js +91 -0
  35. package/dist/modules/staff/api/portal/support-tickets/[id]/messages/route.js.map +7 -0
  36. package/dist/modules/staff/api/portal/support-tickets/[id]/route.js +146 -0
  37. package/dist/modules/staff/api/portal/support-tickets/[id]/route.js.map +7 -0
  38. package/dist/modules/staff/api/portal/support-tickets/options/route.js +60 -0
  39. package/dist/modules/staff/api/portal/support-tickets/options/route.js.map +7 -0
  40. package/dist/modules/staff/api/portal/support-tickets/route.js +220 -0
  41. package/dist/modules/staff/api/portal/support-tickets/route.js.map +7 -0
  42. package/dist/modules/staff/api/timesheets/settings/route.js +7 -1
  43. package/dist/modules/staff/api/timesheets/settings/route.js.map +2 -2
  44. package/dist/modules/staff/api/timesheets/support-routing-rules/fallback-project/route.js +122 -0
  45. package/dist/modules/staff/api/timesheets/support-routing-rules/fallback-project/route.js.map +7 -0
  46. package/dist/modules/staff/api/timesheets/support-routing-rules/preview/route.js +180 -0
  47. package/dist/modules/staff/api/timesheets/support-routing-rules/preview/route.js.map +7 -0
  48. package/dist/modules/staff/api/timesheets/support-routing-rules/route.js +147 -0
  49. package/dist/modules/staff/api/timesheets/support-routing-rules/route.js.map +7 -0
  50. package/dist/modules/staff/api/timesheets/support-tickets/options/route.js +133 -0
  51. package/dist/modules/staff/api/timesheets/support-tickets/options/route.js.map +7 -0
  52. package/dist/modules/staff/api/timesheets/tasks/[id]/claim/route.js +33 -0
  53. package/dist/modules/staff/api/timesheets/tasks/[id]/claim/route.js.map +7 -0
  54. package/dist/modules/staff/api/timesheets/tasks/[id]/comments/route.js +25 -7
  55. package/dist/modules/staff/api/timesheets/tasks/[id]/comments/route.js.map +2 -2
  56. package/dist/modules/staff/api/timesheets/tasks/[id]/release/route.js +32 -0
  57. package/dist/modules/staff/api/timesheets/tasks/[id]/release/route.js.map +7 -0
  58. package/dist/modules/staff/api/timesheets/tasks/route.js +94 -47
  59. package/dist/modules/staff/api/timesheets/tasks/route.js.map +2 -2
  60. package/dist/modules/staff/api/timesheets/time-projects/[id]/employees/route.js +3 -0
  61. package/dist/modules/staff/api/timesheets/time-projects/[id]/employees/route.js.map +2 -2
  62. package/dist/modules/staff/api/timesheets/time-projects/route.js +3 -0
  63. package/dist/modules/staff/api/timesheets/time-projects/route.js.map +2 -2
  64. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.js +1 -0
  65. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.js.map +2 -2
  66. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js +54 -4
  67. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js.map +2 -2
  68. package/dist/modules/staff/backend/staff/time-tracking/projects/projectFormConfig.js +21 -5
  69. package/dist/modules/staff/backend/staff/time-tracking/projects/projectFormConfig.js.map +2 -2
  70. package/dist/modules/staff/backend/staff/time-tracking/settings/page.js +319 -254
  71. package/dist/modules/staff/backend/staff/time-tracking/settings/page.js.map +2 -2
  72. package/dist/modules/staff/backend/staff/time-tracking/support/page.js +363 -0
  73. package/dist/modules/staff/backend/staff/time-tracking/support/page.js.map +7 -0
  74. package/dist/modules/staff/backend/staff/time-tracking/support/page.meta.js +18 -0
  75. package/dist/modules/staff/backend/staff/time-tracking/support/page.meta.js.map +7 -0
  76. package/dist/modules/staff/cli.js +27 -0
  77. package/dist/modules/staff/cli.js.map +2 -2
  78. package/dist/modules/staff/commands/index.js +3 -0
  79. package/dist/modules/staff/commands/index.js.map +2 -2
  80. package/dist/modules/staff/commands/shared.js +10 -1
  81. package/dist/modules/staff/commands/shared.js.map +2 -2
  82. package/dist/modules/staff/commands/support-routing-rules.js +505 -0
  83. package/dist/modules/staff/commands/support-routing-rules.js.map +7 -0
  84. package/dist/modules/staff/commands/support-tickets.js +513 -0
  85. package/dist/modules/staff/commands/support-tickets.js.map +7 -0
  86. package/dist/modules/staff/commands/timesheets-projects.js +72 -14
  87. package/dist/modules/staff/commands/timesheets-projects.js.map +2 -2
  88. package/dist/modules/staff/commands/timesheets-task-assignment.js +270 -0
  89. package/dist/modules/staff/commands/timesheets-task-assignment.js.map +7 -0
  90. package/dist/modules/staff/commands/timesheets-task-comments.js +58 -12
  91. package/dist/modules/staff/commands/timesheets-task-comments.js.map +2 -2
  92. package/dist/modules/staff/commands/timesheets-tasks.js +91 -9
  93. package/dist/modules/staff/commands/timesheets-tasks.js.map +3 -3
  94. package/dist/modules/staff/data/enrichers.js +141 -0
  95. package/dist/modules/staff/data/enrichers.js.map +2 -2
  96. package/dist/modules/staff/data/entities.js +143 -2
  97. package/dist/modules/staff/data/entities.js.map +2 -2
  98. package/dist/modules/staff/data/extensions.js +8 -0
  99. package/dist/modules/staff/data/extensions.js.map +2 -2
  100. package/dist/modules/staff/data/validators.js +34 -5
  101. package/dist/modules/staff/data/validators.js.map +2 -2
  102. package/dist/modules/staff/encryption.js +6 -0
  103. package/dist/modules/staff/encryption.js.map +2 -2
  104. package/dist/modules/staff/events.js +25 -0
  105. package/dist/modules/staff/events.js.map +2 -2
  106. package/dist/modules/staff/events.payloads.js +54 -0
  107. package/dist/modules/staff/events.payloads.js.map +2 -2
  108. package/dist/modules/staff/extension-points.js +1 -0
  109. package/dist/modules/staff/extension-points.js.map +2 -2
  110. package/dist/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.js +468 -0
  111. package/dist/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.js.map +7 -0
  112. package/dist/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.meta.js +12 -0
  113. package/dist/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.meta.js.map +7 -0
  114. package/dist/modules/staff/frontend/[orgSlug]/portal/support/new/page.js +316 -0
  115. package/dist/modules/staff/frontend/[orgSlug]/portal/support/new/page.js.map +7 -0
  116. package/dist/modules/staff/frontend/[orgSlug]/portal/support/new/page.meta.js +12 -0
  117. package/dist/modules/staff/frontend/[orgSlug]/portal/support/new/page.meta.js.map +7 -0
  118. package/dist/modules/staff/frontend/[orgSlug]/portal/support/page.js +190 -0
  119. package/dist/modules/staff/frontend/[orgSlug]/portal/support/page.js.map +7 -0
  120. package/dist/modules/staff/frontend/[orgSlug]/portal/support/page.meta.js +19 -0
  121. package/dist/modules/staff/frontend/[orgSlug]/portal/support/page.meta.js.map +7 -0
  122. package/dist/modules/staff/index.js +1 -1
  123. package/dist/modules/staff/index.js.map +2 -2
  124. package/dist/modules/staff/lib/crud.js +2 -0
  125. package/dist/modules/staff/lib/crud.js.map +2 -2
  126. package/dist/modules/staff/lib/support/attachments.js +36 -0
  127. package/dist/modules/staff/lib/support/attachments.js.map +7 -0
  128. package/dist/modules/staff/lib/support/dictionaries.js +87 -0
  129. package/dist/modules/staff/lib/support/dictionaries.js.map +7 -0
  130. package/dist/modules/staff/lib/support/notifications.js +111 -0
  131. package/dist/modules/staff/lib/support/notifications.js.map +7 -0
  132. package/dist/modules/staff/lib/support/portalAttachments.js +100 -0
  133. package/dist/modules/staff/lib/support/portalAttachments.js.map +7 -0
  134. package/dist/modules/staff/lib/support/portalBroadcast.js +117 -0
  135. package/dist/modules/staff/lib/support/portalBroadcast.js.map +7 -0
  136. package/dist/modules/staff/lib/support/portalRoute.js +99 -0
  137. package/dist/modules/staff/lib/support/portalRoute.js.map +7 -0
  138. package/dist/modules/staff/lib/support/portalStatus.js +12 -0
  139. package/dist/modules/staff/lib/support/portalStatus.js.map +7 -0
  140. package/dist/modules/staff/lib/support/portalTickets.js +148 -0
  141. package/dist/modules/staff/lib/support/portalTickets.js.map +7 -0
  142. package/dist/modules/staff/lib/support/routing.js +81 -0
  143. package/dist/modules/staff/lib/support/routing.js.map +7 -0
  144. package/dist/modules/staff/lib/support/routingRuleDraft.js +25 -0
  145. package/dist/modules/staff/lib/support/routingRuleDraft.js.map +7 -0
  146. package/dist/modules/staff/lib/support/seedDefaults.js +114 -0
  147. package/dist/modules/staff/lib/support/seedDefaults.js.map +7 -0
  148. package/dist/modules/staff/lib/support/supportActivity.js +26 -0
  149. package/dist/modules/staff/lib/support/supportActivity.js.map +7 -0
  150. package/dist/modules/staff/lib/support-ui/PortalSupportNotLinked.js +23 -0
  151. package/dist/modules/staff/lib/support-ui/PortalSupportNotLinked.js.map +7 -0
  152. package/dist/modules/staff/lib/support-ui/portalAttachmentValidation.js +112 -0
  153. package/dist/modules/staff/lib/support-ui/portalAttachmentValidation.js.map +7 -0
  154. package/dist/modules/staff/lib/support-ui/portalPendingUploads.js +45 -0
  155. package/dist/modules/staff/lib/support-ui/portalPendingUploads.js.map +7 -0
  156. package/dist/modules/staff/lib/support-ui/portalSupport.js +51 -0
  157. package/dist/modules/staff/lib/support-ui/portalSupport.js.map +7 -0
  158. package/dist/modules/staff/lib/time-tracking/access.js +3 -39
  159. package/dist/modules/staff/lib/time-tracking/access.js.map +2 -2
  160. package/dist/modules/staff/lib/time-tracking/listProjectAccess.js +54 -0
  161. package/dist/modules/staff/lib/time-tracking/listProjectAccess.js.map +7 -0
  162. package/dist/modules/staff/lib/time-tracking/portalRecipients.js +92 -2
  163. package/dist/modules/staff/lib/time-tracking/portalRecipients.js.map +2 -2
  164. package/dist/modules/staff/lib/time-tracking/projectAssignment.js +112 -0
  165. package/dist/modules/staff/lib/time-tracking/projectAssignment.js.map +7 -0
  166. package/dist/modules/staff/lib/time-tracking/settingKeys.js +14 -0
  167. package/dist/modules/staff/lib/time-tracking/settingKeys.js.map +2 -2
  168. package/dist/modules/staff/lib/time-tracking/settings.js +28 -1
  169. package/dist/modules/staff/lib/time-tracking/settings.js.map +2 -2
  170. package/dist/modules/staff/lib/time-tracking/taskAssignmentRoute.js +143 -0
  171. package/dist/modules/staff/lib/time-tracking/taskAssignmentRoute.js.map +7 -0
  172. package/dist/modules/staff/lib/time-tracking/workRecipients.js +92 -0
  173. package/dist/modules/staff/lib/time-tracking/workRecipients.js.map +7 -0
  174. package/dist/modules/staff/lib/time-tracking-ui/BoardFilterChips.js +32 -3
  175. package/dist/modules/staff/lib/time-tracking-ui/BoardFilterChips.js.map +2 -2
  176. package/dist/modules/staff/lib/time-tracking-ui/KanbanBoard.js +29 -0
  177. package/dist/modules/staff/lib/time-tracking-ui/KanbanBoard.js.map +2 -2
  178. package/dist/modules/staff/lib/time-tracking-ui/KanbanCard.js +4 -0
  179. package/dist/modules/staff/lib/time-tracking-ui/KanbanCard.js.map +2 -2
  180. package/dist/modules/staff/lib/time-tracking-ui/ProjectFormSections.js +141 -1
  181. package/dist/modules/staff/lib/time-tracking-ui/ProjectFormSections.js.map +2 -2
  182. package/dist/modules/staff/lib/time-tracking-ui/SupportAttachmentsSection.js +153 -0
  183. package/dist/modules/staff/lib/time-tracking-ui/SupportAttachmentsSection.js.map +7 -0
  184. package/dist/modules/staff/lib/time-tracking-ui/SupportRequestPanel.js +67 -0
  185. package/dist/modules/staff/lib/time-tracking-ui/SupportRequestPanel.js.map +7 -0
  186. package/dist/modules/staff/lib/time-tracking-ui/TaskBoardScreen.js +3 -1
  187. package/dist/modules/staff/lib/time-tracking-ui/TaskBoardScreen.js.map +2 -2
  188. package/dist/modules/staff/lib/time-tracking-ui/TaskCommentThread.js +80 -17
  189. package/dist/modules/staff/lib/time-tracking-ui/TaskCommentThread.js.map +2 -2
  190. package/dist/modules/staff/lib/time-tracking-ui/TaskDrawer.js +230 -41
  191. package/dist/modules/staff/lib/time-tracking-ui/TaskDrawer.js.map +2 -2
  192. package/dist/modules/staff/lib/time-tracking-ui/boardDirectory.js +21 -1
  193. package/dist/modules/staff/lib/time-tracking-ui/boardDirectory.js.map +2 -2
  194. package/dist/modules/staff/lib/time-tracking-ui/boardFilters.js +16 -4
  195. package/dist/modules/staff/lib/time-tracking-ui/boardFilters.js.map +2 -2
  196. package/dist/modules/staff/lib/time-tracking-ui/kanbanBoardData.js +3 -1
  197. package/dist/modules/staff/lib/time-tracking-ui/kanbanBoardData.js.map +2 -2
  198. package/dist/modules/staff/lib/time-tracking-ui/projectDefaultAssignee.js +68 -0
  199. package/dist/modules/staff/lib/time-tracking-ui/projectDefaultAssignee.js.map +7 -0
  200. package/dist/modules/staff/lib/time-tracking-ui/settingsTabs.js +7 -0
  201. package/dist/modules/staff/lib/time-tracking-ui/settingsTabs.js.map +7 -0
  202. package/dist/modules/staff/lib/time-tracking-ui/supportTicketData.js +50 -0
  203. package/dist/modules/staff/lib/time-tracking-ui/supportTicketData.js.map +7 -0
  204. package/dist/modules/staff/lib/time-tracking-ui/taskDrawerData.js +13 -1
  205. package/dist/modules/staff/lib/time-tracking-ui/taskDrawerData.js.map +2 -2
  206. package/dist/modules/staff/lib/time-tracking-ui/timeTrackingSettingsForm.js +36 -2
  207. package/dist/modules/staff/lib/time-tracking-ui/timeTrackingSettingsForm.js.map +2 -2
  208. package/dist/modules/staff/migrations/Migration20261009180543_staff.js +42 -0
  209. package/dist/modules/staff/migrations/Migration20261009180543_staff.js.map +7 -0
  210. package/dist/modules/staff/notifications.js +92 -0
  211. package/dist/modules/staff/notifications.js.map +2 -2
  212. package/dist/modules/staff/setup.js +6 -2
  213. package/dist/modules/staff/setup.js.map +2 -2
  214. package/dist/modules/staff/subscribers/support-portal-comment-created.js +26 -0
  215. package/dist/modules/staff/subscribers/support-portal-comment-created.js.map +7 -0
  216. package/dist/modules/staff/subscribers/support-portal-comment-deleted.js +26 -0
  217. package/dist/modules/staff/subscribers/support-portal-comment-deleted.js.map +7 -0
  218. package/dist/modules/staff/subscribers/support-portal-comment-updated.js +26 -0
  219. package/dist/modules/staff/subscribers/support-portal-comment-updated.js.map +7 -0
  220. package/dist/modules/staff/subscribers/support-portal-status-column-updated.js +24 -0
  221. package/dist/modules/staff/subscribers/support-portal-status-column-updated.js.map +7 -0
  222. package/dist/modules/staff/subscribers/support-portal-task-status-changed.js +25 -0
  223. package/dist/modules/staff/subscribers/support-portal-task-status-changed.js.map +7 -0
  224. package/dist/modules/staff/subscribers/support-portal-task-updated.js +25 -0
  225. package/dist/modules/staff/subscribers/support-portal-task-updated.js.map +7 -0
  226. package/dist/modules/staff/subscribers/support-ticket-created-notification.js +25 -0
  227. package/dist/modules/staff/subscribers/support-ticket-created-notification.js.map +7 -0
  228. package/dist/modules/staff/subscribers/support-ticket-customer-replied-notification.js +25 -0
  229. package/dist/modules/staff/subscribers/support-ticket-customer-replied-notification.js.map +7 -0
  230. package/dist/modules/staff/subscribers/support-ticket-unnotified-clear.js +26 -0
  231. package/dist/modules/staff/subscribers/support-ticket-unnotified-clear.js.map +7 -0
  232. package/dist/modules/staff/subscribers/time-task-assignee-notification.js +83 -0
  233. package/dist/modules/staff/subscribers/time-task-assignee-notification.js.map +7 -0
  234. package/dist/modules/staff/widgets/injection/portal-support-tickets/widget.client.js +112 -0
  235. package/dist/modules/staff/widgets/injection/portal-support-tickets/widget.client.js.map +7 -0
  236. package/dist/modules/staff/widgets/injection/portal-support-tickets/widget.js +17 -0
  237. package/dist/modules/staff/widgets/injection/portal-support-tickets/widget.js.map +7 -0
  238. package/dist/modules/staff/widgets/injection/time-tracking-settings-support/widget.client.js +870 -0
  239. package/dist/modules/staff/widgets/injection/time-tracking-settings-support/widget.client.js.map +7 -0
  240. package/dist/modules/staff/widgets/injection/time-tracking-settings-support/widget.js +17 -0
  241. package/dist/modules/staff/widgets/injection/time-tracking-settings-support/widget.js.map +7 -0
  242. package/dist/modules/staff/widgets/injection-table.js +16 -1
  243. package/dist/modules/staff/widgets/injection-table.js.map +2 -2
  244. package/dist/modules/wms/components/backend/WmsLocationDetailPage.js +4 -18
  245. package/dist/modules/wms/components/backend/WmsLocationDetailPage.js.map +2 -2
  246. package/dist/modules/wms/components/backend/WmsLotDetailPage.js +4 -18
  247. package/dist/modules/wms/components/backend/WmsLotDetailPage.js.map +2 -2
  248. package/dist/modules/wms/components/backend/WmsSkuDetailPage.js +4 -18
  249. package/dist/modules/wms/components/backend/WmsSkuDetailPage.js.map +2 -2
  250. package/dist/modules/wms/lib/inventoryCsvExport.js +19 -0
  251. package/dist/modules/wms/lib/inventoryCsvExport.js.map +7 -0
  252. package/generated/entities/staff_support_routing_rule/index.ts +9 -0
  253. package/generated/entities/staff_support_ticket/index.ts +17 -0
  254. package/generated/entities/staff_time_project/index.ts +1 -0
  255. package/generated/entities/staff_time_project_member/index.ts +1 -0
  256. package/generated/entities/staff_time_task/index.ts +2 -0
  257. package/generated/entities/staff_time_task_comment/index.ts +4 -0
  258. package/generated/entities.ids.generated.ts +2 -0
  259. package/generated/entity-fields-registry.ts +38 -0
  260. package/package.json +8 -8
  261. package/src/helpers/integration/auth.ts +1 -1
  262. package/src/helpers/integration/communicationChannelsFixtures.ts +60 -2
  263. package/src/modules/catalog/api/bulk-delete/route.ts +12 -2
  264. package/src/modules/communication_channels/api/post/test-seed/route.ts +29 -12
  265. package/src/modules/communication_channels/lib/test-seed.ts +49 -10
  266. package/src/modules/messages/commands/messages.ts +8 -10
  267. package/src/modules/portal/frontend/[orgSlug]/portal/dashboard/page.tsx +2 -2
  268. package/src/modules/staff/AGENTS.md +22 -6
  269. package/src/modules/staff/api/portal/support-tickets/[id]/attachments/route.ts +327 -0
  270. package/src/modules/staff/api/portal/support-tickets/[id]/messages/route.ts +94 -0
  271. package/src/modules/staff/api/portal/support-tickets/[id]/route.ts +160 -0
  272. package/src/modules/staff/api/portal/support-tickets/options/route.ts +60 -0
  273. package/src/modules/staff/api/portal/support-tickets/route.ts +253 -0
  274. package/src/modules/staff/api/timesheets/settings/route.ts +7 -1
  275. package/src/modules/staff/api/timesheets/support-routing-rules/fallback-project/route.ts +131 -0
  276. package/src/modules/staff/api/timesheets/support-routing-rules/preview/route.ts +248 -0
  277. package/src/modules/staff/api/timesheets/support-routing-rules/route.ts +150 -0
  278. package/src/modules/staff/api/timesheets/support-tickets/options/route.ts +181 -0
  279. package/src/modules/staff/api/timesheets/tasks/[id]/claim/route.ts +31 -0
  280. package/src/modules/staff/api/timesheets/tasks/[id]/comments/route.ts +25 -7
  281. package/src/modules/staff/api/timesheets/tasks/[id]/release/route.ts +30 -0
  282. package/src/modules/staff/api/timesheets/tasks/route.ts +119 -67
  283. package/src/modules/staff/api/timesheets/time-projects/[id]/employees/route.ts +3 -0
  284. package/src/modules/staff/api/timesheets/time-projects/route.ts +3 -0
  285. package/src/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.tsx +1 -0
  286. package/src/modules/staff/backend/staff/time-tracking/projects/[id]/page.tsx +67 -2
  287. package/src/modules/staff/backend/staff/time-tracking/projects/projectFormConfig.ts +22 -3
  288. package/src/modules/staff/backend/staff/time-tracking/settings/page.tsx +405 -308
  289. package/src/modules/staff/backend/staff/time-tracking/support/page.meta.ts +14 -0
  290. package/src/modules/staff/backend/staff/time-tracking/support/page.tsx +428 -0
  291. package/src/modules/staff/cli.ts +28 -0
  292. package/src/modules/staff/commands/index.ts +3 -0
  293. package/src/modules/staff/commands/shared.ts +22 -1
  294. package/src/modules/staff/commands/support-routing-rules.ts +595 -0
  295. package/src/modules/staff/commands/support-tickets.ts +580 -0
  296. package/src/modules/staff/commands/timesheets-projects.ts +84 -13
  297. package/src/modules/staff/commands/timesheets-task-assignment.ts +319 -0
  298. package/src/modules/staff/commands/timesheets-task-comments.ts +70 -15
  299. package/src/modules/staff/commands/timesheets-tasks.ts +106 -15
  300. package/src/modules/staff/data/enrichers.ts +203 -0
  301. package/src/modules/staff/data/entities.ts +133 -2
  302. package/src/modules/staff/data/extensions.ts +9 -1
  303. package/src/modules/staff/data/validators.ts +36 -0
  304. package/src/modules/staff/encryption.ts +6 -0
  305. package/src/modules/staff/events.payloads.ts +87 -1
  306. package/src/modules/staff/events.ts +26 -0
  307. package/src/modules/staff/extension-points.ts +1 -0
  308. package/src/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.meta.ts +10 -0
  309. package/src/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.tsx +553 -0
  310. package/src/modules/staff/frontend/[orgSlug]/portal/support/new/page.meta.ts +10 -0
  311. package/src/modules/staff/frontend/[orgSlug]/portal/support/new/page.tsx +346 -0
  312. package/src/modules/staff/frontend/[orgSlug]/portal/support/page.meta.ts +17 -0
  313. package/src/modules/staff/frontend/[orgSlug]/portal/support/page.tsx +253 -0
  314. package/src/modules/staff/i18n/de.json +291 -0
  315. package/src/modules/staff/i18n/en.json +291 -0
  316. package/src/modules/staff/i18n/es.json +291 -0
  317. package/src/modules/staff/i18n/ko.json +291 -0
  318. package/src/modules/staff/i18n/pl.json +291 -0
  319. package/src/modules/staff/index.ts +1 -1
  320. package/src/modules/staff/lib/crud.ts +2 -1
  321. package/src/modules/staff/lib/support/attachments.ts +43 -0
  322. package/src/modules/staff/lib/support/dictionaries.ts +120 -0
  323. package/src/modules/staff/lib/support/notifications.ts +138 -0
  324. package/src/modules/staff/lib/support/portalAttachments.ts +140 -0
  325. package/src/modules/staff/lib/support/portalBroadcast.ts +161 -0
  326. package/src/modules/staff/lib/support/portalRoute.ts +108 -0
  327. package/src/modules/staff/lib/support/portalStatus.ts +15 -0
  328. package/src/modules/staff/lib/support/portalTickets.ts +233 -0
  329. package/src/modules/staff/lib/support/routing.ts +117 -0
  330. package/src/modules/staff/lib/support/routingRuleDraft.ts +45 -0
  331. package/src/modules/staff/lib/support/seedDefaults.ts +151 -0
  332. package/src/modules/staff/lib/support/supportActivity.ts +39 -0
  333. package/src/modules/staff/lib/support-ui/PortalSupportNotLinked.tsx +20 -0
  334. package/src/modules/staff/lib/support-ui/portalAttachmentValidation.ts +125 -0
  335. package/src/modules/staff/lib/support-ui/portalPendingUploads.ts +49 -0
  336. package/src/modules/staff/lib/support-ui/portalSupport.ts +116 -0
  337. package/src/modules/staff/lib/time-tracking/access.ts +2 -62
  338. package/src/modules/staff/lib/time-tracking/listProjectAccess.ts +78 -0
  339. package/src/modules/staff/lib/time-tracking/portalRecipients.ts +173 -5
  340. package/src/modules/staff/lib/time-tracking/projectAssignment.ts +172 -0
  341. package/src/modules/staff/lib/time-tracking/settingKeys.ts +11 -0
  342. package/src/modules/staff/lib/time-tracking/settings.ts +46 -1
  343. package/src/modules/staff/lib/time-tracking/taskAssignmentRoute.ts +165 -0
  344. package/src/modules/staff/lib/time-tracking/workRecipients.ts +183 -0
  345. package/src/modules/staff/lib/time-tracking-ui/BoardFilterChips.tsx +36 -1
  346. package/src/modules/staff/lib/time-tracking-ui/KanbanBoard.tsx +30 -0
  347. package/src/modules/staff/lib/time-tracking-ui/KanbanCard.tsx +9 -0
  348. package/src/modules/staff/lib/time-tracking-ui/ProjectFormSections.tsx +185 -6
  349. package/src/modules/staff/lib/time-tracking-ui/SupportAttachmentsSection.tsx +188 -0
  350. package/src/modules/staff/lib/time-tracking-ui/SupportRequestPanel.tsx +102 -0
  351. package/src/modules/staff/lib/time-tracking-ui/TaskBoardScreen.tsx +2 -0
  352. package/src/modules/staff/lib/time-tracking-ui/TaskCommentThread.tsx +105 -22
  353. package/src/modules/staff/lib/time-tracking-ui/TaskDrawer.tsx +278 -43
  354. package/src/modules/staff/lib/time-tracking-ui/boardDirectory.ts +21 -0
  355. package/src/modules/staff/lib/time-tracking-ui/boardFilters.ts +16 -2
  356. package/src/modules/staff/lib/time-tracking-ui/kanbanBoardData.ts +4 -0
  357. package/src/modules/staff/lib/time-tracking-ui/projectDefaultAssignee.ts +117 -0
  358. package/src/modules/staff/lib/time-tracking-ui/settingsTabs.ts +17 -0
  359. package/src/modules/staff/lib/time-tracking-ui/supportTicketData.ts +71 -0
  360. package/src/modules/staff/lib/time-tracking-ui/taskDrawerData.ts +15 -0
  361. package/src/modules/staff/lib/time-tracking-ui/timeTrackingSettingsForm.ts +48 -1
  362. package/src/modules/staff/migrations/.snapshot-open-mercato.json +714 -0
  363. package/src/modules/staff/migrations/Migration20261009180543_staff.ts +50 -0
  364. package/src/modules/staff/notifications.ts +92 -0
  365. package/src/modules/staff/setup.ts +6 -2
  366. package/src/modules/staff/subscribers/support-portal-comment-created.ts +35 -0
  367. package/src/modules/staff/subscribers/support-portal-comment-deleted.ts +35 -0
  368. package/src/modules/staff/subscribers/support-portal-comment-updated.ts +35 -0
  369. package/src/modules/staff/subscribers/support-portal-status-column-updated.ts +36 -0
  370. package/src/modules/staff/subscribers/support-portal-task-status-changed.ts +34 -0
  371. package/src/modules/staff/subscribers/support-portal-task-updated.ts +34 -0
  372. package/src/modules/staff/subscribers/support-ticket-created-notification.ts +27 -0
  373. package/src/modules/staff/subscribers/support-ticket-customer-replied-notification.ts +27 -0
  374. package/src/modules/staff/subscribers/support-ticket-unnotified-clear.ts +29 -0
  375. package/src/modules/staff/subscribers/time-task-assignee-notification.ts +102 -0
  376. package/src/modules/staff/widgets/injection/portal-support-tickets/widget.client.tsx +151 -0
  377. package/src/modules/staff/widgets/injection/portal-support-tickets/widget.ts +16 -0
  378. package/src/modules/staff/widgets/injection/time-tracking-settings-support/widget.client.tsx +1058 -0
  379. package/src/modules/staff/widgets/injection/time-tracking-settings-support/widget.ts +17 -0
  380. package/src/modules/staff/widgets/injection-table.ts +15 -0
  381. package/src/modules/wms/components/backend/WmsLocationDetailPage.tsx +4 -20
  382. package/src/modules/wms/components/backend/WmsLotDetailPage.tsx +4 -20
  383. package/src/modules/wms/components/backend/WmsSkuDetailPage.tsx +4 -19
  384. package/src/modules/wms/lib/inventoryCsvExport.ts +18 -0
@@ -0,0 +1,299 @@
1
+ import { NextResponse } from "next/server";
2
+ import { z } from "zod";
3
+ import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
4
+ import { runRouteMutationGuards } from "@open-mercato/shared/lib/crud/route-mutation-guard";
5
+ import { emitCrudSideEffects } from "@open-mercato/shared/lib/commands/helpers";
6
+ import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
7
+ import { createLogger } from "@open-mercato/shared/lib/logger";
8
+ import { getTelemetryRuntime } from "@open-mercato/shared/lib/telemetry/runtime";
9
+ import {
10
+ PORTAL_SUPPORT_TICKETS_MANAGE_FEATURE,
11
+ PORTAL_SUPPORT_TICKETS_VIEW_FEATURE,
12
+ loadPortalOwnedTicket,
13
+ resolveSupportPortalScope
14
+ } from "../../../../../lib/support/portalTickets.js";
15
+ import {
16
+ SUPPORT_TICKET_RESOURCE_KIND,
17
+ checkPortalRateLimit,
18
+ portalErrorResponse,
19
+ readPathId
20
+ } from "../../../../../lib/support/portalRoute.js";
21
+ import {
22
+ CUSTOMER_VISIBLE_ATTACHMENT_TAG,
23
+ SUPPORT_TICKET_ATTACHMENT_ENTITY_ID,
24
+ customerUploadTag,
25
+ translateSupportUploadError
26
+ } from "../../../../../lib/support/attachments.js";
27
+ import {
28
+ listCustomerVisibleAttachments,
29
+ loadPortalAttachmentsApi,
30
+ portalAttachmentAuth,
31
+ readAttachmentTags,
32
+ serializePortalAttachment,
33
+ portalSupportAttachmentItemSchema
34
+ } from "../../../../../lib/support/portalAttachments.js";
35
+ import { emitSupportAttachmentActivity } from "../../../../../lib/support/supportActivity.js";
36
+ const logger = createLogger("staff").child({ component: "api/portal/support-tickets/attachments" });
37
+ const metadata = {
38
+ GET: { requireAuth: false },
39
+ POST: { requireAuth: false },
40
+ DELETE: { requireAuth: false }
41
+ };
42
+ const uploadGuardPayloadSchema = z.object({
43
+ fileName: z.string().trim().min(1).max(255),
44
+ fileSize: z.number().int().nonnegative(),
45
+ mimeType: z.string().max(255)
46
+ });
47
+ function unavailable() {
48
+ return NextResponse.json({ ok: false, error: "staff.support.errors.attachmentsUnavailable" }, { status: 503 });
49
+ }
50
+ function notFound() {
51
+ return NextResponse.json({ ok: false, error: "staff.errors.notFound" }, { status: 404 });
52
+ }
53
+ async function resolveDriverFactory(api, scope) {
54
+ try {
55
+ const resolved = scope.container.resolve("storageDriverFactory");
56
+ return resolved ?? new api.StorageDriverFactory(scope.em);
57
+ } catch {
58
+ return new api.StorageDriverFactory(scope.em);
59
+ }
60
+ }
61
+ async function loadVisibleAttachment(api, scope, taskId, attachmentId) {
62
+ const attachment = await findOneWithDecryption(
63
+ scope.em,
64
+ api.Attachment,
65
+ {
66
+ id: attachmentId,
67
+ entityId: SUPPORT_TICKET_ATTACHMENT_ENTITY_ID,
68
+ recordId: taskId,
69
+ tenantId: scope.tenantId,
70
+ organizationId: scope.organizationId
71
+ },
72
+ {},
73
+ { tenantId: scope.tenantId, organizationId: scope.organizationId }
74
+ );
75
+ if (!attachment) return null;
76
+ return readAttachmentTags(api, attachment).includes(CUSTOMER_VISIBLE_ATTACHMENT_TAG) ? attachment : null;
77
+ }
78
+ async function GET(req) {
79
+ try {
80
+ const scope = await resolveSupportPortalScope(req, PORTAL_SUPPORT_TICKETS_VIEW_FEATURE);
81
+ if (scope instanceof Response) return scope;
82
+ const taskId = readPathId(req, /\/support-tickets\/([^/]+)\/attachments/);
83
+ const owned = taskId ? await loadPortalOwnedTicket(scope, taskId) : null;
84
+ if (!owned) return notFound();
85
+ const api = await loadPortalAttachmentsApi();
86
+ if (!api) return unavailable();
87
+ const attachmentId = new URL(req.url).searchParams.get("attachmentId");
88
+ if (attachmentId) {
89
+ if (!z.string().uuid().safeParse(attachmentId).success) return notFound();
90
+ const attachment = await loadVisibleAttachment(api, scope, owned.task.id, attachmentId);
91
+ if (!attachment) return notFound();
92
+ const partition = await scope.em.findOne(api.AttachmentPartition, { code: attachment.partitionCode });
93
+ if (!partition) return notFound();
94
+ if (!api.checkAttachmentAccess(portalAttachmentAuth(scope), attachment, partition, { requireAuthForPublic: true }).ok) return notFound();
95
+ const driver = await (await resolveDriverFactory(api, scope)).resolveForPartition(attachment.partitionCode, {
96
+ tenantId: attachment.tenantId ?? scope.tenantId,
97
+ organizationId: attachment.organizationId ?? scope.organizationId
98
+ });
99
+ let buffer;
100
+ try {
101
+ buffer = (await driver.read(attachment.partitionCode, attachment.storagePath)).buffer;
102
+ } catch {
103
+ return notFound();
104
+ }
105
+ const headers = {
106
+ "Cache-Control": "private, max-age=60",
107
+ "Content-Security-Policy": "default-src 'none'; sandbox",
108
+ "Content-Type": "application/octet-stream",
109
+ "Content-Disposition": `attachment; filename="${api.slugifyAttachmentFileName(attachment.fileName)}"`,
110
+ "X-Content-Type-Options": "nosniff"
111
+ };
112
+ if (attachment.fileSize > 0) headers["Content-Length"] = String(attachment.fileSize);
113
+ return new NextResponse(new Uint8Array(buffer), { status: 200, headers });
114
+ }
115
+ const canManage = await scope.hasFeature(PORTAL_SUPPORT_TICKETS_MANAGE_FEATURE);
116
+ return NextResponse.json({ items: await listCustomerVisibleAttachments(api, scope, owned.task.id, canManage) });
117
+ } catch (err) {
118
+ return portalErrorResponse(err, "staff portal support attachments GET failed");
119
+ }
120
+ }
121
+ async function POST(req) {
122
+ try {
123
+ const scope = await resolveSupportPortalScope(req, PORTAL_SUPPORT_TICKETS_MANAGE_FEATURE);
124
+ if (scope instanceof Response) return scope;
125
+ const taskId = readPathId(req, /\/support-tickets\/([^/]+)\/attachments/);
126
+ const owned = taskId ? await loadPortalOwnedTicket(scope, taskId) : null;
127
+ if (!owned) return notFound();
128
+ const limited = await checkPortalRateLimit(scope, "upload");
129
+ if (limited) return limited;
130
+ const api = await loadPortalAttachmentsApi();
131
+ if (!api) return unavailable();
132
+ const { t } = await resolveTranslations();
133
+ if (!(req.headers.get("content-type") || "").toLowerCase().includes("multipart/form-data")) {
134
+ return NextResponse.json({ ok: false, error: t("staff.errors.invalid_request", "Invalid request") }, { status: 400 });
135
+ }
136
+ if (!api.isMultipartRequestWithinUploadLimit(req.headers.get("content-length"))) {
137
+ return NextResponse.json({ ok: false, error: t("attachments.errors.maxUploadSize", "Attachment exceeds the maximum upload size.") }, { status: 413 });
138
+ }
139
+ const form = await req.formData();
140
+ const file = form.get("file");
141
+ if (!(file instanceof File)) {
142
+ return NextResponse.json({ ok: false, error: t("staff.support.errors.fileRequired", "Choose a file before uploading.") }, { status: 400 });
143
+ }
144
+ const uploadPayload = { fileName: file.name, fileSize: file.size, mimeType: file.type };
145
+ const guarded = await runRouteMutationGuards({
146
+ container: scope.container,
147
+ req,
148
+ auth: { userId: scope.auth.sub, tenantId: scope.tenantId, organizationId: scope.organizationId, userFeatures: [] },
149
+ input: {
150
+ resourceKind: SUPPORT_TICKET_RESOURCE_KIND,
151
+ resourceId: owned.task.id,
152
+ operation: "update",
153
+ mutationPayload: uploadPayload
154
+ }
155
+ });
156
+ if (!guarded.ok) return guarded.response;
157
+ const effectiveUpload = guarded.modifiedPayload ? uploadGuardPayloadSchema.parse({ ...uploadPayload, ...guarded.modifiedPayload, fileSize: file.size }) : uploadPayload;
158
+ let uploadService = null;
159
+ try {
160
+ uploadService = scope.container.resolve("attachmentScopedUploadService");
161
+ } catch {
162
+ uploadService = null;
163
+ }
164
+ if (!uploadService) return unavailable();
165
+ let attachment;
166
+ try {
167
+ attachment = await uploadService.upload({
168
+ tenantId: scope.tenantId,
169
+ organizationId: scope.organizationId,
170
+ entityId: SUPPORT_TICKET_ATTACHMENT_ENTITY_ID,
171
+ recordId: owned.task.id,
172
+ fileName: effectiveUpload.fileName,
173
+ buffer: Buffer.from(await file.arrayBuffer()),
174
+ declaredMimeType: effectiveUpload.mimeType,
175
+ tags: [CUSTOMER_VISIBLE_ATTACHMENT_TAG, customerUploadTag(scope.auth.sub)]
176
+ });
177
+ } catch (error) {
178
+ if (api.isScopedAttachmentUploadError(error)) {
179
+ return NextResponse.json(
180
+ { ok: false, error: translateSupportUploadError(error.code, t), code: error.code },
181
+ { status: error.status }
182
+ );
183
+ }
184
+ throw error;
185
+ }
186
+ await guarded.runAfterSuccess();
187
+ emitSupportAttachmentActivity(owned.task, owned.ticket, "attachment_added", attachment.id);
188
+ return NextResponse.json({ ok: true, item: serializePortalAttachment(api, attachment, owned.task.id, scope.auth.sub, true) }, { status: 201 });
189
+ } catch (err) {
190
+ return portalErrorResponse(err, "staff portal support attachments POST failed");
191
+ }
192
+ }
193
+ async function DELETE(req) {
194
+ try {
195
+ const scope = await resolveSupportPortalScope(req, PORTAL_SUPPORT_TICKETS_MANAGE_FEATURE);
196
+ if (scope instanceof Response) return scope;
197
+ const taskId = readPathId(req, /\/support-tickets\/([^/]+)\/attachments/);
198
+ const owned = taskId ? await loadPortalOwnedTicket(scope, taskId) : null;
199
+ if (!owned) return notFound();
200
+ const api = await loadPortalAttachmentsApi();
201
+ if (!api) return unavailable();
202
+ const attachmentId = new URL(req.url).searchParams.get("attachmentId");
203
+ if (!attachmentId || !z.string().uuid().safeParse(attachmentId).success) return notFound();
204
+ const attachment = await loadVisibleAttachment(api, scope, owned.task.id, attachmentId);
205
+ if (!attachment) return notFound();
206
+ if (!readAttachmentTags(api, attachment).includes(customerUploadTag(scope.auth.sub))) {
207
+ return NextResponse.json({ ok: false, error: "staff.support.errors.attachmentNotYours" }, { status: 403 });
208
+ }
209
+ const guarded = await runRouteMutationGuards({
210
+ container: scope.container,
211
+ req,
212
+ auth: { userId: scope.auth.sub, tenantId: scope.tenantId, organizationId: scope.organizationId, userFeatures: [] },
213
+ input: { resourceKind: SUPPORT_TICKET_RESOURCE_KIND, resourceId: owned.task.id, operation: "delete", mutationPayload: { attachmentId } }
214
+ });
215
+ if (!guarded.ok) return guarded.response;
216
+ await scope.em.remove(attachment).flush();
217
+ await api.clearAttachmentThumbnailCache(attachment.partitionCode, attachment.id).catch((err) => {
218
+ logger.error("staff portal support attachment thumbnail cleanup failed", { err, attachmentId });
219
+ getTelemetryRuntime()?.reportError(err, { module: "staff", code: "staff.portal_attachment_thumbnail_cleanup_failed" });
220
+ });
221
+ if (attachment.storagePath) {
222
+ try {
223
+ const driver = await (await resolveDriverFactory(api, scope)).resolveForPartition(attachment.partitionCode, {
224
+ tenantId: attachment.tenantId ?? scope.tenantId,
225
+ organizationId: attachment.organizationId ?? scope.organizationId
226
+ });
227
+ await driver.delete(attachment.partitionCode, attachment.storagePath);
228
+ } catch (err) {
229
+ logger.error("staff portal support attachment storage cleanup failed", { err, attachmentId });
230
+ getTelemetryRuntime()?.reportError(err, { module: "staff", code: "staff.portal_attachment_storage_cleanup_failed" });
231
+ }
232
+ }
233
+ try {
234
+ const dataEngine = scope.container.resolve("dataEngine");
235
+ await emitCrudSideEffects({
236
+ dataEngine,
237
+ action: "deleted",
238
+ entity: attachment,
239
+ identifiers: { id: attachment.id, organizationId: attachment.organizationId ?? null, tenantId: attachment.tenantId ?? null },
240
+ events: api.attachmentCrudEvents,
241
+ indexer: api.attachmentCrudIndexer
242
+ });
243
+ await dataEngine.flushOrmEntityChanges();
244
+ } catch (err) {
245
+ logger.error("staff portal support attachment side effects failed", { err, attachmentId });
246
+ getTelemetryRuntime()?.reportError(err, { module: "staff", code: "staff.portal_attachment_side_effects_failed" });
247
+ }
248
+ await guarded.runAfterSuccess();
249
+ emitSupportAttachmentActivity(owned.task, owned.ticket, "attachment_removed", attachmentId);
250
+ return NextResponse.json({ ok: true });
251
+ } catch (err) {
252
+ return portalErrorResponse(err, "staff portal support attachments DELETE failed");
253
+ }
254
+ }
255
+ const attachmentItemSchema = portalSupportAttachmentItemSchema;
256
+ const openApi = {
257
+ tag: "Staff Portal",
258
+ summary: "Support ticket attachments",
259
+ methods: {
260
+ GET: {
261
+ summary: "List or download support ticket files",
262
+ description: "Lists customer-visible files of a ticket the caller owns, or streams one when `attachmentId` is given. Staff files that were not shared with the customer are never listed.",
263
+ responses: [{ status: 200, description: "Files", schema: z.object({ items: z.array(attachmentItemSchema) }) }],
264
+ errors: [
265
+ { status: 401, description: "No portal session" },
266
+ { status: 404, description: "Ticket or file not found" },
267
+ { status: 503, description: "Attachments module unavailable" }
268
+ ]
269
+ },
270
+ POST: {
271
+ summary: "Upload a file to a support ticket",
272
+ description: "Multipart upload (`file`). The file is tagged customer-visible and as uploaded by the caller. Rate limited per customer user; rejected files answer with a translated `error` and the machine `code`.",
273
+ responses: [{ status: 201, description: "Uploaded", schema: z.object({ ok: z.literal(true), item: attachmentItemSchema }) }],
274
+ errors: [
275
+ { status: 403, description: "Missing portal.support_tickets.manage" },
276
+ { status: 404, description: "Ticket not found" },
277
+ { status: 413, description: "File too large" },
278
+ { status: 429, description: "Upload rate limit exceeded (per customer user)" }
279
+ ]
280
+ },
281
+ DELETE: {
282
+ summary: "Delete a file you uploaded",
283
+ description: "Deletes a file the caller uploaded (`attachmentId` query). Files uploaded by staff or by other customer users answer 403.",
284
+ responses: [{ status: 200, description: "Deleted", schema: z.object({ ok: z.literal(true) }) }],
285
+ errors: [
286
+ { status: 403, description: "The file was not uploaded by the caller" },
287
+ { status: 404, description: "Ticket or file not found" }
288
+ ]
289
+ }
290
+ }
291
+ };
292
+ export {
293
+ DELETE,
294
+ GET,
295
+ POST,
296
+ metadata,
297
+ openApi
298
+ };
299
+ //# sourceMappingURL=route.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../../../../src/modules/staff/api/portal/support-tickets/%5Bid%5D/attachments/route.ts"],
4
+ "sourcesContent": ["/**\n * `GET|POST|DELETE /api/staff/portal/support-tickets/{id}/attachments`\n *\n * Modeled on `warranty_claims/api/portal/attachments/route.ts`, with two\n * differences: the attachments module is loaded with a dynamic import (staff keeps\n * no static edge to it, and answers 503 when it is absent), and a customer may only\n * delete files they uploaded themselves \u2014 every customer upload is tagged\n * `customer-upload:<customerUserId>` because the attachment row has no uploader\n * column. Only files tagged `customer-visible` are ever listed or streamed.\n */\nimport { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { DataEngine } from '@open-mercato/shared/lib/data/engine'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { runRouteMutationGuards } from '@open-mercato/shared/lib/crud/route-mutation-guard'\nimport { emitCrudSideEffects } from '@open-mercato/shared/lib/commands/helpers'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport type { Attachment as AttachmentEntity } from '@open-mercato/core/modules/attachments/data/entities'\nimport type { ScopedAttachmentUploadService } from '@open-mercato/core/modules/attachments/lib/scoped-upload-service'\nimport {\n PORTAL_SUPPORT_TICKETS_MANAGE_FEATURE,\n PORTAL_SUPPORT_TICKETS_VIEW_FEATURE,\n loadPortalOwnedTicket,\n resolveSupportPortalScope,\n type SupportPortalScope,\n} from '../../../../../lib/support/portalTickets'\nimport {\n SUPPORT_TICKET_RESOURCE_KIND,\n checkPortalRateLimit,\n portalErrorResponse,\n readPathId,\n} from '../../../../../lib/support/portalRoute'\nimport {\n CUSTOMER_VISIBLE_ATTACHMENT_TAG,\n SUPPORT_TICKET_ATTACHMENT_ENTITY_ID,\n customerUploadTag,\n translateSupportUploadError,\n} from '../../../../../lib/support/attachments'\nimport {\n listCustomerVisibleAttachments,\n loadPortalAttachmentsApi,\n portalAttachmentAuth,\n readAttachmentTags,\n serializePortalAttachment,\n portalSupportAttachmentItemSchema,\n type PortalAttachmentsApi,\n} from '../../../../../lib/support/portalAttachments'\nimport { emitSupportAttachmentActivity } from '../../../../../lib/support/supportActivity'\n\nconst logger = createLogger('staff').child({ component: 'api/portal/support-tickets/attachments' })\n\nexport const metadata = {\n GET: { requireAuth: false },\n POST: { requireAuth: false },\n DELETE: { requireAuth: false },\n}\n\nconst uploadGuardPayloadSchema = z.object({\n fileName: z.string().trim().min(1).max(255),\n fileSize: z.number().int().nonnegative(),\n mimeType: z.string().max(255),\n})\n\nfunction unavailable(): Response {\n return NextResponse.json({ ok: false, error: 'staff.support.errors.attachmentsUnavailable' }, { status: 503 })\n}\n\nfunction notFound(): Response {\n return NextResponse.json({ ok: false, error: 'staff.errors.notFound' }, { status: 404 })\n}\n\nasync function resolveDriverFactory(api: PortalAttachmentsApi, scope: SupportPortalScope) {\n try {\n const resolved = scope.container.resolve('storageDriverFactory') as InstanceType<PortalAttachmentsApi['StorageDriverFactory']> | null\n return resolved ?? new api.StorageDriverFactory(scope.em)\n } catch {\n return new api.StorageDriverFactory(scope.em)\n }\n}\n\nasync function loadVisibleAttachment(\n api: PortalAttachmentsApi,\n scope: SupportPortalScope,\n taskId: string,\n attachmentId: string,\n): Promise<AttachmentEntity | null> {\n const attachment = await findOneWithDecryption(\n scope.em,\n api.Attachment,\n {\n id: attachmentId,\n entityId: SUPPORT_TICKET_ATTACHMENT_ENTITY_ID,\n recordId: taskId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n },\n {},\n { tenantId: scope.tenantId, organizationId: scope.organizationId },\n )\n if (!attachment) return null\n return readAttachmentTags(api, attachment).includes(CUSTOMER_VISIBLE_ATTACHMENT_TAG) ? attachment : null\n}\n\nexport async function GET(req: Request) {\n try {\n const scope = await resolveSupportPortalScope(req, PORTAL_SUPPORT_TICKETS_VIEW_FEATURE)\n if (scope instanceof Response) return scope\n const taskId = readPathId(req, /\\/support-tickets\\/([^/]+)\\/attachments/)\n const owned = taskId ? await loadPortalOwnedTicket(scope, taskId) : null\n if (!owned) return notFound()\n const api = await loadPortalAttachmentsApi()\n if (!api) return unavailable()\n const attachmentId = new URL(req.url).searchParams.get('attachmentId')\n if (attachmentId) {\n if (!z.string().uuid().safeParse(attachmentId).success) return notFound()\n const attachment = await loadVisibleAttachment(api, scope, owned.task.id, attachmentId)\n if (!attachment) return notFound()\n const partition = await scope.em.findOne(api.AttachmentPartition, { code: attachment.partitionCode })\n if (!partition) return notFound()\n if (!api.checkAttachmentAccess(portalAttachmentAuth(scope), attachment, partition, { requireAuthForPublic: true }).ok) return notFound()\n const driver = await (await resolveDriverFactory(api, scope)).resolveForPartition(attachment.partitionCode, {\n tenantId: attachment.tenantId ?? scope.tenantId,\n organizationId: attachment.organizationId ?? scope.organizationId,\n })\n let buffer: Buffer\n try {\n buffer = (await driver.read(attachment.partitionCode, attachment.storagePath)).buffer\n } catch {\n return notFound()\n }\n const headers: Record<string, string> = {\n 'Cache-Control': 'private, max-age=60',\n 'Content-Security-Policy': \"default-src 'none'; sandbox\",\n 'Content-Type': 'application/octet-stream',\n 'Content-Disposition': `attachment; filename=\"${api.slugifyAttachmentFileName(attachment.fileName)}\"`,\n 'X-Content-Type-Options': 'nosniff',\n }\n if (attachment.fileSize > 0) headers['Content-Length'] = String(attachment.fileSize)\n return new NextResponse(new Uint8Array(buffer), { status: 200, headers })\n }\n const canManage = await scope.hasFeature(PORTAL_SUPPORT_TICKETS_MANAGE_FEATURE)\n return NextResponse.json({ items: await listCustomerVisibleAttachments(api, scope, owned.task.id, canManage) })\n } catch (err) {\n return portalErrorResponse(err, 'staff portal support attachments GET failed')\n }\n}\n\nexport async function POST(req: Request) {\n try {\n const scope = await resolveSupportPortalScope(req, PORTAL_SUPPORT_TICKETS_MANAGE_FEATURE)\n if (scope instanceof Response) return scope\n const taskId = readPathId(req, /\\/support-tickets\\/([^/]+)\\/attachments/)\n const owned = taskId ? await loadPortalOwnedTicket(scope, taskId) : null\n if (!owned) return notFound()\n const limited = await checkPortalRateLimit(scope, 'upload')\n if (limited) return limited\n const api = await loadPortalAttachmentsApi()\n if (!api) return unavailable()\n const { t } = await resolveTranslations()\n if (!(req.headers.get('content-type') || '').toLowerCase().includes('multipart/form-data')) {\n return NextResponse.json({ ok: false, error: t('staff.errors.invalid_request', 'Invalid request') }, { status: 400 })\n }\n if (!api.isMultipartRequestWithinUploadLimit(req.headers.get('content-length'))) {\n return NextResponse.json({ ok: false, error: t('attachments.errors.maxUploadSize', 'Attachment exceeds the maximum upload size.') }, { status: 413 })\n }\n const form = await req.formData()\n const file = form.get('file')\n if (!(file instanceof File)) {\n return NextResponse.json({ ok: false, error: t('staff.support.errors.fileRequired', 'Choose a file before uploading.') }, { status: 400 })\n }\n const uploadPayload = { fileName: file.name, fileSize: file.size, mimeType: file.type }\n const guarded = await runRouteMutationGuards({\n container: scope.container,\n req,\n auth: { userId: scope.auth.sub, tenantId: scope.tenantId, organizationId: scope.organizationId, userFeatures: [] },\n input: {\n resourceKind: SUPPORT_TICKET_RESOURCE_KIND,\n resourceId: owned.task.id,\n operation: 'update',\n mutationPayload: uploadPayload,\n },\n })\n if (!guarded.ok) return guarded.response\n const effectiveUpload = guarded.modifiedPayload\n ? uploadGuardPayloadSchema.parse({ ...uploadPayload, ...guarded.modifiedPayload, fileSize: file.size })\n : uploadPayload\n let uploadService: ScopedAttachmentUploadService | null = null\n try {\n uploadService = scope.container.resolve('attachmentScopedUploadService') as ScopedAttachmentUploadService\n } catch {\n uploadService = null\n }\n if (!uploadService) return unavailable()\n let attachment: AttachmentEntity\n try {\n attachment = await uploadService.upload({\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n entityId: SUPPORT_TICKET_ATTACHMENT_ENTITY_ID,\n recordId: owned.task.id,\n fileName: effectiveUpload.fileName,\n buffer: Buffer.from(await file.arrayBuffer()),\n declaredMimeType: effectiveUpload.mimeType,\n tags: [CUSTOMER_VISIBLE_ATTACHMENT_TAG, customerUploadTag(scope.auth.sub)],\n })\n } catch (error) {\n if (api.isScopedAttachmentUploadError(error)) {\n return NextResponse.json(\n { ok: false, error: translateSupportUploadError(error.code, t), code: error.code },\n { status: error.status },\n )\n }\n throw error\n }\n await guarded.runAfterSuccess()\n emitSupportAttachmentActivity(owned.task, owned.ticket, 'attachment_added', attachment.id)\n return NextResponse.json({ ok: true, item: serializePortalAttachment(api, attachment, owned.task.id, scope.auth.sub, true) }, { status: 201 })\n } catch (err) {\n return portalErrorResponse(err, 'staff portal support attachments POST failed')\n }\n}\n\nexport async function DELETE(req: Request) {\n try {\n const scope = await resolveSupportPortalScope(req, PORTAL_SUPPORT_TICKETS_MANAGE_FEATURE)\n if (scope instanceof Response) return scope\n const taskId = readPathId(req, /\\/support-tickets\\/([^/]+)\\/attachments/)\n const owned = taskId ? await loadPortalOwnedTicket(scope, taskId) : null\n if (!owned) return notFound()\n const api = await loadPortalAttachmentsApi()\n if (!api) return unavailable()\n const attachmentId = new URL(req.url).searchParams.get('attachmentId')\n if (!attachmentId || !z.string().uuid().safeParse(attachmentId).success) return notFound()\n const attachment = await loadVisibleAttachment(api, scope, owned.task.id, attachmentId)\n if (!attachment) return notFound()\n if (!readAttachmentTags(api, attachment).includes(customerUploadTag(scope.auth.sub))) {\n return NextResponse.json({ ok: false, error: 'staff.support.errors.attachmentNotYours' }, { status: 403 })\n }\n const guarded = await runRouteMutationGuards({\n container: scope.container,\n req,\n auth: { userId: scope.auth.sub, tenantId: scope.tenantId, organizationId: scope.organizationId, userFeatures: [] },\n input: { resourceKind: SUPPORT_TICKET_RESOURCE_KIND, resourceId: owned.task.id, operation: 'delete', mutationPayload: { attachmentId } },\n })\n if (!guarded.ok) return guarded.response\n await scope.em.remove(attachment).flush()\n await api.clearAttachmentThumbnailCache(attachment.partitionCode, attachment.id).catch((err) => {\n logger.error('staff portal support attachment thumbnail cleanup failed', { err, attachmentId })\n getTelemetryRuntime()?.reportError(err, { module: 'staff', code: 'staff.portal_attachment_thumbnail_cleanup_failed' })\n })\n if (attachment.storagePath) {\n try {\n const driver = await (await resolveDriverFactory(api, scope)).resolveForPartition(attachment.partitionCode, {\n tenantId: attachment.tenantId ?? scope.tenantId,\n organizationId: attachment.organizationId ?? scope.organizationId,\n })\n await driver.delete(attachment.partitionCode, attachment.storagePath)\n } catch (err) {\n logger.error('staff portal support attachment storage cleanup failed', { err, attachmentId })\n getTelemetryRuntime()?.reportError(err, { module: 'staff', code: 'staff.portal_attachment_storage_cleanup_failed' })\n }\n }\n try {\n const dataEngine = scope.container.resolve('dataEngine') as DataEngine\n await emitCrudSideEffects({\n dataEngine,\n action: 'deleted',\n entity: attachment,\n identifiers: { id: attachment.id, organizationId: attachment.organizationId ?? null, tenantId: attachment.tenantId ?? null },\n events: api.attachmentCrudEvents,\n indexer: api.attachmentCrudIndexer,\n })\n await dataEngine.flushOrmEntityChanges()\n } catch (err) {\n logger.error('staff portal support attachment side effects failed', { err, attachmentId })\n getTelemetryRuntime()?.reportError(err, { module: 'staff', code: 'staff.portal_attachment_side_effects_failed' })\n }\n await guarded.runAfterSuccess()\n emitSupportAttachmentActivity(owned.task, owned.ticket, 'attachment_removed', attachmentId)\n return NextResponse.json({ ok: true })\n } catch (err) {\n return portalErrorResponse(err, 'staff portal support attachments DELETE failed')\n }\n}\n\nconst attachmentItemSchema = portalSupportAttachmentItemSchema\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Staff Portal',\n summary: 'Support ticket attachments',\n methods: {\n GET: {\n summary: 'List or download support ticket files',\n description:\n 'Lists customer-visible files of a ticket the caller owns, or streams one when `attachmentId` is given. Staff files that were not shared with the customer are never listed.',\n responses: [{ status: 200, description: 'Files', schema: z.object({ items: z.array(attachmentItemSchema) }) }],\n errors: [\n { status: 401, description: 'No portal session' },\n { status: 404, description: 'Ticket or file not found' },\n { status: 503, description: 'Attachments module unavailable' },\n ],\n },\n POST: {\n summary: 'Upload a file to a support ticket',\n description: 'Multipart upload (`file`). The file is tagged customer-visible and as uploaded by the caller. Rate limited per customer user; rejected files answer with a translated `error` and the machine `code`.',\n responses: [{ status: 201, description: 'Uploaded', schema: z.object({ ok: z.literal(true), item: attachmentItemSchema }) }],\n errors: [\n { status: 403, description: 'Missing portal.support_tickets.manage' },\n { status: 404, description: 'Ticket not found' },\n { status: 413, description: 'File too large' },\n { status: 429, description: 'Upload rate limit exceeded (per customer user)' },\n ],\n },\n DELETE: {\n summary: 'Delete a file you uploaded',\n description: 'Deletes a file the caller uploaded (`attachmentId` query). Files uploaded by staff or by other customer users answer 403.',\n responses: [{ status: 200, description: 'Deleted', schema: z.object({ ok: z.literal(true) }) }],\n errors: [\n { status: 403, description: 'The file was not uploaded by the caller' },\n { status: 404, description: 'Ticket or file not found' },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,91 @@
1
+ import { NextResponse } from "next/server";
2
+ import { z } from "zod";
3
+ import { runRouteMutationGuards } from "@open-mercato/shared/lib/crud/route-mutation-guard";
4
+ import { getCommandInterceptorHttpRejection } from "@open-mercato/shared/lib/commands/errors";
5
+ import { readJsonSafe } from "@open-mercato/shared/lib/http/readJsonSafe";
6
+ import {
7
+ PORTAL_SUPPORT_TICKETS_MANAGE_FEATURE,
8
+ loadPortalOwnedTicket,
9
+ resolveCustomerDisplay,
10
+ resolveSupportPortalScope
11
+ } from "../../../../../lib/support/portalTickets.js";
12
+ import {
13
+ SUPPORT_TICKET_RESOURCE_KIND,
14
+ buildPortalCommandContext,
15
+ checkPortalRateLimit,
16
+ portalErrorResponse,
17
+ readPathId
18
+ } from "../../../../../lib/support/portalRoute.js";
19
+ import {
20
+ staffSupportTicketCommandIds
21
+ } from "../../../../../commands/support-tickets.js";
22
+ const metadata = { requireAuth: false };
23
+ const bodySchema = z.object({ body: z.string().trim().min(1).max(1e4) });
24
+ const responseSchema = z.object({ ok: z.literal(true), id: z.string().uuid(), reopened: z.boolean() });
25
+ async function POST(req) {
26
+ try {
27
+ const scope = await resolveSupportPortalScope(req, PORTAL_SUPPORT_TICKETS_MANAGE_FEATURE);
28
+ if (scope instanceof Response) return scope;
29
+ const taskId = readPathId(req, /\/support-tickets\/([^/]+)\/messages/);
30
+ const owned = taskId ? await loadPortalOwnedTicket(scope, taskId) : null;
31
+ if (!owned) return NextResponse.json({ ok: false, error: "staff.errors.notFound" }, { status: 404 });
32
+ const limited = await checkPortalRateLimit(scope, "message");
33
+ if (limited) return limited;
34
+ const body = bodySchema.parse(await readJsonSafe(req, {}));
35
+ const guarded = await runRouteMutationGuards({
36
+ container: scope.container,
37
+ req,
38
+ auth: { userId: scope.auth.sub, tenantId: scope.tenantId, organizationId: scope.organizationId, userFeatures: [] },
39
+ input: { resourceKind: SUPPORT_TICKET_RESOURCE_KIND, resourceId: owned.task.id, operation: "update", mutationPayload: body }
40
+ });
41
+ if (!guarded.ok) return guarded.response;
42
+ const effectiveBody = guarded.modifiedPayload ? bodySchema.parse({ ...body, ...guarded.modifiedPayload }) : body;
43
+ const customer = await resolveCustomerDisplay(scope);
44
+ const input = {
45
+ tenantId: scope.tenantId,
46
+ organizationId: scope.organizationId,
47
+ customerId: scope.customerId,
48
+ customerUserId: scope.auth.sub,
49
+ customerDisplayName: customer.displayName,
50
+ customerKind: customer.kind,
51
+ reporterDisplayName: scope.auth.displayName ?? null,
52
+ taskId: owned.task.id,
53
+ body: effectiveBody.body
54
+ };
55
+ const commandBus = scope.container.resolve("commandBus");
56
+ const { result } = await commandBus.execute(
57
+ staffSupportTicketCommandIds.portalMessage,
58
+ { input, ctx: buildPortalCommandContext(scope, req) }
59
+ );
60
+ await guarded.runAfterSuccess();
61
+ return NextResponse.json(responseSchema.parse({ ok: true, id: result.commentId, reopened: result.reopened }), { status: 201 });
62
+ } catch (err) {
63
+ const rejection = getCommandInterceptorHttpRejection(err);
64
+ if (rejection) return NextResponse.json(rejection.body, { status: rejection.status });
65
+ return portalErrorResponse(err, "staff portal support message POST failed");
66
+ }
67
+ }
68
+ const openApi = {
69
+ tag: "Staff Portal",
70
+ summary: "Reply on a support ticket",
71
+ methods: {
72
+ POST: {
73
+ summary: "Reply on a support ticket",
74
+ description: "Adds a customer message to a ticket the signed-in customer owns. Replying on a resolved ticket reopens it (it moves back to the project default column). Rate limited per customer user.",
75
+ requestBody: { contentType: "application/json", schema: bodySchema },
76
+ responses: [{ status: 201, description: "Message added", schema: responseSchema }],
77
+ errors: [
78
+ { status: 401, description: "No portal session" },
79
+ { status: 403, description: "Missing portal.support_tickets.manage" },
80
+ { status: 404, description: "Not found or not owned by this customer" },
81
+ { status: 429, description: "Rate limit exceeded" }
82
+ ]
83
+ }
84
+ }
85
+ };
86
+ export {
87
+ POST,
88
+ metadata,
89
+ openApi
90
+ };
91
+ //# sourceMappingURL=route.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../../../../src/modules/staff/api/portal/support-tickets/%5Bid%5D/messages/route.ts"],
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { runRouteMutationGuards } from '@open-mercato/shared/lib/crud/route-mutation-guard'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands'\nimport { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport {\n PORTAL_SUPPORT_TICKETS_MANAGE_FEATURE,\n loadPortalOwnedTicket,\n resolveCustomerDisplay,\n resolveSupportPortalScope,\n} from '../../../../../lib/support/portalTickets'\nimport {\n SUPPORT_TICKET_RESOURCE_KIND,\n buildPortalCommandContext,\n checkPortalRateLimit,\n portalErrorResponse,\n readPathId,\n} from '../../../../../lib/support/portalRoute'\nimport {\n staffSupportTicketCommandIds,\n type SupportTicketMessageResult,\n type SupportTicketPortalMessageInput,\n} from '../../../../../commands/support-tickets'\n\nexport const metadata: { requireAuth?: boolean } = { requireAuth: false }\n\nconst bodySchema = z.object({ body: z.string().trim().min(1).max(10000) })\nconst responseSchema = z.object({ ok: z.literal(true), id: z.string().uuid(), reopened: z.boolean() })\n\nexport async function POST(req: Request) {\n try {\n const scope = await resolveSupportPortalScope(req, PORTAL_SUPPORT_TICKETS_MANAGE_FEATURE)\n if (scope instanceof Response) return scope\n const taskId = readPathId(req, /\\/support-tickets\\/([^/]+)\\/messages/)\n const owned = taskId ? await loadPortalOwnedTicket(scope, taskId) : null\n if (!owned) return NextResponse.json({ ok: false, error: 'staff.errors.notFound' }, { status: 404 })\n const limited = await checkPortalRateLimit(scope, 'message')\n if (limited) return limited\n const body = bodySchema.parse(await readJsonSafe<Record<string, unknown>>(req, {}))\n const guarded = await runRouteMutationGuards({\n container: scope.container,\n req,\n auth: { userId: scope.auth.sub, tenantId: scope.tenantId, organizationId: scope.organizationId, userFeatures: [] },\n input: { resourceKind: SUPPORT_TICKET_RESOURCE_KIND, resourceId: owned.task.id, operation: 'update', mutationPayload: body },\n })\n if (!guarded.ok) return guarded.response\n const effectiveBody = guarded.modifiedPayload ? bodySchema.parse({ ...body, ...guarded.modifiedPayload }) : body\n const customer = await resolveCustomerDisplay(scope)\n const input: SupportTicketPortalMessageInput = {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n customerId: scope.customerId,\n customerUserId: scope.auth.sub,\n customerDisplayName: customer.displayName,\n customerKind: customer.kind,\n reporterDisplayName: scope.auth.displayName ?? null,\n taskId: owned.task.id,\n body: effectiveBody.body,\n }\n const commandBus = scope.container.resolve('commandBus') as CommandBus\n const { result } = await commandBus.execute<SupportTicketPortalMessageInput, SupportTicketMessageResult>(\n staffSupportTicketCommandIds.portalMessage,\n { input, ctx: buildPortalCommandContext(scope, req) },\n )\n await guarded.runAfterSuccess()\n return NextResponse.json(responseSchema.parse({ ok: true, id: result.commentId, reopened: result.reopened }), { status: 201 })\n } catch (err) {\n const rejection = getCommandInterceptorHttpRejection(err)\n if (rejection) return NextResponse.json(rejection.body, { status: rejection.status })\n return portalErrorResponse(err, 'staff portal support message POST failed')\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Staff Portal',\n summary: 'Reply on a support ticket',\n methods: {\n POST: {\n summary: 'Reply on a support ticket',\n description:\n 'Adds a customer message to a ticket the signed-in customer owns. Replying on a resolved ticket reopens it (it moves back to the project default column). Rate limited per customer user.',\n requestBody: { contentType: 'application/json', schema: bodySchema },\n responses: [{ status: 201, description: 'Message added', schema: responseSchema }],\n errors: [\n { status: 401, description: 'No portal session' },\n { status: 403, description: 'Missing portal.support_tickets.manage' },\n { status: 404, description: 'Not found or not owned by this customer' },\n { status: 429, description: 'Rate limit exceeded' },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,146 @@
1
+ import { NextResponse } from "next/server";
2
+ import { createLogger } from "@open-mercato/shared/lib/logger";
3
+ import { getTelemetryRuntime } from "@open-mercato/shared/lib/telemetry/runtime";
4
+ import { z } from "zod";
5
+ import { findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
6
+ import { User } from "@open-mercato/core/modules/auth/data/entities";
7
+ import { StaffTimeTaskComment, StaffTimeTaskStatus } from "../../../../data/entities.js";
8
+ import {
9
+ PORTAL_SUPPORT_TICKETS_MANAGE_FEATURE,
10
+ PORTAL_SUPPORT_TICKETS_VIEW_FEATURE,
11
+ loadPortalOwnedTicket,
12
+ resolveSupportPortalScope
13
+ } from "../../../../lib/support/portalTickets.js";
14
+ import { portalErrorResponse, readPathId } from "../../../../lib/support/portalRoute.js";
15
+ import {
16
+ listCustomerVisibleAttachments,
17
+ loadPortalAttachmentsApi,
18
+ portalSupportAttachmentItemSchema
19
+ } from "../../../../lib/support/portalAttachments.js";
20
+ import { derivePortalStatus } from "../../../../lib/support/portalStatus.js";
21
+ const logger = createLogger("staff").child({ component: "api/portal/support-tickets/detail" });
22
+ const metadata = { requireAuth: false };
23
+ const messageSchema = z.object({
24
+ id: z.string().uuid(),
25
+ authorKind: z.enum(["staff", "customer"]),
26
+ authorName: z.string().nullable(),
27
+ isSelf: z.boolean(),
28
+ body: z.string(),
29
+ createdAt: z.string().nullable()
30
+ });
31
+ const detailSchema = z.object({
32
+ id: z.string().uuid(),
33
+ supportReference: z.string(),
34
+ subject: z.string(),
35
+ description: z.string(),
36
+ category: z.object({ value: z.string(), label: z.string() }),
37
+ portalStatus: z.enum(["received", "in_progress", "resolved"]),
38
+ createdAt: z.string().nullable(),
39
+ updatedAt: z.string().nullable(),
40
+ canReply: z.boolean(),
41
+ requester: z.object({ name: z.string().nullable(), isSelf: z.boolean() }),
42
+ messages: z.array(messageSchema),
43
+ attachments: z.array(portalSupportAttachmentItemSchema)
44
+ });
45
+ function nonEmptyName(value) {
46
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
47
+ }
48
+ async function loadVisibleAttachments(scope, taskId, canManage) {
49
+ const api = await loadPortalAttachmentsApi();
50
+ if (!api) return [];
51
+ try {
52
+ return await listCustomerVisibleAttachments(api, scope, taskId, canManage);
53
+ } catch (err) {
54
+ logger.warn("staff portal support ticket attachments could not be listed", { err, taskId });
55
+ getTelemetryRuntime()?.reportError(err, { module: "staff", code: "staff.portal_support_attachments_failed" });
56
+ return [];
57
+ }
58
+ }
59
+ async function GET(req) {
60
+ try {
61
+ const scope = await resolveSupportPortalScope(req, PORTAL_SUPPORT_TICKETS_VIEW_FEATURE);
62
+ if (scope instanceof Response) return scope;
63
+ const taskId = readPathId(req, /\/support-tickets\/([^/]+)/);
64
+ const owned = taskId ? await loadPortalOwnedTicket(scope, taskId) : null;
65
+ if (!owned) return NextResponse.json({ ok: false, error: "staff.errors.notFound" }, { status: 404 });
66
+ const { task, ticket } = owned;
67
+ const decryptionScope = { tenantId: scope.tenantId, organizationId: scope.organizationId };
68
+ const status = await scope.em.findOne(StaffTimeTaskStatus, { id: task.taskStatusId, ...decryptionScope }, { fields: ["id", "isDefault", "isDone"] });
69
+ const comments = await scope.em.find(
70
+ StaffTimeTaskComment,
71
+ { taskId: task.id, ...decryptionScope, visibility: "customer", deletedAt: null },
72
+ { orderBy: { createdAt: "asc", id: "asc" } }
73
+ );
74
+ const staffUserIds = [...new Set(comments.map((comment) => comment.authorUserId).filter((id) => typeof id === "string"))];
75
+ const staffNames = /* @__PURE__ */ new Map();
76
+ if (staffUserIds.length) {
77
+ try {
78
+ const users = await findWithDecryption(
79
+ scope.em,
80
+ User,
81
+ { id: { $in: staffUserIds }, tenantId: scope.tenantId },
82
+ void 0,
83
+ decryptionScope
84
+ );
85
+ for (const user of users) staffNames.set(user.id, nonEmptyName(user.name));
86
+ } catch (err) {
87
+ logger.warn("staff portal support ticket could not resolve staff names", { err, taskId: task.id });
88
+ getTelemetryRuntime()?.reportError(err, { module: "staff", code: "staff.portal_support_staff_names_failed" });
89
+ staffNames.clear();
90
+ }
91
+ }
92
+ const request = ticket.customerRequestSnapshot;
93
+ const canReply = await scope.hasFeature(PORTAL_SUPPORT_TICKETS_MANAGE_FEATURE);
94
+ const attachments = await loadVisibleAttachments(scope, task.id, canReply);
95
+ return NextResponse.json(
96
+ detailSchema.parse({
97
+ id: task.id,
98
+ supportReference: ticket.supportReference,
99
+ subject: request && typeof request.subject === "string" ? request.subject : "",
100
+ description: request && typeof request.body === "string" ? request.body : "",
101
+ category: ticket.categorySnapshot,
102
+ portalStatus: derivePortalStatus(status),
103
+ createdAt: task.createdAt ? task.createdAt.toISOString() : null,
104
+ updatedAt: task.updatedAt ? task.updatedAt.toISOString() : null,
105
+ canReply,
106
+ requester: {
107
+ name: nonEmptyName(ticket.customerSnapshot?.reporterDisplayName),
108
+ isSelf: ticket.reportedByCustomerUserId === scope.auth.sub
109
+ },
110
+ messages: comments.map((comment) => ({
111
+ id: comment.id,
112
+ authorKind: comment.authorKind === "customer" ? "customer" : "staff",
113
+ authorName: comment.authorKind === "customer" ? nonEmptyName(comment.authorDisplaySnapshot?.displayName) : comment.authorUserId ? staffNames.get(comment.authorUserId) ?? null : null,
114
+ isSelf: comment.authorKind === "customer" && comment.authorCustomerUserId === scope.auth.sub,
115
+ body: comment.body,
116
+ createdAt: comment.createdAt ? comment.createdAt.toISOString() : null
117
+ })),
118
+ attachments
119
+ })
120
+ );
121
+ } catch (err) {
122
+ return portalErrorResponse(err, "staff portal support ticket GET failed");
123
+ }
124
+ }
125
+ const openApi = {
126
+ tag: "Staff Portal",
127
+ summary: "Support ticket detail",
128
+ methods: {
129
+ GET: {
130
+ summary: "Support ticket detail",
131
+ description: "One ticket owned by the signed-in customer: the original request, the customer-facing status, and customer-visible messages only, and the customer-visible files (`attachments`, same items as the attachments list; an empty list when the attachments module is unavailable). Internal notes and internal files are never returned. A ticket of another customer answers 404.",
132
+ responses: [{ status: 200, description: "Ticket", schema: detailSchema }],
133
+ errors: [
134
+ { status: 401, description: "No portal session" },
135
+ { status: 403, description: "Missing portal.support_tickets.view" },
136
+ { status: 404, description: "Not found or not owned by this customer" }
137
+ ]
138
+ }
139
+ }
140
+ };
141
+ export {
142
+ GET,
143
+ metadata,
144
+ openApi
145
+ };
146
+ //# sourceMappingURL=route.js.map