fix: resolve TypeORM cascade bug in ticket reply/close/updateStatus

- Replace ticketsRepo.save(ticket) with ticketsRepo.update() to avoid
  cascade orphaning existing messages (SET ticketId=null violates NOT NULL)
- Fix reply(), close() in tickets.service.ts
- Fix updateStatus() in tickets.controller.ts using new updateTicketFields() method
- Add updateTicketFields() helper to TicketsService for safe partial updates
This commit is contained in:
keyhan
2026-04-06 13:34:48 +03:30
parent 3ab647be2e
commit a1dbd0a85f
2 changed files with 23 additions and 18 deletions
+6 -4
View File
@@ -107,11 +107,13 @@ export class TicketsController {
@Request() req: any, @Request() req: any,
@Body() dto: UpdateTicketStatusDto, @Body() dto: UpdateTicketStatusDto,
) { ) {
const ticket = await this.ticketsService.findOne(id, req.user.id, req.user.role); // Access check
ticket.status = dto.status; await this.ticketsService.findOne(id, req.user.id, req.user.role);
const updateData: any = { status: dto.status };
if (dto.status === TicketStatus.CLOSED) { if (dto.status === TicketStatus.CLOSED) {
ticket.closedAt = new Date(); updateData.closedAt = new Date();
} }
return this.ticketsService['ticketsRepo'].save(ticket); await this.ticketsService.updateTicketFields(id, updateData);
return this.ticketsService.findOne(id, req.user.id, req.user.role);
} }
} }
+17 -14
View File
@@ -85,24 +85,20 @@ export class TicketsService {
// ─── Reply to ticket ─── // ─── Reply to ticket ───
async reply(ticketId: string, userId: string, userRole: UserRole, dto: ReplyTicketDto): Promise<TicketMessage> { async reply(ticketId: string, userId: string, userRole: UserRole, dto: ReplyTicketDto): Promise<TicketMessage> {
const ticket = await this.findOne(ticketId, userId, userRole); // Verify access first
await this.findOne(ticketId, userId, userRole);
const msg = this.messagesRepo.create({ const msg = this.messagesRepo.create({
ticketId: ticket.id, ticketId,
senderId: userId, senderId: userId,
senderRole: userRole, senderRole: userRole,
message: dto.message, message: dto.message,
}); });
const saved = await this.messagesRepo.save(msg); const saved = await this.messagesRepo.save(msg);
// Update ticket status based on who replied // Update ticket status directly (avoid cascade issues with loaded relations)
if (userRole === UserRole.USER) { const newStatus = userRole === UserRole.USER ? TicketStatus.WAITING : TicketStatus.ANSWERED;
ticket.status = TicketStatus.WAITING; await this.ticketsRepo.update(ticketId, { status: newStatus });
} else {
// Staff or admin replied
ticket.status = TicketStatus.ANSWERED;
}
await this.ticketsRepo.save(ticket);
this.logger.log(`Reply on ticket ${ticketId} by ${userRole} user ${userId}`); this.logger.log(`Reply on ticket ${ticketId} by ${userRole} user ${userId}`);
return saved; return saved;
@@ -110,10 +106,17 @@ export class TicketsService {
// ─── Close ticket ─── // ─── Close ticket ───
async close(ticketId: string, userId: string, userRole: UserRole): Promise<Ticket> { async close(ticketId: string, userId: string, userRole: UserRole): Promise<Ticket> {
const ticket = await this.findOne(ticketId, userId, userRole); await this.findOne(ticketId, userId, userRole);
ticket.status = TicketStatus.CLOSED; await this.ticketsRepo.update(ticketId, {
ticket.closedAt = new Date(); status: TicketStatus.CLOSED,
return this.ticketsRepo.save(ticket); closedAt: new Date(),
});
return this.findOne(ticketId, userId, userRole);
}
// ─── Update ticket fields (safe, no cascade) ───
async updateTicketFields(ticketId: string, data: Partial<Ticket>): Promise<void> {
await this.ticketsRepo.update(ticketId, data);
} }
// ─── Staff: List department tickets ─── // ─── Staff: List department tickets ───