feat: create message action#173
Conversation
WalkthroughIntroduces ticket message creation end-to-end: a new POST API for ticket messages, shared validation schema, queue plugin/connection with new env var and dependency, a form component and drawer UI on the ticket page, store/composable updates for unanswered ticket badges, and multiple minor UI tweaks (avatars, labels, icons, decorative image, timestamp formats, and removals of bottom icons). Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor U as User
participant C as Ticket Page (Drawer + Form)
participant API as POST /api/ticket/id/:ticketId/message
participant Repo as repository.ticket
participant Q as Queue (connection)
participant S as Stores (ticket/user)
U->>C: Open "Написать сообщение"
C->>API: Submit { text } with Authorization
API->>Repo: find(ticketId)
Repo-->>API: ticket | 404
API->>Repo: createMessage({ ticketId, userId, text })
Repo-->>API: message
API->>Q: emit messageCreated({ ticketId, ticketOwnerId, messageId, ... })
API-->>C: { ok: true, result: message }
par Refresh state
C->>S: ticketStore.update()
C->>S: userStore.update()
end
C->>C: Close drawer
sequenceDiagram
participant Store as ticketStore
participant Nav as useNavigation()
Store-->>Nav: ticketsWithoutAnswer (computed)
Nav-->>Nav: badge = length.toString()
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
apps/atrium-telegram/app/components/ticket/Message.vue (1)
64-115: Type error risk: custom "condition" field isn't part of DropdownMenuItemYou’re adding a non-standard property (condition) to items typed as DropdownMenuItem, which likely fails typecheck and blocks CI. Define a local extended type to carry the condition flag.
Apply this diff:
-import type { DropdownMenuItem } from '@nuxt/ui' +import type { DropdownMenuItem } from '@nuxt/ui' +type MenuItem = DropdownMenuItem & { condition?: boolean } ... -const items = computed<DropdownMenuItem[]>(() => { - const menuItems: DropdownMenuItem[] = [ +const items = computed<DropdownMenuItem[]>(() => { + const menuItems: MenuItem[] = [ ... - return menuItems.filter((item) => item.condition) + return menuItems.filter((item) => item.condition) })apps/atrium-telegram/app/pages/ticket/[ticketId]/index.vue (1)
86-89: “Show last 10 messages” logic is reversed.
slice(0, n)shows oldest N. Use negative index to show the latest N.-const messages = computed(() => ticket.value?.messages.slice(0, shownMessages.value)) +const messages = computed(() => ticket.value?.messages.slice(-shownMessages.value))
🧹 Nitpick comments (18)
apps/atrium-telegram/app/components/ticket/Message.vue (2)
79-81: Clipboard write should handle permission/errorsWrap writeText in try/catch to avoid silent failures (denied permissions, insecure context).
Apply this diff:
- onSelect: () => navigator.clipboard.writeText(message.value?.text ?? ''), + onSelect: async () => { + try { + await navigator.clipboard.writeText(message.value?.text ?? '') + } catch { + // Optionally notify the user + } + },
117-122: Open in new tab safely (prevent reverse‑tabnabbing)Use noopener/noreferrer when opening external URLs.
Apply this diff:
- window.open(fileUrl, '_blank') + window.open(fileUrl, '_blank', 'noopener,noreferrer')apps/atrium-telegram/.env.example (1)
4-6: Document the required QUEUE_URL formatSince the plugin hard-fails when QUEUE_URL is missing, add a sample format to ease setup.
Apply this diff:
# Queue QUEUE_URL= + +# e.g. amqp://user:pass@localhost:5672/vhost +# Required by server/plugins/02.queue.ts at startupapps/atrium-telegram/app/components/ticket/MessageFile.vue (1)
46-48: Typo in Russian copy: “файл” → “файл”The string uses a combining mark (й). Replace with the single letter “й”.
Apply this diff:
- label: 'Прикреплен файл', + label: 'Прикреплен файл',apps/atrium-telegram/server/plugins/02.queue.ts (1)
8-13: Don’t hard‑fail app startup if QUEUE_URL is absent (gate by env or degrade gracefully)Throwing aborts the whole service even for features not using the queue. Prefer skipping init in dev/test or behind an explicit flag, and add clearer error context.
Apply this diff:
export default defineNitroPlugin(async () => { - if (!process.env.QUEUE_URL) { - throw new Error('QUEUE_URL is not defined') - } - - await useCreateConnection(process.env.QUEUE_URL) + const url = process.env.QUEUE_URL + if (!url) { + console.warn('[queue] QUEUE_URL not set — skipping queue initialization') + return + } + try { + await useCreateConnection(url) + // console.info('[queue] connected') + } catch (err) { + console.error('[queue] connection failed', err) + throw err + } })apps/atrium-telegram/app/components/PageContainer.vue (1)
6-12: Lazy-load decorative imageSmall perf win: mark decorative image as lazy/async and non-draggable.
Apply this diff:
- <img + <img src="/sushi-heart.svg" alt="" class="w-10 opacity-25 invert-50" + loading="lazy" + decoding="async" + draggable="false" >apps/atrium-telegram/app/stores/ticket.ts (1)
13-14: Definition of “without answer” — should tickets with no messages count?Current filter excludes tickets with no messages. If “unanswered” should include brand-new tickets, include null lastMessage.
Apply this diff if desired:
- const ticketsWithoutAnswer = computed(() => tickets.value.filter((ticket) => ticket.lastMessage?.userId === ticket.userId)) + const ticketsWithoutAnswer = computed(() => + tickets.value.filter((t) => (t.lastMessage ? t.lastMessage.userId === t.userId : true)), + )apps/atrium-telegram/app/pages/flow/[itemId]/index.vue (2)
16-30: Hardcoded messages count“0” should be dynamic or hidden when unknown.
Suggested pattern:
- <p>0</p> + <p>{{ item?.messagesCount ?? 0 }}</p>Adjust to actual source of truth (store/API) if different.
51-53: Safer date parsingPrefer parseISO to avoid Date() locale quirks.
Apply this diff:
-import { format } from 'date-fns' +import { format, parseISO } from 'date-fns' ... - v-text="format(new Date(item.createdAt), 'd MMMM yyyy в HH:mm', { locale: ru })" + v-text="format(parseISO(item.createdAt), 'd MMMM yyyy в HH:mm', { locale: ru })"apps/atrium-telegram/shared/services/ticket.ts (1)
3-6: Prevent whitespace-only messages; consider normalizing.Schema allows " " x N (length ≥1). Either trim and re‑validate server‑side or enforce trimmed input.
Apply trimming in the API handler (recommended):
- const data = createTicketMessageSchema(body) + const data = createTicketMessageSchema(body) + const text = data.text.trim() + if (!text) { + throw createError({ statusCode: 400, message: 'Text must not be empty' }) + }apps/atrium-telegram/app/components/ticket/Card.vue (1)
36-36: Optional: locale-aware time formatting.If
updatedAtis ISO, this is fine. Consider timezone awareness if server sends UTC.apps/atrium-telegram/app/components/form/CreateTicketMessage.vue (3)
8-16: i18n consistency for label/placeholder.Hardcoded RU strings — use $t like the button.
- <UFormField label="Ваше сообщение" name="text"> + <UFormField :label="$t('ticket.message.label')" name="text"> ... - placeholder="Не торопись, осмотрись..." + :placeholder="$t('ticket.message.placeholder')"
18-28: Disable while submitting; show loading.Prevents double‑submits and improves UX.
- <UButton + <UButton type="submit" variant="solid" color="secondary" size="xl" icon="i-lucide-send" block class="mt-3" - :disabled="!state.text" + :disabled="!state.text || loading" + :loading="loading" :label="$t('common.send')" />
45-47: Initialize state as empty string for better v-model ergonomics.Avoids undefined binding in textarea.
-const state = ref<Partial<CreateTicketMessage>>({ - text: undefined, -}) +const state = ref<Partial<CreateTicketMessage>>({ text: '' })apps/atrium-telegram/server/api/ticket/id/[ticketId]/message.post.ts (2)
30-40: Use trimmed text and return 201.Minor polish.
- const message = await repository.ticket.createMessage({ + const message = await repository.ticket.createMessage({ ticketId, userId: event.context.user.id, - text: data.text, + text: data.text, }) ... - return { + setResponseStatus(event, 201) + return { ok: true, result: message, }
42-51: Defensive defaults for user name/surname.Avoid undefined in emitted payload.
- userName: event.context.user.name, - userSurname: event.context.user.surname, + userName: event.context.user.name ?? '', + userSurname: event.context.user.surname ?? '',apps/atrium-telegram/app/pages/ticket/[ticketId]/index.vue (1)
91-91: Drawer default state.If you want the composer discoverable, consider prompting via FAB instead of relying on the drawer default false. As-is, fine.
apps/atrium-telegram/app/composables/useNavigation.ts (1)
24-24: Hide zero badge and clamp large counts.NavigationRoute declares badge?: string in apps/atrium-telegram/shared/types/index.ts — assigning undefined is allowed. Replace with:
- badge: ticketStore.ticketsWithoutAnswer.length.toString(), + badge: ticketStore.ticketsWithoutAnswer.length + ? Math.min(99, ticketStore.ticketsWithoutAnswer.length).toString() + : undefined,
- Apply the same pattern to the other badges (flowStore, taskStore) to hide zeros and cap counts at 99.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (16)
apps/atrium-telegram/.env.example(1 hunks)apps/atrium-telegram/app/components/PageContainer.vue(1 hunks)apps/atrium-telegram/app/components/form/CreateTicketMessage.vue(1 hunks)apps/atrium-telegram/app/components/ticket/Card.vue(3 hunks)apps/atrium-telegram/app/components/ticket/Message.vue(1 hunks)apps/atrium-telegram/app/components/ticket/MessageFile.vue(1 hunks)apps/atrium-telegram/app/composables/useNavigation.ts(2 hunks)apps/atrium-telegram/app/pages/flow/[itemId]/index.vue(2 hunks)apps/atrium-telegram/app/pages/index.vue(0 hunks)apps/atrium-telegram/app/pages/ticket/[ticketId]/index.vue(3 hunks)apps/atrium-telegram/app/pages/ticket/index.vue(0 hunks)apps/atrium-telegram/app/stores/ticket.ts(2 hunks)apps/atrium-telegram/package.json(1 hunks)apps/atrium-telegram/server/api/ticket/id/[ticketId]/message.post.ts(1 hunks)apps/atrium-telegram/server/plugins/02.queue.ts(1 hunks)apps/atrium-telegram/shared/services/ticket.ts(1 hunks)
💤 Files with no reviewable changes (2)
- apps/atrium-telegram/app/pages/ticket/index.vue
- apps/atrium-telegram/app/pages/index.vue
🧰 Additional context used
🧬 Code graph analysis (4)
apps/atrium-telegram/app/composables/useNavigation.ts (1)
apps/atrium-telegram/app/stores/ticket.ts (1)
useTicketStore(10-47)
apps/atrium-telegram/server/plugins/02.queue.ts (1)
packages/queue/src/connection.ts (1)
useCreateConnection(15-20)
apps/atrium-telegram/server/api/ticket/id/[ticketId]/message.post.ts (1)
apps/atrium-telegram/shared/services/ticket.ts (1)
createTicketMessageSchema(3-5)
apps/atrium-telegram/app/stores/ticket.ts (1)
packages/database/src/tables.ts (1)
tickets(689-701)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (7)
apps/atrium-telegram/app/components/ticket/Message.vue (1)
4-4: Avatar sizing change looks goodConsistent with other components. No functional concerns.
apps/atrium-telegram/app/components/ticket/MessageFile.vue (1)
7-7: UI tweak acknowledgedVariant change to "soft" is fine; matches rest of the UI theme.
apps/atrium-telegram/app/pages/flow/[itemId]/index.vue (1)
39-44: Avatar sizing change looks goodConsistent with ticket message avatars.
apps/atrium-telegram/package.json (1)
20-21: New workspace dependency looks correct — local verification requiredSandbox verification failed: apps/atrium-telegram/package.json not found and pnpm -w cannot run outside a workspace. From the repo root run:
jq -r '.dependencies["@roll-stack/queue"]' apps/atrium-telegram/package.json
pnpm -w why @roll-stack/queue
Confirm @roll-stack/queue is a workspace member and that CI builds it.apps/atrium-telegram/app/pages/ticket/[ticketId]/index.vue (2)
33-49: LGTM on drawer integration.Wiring the composer and closing on submit/success looks good.
59-64: LGTM on button styling.Consistent with the new messaging UX.
apps/atrium-telegram/server/api/ticket/id/[ticketId]/message.post.ts (1)
22-28: Authorization gap — restrict posting to ticket owner or privileged users.File: apps/atrium-telegram/server/api/ticket/id/[ticketId]/message.post.ts (lines 22–28)
const ticket = await repository.ticket.find(ticketId) if (!ticket) { throw createError({ statusCode: 404, message: 'Ticket not found', }) } + // Only owner or privileged users can post + const isOwner = ticket.userId === event.context.user.id + const isPrivileged = Array.isArray(event.context.user.roles) && event.context.user.roles.includes('support') + if (!isOwner && !isPrivileged) { + throw createError({ statusCode: 403, message: 'Forbidden' }) + }Confirm the intended authorization model (owner-only vs. support/privileged roles) and adjust the guard accordingly.
| async function onSubmit(event: FormSubmitEvent<CreateTicketMessage>) { | ||
| emit('submitted') | ||
|
|
||
| try { | ||
| await $fetch(`/api/ticket/id/${ticketId}/message`, { | ||
| method: 'POST', | ||
| headers: { | ||
| Authorization: `tma ${userStore.initDataRaw}`, | ||
| }, | ||
| body: event.data, | ||
| }) | ||
|
|
||
| await Promise.all([ | ||
| ticketStore.update(), | ||
| userStore.update(), | ||
| ]) | ||
|
|
||
| vibrate('success') | ||
| emit('success') | ||
| } catch (error) { | ||
| console.error(error) | ||
| vibrate('error') | ||
| } | ||
| } |
There was a problem hiding this comment.
Trim input, guard whitespace, reset state, and add loading flag.
Prevents whitespace‑only posts and cleans up after success.
+const loading = ref(false)
async function onSubmit(event: FormSubmitEvent<CreateTicketMessage>) {
emit('submitted')
try {
+ loading.value = true
+ const text = event.data.text.trim()
+ if (!text) {
+ vibrate('error')
+ return
+ }
await $fetch(`/api/ticket/id/${ticketId}/message`, {
method: 'POST',
headers: {
Authorization: `tma ${userStore.initDataRaw}`,
},
- body: event.data,
+ body: { text },
})
await Promise.all([
ticketStore.update(),
userStore.update(),
])
vibrate('success')
emit('success')
} catch (error) {
console.error(error)
vibrate('error')
}
+ finally {
+ loading.value = false
+ state.value.text = ''
+ }
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| async function onSubmit(event: FormSubmitEvent<CreateTicketMessage>) { | |
| emit('submitted') | |
| try { | |
| await $fetch(`/api/ticket/id/${ticketId}/message`, { | |
| method: 'POST', | |
| headers: { | |
| Authorization: `tma ${userStore.initDataRaw}`, | |
| }, | |
| body: event.data, | |
| }) | |
| await Promise.all([ | |
| ticketStore.update(), | |
| userStore.update(), | |
| ]) | |
| vibrate('success') | |
| emit('success') | |
| } catch (error) { | |
| console.error(error) | |
| vibrate('error') | |
| } | |
| } | |
| const loading = ref(false) | |
| async function onSubmit(event: FormSubmitEvent<CreateTicketMessage>) { | |
| emit('submitted') | |
| try { | |
| loading.value = true | |
| const text = event.data.text.trim() | |
| if (!text) { | |
| vibrate('error') | |
| return | |
| } | |
| await $fetch(`/api/ticket/id/${ticketId}/message`, { | |
| method: 'POST', | |
| headers: { | |
| Authorization: `tma ${userStore.initDataRaw}`, | |
| }, | |
| body: { text }, | |
| }) | |
| await Promise.all([ | |
| ticketStore.update(), | |
| userStore.update(), | |
| ]) | |
| vibrate('success') | |
| emit('success') | |
| } catch (error) { | |
| console.error(error) | |
| vibrate('error') | |
| } finally { | |
| loading.value = false | |
| state.value.text = '' | |
| } | |
| } |
🤖 Prompt for AI Agents
In apps/atrium-telegram/app/components/form/CreateTicketMessage.vue around lines
49 to 72, the submit handler should trim the input, block whitespace-only
messages, manage a loading flag, and reset form state after success: before
sending, set a local loading=true; trim the relevant text fields on event.data
(or create a trimmed copy) and if the trimmed message is empty, set
loading=false and abort (or emit a validation error); proceed to call the API
with the trimmed payload; in a finally block set loading=false; on success
clear/reset the form model/state and emit 'success' (keep vibrate calls as-is);
ensure errors still log and vibrate('error').
| <div v-if="hasAnswerFromUser" class="flex flex-row items-center gap-1.5 text-error"> | ||
| <UIcon | ||
| name="i-lucide-pointer" | ||
| class="size-8 motion-translate-y-loop-25 motion-preset-seesaw motion-duration-2000" | ||
| /> | ||
| <p class="max-w-22 text-sm/4 font-bold"> | ||
| Есть ответ от партнера | ||
| </p> | ||
| </div> | ||
| </div> |
There was a problem hiding this comment.
Wrong condition for “Ответ от партнера”.
The flag triggers when last message is by the ticket owner, not the partner. Invert the check.
Apply with the script change below (see Lines 47–52).
🤖 Prompt for AI Agents
In apps/atrium-telegram/app/components/ticket/Card.vue around lines 6–15 (and
adjust the related logic at lines 47–52), the template currently shows "Есть
ответ от партнера" when hasAnswerFromUser is true; invert that check so the UI
is shown when the last message is from the partner (e.g., use !hasAnswerFromUser
or a proper hasAnswerFromPartner flag). Update the logic at lines 47–52 where
the last-message author is evaluated to set the boolean true when the author is
the partner (or rename the computed flag to reflect partner ownership) and
adjust all usages accordingly to maintain consistent naming.
| const { ticket } = defineProps<{ | ||
| ticket: TicketWithData | ||
| }>() | ||
|
|
||
| const hasAnswerFromUser = computed(() => ticket.lastMessage?.userId === ticket.userId) | ||
| </script> |
There was a problem hiding this comment.
Prop destructuring drops reactivity; and the condition should be “!==”.
Destructuring defineProps without toRefs makes ticket non‑reactive. Also fix partner reply logic and name accordingly.
-const { ticket } = defineProps<{
- ticket: TicketWithData
-}>()
-
-const hasAnswerFromUser = computed(() => ticket.lastMessage?.userId === ticket.userId)
+const props = defineProps<{ ticket: TicketWithData }>()
+const { ticket } = toRefs(props)
+
+const hasAnswerFromPartner = computed(
+ () => ticket.value.lastMessage?.userId !== ticket.value.userId
+)And in template:
-<div v-if="hasAnswerFromUser" ...
+<div v-if="hasAnswerFromPartner" ...Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In apps/atrium-telegram/app/components/ticket/Card.vue around lines 47–52, the
current destructuring of defineProps makes ticket non‑reactive and the computed
uses the wrong equality; stop destructuring props so reactivity is preserved
(e.g. const props = defineProps<{ ticket: TicketWithData }>(); then create a
reactive ref via toRef(props, 'ticket') or use toRefs), rename the computed to
reflect partner reply (e.g. hasAnswerFromPartner) and change the condition to
!== (ticket.lastMessage?.userId !== ticket.userId), and update template bindings
to use the ref appropriately so reactivity works.
| export default defineEventHandler(async (event) => { | ||
| try { | ||
| const ticketId = getRouterParam(event, 'ticketId') | ||
| if (!ticketId) { | ||
| throw createError({ | ||
| statusCode: 400, | ||
| message: 'Id is required', | ||
| }) | ||
| } | ||
|
|
There was a problem hiding this comment.
Authenticate request early.
Guard missing user; otherwise event.context.user.id will throw.
export default defineEventHandler(async (event) => {
try {
+ if (!event.context.user) {
+ throw createError({ statusCode: 401, message: 'Unauthorized' })
+ }
const ticketId = getRouterParam(event, 'ticketId')
if (!ticketId) {
throw createError({
statusCode: 400,
message: 'Id is required',
})
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export default defineEventHandler(async (event) => { | |
| try { | |
| const ticketId = getRouterParam(event, 'ticketId') | |
| if (!ticketId) { | |
| throw createError({ | |
| statusCode: 400, | |
| message: 'Id is required', | |
| }) | |
| } | |
| export default defineEventHandler(async (event) => { | |
| try { | |
| if (!event.context.user) { | |
| throw createError({ statusCode: 401, message: 'Unauthorized' }) | |
| } | |
| const ticketId = getRouterParam(event, 'ticketId') | |
| if (!ticketId) { | |
| throw createError({ | |
| statusCode: 400, | |
| message: 'Id is required', | |
| }) | |
| } |
🤖 Prompt for AI Agents
In apps/atrium-telegram/server/api/ticket/id/[ticketId]/message.post.ts around
lines 6–15, the handler accesses event.context.user.id without verifying the
user exists; add an early authentication guard: check event.context &&
event.context.user (or event.context.user) immediately after retrieving ticketId
and before any usage, and if missing throw a 401/Unauthorized error via
createError; then safely read and use user.id from a local variable. Ensure the
guard runs before any other logic to prevent runtime exceptions.
| const body = await readBody(event) | ||
| const data = createTicketMessageSchema(body) | ||
| if (data instanceof type.errors) { | ||
| throw data | ||
| } | ||
|
|
There was a problem hiding this comment.
Validate after trimming; reject whitespace-only.
Current schema accepts spaces. Normalize and re‑check.
- const body = await readBody(event)
- const data = createTicketMessageSchema(body)
+ const body = await readBody(event)
+ const data = createTicketMessageSchema(body)
+ data.text = data.text.trim()
+ if (!data.text) {
+ throw createError({ statusCode: 400, message: 'Text must not be empty' })
+ }
if (data instanceof type.errors) {
throw data
}Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In apps/atrium-telegram/server/api/ticket/id/[ticketId]/message.post.ts around
lines 16 to 21, the code validates the raw body but doesn't trim string fields
first so inputs of only whitespace pass; before calling
createTicketMessageSchema, normalize the incoming body by trimming all string
fields (e.g., body.message = body.message?.trim()) and then re-run validation,
and update the schema to reject empty strings (or add a post-validate check) so
that whitespace-only values fail validation and the handler throws an error as
intended.
| } catch (error) { | ||
| throw errorResolver(error) | ||
| } | ||
| }) |
There was a problem hiding this comment.
🧩 Analysis chain
Potential missing import for errorResolver.
Unless auto‑imported, this will throw ReferenceError.
If missing, add the import with the correct path:
+import { errorResolver } from '~/server/utils/errorResolver' // adjust path if different🏁 Script executed:
#!/bin/bash
# Verify errorResolver is globally/auto imported somewhere
rg -n "errorResolver" -C2 apps/atrium-telegram/server | sed -n '1,200p'Length of output: 7620
🏁 Script executed:
#!/bin/bash
# Display the top of the ticket message handler to check for errorResolver import
sed -n '1,30p' apps/atrium-telegram/server/api/ticket/id/\[ticketId\]/message.post.tsLength of output: 836
Add missing import for errorResolver
apps/atrium-telegram/server/api/ticket/id/[ticketId]/message.post.ts calls errorResolver in the catch but does not import it — add at the top:
import { errorResolver } from '../../../../utils/error'
🤖 Prompt for AI Agents
In apps/atrium-telegram/server/api/ticket/id/[ticketId]/message.post.ts around
lines 57-60, the catch block calls errorResolver but the module is not imported;
add the import statement at the top of the file: import { errorResolver } from
'../../../../utils/error' so the function is available for use in the catch
block.


Summary by CodeRabbit
New Features
Style
Chores