feat: add duplicate transaction detection and display in transactions page, enhancing user experience with visual indicators for duplicates

This commit is contained in:
Julien Froidefond
2025-12-08 09:50:32 +01:00
parent cb8628ce39
commit ba4d112cb8
6 changed files with 223 additions and 55 deletions

View File

@@ -41,14 +41,14 @@ export const transactionService = {
// Create sets for fast lookup
const existingFitIdSet = new Set(
existingByFitId.map((t) => `${t.accountId}-${t.fitId}`),
existingByFitId.map((t) => `${t.accountId}-${t.fitId}`)
);
// Create set for duplicates by amount + date + description
const existingCriteriaSet = new Set(
allExistingTransactions.map(
(t) => `${t.accountId}-${t.date}-${t.amount}-${t.description}`,
),
(t) => `${t.accountId}-${t.date}-${t.amount}-${t.description}`
)
);
// Filter out duplicates based on fitId OR (amount + date + description)
@@ -85,7 +85,7 @@ export const transactionService = {
async update(
id: string,
data: Partial<Omit<Transaction, "id">>,
data: Partial<Omit<Transaction, "id">>
): Promise<Transaction> {
const updated = await prisma.transaction.update({
where: { id },
@@ -123,14 +123,67 @@ export const transactionService = {
await prisma.transaction.delete({
where: { id },
});
} catch (error: any) {
if (error.code === "P2025") {
} catch (error: unknown) {
if (
error &&
typeof error === "object" &&
"code" in error &&
error.code === "P2025"
) {
throw new Error(`Transaction with id ${id} not found`);
}
throw error;
}
},
async getDuplicateIds(): Promise<Set<string>> {
// Get all transactions grouped by account
const allTransactions = await prisma.transaction.findMany({
orderBy: [
{ accountId: "asc" },
{ date: "asc" },
{ createdAt: "asc" }, // Oldest first
],
select: {
id: true,
accountId: true,
date: true,
amount: true,
},
});
// Group by account for efficient processing
const transactionsByAccount = new Map<string, typeof allTransactions>();
for (const transaction of allTransactions) {
if (!transactionsByAccount.has(transaction.accountId)) {
transactionsByAccount.set(transaction.accountId, []);
}
transactionsByAccount.get(transaction.accountId)!.push(transaction);
}
const duplicateIds = new Set<string>();
const seenKeys = new Map<string, string>(); // key -> first transaction ID
// For each account, find duplicates by amount + date only
for (const [accountId, transactions] of transactionsByAccount.entries()) {
for (const transaction of transactions) {
const key = `${accountId}-${transaction.date}-${transaction.amount}`;
if (seenKeys.has(key)) {
// This is a duplicate - mark both the first and this one
const firstId = seenKeys.get(key)!;
duplicateIds.add(firstId);
duplicateIds.add(transaction.id);
} else {
// First occurrence
seenKeys.set(key, transaction.id);
}
}
}
return duplicateIds;
},
async deduplicate(): Promise<{
deletedCount: number;
duplicatesFound: number;