From dd7a7fa796e47af70082a03896787da8928e30c3 Mon Sep 17 00:00:00 2001
From: Matiss Janis Aboltins <matiss@mja.lv>
Date: Sat, 2 Sep 2023 18:03:38 +0100
Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20(imports)=20ability=20to=20toggl?=
 =?UTF-8?q?e=20on/off=20OFX=20import=20fallback=20payee=20(#1631)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../components/modals/ImportTransactions.js   | 38 ++++++++++++++++++-
 .../src/server/accounts/parse-file.ts         | 23 +++++++----
 packages/loot-core/src/types/prefs.d.ts       |  1 +
 upcoming-release-notes/1631.md                |  6 +++
 4 files changed, 59 insertions(+), 9 deletions(-)
 create mode 100644 upcoming-release-notes/1631.md

diff --git a/packages/desktop-client/src/components/modals/ImportTransactions.js b/packages/desktop-client/src/components/modals/ImportTransactions.js
index 760650c10..e301c4503 100644
--- a/packages/desktop-client/src/components/modals/ImportTransactions.js
+++ b/packages/desktop-client/src/components/modals/ImportTransactions.js
@@ -577,6 +577,9 @@ export default function ImportTransactions({ modalProps, options }) {
   let [hasHeaderRow, setHasHeaderRow] = useState(
     prefs[`csv-has-header-${accountId}`] ?? true,
   );
+  let [fallbackMissingPayeeToMemo, setFallbackMissingPayeeToMemo] = useState(
+    prefs[`ofx-fallback-missing-payee-${accountId}`] ?? true,
+  );
 
   let [parseDateFormat, setParseDateFormat] = useState(null);
 
@@ -645,10 +648,14 @@ export default function ImportTransactions({ modalProps, options }) {
   }
 
   useEffect(() => {
+    const fileType = getFileType(options.filename);
+
     parse(
       options.filename,
-      getFileType(options.filename) === 'csv'
+      fileType === 'csv'
         ? { delimiter: csvDelimiter, hasHeaderRow }
+        : fileType === 'ofx'
+        ? { fallbackMissingPayeeToMemo }
         : null,
     );
   }, [parseTransactions, options.filename]);
@@ -693,9 +700,15 @@ export default function ImportTransactions({ modalProps, options }) {
       ],
     });
 
+    const fileType = getFileType(res[0]);
+
     parse(
       res[0],
-      getFileType(res[0]) === 'csv' ? { delimiter: csvDelimiter } : null,
+      fileType === 'csv'
+        ? { delimiter: csvDelimiter }
+        : fileType === 'ofx'
+        ? { fallbackMissingPayeeToMemo }
+        : null,
     );
   }
 
@@ -754,6 +767,12 @@ export default function ImportTransactions({ modalProps, options }) {
       savePrefs({ [key]: parseDateFormat });
     }
 
+    if (filetype === 'ofx') {
+      savePrefs({
+        [`ofx-fallback-missing-payee-${accountId}`]: fallbackMissingPayeeToMemo,
+      });
+    }
+
     if (filetype === 'csv') {
       savePrefs({
         [`csv-mappings-${accountId}`]: JSON.stringify(fieldMappings),
@@ -881,6 +900,21 @@ export default function ImportTransactions({ modalProps, options }) {
         </View>
       )}
 
+      {filetype === 'ofx' && (
+        <CheckboxOption
+          id="form_fallback_missing_payee"
+          checked={fallbackMissingPayeeToMemo}
+          onChange={() => {
+            setFallbackMissingPayeeToMemo(state => !state);
+            parse(filename, {
+              fallbackMissingPayeeToMemo: !fallbackMissingPayeeToMemo,
+            });
+          }}
+        >
+          Use Memo as a fallback for empty Payees
+        </CheckboxOption>
+      )}
+
       {/*Import Options */}
       {(filetype === 'qif' || filetype === 'csv') && (
         <View style={{ marginTop: 25 }}>
diff --git a/packages/loot-core/src/server/accounts/parse-file.ts b/packages/loot-core/src/server/accounts/parse-file.ts
index 16434a8a2..4a3a2d1bc 100644
--- a/packages/loot-core/src/server/accounts/parse-file.ts
+++ b/packages/loot-core/src/server/accounts/parse-file.ts
@@ -14,7 +14,11 @@ export type ParseFileResult = {
 
 export async function parseFile(
   filepath,
-  options?: { delimiter?: string; hasHeaderRow: boolean },
+  options?: {
+    delimiter?: string;
+    hasHeaderRow: boolean;
+    fallbackMissingPayeeToMemo?: boolean;
+  },
 ): Promise<ParseFileResult> {
   let errors = Array<ParseError>();
   let m = filepath.match(/\.[^.]*$/);
@@ -30,7 +34,7 @@ export async function parseFile(
         return parseCSV(filepath, options);
       case '.ofx':
       case '.qfx':
-        return parseOFX(filepath);
+        return parseOFX(filepath, options);
       default:
     }
   }
@@ -101,7 +105,12 @@ async function parseQIF(filepath): Promise<ParseFileResult> {
   };
 }
 
-async function parseOFX(filepath): Promise<ParseFileResult> {
+async function parseOFX(
+  filepath,
+  options: { fallbackMissingPayeeToMemo?: boolean } = {
+    fallbackMissingPayeeToMemo: true,
+  },
+): Promise<ParseFileResult> {
   let { getOFXTransactions, initModule } = await import(
     /* webpackChunkName: 'xfo' */ 'node-libofx'
   );
@@ -123,7 +132,7 @@ async function parseOFX(filepath): Promise<ParseFileResult> {
 
   // Banks don't always implement the OFX standard properly
   // If no payee is available try and fallback to memo
-  let useName = data.some(trans => trans.name != null && trans.name !== '');
+  let useMemoFallback = options.fallbackMissingPayeeToMemo;
 
   return {
     errors,
@@ -131,9 +140,9 @@ async function parseOFX(filepath): Promise<ParseFileResult> {
       amount: trans.amount,
       imported_id: trans.fi_id,
       date: trans.date ? dayFromDate(new Date(trans.date * 1000)) : null,
-      payee_name: useName ? trans.name : trans.memo,
-      imported_payee: useName ? trans.name : trans.memo,
-      notes: useName ? trans.memo || null : null, //memo used for payee
+      payee_name: trans.name || (useMemoFallback ? trans.memo : null),
+      imported_payee: trans.name || (useMemoFallback ? trans.memo : null),
+      notes: !!trans.name || !useMemoFallback ? trans.memo || null : null, //memo used for payee
     })),
   };
 }
diff --git a/packages/loot-core/src/types/prefs.d.ts b/packages/loot-core/src/types/prefs.d.ts
index ece250533..18c8f52d1 100644
--- a/packages/loot-core/src/types/prefs.d.ts
+++ b/packages/loot-core/src/types/prefs.d.ts
@@ -34,6 +34,7 @@ export type LocalPrefs = Partial<
     [key: `csv-mappings-${string}`]: string;
     [key: `csv-delimiter-${string}`]: ',' | ';' | '\t';
     [key: `csv-has-header-${string}`]: boolean;
+    [key: `ofx-fallback-missing-payee-${string}`]: boolean;
     [key: `flip-amount-${string}-${'csv' | 'qif'}`]: boolean;
     'flags.updateNotificationShownForVersion': string;
     id: string;
diff --git a/upcoming-release-notes/1631.md b/upcoming-release-notes/1631.md
new file mode 100644
index 000000000..3de4609c9
--- /dev/null
+++ b/upcoming-release-notes/1631.md
@@ -0,0 +1,6 @@
+---
+category: Enhancements
+authors: [MatissJanis]
+---
+
+Imports: ability to toggle on/off the fallback logic for payee field (OFX imports)
-- 
GitLab