fix(folders): logging, files-rescue, hard-delete wiring, audit logs

- A6: logger import + warn calls in document-folders.service.ts
- G-C1: re-parent files (not just documents) in deleteFolderSoftRescue
- A4: importer sets files.folder_id (was only setting documents.folder_id)
- A7 + G-C3: demote system folder + nullify scratchpadNotes in client-hard-delete
- Defense-in-depth portId on folder-move UPDATE
- Audit logs for createFolder, syncEntityFolderName, archive/restore suffix
- portId in companies/yachts archive log context
- Row-count telemetry in backfill CLI

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-11 13:57:42 +02:00
parent b5ebed9c36
commit 955911302b
8 changed files with 213 additions and 25 deletions

View File

@@ -39,11 +39,13 @@ import { files, documents, formSubmissions } from '@/lib/db/schema/documents';
import { documentSends } from '@/lib/db/schema/brochures';
import { emailThreads } from '@/lib/db/schema/email';
import { reminders } from '@/lib/db/schema/operations';
import { scratchpadNotes } from '@/lib/db/schema/system';
import { user as authUser } from '@/lib/db/schema/users';
import { redis } from '@/lib/redis';
import { sendEmail } from '@/lib/email';
import { logger } from '@/lib/logger';
import { createAuditLog, type AuditMeta } from '@/lib/audit';
import { demoteSystemFolderOnEntityDelete } from '@/lib/services/document-folders.service';
import { ConflictError, NotFoundError, ValidationError } from '@/lib/errors';
const CODE_TTL_SECONDS = 10 * 60;
@@ -203,6 +205,14 @@ export async function hardDeleteClient(args: {
.update(documentSends)
.set({ clientId: null })
.where(eq(documentSends.clientId, args.clientId));
// G-C2: scratchpad_notes.linked_client_id is RESTRICT (default for no
// onDelete clause). Any rep who linked a scratchpad note to this client
// would otherwise throw an FK violation when we try to delete the
// client row below. Nullify so the note survives the hard-delete.
await tx
.update(scratchpadNotes)
.set({ linkedClientId: null })
.where(eq(scratchpadNotes.linkedClientId, args.clientId));
// client_merge_log.surviving_client_id has no cascade and is
// notNull → must be deleted explicitly. Merged records remain in
@@ -218,6 +228,18 @@ export async function hardDeleteClient(args: {
await tx.delete(clients).where(eq(clients.id, args.clientId));
});
// G-C3 / A7: demote the system-managed folder so the partial unique
// index `uniq_document_folders_entity` releases its slot. Done as a
// post-commit fire-and-forget — folder hygiene is non-essential to the
// delete being durable, and we don't want a folder-table glitch to
// un-delete the client by aborting the outer transaction.
void demoteSystemFolderOnEntityDelete(args.portId, 'client', args.clientId).catch((err) => {
logger.error(
{ err, clientId: args.clientId, portId: args.portId },
'hardDeleteClient: failed to demote system folder',
);
});
void createAuditLog({
portId: args.portId,
userId: args.requesterUserId,