import { Body, Controller, Get, Param, Patch, Post, Query, Req, UnauthorizedException, UseGuards, } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; import { IsPublic } from '@tria-plc/api-common/modules/auth/decorators/public.decorator'; import { SupportService } from './support.service'; import { JwtGuard } from '../../common/jwt.guard'; import { CreateConversationDto, CreateGuestConversationDto, DeviceIdBodyDto, DeviceSendMessageDto, GuestIdBodyDto, GuestSendMessageDto, ListConversationsQueryDto, SendMessageDto, UpdateStatusDto, } from './support.dto'; function userId(req: any): string { const id = req?.user?.id ?? req?.user?.sub; if (!id) throw new UnauthorizedException(); return id; } @ApiTags('Support') @Controller('support') export class SupportController { constructor(private service: SupportService) {} // ---- FAQ (public) ------------------------------------------------------ @Get('faq') @IsPublic() @ApiOperation({ summary: 'Get FAQ categories' }) getFaqCategories() { return this.service.getFaqCategories(); } @Get('faq/:categoryId/articles') @IsPublic() @ApiOperation({ summary: 'Get FAQ articles for a category' }) getFaqArticles(@Param('categoryId') id: string) { return this.service.getFaqArticles(id); } // ---- customer: authenticated passenger -------------------------------- @Post('conversations') @UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth') @ApiOperation({ summary: 'Open a new support conversation' }) createConversation(@Req() req: any, @Body() body: CreateConversationDto) { return this.service.createConversation(userId(req), body); } @Get('conversations') @UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth') @ApiOperation({ summary: 'List my support conversations' }) listMine(@Req() req: any, @Query() query: ListConversationsQueryDto) { return this.service.listForCustomer({ iamUserId: userId(req) }, query); } @Get('conversations/:id/messages') @UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth') @ApiOperation({ summary: 'List messages in one of my conversations' }) messages(@Req() req: any, @Param('id') id: string) { return this.service.getMessages(id, { iamUserId: userId(req) }); } @Post('conversations/:id/messages') @UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth') @ApiOperation({ summary: 'Send a message as the customer' }) send(@Req() req: any, @Param('id') id: string, @Body() body: SendMessageDto) { return this.service.sendMessage(id, 'USER', body.text, { iamUserId: userId(req), }); } @Post('conversations/:id/read') @UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth') @ApiOperation({ summary: 'Mark a conversation read (customer side)' }) read(@Req() req: any, @Param('id') id: string) { return this.service.markRead(id, 'USER', { iamUserId: userId(req) }); } @Get('unread-count') @UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth') @ApiOperation({ summary: 'Count my unread support conversations' }) unread(@Req() req: any) { return this.service.unreadCount('USER', { iamUserId: userId(req) }); } // ---- customer: device-scoped single thread (portal) ------------------- // No auth, no forms. One conversation per device id (localStorage). Anyone // with the device id can see that thread — accepted MVP trade-off. @Get('device/thread') @IsPublic() @ApiOperation({ summary: "Get the device's support thread + messages" }) deviceThread(@Query('deviceId') deviceId: string) { return this.service.getDeviceThread(deviceId); } @Post('device/messages') @IsPublic() @ApiOperation({ summary: 'Send a message (creates the thread on first send)' }) deviceSend(@Body() body: DeviceSendMessageDto) { return this.service.sendDeviceMessage(body.deviceId, body.text); } @Post('device/read') @IsPublic() @ApiOperation({ summary: 'Mark the device thread read' }) deviceRead(@Body() body: DeviceIdBodyDto) { return this.service.markDeviceRead(body.deviceId); } @Get('device/unread-count') @IsPublic() @ApiOperation({ summary: "Count the device thread's unread messages" }) deviceUnread(@Query('deviceId') deviceId: string) { return this.service.unreadCount('USER', { guestId: deviceId }); } // ---- customer: guest (unauthenticated, multi-ticket) ------------------ // No JwtGuard. Access is scoped by a client-generated `guestId` (the bearer // of access — anyone with it sees that thread; accepted MVP trade-off). @Post('guest/conversations') @IsPublic() @ApiOperation({ summary: 'Open a support conversation as a guest' }) guestCreate(@Body() body: CreateGuestConversationDto) { return this.service.createGuestConversation(body); } @Get('guest/conversations') @IsPublic() @ApiOperation({ summary: 'List a guest\'s conversations' }) guestList( @Query('guestId') guestId: string, @Query() query: ListConversationsQueryDto, ) { return this.service.listForCustomer({ guestId }, query); } @Get('guest/conversations/:id/messages') @IsPublic() @ApiOperation({ summary: 'List messages in a guest conversation' }) guestMessages(@Param('id') id: string, @Query('guestId') guestId: string) { return this.service.getMessages(id, { guestId }); } @Post('guest/conversations/:id/messages') @IsPublic() @ApiOperation({ summary: 'Send a message as a guest' }) guestSend(@Param('id') id: string, @Body() body: GuestSendMessageDto) { return this.service.sendMessage(id, 'USER', body.text, { guestId: body.guestId, }); } @Post('guest/conversations/:id/read') @IsPublic() @ApiOperation({ summary: 'Mark a guest conversation read' }) guestRead(@Param('id') id: string, @Body() body: GuestIdBodyDto) { return this.service.markRead(id, 'USER', { guestId: body.guestId }); } @Get('guest/unread-count') @IsPublic() @ApiOperation({ summary: 'Count a guest\'s unread conversations' }) guestUnread(@Query('guestId') guestId: string) { return this.service.unreadCount('USER', { guestId }); } // ---- agent (backoffice) ------------------------------------------------ // TODO: gate agent routes with a staff permission once passenger RBAC is wired. @Get('agent/conversations') @UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth') @ApiOperation({ summary: 'List all support conversations (shared inbox)' }) agentList(@Query() query: ListConversationsQueryDto) { return this.service.listForAgents(query); } @Get('agent/conversations/:id/messages') @UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth') @ApiOperation({ summary: 'List messages in a conversation' }) agentMessages(@Param('id') id: string) { return this.service.getMessages(id); } @Post('agent/conversations/:id/messages') @UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth') @ApiOperation({ summary: 'Reply as an agent' }) agentSend(@Param('id') id: string, @Body() body: SendMessageDto) { return this.service.sendMessage(id, 'AGENT', body.text); } @Patch('agent/conversations/:id/status') @UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth') @ApiOperation({ summary: "Change a conversation's status" }) agentStatus(@Param('id') id: string, @Body() body: UpdateStatusDto) { return this.service.setStatus(id, body.status); } @Post('agent/conversations/:id/read') @UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth') @ApiOperation({ summary: 'Mark a conversation read (agent side)' }) agentRead(@Param('id') id: string) { return this.service.markRead(id, 'AGENT'); } @Get('agent/unread-count') @UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth') @ApiOperation({ summary: 'Count unread conversations (agent side)' }) agentUnread() { return this.service.unreadCount('AGENT'); } }