feat(import-transactions-drawer): Add json-logic-js dependency and improve import logic
This commit is contained in:
parent
4dcce018e5
commit
1df3bad3c3
6
bun.lock
6
bun.lock
|
|
@ -33,6 +33,7 @@
|
|||
"date-fns": "^4.1.0",
|
||||
"globals": "^15.14.0",
|
||||
"input-otp": "^1.4.2",
|
||||
"json-logic-js": "^2.0.5",
|
||||
"laravel-vite-plugin": "^2.0",
|
||||
"lucide-react": "^0.553.0",
|
||||
"react": "^19.2.0",
|
||||
|
|
@ -49,6 +50,7 @@
|
|||
"devDependencies": {
|
||||
"@eslint/js": "^9.19.0",
|
||||
"@laravel/vite-plugin-wayfinder": "^0.1.3",
|
||||
"@types/json-logic-js": "^2.0.8",
|
||||
"@types/node": "^22.13.5",
|
||||
"babel-plugin-react-compiler": "^1.0.0",
|
||||
"eslint": "^9.17.0",
|
||||
|
|
@ -417,6 +419,8 @@
|
|||
|
||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||
|
||||
"@types/json-logic-js": ["@types/json-logic-js@2.0.8", "", {}, "sha512-WgNsDPuTPKYXl0Jh0IfoCoJoAGGYZt5qzpmjuLSEg7r0cKp/kWtWp0HAsVepyPSPyXiHo6uXp/B/kW/2J1fa2Q=="],
|
||||
|
||||
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||
|
||||
"@types/lodash": ["@types/lodash@4.17.20", "", {}, "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA=="],
|
||||
|
|
@ -777,6 +781,8 @@
|
|||
|
||||
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
|
||||
|
||||
"json-logic-js": ["json-logic-js@2.0.5", "", {}, "sha512-rTT2+lqcuUmj4DgWfmzupZqQDA64AdmYqizzMPWj3DxGdfFNsxPpcNVSaTj4l8W2tG/+hg7/mQhxjU3aPacO6g=="],
|
||||
|
||||
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
||||
|
||||
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
|
||||
|
|
|
|||
|
|
@ -1,33 +1,36 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import AlertError from '@/components/alert-error';
|
||||
import {
|
||||
Drawer,
|
||||
DrawerContent,
|
||||
DrawerDescription,
|
||||
DrawerHeader,
|
||||
DrawerTitle,
|
||||
DrawerDescription,
|
||||
} from '@/components/ui/drawer';
|
||||
import { ImportStepAccount } from './import-step-account';
|
||||
import { ImportStepUpload } from './import-step-upload';
|
||||
import { ImportStepMapping } from './import-step-mapping';
|
||||
import { ImportStepPreview } from './import-step-preview';
|
||||
import { useEncryptionKey } from '@/contexts/encryption-key-context';
|
||||
import { useSyncContext } from '@/contexts/sync-context';
|
||||
import {
|
||||
ImportStep,
|
||||
DateFormat,
|
||||
type ImportState,
|
||||
type ColumnMapping,
|
||||
} from '@/types/import';
|
||||
import { type Account } from '@/types/account';
|
||||
import {
|
||||
parseFile,
|
||||
autoDetectColumns,
|
||||
convertRowsToTransactions,
|
||||
parseFile,
|
||||
} from '@/lib/file-parser';
|
||||
import { transactionSyncService } from '@/services/transaction-sync';
|
||||
import {
|
||||
loadImportConfig,
|
||||
saveImportConfig,
|
||||
} from '@/lib/import-config-storage';
|
||||
import { accountSyncService } from '@/services/account-sync';
|
||||
import { useSyncContext } from '@/contexts/sync-context';
|
||||
import { useEncryptionKey } from '@/contexts/encryption-key-context';
|
||||
import AlertError from '@/components/alert-error';
|
||||
import { saveImportConfig, loadImportConfig } from '@/lib/import-config-storage';
|
||||
import { transactionSyncService } from '@/services/transaction-sync';
|
||||
import { type Account } from '@/types/account';
|
||||
import {
|
||||
DateFormat,
|
||||
ImportStep,
|
||||
type ColumnMapping,
|
||||
type ImportState,
|
||||
} from '@/types/import';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ImportStepAccount } from './import-step-account';
|
||||
import { ImportStepMapping } from './import-step-mapping';
|
||||
import { ImportStepPreview } from './import-step-preview';
|
||||
import { ImportStepUpload } from './import-step-upload';
|
||||
|
||||
interface ImportTransactionsDrawerProps {
|
||||
open: boolean;
|
||||
|
|
@ -42,7 +45,9 @@ export function ImportTransactionsDrawer({
|
|||
const { isKeySet } = useEncryptionKey();
|
||||
const [isImporting, setIsImporting] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [selectedAccount, setSelectedAccount] = useState<Account | null>(null);
|
||||
const [selectedAccount, setSelectedAccount] = useState<Account | null>(
|
||||
null,
|
||||
);
|
||||
const [state, setState] = useState<ImportState>({
|
||||
step: ImportStep.SelectAccount,
|
||||
selectedAccountId: null,
|
||||
|
|
@ -62,11 +67,13 @@ export function ImportTransactionsDrawer({
|
|||
|
||||
useEffect(() => {
|
||||
if (state.selectedAccountId) {
|
||||
accountSyncService.getById(state.selectedAccountId).then((account) => {
|
||||
if (account) {
|
||||
setSelectedAccount(account);
|
||||
}
|
||||
});
|
||||
accountSyncService
|
||||
.getById(state.selectedAccountId)
|
||||
.then((account) => {
|
||||
if (account) {
|
||||
setSelectedAccount(account);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [state.selectedAccountId]);
|
||||
|
||||
|
|
@ -111,16 +118,25 @@ export function ImportTransactionsDrawer({
|
|||
}
|
||||
|
||||
try {
|
||||
const { headers, data, columns, headerRowIndex } = await parseFile(file);
|
||||
const { headers, data, columns, headerRowIndex } =
|
||||
await parseFile(file);
|
||||
const autoMapping = autoDetectColumns(headers);
|
||||
|
||||
const columnOptions = headers.map((header, index) => {
|
||||
const columnData = columns[index] || [];
|
||||
const middleIndex = Math.floor(columnData.length / 2);
|
||||
const examples = columnData
|
||||
.slice(Math.max(headerRowIndex + 1, middleIndex), Math.max(headerRowIndex + 1, middleIndex) + 3)
|
||||
.filter(cell => cell !== null && cell !== undefined && String(cell).trim() !== '')
|
||||
.map(cell => String(cell))
|
||||
.slice(
|
||||
Math.max(headerRowIndex + 1, middleIndex),
|
||||
Math.max(headerRowIndex + 1, middleIndex) + 3,
|
||||
)
|
||||
.filter(
|
||||
(cell) =>
|
||||
cell !== null &&
|
||||
cell !== undefined &&
|
||||
String(cell).trim() !== '',
|
||||
)
|
||||
.map((cell) => String(cell))
|
||||
.slice(0, 3);
|
||||
|
||||
return {
|
||||
|
|
@ -133,8 +149,13 @@ export function ImportTransactionsDrawer({
|
|||
let detectedFormat = DateFormat.YearMonthDay;
|
||||
let formatDetected = false;
|
||||
if (autoMapping.transaction_date) {
|
||||
const { autoDetectDateFormat } = await import('@/lib/file-parser');
|
||||
const detected = autoDetectDateFormat(data, autoMapping.transaction_date);
|
||||
const { autoDetectDateFormat } = await import(
|
||||
'@/lib/file-parser'
|
||||
);
|
||||
const detected = autoDetectDateFormat(
|
||||
data,
|
||||
autoMapping.transaction_date,
|
||||
);
|
||||
if (detected) {
|
||||
detectedFormat = detected;
|
||||
formatDetected = true;
|
||||
|
|
@ -148,9 +169,15 @@ export function ImportTransactionsDrawer({
|
|||
const savedConfig = loadImportConfig(state.selectedAccountId);
|
||||
|
||||
if (savedConfig) {
|
||||
const isValidMapping = (mapping: ColumnMapping): boolean => {
|
||||
const values = Object.values(mapping).filter(v => v !== null);
|
||||
return values.every(value => headers.includes(value as string));
|
||||
const isValidMapping = (
|
||||
mapping: ColumnMapping,
|
||||
): boolean => {
|
||||
const values = Object.values(mapping).filter(
|
||||
(v) => v !== null,
|
||||
);
|
||||
return values.every((value) =>
|
||||
headers.includes(value as string),
|
||||
);
|
||||
};
|
||||
|
||||
if (isValidMapping(savedConfig.columnMapping)) {
|
||||
|
|
@ -173,17 +200,12 @@ export function ImportTransactionsDrawer({
|
|||
}));
|
||||
} catch (err) {
|
||||
setError(
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: 'Failed to parse file'
|
||||
err instanceof Error ? err.message : 'Failed to parse file',
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMappingChange = (
|
||||
field: keyof ColumnMapping,
|
||||
value: string
|
||||
) => {
|
||||
const handleMappingChange = (field: keyof ColumnMapping, value: string) => {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
columnMapping: {
|
||||
|
|
@ -202,11 +224,11 @@ export function ImportTransactionsDrawer({
|
|||
const parsedTransactions = convertRowsToTransactions(
|
||||
state.parsedData,
|
||||
state.columnMapping,
|
||||
state.dateFormat
|
||||
state.dateFormat,
|
||||
);
|
||||
|
||||
const account = await accountSyncService.getById(
|
||||
state.selectedAccountId!
|
||||
state.selectedAccountId!,
|
||||
);
|
||||
|
||||
if (!account) {
|
||||
|
|
@ -214,20 +236,16 @@ export function ImportTransactionsDrawer({
|
|||
return;
|
||||
}
|
||||
|
||||
const transactionsWithDuplicateCheck = await Promise.all(
|
||||
parsedTransactions.map(async (transaction) => {
|
||||
const isDuplicate = await transactionSyncService.isDuplicate(
|
||||
account.id,
|
||||
transaction.transaction_date,
|
||||
transaction.amount,
|
||||
transaction.description
|
||||
);
|
||||
const duplicateFlags = await transactionSyncService.checkDuplicates(
|
||||
account.id,
|
||||
parsedTransactions,
|
||||
);
|
||||
|
||||
return {
|
||||
...transaction,
|
||||
isDuplicate,
|
||||
};
|
||||
})
|
||||
const transactionsWithDuplicateCheck = parsedTransactions.map(
|
||||
(transaction, index) => ({
|
||||
...transaction,
|
||||
isDuplicate: duplicateFlags[index],
|
||||
}),
|
||||
);
|
||||
|
||||
if (state.selectedAccountId) {
|
||||
|
|
@ -246,7 +264,7 @@ export function ImportTransactionsDrawer({
|
|||
setError(
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: 'Failed to process transactions'
|
||||
: 'Failed to process transactions',
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
@ -266,18 +284,20 @@ export function ImportTransactionsDrawer({
|
|||
}
|
||||
|
||||
const newTransactions = state.transactions.filter(
|
||||
(t) => !t.isDuplicate
|
||||
(t) => !t.isDuplicate,
|
||||
);
|
||||
|
||||
const transactionsToImport = await Promise.all(
|
||||
newTransactions.map(async (transaction) => {
|
||||
const { encrypted, iv } =
|
||||
await transactionSyncService.encryptDescription(
|
||||
transaction.description
|
||||
transaction.description,
|
||||
);
|
||||
|
||||
return {
|
||||
user_id: (selectedAccount as Account & { user_id?: number }).user_id || 0,
|
||||
user_id:
|
||||
(selectedAccount as Account & { user_id?: number })
|
||||
.user_id || 0,
|
||||
account_id: selectedAccount.id,
|
||||
category_id: null,
|
||||
description: encrypted,
|
||||
|
|
@ -288,7 +308,7 @@ export function ImportTransactionsDrawer({
|
|||
notes: null,
|
||||
notes_iv: null,
|
||||
};
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
await transactionSyncService.createMany(transactionsToImport);
|
||||
|
|
@ -300,7 +320,7 @@ export function ImportTransactionsDrawer({
|
|||
setError(
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: 'Failed to import transactions'
|
||||
: 'Failed to import transactions',
|
||||
);
|
||||
} finally {
|
||||
setIsImporting(false);
|
||||
|
|
@ -316,17 +336,20 @@ export function ImportTransactionsDrawer({
|
|||
case ImportStep.SelectAccount:
|
||||
return {
|
||||
title: 'Select Account',
|
||||
description: 'Choose the account where transactions will be imported',
|
||||
description:
|
||||
'Choose the account where transactions will be imported',
|
||||
};
|
||||
case ImportStep.UploadFile:
|
||||
return {
|
||||
title: 'Upload File',
|
||||
description: 'Drop your CSV or Excel file here, or click to browse',
|
||||
description:
|
||||
'Drop your CSV or Excel file here, or click to browse',
|
||||
};
|
||||
case ImportStep.MapColumns:
|
||||
return {
|
||||
title: 'Map Columns',
|
||||
description: 'Match your file columns to transaction fields',
|
||||
description:
|
||||
'Match your file columns to transaction fields',
|
||||
};
|
||||
case ImportStep.Preview:
|
||||
return {
|
||||
|
|
@ -398,7 +421,9 @@ export function ImportTransactionsDrawer({
|
|||
<div className="mx-auto w-full max-w-3xl overflow-y-auto p-6">
|
||||
<DrawerHeader className="px-0">
|
||||
<DrawerTitle>{stepInfo.title}</DrawerTitle>
|
||||
<DrawerDescription>{stepInfo.description}</DrawerDescription>
|
||||
<DrawerDescription>
|
||||
{stepInfo.description}
|
||||
</DrawerDescription>
|
||||
</DrawerHeader>
|
||||
{error && (
|
||||
<div className="mt-4">
|
||||
|
|
@ -411,4 +436,3 @@ export function ImportTransactionsDrawer({
|
|||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { SyncManager } from '@/lib/sync-manager';
|
||||
import { indexedDBService } from '@/lib/indexeddb';
|
||||
import { encrypt, importKey } from '@/lib/crypto';
|
||||
import { indexedDBService } from '@/lib/indexeddb';
|
||||
import { getStoredKey } from '@/lib/key-storage';
|
||||
import { SyncManager } from '@/lib/sync-manager';
|
||||
import { uuidv7 } from 'uuidv7';
|
||||
|
||||
export interface Transaction {
|
||||
|
|
@ -45,18 +45,28 @@ class TransactionSyncService {
|
|||
async getByAccountId(accountId: number): Promise<Transaction[]> {
|
||||
try {
|
||||
const allTransactions = await this.getAll();
|
||||
return allTransactions.filter(t => t.account_id === accountId);
|
||||
return allTransactions.filter((t) => t.account_id === accountId);
|
||||
} catch (error) {
|
||||
console.warn('Failed to get transactions from IndexedDB:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async create(data: Omit<Transaction, 'id' | 'created_at' | 'updated_at'>): Promise<Transaction> {
|
||||
return await this.syncManager.createLocal<Transaction>(data as Omit<Transaction, 'id' | 'created_at' | 'updated_at'> & { id?: number; created_at?: string; updated_at?: string });
|
||||
async create(
|
||||
data: Omit<Transaction, 'id' | 'created_at' | 'updated_at'>,
|
||||
): Promise<Transaction> {
|
||||
return await this.syncManager.createLocal<Transaction>(
|
||||
data as Omit<Transaction, 'id' | 'created_at' | 'updated_at'> & {
|
||||
id?: number;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async createMany(transactions: Omit<Transaction, 'id' | 'created_at' | 'updated_at'>[]): Promise<Transaction[]> {
|
||||
async createMany(
|
||||
transactions: Omit<Transaction, 'id' | 'created_at' | 'updated_at'>[],
|
||||
): Promise<Transaction[]> {
|
||||
try {
|
||||
const timestamp = new Date().toISOString();
|
||||
const created: Transaction[] = [];
|
||||
|
|
@ -82,7 +92,9 @@ class TransactionSyncService {
|
|||
return created;
|
||||
} catch (error) {
|
||||
console.error('Failed to create transactions in IndexedDB:', error);
|
||||
throw new Error('Failed to save transactions locally. Please refresh the page and try again.');
|
||||
throw new Error(
|
||||
'Failed to save transactions locally. Please refresh the page and try again.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -99,7 +111,11 @@ class TransactionSyncService {
|
|||
};
|
||||
|
||||
await indexedDBService.put('transactions', updated);
|
||||
await indexedDBService.addPendingChange('transactions', 'update', updated);
|
||||
await indexedDBService.addPendingChange(
|
||||
'transactions',
|
||||
'update',
|
||||
updated,
|
||||
);
|
||||
}
|
||||
|
||||
async updateMany(ids: string[], data: Partial<Transaction>): Promise<void> {
|
||||
|
|
@ -119,7 +135,11 @@ class TransactionSyncService {
|
|||
};
|
||||
|
||||
await indexedDBService.put('transactions', updated);
|
||||
await indexedDBService.addPendingChange('transactions', 'update', updated);
|
||||
await indexedDBService.addPendingChange(
|
||||
'transactions',
|
||||
'update',
|
||||
updated,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -130,7 +150,11 @@ class TransactionSyncService {
|
|||
}
|
||||
|
||||
await indexedDBService.delete('transactions', id);
|
||||
await indexedDBService.addPendingChange('transactions', 'delete', transaction);
|
||||
await indexedDBService.addPendingChange(
|
||||
'transactions',
|
||||
'delete',
|
||||
transaction,
|
||||
);
|
||||
}
|
||||
|
||||
async deleteMany(ids: string[]): Promise<void> {
|
||||
|
|
@ -142,7 +166,116 @@ class TransactionSyncService {
|
|||
}
|
||||
|
||||
await indexedDBService.delete('transactions', id);
|
||||
await indexedDBService.addPendingChange('transactions', 'delete', transaction);
|
||||
await indexedDBService.addPendingChange(
|
||||
'transactions',
|
||||
'delete',
|
||||
transaction,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async checkDuplicates(
|
||||
accountId: number,
|
||||
transactions: Array<{
|
||||
transaction_date: string;
|
||||
amount: number;
|
||||
description: string;
|
||||
}>,
|
||||
): Promise<boolean[]> {
|
||||
try {
|
||||
if (transactions.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const dates = transactions.map((t) => t.transaction_date);
|
||||
const minDate = dates.reduce((a, b) => (a < b ? a : b));
|
||||
const maxDate = dates.reduce((a, b) => (a > b ? a : b));
|
||||
|
||||
const allTransactions = await this.getByAccountId(accountId);
|
||||
const transactionsInRange = allTransactions.filter(
|
||||
(t) =>
|
||||
t.transaction_date >= minDate &&
|
||||
t.transaction_date <= maxDate,
|
||||
);
|
||||
|
||||
console.log(
|
||||
`Checking duplicates for ${transactions.length} transactions. Found ${transactionsInRange.length} existing transactions between ${minDate} and ${maxDate}`,
|
||||
);
|
||||
|
||||
const keyString = getStoredKey();
|
||||
if (!keyString) {
|
||||
console.warn('No encryption key found for duplicate check');
|
||||
return transactions.map(() => false);
|
||||
}
|
||||
|
||||
const key = await importKey(keyString);
|
||||
const { decrypt } = await import('@/lib/crypto');
|
||||
|
||||
const decryptedTransactions = await Promise.all(
|
||||
transactionsInRange.map(async (t) => {
|
||||
try {
|
||||
const decryptedDescription = await decrypt(
|
||||
t.description,
|
||||
key,
|
||||
t.description_iv,
|
||||
);
|
||||
return {
|
||||
transaction_date: t.transaction_date,
|
||||
amount: parseFloat(t.amount),
|
||||
description: decryptedDescription
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/\s+/g, ' '),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Failed to decrypt transaction:',
|
||||
t.id,
|
||||
error,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
const validDecryptedTransactions = decryptedTransactions.filter(
|
||||
(t) => t !== null,
|
||||
);
|
||||
|
||||
console.log(
|
||||
`Successfully decrypted ${validDecryptedTransactions.length} transactions`,
|
||||
);
|
||||
|
||||
const results = transactions.map((importingTx) => {
|
||||
const normalizedDescription = importingTx.description
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/\s+/g, ' ');
|
||||
|
||||
const isDuplicate = validDecryptedTransactions.some(
|
||||
(existing) =>
|
||||
existing.transaction_date ===
|
||||
importingTx.transaction_date &&
|
||||
Math.abs(existing.amount - importingTx.amount) <
|
||||
0.001 &&
|
||||
existing.description === normalizedDescription,
|
||||
);
|
||||
|
||||
return isDuplicate;
|
||||
});
|
||||
|
||||
const duplicateCount = results.filter((r) => r).length;
|
||||
console.log(
|
||||
`Found ${duplicateCount} duplicates out of ${transactions.length} transactions`,
|
||||
);
|
||||
|
||||
return results;
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
'Duplicate check failed, assuming no duplicates:',
|
||||
error,
|
||||
);
|
||||
return transactions.map(() => false);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -150,48 +283,17 @@ class TransactionSyncService {
|
|||
accountId: number,
|
||||
transactionDate: string,
|
||||
amount: number,
|
||||
description: string
|
||||
description: string,
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const existingTransactions = await this.getByAccountId(accountId);
|
||||
|
||||
const keyString = getStoredKey();
|
||||
if (!keyString) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const key = await importKey(keyString);
|
||||
|
||||
for (const existing of existingTransactions) {
|
||||
if (
|
||||
existing.transaction_date === transactionDate &&
|
||||
parseFloat(existing.amount) === amount
|
||||
) {
|
||||
try {
|
||||
const { decrypt } = await import('@/lib/crypto');
|
||||
const decryptedExisting = await decrypt(
|
||||
existing.description,
|
||||
key,
|
||||
existing.description_iv
|
||||
);
|
||||
|
||||
if (decryptedExisting.toLowerCase() === description.toLowerCase()) {
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to decrypt description for duplicate check:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.warn('Duplicate check failed, assuming no duplicates:', error);
|
||||
return false;
|
||||
}
|
||||
const results = await this.checkDuplicates(accountId, [
|
||||
{ transaction_date: transactionDate, amount, description },
|
||||
]);
|
||||
return results[0] || false;
|
||||
}
|
||||
|
||||
async encryptDescription(description: string): Promise<{ encrypted: string; iv: string }> {
|
||||
async encryptDescription(
|
||||
description: string,
|
||||
): Promise<{ encrypted: string; iv: string }> {
|
||||
const keyString = getStoredKey();
|
||||
if (!keyString) {
|
||||
throw new Error('Encryption key not set');
|
||||
|
|
@ -211,4 +313,3 @@ class TransactionSyncService {
|
|||
}
|
||||
|
||||
export const transactionSyncService = new TransactionSyncService();
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue