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) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-03-20 20:50:44 +01:00
parent 3ac976f600
commit b38e5aecf9
3 changed files with 19 additions and 2 deletions

View file

@ -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();

View file

@ -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],

View file

@ -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)