From 310cc04a2bc220ee9e25573a95b4805932bde0c6 Mon Sep 17 00:00:00 2001 From: Matiss Janis Aboltins <matiss@mja.lv> Date: Tue, 9 Apr 2024 07:06:04 +0100 Subject: [PATCH] :recycle: (bank-sync) unify the sync/reconciliation logic for internal & external sync (#2534) --- .../loot-core/src/client/actions/account.ts | 20 - .../accounts/__snapshots__/sync.test.ts.snap | 2503 ----------------- .../loot-core/src/server/accounts/link.ts | 50 +- .../src/server/accounts/sync.test.ts | 284 +- .../loot-core/src/server/accounts/sync.ts | 356 +-- packages/loot-core/src/server/main.test.ts | 58 - packages/loot-core/src/server/main.ts | 65 +- .../loot-core/src/types/server-handlers.d.ts | 14 - upcoming-release-notes/2534.md | 6 + 9 files changed, 66 insertions(+), 3290 deletions(-) create mode 100644 upcoming-release-notes/2534.md diff --git a/packages/loot-core/src/client/actions/account.ts b/packages/loot-core/src/client/actions/account.ts index f6448ce9e..0f31d3b4a 100644 --- a/packages/loot-core/src/client/actions/account.ts +++ b/packages/loot-core/src/client/actions/account.ts @@ -78,26 +78,6 @@ export function linkAccountSimpleFin(externalAccount, upgradingId) { }; } -// TODO: type correctly or remove (unused) -export function connectAccounts( - institution, - publicToken, - accountIds, - offbudgetIds, -) { - return async (dispatch: Dispatch) => { - const ids = await send('accounts-connect', { - institution, - publicToken, - accountIds, - offbudgetIds, - }); - await dispatch(getPayees()); - await dispatch(getAccounts()); - return ids; - }; -} - export function syncAccounts(id?: string) { return async (dispatch: Dispatch, getState: GetState) => { // Disallow two parallel sync operations 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 53dec3886..0602bdc55 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 @@ -1,2508 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Account sync import never matches existing with financial ids 1`] = ` -Array [ - Object { - "account": "one", - "amount": 8105, - "category": null, - "cleared": 1, - "date": 20171015, - "error": null, - "id": "id18", - "imported_id": "5629ec0c-e559-49f4-9105-91d7b8b8738a", - "imported_payee": "Transaction 90", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id5", - "payee_name": "Transaction 90", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -473, - "category": null, - "cleared": 1, - "date": 20171015, - "error": null, - "id": "id22", - "imported_id": "45a2ad98-acbc-4120-a856-dec0839fa73c", - "imported_payee": "Transaction 54", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id9", - "payee_name": "Transaction 54", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -1462, - "category": null, - "cleared": 1, - "date": 20171015, - "error": null, - "id": "id20", - "imported_id": "8791ca7e-5c19-4f23-bdf9-f60ee2a90081", - "imported_payee": "Transaction 79", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id7", - "payee_name": "Transaction 79", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -4207, - "category": null, - "cleared": 1, - "date": 20171015, - "error": null, - "id": "id19", - "imported_id": "254ad9b3-23aa-4f70-b617-e126e054cc0e", - "imported_payee": "Transaction 3", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id6", - "payee_name": "Transaction 3", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -5093, - "category": null, - "cleared": 1, - "date": 20171015, - "error": null, - "id": "id21", - "imported_id": "8e0ddc45-d545-4504-bed7-23c417a90f90", - "imported_payee": "Transaction 51", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id8", - "payee_name": "Transaction 51", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -5938, - "category": null, - "cleared": 1, - "date": 20171015, - "error": null, - "id": "id23", - "imported_id": "00ab7e01-73f7-4da0-98f3-233f1dcc5b3e", - "imported_payee": "Transaction 68", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id10", - "payee_name": "Transaction 68", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": 35884, - "category": "id2", - "cleared": 1, - "date": 20171014, - "error": null, - "id": "id4", - "imported_id": null, - "imported_payee": null, - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id3", - "payee_name": "Starting Balance", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 1, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -1105, - "category": null, - "cleared": 1, - "date": 20171014, - "error": null, - "id": "id25", - "imported_id": "6a764abf-69f4-47a6-94e1-638c7df0e245", - "imported_payee": "Transaction 64", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id12", - "payee_name": "Transaction 64", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -3200, - "category": null, - "cleared": 1, - "date": 20171014, - "error": null, - "id": "id24", - "imported_id": "1b25b9df-8aa4-47c8-9792-75f4c38e351f", - "imported_payee": "Transaction 11", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id11", - "payee_name": "Transaction 11", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -3342, - "category": null, - "cleared": 1, - "date": 20171014, - "error": null, - "id": "id28", - "imported_id": "f4b34ee3-6d5b-4625-bffc-057a72270140", - "imported_payee": "Transaction 48", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id15", - "payee_name": "Transaction 48", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -3984, - "category": null, - "cleared": 1, - "date": 20171014, - "error": null, - "id": "id26", - "imported_id": "768b6d36-0ad7-47fb-a631-145170adc0b9", - "imported_payee": "Transaction 107", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id13", - "payee_name": "Transaction 107", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -4524, - "category": null, - "cleared": 1, - "date": 20171014, - "error": null, - "id": "id30", - "imported_id": "f4e3a3f2-c20f-4667-8df6-6530bf5f4cb0", - "imported_payee": "Transaction 56", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id17", - "payee_name": "Transaction 56", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -4881, - "category": null, - "cleared": 1, - "date": 20171014, - "error": null, - "id": "id27", - "imported_id": "ceb53daf-7318-4aee-a562-19a45654eaa7", - "imported_payee": "Transaction 28", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id14", - "payee_name": "Transaction 28", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -5541, - "category": null, - "cleared": 1, - "date": 20171014, - "error": null, - "id": "id29", - "imported_id": "af2d6eba-f75e-4ee0-a9ad-5d9f8264797b", - "imported_payee": "Transaction 114", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id16", - "payee_name": "Transaction 114", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, -] -`; - -exports[`Account sync import never matches existing with financial ids 2`] = ` -"Snapshot Diff: -- First value -+ Second value - -@@ -68,10 +68,56 @@ - \\"tombstone\\": 0, - \\"transfer_id\\": null, - }, - Object { - \\"account\\": \\"one\\", -+ \\"amount\\": -2947, -+ \\"category\\": null, -+ \\"cleared\\": 1, -+ \\"date\\": 20171015, -+ \\"error\\": null, -+ \\"id\\": \\"one\\", -+ \\"imported_id\\": \\"trans1\\", -+ \\"imported_payee\\": null, -+ \\"is_child\\": 0, -+ \\"is_parent\\": 0, -+ \\"notes\\": null, -+ \\"parent_id\\": null, -+ \\"payee\\": \\"id31\\", -+ \\"payee_name\\": \\"foo\\", -+ \\"reconciled\\": 0, -+ \\"schedule\\": null, -+ \\"sort_order\\": 123456789, -+ \\"starting_balance_flag\\": 0, -+ \\"tombstone\\": 0, -+ \\"transfer_id\\": null, -+ }, -+ Object { -+ \\"account\\": \\"one\\", -+ \\"amount\\": -2947, -+ \\"category\\": null, -+ \\"cleared\\": 1, -+ \\"date\\": 20171015, -+ \\"error\\": null, -+ \\"id\\": \\"two\\", -+ \\"imported_id\\": \\"trans2\\", -+ \\"imported_payee\\": null, -+ \\"is_child\\": 0, -+ \\"is_parent\\": 0, -+ \\"notes\\": null, -+ \\"parent_id\\": null, -+ \\"payee\\": \\"id32\\", -+ \\"payee_name\\": \\"bar\\", -+ \\"reconciled\\": 0, -+ \\"schedule\\": null, -+ \\"sort_order\\": 123456789, -+ \\"starting_balance_flag\\": 0, -+ \\"tombstone\\": 0, -+ \\"transfer_id\\": null, -+ }, -+ Object { -+ \\"account\\": \\"one\\", - \\"amount\\": -4207, - \\"category\\": null, - \\"cleared\\": 1, - \\"date\\": 20171015, - \\"error\\": null," -`; - -exports[`Account sync import never matches existing with financial ids 3`] = ` -"Snapshot Diff: -- First value -+ Second value - -@@ -1,114 +1,160 @@ - Array [ - Object { - \\"account\\": \\"one\\", -- \\"amount\\": 8105, -+ \\"amount\\": -1865, -+ \\"category\\": null, -+ \\"cleared\\": 1, -+ \\"date\\": 20171017, -+ \\"error\\": null, -+ \\"id\\": \\"id36\\", -+ \\"imported_id\\": \\"622f7b61-a6be-4ce5-bd2f-50eb14c12f42\\", -+ \\"imported_payee\\": \\"Transaction 53\\", -+ \\"is_child\\": 0, -+ \\"is_parent\\": 0, -+ \\"notes\\": null, -+ \\"parent_id\\": null, -+ \\"payee\\": \\"id34\\", -+ \\"payee_name\\": \\"Transaction 53\\", -+ \\"reconciled\\": 0, -+ \\"schedule\\": null, -+ \\"sort_order\\": 123456789, -+ \\"starting_balance_flag\\": 0, -+ \\"tombstone\\": 0, -+ \\"transfer_id\\": null, -+ }, -+ Object { -+ \\"account\\": \\"one\\", -+ \\"amount\\": -2947, - \\"category\\": null, - \\"cleared\\": 1, -- \\"date\\": 20171015, -+ \\"date\\": 20171017, - \\"error\\": null, -- \\"id\\": \\"id18\\", -- \\"imported_id\\": \\"5629ec0c-e559-49f4-9105-91d7b8b8738a\\", -- \\"imported_payee\\": \\"Transaction 90\\", -+ \\"id\\": \\"one\\", -+ \\"imported_id\\": \\"3591ad03-b705-42e0-945d-402a70371c49\\", -+ \\"imported_payee\\": \\"foo\\", - \\"is_child\\": 0, - \\"is_parent\\": 0, - \\"notes\\": null, - \\"parent_id\\": null, -- \\"payee\\": \\"id5\\", -- \\"payee_name\\": \\"Transaction 90\\", -+ \\"payee\\": \\"id31\\", -+ \\"payee_name\\": \\"foo\\", - \\"reconciled\\": 0, - \\"schedule\\": null, - \\"sort_order\\": 123456789, - \\"starting_balance_flag\\": 0, - \\"tombstone\\": 0, - \\"transfer_id\\": null, - }, - Object { - \\"account\\": \\"one\\", -- \\"amount\\": -473, -+ \\"amount\\": -2947, - \\"category\\": null, - \\"cleared\\": 1, -- \\"date\\": 20171015, -+ \\"date\\": 20171017, - \\"error\\": null, -- \\"id\\": \\"id22\\", -- \\"imported_id\\": \\"45a2ad98-acbc-4120-a856-dec0839fa73c\\", -- \\"imported_payee\\": \\"Transaction 54\\", -+ \\"id\\": \\"two\\", -+ \\"imported_id\\": \\"01a3a594-a381-49d1-bcf8-331a3c410900\\", -+ \\"imported_payee\\": \\"bar\\", - \\"is_child\\": 0, - \\"is_parent\\": 0, - \\"notes\\": null, - \\"parent_id\\": null, -- \\"payee\\": \\"id9\\", -- \\"payee_name\\": \\"Transaction 54\\", -+ \\"payee\\": \\"id32\\", -+ \\"payee_name\\": \\"bar\\", - \\"reconciled\\": 0, - \\"schedule\\": null, - \\"sort_order\\": 123456789, - \\"starting_balance_flag\\": 0, - \\"tombstone\\": 0, - \\"transfer_id\\": null, - }, - Object { - \\"account\\": \\"one\\", -- \\"amount\\": -1462, -+ \\"amount\\": -2407, -+ \\"category\\": null, -+ \\"cleared\\": 1, -+ \\"date\\": 20171016, -+ \\"error\\": null, -+ \\"id\\": \\"id35\\", -+ \\"imported_id\\": \\"753911ce-7b09-4cb3-8447-ac6eb74e727e\\", -+ \\"imported_payee\\": \\"Transaction 32\\", -+ \\"is_child\\": 0, -+ \\"is_parent\\": 0, -+ \\"notes\\": null, -+ \\"parent_id\\": null, -+ \\"payee\\": \\"id33\\", -+ \\"payee_name\\": \\"Transaction 32\\", -+ \\"reconciled\\": 0, -+ \\"schedule\\": null, -+ \\"sort_order\\": 123456789, -+ \\"starting_balance_flag\\": 0, -+ \\"tombstone\\": 0, -+ \\"transfer_id\\": null, -+ }, -+ Object { -+ \\"account\\": \\"one\\", -+ \\"amount\\": 8105, - \\"category\\": null, - \\"cleared\\": 1, - \\"date\\": 20171015, - \\"error\\": null, -- \\"id\\": \\"id20\\", -- \\"imported_id\\": \\"8791ca7e-5c19-4f23-bdf9-f60ee2a90081\\", -- \\"imported_payee\\": \\"Transaction 79\\", -+ \\"id\\": \\"id18\\", -+ \\"imported_id\\": \\"5629ec0c-e559-49f4-9105-91d7b8b8738a\\", -+ \\"imported_payee\\": \\"Transaction 90\\", - \\"is_child\\": 0, - \\"is_parent\\": 0, - \\"notes\\": null, - \\"parent_id\\": null, -- \\"payee\\": \\"id7\\", -- \\"payee_name\\": \\"Transaction 79\\", -+ \\"payee\\": \\"id5\\", -+ \\"payee_name\\": \\"Transaction 90\\", - \\"reconciled\\": 0, - \\"schedule\\": null, - \\"sort_order\\": 123456789, - \\"starting_balance_flag\\": 0, - \\"tombstone\\": 0, - \\"transfer_id\\": null, - }, - Object { - \\"account\\": \\"one\\", -- \\"amount\\": -2947, -+ \\"amount\\": -473, - \\"category\\": null, - \\"cleared\\": 1, - \\"date\\": 20171015, - \\"error\\": null, -- \\"id\\": \\"one\\", -- \\"imported_id\\": \\"trans1\\", -- \\"imported_payee\\": null, -+ \\"id\\": \\"id22\\", -+ \\"imported_id\\": \\"45a2ad98-acbc-4120-a856-dec0839fa73c\\", -+ \\"imported_payee\\": \\"Transaction 54\\", - \\"is_child\\": 0, - \\"is_parent\\": 0, - \\"notes\\": null, - \\"parent_id\\": null, -- \\"payee\\": \\"id31\\", -- \\"payee_name\\": \\"foo\\", -+ \\"payee\\": \\"id9\\", -+ \\"payee_name\\": \\"Transaction 54\\", - \\"reconciled\\": 0, - \\"schedule\\": null, - \\"sort_order\\": 123456789, - \\"starting_balance_flag\\": 0, - \\"tombstone\\": 0, - \\"transfer_id\\": null, - }, - Object { - \\"account\\": \\"one\\", -- \\"amount\\": -2947, -+ \\"amount\\": -1462, - \\"category\\": null, - \\"cleared\\": 1, - \\"date\\": 20171015, - \\"error\\": null, -- \\"id\\": \\"two\\", -- \\"imported_id\\": \\"trans2\\", -- \\"imported_payee\\": null, -+ \\"id\\": \\"id20\\", -+ \\"imported_id\\": \\"8791ca7e-5c19-4f23-bdf9-f60ee2a90081\\", -+ \\"imported_payee\\": \\"Transaction 79\\", - \\"is_child\\": 0, - \\"is_parent\\": 0, - \\"notes\\": null, - \\"parent_id\\": null, -- \\"payee\\": \\"id32\\", -- \\"payee_name\\": \\"bar\\", -+ \\"payee\\": \\"id7\\", -+ \\"payee_name\\": \\"Transaction 79\\", - \\"reconciled\\": 0, - \\"schedule\\": null, - \\"sort_order\\": 123456789, - \\"starting_balance_flag\\": 0, - \\"tombstone\\": 0," -`; - -exports[`Account sync import updates transfers when matched 1`] = ` -Array [ - Object { - "account": "one", - "amount": 8105, - "category": null, - "cleared": 1, - "date": 20171015, - "error": null, - "id": "id18", - "imported_id": "5629ec0c-e559-49f4-9105-91d7b8b8738a", - "imported_payee": "Transaction 90", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id5", - "payee_name": "Transaction 90", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -473, - "category": null, - "cleared": 1, - "date": 20171015, - "error": null, - "id": "id22", - "imported_id": "45a2ad98-acbc-4120-a856-dec0839fa73c", - "imported_payee": "Transaction 54", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id9", - "payee_name": "Transaction 54", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -1462, - "category": null, - "cleared": 1, - "date": 20171015, - "error": null, - "id": "id20", - "imported_id": "8791ca7e-5c19-4f23-bdf9-f60ee2a90081", - "imported_payee": "Transaction 79", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id7", - "payee_name": "Transaction 79", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -4207, - "category": null, - "cleared": 1, - "date": 20171015, - "error": null, - "id": "id19", - "imported_id": "254ad9b3-23aa-4f70-b617-e126e054cc0e", - "imported_payee": "Transaction 3", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id6", - "payee_name": "Transaction 3", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -5093, - "category": null, - "cleared": 1, - "date": 20171015, - "error": null, - "id": "id21", - "imported_id": "8e0ddc45-d545-4504-bed7-23c417a90f90", - "imported_payee": "Transaction 51", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id8", - "payee_name": "Transaction 51", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -5938, - "category": null, - "cleared": 1, - "date": 20171015, - "error": null, - "id": "id23", - "imported_id": "00ab7e01-73f7-4da0-98f3-233f1dcc5b3e", - "imported_payee": "Transaction 68", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id10", - "payee_name": "Transaction 68", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": 35884, - "category": "id2", - "cleared": 1, - "date": 20171014, - "error": null, - "id": "id4", - "imported_id": null, - "imported_payee": null, - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id3", - "payee_name": "Starting Balance", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 1, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -1105, - "category": null, - "cleared": 1, - "date": 20171014, - "error": null, - "id": "id25", - "imported_id": "6a764abf-69f4-47a6-94e1-638c7df0e245", - "imported_payee": "Transaction 64", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id12", - "payee_name": "Transaction 64", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -3200, - "category": null, - "cleared": 1, - "date": 20171014, - "error": null, - "id": "id24", - "imported_id": "1b25b9df-8aa4-47c8-9792-75f4c38e351f", - "imported_payee": "Transaction 11", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id11", - "payee_name": "Transaction 11", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -3342, - "category": null, - "cleared": 1, - "date": 20171014, - "error": null, - "id": "id28", - "imported_id": "f4b34ee3-6d5b-4625-bffc-057a72270140", - "imported_payee": "Transaction 48", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id15", - "payee_name": "Transaction 48", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -3984, - "category": null, - "cleared": 1, - "date": 20171014, - "error": null, - "id": "id26", - "imported_id": "768b6d36-0ad7-47fb-a631-145170adc0b9", - "imported_payee": "Transaction 107", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id13", - "payee_name": "Transaction 107", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -4524, - "category": null, - "cleared": 1, - "date": 20171014, - "error": null, - "id": "id30", - "imported_id": "f4e3a3f2-c20f-4667-8df6-6530bf5f4cb0", - "imported_payee": "Transaction 56", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id17", - "payee_name": "Transaction 56", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -4881, - "category": null, - "cleared": 1, - "date": 20171014, - "error": null, - "id": "id27", - "imported_id": "ceb53daf-7318-4aee-a562-19a45654eaa7", - "imported_payee": "Transaction 28", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id14", - "payee_name": "Transaction 28", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -5541, - "category": null, - "cleared": 1, - "date": 20171014, - "error": null, - "id": "id29", - "imported_id": "af2d6eba-f75e-4ee0-a9ad-5d9f8264797b", - "imported_payee": "Transaction 114", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id16", - "payee_name": "Transaction 114", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, -] -`; - -exports[`Account sync import updates transfers when matched 2`] = ` -"Snapshot Diff: -- First value -+ Second value - -@@ -21,10 +21,33 @@ - \\"starting_balance_flag\\": 0, - \\"tombstone\\": 0, - \\"transfer_id\\": null, - }, - Object { -+ \\"account\\": \\"two\\", -+ \\"amount\\": 2948, -+ \\"category\\": null, -+ \\"cleared\\": 1, -+ \\"date\\": 20171015, -+ \\"error\\": null, -+ \\"id\\": \\"one\\", -+ \\"imported_id\\": null, -+ \\"imported_payee\\": null, -+ \\"is_child\\": 0, -+ \\"is_parent\\": 0, -+ \\"notes\\": null, -+ \\"parent_id\\": null, -+ \\"payee\\": \\"transfer-one\\", -+ \\"payee_name\\": \\"\\", -+ \\"reconciled\\": 0, -+ \\"schedule\\": null, -+ \\"sort_order\\": 123456789, -+ \\"starting_balance_flag\\": 0, -+ \\"tombstone\\": 0, -+ \\"transfer_id\\": \\"id31\\", -+ }, -+ Object { - \\"account\\": \\"one\\", - \\"amount\\": -473, - \\"category\\": null, - \\"cleared\\": 1, - \\"date\\": 20171015, -@@ -65,10 +88,33 @@ - \\"schedule\\": null, - \\"sort_order\\": 123456789, - \\"starting_balance_flag\\": 0, - \\"tombstone\\": 0, - \\"transfer_id\\": null, -+ }, -+ Object { -+ \\"account\\": \\"one\\", -+ \\"amount\\": -2948, -+ \\"category\\": null, -+ \\"cleared\\": 0, -+ \\"date\\": 20171015, -+ \\"error\\": null, -+ \\"id\\": \\"id31\\", -+ \\"imported_id\\": null, -+ \\"imported_payee\\": null, -+ \\"is_child\\": 0, -+ \\"is_parent\\": 0, -+ \\"notes\\": null, -+ \\"parent_id\\": null, -+ \\"payee\\": \\"transfer-two\\", -+ \\"payee_name\\": \\"\\", -+ \\"reconciled\\": 0, -+ \\"schedule\\": null, -+ \\"sort_order\\": 123456789, -+ \\"starting_balance_flag\\": 0, -+ \\"tombstone\\": 0, -+ \\"transfer_id\\": \\"one\\", - }, - Object { - \\"account\\": \\"one\\", - \\"amount\\": -4207, - \\"category\\": null," -`; - -exports[`Account sync import updates transfers when matched 3`] = ` -Array [ - Object { - "account": "two", - "amount": 2948, - "category": null, - "cleared": 1, - "date": 20171017, - "error": null, - "id": "one", - "imported_id": null, - "imported_payee": null, - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "transfer-one", - "payee_name": "", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": "id31", - }, - Object { - "account": "one", - "amount": -1865, - "category": null, - "cleared": 1, - "date": 20171017, - "error": null, - "id": "id38", - "imported_id": "622f7b61-a6be-4ce5-bd2f-50eb14c12f42", - "imported_payee": "Transaction 53", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id35", - "payee_name": "Transaction 53", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -2948, - "category": null, - "cleared": 1, - "date": 20171017, - "error": null, - "id": "id31", - "imported_id": "3591ad03-b705-42e0-945d-402a70371c49", - "imported_payee": "#001 fenn st Macy’s 33333 EMX", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "transfer-two", - "payee_name": "", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": "one", - }, - Object { - "account": "one", - "amount": -4911, - "category": null, - "cleared": 1, - "date": 20171017, - "error": null, - "id": "id37", - "imported_id": "01a3a594-a381-49d1-bcf8-331a3c410900", - "imported_payee": "Transaction 78", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id34", - "payee_name": "Transaction 78", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -2407, - "category": null, - "cleared": 1, - "date": 20171016, - "error": null, - "id": "id36", - "imported_id": "753911ce-7b09-4cb3-8447-ac6eb74e727e", - "imported_payee": "Transaction 32", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id32", - "payee_name": "Transaction 32", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": 8105, - "category": null, - "cleared": 1, - "date": 20171015, - "error": null, - "id": "id18", - "imported_id": "5629ec0c-e559-49f4-9105-91d7b8b8738a", - "imported_payee": "Transaction 90", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id5", - "payee_name": "Transaction 90", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -473, - "category": null, - "cleared": 1, - "date": 20171015, - "error": null, - "id": "id22", - "imported_id": "45a2ad98-acbc-4120-a856-dec0839fa73c", - "imported_payee": "Transaction 54", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id9", - "payee_name": "Transaction 54", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -1462, - "category": null, - "cleared": 1, - "date": 20171015, - "error": null, - "id": "id20", - "imported_id": "8791ca7e-5c19-4f23-bdf9-f60ee2a90081", - "imported_payee": "Transaction 79", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id7", - "payee_name": "Transaction 79", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -4207, - "category": null, - "cleared": 1, - "date": 20171015, - "error": null, - "id": "id19", - "imported_id": "254ad9b3-23aa-4f70-b617-e126e054cc0e", - "imported_payee": "Transaction 3", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id6", - "payee_name": "Transaction 3", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -5093, - "category": null, - "cleared": 1, - "date": 20171015, - "error": null, - "id": "id21", - "imported_id": "8e0ddc45-d545-4504-bed7-23c417a90f90", - "imported_payee": "Transaction 51", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id8", - "payee_name": "Transaction 51", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -5938, - "category": null, - "cleared": 1, - "date": 20171015, - "error": null, - "id": "id23", - "imported_id": "00ab7e01-73f7-4da0-98f3-233f1dcc5b3e", - "imported_payee": "Transaction 68", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id10", - "payee_name": "Transaction 68", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": 35884, - "category": "id2", - "cleared": 1, - "date": 20171014, - "error": null, - "id": "id4", - "imported_id": null, - "imported_payee": null, - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id3", - "payee_name": "Starting Balance", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 1, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -1105, - "category": null, - "cleared": 1, - "date": 20171014, - "error": null, - "id": "id25", - "imported_id": "6a764abf-69f4-47a6-94e1-638c7df0e245", - "imported_payee": "Transaction 64", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id12", - "payee_name": "Transaction 64", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -3200, - "category": null, - "cleared": 1, - "date": 20171014, - "error": null, - "id": "id24", - "imported_id": "1b25b9df-8aa4-47c8-9792-75f4c38e351f", - "imported_payee": "Transaction 11", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id11", - "payee_name": "Transaction 11", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -3342, - "category": null, - "cleared": 1, - "date": 20171014, - "error": null, - "id": "id28", - "imported_id": "f4b34ee3-6d5b-4625-bffc-057a72270140", - "imported_payee": "Transaction 48", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id15", - "payee_name": "Transaction 48", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -3984, - "category": null, - "cleared": 1, - "date": 20171014, - "error": null, - "id": "id26", - "imported_id": "768b6d36-0ad7-47fb-a631-145170adc0b9", - "imported_payee": "Transaction 107", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id13", - "payee_name": "Transaction 107", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -4524, - "category": null, - "cleared": 1, - "date": 20171014, - "error": null, - "id": "id30", - "imported_id": "f4e3a3f2-c20f-4667-8df6-6530bf5f4cb0", - "imported_payee": "Transaction 56", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id17", - "payee_name": "Transaction 56", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -4881, - "category": null, - "cleared": 1, - "date": 20171014, - "error": null, - "id": "id27", - "imported_id": "ceb53daf-7318-4aee-a562-19a45654eaa7", - "imported_payee": "Transaction 28", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id14", - "payee_name": "Transaction 28", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -5541, - "category": null, - "cleared": 1, - "date": 20171014, - "error": null, - "id": "id29", - "imported_id": "af2d6eba-f75e-4ee0-a9ad-5d9f8264797b", - "imported_payee": "Transaction 114", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id16", - "payee_name": "Transaction 114", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, -] -`; - -exports[`Account sync imports transactions for current day and adds latest 1`] = ` -Array [ - Object { - "account": "one", - "amount": 8105, - "category": null, - "cleared": 1, - "date": 20171015, - "error": null, - "id": "id18", - "imported_id": "5629ec0c-e559-49f4-9105-91d7b8b8738a", - "imported_payee": "Transaction 90", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id5", - "payee_name": "Transaction 90", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -473, - "category": null, - "cleared": 1, - "date": 20171015, - "error": null, - "id": "id22", - "imported_id": "45a2ad98-acbc-4120-a856-dec0839fa73c", - "imported_payee": "Transaction 54", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id9", - "payee_name": "Transaction 54", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -1462, - "category": null, - "cleared": 1, - "date": 20171015, - "error": null, - "id": "id20", - "imported_id": "8791ca7e-5c19-4f23-bdf9-f60ee2a90081", - "imported_payee": "Transaction 79", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id7", - "payee_name": "Transaction 79", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -4207, - "category": null, - "cleared": 1, - "date": 20171015, - "error": null, - "id": "id19", - "imported_id": "254ad9b3-23aa-4f70-b617-e126e054cc0e", - "imported_payee": "Transaction 3", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id6", - "payee_name": "Transaction 3", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -5093, - "category": null, - "cleared": 1, - "date": 20171015, - "error": null, - "id": "id21", - "imported_id": "8e0ddc45-d545-4504-bed7-23c417a90f90", - "imported_payee": "Transaction 51", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id8", - "payee_name": "Transaction 51", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -5938, - "category": null, - "cleared": 1, - "date": 20171015, - "error": null, - "id": "id23", - "imported_id": "00ab7e01-73f7-4da0-98f3-233f1dcc5b3e", - "imported_payee": "Transaction 68", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id10", - "payee_name": "Transaction 68", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": 35884, - "category": "id2", - "cleared": 1, - "date": 20171014, - "error": null, - "id": "id4", - "imported_id": null, - "imported_payee": null, - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id3", - "payee_name": "Starting Balance", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 1, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -1105, - "category": null, - "cleared": 1, - "date": 20171014, - "error": null, - "id": "id25", - "imported_id": "6a764abf-69f4-47a6-94e1-638c7df0e245", - "imported_payee": "Transaction 64", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id12", - "payee_name": "Transaction 64", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -3200, - "category": null, - "cleared": 1, - "date": 20171014, - "error": null, - "id": "id24", - "imported_id": "1b25b9df-8aa4-47c8-9792-75f4c38e351f", - "imported_payee": "Transaction 11", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id11", - "payee_name": "Transaction 11", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -3342, - "category": null, - "cleared": 1, - "date": 20171014, - "error": null, - "id": "id28", - "imported_id": "f4b34ee3-6d5b-4625-bffc-057a72270140", - "imported_payee": "Transaction 48", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id15", - "payee_name": "Transaction 48", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -3984, - "category": null, - "cleared": 1, - "date": 20171014, - "error": null, - "id": "id26", - "imported_id": "768b6d36-0ad7-47fb-a631-145170adc0b9", - "imported_payee": "Transaction 107", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id13", - "payee_name": "Transaction 107", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -4524, - "category": null, - "cleared": 1, - "date": 20171014, - "error": null, - "id": "id30", - "imported_id": "f4e3a3f2-c20f-4667-8df6-6530bf5f4cb0", - "imported_payee": "Transaction 56", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id17", - "payee_name": "Transaction 56", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -4881, - "category": null, - "cleared": 1, - "date": 20171014, - "error": null, - "id": "id27", - "imported_id": "ceb53daf-7318-4aee-a562-19a45654eaa7", - "imported_payee": "Transaction 28", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id14", - "payee_name": "Transaction 28", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -5541, - "category": null, - "cleared": 1, - "date": 20171014, - "error": null, - "id": "id29", - "imported_id": "af2d6eba-f75e-4ee0-a9ad-5d9f8264797b", - "imported_payee": "Transaction 114", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id16", - "payee_name": "Transaction 114", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, -] -`; - -exports[`Account sync imports transactions for current day and adds latest 2`] = ` -Array [ - Object { - "account": "one", - "amount": -434, - "category": null, - "cleared": 1, - "date": 20171017, - "error": null, - "id": "id58", - "imported_id": "b89c446a-c425-4ce2-b3b9-251b3fbe1568", - "imported_payee": "Transaction 118", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id40", - "payee_name": "Transaction 118", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -1865, - "category": null, - "cleared": 1, - "date": 20171017, - "error": null, - "id": "id51", - "imported_id": "622f7b61-a6be-4ce5-bd2f-50eb14c12f42", - "imported_payee": "Transaction 53", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id33", - "payee_name": "Transaction 53", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -2147, - "category": null, - "cleared": 1, - "date": 20171017, - "error": null, - "id": "id49", - "imported_id": "3591ad03-b705-42e0-945d-402a70371c49", - "imported_payee": "Transaction 13", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id31", - "payee_name": "Transaction 13", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -2947, - "category": null, - "cleared": 1, - "date": 20171017, - "error": null, - "id": "id54", - "imported_id": "d5ec5f37-2ac1-4b8d-b020-8eda39d4f61a", - "imported_payee": "Transaction 96", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id36", - "payee_name": "Transaction 96", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -2947, - "category": null, - "cleared": 1, - "date": 20171017, - "error": null, - "id": "id55", - "imported_id": "d5ec5f37-2ac1-4b8d-b020-8eda39d4f61a2", - "imported_payee": "Transaction Bazillion", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id37", - "payee_name": "Transaction Bazillion", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -3170, - "category": null, - "cleared": 1, - "date": 20171017, - "error": null, - "id": "id59", - "imported_id": "aa69713d-f3ea-4914-a4fd-f04fcad242ba", - "imported_payee": "Transaction 89", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id41", - "payee_name": "Transaction 89", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -3339, - "category": null, - "cleared": 1, - "date": 20171017, - "error": null, - "id": "id57", - "imported_id": "ade5fc3c-cce6-435e-9634-9e03c7876373", - "imported_payee": "Transaction 65", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id39", - "payee_name": "Transaction 65", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -4911, - "category": null, - "cleared": 1, - "date": 20171017, - "error": null, - "id": "id50", - "imported_id": "01a3a594-a381-49d1-bcf8-331a3c410900", - "imported_payee": "Transaction 78", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id32", - "payee_name": "Transaction 78", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -5391, - "category": null, - "cleared": 1, - "date": 20171017, - "error": null, - "id": "id56", - "imported_id": "8083e915-7eb0-4226-9514-2574e39ed333", - "imported_payee": "Transaction 49", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id38", - "payee_name": "Transaction 49", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -5709, - "category": null, - "cleared": 1, - "date": 20171017, - "error": null, - "id": "id53", - "imported_id": "7f438a75-1ba3-40a0-a25b-689c8e720e31", - "imported_payee": "Transaction 100", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id35", - "payee_name": "Transaction 100", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -5730, - "category": null, - "cleared": 1, - "date": 20171017, - "error": null, - "id": "id52", - "imported_id": "7b5a9df8-ca05-411b-82f9-4261b2df5b8c", - "imported_payee": "Transaction 115", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id34", - "payee_name": "Transaction 115", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": 9675, - "category": null, - "cleared": 1, - "date": 20171016, - "error": null, - "id": "id63", - "imported_id": "4a907027-bed2-4f5d-ac67-b78fb14cead9", - "imported_payee": "Transaction 67", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id45", - "payee_name": "Transaction 67", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -2407, - "category": null, - "cleared": 1, - "date": 20171016, - "error": null, - "id": "id60", - "imported_id": "753911ce-7b09-4cb3-8447-ac6eb74e727e", - "imported_payee": "Transaction 32", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id42", - "payee_name": "Transaction 32", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -3006, - "category": null, - "cleared": 1, - "date": 20171016, - "error": null, - "id": "id65", - "imported_id": "14ea70a2-511f-4797-92ae-81772acedf86", - "imported_payee": "Transaction 20", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id47", - "payee_name": "Transaction 20", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -3935, - "category": null, - "cleared": 1, - "date": 20171016, - "error": null, - "id": "id64", - "imported_id": "5568650a-db53-43ce-9812-2755e8c7ca62", - "imported_payee": "Transaction 14", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id46", - "payee_name": "Transaction 14", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -3987, - "category": null, - "cleared": 1, - "date": 20171016, - "error": null, - "id": "id62", - "imported_id": "831051b1-5d93-4df8-ab04-dd69615cd001", - "imported_payee": "Transaction 6", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id44", - "payee_name": "Transaction 6", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -4837, - "category": null, - "cleared": 1, - "date": 20171016, - "error": null, - "id": "id61", - "imported_id": "e1f42f2d-ab97-468c-a859-df571e858896", - "imported_payee": "Transaction 119", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id43", - "payee_name": "Transaction 119", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -4936, - "category": null, - "cleared": 1, - "date": 20171016, - "error": null, - "id": "id66", - "imported_id": "86c867c2-6d44-4556-84e7-015226144a7d", - "imported_payee": "Transaction 21", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id48", - "payee_name": "Transaction 21", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": 8105, - "category": null, - "cleared": 1, - "date": 20171015, - "error": null, - "id": "id18", - "imported_id": "5629ec0c-e559-49f4-9105-91d7b8b8738a", - "imported_payee": "Transaction 90", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id5", - "payee_name": "Transaction 90", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -473, - "category": null, - "cleared": 1, - "date": 20171015, - "error": null, - "id": "id22", - "imported_id": "45a2ad98-acbc-4120-a856-dec0839fa73c", - "imported_payee": "Transaction 54", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id9", - "payee_name": "Transaction 54", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -1462, - "category": null, - "cleared": 1, - "date": 20171015, - "error": null, - "id": "id20", - "imported_id": "8791ca7e-5c19-4f23-bdf9-f60ee2a90081", - "imported_payee": "Transaction 79", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id7", - "payee_name": "Transaction 79", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -4207, - "category": null, - "cleared": 1, - "date": 20171015, - "error": null, - "id": "id19", - "imported_id": "254ad9b3-23aa-4f70-b617-e126e054cc0e", - "imported_payee": "Transaction 3", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id6", - "payee_name": "Transaction 3", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -5093, - "category": null, - "cleared": 1, - "date": 20171015, - "error": null, - "id": "id21", - "imported_id": "8e0ddc45-d545-4504-bed7-23c417a90f90", - "imported_payee": "Transaction 51", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id8", - "payee_name": "Transaction 51", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -5938, - "category": null, - "cleared": 1, - "date": 20171015, - "error": null, - "id": "id23", - "imported_id": "00ab7e01-73f7-4da0-98f3-233f1dcc5b3e", - "imported_payee": "Transaction 68", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id10", - "payee_name": "Transaction 68", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": 35884, - "category": "id2", - "cleared": 1, - "date": 20171014, - "error": null, - "id": "id4", - "imported_id": null, - "imported_payee": null, - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id3", - "payee_name": "Starting Balance", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 1, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -1105, - "category": null, - "cleared": 1, - "date": 20171014, - "error": null, - "id": "id25", - "imported_id": "6a764abf-69f4-47a6-94e1-638c7df0e245", - "imported_payee": "Transaction 64", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id12", - "payee_name": "Transaction 64", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -3200, - "category": null, - "cleared": 1, - "date": 20171014, - "error": null, - "id": "id24", - "imported_id": "1b25b9df-8aa4-47c8-9792-75f4c38e351f", - "imported_payee": "Transaction 11", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id11", - "payee_name": "Transaction 11", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -3342, - "category": null, - "cleared": 1, - "date": 20171014, - "error": null, - "id": "id28", - "imported_id": "f4b34ee3-6d5b-4625-bffc-057a72270140", - "imported_payee": "Transaction 48", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id15", - "payee_name": "Transaction 48", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -3984, - "category": null, - "cleared": 1, - "date": 20171014, - "error": null, - "id": "id26", - "imported_id": "768b6d36-0ad7-47fb-a631-145170adc0b9", - "imported_payee": "Transaction 107", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id13", - "payee_name": "Transaction 107", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -4524, - "category": null, - "cleared": 1, - "date": 20171014, - "error": null, - "id": "id30", - "imported_id": "f4e3a3f2-c20f-4667-8df6-6530bf5f4cb0", - "imported_payee": "Transaction 56", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id17", - "payee_name": "Transaction 56", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -4881, - "category": null, - "cleared": 1, - "date": 20171014, - "error": null, - "id": "id27", - "imported_id": "ceb53daf-7318-4aee-a562-19a45654eaa7", - "imported_payee": "Transaction 28", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id14", - "payee_name": "Transaction 28", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, - Object { - "account": "one", - "amount": -5541, - "category": null, - "cleared": 1, - "date": 20171014, - "error": null, - "id": "id29", - "imported_id": "af2d6eba-f75e-4ee0-a9ad-5d9f8264797b", - "imported_payee": "Transaction 114", - "is_child": 0, - "is_parent": 0, - "notes": null, - "parent_id": null, - "payee": "id16", - "payee_name": "Transaction 114", - "reconciled": 0, - "schedule": null, - "sort_order": 123456789, - "starting_balance_flag": 0, - "tombstone": 0, - "transfer_id": null, - }, -] -`; - exports[`Account sync reconcile handles transactions with undefined fields 1`] = ` Array [ Object { diff --git a/packages/loot-core/src/server/accounts/link.ts b/packages/loot-core/src/server/accounts/link.ts index ff2618af8..2f7282c5c 100644 --- a/packages/loot-core/src/server/accounts/link.ts +++ b/packages/loot-core/src/server/accounts/link.ts @@ -63,48 +63,6 @@ export async function findOrCreateBank(institution, requisitionId) { return bankData; } -export async function addAccounts(bankId, accountIds, offbudgetIds = []) { - const [[, userId], [, userKey]] = await asyncStorage.multiGet([ - 'user-id', - 'user-key', - ]); - - // Get all the available accounts - let accounts = await bankSync.getAccounts(userId, userKey, bankId); - - // Only add the selected accounts - accounts = accounts.filter(acct => accountIds.includes(acct.account_id)); - - return Promise.all( - accounts.map(async acct => { - const id = await runMutator(async () => { - const id = await db.insertAccount({ - account_id: acct.account_id, - name: acct.name, - official_name: acct.official_name, - balance_current: amountToInteger(acct.balances.current), - mask: acct.mask, - bank: bankId, - offbudget: offbudgetIds.includes(acct.account_id) ? 1 : 0, - }); - - // Create a transfer payee - await db.insertPayee({ - name: '', - transfer_acct: id, - }); - - return id; - }); - - // Do an initial sync - await bankSync.syncAccount(userId, userKey, id, acct.account_id, bankId); - - return id; - }), - ); -} - export async function addGoCardlessAccounts( bankId, accountIds, @@ -144,13 +102,7 @@ export async function addGoCardlessAccounts( }); // Do an initial sync - await bankSync.syncExternalAccount( - userId, - userKey, - id, - acct.account_id, - bankId, - ); + await bankSync.syncAccount(userId, userKey, id, acct.account_id, bankId); return id; }), diff --git a/packages/loot-core/src/server/accounts/sync.test.ts b/packages/loot-core/src/server/accounts/sync.test.ts index e6aaae936..0c8c9e8e1 100644 --- a/packages/loot-core/src/server/accounts/sync.test.ts +++ b/packages/loot-core/src/server/accounts/sync.test.ts @@ -1,24 +1,12 @@ // @ts-strict-ignore -import snapshotDiff from 'snapshot-diff'; - import * as monthUtils from '../../shared/months'; import * as db from '../db'; import { loadMappings } from '../db/mappings'; import { post } from '../post'; import { getServer } from '../server-config'; -import * as mockSyncServer from '../tests/mockSyncServer'; - -import { - syncAccount, - reconcileTransactions, - addTransactions, - fromPlaid, -} from './sync'; -import { loadRules, insertRule } from './transaction-rules'; -import * as transfer from './transfer'; -const papaJohns = 'Papa Johns east side'; -const lowes = 'Lowe’s Store'; +import { reconcileTransactions, addTransactions } from './sync'; +import { loadRules, insertRule } from './transaction-rules'; jest.mock('../../shared/months', () => ({ ...jest.requireActual('../../shared/months'), @@ -27,7 +15,6 @@ jest.mock('../../shared/months', () => ({ })); beforeEach(async () => { - mockSyncServer.reset(); jest.resetAllMocks(); (monthUtils.currentDay as jest.Mock).mockReturnValue('2017-10-15'); (monthUtils.currentMonth as jest.Mock).mockReturnValue('2017-10'); @@ -46,37 +33,6 @@ function getAllTransactions() { ); } -function expectSnapshotWithDiffer(initialValue) { - let currentValue = initialValue; - expect(initialValue).toMatchSnapshot(); - return { - expectToMatchDiff: value => { - expect(snapshotDiff(currentValue, value)).toMatchSnapshot(); - currentValue = value; - }, - }; -} - -function prepMockTransactions() { - let mockTransactions; - mockSyncServer.filterMockData(data => { - const account_id = data.accounts[0].account_id; - const transactions = data.transactions[account_id].filter(t => !t.pending); - - mockTransactions = [ - ...transactions.filter(t => t.date <= '2017-10-15'), - ...transactions.filter(t => t.date === '2017-10-16').slice(0, 1), - ...transactions.filter(t => t.date === '2017-10-17').slice(0, 3), - ]; - - return { - accounts: data.accounts, - transactions: { [account_id]: mockTransactions }, - }; - }); - return mockTransactions; -} - async function prepareDatabase() { await db.insertCategoryGroup({ id: 'group1', name: 'group1', is_income: 1 }); await db.insertCategory({ @@ -136,229 +92,6 @@ describe('Account sync', () => { ); }); - test('reconcile matches single transaction', async () => { - const mockTransactions = prepMockTransactions(); - const { id, account_id } = await prepareDatabase(); - - await syncAccount('userId', 'userKey', id, account_id, 'bank'); - - // The payee can be anything, all that matters is the amount is the same - const mockTransaction = mockTransactions.find(t => t.date === '2017-10-17'); - mockTransaction.amount = 29.47; - - const payeeId = await db.insertPayee({ name: 'macy' }); - await db.insertTransaction({ - id: 'one', - account: id, - amount: -2947, - date: '2017-10-15', - payee: payeeId, - }); - - const { added, updated } = await reconcileTransactions( - id, - mockTransactions.filter(t => t.date >= '2017-10-15').map(fromPlaid), - ); - - expect(added.length).toBe(3); - expect(updated.length).toBe(1); - - const transactions = await getAllTransactions(); - const transaction = transactions.find(t => t.amount === -2947); - expect(transaction.id).toBe(updated[0]); - - // The payee has not been updated - it's still the payee that the original transaction had - const payees = await getAllPayees(); - expect(payees.length).toBe(18); - expect(transaction.payee).toBe(payeeId); - }); - - test('reconcile matches multiple transactions', async () => { - const mockTransactions = prepMockTransactions(); - const { id, account_id } = await prepareDatabase(); - - await syncAccount('userId', 'userKey', id, account_id, 'bank'); - - // These should all match, but note that the one with the payee - // `macy` should match with the imported one with the same payee - // name. This should happen even though other transactions with - // the same amount are imported first, i.e. high fidelity matches - // always win - const mocked = mockTransactions.filter(t => t.date === '2017-10-17'); - mocked[0].name = papaJohns; - mocked[0].amount = 29.47; - mocked[1].name = 'Lowe’s Store'; - mocked[1].amount = 29.47; - mocked[2].name = 'macy'; - mocked[2].amount = 29.47; - - // Make sure that it macy is correctly matched from a different - // day first, and then the other two are matched based on amount. - // And it should never match the same transactions twice - await db.insertTransaction({ - id: 'one', - account: id, - amount: -2947, - date: '2017-10-15', - payee: await db.insertPayee({ name: 'papa johns' }), - }); - await db.insertTransaction({ - id: 'two', - account: id, - amount: -2947, - date: '2017-10-17', - payee: await db.insertPayee({ name: 'lowes' }), - }); - await db.insertTransaction({ - id: 'three', - account: id, - amount: -2947, - date: '2017-10-17', - payee: await db.insertPayee({ name: 'macy' }), - }); - - const { added, updated } = await reconcileTransactions( - id, - mockTransactions.filter(t => t.date >= '2017-10-15').map(fromPlaid), - ); - - const transactions = await getAllTransactions(); - expect(updated.length).toBe(3); - expect(added.length).toBe(1); - - expect(transactions.find(t => t.id === 'one').imported_id).toBe( - mocked[1].transaction_id, - ); - expect(transactions.find(t => t.id === 'two').imported_id).toBe( - mocked[0].transaction_id, - ); - expect(transactions.find(t => t.id === 'three').imported_id).toBe( - mocked[2].transaction_id, - ); - }); - - test('reconcile matches multiple transactions (imported_id wins)', async () => { - const mockTransactions = prepMockTransactions(); - const { id, account_id } = await prepareDatabase(); - - await syncAccount('userId', 'userKey', id, account_id, 'bank'); - - const mocked = mockTransactions.filter(t => t.date === '2017-10-17'); - mocked[0].name = papaJohns; - mocked[0].amount = 29.47; - mocked[1].name = lowes; - mocked[1].amount = 29.47; - mocked[1].transaction_id = 'imported1'; - - // Technically, the amount doesn't even matter. The - // imported_id will always match no matter what - await db.insertTransaction({ - id: 'one', - account: id, - amount: -3000, - date: '2017-10-15', - imported_id: 'imported1', - payee: await db.insertPayee({ name: 'papa johns' }), - }); - await db.insertTransaction({ - id: 'two', - account: id, - amount: -2947, - date: '2017-10-17', - payee: await db.insertPayee({ name: 'lowes' }), - }); - - const { added, updated } = await reconcileTransactions( - id, - mockTransactions.filter(t => t.date >= '2017-10-15').map(fromPlaid), - ); - - const transactions = await getAllTransactions(); - expect(updated).toEqual(['two', 'one']); - expect(added.length).toBe(2); - - // Make sure lowes, which has the imported_id, is the one that - // got matched with the same imported_id - expect(transactions.find(t => t.id === 'one').imported_payee).toBe(lowes); - }); - - test('import never matches existing with financial ids', async () => { - let mockTransactions = prepMockTransactions(); - const { id, account_id } = await prepareDatabase(); - - await syncAccount('userId', 'userKey', id, account_id, 'bank'); - const differ = expectSnapshotWithDiffer(await getAllTransactions()); - - mockTransactions = mockTransactions.filter(t => t.date === '2017-10-17'); - mockTransactions[0].name = 'foo'; - mockTransactions[0].amount = 29.47; - mockTransactions[1].name = 'bar'; - mockTransactions[1].amount = 29.47; - - // Make sure, no matter what, it never tries to match with an - // existing transaction that already has a financial id - await db.insertTransaction({ - id: 'one', - account: id, - amount: -2947, - date: '2017-10-15', - payee: await db.insertPayee({ name: 'foo' }), - imported_id: 'trans1', - }); - - await db.insertTransaction({ - id: 'two', - account: id, - amount: -2947, - date: '2017-10-15', - payee: await db.insertPayee({ name: 'bar' }), - imported_id: 'trans2', - }); - - differ.expectToMatchDiff(await getAllTransactions()); - - (monthUtils.currentDay as jest.Mock).mockReturnValue('2017-10-17'); - await syncAccount('userId', 'userKey', id, account_id, 'bank'); - - differ.expectToMatchDiff(await getAllTransactions()); - }); - - test('import updates transfers when matched', async () => { - const mockTransactions = prepMockTransactions(); - const { id, account_id } = await prepareDatabase(); - await db.insertAccount({ id: 'two', name: 'two' }); - await db.insertPayee({ - id: 'transfer-two', - name: '', - transfer_acct: 'two', - }); - - await syncAccount('userId', 'userKey', id, account_id, 'bank'); - const differ = expectSnapshotWithDiffer(await getAllTransactions()); - - const mockTransaction = mockTransactions.find(t => t.date === '2017-10-17'); - mockTransaction.name = '#001 fenn st Macy’s 33333 EMX'; - mockTransaction.amount = 29.48; - - const transactionId = await db.insertTransaction({ - id: 'one', - account: 'two', - amount: 2948, - date: '2017-10-15', - payee: 'transfer-' + id, - }); - await transfer.onInsert(await db.getTransaction(transactionId)); - - differ.expectToMatchDiff(await getAllTransactions()); - - (monthUtils.currentDay as jest.Mock).mockReturnValue('2017-10-17'); - await syncAccount('userId', 'userKey', id, account_id, 'bank'); - - // Don't use `differ.expectToMatchDiff` because there's too many - // changes that look too confusing - expect(await getAllTransactions()).toMatchSnapshot(); - }); - test('reconcile handles transactions with undefined fields', async () => { const { id: acctId } = await prepareDatabase(); @@ -609,17 +342,4 @@ describe('Account sync', () => { 'bakkerij-renamed', ]); }); - - test('imports transactions for current day and adds latest', async () => { - const { id, account_id } = await prepareDatabase(); - - expect((await getAllTransactions()).length).toBe(0); - await syncAccount('userId', 'userKey', id, account_id, 'bank'); - expect(await getAllTransactions()).toMatchSnapshot(); - - (monthUtils.currentDay as jest.Mock).mockReturnValue('2017-10-17'); - - await syncAccount('userId', 'userKey', id, account_id, 'bank'); - expect(await getAllTransactions()).toMatchSnapshot(); - }); }); diff --git a/packages/loot-core/src/server/accounts/sync.ts b/packages/loot-core/src/server/accounts/sync.ts index 494759b72..03bd3fb9d 100644 --- a/packages/loot-core/src/server/accounts/sync.ts +++ b/packages/loot-core/src/server/accounts/sync.ts @@ -328,7 +328,7 @@ async function normalizeTransactions( return { normalized, payeesToCreate }; } -async function normalizeExternalTransactions(transactions, acctId) { +async function normalizeBankSyncTransactions(transactions, acctId) { const payeesToCreate = new Map(); const normalized = []; @@ -437,14 +437,22 @@ async function createNewPayees(payeesToCreate, addsAndUpdates) { }); } -export async function reconcileExternalTransactions(acctId, transactions) { +export async function reconcileTransactions( + acctId, + transactions, + isBankSyncAccount = false, +) { console.log('Performing transaction reconciliation'); const hasMatched = new Set(); const updated = []; const added = []; - const { normalized, payeesToCreate } = await normalizeExternalTransactions( + const transactionNormalization = isBankSyncAccount + ? normalizeBankSyncTransactions + : normalizeTransactions; + + const { normalized, payeesToCreate } = await transactionNormalization( transactions, acctId, ); @@ -575,6 +583,7 @@ export async function reconcileExternalTransactions(acctId, transactions) { // Update the transaction const updates = { + ...(isBankSyncAccount ? {} : { date: trans.date }), imported_id: trans.imported_id || null, payee: existing.payee || trans.payee || null, category: existing.category || trans.category || null, @@ -630,190 +639,6 @@ export async function reconcileExternalTransactions(acctId, transactions) { }; } -export async function reconcileTransactions(acctId, transactions) { - const hasMatched = new Set(); - const updated = []; - const added = []; - - const { normalized, payeesToCreate } = await normalizeTransactions( - transactions, - acctId, - ); - - // The first pass runs the rules, and preps data for fuzzy matching - const transactionsStep1 = []; - for (const { - payee_name, - trans: originalTrans, - subtransactions, - } of normalized) { - // Run the rules - const trans = runRules(originalTrans); - - let match = null; - let fuzzyDataset = null; - - // First, match with an existing transaction's imported_id. This - // is the highest fidelity match and should always be attempted - // first. - if (trans.imported_id) { - match = await db.first( - 'SELECT * FROM v_transactions WHERE imported_id = ? AND account = ?', - [trans.imported_id, acctId], - ); - - if (match) { - hasMatched.add(match.id); - } - } - - // If it didn't match, query data needed for fuzzy matching - if (!match) { - // Look 7 days ahead and 7 days back when fuzzy matching. This - // needs to select all fields that need to be read from the - // matched transaction. See the final pass below for the needed - // fields. - fuzzyDataset = await db.all( - `SELECT id, is_parent, date, imported_id, payee, category, notes, reconciled FROM v_transactions - WHERE date >= ? AND date <= ? AND amount = ? AND account = ?`, - [ - db.toDateRepr(monthUtils.subDays(trans.date, 7)), - db.toDateRepr(monthUtils.addDays(trans.date, 7)), - trans.amount || 0, - acctId, - ], - ); - - // Sort the matched transactions according to the distance from the original - // transactions date. i.e. if the original transaction is in 21-02-2024 and - // the matched transactions are: 20-02-2024, 21-02-2024, 29-02-2024 then - // the resulting data-set should be: 21-02-2024, 20-02-2024, 29-02-2024. - fuzzyDataset = fuzzyDataset.sort((a, b) => { - const aDistance = Math.abs( - dateFns.differenceInMilliseconds( - dateFns.parseISO(trans.date), - dateFns.parseISO(db.fromDateRepr(a.date)), - ), - ); - const bDistance = Math.abs( - dateFns.differenceInMilliseconds( - dateFns.parseISO(trans.date), - dateFns.parseISO(db.fromDateRepr(b.date)), - ), - ); - return aDistance > bDistance ? 1 : -1; - }); - } - - transactionsStep1.push({ - payee_name, - trans, - subtransactions: trans.subtransactions || subtransactions, - match, - fuzzyDataset, - }); - } - - // Next, do the fuzzy matching. This first pass matches based on the - // payee id. We do this in multiple passes so that higher fidelity - // matching always happens first, i.e. a transaction should match - // match with low fidelity if a later transaction is going to match - // the same one with high fidelity. - const transactionsStep2 = transactionsStep1.map(data => { - if (!data.match && data.fuzzyDataset) { - // Try to find one where the payees match. - const match = data.fuzzyDataset.find( - row => !hasMatched.has(row.id) && data.trans.payee === row.payee, - ); - - if (match) { - hasMatched.add(match.id); - return { ...data, match }; - } - } - return data; - }); - - // The final fuzzy matching pass. This is the lowest fidelity - // matching: it just find the first transaction that hasn't been - // matched yet. Remember the dataset only contains transactions - // around the same date with the same amount. - const transactionsStep3 = transactionsStep2.map(data => { - if (!data.match && data.fuzzyDataset) { - const match = data.fuzzyDataset.find(row => !hasMatched.has(row.id)); - if (match) { - hasMatched.add(match.id); - return { ...data, match }; - } - } - return data; - }); - - // Finally, generate & commit the changes - for (const { trans, subtransactions, match } of transactionsStep3) { - if (match) { - // Skip updating already reconciled (locked) transactions - if (match.reconciled) { - continue; - } - - // TODO: change the above sql query to use aql - const existing = { - ...match, - cleared: match.cleared === 1, - date: db.fromDateRepr(match.date), - }; - - // Update the transaction - const updates = { - date: trans.date, - imported_id: trans.imported_id || null, - payee: existing.payee || trans.payee || null, - category: existing.category || trans.category || null, - imported_payee: trans.imported_payee || null, - notes: existing.notes || trans.notes || null, - cleared: trans.cleared != null ? trans.cleared : true, - }; - - if (hasFieldsChanged(existing, updates, Object.keys(updates))) { - updated.push({ id: existing.id, ...updates }); - } - - if (existing.is_parent && existing.cleared !== updates.cleared) { - const children = await db.all( - 'SELECT id FROM v_transactions WHERE parent_id = ?', - [existing.id], - ); - for (const child of children) { - updated.push({ id: child.id, cleared: updates.cleared }); - } - } - } else { - // Insert a new transaction - const finalTransaction = { - ...trans, - id: uuidv4(), - category: trans.category || null, - cleared: trans.cleared != null ? trans.cleared : true, - }; - - if (subtransactions && subtransactions.length > 0) { - added.push(...makeSplitTransaction(finalTransaction, subtransactions)); - } else { - added.push(finalTransaction); - } - } - } - - await createNewPayees(payeesToCreate, [...added, ...updated]); - await batchUpdateTransactions({ added, updated }); - - return { - added: added.map(trans => trans.id), - updated: updated.map(trans => trans.id), - }; -} - // This is similar to `reconcileTransactions` except much simpler: it // does not try to match any transactions. It just adds them export async function addTransactions( @@ -872,7 +697,13 @@ export async function addTransactions( return newTransactions; } -export async function syncExternalAccount(userId, userKey, id, acctId, bankId) { +export async function syncAccount( + userId: string, + userKey: string, + id: string, + acctId: string, + bankId: string, +) { // TODO: Handle the case where transactions exist in the future // (that will make start date after end date) const latestTransaction = await db.first( @@ -887,9 +718,8 @@ export async function syncExternalAccount(userId, userKey, id, acctId, bankId) { 'SELECT date FROM v_transactions WHERE account = ? ORDER BY date ASC LIMIT 1', [id], ); - const startingDate = monthUtils.parseDate( - db.fromDateRepr(startingTransaction.date), - ); + const startingDate = db.fromDateRepr(startingTransaction.date); + // assert(startingTransaction) const startDate = monthUtils.dayFromDate( dateFns.max([ @@ -898,7 +728,7 @@ export async function syncExternalAccount(userId, userKey, id, acctId, bankId) { monthUtils.parseDate(monthUtils.subDays(monthUtils.currentDay(), 90)), // Never download transactions before the starting date. - startingDate, + monthUtils.parseDate(startingDate), ]), ); @@ -914,6 +744,31 @@ export async function syncExternalAccount(userId, userKey, id, acctId, bankId) { bankId, startDate, ); + } else { + // Get all transactions since the latest transaction, plus any 5 + // days before the latest transaction. This gives us a chance to + // resolve any transactions that were entered manually. + // + // TODO: What this really should do is query the last imported_id + // and since then + let date = monthUtils.subDays( + db.fromDateRepr(latestTransaction.date), + 31, + ); + + // Never download transactions before the starting date. This was + // when the account was added to the system. + if (date < startingDate) { + date = startingDate; + } + + download = await downloadTransactions( + userId, + userKey, + acctId, + bankId, + date, + ); } const { transactions: originalTransactions, accountBalance } = download; @@ -928,16 +783,16 @@ export async function syncExternalAccount(userId, userKey, id, acctId, bankId) { })); return runMutator(async () => { - const result = await reconcileExternalTransactions(id, transactions); + const result = await reconcileTransactions(id, transactions, true); await updateAccountBalance(id, accountBalance); return result; }); } else { + let download; + // Otherwise, download transaction for the past 90 days const startingDay = monthUtils.subDays(monthUtils.currentDay(), 90); - let download; - if (acctRow.account_sync_source === 'simpleFin') { download = await downloadSimpleFinTransactions(acctId, startingDay); } else if (acctRow.account_sync_source === 'goCardless') { @@ -950,12 +805,11 @@ export async function syncExternalAccount(userId, userKey, id, acctId, bankId) { ); } - const { transactions, startingBalance } = download; - - let balanceToUse = startingBalance; + const { transactions } = download; + let balanceToUse = download.startingBalance; if (acctRow.account_sync_source === 'simpleFin') { - const currentBalance = startingBalance; + const currentBalance = download.startingBalance; const previousBalance = transactions.reduce((total, trans) => { return ( total - parseInt(trans.transactionAmount.amount.replace('.', '')) @@ -984,109 +838,7 @@ export async function syncExternalAccount(userId, userKey, id, acctId, bankId) { starting_balance_flag: true, }); - const result = await reconcileExternalTransactions(id, transactions); - return { - ...result, - added: [initialId, ...result.added], - }; - }); - } -} - -export async function syncAccount(userId, userKey, id, acctId, bankId) { - // TODO: Handle the case where transactions exist in the future - // (that will make start date after end date) - const latestTransaction = await db.first( - 'SELECT * FROM v_transactions WHERE account = ? ORDER BY date DESC LIMIT 1', - [id], - ); - - if (latestTransaction) { - const startingTransaction = await db.first( - 'SELECT date FROM v_transactions WHERE account = ? ORDER BY date ASC LIMIT 1', - [id], - ); - const startingDate = db.fromDateRepr(startingTransaction.date); - // assert(startingTransaction) - - // Get all transactions since the latest transaction, plus any 5 - // days before the latest transaction. This gives us a chance to - // resolve any transactions that were entered manually. - // - // TODO: What this really should do is query the last imported_id - // and since then - let date = monthUtils.subDays(db.fromDateRepr(latestTransaction.date), 31); - - // Never download transactions before the starting date. This was - // when the account was added to the system. - if (date < startingDate) { - date = startingDate; - } - - const { transactions: originalTransactions, accountBalance } = - await downloadTransactions(userId, userKey, acctId, bankId, date); - if (originalTransactions.length === 0) { - return { added: [], updated: [] }; - } - - const transactions = originalTransactions.map(trans => ({ - ...trans, - account: id, - })); - - return runMutator(async () => { - const result = await reconcileTransactions(id, transactions); - await updateAccountBalance(id, accountBalance); - return result; - }); - } else { - const acctRow = await db.select('accounts', id); - - // Otherwise, download transaction for the last few days if it's an - // on-budget account, or for the past 30 days if off-budget - const startingDay = monthUtils.subDays( - monthUtils.currentDay(), - acctRow.offbudget === 0 ? 1 : 30, - ); - - const { transactions } = await downloadTransactions( - userId, - userKey, - acctId, - bankId, - dateFns.format(dateFns.parseISO(startingDay), 'yyyy-MM-dd'), - ); - - // We need to add a transaction that represents the starting - // balance for everything to balance out. In order to get balance - // before the first imported transaction, we need to get the - // current balance from the accounts table and subtract all the - // imported transactions. - const currentBalance = acctRow.balance_current; - - const previousBalance = transactions.reduce((total, trans) => { - return total - trans.amount; - }, currentBalance); - - const oldestDate = - transactions.length > 0 - ? transactions[transactions.length - 1].date - : monthUtils.currentDay(); - - const payee = await getStartingBalancePayee(); - - return runMutator(async () => { - const initialId = await db.insertTransaction({ - account: id, - amount: previousBalance, - category: acctRow.offbudget === 0 ? payee.category : null, - payee: payee.id, - date: oldestDate, - cleared: true, - starting_balance_flag: true, - }); - - const result = await reconcileTransactions(id, transactions); + const result = await reconcileTransactions(id, transactions, true); return { ...result, added: [initialId, ...result.added], diff --git a/packages/loot-core/src/server/main.test.ts b/packages/loot-core/src/server/main.test.ts index 4625011bc..ea4d87130 100644 --- a/packages/loot-core/src/server/main.test.ts +++ b/packages/loot-core/src/server/main.test.ts @@ -16,7 +16,6 @@ import { disableGlobalMutations, enableGlobalMutations, } from './mutators'; -import { post } from './post'; import * as prefs from './prefs'; import * as sheet from './sheet'; @@ -103,63 +102,6 @@ describe('Budgets', () => { }); describe('Accounts', () => { - test('create accounts with correct starting balance', async () => { - prefs.loadPrefs(); - prefs.savePrefs({ groupId: 'group' }); - - await runMutator(async () => { - // An income category is required because the starting balance is - // categorized to it. Create one now. - await db.insertCategoryGroup({ - id: 'group1', - name: 'income', - is_income: 1, - }); - await db.insertCategory({ - name: 'income', - cat_group: 'group1', - is_income: 1, - }); - }); - - // Get accounts from the server. This isn't the normal API call, - // we know that the mock server just returns hardcoded accounts - const { accounts } = await post('/plaid/accounts', {}); - - // Create the accounts for the bank (bank is generally ignored in tests) - await runHandler(handlers['accounts-connect'], { - institution: { institution_id: 1, name: 'Jamesy Bank' }, - publicToken: 'foo', - accountIds: accounts.map(acct => acct.account_id), - }); - - // Import transactions for all accounts - await runHandler(handlers['accounts-sync'], {}); - - // Go through each account and make sure the starting balance was - // created correctly - const res = await db.all('SELECT * FROM accounts'); - for (const account of res) { - const sum = await db.first( - 'SELECT sum(amount) as sum FROM transactions WHERE acct = ? AND starting_balance_flag = 0', - [account.id], - ); - const starting = await db.first( - 'SELECT * FROM transactions WHERE acct = ? AND starting_balance_flag = 1', - [account.id], - ); - expect(account.balance_current - sum.sum).toBe(starting.amount); - - // Also ensure that the starting balance has the earliest date - // possible - const earliestTrans = await db.first( - 'SELECT p.name as payee_name FROM transactions t LEFT JOIN payees p ON p.id = t.description WHERE acct = ? ORDER BY date LIMIT 1', - [account.id], - ); - expect(earliestTrans.payee_name).toBe('Starting Balance'); - } - }); - test('Transfers are properly updated', async () => { await runMutator(async () => { await db.insertAccount({ id: 'one', name: 'one' }); diff --git a/packages/loot-core/src/server/main.ts b/packages/loot-core/src/server/main.ts index 0cc70892d..c0ca4ec8e 100644 --- a/packages/loot-core/src/server/main.ts +++ b/packages/loot-core/src/server/main.ts @@ -610,54 +610,6 @@ handlers['account-properties'] = async function ({ id }) { return { balance: balance || 0, numTransactions: count }; }; -handlers['accounts-link'] = async function ({ - institution, - publicToken, - accountId, - upgradingId, -}) { - const bankId = await link.handoffPublicToken(institution, publicToken); - - const [[, userId], [, userKey]] = await asyncStorage.multiGet([ - 'user-id', - 'user-key', - ]); - - // Get all the available accounts and find the selected one - const accounts = await bankSync.getGoCardlessAccounts( - userId, - userKey, - bankId, - ); - const account = accounts.find(acct => acct.account_id === accountId); - - await db.update('accounts', { - id: upgradingId, - account_id: account.account_id, - official_name: account.official_name, - balance_current: amountToInteger(account.balances.current), - balance_available: amountToInteger(account.balances.available), - balance_limit: amountToInteger(account.balances.limit), - mask: account.mask, - bank: bankId, - }); - - await bankSync.syncAccount( - userId, - userKey, - upgradingId, - account.account_id, - bankId, - ); - - connection.send('sync-event', { - type: 'success', - tables: ['transactions'], - }); - - return 'ok'; -}; - handlers['gocardless-accounts-link'] = async function ({ requisitionId, account, @@ -694,7 +646,7 @@ handlers['gocardless-accounts-link'] = async function ({ }); } - await bankSync.syncExternalAccount( + await bankSync.syncAccount( undefined, undefined, id, @@ -752,7 +704,7 @@ handlers['simplefin-accounts-link'] = async function ({ }); } - await bankSync.syncExternalAccount( + await bankSync.syncAccount( undefined, undefined, id, @@ -768,17 +720,6 @@ handlers['simplefin-accounts-link'] = async function ({ return 'ok'; }; -handlers['accounts-connect'] = async function ({ - institution, - publicToken, - accountIds, - offbudgetIds, -}) { - const bankId = await link.handoffPublicToken(institution, publicToken); - const ids = await link.addAccounts(bankId, accountIds, offbudgetIds); - return ids; -}; - handlers['gocardless-accounts-connect'] = async function ({ institution, publicToken, @@ -1288,7 +1229,7 @@ handlers['gocardless-accounts-sync'] = async function ({ id }) { if (acct.bankId) { try { console.group('Bank Sync operation'); - const res = await bankSync.syncExternalAccount( + const res = await bankSync.syncAccount( userId, userKey, acct.id, diff --git a/packages/loot-core/src/types/server-handlers.d.ts b/packages/loot-core/src/types/server-handlers.d.ts index eff46e2f6..476bbeaa9 100644 --- a/packages/loot-core/src/types/server-handlers.d.ts +++ b/packages/loot-core/src/types/server-handlers.d.ts @@ -149,13 +149,6 @@ export interface ServerHandlers { id; }) => Promise<{ balance: number; numTransactions: number }>; - 'accounts-link': (arg: { - institution; - publicToken; - accountId; - upgradingId; - }) => Promise<'ok'>; - 'gocardless-accounts-link': (arg: { requisitionId; account; @@ -167,13 +160,6 @@ export interface ServerHandlers { upgradingId; }) => Promise<'ok'>; - 'accounts-connect': (arg: { - institution; - publicToken; - accountIds; - offbudgetIds?; - }) => Promise<unknown>; - 'gocardless-accounts-connect': (arg: { institution; publicToken; diff --git a/upcoming-release-notes/2534.md b/upcoming-release-notes/2534.md new file mode 100644 index 000000000..6101f9b69 --- /dev/null +++ b/upcoming-release-notes/2534.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [MatissJanis] +--- + +Removing code duplication in bank-sync logic -- GitLab