diff --git a/packages/desktop-client/src/components/FinancesApp.tsx b/packages/desktop-client/src/components/FinancesApp.tsx index 5245becb5df..b56947979bd 100644 --- a/packages/desktop-client/src/components/FinancesApp.tsx +++ b/packages/desktop-client/src/components/FinancesApp.tsx @@ -24,6 +24,7 @@ import { theme } from '../style'; import { getIsOutdated, getLatestVersion } from '../util/versions'; import { UserAccessPage } from './admin/UserAccess/UserAccessPage'; +import { BankSync } from './banksync'; import { BankSyncStatus } from './BankSyncStatus'; import { View } from './common/View'; import { GlobalKeys } from './GlobalKeys'; @@ -248,6 +249,7 @@ export function FinancesApp() { } /> } /> + } /> } /> ; + case 'synced-account-edit': + return ; + case 'account-menu': return ( { + if (!ts) return 'Unknown'; + + const parsed = new Date(parseInt(ts, 10)); + return `${format(parsed, dateFormat)} ${format(parsed, 'HH:mm:ss')}`; +}; + +type AccountRowProps = { + account: AccountEntity; + hovered: boolean; + onHover: (id: AccountEntity['id'] | null) => void; + onAction: (account: AccountEntity, action: 'link' | 'edit') => void; +}; + +export const AccountRow = memo( + ({ account, hovered, onHover, onAction }: AccountRowProps) => { + const backgroundFocus = hovered; + + const dateFormat = useDateFormat() || 'MM/dd/yyyy'; + + const lastSync = tsToString(account.last_sync, dateFormat); + + return ( + onHover && onHover(account.id)} + onMouseLeave={() => onHover && onHover(null)} + > + + {account.name} + + + + {account.bankName} + + + + {account.account_sync_source ? lastSync : ''} + + + {account.account_sync_source ? ( + + + + ) : ( + + + + )} + + ); + }, +); + +AccountRow.displayName = 'AccountRow'; diff --git a/packages/desktop-client/src/components/banksync/AccountsHeader.tsx b/packages/desktop-client/src/components/banksync/AccountsHeader.tsx new file mode 100644 index 00000000000..c5000046705 --- /dev/null +++ b/packages/desktop-client/src/components/banksync/AccountsHeader.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +import { Cell, TableHeader } from '../table'; + +type AccountsHeaderProps = { + unlinked: boolean; +}; + +export function AccountsHeader({ unlinked }: AccountsHeaderProps) { + const { t } = useTranslation(); + + return ( + + + {!unlinked && ( + <> + + + + + )} + + ); +} diff --git a/packages/desktop-client/src/components/banksync/AccountsList.tsx b/packages/desktop-client/src/components/banksync/AccountsList.tsx new file mode 100644 index 00000000000..d1930bef993 --- /dev/null +++ b/packages/desktop-client/src/components/banksync/AccountsList.tsx @@ -0,0 +1,43 @@ +import React from 'react'; + +import { type AccountEntity } from 'loot-core/src/types/models'; + +import { View } from '../common/View'; + +import { AccountRow } from './AccountRow'; + +type AccountsListProps = { + accounts: AccountEntity[]; + hoveredAccount?: string | null; + onHover: (id: AccountEntity['id'] | null) => void; + onAction: (account: AccountEntity, action: 'link' | 'edit') => void; +}; + +export function AccountsList({ + accounts, + hoveredAccount, + onHover, + onAction, +}: AccountsListProps) { + if (accounts.length === 0) { + return null; + } + + return ( + + {accounts.map(account => { + const hovered = hoveredAccount === account.id; + + return ( + + ); + })} + + ); +} diff --git a/packages/desktop-client/src/components/banksync/EditSyncAccount.tsx b/packages/desktop-client/src/components/banksync/EditSyncAccount.tsx new file mode 100644 index 00000000000..32426028998 --- /dev/null +++ b/packages/desktop-client/src/components/banksync/EditSyncAccount.tsx @@ -0,0 +1,240 @@ +import React, { useMemo, useState } from 'react'; +import { Trans, useTranslation } from 'react-i18next'; + +import { useTransactions } from 'loot-core/client/data-hooks/transactions'; +import { + defaultMappings, + type Mappings, + mappingsFromString, + mappingsToString, +} from 'loot-core/server/util/custom-sync-mapping'; +import { q } from 'loot-core/src/shared/query'; +import { + type TransactionEntity, + type AccountEntity, +} from 'loot-core/src/types/models'; + +import { useSyncedPref } from '../../hooks/useSyncedPref'; +import { Button } from '../common/Button2'; +import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal'; +import { Stack } from '../common/Stack'; +import { Text } from '../common/Text'; +import { CheckboxOption } from '../modals/ImportTransactionsModal/CheckboxOption'; + +import { FieldMapping } from './FieldMapping'; + +export type TransactionDirection = 'payment' | 'deposit'; + +type MappableActualFields = 'date' | 'payee' | 'notes'; + +export type MappableField = { + actualField: MappableActualFields; + syncFields: string[]; +}; +export type MappableFieldWithExample = { + actualField: MappableActualFields; + syncFields: { + field: string; + example: string; + }[]; +}; + +const mappableFields: MappableField[] = [ + { + actualField: 'date', + syncFields: [ + 'date', + 'bookingDate', + 'valueDate', + 'postedDate', + 'transactedDate', + ], + }, + { + actualField: 'payee', + syncFields: [ + 'payeeName', + 'creditorName', + 'debtorName', + 'remittanceInformationUnstructured', + 'remittanceInformationUnstructuredArrayString', + 'remittanceInformationStructured', + 'remittanceInformationStructuredArrayString', + 'additionalInformation', + ], + }, + { + actualField: 'notes', + syncFields: [ + 'notes', + 'remittanceInformationUnstructured', + 'remittanceInformationUnstructuredArrayString', + 'remittanceInformationStructured', + 'remittanceInformationStructuredArrayString', + 'additionalInformation', + ], + }, +]; + +const getFields = (transaction: TransactionEntity) => + mappableFields.map(field => ({ + actualField: field.actualField, + syncFields: field.syncFields + .filter(syncField => transaction[syncField as keyof TransactionEntity]) + .map(syncField => ({ + field: syncField, + example: transaction[syncField as keyof TransactionEntity], + })), + })); + +export type EditSyncAccountProps = { + account: AccountEntity; +}; + +export function EditSyncAccount({ account }: EditSyncAccountProps) { + const { t } = useTranslation(); + + const [savedMappings = mappingsToString(defaultMappings), setSavedMappings] = + useSyncedPref(`custom-sync-mappings-${account.id}`); + const [savedImportNotes = true, setSavedImportNotes] = useSyncedPref( + `sync-import-notes-${account.id}`, + ); + const [savedImportPending = true, setSavedImportPending] = useSyncedPref( + `sync-import-pending-${account.id}`, + ); + + const [transactionDirection, setTransactionDirection] = + useState('payment'); + const [importPending, setImportPending] = useState( + String(savedImportPending) === 'true', + ); + const [importNotes, setImportNotes] = useState( + String(savedImportNotes) === 'true', + ); + const [mappings, setMappings] = useState( + mappingsFromString(savedMappings), + ); + + const transactionQuery = useMemo( + () => + q('transactions') + .filter({ + account: account.id, + amount: transactionDirection === 'payment' ? { $lte: 0 } : { $gt: 0 }, + raw_synced_data: { $ne: null }, + }) + .options({ splits: 'none' }) + .select('*'), + [account.id, transactionDirection], + ); + + const { transactions } = useTransactions({ + query: transactionQuery, + }); + + const exampleTransaction = useMemo(() => { + const data = transactions?.[0]?.raw_synced_data; + if (!data) return undefined; + try { + return JSON.parse(data); + } catch (error) { + console.error('Failed to parse transaction data:', error); + return undefined; + } + }, [transactions]); + + const onSave = async (close: () => void) => { + const mappingsStr = mappingsToString(mappings); + setSavedMappings(mappingsStr); + setSavedImportPending(String(importPending)); + setSavedImportNotes(String(importNotes)); + close(); + }; + + const setMapping = (field: string, value: string) => { + setMappings(prev => { + const updated = new Map(prev); + updated?.get(transactionDirection)?.set(field, value); + return updated; + }); + }; + + const fields = exampleTransaction ? getFields(exampleTransaction) : []; + const mapping = mappings.get(transactionDirection); + + return ( + + {({ state: { close } }) => ( + <> + } + /> + + + Field mapping + + + {fields.length > 0 ? ( + + ) : ( + + + No transactions found with mappable fields, accounts must have + been synced at least once for this function to be available. + + + )} + + + Options + + + setImportPending(!importPending)} + > + Import pending transactions + + + setImportNotes(!importNotes)} + > + Import transaction notes + + + + + + + + )} + + ); +} diff --git a/packages/desktop-client/src/components/banksync/FieldMapping.tsx b/packages/desktop-client/src/components/banksync/FieldMapping.tsx new file mode 100644 index 00000000000..4c73963e6a2 --- /dev/null +++ b/packages/desktop-client/src/components/banksync/FieldMapping.tsx @@ -0,0 +1,155 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +import { SvgRightArrow2 } from '../../icons/v0'; +import { SvgEquals } from '../../icons/v1'; +import { theme } from '../../style'; +import { Select } from '../common/Select'; +import { Text } from '../common/Text'; +import { Row, Cell, TableHeader } from '../table'; + +import { + type MappableFieldWithExample, + type TransactionDirection, +} from './EditSyncAccount'; + +const useTransactionDirectionOptions = () => { + const { t } = useTranslation(); + + const transactionDirectionOptions = [ + { + value: 'payment', + label: t('Payment'), + }, + { + value: 'deposit', + label: t('Deposit'), + }, + ]; + + return { transactionDirectionOptions }; +}; + +type FieldMappingProps = { + transactionDirection: TransactionDirection; + setTransactionDirection: (newValue: TransactionDirection) => void; + fields: MappableFieldWithExample[]; + mapping: Map; + setMapping: (field: string, value: string) => void; +}; + +export function FieldMapping({ + transactionDirection, + setTransactionDirection, + fields, + mapping, + setMapping, +}: FieldMappingProps) { + const { t } = useTranslation(); + + const { transactionDirectionOptions } = useTransactionDirectionOptions(); + + return ( + <> + [field, field])} + value={mapping.get(field.actualField)} + style={{ + width: 290, + }} + onChange={newValue => { + if (newValue) setMapping(field.actualField, newValue); + }} + /> + + + + + + f.field === mapping.get(field.actualField), + )?.example + } + width="flex" + style={{ paddingLeft: '10px', height: '100%', border: 0 }} + /> + + ); + })} + + ); +} diff --git a/packages/desktop-client/src/components/banksync/index.tsx b/packages/desktop-client/src/components/banksync/index.tsx new file mode 100644 index 00000000000..e1615f47cee --- /dev/null +++ b/packages/desktop-client/src/components/banksync/index.tsx @@ -0,0 +1,138 @@ +import { useMemo, useState, useCallback } from 'react'; +import { Trans, useTranslation } from 'react-i18next'; + +import { pushModal } from 'loot-core/src/client/actions/modals'; +import { + type BankSyncProviders, + type AccountEntity, +} from 'loot-core/types/models'; + +import { useAccounts } from '../../hooks/useAccounts'; +import { useGlobalPref } from '../../hooks/useGlobalPref'; +import { useDispatch } from '../../redux'; +import { Text } from '../common/Text'; +import { View } from '../common/View'; +import { MOBILE_NAV_HEIGHT } from '../mobile/MobileNavTabs'; +import { Page } from '../Page'; +import { useResponsive } from '../responsive/ResponsiveProvider'; + +import { AccountsHeader } from './AccountsHeader'; +import { AccountsList } from './AccountsList'; + +type SyncProviders = BankSyncProviders | 'unlinked'; + +const useSyncSourceReadable = () => { + const { t } = useTranslation(); + + const syncSourceReadable: Record = { + goCardless: 'GoCardless', + simpleFin: 'SimpleFIN', + unlinked: t('Unlinked'), + }; + + return { syncSourceReadable }; +}; + +export function BankSync() { + const { t } = useTranslation(); + const [floatingSidebar] = useGlobalPref('floatingSidebar'); + + const { syncSourceReadable } = useSyncSourceReadable(); + + const accounts = useAccounts(); + const dispatch = useDispatch(); + const { isNarrowWidth } = useResponsive(); + + const [hoveredAccount, setHoveredAccount] = useState< + AccountEntity['id'] | null + >(null); + + const groupedAccounts = useMemo(() => { + const unsorted = accounts + .filter(a => !a.closed) + .reduce( + (acc, a) => { + const syncSource = a.account_sync_source ?? 'unlinked'; + acc[syncSource] = acc[syncSource] || []; + acc[syncSource].push(a); + return acc; + }, + {} as Record, + ); + + const sortedKeys = Object.keys(unsorted).sort((keyA, keyB) => { + if (keyA === 'unlinked') return 1; + if (keyB === 'unlinked') return -1; + return keyA.localeCompare(keyB); + }); + + return sortedKeys.reduce( + (sorted, key) => { + sorted[key as SyncProviders] = unsorted[key as SyncProviders]; + return sorted; + }, + {} as Record, + ); + }, [accounts]); + + const onAction = async (account: AccountEntity, action: 'link' | 'edit') => { + switch (action) { + case 'edit': + dispatch( + pushModal('synced-account-edit', { + account, + }), + ); + break; + case 'link': + dispatch(pushModal('add-account', { upgradingAccountId: account.id })); + break; + default: + break; + } + }; + + const onHover = useCallback((id: AccountEntity['id'] | null) => { + setHoveredAccount(id); + }, []); + + return ( + + + {accounts.length === 0 && ( + + + To use the bank syncing features, you must first add an account. + + + )} + {Object.entries(groupedAccounts).map(([syncProvider, accounts]) => { + return ( + + {Object.keys(groupedAccounts).length > 1 && ( + + {syncSourceReadable[syncProvider as SyncProviders]} + + )} + + + + ); + })} + + + ); +} diff --git a/packages/desktop-client/src/components/common/Input.tsx b/packages/desktop-client/src/components/common/Input.tsx index fa915aa0884..a8173627ba1 100644 --- a/packages/desktop-client/src/components/common/Input.tsx +++ b/packages/desktop-client/src/components/common/Input.tsx @@ -54,6 +54,9 @@ export function Input({ css( defaultInputStyle, { + color: nativeProps.disabled + ? theme.formInputTextPlaceholder + : theme.formInputText, whiteSpace: 'nowrap', overflow: 'hidden', flexShrink: 0, diff --git a/packages/desktop-client/src/components/sidebar/PrimaryButtons.tsx b/packages/desktop-client/src/components/sidebar/PrimaryButtons.tsx index 51063cc8603..067151409e1 100644 --- a/packages/desktop-client/src/components/sidebar/PrimaryButtons.tsx +++ b/packages/desktop-client/src/components/sidebar/PrimaryButtons.tsx @@ -2,10 +2,12 @@ import React, { useState, useCallback, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { useLocation } from 'react-router-dom'; +import { useSyncServerStatus } from '../../hooks/useSyncServerStatus'; import { SvgCheveronDown, SvgCheveronRight, SvgCog, + SvgCreditCard, SvgReports, SvgStoreFront, SvgTuning, @@ -23,9 +25,16 @@ export function PrimaryButtons() { const onToggle = useCallback(() => setOpen(open => !open), []); const location = useLocation(); - const isActive = ['/payees', '/rules', '/settings', '/tools'].some(route => - location.pathname.startsWith(route), - ); + const syncServerStatus = useSyncServerStatus(); + const isUsingServer = syncServerStatus !== 'no-server'; + + const isActive = [ + '/payees', + '/rules', + '/bank-sync', + '/settings', + '/tools', + ].some(route => location.pathname.startsWith(route)); useEffect(() => { if (isActive) { @@ -59,6 +68,14 @@ export function PrimaryButtons() { to="/rules" indent={15} /> + {isUsingServer && ( + + )} 0 || matchedTransactions.length > 0; } diff --git a/packages/loot-core/src/client/state-types/modals.d.ts b/packages/loot-core/src/client/state-types/modals.d.ts index d2140d43cd8..da866e12a7a 100644 --- a/packages/loot-core/src/client/state-types/modals.d.ts +++ b/packages/loot-core/src/client/state-types/modals.d.ts @@ -186,6 +186,11 @@ type FinanceModals = { 'schedules-upcoming-length': null; 'schedule-posts-offline-notification': null; + + 'synced-account-edit': { + account: AccountEntity; + }; + 'account-menu': { accountId: string; onSave: (account: AccountEntity) => void; diff --git a/packages/loot-core/src/mocks/index.ts b/packages/loot-core/src/mocks/index.ts index 82005d29188..e008df1d7a1 100644 --- a/packages/loot-core/src/mocks/index.ts +++ b/packages/loot-core/src/mocks/index.ts @@ -16,15 +16,10 @@ export function generateAccount( name: AccountEntity['name'], isConnected?: boolean, offbudget?: boolean, -): AccountEntity & { bankId: number | null; bankName: string | null } { - const offlineAccount: AccountEntity & { - bankId: number | null; - bankName: string | null; - } = { +): AccountEntity { + const offlineAccount: AccountEntity = { id: uuidv4(), name, - bankId: null, - bankName: null, offbudget: offbudget ? 1 : 0, sort_order: 0, tombstone: 0, @@ -45,6 +40,7 @@ export function generateAccount( balance_available: 0, balance_limit: 0, account_sync_source: 'goCardless', + last_sync: new Date().getTime().toString(), }; } @@ -55,12 +51,15 @@ function emptySyncFields(): _SyncFields { return { account_id: null, bank: null, + bankId: null, + bankName: null, mask: null, official_name: null, balance_current: null, balance_available: null, balance_limit: null, account_sync_source: null, + last_sync: null, }; } diff --git a/packages/loot-core/src/server/__snapshots__/main.test.ts.snap b/packages/loot-core/src/server/__snapshots__/main.test.ts.snap index 1825a6e8480..572444f3637 100644 --- a/packages/loot-core/src/server/__snapshots__/main.test.ts.snap +++ b/packages/loot-core/src/server/__snapshots__/main.test.ts.snap @@ -19,6 +19,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456789, @@ -44,6 +45,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456789, @@ -75,7 +77,7 @@ exports[`Accounts Transfers are properly updated 2`] = ` \\"id\\": \\"test-transfer\\", \\"imported_description\\": null, \\"isChild\\": 0, -@@ -23,15 +23,15 @@ +@@ -24,15 +24,15 @@ \\"tombstone\\": 0, \\"transferred_id\\": \\"id2\\", \\"type\\": null, @@ -100,8 +102,8 @@ exports[`Accounts Transfers are properly updated 3`] = ` - First value + Second value -@@ -18,12 +18,12 @@ - \\"pending\\": 0, +@@ -19,12 +19,12 @@ + \\"raw_synced_data\\": null, \\"reconciled\\": 0, \\"schedule\\": null, \\"sort_order\\": 123456789, @@ -115,8 +117,8 @@ exports[`Accounts Transfers are properly updated 3`] = ` Object { \\"acct\\": \\"three\\", \\"amount\\": -5000, -@@ -43,10 +43,10 @@ - \\"pending\\": 0, +@@ -45,10 +45,10 @@ + \\"raw_synced_data\\": null, \\"reconciled\\": 0, \\"schedule\\": null, \\"sort_order\\": 123456789, diff --git a/packages/loot-core/src/server/accounts/__snapshots__/parse-file.test.ts.snap b/packages/loot-core/src/server/accounts/__snapshots__/parse-file.test.ts.snap index 316320ba970..0b432f716c9 100644 --- a/packages/loot-core/src/server/accounts/__snapshots__/parse-file.test.ts.snap +++ b/packages/loot-core/src/server/accounts/__snapshots__/parse-file.test.ts.snap @@ -21,6 +21,7 @@ Array [ GENODEF1PFK ABWE: Testkonto 1", "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456789, @@ -48,6 +49,7 @@ Array [ GENODEF1PFK ABWE: Testkonto 1", "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456788, @@ -73,6 +75,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456787, @@ -101,6 +104,7 @@ Array [ GENODEF1PFK ABWE: Testkonto", "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456786, @@ -126,6 +130,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456785, @@ -151,6 +156,7 @@ Array [ "notes": "Lastschrift 2. Zahlung TAN:747216 ", "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456784, @@ -176,6 +182,7 @@ Array [ "notes": "Lastschrift 1. Zahlung TAN:747216 ", "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456783, @@ -206,6 +213,7 @@ Array [ "notes": "PREAUTHORIZED DEBIT;B.C. HYDRO & POWER AUTHORITY;Electronic Funds Transfer", "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456789, @@ -231,6 +239,7 @@ Array [ "notes": "PREAUTHORIZED DEBIT;LUXMORE REALTY PPTY MGMT;Electronic Funds Transfer", "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456788, @@ -261,6 +270,7 @@ Array [ "notes": "PWW", "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456789, @@ -291,6 +301,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456789, @@ -321,6 +332,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456789, @@ -346,6 +358,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456788, @@ -371,6 +384,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456787, @@ -396,6 +410,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456786, @@ -421,6 +436,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456785, @@ -446,6 +462,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456784, @@ -471,6 +488,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456783, @@ -496,6 +514,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456782, @@ -521,6 +540,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456781, @@ -546,6 +566,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456780, @@ -576,6 +597,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456789, @@ -601,6 +623,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456788, @@ -626,6 +649,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456787, @@ -651,6 +675,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456786, @@ -676,6 +701,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456785, @@ -701,6 +727,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456784, @@ -726,6 +753,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456783, @@ -751,6 +779,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456782, @@ -776,6 +805,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456781, @@ -801,6 +831,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456780, @@ -831,6 +862,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456789, @@ -856,6 +888,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456788, @@ -881,6 +914,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456787, @@ -906,6 +940,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456786, @@ -931,6 +966,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456785, @@ -956,6 +992,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456784, @@ -981,6 +1018,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456783, @@ -1006,6 +1044,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456782, @@ -1031,6 +1070,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456781, @@ -1056,6 +1096,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456780, @@ -1081,6 +1122,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456779, @@ -1106,6 +1148,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456778, @@ -1131,6 +1174,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456777, @@ -1156,6 +1200,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456776, @@ -1181,6 +1226,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456775, @@ -1206,6 +1252,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456774, @@ -1231,6 +1278,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456773, @@ -1256,6 +1304,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456772, diff --git a/packages/loot-core/src/server/accounts/__snapshots__/sync.test.ts.snap b/packages/loot-core/src/server/accounts/__snapshots__/sync.test.ts.snap index 0602bdc5574..d82a0a3b244 100644 --- a/packages/loot-core/src/server/accounts/__snapshots__/sync.test.ts.snap +++ b/packages/loot-core/src/server/accounts/__snapshots__/sync.test.ts.snap @@ -18,6 +18,7 @@ Array [ "parent_id": null, "payee": null, "payee_name": null, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456789, @@ -41,6 +42,7 @@ Array [ "parent_id": null, "payee": null, "payee_name": null, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456789, diff --git a/packages/loot-core/src/server/accounts/__snapshots__/transfer.test.ts.snap b/packages/loot-core/src/server/accounts/__snapshots__/transfer.test.ts.snap index 6890d6ba26f..815c8e77b5a 100644 --- a/packages/loot-core/src/server/accounts/__snapshots__/transfer.test.ts.snap +++ b/packages/loot-core/src/server/accounts/__snapshots__/transfer.test.ts.snap @@ -18,6 +18,7 @@ Array [ "parent_id": null, "payee": "id3", "payee_name": "", + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456789, @@ -41,6 +42,7 @@ Array [ "parent_id": null, "payee": "id2", "payee_name": "", + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456789, @@ -68,7 +70,7 @@ exports[`Transfer split transfers are retained on child transactions 2`] = ` \\"parent_id\\": null, \\"payee\\": \\"id3\\", \\"payee_name\\": \\"\\", - \\"reconciled\\": 0," + \\"raw_synced_data\\": null," `; exports[`Transfer split transfers are retained on child transactions 3`] = ` @@ -76,7 +78,7 @@ exports[`Transfer split transfers are retained on child transactions 3`] = ` - First value + Second value -@@ -21,10 +21,56 @@ +@@ -22,10 +22,58 @@ \\"starting_balance_flag\\": 0, \\"tombstone\\": 0, \\"transfer_id\\": \\"id6\\", @@ -97,6 +99,7 @@ exports[`Transfer split transfers are retained on child transactions 3`] = ` + \\"parent_id\\": \\"id5\\", + \\"payee\\": \\"id2\\", + \\"payee_name\\": \\"\\", ++ \\"raw_synced_data\\": null, + \\"reconciled\\": 0, + \\"schedule\\": null, + \\"sort_order\\": 123456789, @@ -120,6 +123,7 @@ exports[`Transfer split transfers are retained on child transactions 3`] = ` + \\"parent_id\\": null, + \\"payee\\": \\"id2\\", + \\"payee_name\\": \\"\\", ++ \\"raw_synced_data\\": null, + \\"reconciled\\": 0, + \\"schedule\\": null, + \\"sort_order\\": 123456789, @@ -153,6 +157,7 @@ Array [ "parent_id": null, "payee": "id5", "payee_name": "Non-transfer", + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456789, @@ -168,7 +173,7 @@ exports[`Transfer transfers are properly de-categorized 2`] = ` - First value + Second value -@@ -9,17 +9,40 @@ +@@ -9,18 +9,42 @@ \\"id\\": \\"id6\\", \\"imported_id\\": null, \\"imported_payee\\": null, @@ -176,14 +181,18 @@ exports[`Transfer transfers are properly de-categorized 2`] = ` \\"is_parent\\": 0, - \\"notes\\": null, + \\"notes\\": \\"hi\\", -+ \\"parent_id\\": null, + \\"parent_id\\": null, +- \\"payee\\": \\"id5\\", +- \\"payee_name\\": \\"Non-transfer\\", + \\"payee\\": \\"id4\\", + \\"payee_name\\": \\"\\", -+ \\"reconciled\\": 0, -+ \\"schedule\\": null, -+ \\"sort_order\\": 123456789, -+ \\"starting_balance_flag\\": 0, -+ \\"tombstone\\": 0, + \\"raw_synced_data\\": null, + \\"reconciled\\": 0, + \\"schedule\\": null, + \\"sort_order\\": 123456789, + \\"starting_balance_flag\\": 0, + \\"tombstone\\": 0, +- \\"transfer_id\\": null, + \\"transfer_id\\": \\"id7\\", + }, + Object { @@ -199,17 +208,15 @@ exports[`Transfer transfers are properly de-categorized 2`] = ` + \\"is_child\\": 0, + \\"is_parent\\": 0, + \\"notes\\": \\"hi\\", - \\"parent_id\\": null, -- \\"payee\\": \\"id5\\", -- \\"payee_name\\": \\"Non-transfer\\", ++ \\"parent_id\\": null, + \\"payee\\": \\"id2\\", + \\"payee_name\\": \\"\\", - \\"reconciled\\": 0, - \\"schedule\\": null, - \\"sort_order\\": 123456789, - \\"starting_balance_flag\\": 0, - \\"tombstone\\": 0, -- \\"transfer_id\\": null, ++ \\"raw_synced_data\\": null, ++ \\"reconciled\\": 0, ++ \\"schedule\\": null, ++ \\"sort_order\\": 123456789, ++ \\"starting_balance_flag\\": 0, ++ \\"tombstone\\": 0, + \\"transfer_id\\": \\"id6\\", }, ]" @@ -220,7 +227,7 @@ exports[`Transfer transfers are properly de-categorized 3`] = ` - First value + Second value -@@ -1,31 +1,31 @@ +@@ -1,32 +1,32 @@ Array [ Object { \\"account\\": \\"one\\", @@ -240,6 +247,7 @@ exports[`Transfer transfers are properly de-categorized 3`] = ` - \\"payee\\": \\"id4\\", + \\"payee\\": \\"id3\\", \\"payee_name\\": \\"\\", + \\"raw_synced_data\\": null, \\"reconciled\\": 0, \\"schedule\\": null, \\"sort_order\\": 123456789, @@ -275,6 +283,7 @@ Array [ "parent_id": null, "payee": "id5", "payee_name": "Non-transfer", + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456789, @@ -290,7 +299,7 @@ exports[`Transfer transfers are properly inserted/updated/deleted 2`] = ` - First value + Second value -@@ -20,6 +20,52 @@ +@@ -21,6 +21,54 @@ \\"sort_order\\": 123456789, \\"starting_balance_flag\\": 0, \\"tombstone\\": 0, @@ -312,6 +321,7 @@ exports[`Transfer transfers are properly inserted/updated/deleted 2`] = ` + \\"parent_id\\": null, + \\"payee\\": \\"id3\\", + \\"payee_name\\": \\"\\", ++ \\"raw_synced_data\\": null, + \\"reconciled\\": 0, + \\"schedule\\": null, + \\"sort_order\\": 123456789, @@ -335,6 +345,7 @@ exports[`Transfer transfers are properly inserted/updated/deleted 2`] = ` + \\"parent_id\\": null, + \\"payee\\": \\"id2\\", + \\"payee_name\\": \\"\\", ++ \\"raw_synced_data\\": null, + \\"reconciled\\": 0, + \\"schedule\\": null, + \\"sort_order\\": 123456789, @@ -350,7 +361,7 @@ exports[`Transfer transfers are properly inserted/updated/deleted 3`] = ` - First value + Second value -@@ -2,70 +2,70 @@ +@@ -2,73 +2,73 @@ Object { \\"account\\": \\"one\\", \\"amount\\": 5000, @@ -372,6 +383,7 @@ exports[`Transfer transfers are properly inserted/updated/deleted 3`] = ` - \\"payee_name\\": \\"Non-transfer\\", + \\"payee\\": \\"id3\\", + \\"payee_name\\": \\"\\", + \\"raw_synced_data\\": null, \\"reconciled\\": 0, \\"schedule\\": null, \\"sort_order\\": 123456789, @@ -403,6 +415,7 @@ exports[`Transfer transfers are properly inserted/updated/deleted 3`] = ` - \\"payee\\": \\"id3\\", + \\"payee\\": \\"id2\\", \\"payee_name\\": \\"\\", + \\"raw_synced_data\\": null, \\"reconciled\\": 0, \\"schedule\\": null, \\"sort_order\\": 123456789, @@ -433,6 +446,7 @@ exports[`Transfer transfers are properly inserted/updated/deleted 3`] = ` - \\"payee_name\\": \\"\\", + \\"payee\\": \\"id5\\", + \\"payee_name\\": \\"Non-transfer\\", + \\"raw_synced_data\\": null, \\"reconciled\\": 0, \\"schedule\\": null, \\"sort_order\\": 123456789, @@ -449,7 +463,7 @@ exports[`Transfer transfers are properly inserted/updated/deleted 4`] = ` - First value + Second value -@@ -11,21 +11,21 @@ +@@ -11,22 +11,22 @@ \\"imported_payee\\": null, \\"is_child\\": 0, \\"is_parent\\": 0, @@ -458,6 +472,7 @@ exports[`Transfer transfers are properly inserted/updated/deleted 4`] = ` - \\"payee\\": \\"id3\\", + \\"payee\\": \\"id4\\", \\"payee_name\\": \\"\\", + \\"raw_synced_data\\": null, \\"reconciled\\": 0, \\"schedule\\": null, \\"sort_order\\": 123456789, @@ -480,7 +495,7 @@ exports[`Transfer transfers are properly inserted/updated/deleted 5`] = ` - First value + Second value -@@ -11,41 +11,18 @@ +@@ -11,43 +11,19 @@ \\"imported_payee\\": null, \\"is_child\\": 0, \\"is_parent\\": 0, @@ -488,11 +503,14 @@ exports[`Transfer transfers are properly inserted/updated/deleted 5`] = ` \\"parent_id\\": null, - \\"payee\\": \\"id4\\", - \\"payee_name\\": \\"\\", -- \\"reconciled\\": 0, -- \\"schedule\\": null, -- \\"sort_order\\": 123456789, -- \\"starting_balance_flag\\": 0, -- \\"tombstone\\": 0, ++ \\"payee\\": \\"id9\\", ++ \\"payee_name\\": \\"Not transferred anymore\\", + \\"raw_synced_data\\": null, + \\"reconciled\\": 0, + \\"schedule\\": null, + \\"sort_order\\": 123456789, + \\"starting_balance_flag\\": 0, + \\"tombstone\\": 0, - \\"transfer_id\\": \\"id8\\", - }, - Object { @@ -511,13 +529,12 @@ exports[`Transfer transfers are properly inserted/updated/deleted 5`] = ` - \\"parent_id\\": null, - \\"payee\\": \\"id2\\", - \\"payee_name\\": \\"\\", -+ \\"payee\\": \\"id9\\", -+ \\"payee_name\\": \\"Not transferred anymore\\", - \\"reconciled\\": 0, - \\"schedule\\": null, - \\"sort_order\\": 123456789, - \\"starting_balance_flag\\": 0, - \\"tombstone\\": 0, +- \\"raw_synced_data\\": null, +- \\"reconciled\\": 0, +- \\"schedule\\": null, +- \\"sort_order\\": 123456789, +- \\"starting_balance_flag\\": 0, +- \\"tombstone\\": 0, - \\"transfer_id\\": \\"id7\\", + \\"transfer_id\\": null, }, @@ -532,7 +549,7 @@ exports[`Transfer transfers are properly inserted/updated/deleted 6`] = ` - First value + Second value -@@ -11,18 +11,41 @@ +@@ -11,19 +11,43 @@ \\"imported_payee\\": null, \\"is_child\\": 0, \\"is_parent\\": 0, @@ -542,11 +559,13 @@ exports[`Transfer transfers are properly inserted/updated/deleted 6`] = ` - \\"payee_name\\": \\"Not transferred anymore\\", + \\"payee\\": \\"id3\\", + \\"payee_name\\": \\"\\", -+ \\"reconciled\\": 0, -+ \\"schedule\\": null, -+ \\"sort_order\\": 123456789, -+ \\"starting_balance_flag\\": 0, -+ \\"tombstone\\": 0, + \\"raw_synced_data\\": null, + \\"reconciled\\": 0, + \\"schedule\\": null, + \\"sort_order\\": 123456789, + \\"starting_balance_flag\\": 0, + \\"tombstone\\": 0, +- \\"transfer_id\\": null, + \\"transfer_id\\": \\"id10\\", + }, + Object { @@ -565,12 +584,12 @@ exports[`Transfer transfers are properly inserted/updated/deleted 6`] = ` + \\"parent_id\\": null, + \\"payee\\": \\"id2\\", + \\"payee_name\\": \\"\\", - \\"reconciled\\": 0, - \\"schedule\\": null, - \\"sort_order\\": 123456789, - \\"starting_balance_flag\\": 0, - \\"tombstone\\": 0, -- \\"transfer_id\\": null, ++ \\"raw_synced_data\\": null, ++ \\"reconciled\\": 0, ++ \\"schedule\\": null, ++ \\"sort_order\\": 123456789, ++ \\"starting_balance_flag\\": 0, ++ \\"tombstone\\": 0, + \\"transfer_id\\": \\"id7\\", }, Object { @@ -584,7 +603,7 @@ exports[`Transfer transfers are properly inserted/updated/deleted 7`] = ` - First value + Second value -@@ -2,56 +2,10 @@ +@@ -2,58 +2,10 @@ Object { \\"account\\": \\"one\\", \\"amount\\": 5000, @@ -601,6 +620,7 @@ exports[`Transfer transfers are properly inserted/updated/deleted 7`] = ` - \\"parent_id\\": null, - \\"payee\\": \\"id3\\", - \\"payee_name\\": \\"\\", +- \\"raw_synced_data\\": null, - \\"reconciled\\": 0, - \\"schedule\\": null, - \\"sort_order\\": 123456789, @@ -624,6 +644,7 @@ exports[`Transfer transfers are properly inserted/updated/deleted 7`] = ` - \\"parent_id\\": null, - \\"payee\\": \\"id2\\", - \\"payee_name\\": \\"\\", +- \\"raw_synced_data\\": null, - \\"reconciled\\": 0, - \\"schedule\\": null, - \\"sort_order\\": 123456789, diff --git a/packages/loot-core/src/server/accounts/sync.ts b/packages/loot-core/src/server/accounts/sync.ts index 75651164212..ac2c1dc0ccc 100644 --- a/packages/loot-core/src/server/accounts/sync.ts +++ b/packages/loot-core/src/server/accounts/sync.ts @@ -26,6 +26,10 @@ import { runMutator } from '../mutators'; import { post } from '../post'; import { getServer } from '../server-config'; import { batchMessages } from '../sync'; +import { + defaultMappings, + mappingsFromString, +} from '../util/custom-sync-mapping'; import { getStartingBalancePayee } from './payees'; import { title } from './title'; @@ -322,57 +326,83 @@ async function normalizeTransactions( async function normalizeBankSyncTransactions(transactions, acctId) { const payeesToCreate = new Map(); + const [customMappingsRaw, importPending, importNotes] = await Promise.all([ + runQuery( + q('preferences') + .filter({ id: `custom-sync-mappings-${acctId}` }) + .select('value'), + ).then(data => data?.data?.[0]?.value), + runQuery( + q('preferences') + .filter({ id: `sync-import-pending-${acctId}` }) + .select('value'), + ).then(data => String(data?.data?.[0]?.value ?? 'true') === 'true'), + runQuery( + q('preferences') + .filter({ id: `sync-import-notes-${acctId}` }) + .select('value'), + ).then(data => String(data?.data?.[0]?.value ?? 'true') === 'true'), + ]); + + const mappings = customMappingsRaw + ? mappingsFromString(customMappingsRaw) + : defaultMappings; + const normalized = []; for (const trans of transactions) { + trans.cleared = Boolean(trans.booked); + + if (!importPending && !trans.cleared) continue; + if (!trans.amount) { trans.amount = trans.transactionAmount.amount; } + const mapping = mappings.get(trans.amount <= 0 ? 'payment' : 'deposit'); + + const date = trans[mapping.get('date')] ?? trans.date; + const payeeName = trans[mapping.get('payee')]; + const notes = trans[mapping.get('notes')]; + // Validate the date because we do some stuff with it. The db // layer does better validation, but this will give nicer errors - if (trans.date == null) { + if (date == null) { throw new Error('`date` is required when adding a transaction'); } - if (trans.payeeName == null) { + if (payeeName == null) { throw new Error('`payeeName` is required when adding a transaction'); } - trans.imported_payee = trans.imported_payee || trans.payeeName; + trans.imported_payee = trans.imported_payee || payeeName; if (trans.imported_payee) { trans.imported_payee = trans.imported_payee.trim(); } - // It's important to resolve both the account and payee early so - // when rules are run, they have the right data. Resolving payees - // also simplifies the payee creation process - trans.account = acctId; - trans.payee = await resolvePayee(trans, trans.payeeName, payeesToCreate); - - trans.cleared = Boolean(trans.booked); - let imported_id = trans.transactionId; - if (trans.cleared && !trans.transactionId && trans.internalTransactionId) { imported_id = `${trans.account}-${trans.internalTransactionId}`; } - const notes = - trans.remittanceInformationUnstructured || - (trans.remittanceInformationUnstructuredArray || []).join(', '); + // It's important to resolve both the account and payee early so + // when rules are run, they have the right data. Resolving payees + // also simplifies the payee creation process + trans.account = acctId; + trans.payee = await resolvePayee(trans, payeeName, payeesToCreate); normalized.push({ - payee_name: trans.payeeName, + payee_name: payeeName, trans: { amount: amountToInteger(trans.amount), payee: trans.payee, account: trans.account, - date: trans.date, - notes: notes.trim().replace('#', '##'), + date, + notes: importNotes && notes ? notes.trim().replace(/#/g, '##') : null, category: trans.category ?? null, imported_id, imported_payee: trans.imported_payee, cleared: trans.cleared, + raw_synced_data: JSON.stringify(trans), }, }); } diff --git a/packages/loot-core/src/server/aql/schema/index.ts b/packages/loot-core/src/server/aql/schema/index.ts index df445ab01b9..5b85c75a65c 100644 --- a/packages/loot-core/src/server/aql/schema/index.ts +++ b/packages/loot-core/src/server/aql/schema/index.ts @@ -53,6 +53,7 @@ export const schema = { reconciled: f('boolean', { default: false }), tombstone: f('boolean'), schedule: f('id', { ref: 'schedules' }), + raw_synced_data: f('string'), // subtransactions is a special field added if the table has the // `splits: grouped` option }, @@ -73,6 +74,7 @@ export const schema = { account_id: f('string'), official_name: f('string'), account_sync_source: f('string'), + last_sync: f('string'), }, categories: { id: f('id'), diff --git a/packages/loot-core/src/server/main.ts b/packages/loot-core/src/server/main.ts index fa1e4bf8e3f..d57f94dcfed 100644 --- a/packages/loot-core/src/server/main.ts +++ b/packages/loot-core/src/server/main.ts @@ -1050,7 +1050,7 @@ handlers['gocardless-create-web-token'] = async function ({ } }; -function handleSyncResponse( +async function handleSyncResponse( res, acct, newTransactions, @@ -1065,6 +1065,10 @@ function handleSyncResponse( if (added.length > 0) { updatedAccounts.push(acct.id); } + + const ts = new Date().getTime().toString(); + const id = acct.id; + await db.runQuery(`UPDATE accounts SET last_sync = ? WHERE id = ?`, [ts, id]); } function handleSyncError(err, acct) { @@ -1132,7 +1136,7 @@ handlers['accounts-bank-sync'] = async function ({ ids = [] }) { acct.bankId, ); - handleSyncResponse( + await handleSyncResponse( res, acct, newTransactions, @@ -1201,7 +1205,7 @@ handlers['simplefin-batch-sync'] = async function ({ ids = [] }) { ), ); } else { - handleSyncResponse( + await handleSyncResponse( account.res, accounts.find(a => a.id === account.accountId), newTransactions, diff --git a/packages/loot-core/src/server/util/custom-sync-mapping.ts b/packages/loot-core/src/server/util/custom-sync-mapping.ts new file mode 100644 index 00000000000..ddf9311b71d --- /dev/null +++ b/packages/loot-core/src/server/util/custom-sync-mapping.ts @@ -0,0 +1,48 @@ +export type Mappings = Map>; + +export const mappingsToString = (mapping: Mappings): string => + JSON.stringify( + Object.fromEntries( + [...mapping.entries()].map(([key, value]) => [ + key, + Object.fromEntries(value), + ]), + ), + ); + +export const mappingsFromString = (str: string): Mappings => { + try { + const parsed = JSON.parse(str); + if (typeof parsed !== 'object' || parsed === null) { + throw new Error('Invalid mapping format'); + } + return new Map( + Object.entries(parsed).map(([key, value]) => [ + key, + new Map(Object.entries(value as object)), + ]), + ); + } catch (e) { + const message = e instanceof Error ? e.message : e; + throw new Error(`Failed to parse mapping: ${message}`); + } +}; + +export const defaultMappings: Mappings = new Map([ + [ + 'payment', + new Map([ + ['date', 'date'], + ['payee', 'payeeName'], + ['notes', 'notes'], + ]), + ], + [ + 'deposit', + new Map([ + ['date', 'date'], + ['payee', 'payeeName'], + ['notes', 'notes'], + ]), + ], +]); diff --git a/packages/loot-core/src/types/models/account.d.ts b/packages/loot-core/src/types/models/account.d.ts index cb668f6622f..74436bad581 100644 --- a/packages/loot-core/src/types/models/account.d.ts +++ b/packages/loot-core/src/types/models/account.d.ts @@ -10,12 +10,15 @@ export type AccountEntity = { type _SyncFields = { account_id: T extends true ? string : null; bank: T extends true ? string : null; + bankName: T extends true ? string : null; + bankId: T extends true ? number : null; mask: T extends true ? string : null; // end of bank account number official_name: T extends true ? string : null; balance_current: T extends true ? number : null; balance_available: T extends true ? number : null; balance_limit: T extends true ? number : null; account_sync_source: T extends true ? AccountSyncSource : null; + last_sync: T extends true ? string : null; }; export type AccountSyncSource = 'simpleFin' | 'goCardless'; diff --git a/packages/loot-core/src/types/models/bank-sync.d.ts b/packages/loot-core/src/types/models/bank-sync.d.ts index 279a23cfae7..f0ab8a3758c 100644 --- a/packages/loot-core/src/types/models/bank-sync.d.ts +++ b/packages/loot-core/src/types/models/bank-sync.d.ts @@ -19,3 +19,5 @@ export type BankSyncResponse = { error_type: string; error_code: string; }; + +export type BankSyncProviders = 'goCardless' | 'simpleFin'; diff --git a/packages/loot-core/src/types/models/transaction.d.ts b/packages/loot-core/src/types/models/transaction.d.ts index 7fc23821bef..60c99b673a2 100644 --- a/packages/loot-core/src/types/models/transaction.d.ts +++ b/packages/loot-core/src/types/models/transaction.d.ts @@ -32,4 +32,5 @@ export interface TransactionEntity { version: 1; difference: number; } | null; + raw_synced_data?: string | undefined; } diff --git a/packages/loot-core/src/types/prefs.d.ts b/packages/loot-core/src/types/prefs.d.ts index 6cf89ed4509..d3c044ceb6b 100644 --- a/packages/loot-core/src/types/prefs.d.ts +++ b/packages/loot-core/src/types/prefs.d.ts @@ -28,6 +28,9 @@ export type SyncedPrefs = Partial< | `csv-in-out-mode-${string}` | `csv-out-value-${string}` | `csv-has-header-${string}` + | `custom-sync-mappings-${string}` + | `sync-import-pending-${string}` + | `sync-import-notes-${string}` | `ofx-fallback-missing-payee-${string}` | `flip-amount-${string}-${'csv' | 'qif'}` | `flags.${FeatureFlag}` diff --git a/packages/sync-server/src/app-gocardless/README.md b/packages/sync-server/src/app-gocardless/README.md index de406253dc3..b24bd3f519f 100644 --- a/packages/sync-server/src/app-gocardless/README.md +++ b/packages/sync-server/src/app-gocardless/README.md @@ -10,9 +10,9 @@ If the default bank integration does not work for you, you can integrate a new b This will trigger the process of fetching the data from the bank and will log the data in the backend. Use this data to fill the logic of the bank class. -4. Create new a bank class based on `app-gocardless/banks/sandboxfinance-sfin0000.js`. +4. Create new a bank class based on an existing example in `app-gocardless/banks`. - Name of the file and class should be created based on the ID of the integrated institution, found in step 1. + Name of the file and class should follow the existing patterns and be created based on the ID of the integrated institution, found in step 1. 5. Fill the logic of `normalizeAccount`, `normalizeTransaction`, `sortTransactions`, and `calculateStartingBalance` functions. You do not need to fill every function, only those which are necessary for the integration to work. @@ -162,3 +162,34 @@ If the default bank integration does not work for you, you can integrate a new b 6. Add new bank integration to `BankFactory` class in file `actual-server/app-gocardless/bank-factory.js` 7. Remember to add tests for new bank integration in + +## normalizeTransaction +This is the most commonly used override as it allows you to change the data that is returned to the client. + +Please follow the following patterns when implementing a custom normalizeTransaction method: +1. If you need to edit the values of transaction fields (excluding the transaction amount) do not mutate the original transaction object. Instead, create a shallow copy and make your changes there. +2. End the function by returning the result of calling the fallback normalizeTransaction method from integration-bank.js + +E.g. +```js +import Fallback from './integration-bank.js'; + +export default { + ... + + normalizeTransaction(transaction, booked) { + // create a shallow copy of the transaction object + const editedTrans = { ...transaction }; + + // make any changes required to the copy + editedTrans.remittanceInformationUnstructured = transaction.remittanceInformationStructured; + + // call the fallback method, passing in your edited transaction as the 3rd parameter + // this will calculate the date, payee name and notes fields based on your changes + // but leave the original fields available for mapping in the UI + return Fallback.normalizeTransaction(transaction, booked, editedTrans); + } + + ... +} +``` diff --git a/packages/sync-server/src/app-gocardless/banks/abanca_caglesmm.js b/packages/sync-server/src/app-gocardless/banks/abanca_caglesmm.js index 4562869be66..a564be9bf9c 100644 --- a/packages/sync-server/src/app-gocardless/banks/abanca_caglesmm.js +++ b/packages/sync-server/src/app-gocardless/banks/abanca_caglesmm.js @@ -1,5 +1,3 @@ -import { formatPayeeName } from '../../util/payee-name.js'; - import Fallback from './integration-bank.js'; /** @type {import('./bank.interface.js').IBank} */ @@ -13,14 +11,12 @@ export default { ], // Abanca transactions doesn't get the creditorName/debtorName properly - normalizeTransaction(transaction, _booked) { - transaction.creditorName = transaction.remittanceInformationStructured; - transaction.debtorName = transaction.remittanceInformationStructured; + normalizeTransaction(transaction, booked) { + const editedTrans = { ...transaction }; + + editedTrans.creditorName = transaction.remittanceInformationStructured; + editedTrans.debtorName = transaction.remittanceInformationStructured; - return { - ...transaction, - payeeName: formatPayeeName(transaction), - date: transaction.bookingDate || transaction.valueDate, - }; + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, }; diff --git a/packages/sync-server/src/app-gocardless/banks/abnamro_abnanl2a.js b/packages/sync-server/src/app-gocardless/banks/abnamro_abnanl2a.js index 9749c4002bc..8999b57346a 100644 --- a/packages/sync-server/src/app-gocardless/banks/abnamro_abnanl2a.js +++ b/packages/sync-server/src/app-gocardless/banks/abnamro_abnanl2a.js @@ -1,4 +1,3 @@ -import { formatPayeeName } from '../../util/payee-name.js'; import { amountToInteger } from '../utils.js'; import Fallback from './integration-bank.js'; @@ -9,9 +8,11 @@ export default { institutionIds: ['ABNAMRO_ABNANL2A'], - normalizeTransaction(transaction, _booked) { + normalizeTransaction(transaction, booked) { + const editedTrans = { ...transaction }; + // There is no remittanceInformationUnstructured, so we'll make it - transaction.remittanceInformationUnstructured = + editedTrans.remittanceInformationUnstructured = transaction.remittanceInformationUnstructuredArray.join(', '); // Remove clutter to extract the payee from remittanceInformationUnstructured ... @@ -19,14 +20,13 @@ export default { const payeeName = transaction.remittanceInformationUnstructuredArray .map(el => el.match(/^(?:.*\*)?(.+),PAS\d+$/)) .find(match => match)?.[1]; - transaction.debtorName = transaction.debtorName || payeeName; - transaction.creditorName = transaction.creditorName || payeeName; - - return { - ...transaction, - payeeName: formatPayeeName(transaction), - date: transaction.valueDateTime.slice(0, 10), - }; + + editedTrans.debtorName = transaction.debtorName || payeeName; + editedTrans.creditorName = transaction.creditorName || payeeName; + + editedTrans.date = transaction.valueDateTime.slice(0, 10); + + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, sortTransactions(transactions = []) { diff --git a/packages/sync-server/src/app-gocardless/banks/american_express_aesudef1.js b/packages/sync-server/src/app-gocardless/banks/american_express_aesudef1.js index 418da7bbafb..e10e5e8c6a8 100644 --- a/packages/sync-server/src/app-gocardless/banks/american_express_aesudef1.js +++ b/packages/sync-server/src/app-gocardless/banks/american_express_aesudef1.js @@ -1,4 +1,3 @@ -import { formatPayeeName } from '../../util/payee-name.js'; import { amountToInteger } from '../utils.js'; import Fallback from './integration-bank.js'; @@ -21,14 +20,6 @@ export default { }; }, - normalizeTransaction(transaction, _booked) { - return { - ...transaction, - payeeName: formatPayeeName(transaction), - date: transaction.bookingDate, - }; - }, - /** * For AMERICAN_EXPRESS_AESUDEF1 we don't know what balance was * after each transaction so we have to calculate it by getting diff --git a/packages/sync-server/src/app-gocardless/banks/bancsabadell_bsabesbbb.js b/packages/sync-server/src/app-gocardless/banks/bancsabadell_bsabesbbb.js index 723de21c561..ecf6e2bba55 100644 --- a/packages/sync-server/src/app-gocardless/banks/bancsabadell_bsabesbbb.js +++ b/packages/sync-server/src/app-gocardless/banks/bancsabadell_bsabesbbb.js @@ -1,5 +1,3 @@ -import { formatPayeeName } from '../../util/payee-name.js'; - import Fallback from './integration-bank.js'; /** @type {import('./bank.interface.js').IBank} */ @@ -9,7 +7,9 @@ export default { institutionIds: ['BANCSABADELL_BSABESBB'], // Sabadell transactions don't get the creditorName/debtorName properly - normalizeTransaction(transaction, _booked) { + normalizeTransaction(transaction, booked) { + const editedTrans = { ...transaction }; + const amount = transaction.transactionAmount.amount; // The amount is negative for outgoing transactions, positive for incoming transactions. @@ -23,13 +23,9 @@ export default { const creditorName = isCreditorPayee ? payeeName : null; const debtorName = isCreditorPayee ? null : payeeName; - transaction.creditorName = creditorName; - transaction.debtorName = debtorName; + editedTrans.creditorName = creditorName; + editedTrans.debtorName = debtorName; - return { - ...transaction, - payeeName: formatPayeeName(transaction), - date: transaction.bookingDate || transaction.valueDate, - }; + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, }; diff --git a/packages/sync-server/src/app-gocardless/banks/bank.interface.ts b/packages/sync-server/src/app-gocardless/banks/bank.interface.ts index 4fc484d05fb..e0dfc47e726 100644 --- a/packages/sync-server/src/app-gocardless/banks/bank.interface.ts +++ b/packages/sync-server/src/app-gocardless/banks/bank.interface.ts @@ -4,6 +4,14 @@ import { NormalizedAccountDetails, } from '../gocardless.types.js'; +type TransactionExtended = Transaction & { + date?: string; + payeeName?: string; + notes?: string; + remittanceInformationUnstructuredArrayString?: string; + remittanceInformationStructuredArrayString?: string; +}; + export interface IBank { institutionIds: string[]; @@ -23,9 +31,10 @@ export interface IBank { * transaction date. */ normalizeTransaction: ( - transaction: Transaction, + transaction: TransactionExtended, booked: boolean, - ) => (Transaction & { date?: string; payeeName: string }) | null; + editedTransaction?: TransactionExtended, + ) => TransactionExtended | null; /** * Function sorts an array of transactions from newest to oldest diff --git a/packages/sync-server/src/app-gocardless/banks/bank_of_ireland_b365_bofiie2d.js b/packages/sync-server/src/app-gocardless/banks/bank_of_ireland_b365_bofiie2d.js index 959a0fd9865..0225bbbb9b5 100644 --- a/packages/sync-server/src/app-gocardless/banks/bank_of_ireland_b365_bofiie2d.js +++ b/packages/sync-server/src/app-gocardless/banks/bank_of_ireland_b365_bofiie2d.js @@ -7,11 +7,13 @@ export default { institutionIds: ['BANK_OF_IRELAND_B365_BOFIIE2D'], normalizeTransaction(transaction, booked) { - transaction.remittanceInformationUnstructured = fixupPayee( + const editedTrans = { ...transaction }; + + editedTrans.remittanceInformationUnstructured = fixupPayee( transaction.remittanceInformationUnstructured, ); - return Fallback.normalizeTransaction(transaction, booked); + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, }; diff --git a/packages/sync-server/src/app-gocardless/banks/bankinter_bkbkesmm.js b/packages/sync-server/src/app-gocardless/banks/bankinter_bkbkesmm.js index 0ef9b2106d8..fd824e1dd32 100644 --- a/packages/sync-server/src/app-gocardless/banks/bankinter_bkbkesmm.js +++ b/packages/sync-server/src/app-gocardless/banks/bankinter_bkbkesmm.js @@ -1,5 +1,3 @@ -import { formatPayeeName } from '../../util/payee-name.js'; - import Fallback from './integration-bank.js'; /** @type {import('./bank.interface.js').IBank} */ @@ -8,21 +6,19 @@ export default { institutionIds: ['BANKINTER_BKBKESMM'], - normalizeTransaction(transaction, _booked) { - transaction.remittanceInformationUnstructured = + normalizeTransaction(transaction, booked) { + const editedTrans = { ...transaction }; + + editedTrans.remittanceInformationUnstructured = transaction.remittanceInformationUnstructured .replaceAll(/\/Txt\/(\w\|)?/gi, '') .replaceAll(';', ' '); - transaction.debtorName = transaction.debtorName?.replaceAll(';', ' '); - transaction.creditorName = + editedTrans.debtorName = transaction.debtorName?.replaceAll(';', ' '); + editedTrans.creditorName = transaction.creditorName?.replaceAll(';', ' ') ?? - transaction.remittanceInformationUnstructured; + editedTrans.remittanceInformationUnstructured; - return { - ...transaction, - payeeName: formatPayeeName(transaction), - date: transaction.bookingDate || transaction.valueDate, - }; + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, }; diff --git a/packages/sync-server/src/app-gocardless/banks/belfius_gkccbebb.js b/packages/sync-server/src/app-gocardless/banks/belfius_gkccbebb.js index e5d93e2dc84..5b7ede23c16 100644 --- a/packages/sync-server/src/app-gocardless/banks/belfius_gkccbebb.js +++ b/packages/sync-server/src/app-gocardless/banks/belfius_gkccbebb.js @@ -1,5 +1,3 @@ -import { formatPayeeName } from '../../util/payee-name.js'; - import Fallback from './integration-bank.js'; /** @type {import('./bank.interface.js').IBank} */ @@ -11,12 +9,9 @@ export default { // The problem is that we have transaction with duplicated transaction ids. // This is not expected and the nordigen api has a work-around for some backs // They will set an internalTransactionId which is unique - normalizeTransaction(transaction, _booked) { - return { - ...transaction, - transactionId: transaction.internalTransactionId, - payeeName: formatPayeeName(transaction), - date: transaction.bookingDate || transaction.valueDate, - }; + normalizeTransaction(transaction, booked) { + transaction.transactionId = transaction.internalTransactionId; + + return Fallback.normalizeTransaction(transaction, booked); }, }; diff --git a/packages/sync-server/src/app-gocardless/banks/berliner_sparkasse_beladebexxx.js b/packages/sync-server/src/app-gocardless/banks/berliner_sparkasse_beladebexxx.js index 842bc27de8f..619b76f3c6d 100644 --- a/packages/sync-server/src/app-gocardless/banks/berliner_sparkasse_beladebexxx.js +++ b/packages/sync-server/src/app-gocardless/banks/berliner_sparkasse_beladebexxx.js @@ -1,4 +1,3 @@ -import { formatPayeeName } from '../../util/payee-name.js'; import { amountToInteger } from '../utils.js'; import Fallback from './integration-bank.js'; @@ -9,27 +8,8 @@ export default { institutionIds: ['BERLINER_SPARKASSE_BELADEBEXXX'], - /** - * Following the GoCardless documentation[0] we should prefer `bookingDate` - * here, though some of their bank integrations uses the date field - * differently from what's described in their documentation and so it's - * sometimes necessary to use `valueDate` instead. - * - * [0]: https://nordigen.zendesk.com/hc/en-gb/articles/7899367372829-valueDate-and-bookingDate-for-transactions - */ - normalizeTransaction(transaction, _booked) { - const date = - transaction.bookingDate || - transaction.bookingDateTime || - transaction.valueDate || - transaction.valueDateTime; - - // If we couldn't find a valid date field we filter out this transaction - // and hope that we will import it again once the bank has processed the - // transaction further. - if (!date) { - return null; - } + normalizeTransaction(transaction, booked) { + const editedTrans = { ...transaction }; let remittanceInformationUnstructured; @@ -54,15 +34,11 @@ export default { transaction.creditorName || transaction.debtorName; - transaction.creditorName = usefulCreditorName; - transaction.remittanceInformationUnstructured = + editedTrans.creditorName = usefulCreditorName; + editedTrans.remittanceInformationUnstructured = remittanceInformationUnstructured; - return { - ...transaction, - payeeName: formatPayeeName(transaction), - date: transaction.bookingDate || transaction.valueDate, - }; + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, /** diff --git a/packages/sync-server/src/app-gocardless/banks/bnp_be_gebabebb.js b/packages/sync-server/src/app-gocardless/banks/bnp_be_gebabebb.js index e24a1818fc0..28f4be55abe 100644 --- a/packages/sync-server/src/app-gocardless/banks/bnp_be_gebabebb.js +++ b/packages/sync-server/src/app-gocardless/banks/bnp_be_gebabebb.js @@ -1,5 +1,3 @@ -import { formatPayeeName } from '../../util/payee-name.js'; - import Fallback from './integration-bank.js'; /** @type {import('./bank.interface.js').IBank} */ @@ -22,7 +20,9 @@ export default { * The goal of the normalization is to place any relevant information of the additionalInformation * field in the remittanceInformationUnstructuredArray field. */ - normalizeTransaction(transaction, _booked) { + normalizeTransaction(transaction, booked) { + const editedTrans = { ...transaction }; + // Extract the creditor name to fill it in with information from the // additionalInformation field in case it's not yet defined. let creditorName = transaction.creditorName; @@ -49,7 +49,7 @@ export default { additionalInformationObject[key] = value; } // Keep existing unstructuredArray and add atmPosName and narrative - transaction.remittanceInformationUnstructuredArray = [ + editedTrans.remittanceInformationUnstructuredArray = [ transaction.remittanceInformationUnstructuredArray ?? '', additionalInformationObject?.atmPosName ?? '', additionalInformationObject?.narrative ?? '', @@ -66,12 +66,8 @@ export default { } } - transaction.creditorName = creditorName; + editedTrans.creditorName = creditorName; - return { - ...transaction, - payeeName: formatPayeeName(transaction), - date: transaction.valueDate || transaction.bookingDate, - }; + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, }; diff --git a/packages/sync-server/src/app-gocardless/banks/cbc_cregbebb.js b/packages/sync-server/src/app-gocardless/banks/cbc_cregbebb.js index b8e3bec7e68..832d15cd494 100644 --- a/packages/sync-server/src/app-gocardless/banks/cbc_cregbebb.js +++ b/packages/sync-server/src/app-gocardless/banks/cbc_cregbebb.js @@ -11,26 +11,21 @@ export default { * For negative amounts, the only payee information we have is returned in * remittanceInformationUnstructured. */ - normalizeTransaction(transaction, _booked) { - if (Number(transaction.transactionAmount.amount) > 0) { - return { - ...transaction, - payeeName: - transaction.debtorName || - transaction.remittanceInformationUnstructured, - date: transaction.bookingDate || transaction.valueDate, - }; - } + normalizeTransaction(transaction, booked) { + const editedTrans = { ...transaction }; - return { - ...transaction, - payeeName: + if (Number(transaction.transactionAmount.amount) > 0) { + editedTrans.payeeName = + transaction.debtorName || transaction.remittanceInformationUnstructured; + } else { + editedTrans.payeeName = transaction.creditorName || extractPayeeNameFromRemittanceInfo( transaction.remittanceInformationUnstructured, ['Paiement', 'Domiciliation', 'Transfert', 'Ordre permanent'], - ), - date: transaction.bookingDate || transaction.valueDate, - }; + ); + } + + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, }; diff --git a/packages/sync-server/src/app-gocardless/banks/commerzbank_cobadeff.js b/packages/sync-server/src/app-gocardless/banks/commerzbank_cobadeff.js index d58b98a191f..e2fe8bc9a2a 100644 --- a/packages/sync-server/src/app-gocardless/banks/commerzbank_cobadeff.js +++ b/packages/sync-server/src/app-gocardless/banks/commerzbank_cobadeff.js @@ -1,5 +1,3 @@ -import { formatPayeeName } from '../../util/payee-name.js'; - import Fallback from './integration-bank.js'; /** @type {import('./bank.interface.js').IBank} */ @@ -8,11 +6,13 @@ export default { institutionIds: ['COMMERZBANK_COBADEFF'], - normalizeTransaction(transaction, _booked) { + normalizeTransaction(transaction, booked) { + const editedTrans = { ...transaction }; + // remittanceInformationUnstructured is limited to 140 chars thus ... // ... missing information form remittanceInformationUnstructuredArray ... // ... so we recreate it. - transaction.remittanceInformationUnstructured = + editedTrans.remittanceInformationUnstructured = transaction.remittanceInformationUnstructuredArray.join(' '); // The limitations of remittanceInformationUnstructuredArray ... @@ -27,8 +27,8 @@ export default { 'Dauerauftrag', ]; keywords.forEach(keyword => { - transaction.remittanceInformationUnstructured = - transaction.remittanceInformationUnstructured.replace( + editedTrans.remittanceInformationUnstructured = + editedTrans.remittanceInformationUnstructured.replace( // There can be spaces in keywords RegExp(keyword.split('').join('\\s*'), 'gi'), ', ' + keyword + ' ', @@ -39,17 +39,13 @@ export default { // ... that are added to the remittanceInformation field), and ... // ... remove clutter like "End-to-End-Ref.: NOTPROVIDED" const payee = transaction.creditorName || transaction.debtorName || ''; - transaction.remittanceInformationUnstructured = - transaction.remittanceInformationUnstructured + editedTrans.remittanceInformationUnstructured = + editedTrans.remittanceInformationUnstructured .replace(/\s*(,)?\s+/g, '$1 ') .replace(RegExp(payee.split(' ').join('(/*| )'), 'gi'), ' ') .replace(', End-to-End-Ref.: NOTPROVIDED', '') .trim(); - return { - ...transaction, - payeeName: formatPayeeName(transaction), - date: transaction.bookingDate, - }; + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, }; diff --git a/packages/sync-server/src/app-gocardless/banks/danskebank_dabno22.js b/packages/sync-server/src/app-gocardless/banks/danskebank_dabno22.js index 215925dfa2c..94a34c648d5 100644 --- a/packages/sync-server/src/app-gocardless/banks/danskebank_dabno22.js +++ b/packages/sync-server/src/app-gocardless/banks/danskebank_dabno22.js @@ -1,4 +1,3 @@ -import { formatPayeeName } from '../../util/payee-name.js'; import { amountToInteger } from '../utils.js'; import Fallback from './integration-bank.js'; @@ -9,7 +8,9 @@ export default { institutionIds: ['DANSKEBANK_DABANO22'], - normalizeTransaction(transaction, _booked) { + normalizeTransaction(transaction, booked) { + const editedTrans = { ...transaction }; + /** * Danske Bank appends the EndToEndID: NOTPROVIDED to * remittanceInformationUnstructured, cluttering the data. @@ -17,21 +18,13 @@ export default { * We clean thais up by removing any instances of this string from all transactions. * */ - transaction.remittanceInformationUnstructured = + editedTrans.remittanceInformationUnstructured = transaction.remittanceInformationUnstructured.replace( '\nEndToEndID: NOTPROVIDED', '', ); - /** - * The valueDate in transactions from Danske Bank is not the one expected, but rather the date - * the funds are expected to be paid back for credit accounts. - */ - return { - ...transaction, - payeeName: formatPayeeName(transaction), - date: transaction.bookingDate, - }; + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, calculateStartingBalance(sortedTransactions = [], balances = []) { diff --git a/packages/sync-server/src/app-gocardless/banks/direkt_heladef1822.js b/packages/sync-server/src/app-gocardless/banks/direkt_heladef1822.js index 74a2393547b..4d753e0326c 100644 --- a/packages/sync-server/src/app-gocardless/banks/direkt_heladef1822.js +++ b/packages/sync-server/src/app-gocardless/banks/direkt_heladef1822.js @@ -7,10 +7,12 @@ export default { institutionIds: ['DIREKT_HELADEF1822'], normalizeTransaction(transaction, booked) { - transaction.remittanceInformationUnstructured = + const editedTrans = { ...transaction }; + + editedTrans.remittanceInformationUnstructured = transaction.remittanceInformationUnstructured ?? transaction.remittanceInformationStructured; - return Fallback.normalizeTransaction(transaction, booked); + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, }; diff --git a/packages/sync-server/src/app-gocardless/banks/easybank_bawaatww.js b/packages/sync-server/src/app-gocardless/banks/easybank_bawaatww.js index 7becc29b2e6..c5a88596111 100644 --- a/packages/sync-server/src/app-gocardless/banks/easybank_bawaatww.js +++ b/packages/sync-server/src/app-gocardless/banks/easybank_bawaatww.js @@ -21,24 +21,14 @@ export default { return parseInt(b.transactionId) - parseInt(a.transactionId); }), - normalizeTransaction(transaction, _booked) { - const date = transaction.bookingDate || transaction.valueDate; - - // If we couldn't find a valid date field we filter out this transaction - // and hope that we will import it again once the bank has processed the - // transaction further. - if (!date) { - return null; - } + normalizeTransaction(transaction, booked) { + const editedTrans = { ...transaction }; let payeeName = formatPayeeName(transaction); if (!payeeName) payeeName = extractPayeeName(transaction); + editedTrans.payeeName = payeeName; - return { - ...transaction, - payeeName, - date: d.format(d.parseISO(date), 'yyyy-MM-dd'), - }; + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, }; diff --git a/packages/sync-server/src/app-gocardless/banks/entercard_swednokk.js b/packages/sync-server/src/app-gocardless/banks/entercard_swednokk.js index 0cfa09797ce..ce7ddf14223 100644 --- a/packages/sync-server/src/app-gocardless/banks/entercard_swednokk.js +++ b/packages/sync-server/src/app-gocardless/banks/entercard_swednokk.js @@ -1,6 +1,3 @@ -import * as d from 'date-fns'; - -import { formatPayeeName } from '../../util/payee-name.js'; import { amountToInteger } from '../utils.js'; import Fallback from './integration-bank.js'; @@ -11,7 +8,9 @@ export default { institutionIds: ['ENTERCARD_SWEDNOKK'], - normalizeTransaction(transaction, _booked) { + normalizeTransaction(transaction, booked) { + const editedTrans = { ...transaction }; + // GoCardless's Entercard integration returns forex transactions with the // foreign amount in `transactionAmount`, but at least the amount actually // billed to the account is now available in @@ -25,11 +24,9 @@ export default { }; } - return { - ...transaction, - payeeName: formatPayeeName(transaction), - date: d.format(d.parseISO(transaction.valueDate), 'yyyy-MM-dd'), - }; + editedTrans.date = transaction.valueDate; + + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, calculateStartingBalance(sortedTransactions = [], balances = []) { diff --git a/packages/sync-server/src/app-gocardless/banks/fortuneo_ftnofrp1xxx.js b/packages/sync-server/src/app-gocardless/banks/fortuneo_ftnofrp1xxx.js index 3dab221946a..b84452ec7c8 100644 --- a/packages/sync-server/src/app-gocardless/banks/fortuneo_ftnofrp1xxx.js +++ b/packages/sync-server/src/app-gocardless/banks/fortuneo_ftnofrp1xxx.js @@ -1,5 +1,3 @@ -import * as d from 'date-fns'; - import { formatPayeeName } from '../../util/payee-name.js'; import Fallback from './integration-bank.js'; @@ -10,18 +8,8 @@ export default { institutionIds: ['FORTUNEO_FTNOFRP1XXX'], - normalizeTransaction(transaction, _booked) { - const date = - transaction.bookingDate || - transaction.bookingDateTime || - transaction.valueDate || - transaction.valueDateTime; - // If we couldn't find a valid date field we filter out this transaction - // and hope that we will import it again once the bank has processed the - // transaction further. - if (!date) { - return null; - } + normalizeTransaction(transaction, booked) { + const editedTrans = { ...transaction }; // Most of the information from the transaction is in the remittanceInformationUnstructuredArray field. // We extract the creditor and debtor names from this field. @@ -50,13 +38,9 @@ export default { const creditorName = isCreditorPayee ? payeeName : null; const debtorName = isCreditorPayee ? null : payeeName; - transaction.creditorName = creditorName; - transaction.debtorName = debtorName; + editedTrans.creditorName = creditorName; + editedTrans.debtorName = debtorName; - return { - ...transaction, - payeeName: formatPayeeName(transaction), - date: d.format(d.parseISO(date), 'yyyy-MM-dd'), - }; + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, }; diff --git a/packages/sync-server/src/app-gocardless/banks/hype_hyeeit22.js b/packages/sync-server/src/app-gocardless/banks/hype_hyeeit22.js index e165a2f51b1..deef0019672 100644 --- a/packages/sync-server/src/app-gocardless/banks/hype_hyeeit22.js +++ b/packages/sync-server/src/app-gocardless/banks/hype_hyeeit22.js @@ -1,5 +1,3 @@ -import { formatPayeeName } from '../../util/payee-name.js'; - import Fallback from './integration-bank.js'; /** @type {import('./bank.interface.js').IBank} */ @@ -8,13 +6,15 @@ export default { institutionIds: ['HYPE_HYEEIT22'], - normalizeTransaction(transaction, _booked) { + normalizeTransaction(transaction, booked) { + const editedTrans = { ...transaction }; + /** Online card payments - identified by "crd" transaction code * always start with PAGAMENTO PRESSO + */ if (transaction.proprietaryBankTransactionCode === 'crd') { // remove PAGAMENTO PRESSO and set payee name - transaction.debtorName = + editedTrans.debtorName = transaction.remittanceInformationUnstructured?.slice( 'PAGAMENTO PRESSO '.length, ); @@ -32,7 +32,7 @@ export default { // NOTE: if {payee_name} contains dashes (unlikely / impossible?), this probably gets bugged! const infoIdx = transaction.remittanceInformationUnstructured.indexOf(' - ') + 3; - transaction.remittanceInformationUnstructured = + editedTrans.remittanceInformationUnstructured = infoIdx === -1 ? transaction.remittanceInformationUnstructured : transaction.remittanceInformationUnstructured.slice(infoIdx).trim(); @@ -64,12 +64,11 @@ export default { idx = str.indexOf('\\U'); // slight inefficiency? start_idx = idx; } - transaction.remittanceInformationUnstructured = str; + editedTrans.remittanceInformationUnstructured = str; } - return { - ...transaction, - payeeName: formatPayeeName(transaction), - date: transaction.valueDate || transaction.bookingDate, - }; + + editedTrans.date = transaction.valueDate || transaction.bookingDate; + + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, }; diff --git a/packages/sync-server/src/app-gocardless/banks/ing_ingbrobu.js b/packages/sync-server/src/app-gocardless/banks/ing_ingbrobu.js index b514d78e8d4..4305502b6ea 100644 --- a/packages/sync-server/src/app-gocardless/banks/ing_ingbrobu.js +++ b/packages/sync-server/src/app-gocardless/banks/ing_ingbrobu.js @@ -7,6 +7,8 @@ export default { institutionIds: ['ING_INGBROBU'], normalizeTransaction(transaction, booked) { + const editedTrans = { ...transaction }; + //Merchant transactions all have the same transactionId of 'NOTPROVIDED'. //For booked transactions, this can be set to the internalTransactionId //For pending transactions, this needs to be removed for them to show up in Actual @@ -19,7 +21,7 @@ export default { transaction.proprietaryBankTransactionCode && !transaction.remittanceInformationUnstructured ) { - transaction.remittanceInformationUnstructured = + editedTrans.remittanceInformationUnstructured = transaction.proprietaryBankTransactionCode; } @@ -31,11 +33,11 @@ export default { .toLowerCase() .includes('card no:') ) { - transaction.creditorName = + editedTrans.creditorName = transaction.remittanceInformationUnstructured.split(',')[0]; //Catch all case for other types of payees } else { - transaction.creditorName = + editedTrans.creditorName = transaction.remittanceInformationUnstructured; } } else { @@ -47,22 +49,22 @@ export default { .toLowerCase() .includes('card no:') ) { - transaction.creditorName = + editedTrans.creditorName = transaction.remittanceInformationUnstructured.replace( /x{4}/g, 'Xxxx ', ); //Catch all case for other types of payees } else { - transaction.creditorName = + editedTrans.creditorName = transaction.remittanceInformationUnstructured; } //Remove remittanceInformationUnstructured from pending transactions, so the `notes` field remains empty (there is no merchant information) //Once booked, the right `notes` (containing the merchant) will be populated - transaction.remittanceInformationUnstructured = null; + editedTrans.remittanceInformationUnstructured = null; } } - return Fallback.normalizeTransaction(transaction, booked); + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, }; diff --git a/packages/sync-server/src/app-gocardless/banks/ing_ingddeff.js b/packages/sync-server/src/app-gocardless/banks/ing_ingddeff.js index 9d4ea894b73..bb77aa0d854 100644 --- a/packages/sync-server/src/app-gocardless/banks/ing_ingddeff.js +++ b/packages/sync-server/src/app-gocardless/banks/ing_ingddeff.js @@ -1,4 +1,3 @@ -import { formatPayeeName } from '../../util/payee-name.js'; import { amountToInteger } from '../utils.js'; import Fallback from './integration-bank.js'; @@ -9,20 +8,18 @@ export default { institutionIds: ['ING_INGDDEFF'], - normalizeTransaction(transaction, _booked) { + normalizeTransaction(transaction, booked) { + const editedTrans = { ...transaction }; + const remittanceInformationMatch = /remittanceinformation:(.*)$/.exec( transaction.remittanceInformationUnstructured, ); - transaction.remittanceInformationUnstructured = remittanceInformationMatch + editedTrans.remittanceInformationUnstructured = remittanceInformationMatch ? remittanceInformationMatch[1] : transaction.remittanceInformationUnstructured; - return { - ...transaction, - payeeName: formatPayeeName(transaction), - date: transaction.bookingDate || transaction.valueDate, - }; + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, sortTransactions(transactions = []) { diff --git a/packages/sync-server/src/app-gocardless/banks/ing_pl_ingbplpw.js b/packages/sync-server/src/app-gocardless/banks/ing_pl_ingbplpw.js index 5e7ead0f858..2f7e69aa107 100644 --- a/packages/sync-server/src/app-gocardless/banks/ing_pl_ingbplpw.js +++ b/packages/sync-server/src/app-gocardless/banks/ing_pl_ingbplpw.js @@ -1,4 +1,3 @@ -import { formatPayeeName } from '../../util/payee-name.js'; import { amountToInteger } from '../utils.js'; import Fallback from './integration-bank.js'; @@ -9,12 +8,12 @@ export default { institutionIds: ['ING_PL_INGBPLPW'], - normalizeTransaction(transaction, _booked) { - return { - ...transaction, - payeeName: formatPayeeName(transaction), - date: transaction.valueDate ?? transaction.bookingDate, - }; + normalizeTransaction(transaction, booked) { + const editedTrans = { ...transaction }; + + editedTrans.date = transaction.valueDate; + + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, sortTransactions(transactions = []) { diff --git a/packages/sync-server/src/app-gocardless/banks/integration-bank.js b/packages/sync-server/src/app-gocardless/banks/integration-bank.js index a4cd882a097..adb5e94a225 100644 --- a/packages/sync-server/src/app-gocardless/banks/integration-bank.js +++ b/packages/sync-server/src/app-gocardless/banks/integration-bank.js @@ -44,22 +44,38 @@ export default { }; }, - normalizeTransaction(transaction, _booked) { + normalizeTransaction(transaction, _booked, editedTransaction = null) { + const trans = editedTransaction ?? transaction; + const date = + trans.date || transaction.bookingDate || transaction.bookingDateTime || transaction.valueDate || transaction.valueDateTime; + // If we couldn't find a valid date field we filter out this transaction // and hope that we will import it again once the bank has processed the // transaction further. if (!date) { return null; } + + const notes = + trans.notes ?? + trans.remittanceInformationUnstructured ?? + trans.remittanceInformationUnstructuredArray?.join(' '); + + transaction.remittanceInformationUnstructuredArrayString = + transaction.remittanceInformationUnstructuredArray?.join(','); + transaction.remittanceInformationStructuredArrayString = + transaction.remittanceInformationStructuredArray?.join(','); + return { ...transaction, - payeeName: formatPayeeName(transaction), + payeeName: trans.payeeName ?? formatPayeeName(trans), date: d.format(d.parseISO(date), 'yyyy-MM-dd'), + notes, }; }, diff --git a/packages/sync-server/src/app-gocardless/banks/isybank_itbbitmm.js b/packages/sync-server/src/app-gocardless/banks/isybank_itbbitmm.js index a6685ccae02..a927412facf 100644 --- a/packages/sync-server/src/app-gocardless/banks/isybank_itbbitmm.js +++ b/packages/sync-server/src/app-gocardless/banks/isybank_itbbitmm.js @@ -9,8 +9,10 @@ export default { // It has been reported that valueDate is more accurate than booking date // when it is provided normalizeTransaction(transaction, booked) { - transaction.bookingDate = transaction.valueDate ?? transaction.bookingDate; + const editedTrans = { ...transaction }; - return Fallback.normalizeTransaction(transaction, booked); + editedTrans.date = transaction.valueDate ?? transaction.bookingDate; + + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, }; diff --git a/packages/sync-server/src/app-gocardless/banks/kbc_kredbebb.js b/packages/sync-server/src/app-gocardless/banks/kbc_kredbebb.js index bd9df6f90a7..477686318ee 100644 --- a/packages/sync-server/src/app-gocardless/banks/kbc_kredbebb.js +++ b/packages/sync-server/src/app-gocardless/banks/kbc_kredbebb.js @@ -11,27 +11,23 @@ export default { * For negative amounts, the only payee information we have is returned in * remittanceInformationUnstructured. */ - normalizeTransaction(transaction, _booked) { - if (Number(transaction.transactionAmount.amount) > 0) { - return { - ...transaction, - payeeName: - transaction.debtorName || - transaction.remittanceInformationUnstructured || - 'undefined', - date: transaction.bookingDate || transaction.valueDate, - }; - } + normalizeTransaction(transaction, booked) { + const editedTrans = { ...transaction }; - return { - ...transaction, - payeeName: + if (Number(transaction.transactionAmount.amount) > 0) { + editedTrans.payeeName = + transaction.debtorName || + transaction.remittanceInformationUnstructured || + 'undefined'; + } else { + editedTrans.payeeName = transaction.creditorName || extractPayeeNameFromRemittanceInfo( transaction.remittanceInformationUnstructured, ['Betaling met', 'Domiciliëring', 'Overschrijving'], - ), - date: transaction.bookingDate || transaction.valueDate, - }; + ); + } + + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, }; diff --git a/packages/sync-server/src/app-gocardless/banks/lhv-lhvbee22.js b/packages/sync-server/src/app-gocardless/banks/lhv-lhvbee22.js index 8f948c6508b..1f3f19664ac 100644 --- a/packages/sync-server/src/app-gocardless/banks/lhv-lhvbee22.js +++ b/packages/sync-server/src/app-gocardless/banks/lhv-lhvbee22.js @@ -9,6 +9,8 @@ export default { institutionIds: ['LHV_LHVBEE22'], normalizeTransaction(transaction, booked) { + const editedTrans = { ...transaction }; + // extract bookingDate and creditorName for card transactions, e.g. // (..1234) 2025-01-02 09:32 CrustumOU\Poordi 3\Tallinn\10156 ESTEST // bookingDate: 2025-01-02 @@ -22,19 +24,13 @@ export default { if (cardTxMatch) { const extractedDate = d.parse(cardTxMatch[2], 'yyyy-MM-dd', new Date()); - transaction = { - ...transaction, - creditorName: cardTxMatch[4].split('\\')[0].trim(), - }; + editedTrans.payeeName = cardTxMatch[4].split('\\')[0].trim(); if (booked && d.isValid(extractedDate)) { - transaction = { - ...transaction, - bookingDate: d.format(extractedDate, 'yyyy-MM-dd'), - }; + editedTrans.date = d.format(extractedDate, 'yyyy-MM-dd'); } } - return Fallback.normalizeTransaction(transaction, booked); + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, }; diff --git a/packages/sync-server/src/app-gocardless/banks/mbank_retail_brexplpw.js b/packages/sync-server/src/app-gocardless/banks/mbank_retail_brexplpw.js index 7338b35ec8f..6e15cd8f9c9 100644 --- a/packages/sync-server/src/app-gocardless/banks/mbank_retail_brexplpw.js +++ b/packages/sync-server/src/app-gocardless/banks/mbank_retail_brexplpw.js @@ -1,4 +1,3 @@ -import { formatPayeeName } from '../../util/payee-name.js'; import { amountToInteger } from '../utils.js'; import Fallback from './integration-bank.js'; @@ -9,14 +8,6 @@ export default { institutionIds: ['MBANK_RETAIL_BREXPLPW'], - normalizeTransaction(transaction, _booked) { - return { - ...transaction, - payeeName: formatPayeeName(transaction), - date: transaction.bookingDate || transaction.valueDate, - }; - }, - sortTransactions(transactions = []) { return transactions.sort( (a, b) => Number(b.transactionId) - Number(a.transactionId), diff --git a/packages/sync-server/src/app-gocardless/banks/nationwide_naiagb21.js b/packages/sync-server/src/app-gocardless/banks/nationwide_naiagb21.js index fdcb933527d..22efe83a5ee 100644 --- a/packages/sync-server/src/app-gocardless/banks/nationwide_naiagb21.js +++ b/packages/sync-server/src/app-gocardless/banks/nationwide_naiagb21.js @@ -7,6 +7,8 @@ export default { institutionIds: ['NATIONWIDE_NAIAGB21'], normalizeTransaction(transaction, booked) { + const editedTrans = { ...transaction }; + // Nationwide can sometimes return pending transactions with a date // representing the latest a transaction could be booked. This stops // actual's deduplication logic from working as it only checks 7 days @@ -19,7 +21,7 @@ export default { new Date().getTime(), ), ); - transaction.bookingDate = useDate.toISOString().slice(0, 10); + editedTrans.date = useDate.toISOString().slice(0, 10); } // Nationwide also occasionally returns erroneous transaction_ids @@ -39,6 +41,6 @@ export default { transaction.transactionId = null; } - return Fallback.normalizeTransaction(transaction, booked); + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, }; diff --git a/packages/sync-server/src/app-gocardless/banks/nbg_ethngraaxxx.js b/packages/sync-server/src/app-gocardless/banks/nbg_ethngraaxxx.js index 8bce27d75d0..60361ebb402 100644 --- a/packages/sync-server/src/app-gocardless/banks/nbg_ethngraaxxx.js +++ b/packages/sync-server/src/app-gocardless/banks/nbg_ethngraaxxx.js @@ -1,4 +1,3 @@ -import { formatPayeeName } from '../../util/payee-name.js'; import { amountToInteger } from '../utils.js'; import Fallback from './integration-bank.js'; @@ -14,27 +13,22 @@ export default { * - Corrects amount to negative (nbg erroneously omits the minus sign in pending transactions) * - Removes prefix 'ΑΓΟΡΑ' from remittance information to align with the booked transaction (necessary for fuzzy matching to work) */ - normalizeTransaction(transaction, _booked) { + normalizeTransaction(transaction, booked) { + const editedTrans = { ...transaction }; + if ( !transaction.transactionId && transaction.remittanceInformationUnstructured.startsWith('ΑΓΟΡΑ ') ) { - transaction = { - ...transaction, - transactionAmount: { - amount: '-' + transaction.transactionAmount.amount, - currency: transaction.transactionAmount.currency, - }, - remittanceInformationUnstructured: - transaction.remittanceInformationUnstructured.substring(6), + transaction.transactionAmount = { + amount: '-' + transaction.transactionAmount.amount, + currency: transaction.transactionAmount.currency, }; + editedTrans.remittanceInformationUnstructured = + transaction.remittanceInformationUnstructured.substring(6); } - return { - ...transaction, - payeeName: formatPayeeName(transaction), - date: transaction.bookingDate || transaction.valueDate, - }; + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, /** diff --git a/packages/sync-server/src/app-gocardless/banks/norwegian_xx_norwnok1.js b/packages/sync-server/src/app-gocardless/banks/norwegian_xx_norwnok1.js index 05c3498da50..3846dd7cbd7 100644 --- a/packages/sync-server/src/app-gocardless/banks/norwegian_xx_norwnok1.js +++ b/packages/sync-server/src/app-gocardless/banks/norwegian_xx_norwnok1.js @@ -1,4 +1,3 @@ -import { formatPayeeName } from '../../util/payee-name.js'; import { amountToInteger } from '../utils.js'; import Fallback from './integration-bank.js'; @@ -17,12 +16,11 @@ export default { ], normalizeTransaction(transaction, booked) { + const editedTrans = { ...transaction }; + if (booked) { - return { - ...transaction, - payeeName: formatPayeeName(transaction), - date: transaction.bookingDate, - }; + editedTrans.date = transaction.bookingDate; + return Fallback.normalizeTransaction(transaction, booked, editedTrans); } /** @@ -38,11 +36,8 @@ export default { * once the bank has processed it further. */ if (transaction.valueDate !== undefined) { - return { - ...transaction, - payeeName: formatPayeeName(transaction), - date: transaction.valueDate, - }; + editedTrans.date = transaction.valueDate; + return Fallback.normalizeTransaction(transaction, booked, editedTrans); } if (transaction.remittanceInformationStructured) { @@ -50,12 +45,8 @@ export default { const matches = transaction.remittanceInformationStructured.match(remittanceInfoRegex); if (matches) { - transaction.valueDate = matches[1]; - return { - ...transaction, - payeeName: formatPayeeName(transaction), - date: matches[1], - }; + editedTrans.date = matches[1]; + return Fallback.normalizeTransaction(transaction, booked, editedTrans); } } diff --git a/packages/sync-server/src/app-gocardless/banks/revolut_revolt21.js b/packages/sync-server/src/app-gocardless/banks/revolut_revolt21.js index 1e25618c8b2..b5fb4321624 100644 --- a/packages/sync-server/src/app-gocardless/banks/revolut_revolt21.js +++ b/packages/sync-server/src/app-gocardless/banks/revolut_revolt21.js @@ -1,7 +1,3 @@ -import * as d from 'date-fns'; - -import { formatPayeeName } from '../../util/payee-name.js'; - import Fallback from './integration-bank.js'; /** @type {import('./bank.interface.js').IBank} */ @@ -10,29 +6,21 @@ export default { institutionIds: ['REVOLUT_REVOLT21'], - normalizeTransaction(transaction, _booked) { + normalizeTransaction(transaction, booked) { + const editedTrans = { ...transaction }; + if ( transaction.remittanceInformationUnstructuredArray[0].startsWith( 'Bizum payment from: ', ) ) { - const date = - transaction.bookingDate || - transaction.bookingDateTime || - transaction.valueDate || - transaction.valueDateTime; - - return { - ...transaction, - payeeName: - transaction.remittanceInformationUnstructuredArray[0].replace( - 'Bizum payment from: ', - '', - ), - remittanceInformationUnstructured: - transaction.remittanceInformationUnstructuredArray[1], - date: d.format(d.parseISO(date), 'yyyy-MM-dd'), - }; + editedTrans.payeeName = + transaction.remittanceInformationUnstructuredArray[0].replace( + 'Bizum payment from: ', + '', + ); + editedTrans.remittanceInformationUnstructured = + transaction.remittanceInformationUnstructuredArray[1]; } if ( @@ -40,21 +28,10 @@ export default { 'Bizum payment to: ', ) ) { - const date = - transaction.bookingDate || - transaction.bookingDateTime || - transaction.valueDate || - transaction.valueDateTime; - - return { - ...transaction, - payeeName: formatPayeeName(transaction), - remittanceInformationUnstructured: - transaction.remittanceInformationUnstructuredArray[1], - date: d.format(d.parseISO(date), 'yyyy-MM-dd'), - }; + editedTrans.remittanceInformationUnstructured = + transaction.remittanceInformationUnstructuredArray[1]; } - return Fallback.normalizeTransaction(transaction, _booked); + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, }; diff --git a/packages/sync-server/src/app-gocardless/banks/sandboxfinance_sfin0000.js b/packages/sync-server/src/app-gocardless/banks/sandboxfinance_sfin0000.js index 5e61ccbd830..f63a3c9c8d9 100644 --- a/packages/sync-server/src/app-gocardless/banks/sandboxfinance_sfin0000.js +++ b/packages/sync-server/src/app-gocardless/banks/sandboxfinance_sfin0000.js @@ -1,4 +1,3 @@ -import { formatPayeeName } from '../../util/payee-name.js'; import { amountToInteger } from '../utils.js'; import Fallback from './integration-bank.js'; @@ -9,22 +8,6 @@ export default { institutionIds: ['SANDBOXFINANCE_SFIN0000'], - /** - * Following the GoCardless documentation[0] we should prefer `bookingDate` - * here, though some of their bank integrations uses the date field - * differently from what's described in their documentation and so it's - * sometimes necessary to use `valueDate` instead. - * - * [0]: https://nordigen.zendesk.com/hc/en-gb/articles/7899367372829-valueDate-and-bookingDate-for-transactions - */ - normalizeTransaction(transaction, _booked) { - return { - ...transaction, - payeeName: formatPayeeName(transaction), - date: transaction.bookingDate || transaction.valueDate, - }; - }, - /** * For SANDBOXFINANCE_SFIN0000 we don't know what balance was * after each transaction so we have to calculate it by getting diff --git a/packages/sync-server/src/app-gocardless/banks/seb_kort_bank_ab.js b/packages/sync-server/src/app-gocardless/banks/seb_kort_bank_ab.js index 2db12b4eea0..241ceaefb22 100644 --- a/packages/sync-server/src/app-gocardless/banks/seb_kort_bank_ab.js +++ b/packages/sync-server/src/app-gocardless/banks/seb_kort_bank_ab.js @@ -1,4 +1,3 @@ -import { formatPayeeName } from '../../util/payee-name.js'; import { amountToInteger } from '../utils.js'; import Fallback from './integration-bank.js'; @@ -16,20 +15,18 @@ export default { /** * Sign of transaction amount needs to be flipped for SEB credit cards */ - normalizeTransaction(transaction, _booked) { + normalizeTransaction(transaction, booked) { + const editedTrans = { ...transaction }; + // Creditor name is stored in additionInformation for SEB - transaction.creditorName = transaction.additionalInformation; + editedTrans.creditorName = transaction.additionalInformation; transaction.transactionAmount = { // Flip transaction amount sign amount: (-parseFloat(transaction.transactionAmount.amount)).toString(), currency: transaction.transactionAmount.currency, }; - return { - ...transaction, - payeeName: formatPayeeName(transaction), - date: transaction.valueDate, - }; + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, /** diff --git a/packages/sync-server/src/app-gocardless/banks/seb_privat.js b/packages/sync-server/src/app-gocardless/banks/seb_privat.js index c4baf7f4226..c7725319cf6 100644 --- a/packages/sync-server/src/app-gocardless/banks/seb_privat.js +++ b/packages/sync-server/src/app-gocardless/banks/seb_privat.js @@ -1,6 +1,3 @@ -import * as d from 'date-fns'; - -import { formatPayeeName } from '../../util/payee-name.js'; import { amountToInteger } from '../utils.js'; import Fallback from './integration-bank.js'; @@ -11,27 +8,13 @@ export default { institutionIds: ['SEB_ESSESESS_PRIVATE'], - normalizeTransaction(transaction, _booked) { - const date = - transaction.bookingDate || - transaction.bookingDateTime || - transaction.valueDate || - transaction.valueDateTime; - // If we couldn't find a valid date field we filter out this transaction - // and hope that we will import it again once the bank has processed the - // transaction further. - if (!date) { - return null; - } + normalizeTransaction(transaction, booked) { + const editedTrans = { ...transaction }; // Creditor name is stored in additionInformation for SEB - transaction.creditorName = transaction.additionalInformation; + editedTrans.creditorName = transaction.additionalInformation; - return { - ...transaction, - payeeName: formatPayeeName(transaction), - date: d.format(d.parseISO(date), 'yyyy-MM-dd'), - }; + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, calculateStartingBalance(sortedTransactions = [], balances = []) { diff --git a/packages/sync-server/src/app-gocardless/banks/sparnord_spnodk22.js b/packages/sync-server/src/app-gocardless/banks/sparnord_spnodk22.js index 7364d8c5640..9f5dec860c3 100644 --- a/packages/sync-server/src/app-gocardless/banks/sparnord_spnodk22.js +++ b/packages/sync-server/src/app-gocardless/banks/sparnord_spnodk22.js @@ -1,5 +1,3 @@ -import { formatPayeeName } from '../../util/payee-name.js'; - import Fallback from './integration-bank.js'; /** @type {import('./bank.interface.js').IBank} */ @@ -15,14 +13,12 @@ export default { /** * Banks on the BEC backend only give information regarding the transaction in additionalInformation */ - normalizeTransaction(transaction, _booked) { - transaction.remittanceInformationUnstructured = + normalizeTransaction(transaction, booked) { + const editedTrans = { ...transaction }; + + editedTrans.remittanceInformationUnstructured = transaction.additionalInformation; - return { - ...transaction, - payeeName: formatPayeeName(transaction), - date: transaction.bookingDate, - }; + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, }; diff --git a/packages/sync-server/src/app-gocardless/banks/spk_karlsruhe_karsde66.js b/packages/sync-server/src/app-gocardless/banks/spk_karlsruhe_karsde66.js index c3c2f1d378f..59dda43cbe5 100644 --- a/packages/sync-server/src/app-gocardless/banks/spk_karlsruhe_karsde66.js +++ b/packages/sync-server/src/app-gocardless/banks/spk_karlsruhe_karsde66.js @@ -1,4 +1,3 @@ -import { formatPayeeName } from '../../util/payee-name.js'; import { amountToInteger } from '../utils.js'; import Fallback from './integration-bank.js'; @@ -9,27 +8,8 @@ export default { institutionIds: ['SPK_KARLSRUHE_KARSDE66XXX'], - /** - * Following the GoCardless documentation[0] we should prefer `bookingDate` - * here, though some of their bank integrations uses the date field - * differently from what's described in their documentation and so it's - * sometimes necessary to use `valueDate` instead. - * - * [0]: https://nordigen.zendesk.com/hc/en-gb/articles/7899367372829-valueDate-and-bookingDate-for-transactions - */ - normalizeTransaction(transaction, _booked) { - const date = - transaction.bookingDate || - transaction.bookingDateTime || - transaction.valueDate || - transaction.valueDateTime; - - // If we couldn't find a valid date field we filter out this transaction - // and hope that we will import it again once the bank has processed the - // transaction further. - if (!date) { - return null; - } + normalizeTransaction(transaction, booked) { + const editedTrans = { ...transaction }; let remittanceInformationUnstructured; @@ -54,15 +34,11 @@ export default { transaction.creditorName || transaction.debtorName; - transaction.creditorName = usefulCreditorName; - transaction.remittanceInformationUnstructured = + editedTrans.creditorName = usefulCreditorName; + editedTrans.remittanceInformationUnstructured = remittanceInformationUnstructured; - return { - ...transaction, - payeeName: formatPayeeName(transaction), - date: transaction.bookingDate || transaction.valueDate, - }; + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, /** diff --git a/packages/sync-server/src/app-gocardless/banks/spk_marburg_biedenkopf_heladef1mar.js b/packages/sync-server/src/app-gocardless/banks/spk_marburg_biedenkopf_heladef1mar.js index 9dea0dafc17..e5c5079830e 100644 --- a/packages/sync-server/src/app-gocardless/banks/spk_marburg_biedenkopf_heladef1mar.js +++ b/packages/sync-server/src/app-gocardless/banks/spk_marburg_biedenkopf_heladef1mar.js @@ -1,7 +1,3 @@ -import d from 'date-fns'; - -import { formatPayeeName } from '../../util/payee-name.js'; - import Fallback from './integration-bank.js'; /** @type {import('./bank.interface.js').IBank} */ @@ -10,19 +6,8 @@ export default { institutionIds: ['SPK_MARBURG_BIEDENKOPF_HELADEF1MAR'], - normalizeTransaction(transaction, _booked) { - const date = - transaction.bookingDate || - transaction.bookingDateTime || - transaction.valueDate || - transaction.valueDateTime; - - // If we couldn't find a valid date field we filter out this transaction - // and hope that we will import it again once the bank has processed the - // transaction further. - if (!date) { - return null; - } + normalizeTransaction(transaction, booked) { + const editedTrans = { ...transaction }; let remittanceInformationUnstructured; @@ -37,13 +22,9 @@ export default { transaction.remittanceInformationStructuredArray?.join(' '); } - transaction.remittanceInformationUnstructured = + editedTrans.remittanceInformationUnstructured = remittanceInformationUnstructured; - return { - ...transaction, - payeeName: formatPayeeName(transaction), - date: d.format(d.parseISO(date), 'yyyy-MM-dd'), - }; + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, }; diff --git a/packages/sync-server/src/app-gocardless/banks/spk_worms_alzey_ried_malade51wor.js b/packages/sync-server/src/app-gocardless/banks/spk_worms_alzey_ried_malade51wor.js index dbc1b1f90b6..06aaea5f004 100644 --- a/packages/sync-server/src/app-gocardless/banks/spk_worms_alzey_ried_malade51wor.js +++ b/packages/sync-server/src/app-gocardless/banks/spk_worms_alzey_ried_malade51wor.js @@ -1,5 +1,3 @@ -import { formatPayeeName } from '../../util/payee-name.js'; - import Fallback from './integration-bank.js'; /** @type {import('./bank.interface.js').IBank} */ @@ -8,20 +6,14 @@ export default { institutionIds: ['SPK_WORMS_ALZEY_RIED_MALADE51WOR'], - normalizeTransaction(transaction, _booked) { - const date = transaction.bookingDate || transaction.valueDate; - if (!date) { - return null; - } + normalizeTransaction(transaction, booked) { + const editedTrans = { ...transaction }; - transaction.remittanceInformationUnstructured = + editedTrans.remittanceInformationUnstructured = transaction.remittanceInformationUnstructured ?? transaction.remittanceInformationStructured ?? transaction.remittanceInformationStructuredArray?.join(' '); - return { - ...transaction, - payeeName: formatPayeeName(transaction), - date: transaction.bookingDate || transaction.valueDate, - }; + + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, }; diff --git a/packages/sync-server/src/app-gocardless/banks/ssk_dusseldorf_dussdeddxxx.js b/packages/sync-server/src/app-gocardless/banks/ssk_dusseldorf_dussdeddxxx.js index b4c6dc2b17a..d1e8ecc5abc 100644 --- a/packages/sync-server/src/app-gocardless/banks/ssk_dusseldorf_dussdeddxxx.js +++ b/packages/sync-server/src/app-gocardless/banks/ssk_dusseldorf_dussdeddxxx.js @@ -6,12 +6,14 @@ export default { institutionIds: ['SSK_DUSSELDORF_DUSSDEDDXXX'], - normalizeTransaction(transaction, _booked) { + normalizeTransaction(transaction, booked) { + const editedTrans = { ...transaction }; + // If the transaction is not booked yet by the bank, don't import it. // Reason being that the transaction doesn't have the information yet // to make the payee and notes field be of any use. It's filled with // a placeholder text and wouldn't be corrected on the next sync. - if (!_booked) { + if (!booked) { console.debug( 'Skipping unbooked transaction:', transaction.transactionId, @@ -39,10 +41,10 @@ export default { transaction.creditorName || transaction.debtorName; - transaction.creditorName = usefulCreditorName; - transaction.remittanceInformationUnstructured = + editedTrans.creditorName = usefulCreditorName; + editedTrans.remittanceInformationUnstructured = remittanceInformationUnstructured; - return Fallback.normalizeTransaction(transaction, _booked); + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, }; diff --git a/packages/sync-server/src/app-gocardless/banks/swedbank_habalv22.js b/packages/sync-server/src/app-gocardless/banks/swedbank_habalv22.js index 205bf045586..e9b2283df3d 100644 --- a/packages/sync-server/src/app-gocardless/banks/swedbank_habalv22.js +++ b/packages/sync-server/src/app-gocardless/banks/swedbank_habalv22.js @@ -12,6 +12,8 @@ export default { * The actual transaction date for card transactions is only available in the remittanceInformationUnstructured field when the transaction is booked. */ normalizeTransaction(transaction, booked) { + const editedTrans = { ...transaction }; + const isCardTransaction = transaction.remittanceInformationUnstructured?.startsWith('PIRKUMS'); @@ -23,10 +25,7 @@ export default { ); if (creditorNameMatch) { - transaction = { - ...transaction, - creditorName: creditorNameMatch[1], - }; + editedTrans.creditorName = creditorNameMatch[1]; } } @@ -35,15 +34,14 @@ export default { ); if (dateMatch) { - const extractedDate = d.parse(dateMatch[1], 'dd.MM.yyyy', new Date()); + const extractedDate = d + .parse(dateMatch[1], 'dd.MM.yyyy', new Date()) + .toISOString(); - transaction = { - ...transaction, - bookingDate: d.format(extractedDate, 'yyyy-MM-dd'), - }; + editedTrans.date = extractedDate; } } - return Fallback.normalizeTransaction(transaction, booked); + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, }; diff --git a/packages/sync-server/src/app-gocardless/banks/tests/abanca_caglesmm.spec.js b/packages/sync-server/src/app-gocardless/banks/tests/abanca_caglesmm.spec.js index bdcaa3e0f3a..54af0cc421c 100644 --- a/packages/sync-server/src/app-gocardless/banks/tests/abanca_caglesmm.spec.js +++ b/packages/sync-server/src/app-gocardless/banks/tests/abanca_caglesmm.spec.js @@ -9,17 +9,13 @@ describe('Abanca', () => { internalTransactionId: 'D202301180000003', transactionAmount: mockTransactionAmount, remittanceInformationStructured: 'some-creditor-name', + date: new Date().toISOString(), }; const normalizedTransaction = Abanca.normalizeTransaction( transaction, true, ); - expect(normalizedTransaction.creditorName).toEqual( - transaction.remittanceInformationStructured, - ); - expect(normalizedTransaction.debtorName).toEqual( - transaction.remittanceInformationStructured, - ); + expect(normalizedTransaction.payeeName).toEqual('Some-Creditor-Name'); }); }); }); diff --git a/packages/sync-server/src/app-gocardless/banks/tests/abnamro_abnanl2a.spec.js b/packages/sync-server/src/app-gocardless/banks/tests/abnamro_abnanl2a.spec.js index db93bd2c7f3..fd9b35c5a50 100644 --- a/packages/sync-server/src/app-gocardless/banks/tests/abnamro_abnanl2a.spec.js +++ b/packages/sync-server/src/app-gocardless/banks/tests/abnamro_abnanl2a.spec.js @@ -24,7 +24,7 @@ describe('AbnamroAbnanl2a', () => { false, ); - expect(normalizedTransaction.remittanceInformationUnstructured).toEqual( + expect(normalizedTransaction.notes).toEqual( 'BEA, Betaalpas, My Payee Name,PAS123, NR:123A4B, 09.12.23/15:43, CITY', ); expect(normalizedTransaction.payeeName).toEqual('My Payee Name'); @@ -52,7 +52,7 @@ describe('AbnamroAbnanl2a', () => { false, ); - expect(normalizedTransaction.remittanceInformationUnstructured).toEqual( + expect(normalizedTransaction.notes).toEqual( 'BEA, Google Pay, CCV*Other payee name,PAS123, NR:123A4B, 09.12.23/15:43, CITY', ); expect(normalizedTransaction.payeeName).toEqual('Other Payee Name'); diff --git a/packages/sync-server/src/app-gocardless/banks/tests/bancsabadell_bsabesbbb.spec.js b/packages/sync-server/src/app-gocardless/banks/tests/bancsabadell_bsabesbbb.spec.js index 1e7cb914018..9cf1949d122 100644 --- a/packages/sync-server/src/app-gocardless/banks/tests/bancsabadell_bsabesbbb.spec.js +++ b/packages/sync-server/src/app-gocardless/banks/tests/bancsabadell_bsabesbbb.spec.js @@ -15,10 +15,7 @@ describe('BancSabadell', () => { transaction, true, ); - expect(normalizedTransaction.creditorName).toEqual( - 'some-creditor-name', - ); - expect(normalizedTransaction.debtorName).toEqual(null); + expect(normalizedTransaction.payeeName).toEqual('Some-Creditor-Name'); }); it('creditor role - amount > 0', () => { @@ -33,8 +30,7 @@ describe('BancSabadell', () => { transaction, true, ); - expect(normalizedTransaction.debtorName).toEqual('some-debtor-name'); - expect(normalizedTransaction.creditorName).toEqual(null); + expect(normalizedTransaction.payeeName).toEqual('Some-Debtor-Name'); }); }); diff --git a/packages/sync-server/src/app-gocardless/banks/tests/belfius_gkccbebb.spec.js b/packages/sync-server/src/app-gocardless/banks/tests/belfius_gkccbebb.spec.js index da9d5530d6a..9a61253fd75 100644 --- a/packages/sync-server/src/app-gocardless/banks/tests/belfius_gkccbebb.spec.js +++ b/packages/sync-server/src/app-gocardless/banks/tests/belfius_gkccbebb.spec.js @@ -8,6 +8,7 @@ describe('Belfius', () => { transactionId: 'non-unique-id', internalTransactionId: 'D202301180000003', transactionAmount: mockTransactionAmount, + date: new Date().toISOString(), }; const normalizedTransaction = Belfius.normalizeTransaction( transaction, diff --git a/packages/sync-server/src/app-gocardless/banks/tests/cbc_cregbebb.spec.js b/packages/sync-server/src/app-gocardless/banks/tests/cbc_cregbebb.spec.js index 4114ac4944f..21d248a85d2 100644 --- a/packages/sync-server/src/app-gocardless/banks/tests/cbc_cregbebb.spec.js +++ b/packages/sync-server/src/app-gocardless/banks/tests/cbc_cregbebb.spec.js @@ -7,6 +7,7 @@ describe('cbc_cregbebb', () => { remittanceInformationUnstructured: 'ONKART FR Viry Paiement Maestro par Carte de débit CBC 05-09-2024 à 15.43 heures 6703 19XX XXXX X201 5 JOHN DOE', transactionAmount: { amount: '-45.00', currency: 'EUR' }, + date: new Date().toISOString(), }; const normalizedTransaction = CBCcregbebb.normalizeTransaction( transaction, @@ -21,6 +22,7 @@ describe('cbc_cregbebb', () => { remittanceInformationUnstructured: 'ONKART FR Viry Paiement Maestro par Carte de débit CBC 05-09-2024 à 15.43 heures 6703 19XX XXXX X201 5 JOHN DOE', transactionAmount: { amount: '10.99', currency: 'EUR' }, + date: new Date().toISOString(), }; const normalizedTransaction = CBCcregbebb.normalizeTransaction( transaction, diff --git a/packages/sync-server/src/app-gocardless/banks/tests/commerzbank_cobadeff.spec.js b/packages/sync-server/src/app-gocardless/banks/tests/commerzbank_cobadeff.spec.js index 9667ce22609..fe3a161f682 100644 --- a/packages/sync-server/src/app-gocardless/banks/tests/commerzbank_cobadeff.spec.js +++ b/packages/sync-server/src/app-gocardless/banks/tests/commerzbank_cobadeff.spec.js @@ -28,7 +28,7 @@ describe('CommerzbankCobadeff', () => { transaction, false, ); - expect(normalizedTransaction.remittanceInformationUnstructured).toEqual( + expect(normalizedTransaction.notes).toEqual( '2024-12-19T15:34:31 KFN 1 AB 1234, Kartenzahlung', ); }); @@ -66,7 +66,7 @@ describe('CommerzbankCobadeff', () => { transaction, false, ); - expect(normalizedTransaction.remittanceInformationUnstructured).toEqual( + expect(normalizedTransaction.notes).toEqual( '901234567890/. Long description tha t gets cut and is very long, did I mention it is long, End-to-End-Ref.: 901234567890, Mandatsref: ABC123DEF456, Gläubiger-ID: AB12CDE0000000000000000012, SEPA-BASISLASTSCHRIFT wiederholend', ); }); @@ -102,7 +102,7 @@ describe('CommerzbankCobadeff', () => { transaction, false, ); - expect(normalizedTransaction.remittanceInformationUnstructured).toEqual( + expect(normalizedTransaction.notes).toEqual( 'CREDITOR00BIC CREDITOR000IBAN DESCRIPTION, Dauerauftrag', ); }); diff --git a/packages/sync-server/src/app-gocardless/banks/tests/fortuneo_ftnofrp1xxx.spec.js b/packages/sync-server/src/app-gocardless/banks/tests/fortuneo_ftnofrp1xxx.spec.js index d4a1b30ff74..a19dd3b5609 100644 --- a/packages/sync-server/src/app-gocardless/banks/tests/fortuneo_ftnofrp1xxx.spec.js +++ b/packages/sync-server/src/app-gocardless/banks/tests/fortuneo_ftnofrp1xxx.spec.js @@ -140,14 +140,12 @@ describe('Fortuneo', () => { true, ); - expect(normalizedCreditorTransaction.creditorName).toBeDefined(); - expect(normalizedCreditorTransaction.debtorName).toBeNull(); + expect(normalizedCreditorTransaction.payeeName).toBeDefined(); expect( parseFloat(normalizedCreditorTransaction.transactionAmount.amount), ).toBeLessThan(0); - expect(normalizedDebtorTransaction.debtorName).toBeDefined(); - expect(normalizedDebtorTransaction.creditorName).toBeNull(); + expect(normalizedDebtorTransaction.payeeName).toBeDefined(); expect( parseFloat(normalizedDebtorTransaction.transactionAmount.amount), ).toBeGreaterThan(0); @@ -160,7 +158,7 @@ describe('Fortuneo', () => { true, ); - expect(normalizedTransaction.creditorName).toBe('ONG'); + expect(normalizedTransaction.payeeName).toBe('Ong'); const transaction2 = transactionsRaw[2]; const normalizedTransaction2 = Fortuneo.normalizeTransaction( @@ -168,7 +166,7 @@ describe('Fortuneo', () => { true, ); - expect(normalizedTransaction2.creditorName).toBe('XXXYYYYZZZ'); + expect(normalizedTransaction2.payeeName).toBe('Xxxyyyyzzz'); const transaction3 = transactionsRaw[3]; const normalizedTransaction3 = Fortuneo.normalizeTransaction( @@ -176,9 +174,7 @@ describe('Fortuneo', () => { true, ); - expect(normalizedTransaction3.creditorName).toBe( - 'Google Payment I Dublin', - ); + expect(normalizedTransaction3.payeeName).toBe('Google Payment I Dublin'); const transaction4 = transactionsRaw[4]; const normalizedTransaction4 = Fortuneo.normalizeTransaction( @@ -186,7 +182,7 @@ describe('Fortuneo', () => { true, ); - expect(normalizedTransaction4.creditorName).toBe('SPORT MARKET'); + expect(normalizedTransaction4.payeeName).toBe('Sport Market'); const transaction5 = transactionsRaw[5]; const normalizedTransaction5 = Fortuneo.normalizeTransaction( @@ -194,7 +190,7 @@ describe('Fortuneo', () => { true, ); - expect(normalizedTransaction5.debtorName).toBe('WEEZEVENT SOMEPLACE'); + expect(normalizedTransaction5.payeeName).toBe('Weezevent Someplace'); const transaction7 = transactionsRaw[7]; const normalizedTransaction7 = Fortuneo.normalizeTransaction( @@ -202,8 +198,8 @@ describe('Fortuneo', () => { true, ); - expect(normalizedTransaction7.creditorName).toBe( - 'Leclerc XXXX Leclerc XXXX 44321IXCRT211141232', + expect(normalizedTransaction7.payeeName).toBe( + 'Leclerc Xxxx Leclerc Xxxx 44321ixcrt211141232', ); }); }); diff --git a/packages/sync-server/src/app-gocardless/banks/tests/kbc_kredbebb.spec.js b/packages/sync-server/src/app-gocardless/banks/tests/kbc_kredbebb.spec.js index 21e67dcd36a..e3326a12af7 100644 --- a/packages/sync-server/src/app-gocardless/banks/tests/kbc_kredbebb.spec.js +++ b/packages/sync-server/src/app-gocardless/banks/tests/kbc_kredbebb.spec.js @@ -7,6 +7,7 @@ describe('kbc_kredbebb', () => { remittanceInformationUnstructured: 'CARREFOUR ST GIL BE1060 BRUXELLES Betaling met Google Pay via Debit Mastercard 28-08-2024 om 19.15 uur 5127 04XX XXXX 1637 5853 98XX XXXX 2266 JOHN SMITH', transactionAmount: { amount: '-10.99', currency: 'EUR' }, + date: new Date().toISOString(), }; const normalizedTransaction = KBCkredbebb.normalizeTransaction( transaction, @@ -23,6 +24,7 @@ describe('kbc_kredbebb', () => { remittanceInformationUnstructured: 'CARREFOUR ST GIL BE1060 BRUXELLES Betaling met Google Pay via Debit Mastercard 28-08-2024 om 19.15 uur 5127 04XX XXXX 1637 5853 98XX XXXX 2266 JOHN SMITH', transactionAmount: { amount: '10.99', currency: 'EUR' }, + date: new Date().toISOString(), }; const normalizedTransaction = KBCkredbebb.normalizeTransaction( transaction, diff --git a/packages/sync-server/src/app-gocardless/banks/tests/lhv-lhvbee22.spec.js b/packages/sync-server/src/app-gocardless/banks/tests/lhv-lhvbee22.spec.js index 72e61fb384a..0f40c49d741 100644 --- a/packages/sync-server/src/app-gocardless/banks/tests/lhv-lhvbee22.spec.js +++ b/packages/sync-server/src/app-gocardless/banks/tests/lhv-lhvbee22.spec.js @@ -18,16 +18,11 @@ describe('#normalizeTransaction', () => { it('extracts booked card transaction creditor name', () => { expect( - LhvLhvbee22.normalizeTransaction(bookedCardTransaction, true) - .creditorName, + LhvLhvbee22.normalizeTransaction(bookedCardTransaction, true).payeeName, ).toEqual('CrustumOU'); }); it('extracts booked card transaction date', () => { - expect( - LhvLhvbee22.normalizeTransaction(bookedCardTransaction, true).bookingDate, - ).toEqual('2025-01-02'); - expect( LhvLhvbee22.normalizeTransaction(bookedCardTransaction, true).date, ).toEqual('2025-01-02'); @@ -45,7 +40,6 @@ describe('#normalizeTransaction', () => { }; const normalized = LhvLhvbee22.normalizeTransaction(transaction, true); - expect(normalized.bookingDate).toEqual('2025-01-03'); expect(normalized.date).toEqual('2025-01-03'); }); @@ -62,17 +56,11 @@ describe('#normalizeTransaction', () => { it('extracts pending card transaction creditor name', () => { expect( - LhvLhvbee22.normalizeTransaction(pendingCardTransaction, false) - .creditorName, + LhvLhvbee22.normalizeTransaction(pendingCardTransaction, false).payeeName, ).toEqual('CrustumOU'); }); it('extracts pending card transaction date', () => { - expect( - LhvLhvbee22.normalizeTransaction(pendingCardTransaction, false) - .bookingDate, - ).toEqual(undefined); - expect( LhvLhvbee22.normalizeTransaction(pendingCardTransaction, false).date, ).toEqual('2025-01-03'); diff --git a/packages/sync-server/src/app-gocardless/banks/tests/revolut_revolt21.spec.js b/packages/sync-server/src/app-gocardless/banks/tests/revolut_revolt21.spec.js index 40e8bef752d..c48e3447b99 100644 --- a/packages/sync-server/src/app-gocardless/banks/tests/revolut_revolt21.spec.js +++ b/packages/sync-server/src/app-gocardless/banks/tests/revolut_revolt21.spec.js @@ -17,9 +17,7 @@ describe('RevolutRevolt21', () => { true, ); - expect(normalizedTransaction.remittanceInformationUnstructured).toEqual( - 'Bizum description', - ); + expect(normalizedTransaction.notes).toEqual('Bizum description'); }); }); @@ -39,8 +37,6 @@ describe('RevolutRevolt21', () => { ); expect(normalizedTransaction.payeeName).toEqual('DEBTOR NAME'); - expect(normalizedTransaction.remittanceInformationUnstructured).toEqual( - 'Bizum description', - ); + expect(normalizedTransaction.notes).toEqual('Bizum description'); }); }); diff --git a/packages/sync-server/src/app-gocardless/banks/tests/spk_marburg_biedenkopf_heladef1mar.spec.js b/packages/sync-server/src/app-gocardless/banks/tests/spk_marburg_biedenkopf_heladef1mar.spec.js index 87b51c9a03a..62b4ee6f655 100644 --- a/packages/sync-server/src/app-gocardless/banks/tests/spk_marburg_biedenkopf_heladef1mar.spec.js +++ b/packages/sync-server/src/app-gocardless/banks/tests/spk_marburg_biedenkopf_heladef1mar.spec.js @@ -148,7 +148,7 @@ describe('SpkMarburgBiedenkopfHeladef1mar', () => { expect( SpkMarburgBiedenkopfHeladef1mar.normalizeTransaction(transaction, true) - .remittanceInformationUnstructured, + .notes, ).toEqual('AUTORISATION 28.12. 18:30'); }); @@ -172,7 +172,7 @@ describe('SpkMarburgBiedenkopfHeladef1mar', () => { expect( SpkMarburgBiedenkopfHeladef1mar.normalizeTransaction(transaction, true) - .remittanceInformationUnstructured, + .notes, ).toEqual('Entgeltabrechnung siehe Anlage'); }); }); diff --git a/packages/sync-server/src/app-gocardless/banks/tests/ssk_dusseldorf_dussdeddxxx.spec.js b/packages/sync-server/src/app-gocardless/banks/tests/ssk_dusseldorf_dussdeddxxx.spec.js index 0781c899323..68bf9299762 100644 --- a/packages/sync-server/src/app-gocardless/banks/tests/ssk_dusseldorf_dussdeddxxx.spec.js +++ b/packages/sync-server/src/app-gocardless/banks/tests/ssk_dusseldorf_dussdeddxxx.spec.js @@ -47,14 +47,14 @@ describe('ssk_dusseldorf_dussdeddxxx', () => { SskDusseldorfDussdeddxxx.normalizeTransaction( bookedTransactionOne, true, - ).remittanceInformationUnstructured, + ).notes, ).toEqual('unstructured information some additional information'); expect( SskDusseldorfDussdeddxxx.normalizeTransaction( bookedTransactionTwo, true, - ).remittanceInformationUnstructured, + ).notes, ).toEqual('structured information some additional information'); }); diff --git a/packages/sync-server/src/app-gocardless/banks/tests/swedbank_habalv22.spec.js b/packages/sync-server/src/app-gocardless/banks/tests/swedbank_habalv22.spec.js index 31673a4eb33..7fa3c0ad79f 100644 --- a/packages/sync-server/src/app-gocardless/banks/tests/swedbank_habalv22.spec.js +++ b/packages/sync-server/src/app-gocardless/banks/tests/swedbank_habalv22.spec.js @@ -17,11 +17,6 @@ describe('#normalizeTransaction', () => { }; it('extracts card transaction date', () => { - expect( - SwedbankHabaLV22.normalizeTransaction(bookedCardTransaction, true) - .bookingDate, - ).toEqual('2024-10-28'); - expect( SwedbankHabaLV22.normalizeTransaction(bookedCardTransaction, true).date, ).toEqual('2024-10-28'); @@ -56,7 +51,7 @@ describe('#normalizeTransaction', () => { it('extracts pending card transaction creditor name', () => { expect( SwedbankHabaLV22.normalizeTransaction(pendingCardTransaction, false) - .creditorName, - ).toEqual('SOME CREDITOR NAME'); + .payeeName, + ).toEqual('Some Creditor Name'); }); }); diff --git a/packages/sync-server/src/app-gocardless/banks/tests/virgin_nrnbgb22.spec.js b/packages/sync-server/src/app-gocardless/banks/tests/virgin_nrnbgb22.spec.js index e4f6f52d655..902a221564a 100644 --- a/packages/sync-server/src/app-gocardless/banks/tests/virgin_nrnbgb22.spec.js +++ b/packages/sync-server/src/app-gocardless/banks/tests/virgin_nrnbgb22.spec.js @@ -15,13 +15,8 @@ describe('Virgin', () => { true, ); - expect(normalizedTransaction.creditorName).toEqual( - 'DIRECT DEBIT PAYMENT', - ); - expect(normalizedTransaction.debtorName).toEqual('DIRECT DEBIT PAYMENT'); - expect(normalizedTransaction.remittanceInformationUnstructured).toEqual( - 'DIRECT DEBIT PAYMENT', - ); + expect(normalizedTransaction.payeeName).toEqual('Direct Debit Payment'); + expect(normalizedTransaction.notes).toEqual('DIRECT DEBIT PAYMENT'); }); it('formats bank transfer payee and references', () => { @@ -36,11 +31,8 @@ describe('Virgin', () => { true, ); - expect(normalizedTransaction.creditorName).toEqual('Joe Bloggs'); - expect(normalizedTransaction.debtorName).toEqual('Joe Bloggs'); - expect(normalizedTransaction.remittanceInformationUnstructured).toEqual( - 'Food', - ); + expect(normalizedTransaction.payeeName).toEqual('Joe Bloggs'); + expect(normalizedTransaction.notes).toEqual('Food'); }); it('removes method information from payee name', () => { @@ -55,11 +47,8 @@ describe('Virgin', () => { true, ); - expect(normalizedTransaction.creditorName).toEqual('Tesco Express'); - expect(normalizedTransaction.debtorName).toEqual('Tesco Express'); - expect(normalizedTransaction.remittanceInformationUnstructured).toEqual( - 'Card 99, Tesco Express', - ); + expect(normalizedTransaction.payeeName).toEqual('Tesco Express'); + expect(normalizedTransaction.notes).toEqual('Card 99, Tesco Express'); }); }); }); diff --git a/packages/sync-server/src/app-gocardless/banks/virgin_nrnbgb22.js b/packages/sync-server/src/app-gocardless/banks/virgin_nrnbgb22.js index 78418bc2dfe..7529c6d4cf0 100644 --- a/packages/sync-server/src/app-gocardless/banks/virgin_nrnbgb22.js +++ b/packages/sync-server/src/app-gocardless/banks/virgin_nrnbgb22.js @@ -7,6 +7,8 @@ export default { institutionIds: ['VIRGIN_NRNBGB22'], normalizeTransaction(transaction, booked) { + const editedTrans = { ...transaction }; + const transferPrefixes = ['MOB', 'FPS']; const methodRegex = /^(Card|WLT)\s\d+/; @@ -17,21 +19,21 @@ export default { // the second field contains the payee and the third contains the // reference - transaction.creditorName = parts[1]; - transaction.debtorName = parts[1]; - transaction.remittanceInformationUnstructured = parts[2]; + editedTrans.creditorName = parts[1]; + editedTrans.debtorName = parts[1]; + editedTrans.remittanceInformationUnstructured = parts[2]; } else if (parts[0].match(methodRegex)) { // The payee is prefixed with the payment method, eg "Card 11, {payee}" - transaction.creditorName = parts[1]; - transaction.debtorName = parts[1]; + editedTrans.creditorName = parts[1]; + editedTrans.debtorName = parts[1]; } else { // Simple payee name - transaction.creditorName = transaction.remittanceInformationUnstructured; - transaction.debtorName = transaction.remittanceInformationUnstructured; + editedTrans.creditorName = transaction.remittanceInformationUnstructured; + editedTrans.debtorName = transaction.remittanceInformationUnstructured; } - return Fallback.normalizeTransaction(transaction, booked); + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, }; diff --git a/packages/sync-server/src/app-gocardless/services/tests/fixtures.js b/packages/sync-server/src/app-gocardless/services/tests/fixtures.js index 40c34501a32..aecf48b695a 100644 --- a/packages/sync-server/src/app-gocardless/services/tests/fixtures.js +++ b/packages/sync-server/src/app-gocardless/services/tests/fixtures.js @@ -35,8 +35,8 @@ export const mockTransactions = { amount: '328.18', }, bankTransactionCode: 'string', - bookingDate: 'date', - valueDate: 'date', + bookingDate: '2000-01-01', + valueDate: '2000-01-01', }, { transactionId: 'string', @@ -45,8 +45,8 @@ export const mockTransactions = { amount: '947.26', }, bankTransactionCode: 'string', - bookingDate: 'date', - valueDate: 'date', + bookingDate: '2000-01-01', + valueDate: '2000-01-01', }, ], pending: [ @@ -55,7 +55,7 @@ export const mockTransactions = { currency: 'EUR', amount: '947.26', }, - valueDate: 'date', + valueDate: '2000-01-01', }, ], }, diff --git a/packages/sync-server/src/app-gocardless/services/tests/gocardless-service.spec.js b/packages/sync-server/src/app-gocardless/services/tests/gocardless-service.spec.js index a96d765a1b5..3ba22679881 100644 --- a/packages/sync-server/src/app-gocardless/services/tests/gocardless-service.spec.js +++ b/packages/sync-server/src/app-gocardless/services/tests/gocardless-service.spec.js @@ -417,42 +417,51 @@ describe('goCardlessService', () => { "booked": [ { "bankTransactionCode": "string", - "bookingDate": "date", - "date": "date", + "bookingDate": "2000-01-01", + "date": "2000-01-01", "debtorAccount": { "iban": "string", }, "debtorName": "string", + "notes": undefined, "payeeName": "String (stri XXX ring)", + "remittanceInformationStructuredArrayString": undefined, + "remittanceInformationUnstructuredArrayString": undefined, "transactionAmount": { "amount": "328.18", "currency": "EUR", }, "transactionId": "string", - "valueDate": "date", + "valueDate": "2000-01-01", }, { "bankTransactionCode": "string", - "bookingDate": "date", - "date": "date", + "bookingDate": "2000-01-01", + "date": "2000-01-01", + "notes": undefined, "payeeName": "", + "remittanceInformationStructuredArrayString": undefined, + "remittanceInformationUnstructuredArrayString": undefined, "transactionAmount": { "amount": "947.26", "currency": "EUR", }, "transactionId": "string", - "valueDate": "date", + "valueDate": "2000-01-01", }, ], "pending": [ { - "date": "date", + "date": "2000-01-01", + "notes": undefined, "payeeName": "", + "remittanceInformationStructuredArrayString": undefined, + "remittanceInformationUnstructuredArrayString": undefined, "transactionAmount": { "amount": "947.26", "currency": "EUR", }, - "valueDate": "date", + "valueDate": "2000-01-01", }, ], }, diff --git a/packages/sync-server/src/app-simplefin/app-simplefin.js b/packages/sync-server/src/app-simplefin/app-simplefin.js index 9885a962317..c6f6fdb9cb9 100644 --- a/packages/sync-server/src/app-simplefin/app-simplefin.js +++ b/packages/sync-server/src/app-simplefin/app-simplefin.js @@ -222,11 +222,19 @@ function getAccountResponse(results, accountId, startDate) { newTrans.sortOrder = dateToUse; newTrans.date = getDate(transactionDate); newTrans.payeeName = trans.payee; - newTrans.remittanceInformationUnstructured = trans.description; + newTrans.notes = trans.description; newTrans.transactionAmount = { amount: trans.amount, currency: 'USD' }; newTrans.transactionId = trans.id; newTrans.valueDate = newTrans.bookingDate; + if (trans.transacted_at) { + newTrans.transactedDate = getDate(new Date(trans.transacted_at * 1000)); + } + + if (trans.posted) { + newTrans.postedDate = getDate(new Date(trans.posted * 1000)); + } + if (newTrans.booked) { booked.push(newTrans); } else { diff --git a/upcoming-release-notes/4253.md b/upcoming-release-notes/4253.md new file mode 100644 index 00000000000..2c2f82b61cc --- /dev/null +++ b/upcoming-release-notes/4253.md @@ -0,0 +1,6 @@ +--- +category: Features +authors: [matt-fidd] +--- + +Add a UI for bank sync settings