diff --git a/src/features/settings/actions.ts b/src/features/settings/actions.ts index 5b0eec6..c9fff4c 100644 --- a/src/features/settings/actions.ts +++ b/src/features/settings/actions.ts @@ -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 = { 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 diff --git a/src/features/transactions/actions/bulk-actions.ts b/src/features/transactions/actions/bulk-actions.ts index 5bb44b0..79120cd 100644 --- a/src/features/transactions/actions/bulk-actions.ts +++ b/src/features/transactions/actions/bulk-actions.ts @@ -1,7 +1,7 @@ "use server"; import { and, asc, eq, inArray, isNull, sql } from "drizzle-orm"; -import { transactions } from "@/db/schema"; +import { attachments, transactionAttachments, transactions } from "@/db/schema"; import { PAYMENT_METHODS, TRANSACTION_CONDITIONS, @@ -17,6 +17,7 @@ import { import type { ActionResult } from "@/shared/lib/types/actions"; import { addMonthsToDate, parseLocalDateString } from "@/shared/utils/date"; import { addMonthsToPeriod, parsePeriod } from "@/shared/utils/period"; +import { cleanupAttachmentsAfterTransactionDelete } from "./attachments"; import { centsToDecimalString, type DeleteBulkInput, @@ -78,71 +79,64 @@ export async function deleteTransactionBulkAction( }; } + let scopeFilter: ReturnType; + let successMessage: string; + if (data.scope === "current") { - await db - .delete(transactions) - .where( - and(eq(transactions.id, data.id), eq(transactions.userId, user.id)), - ); - - revalidate(user.id); - return { success: true, message: "Lançamento removido com sucesso." }; + scopeFilter = eq(transactions.id, data.id); + successMessage = "Lançamento removido com sucesso."; + } else if (data.scope === "period") { + scopeFilter = and( + eq(transactions.seriesId, existing.seriesId), + eq(transactions.period, existing.period ?? ""), + ); + successMessage = "Todos os lançamentos do período foram removidos."; + } else if (data.scope === "future") { + scopeFilter = and( + eq(transactions.seriesId, existing.seriesId), + sql`${transactions.period} >= ${existing.period}`, + ); + successMessage = "Lançamentos removidos com sucesso."; + } else if (data.scope === "all") { + scopeFilter = eq(transactions.seriesId, existing.seriesId); + successMessage = "Todos os lançamentos da série foram removidos."; + } else { + return { success: false, error: "Escopo de ação inválido." }; } - if (data.scope === "period") { - await db - .delete(transactions) - .where( - and( - eq(transactions.seriesId, existing.seriesId), - eq(transactions.userId, user.id), - eq(transactions.period, existing.period ?? ""), - ), - ); + const targetRows = await db + .select({ id: transactions.id }) + .from(transactions) + .where(and(scopeFilter, eq(transactions.userId, user.id))); - revalidate(user.id); - return { - success: true, - message: "Todos os lançamentos do período foram removidos.", - }; + const targetIds = targetRows.map((r) => r.id); + + if (targetIds.length === 0) { + return { success: false, error: "Nenhum lançamento encontrado." }; } - if (data.scope === "future") { - await db - .delete(transactions) - .where( - and( - eq(transactions.seriesId, existing.seriesId), - eq(transactions.userId, user.id), - sql`${transactions.period} >= ${existing.period}`, - ), - ); + const linkedAttachments = await db + .select({ id: attachments.id, fileKey: attachments.fileKey }) + .from(transactionAttachments) + .innerJoin( + attachments, + eq(transactionAttachments.attachmentId, attachments.id), + ) + .where(inArray(transactionAttachments.transactionId, targetIds)); - revalidate(user.id); - return { - success: true, - message: "Lançamentos removidos com sucesso.", - }; - } + await db + .delete(transactions) + .where( + and( + inArray(transactions.id, targetIds), + eq(transactions.userId, user.id), + ), + ); - if (data.scope === "all") { - await db - .delete(transactions) - .where( - and( - eq(transactions.seriesId, existing.seriesId), - eq(transactions.userId, user.id), - ), - ); + await cleanupAttachmentsAfterTransactionDelete(linkedAttachments); - revalidate(user.id); - return { - success: true, - message: "Todos os lançamentos da série foram removidos.", - }; - } - - return { success: false, error: "Escopo de ação inválido." }; + revalidate(user.id); + return { success: true, message: successMessage }; } catch (error) { return handleActionError(error); } @@ -759,6 +753,15 @@ export async function deleteMultipleTransactionsAction( return { success: false, error: "Nenhum lançamento encontrado." }; } + const linkedAttachments = await db + .select({ id: attachments.id, fileKey: attachments.fileKey }) + .from(transactionAttachments) + .innerJoin( + attachments, + eq(transactionAttachments.attachmentId, attachments.id), + ) + .where(inArray(transactionAttachments.transactionId, data.ids)); + await db .delete(transactions) .where( @@ -768,6 +771,8 @@ export async function deleteMultipleTransactionsAction( ), ); + await cleanupAttachmentsAfterTransactionDelete(linkedAttachments); + const notificationData = existing .filter( ( diff --git a/src/shared/lib/storage/presign.ts b/src/shared/lib/storage/presign.ts index 08b6abd..6c67fec 100644 --- a/src/shared/lib/storage/presign.ts +++ b/src/shared/lib/storage/presign.ts @@ -48,5 +48,16 @@ export async function deleteS3Object(fileKey: string): Promise { Bucket: S3_BUCKET, Key: fileKey, }); - await s3.send(command); + try { + await s3.send(command); + } catch (err) { + if ( + err instanceof Error && + "Code" in err && + (err as { Code: string }).Code === "NoSuchKey" + ) { + return; + } + throw err; + } }