From b38e5aecf9642aa30f4da9e72c3302e7dd8f2e59 Mon Sep 17 00:00:00 2001 From: Till JS Date: Fri, 20 Mar 2026 20:50:44 +0100 Subject: [PATCH] fix(contacts): clean up orphaned photos on duplicate merge DuplicatesService.mergeContacts() now deletes S3 photos of merged contacts before removing them from the DB. Photos that were adopted by the primary contact (via mergeContactData) are preserved. - Import PhotoModule in DuplicatesModule - Inject PhotoService into DuplicatesService - Add photo cleanup loop before DB deletion - Update test mock Co-Authored-By: Claude Opus 4.6 (1M context) --- .../duplicates/__tests__/duplicates.service.spec.ts | 5 +++++ .../backend/src/duplicates/duplicates.module.ts | 3 ++- .../backend/src/duplicates/duplicates.service.ts | 13 ++++++++++++- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/apps/contacts/apps/backend/src/duplicates/__tests__/duplicates.service.spec.ts b/apps/contacts/apps/backend/src/duplicates/__tests__/duplicates.service.spec.ts index f456c180e..06d7793a9 100644 --- a/apps/contacts/apps/backend/src/duplicates/__tests__/duplicates.service.spec.ts +++ b/apps/contacts/apps/backend/src/duplicates/__tests__/duplicates.service.spec.ts @@ -1,6 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { NotFoundException } from '@nestjs/common'; import { DuplicatesService } from '../duplicates.service'; +import { PhotoService } from '../../photo/photo.service'; import { DATABASE_CONNECTION } from '../../db/database.module'; describe('DuplicatesService', () => { @@ -110,6 +111,10 @@ describe('DuplicatesService', () => { provide: DATABASE_CONNECTION, useValue: mockDb, }, + { + provide: PhotoService, + useValue: { deletePhotoByUrl: jest.fn() }, + }, ], }).compile(); diff --git a/apps/contacts/apps/backend/src/duplicates/duplicates.module.ts b/apps/contacts/apps/backend/src/duplicates/duplicates.module.ts index c227a7580..6938aec09 100644 --- a/apps/contacts/apps/backend/src/duplicates/duplicates.module.ts +++ b/apps/contacts/apps/backend/src/duplicates/duplicates.module.ts @@ -2,9 +2,10 @@ import { Module } from '@nestjs/common'; import { DuplicatesController } from './duplicates.controller'; import { DuplicatesService } from './duplicates.service'; import { DatabaseModule } from '../db/database.module'; +import { PhotoModule } from '../photo/photo.module'; @Module({ - imports: [DatabaseModule], + imports: [DatabaseModule, PhotoModule], controllers: [DuplicatesController], providers: [DuplicatesService], exports: [DuplicatesService], diff --git a/apps/contacts/apps/backend/src/duplicates/duplicates.service.ts b/apps/contacts/apps/backend/src/duplicates/duplicates.service.ts index 7e4381d55..5de51cbbb 100644 --- a/apps/contacts/apps/backend/src/duplicates/duplicates.service.ts +++ b/apps/contacts/apps/backend/src/duplicates/duplicates.service.ts @@ -4,6 +4,7 @@ import { DATABASE_CONNECTION } from '../db/database.module'; import { Database } from '../db/connection'; import { contacts } from '../db/schema'; import type { Contact } from '../db/schema'; +import { PhotoService } from '../photo/photo.service'; export interface DuplicateGroup { id: string; @@ -19,7 +20,10 @@ export interface MergeResult { @Injectable() export class DuplicatesService { - constructor(@Inject(DATABASE_CONNECTION) private db: Database) {} + constructor( + @Inject(DATABASE_CONNECTION) private db: Database, + private readonly photoService: PhotoService + ) {} /** Maximum number of duplicate groups to return per match type */ private static readonly MAX_GROUPS_PER_TYPE = 50; @@ -322,6 +326,13 @@ export class DuplicatesService { .where(eq(contacts.id, primaryId)) .returning(); + // Clean up photos of merged contacts (skip if photo was adopted by primary) + for (const mergedContact of contactsToMerge) { + if (mergedContact.photoUrl && mergedContact.photoUrl !== updatedContact.photoUrl) { + await this.photoService.deletePhotoByUrl(mergedContact.photoUrl); + } + } + // Delete merged contacts await this.db .delete(contacts)