import { createHash } from 'node:crypto'; import type { ArtifactDeletionRequest, AssessorRepository } from '../../src/persistence/assessor-repository'; import { RepositoryConflictError, RepositoryNotFoundError, RepositoryValidationError } from '../../src/persistence/repository-errors'; import { createGpoReprocessingJob } from './gpo-backup-submission-service'; import type { BackendArtifactStore } from '../artifacts/backend-artifact-store'; function stableDeletionId(storageKey: string): string { return `artifact-delete-${createHash('sha256').update(storageKey).digest('hex').slice(0, 32)}`; } export class GpoBackupManagementService { constructor( private readonly repository: AssessorRepository, private readonly artifactStore: BackendArtifactStore, private readonly now: () => Date = () => new Date() ) {} async retry(projectId: string, artifactId: string) { const jobs = (await this.repository.listIngestionJobs(projectId)) .filter(item => item.artifactId === artifactId && item.kind === 'analyze-gpo-backup') .sort((left, right) => Date.parse(right.updatedAt) - Date.parse(left.updatedAt) || right.id.localeCompare(left.id)); if (!jobs[0]) throw new RepositoryNotFoundError('Der GPO-Analysejob ist im Mandanten nicht vorhanden.'); const job = await this.repository.retryIngestionJob(projectId, jobs[0].id, this.now().toISOString()); return { version: 1 as const, artifactId, jobId: job.id, status: job.status, attempts: job.attempts }; } async reprocess(projectId: string, artifactId: string, expectedLatestProcessingRunId: string) { if (!expectedLatestProcessingRunId.trim()) throw new RepositoryValidationError('Der erwartete letzte Processing Run ist erforderlich.'); const artifact = (await this.repository.listImportArtifacts(projectId)).find(item => item.id === artifactId); if (!artifact || artifact.storage.kind !== 'backend-controlled') throw new RepositoryNotFoundError('Das GPO-Artefakt ist im Mandanten nicht vorhanden.'); const runs = (await this.repository.listProcessingRuns(artifactId)) .filter(item => item.projectId === projectId) .sort((left, right) => Date.parse(right.completedAt) - Date.parse(left.completedAt) || right.id.localeCompare(left.id)); const latest = runs[0]; if (!latest) throw new RepositoryNotFoundError('Für das GPO-Artefakt ist noch kein Processing Run vorhanden.'); if (latest.id !== expectedLatestProcessingRunId) { throw new RepositoryConflictError('Der erwartete letzte Processing Run ist nicht mehr aktuell.'); } const job = await this.repository.enqueueIngestionJob(createGpoReprocessingJob(artifact, latest.id, this.now().toISOString())); return { version: 1 as const, artifactId, jobId: job.id, status: job.status, supersedesProcessingRunId: latest.id }; } async delete(projectId: string, artifactId: string) { const artifact = (await this.repository.listImportArtifacts(projectId)).find(item => item.id === artifactId); if (!artifact || artifact.storage.kind !== 'backend-controlled') throw new RepositoryNotFoundError('Das GPO-Artefakt ist im Mandanten nicht vorhanden.'); const timestamp = this.now().toISOString(); const request: ArtifactDeletionRequest = { version: 1, id: stableDeletionId(artifact.storage.storageKey), projectId, artifactId, storageKey: artifact.storage.storageKey, attempts: 0, nextAttemptAt: timestamp, createdAt: timestamp }; const deleted = await this.repository.deleteGpoArtifact(projectId, artifactId, request); const storageRemoval = await processArtifactDeletionRequest(this.repository, this.artifactStore, deleted.request, this.now); return { version: 1 as const, artifactId, deleted: true as const, storageRemoval }; } } export async function processArtifactDeletionRequest( repository: AssessorRepository, artifactStore: BackendArtifactStore, request: ArtifactDeletionRequest, now: () => Date = () => new Date() ): Promise<'complete' | 'pending'> { try { await artifactStore.remove(request.storageKey); await repository.completeArtifactDeletionRequest(request.id); return 'complete'; } catch { const failedAt = now(); const retryAt = new Date(failedAt.getTime() + Math.min(60 * 60 * 1_000, 5_000 * 2 ** Math.min(request.attempts, 8))); await repository.failArtifactDeletionRequest(request.id, failedAt.toISOString(), 'artifact-storage-delete-failed', retryAt.toISOString()) .catch(() => undefined); return 'pending'; } }