initial: standalone repo from monorepo split

This commit is contained in:
kevin-asprec
2026-04-13 09:36:55 +08:00
commit 4a8f1e9318
157 changed files with 9198 additions and 0 deletions

View File

@@ -0,0 +1,186 @@
import {
Injectable,
NotFoundException,
BadRequestException,
} from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { CreateTicketDto } from './dto/create-ticket.dto';
import { UpdateTicketDto } from './dto/update-ticket.dto';
import { NotificationService } from '../notification/notification.service';
export interface TicketResolvedEvent {
ticketId: string;
tenantId: string;
clientId: string | null;
type: string;
}
@Injectable()
export class TicketService {
// Event handler for ticket resolution — set by SubscriptionService
onTicketResolved: ((event: TicketResolvedEvent) => Promise<void>) | null =
null;
constructor(
private readonly prisma: PrismaService,
private readonly notificationService: NotificationService,
) {}
async findAll(tenantId: string, filters?: { clientId?: string; status?: string; type?: string }) {
const db = this.prisma.forTenant(tenantId);
return db.ticket.findMany({
where: {
...(filters?.clientId && { clientId: filters.clientId }),
...(filters?.status && { status: filters.status }),
...(filters?.type && { type: filters.type }),
},
include: {
client: { select: { id: true, firstName: true, lastName: true, accountNumber: true } },
assignee: { select: { id: true, firstName: true, lastName: true } },
createdBy: { select: { id: true, firstName: true, lastName: true } },
},
orderBy: { createdAt: 'desc' },
});
}
async findById(tenantId: string, id: string) {
const db = this.prisma.forTenant(tenantId);
const ticket = await db.ticket.findFirst({
where: { id },
include: {
client: true,
assignee: { select: { id: true, firstName: true, lastName: true } },
createdBy: { select: { id: true, firstName: true, lastName: true } },
},
});
if (!ticket) {
throw new NotFoundException('Ticket not found');
}
return ticket;
}
async create(tenantId: string, createdById: string, dto: CreateTicketDto) {
return this.prisma.ticket.create({
data: {
tenantId,
clientId: dto.clientId,
createdById,
assigneeId: dto.assigneeId,
type: dto.type,
title: dto.title,
description: dto.description,
priority: dto.priority ?? 'normal',
},
});
}
async createSystemTicket(
tenantId: string,
systemUserId: string,
data: {
clientId: string;
type: string;
title: string;
description?: string;
},
) {
return this.prisma.ticket.create({
data: {
tenantId,
clientId: data.clientId,
createdById: systemUserId,
type: data.type,
title: data.title,
description: data.description,
priority: 'high',
},
});
}
async update(tenantId: string, id: string, dto: UpdateTicketDto) {
const db = this.prisma.forTenant(tenantId);
const existing = await db.ticket.findFirst({ where: { id } });
if (!existing) {
throw new NotFoundException('Ticket not found');
}
return this.prisma.ticket.update({
where: { id },
data: {
...(dto.assigneeId !== undefined && { assigneeId: dto.assigneeId }),
...(dto.title && { title: dto.title }),
...(dto.description !== undefined && { description: dto.description }),
...(dto.priority && { priority: dto.priority }),
...(dto.status && { status: dto.status }),
},
}).then(async (ticket) => {
// Notify on status change
if (dto.status) {
this.notificationService.create(tenantId, {
type: 'in_app',
channel: 'ticket_update',
title: 'Ticket Updated',
message: `Ticket "${existing.title}" status changed to ${dto.status.replace(/_/g, ' ')}`,
}).catch(() => {});
}
return ticket;
});
}
async resolve(tenantId: string, id: string, resolvedById: string, coords?: { latitude?: number; longitude?: number }) {
const db = this.prisma.forTenant(tenantId);
const ticket = await db.ticket.findFirst({ where: { id } });
if (!ticket) {
throw new NotFoundException('Ticket not found');
}
if (ticket.status === 'resolved') {
throw new BadRequestException('Ticket is already resolved');
}
if (ticket.status === 'cancelled') {
throw new BadRequestException('Cannot resolve a cancelled ticket');
}
const resolved = await this.prisma.ticket.update({
where: { id },
data: {
status: 'resolved',
resolvedAt: new Date(),
assigneeId: resolvedById,
},
});
// Update client coordinates if this is an installation ticket with coords
if (coords?.latitude !== undefined && coords?.longitude !== undefined && ticket.clientId && ticket.type === 'installation') {
await this.prisma.client.update({
where: { id: ticket.clientId },
data: { latitude: coords.latitude, longitude: coords.longitude },
});
}
// Fire event for workflow automation
if (this.onTicketResolved && ticket.clientId) {
await this.onTicketResolved({
ticketId: id,
tenantId,
clientId: ticket.clientId,
type: ticket.type,
});
}
// Notify on ticket resolution
this.notificationService.create(tenantId, {
type: 'in_app',
channel: 'ticket_update',
title: 'Ticket Resolved',
message: `Ticket "${ticket.title}" has been resolved`,
}).catch(() => {});
return resolved;
}
}