From d083008001c9ea2113ebdebdb3fdac0a95480925 Mon Sep 17 00:00:00 2001 From: hailin Date: Fri, 6 Feb 2026 20:49:03 -0800 Subject: [PATCH] fix(conversations): implement soft-delete for conversation deletion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The delete conversation endpoint was a no-op — it verified ownership but never actually modified the record. Users saw conversations disappear (frontend optimistic removal) but they reappeared on refresh. Changes: - conversation.entity.ts: Add DELETED status, softDelete() and isDeleted() - conversation.service.ts: Call softDelete() + update instead of no-op - conversation-postgres.repository.ts: Exclude DELETED conversations from findByUserId() queries so they don't appear in user's list Co-Authored-By: Claude Opus 4.6 --- .../persistence/conversation-postgres.repository.ts | 3 ++- .../src/application/services/conversation.service.ts | 8 +++----- .../src/domain/entities/conversation.entity.ts | 10 ++++++++++ 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/services/conversation-service/src/adapters/outbound/persistence/conversation-postgres.repository.ts b/packages/services/conversation-service/src/adapters/outbound/persistence/conversation-postgres.repository.ts index da65055..0bc45f6 100644 --- a/packages/services/conversation-service/src/adapters/outbound/persistence/conversation-postgres.repository.ts +++ b/packages/services/conversation-service/src/adapters/outbound/persistence/conversation-postgres.repository.ts @@ -38,7 +38,8 @@ export class ConversationPostgresRepository options?: { status?: ConversationStatusType; limit?: number }, ): Promise { const queryBuilder = this.createTenantQueryBuilder('conversation') - .andWhere('conversation.user_id = :userId', { userId }); + .andWhere('conversation.user_id = :userId', { userId }) + .andWhere('conversation.status != :deletedStatus', { deletedStatus: 'DELETED' }); if (options?.status) { queryBuilder.andWhere('conversation.status = :status', { status: options.status }); diff --git a/packages/services/conversation-service/src/application/services/conversation.service.ts b/packages/services/conversation-service/src/application/services/conversation.service.ts index 55d6cf6..c51d6bb 100644 --- a/packages/services/conversation-service/src/application/services/conversation.service.ts +++ b/packages/services/conversation-service/src/application/services/conversation.service.ts @@ -308,11 +308,9 @@ export class ConversationService { * Delete a conversation and its messages */ async deleteConversation(conversationId: string, userId: string): Promise { - // Verify user owns the conversation - await this.getConversation(conversationId, userId); - - // Note: In a real application, you'd want to delete messages in the repository - // For now, we rely on database cascade or separate cleanup + const conversation = await this.getConversation(conversationId, userId); + conversation.softDelete(); + await this.conversationRepo.update(conversation); } /** diff --git a/packages/services/conversation-service/src/domain/entities/conversation.entity.ts b/packages/services/conversation-service/src/domain/entities/conversation.entity.ts index e85e439..bf5626d 100644 --- a/packages/services/conversation-service/src/domain/entities/conversation.entity.ts +++ b/packages/services/conversation-service/src/domain/entities/conversation.entity.ts @@ -5,6 +5,7 @@ export const ConversationStatus = { ACTIVE: 'ACTIVE', ENDED: 'ENDED', ARCHIVED: 'ARCHIVED', + DELETED: 'DELETED', } as const; export type ConversationStatusType = @@ -236,6 +237,11 @@ export class ConversationEntity { this.updatedAt = new Date(); } + softDelete(): void { + this.status = ConversationStatus.DELETED; + this.updatedAt = new Date(); + } + isActive(): boolean { return this.status === ConversationStatus.ACTIVE; } @@ -243,4 +249,8 @@ export class ConversationEntity { isEnded(): boolean { return this.status === ConversationStatus.ENDED; } + + isDeleted(): boolean { + return this.status === ConversationStatus.DELETED; + } }