Prevent duplicate notes when re-evaluating automation rules

Add appendNoteIfNotPresent helper to check if rule note already exists
in transaction notes before appending, preventing duplicate concatenation
when rules are re-evaluated multiple times.
This commit is contained in:
Víctor Falcón 2025-12-04 16:42:54 +01:00
parent d04b6a0174
commit f6f1cba041
5 changed files with 65 additions and 27 deletions

View File

@ -23,6 +23,7 @@ import { useEncryptionKey } from '@/contexts/encryption-key-context';
import { decrypt, encrypt, importKey } from '@/lib/crypto';
import { getStoredKey } from '@/lib/key-storage';
import { evaluateRulesForNewTransaction } from '@/lib/rule-engine';
import { appendNoteIfNotPresent } from '@/lib/utils';
import { automationRuleSyncService } from '@/services/automation-rule-sync';
import { transactionSyncService } from '@/services/transaction-sync';
import {
@ -198,11 +199,10 @@ export function EditTransactionDialog({
result.noteIv,
);
if (finalNotes) {
finalNotes = `${finalNotes}\n${decryptedRuleNote}`;
} else {
finalNotes = decryptedRuleNote;
}
finalNotes = appendNoteIfNotPresent(
finalNotes || undefined,
decryptedRuleNote,
);
}
}

View File

@ -76,7 +76,12 @@ function formatMonth(yearMonth: string): string {
const isCurrentYear = date.getFullYear() === new Date().getFullYear();
return date.toLocaleDateString('en-US', isCurrentYear ? { month: 'short' } : { year: '2-digit', month: 'short' });
return date.toLocaleDateString(
'en-US',
isCurrentYear
? { month: 'short' }
: { year: '2-digit', month: 'short' },
);
}
export function useDashboardData(): DashboardData {

View File

@ -1,6 +1,7 @@
import { decrypt, encrypt, importKey } from '@/lib/crypto';
import { getStoredKey } from '@/lib/key-storage';
import { evaluateRules } from '@/lib/rule-engine';
import { appendNoteIfNotPresent } from '@/lib/utils';
import { automationRuleSyncService } from '@/services/automation-rule-sync';
import { transactionSyncService } from '@/services/transaction-sync';
import type { Account, Bank } from '@/types/account';
@ -77,17 +78,23 @@ export function useReEvaluateAllTransactions() {
let finalNotesIv = transaction.notes_iv;
if (result.note && result.noteIv) {
if (transaction.decryptedNotes) {
const combinedNote = `${transaction.decryptedNotes}\n${await decrypt(result.note, key, result.noteIv)}`;
const decryptedRuleNote = await decrypt(
result.note,
key,
result.noteIv,
);
const combinedNote = appendNoteIfNotPresent(
transaction.decryptedNotes,
decryptedRuleNote,
);
if (combinedNote !== transaction.decryptedNotes) {
const encrypted = await encrypt(
combinedNote,
key,
);
finalNotes = encrypted.encrypted;
finalNotesIv = encrypted.iv;
} else {
finalNotes = result.note;
finalNotesIv = result.noteIv;
}
}

View File

@ -16,3 +16,18 @@ export function isSameUrl(
export function resolveUrl(url: NonNullable<InertiaLinkProps['href']>): string {
return typeof url === 'string' ? url : url.url;
}
export function appendNoteIfNotPresent(
existingNotes: string | undefined,
newNote: string,
): string {
if (!existingNotes) {
return newNote;
}
if (existingNotes.includes(newNote)) {
return existingNotes;
}
return `${existingNotes}\n${newNote}`;
}

View File

@ -55,6 +55,7 @@ import { consoleDebug } from '@/lib/debug';
import { db } from '@/lib/dexie-db';
import { getStoredKey } from '@/lib/key-storage';
import { evaluateRules } from '@/lib/rule-engine';
import { appendNoteIfNotPresent } from '@/lib/utils';
import { automationRuleSyncService } from '@/services/automation-rule-sync';
import { transactionSyncService } from '@/services/transaction-sync';
import { type BreadcrumbItem } from '@/types';
@ -600,18 +601,23 @@ export default function Transactions({ categories, accounts, banks }: Props) {
if (result.note && result.noteIv) {
consoleDebug('Adding note from rule');
if (transaction.decryptedNotes) {
const combinedNote = `${transaction.decryptedNotes}\n${await decrypt(result.note, key, result.noteIv)}`;
const decryptedRuleNote = await decrypt(
result.note,
key,
result.noteIv,
);
const combinedNote = appendNoteIfNotPresent(
transaction.decryptedNotes,
decryptedRuleNote,
);
if (combinedNote !== transaction.decryptedNotes) {
const encrypted = await encrypt(combinedNote, key);
finalNotes = encrypted.encrypted;
finalNotesIv = encrypted.iv;
consoleDebug(
'Combined existing notes with rule note',
);
consoleDebug('Combined notes with rule note');
} else {
finalNotes = result.note;
finalNotesIv = result.noteIv;
consoleDebug('Set rule note as new note');
consoleDebug('Rule note already present, skipping');
}
}
@ -744,18 +750,23 @@ export default function Transactions({ categories, accounts, banks }: Props) {
if (result.note && result.noteIv) {
consoleDebug('Adding note from rule');
if (transaction.decryptedNotes) {
const combinedNote = `${transaction.decryptedNotes}\n${await decrypt(result.note, key, result.noteIv)}`;
const decryptedRuleNote = await decrypt(
result.note,
key,
result.noteIv,
);
const combinedNote = appendNoteIfNotPresent(
transaction.decryptedNotes,
decryptedRuleNote,
);
if (combinedNote !== transaction.decryptedNotes) {
const encrypted = await encrypt(combinedNote, key);
finalNotes = encrypted.encrypted;
finalNotesIv = encrypted.iv;
consoleDebug(
'Combined existing notes with rule note',
);
consoleDebug('Combined notes with rule note');
} else {
finalNotes = result.note;
finalNotesIv = result.noteIv;
consoleDebug('Set rule note as new note');
consoleDebug('Rule note already present, skipping');
}
}