Skip to content

finances/stmt.ts: questrade added#13

Merged
denversc merged 6 commits into
mainfrom
questrade
Jun 17, 2026
Merged

finances/stmt.ts: questrade added#13
denversc merged 6 commits into
mainfrom
questrade

Conversation

@denversc

Copy link
Copy Markdown
Owner

No description provided.

denversc added 5 commits June 17, 2026 00:21
diff --git a/finances/stmt.ts b/finances/stmt.ts
index f29a007..09ba10c 100644
--- a/finances/stmt.ts
+++ b/finances/stmt.ts
@@ -105,7 +105,7 @@ function isParsePdfError(e: unknown): e is ParsePdfError {

 interface ParsedPublicMobileStatement {
   type: "PublicMobileStatement";
-  invoiceDate: Date;
+  invoiceDate: string;
   totalAmountPaid: string;
 }

@@ -125,9 +125,9 @@ function parsePublicMobileStatement(
       message: "expected line after INVOICE line",
     };
   }
-  let invoiceDate: Date;
+  let parsedInvoiceDate: Date;
   try {
-    invoiceDate = parse(invoiceDateStr, "MMM D, YYYY", "en");
+    parsedInvoiceDate = parse(invoiceDateStr, "MMM D, YYYY", "en");
   } catch (e: unknown) {
     return {
       type: "ParsePdfError",
@@ -135,13 +135,15 @@ function parsePublicMobileStatement(
     };
   }

-  if (isNaN(invoiceDate.getTime())) {
+  if (isNaN(parsedInvoiceDate.getTime())) {
     return {
       type: "ParsePdfError",
       message: `invalid invoice date: ${invoiceDateStr}`,
     };
   }

+  const invoiceDate = format(parsedInvoiceDate, "YYYY-MM-DD");
+
   const totalAmountPaidLine = pdfLines.find((line) =>
     line.toLowerCase().startsWith("total amount paid"),
   );
@@ -157,7 +159,45 @@ function parsePublicMobileStatement(
   return { type: "PublicMobileStatement", invoiceDate, totalAmountPaid };
 }

-type ParsedPdf = ParsedPublicMobileStatement;
+interface ParsedQuestradeStatement {
+  type: QuestradeStatementType;
+  statementDate: Date;
+  accountNumber: string;
+  balance: string;
+}
+
+function parseQuestradeStatement(
+  pdfLines: readonly string[],
+): ParsedQuestradeStatement | ParsePdfError {
+  const type = identifyQuestradeStatementType(pdfLines);
+  if (!type) {
+    return {
+      type: "ParsePdfError",
+      message: "Unable to determine Questrade statement type",
+    };
+  }
+
+  const accountNumberRegex = /Account\s*#:\s*(\d+)/i;
+  const accountNumberLine = pdfLines.find((line) =>
+    line.match(accountNumberRegex),
+  );
+  if (!accountNumberLine) {
+    return { type: "ParsePdfError", message: "Account number line not found" };
+  }
+  const accountNumber = accountNumberLine.match(accountNumberRegex)?.[1];
+  if (!accountNumber) {
+    throw new Error(
+      "internal error rhtan4myg2: " + "accountNumber should have matched",
+    );
+  }
+
+  const statementDate = new Date();
+  const balance = "$0.00";
+
+  return { type, statementDate, accountNumber, balance };
+}
+
+type ParsedPdf = ParsedPublicMobileStatement | ParsedQuestradeStatement;

 function parsePdf(pdfLines: string[]): ParsedPdf | ParsePdfError {
   const type = identify(pdfLines);
@@ -165,6 +205,8 @@ function parsePdf(pdfLines: string[]): ParsedPdf | ParsePdfError {
     return { type: "ParsePdfError", message: "unrecognized pdf content" };
   } else if (type === "PublicMobileStatement") {
     return parsePublicMobileStatement(pdfLines);
+  } else if (isQuestradeStatementType(type)) {
+    return parseQuestradeStatement(pdfLines);
   } else {
     unreachable(type, "unknown type");
   }
@@ -175,6 +217,8 @@ function calculateFileName(parsedPdf: ParsedPdf): string {
     const { invoiceDate, totalAmountPaid } = parsedPdf;
     const formattedDate = format(invoiceDate, "YYYY-MM-DD");
     return `${formattedDate} Public Mobile Payment ${totalAmountPaid}.pdf`;
+  } else if (isQuestradeStatementType(parsedPdf.type)) {
+    throw new Error("not implemented h6rjr85kc6");
   } else {
     unreachable(parsedPdf.type, "unknown type");
   }
@@ -350,13 +394,10 @@ async function parseCommand(
   if (options?.v) {
     console.log(filePath);
   }
-  console.log({
-    ...parsedPdf,
-    invoiceDate: format(parsedPdf.invoiceDate, "YYYY-MM-DD"),
-  });
+  console.log(parsedPdf);
 }

-function identify(pdfLines: string[]): PdfType | undefined {
+function identify(pdfLines: readonly string[]): PdfType | undefined {
   if (pdfLines.includes("Public Mobile Account")) {
     return "PublicMobileStatement";
   }
@@ -366,26 +407,56 @@ function identify(pdfLines: string[]): PdfType | undefined {
       line.toLowerCase().startsWith("questrade wealth management inc."),
     )
   ) {
-    if (pdfLines.some((line) => line.includes("RESP"))) {
-      return "QuestradeRESPStatement";
-    } else if (
-      pdfLines.some((line) =>
-        line.toLowerCase().includes("registered retirement savings plan"),
-      )
-    ) {
-      return "QuestradeRRSPStatement";
-    } else if (
-      pdfLines.some((line) =>
-        line.toLowerCase().includes("individual margin account"),
-      )
-    ) {
-      return "QuestradeMarginStatement";
+    const questradeStatementType = identifyQuestradeStatementType(pdfLines);
+    if (questradeStatementType) {
+      return questradeStatementType;
     }
   }

   return undefined;
 }

+type QuestradeStatementType =
+  | "QuestradeRESPStatement"
+  | "QuestradeRRSPStatement"
+  | "QuestradeMarginStatement";
+
+function isQuestradeStatementType(
+  value: unknown,
+): value is QuestradeStatementType {
+  return (
+    value === "QuestradeRESPStatement" ||
+    value === "QuestradeRRSPStatement" ||
+    value === "QuestradeMarginStatement"
+  );
+}
+
+function identifyQuestradeStatementType(
+  pdfLines: readonly string[],
+): QuestradeStatementType | undefined {
+  if (pdfLines.some((line) => line.includes("(RESP)"))) {
+    return "QuestradeRESPStatement";
+  }
+
+  if (
+    pdfLines.some((line) =>
+      line.toLowerCase().includes("registered retirement savings plan"),
+    )
+  ) {
+    return "QuestradeRRSPStatement";
+  }
+
+  if (
+    pdfLines.some((line) =>
+      line.toLowerCase().includes("individual margin account"),
+    )
+  ) {
+    return "QuestradeMarginStatement";
+  }
+
+  return undefined;
+}
+
 interface IdentifyOptions {
   v?: boolean;
 }
diff --git a/finances/stmt.ts b/finances/stmt.ts
index e38ee20..2f24d27 100644
--- a/finances/stmt.ts
+++ b/finances/stmt.ts
@@ -273,7 +273,18 @@ function calculateFileName(parsedPdf: ParsedPdf): string {
     const { invoiceDate, totalAmountPaid } = parsedPdf;
     return `${invoiceDate} Public Mobile Payment ${totalAmountPaid}.pdf`;
   } else if (isQuestradeStatementType(parsedPdf.type)) {
-    throw new Error("not implemented h6rjr85kc6");
+    const { statementDate, accountNumber, balance } = parsedPdf;
+    let typeName: string;
+    if (parsedPdf.type === "QuestradeRESPStatement") {
+      typeName = "RESP";
+    } else if (parsedPdf.type === "QuestradeRRSPStatement") {
+      typeName = "RRSP";
+    } else if (parsedPdf.type === "QuestradeMarginStatement") {
+      typeName = "Margin Account";
+    } else {
+      unreachable(parsedPdf.type, "unknown type");
+    }
+    return `${statementDate} Questrade ${typeName} ${accountNumber} Statement ${balance}.pdf`;
   } else {
     unreachable(parsedPdf.type, "unknown type");
   }

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request adds support for parsing Questrade statements (RESP, RRSP, and Margin Accounts) to the finance statement parser, alongside refactoring date parsing to use a helper function that returns a formatted string. The review feedback highlights a copy-paste error in an error message, opportunities to simplify and optimize several regular expressions, and a suggestion to use RegExp.prototype.test() for more idiomatic boolean checks.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread finances/stmt.ts
Comment thread finances/stmt.ts Outdated
Comment thread finances/stmt.ts Outdated
Comment thread finances/stmt.ts
Comment thread finances/stmt.ts
@denversc denversc merged commit 1fb93d2 into main Jun 17, 2026
3 checks passed
@denversc denversc deleted the questrade branch June 17, 2026 05:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant