From a1dbd0a85f7c0a94a3785ebe6a61bff5d0b2e5f7 Mon Sep 17 00:00:00 2001 From: keyhan Date: Mon, 6 Apr 2026 13:34:48 +0330 Subject: [PATCH] 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 --- backend/src/tickets/tickets.controller.ts | 10 +++++--- backend/src/tickets/tickets.service.ts | 31 +++++++++++++---------- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/backend/src/tickets/tickets.controller.ts b/backend/src/tickets/tickets.controller.ts index 1666f9d..05e2ffc 100644 --- a/backend/src/tickets/tickets.controller.ts +++ b/backend/src/tickets/tickets.controller.ts @@ -107,11 +107,13 @@ export class TicketsController { @Request() req: any, @Body() dto: UpdateTicketStatusDto, ) { - const ticket = await this.ticketsService.findOne(id, req.user.id, req.user.role); - ticket.status = dto.status; + // Access check + await this.ticketsService.findOne(id, req.user.id, req.user.role); + const updateData: any = { status: dto.status }; 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); } } diff --git a/backend/src/tickets/tickets.service.ts b/backend/src/tickets/tickets.service.ts index 492aa11..59a1b1c 100644 --- a/backend/src/tickets/tickets.service.ts +++ b/backend/src/tickets/tickets.service.ts @@ -85,24 +85,20 @@ export class TicketsService { // ─── Reply to ticket ─── async reply(ticketId: string, userId: string, userRole: UserRole, dto: ReplyTicketDto): Promise { - const ticket = await this.findOne(ticketId, userId, userRole); + // Verify access first + await this.findOne(ticketId, userId, userRole); const msg = this.messagesRepo.create({ - ticketId: ticket.id, + ticketId, senderId: userId, senderRole: userRole, message: dto.message, }); const saved = await this.messagesRepo.save(msg); - // Update ticket status based on who replied - if (userRole === UserRole.USER) { - ticket.status = TicketStatus.WAITING; - } else { - // Staff or admin replied - ticket.status = TicketStatus.ANSWERED; - } - await this.ticketsRepo.save(ticket); + // Update ticket status directly (avoid cascade issues with loaded relations) + const newStatus = userRole === UserRole.USER ? TicketStatus.WAITING : TicketStatus.ANSWERED; + await this.ticketsRepo.update(ticketId, { status: newStatus }); this.logger.log(`Reply on ticket ${ticketId} by ${userRole} user ${userId}`); return saved; @@ -110,10 +106,17 @@ export class TicketsService { // ─── Close ticket ─── async close(ticketId: string, userId: string, userRole: UserRole): Promise { - const ticket = await this.findOne(ticketId, userId, userRole); - ticket.status = TicketStatus.CLOSED; - ticket.closedAt = new Date(); - return this.ticketsRepo.save(ticket); + await this.findOne(ticketId, userId, userRole); + await this.ticketsRepo.update(ticketId, { + status: TicketStatus.CLOSED, + closedAt: new Date(), + }); + return this.findOne(ticketId, userId, userRole); + } + + // ─── Update ticket fields (safe, no cascade) ─── + async updateTicketFields(ticketId: string, data: Partial): Promise { + await this.ticketsRepo.update(ticketId, data); } // ─── Staff: List department tickets ───