fix(attachments): limpar arquivos órfãos no S3 em deleções e reset

Três caminhos de deleção não chamavam o cleanup de storage, deixando
arquivos órfãos no S3:

- deleteTransactionBulkAction: deleções por escopo de série (período,
  futuras, todas) agora coletam attachments vinculados antes do delete
  e disparam cleanupAttachmentsAfterTransactionDelete
- deleteMultipleTransactionsAction: mesma correção para seleção
  múltipla de lançamentos
- resetUserAppData: reset de conta em Ajustes coleta os fileKeys
  antes de truncar e remove os objetos do S3 em paralelo

Também ajusta deleteS3Object para ignorar NoSuchKey silenciosamente,
necessário para providers S3-compatíveis como Cloudflare R2 que não
são idempotentes nessa operação.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Felipe Coutinho
2026-04-25 14:45:45 +00:00
parent b14f487824
commit 7f05d2a681
3 changed files with 91 additions and 58 deletions

View File

@@ -18,6 +18,7 @@ import {
} from "@/shared/lib/payers/constants";
import { getAdminPayerId } from "@/shared/lib/payers/get-admin-id";
import { normalizeNameFromEmail } from "@/shared/lib/payers/utils";
import { deleteS3Object } from "@/shared/lib/storage/presign";
type ActionResponse<T = void> = {
success: boolean;
@@ -85,6 +86,11 @@ async function resetUserAppData(
const avatarUrl = user.image ?? DEFAULT_PAYER_AVATAR;
const defaultPayerStatus = PAYER_STATUS_OPTIONS[0];
const userAttachments = await db
.select({ id: schema.attachments.id, fileKey: schema.attachments.fileKey })
.from(schema.attachments)
.where(eq(schema.attachments.userId, userId));
await db.transaction(async (tx: typeof db) => {
await tx
.delete(schema.payerShares)
@@ -115,6 +121,9 @@ async function resetUserAppData(
await tx
.delete(schema.transactions)
.where(eq(schema.transactions.userId, userId));
await tx
.delete(schema.attachments)
.where(eq(schema.attachments.userId, userId));
await tx.delete(schema.invoices).where(eq(schema.invoices.userId, userId));
await tx.delete(schema.cards).where(eq(schema.cards.userId, userId));
await tx
@@ -147,6 +156,14 @@ async function resetUserAppData(
userId,
});
});
await Promise.all(
userAttachments.map((att) =>
deleteS3Object(att.fileKey).catch((err) => {
console.error("Falha ao remover anexo do S3 no reset:", err);
}),
),
);
}
// Actions