mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 07:10:57 +00:00
feat: make the support require nothing
This commit is contained in:
@@ -17,6 +17,8 @@ import { JwtGuard } from '../../common/jwt.guard';
|
||||
import {
|
||||
CreateConversationDto,
|
||||
CreateGuestConversationDto,
|
||||
DeviceIdBodyDto,
|
||||
DeviceSendMessageDto,
|
||||
GuestIdBodyDto,
|
||||
GuestSendMessageDto,
|
||||
ListConversationsQueryDto,
|
||||
@@ -103,7 +105,39 @@ export class SupportController {
|
||||
return this.service.unreadCount('USER', { iamUserId: userId(req) });
|
||||
}
|
||||
|
||||
// ---- customer: guest (unauthenticated) --------------------------------
|
||||
// ---- 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).
|
||||
|
||||
|
||||
@@ -93,6 +93,26 @@ export class GuestIdBodyDto {
|
||||
guestId!: string;
|
||||
}
|
||||
|
||||
export class DeviceSendMessageDto {
|
||||
@ApiProperty({ description: 'Client device id (localStorage).' })
|
||||
@IsString()
|
||||
@Length(8, 120)
|
||||
deviceId!: string;
|
||||
|
||||
@ApiProperty({ description: 'Message text.' })
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
@MaxLength(4000)
|
||||
text!: string;
|
||||
}
|
||||
|
||||
export class DeviceIdBodyDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@Length(8, 120)
|
||||
deviceId!: string;
|
||||
}
|
||||
|
||||
export class UpdateStatusDto {
|
||||
@ApiProperty({ enum: SupportStatusDto })
|
||||
@IsEnum(SupportStatusDto)
|
||||
|
||||
@@ -113,6 +113,61 @@ export class SupportService {
|
||||
return this.firstMessage(conversation, input.initialMessage);
|
||||
}
|
||||
|
||||
// ---- customer: device-scoped single thread (portal) -------------------
|
||||
|
||||
/** The device's single conversation + its messages ({conversation:null} if none). */
|
||||
async getDeviceThread(deviceId: string): Promise<T.PassengerSupportThreadDto> {
|
||||
if (!deviceId) return { conversation: null, messages: [] };
|
||||
const c = (await this.prisma.supportConversation.findFirst({
|
||||
where: { guestId: deviceId },
|
||||
orderBy: { createdAt: 'asc' },
|
||||
})) as ConversationRow | null;
|
||||
if (!c) return { conversation: null, messages: [] };
|
||||
const rows = await this.prisma.supportMessage.findMany({
|
||||
where: { conversationId: c.id },
|
||||
orderBy: { createdAt: 'asc' },
|
||||
});
|
||||
const unread = await this.computeUnread([c], 'USER');
|
||||
return {
|
||||
conversation: this.toConversationDto(c, unread.get(c.id) ?? 0),
|
||||
messages: rows.map((m) => this.toMessageDto(m)),
|
||||
};
|
||||
}
|
||||
|
||||
/** Append a message to the device's thread, creating it on first message. */
|
||||
async sendDeviceMessage(
|
||||
deviceId: string,
|
||||
text: string,
|
||||
): Promise<T.PassengerSupportMessageDto> {
|
||||
let c = (await this.prisma.supportConversation.findFirst({
|
||||
where: { guestId: deviceId },
|
||||
orderBy: { createdAt: 'asc' },
|
||||
})) as ConversationRow | null;
|
||||
if (!c) {
|
||||
c = (await this.prisma.supportConversation.create({
|
||||
data: { guestId: deviceId, subject: 'Support chat', status: 'OPEN' },
|
||||
})) as ConversationRow;
|
||||
}
|
||||
const updated = await this.appendMessage(c, 'USER', text);
|
||||
const last = updated.messages[updated.messages.length - 1];
|
||||
return this.toMessageDto(last);
|
||||
}
|
||||
|
||||
/** Mark the device's thread read (customer side). */
|
||||
async markDeviceRead(deviceId: string): Promise<{ unreadCount: number }> {
|
||||
const c = await this.prisma.supportConversation.findFirst({
|
||||
where: { guestId: deviceId },
|
||||
orderBy: { createdAt: 'asc' },
|
||||
});
|
||||
if (c) {
|
||||
await this.prisma.supportConversation.update({
|
||||
where: { id: c.id },
|
||||
data: { userLastReadAt: new Date() },
|
||||
});
|
||||
}
|
||||
return this.unreadCount('USER', { guestId: deviceId });
|
||||
}
|
||||
|
||||
async listForCustomer(
|
||||
owner: CustomerOwner,
|
||||
query: ListQuery,
|
||||
|
||||
Reference in New Issue
Block a user