Files
edr-platform/apps/edr-passenger-api/src/modules/support/support.controller.ts
2026-07-08 05:33:02 +00:00

242 lines
7.7 KiB
TypeScript

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');
}
}