Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/finances.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
name: Finances CI
name: finances

on:
workflow_dispatch:
push:
branches: [main]
paths:
Expand Down
64 changes: 64 additions & 0 deletions finances/document_utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { isDocumentParseError, type DocumentParseError } from "./document.ts";
import { parseDateToYYYYMMDD, isParseDateError } from "./date.ts";

interface StringFromLinesOptions {
resultPrefix: string;
}

export function stringFromLines(
lines: readonly string[],
regex: RegExp,
options?: Partial<StringFromLinesOptions>,
): string | DocumentParseError {
for (const line of lines) {
const trimmedLine = line.trim();
const match = trimmedLine.match(regex);
if (!match) {
continue;
}

const matchingString = match[1];
if (typeof matchingString === "undefined") {
throw new Error(
`internal error stdp7xw6rb: regex should have matched line; ` +
`regex=${regex.source}, line=${trimmedLine}`,
);
}

const resultPrefix = options?.resultPrefix;
if (typeof resultPrefix === "undefined") {
return matchingString;
} else {
return resultPrefix + matchingString;
}
}

return {
type: "DocumentParseError",
message: `line not found matching regex: ${regex.source}`,
};
}

export function yyyymmddDateFromLines(
lines: readonly string[],
regex: RegExp,
dateFormat: string,
): string | DocumentParseError {
const dateStr = stringFromLines(lines, regex);
if (isDocumentParseError(dateStr)) {
return dateStr;
}

const date = parseDateToYYYYMMDD(dateFormat, dateStr);
if (isParseDateError(date)) {
const { message } = date;
return {
type: "DocumentParseError",
message:
`unable to parse date "${dateStr}" ` +
`using format "${dateFormat}": ${message}`,
};
}

return date;
}
6 changes: 5 additions & 1 deletion finances/documents.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import { rogersBill } from "./documents/rogers.ts";
import { publicMobileStatement } from "./documents/public_mobile.ts";
import { kitchenerUtilitiesBill } from "./documents/kitchener_utilities.ts";
import { morganStanleyRelease } from "./documents/morgan_stanley.ts";
import {
questradeMarginStatement,
questradeRESPStatement,
questradeRRSPStatement,
} from "./documents/questrade.ts";

export const allDocuments = Object.freeze([
kitchenerUtilitiesBill,
morganStanleyRelease,
publicMobileStatement,
rogersBill,
questradeMarginStatement,
questradeRESPStatement,
questradeRRSPStatement,
rogersBill,
]);

export type Documents = Exclude<(typeof allDocuments)[number], undefined>;
56 changes: 56 additions & 0 deletions finances/documents/kitchener_utilities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import {
isDocumentParseError,
type Document,
type DocumentParseError,
} from "../document.ts";
import { stringFromLines, yyyymmddDateFromLines } from "../document_utils.ts";

export interface ParsedKitchenerUtilitiesBill {
type: "KitchenerUtilitiesBill";
statementDate: string;
amountDue: string;
}

class KitchenerUtilitiesBill implements Document<
ParsedKitchenerUtilitiesBill,
"KitchenerUtilitiesBill"
> {
readonly type = "KitchenerUtilitiesBill" as const;

identify(lines: readonly string[]): boolean {
return lines.some((line) => line.includes("UTILITIES@KITCHENER.CA"));
Comment thread
denversc marked this conversation as resolved.
}

calculateFileName(pdf: Readonly<ParsedKitchenerUtilitiesBill>): string {
const { statementDate, amountDue } = pdf;
return `${statementDate} Kitchener Utilities Bill ${amountDue}.pdf`;
}

parse(
lines: readonly string[],
): ParsedKitchenerUtilitiesBill | DocumentParseError {
const statementDate = yyyymmddDateFromLines(
lines,
/^Statement Date:\s*(\w+\s+\d+\s+\d+)\s+/i,
"MMM D YYYY",
);
Comment thread
denversc marked this conversation as resolved.
if (isDocumentParseError(statementDate)) {
return statementDate;
}
Comment thread
denversc marked this conversation as resolved.

const amountDue = stringFromLines(
lines,
/^Pre-authorized Withdrawal:\s*(\d+\.\d+)$/i,
{ resultPrefix: "$" },
);
Comment thread
denversc marked this conversation as resolved.
Comment thread
denversc marked this conversation as resolved.
if (isDocumentParseError(amountDue)) {
return amountDue;
}
Comment thread
denversc marked this conversation as resolved.

return { type: "KitchenerUtilitiesBill", statementDate, amountDue };
}
}

export const kitchenerUtilitiesBill: Readonly<
Document<ParsedKitchenerUtilitiesBill, "KitchenerUtilitiesBill">
> = Object.freeze(new KitchenerUtilitiesBill());
108 changes: 108 additions & 0 deletions finances/documents/morgan_stanley.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import {
isDocumentParseError,
type Document,
type DocumentParseError,
} from "../document.ts";
import { stringFromLines, yyyymmddDateFromLines } from "../document_utils.ts";

export interface ParsedMorganStanleyRelease {
type: "MorganStanleyRelease";
awardId: string;
settlementDate: string;
vestedValue: string;
saleAmount: string;
sharesSold: string;
salePrice: string;
}

class MorganStanleyRelease implements Document<
ParsedMorganStanleyRelease,
"MorganStanleyRelease"
> {
readonly type = "MorganStanleyRelease" as const;

identify(lines: readonly string[]): boolean {
return lines.some((line) => line.toLowerCase() === "release confirmation");
Comment thread
denversc marked this conversation as resolved.
}

calculateFileName(pdf: Readonly<ParsedMorganStanleyRelease>): string {
const {
awardId,
settlementDate,
vestedValue,
saleAmount,
sharesSold,
salePrice,
} = pdf;
return (
`${settlementDate} Morgan Stanley Release Confirmation ` +
`${awardId} ${sharesSold} shares vested for ` +
`${vestedValue} sold for ${saleAmount} ` +
`(${salePrice} per share).pdf`
);
}

parse(
lines: readonly string[],
): ParsedMorganStanleyRelease | DocumentParseError {
const awardId = stringFromLines(lines, /^Award ID:\s+([\w\d]+)$/i);
Comment thread
denversc marked this conversation as resolved.
if (isDocumentParseError(awardId)) {
return awardId;
}
Comment thread
denversc marked this conversation as resolved.

const settlementDate = yyyymmddDateFromLines(
lines,
/^Settlement Date:\s*(\d+-\w+-\d+)$/i,
"DD-MMM-YYYY",
);
Comment thread
denversc marked this conversation as resolved.
if (isDocumentParseError(settlementDate)) {
return settlementDate;
}
Comment thread
denversc marked this conversation as resolved.

const vestedValue = stringFromLines(
lines,
/^Total Gain.*:\s*(\$[\d,.]+)$/i,
);
Comment thread
denversc marked this conversation as resolved.
if (isDocumentParseError(vestedValue)) {
return vestedValue;
}
Comment thread
denversc marked this conversation as resolved.

const saleAmount = stringFromLines(
lines,
/^Sale PricexQuantity Sold:\s*\((\$[\d,.]+)\)$/i,
);
Comment thread
denversc marked this conversation as resolved.
Comment thread
denversc marked this conversation as resolved.
if (isDocumentParseError(saleAmount)) {
return saleAmount;
}
Comment thread
denversc marked this conversation as resolved.

const sharesSold = stringFromLines(
lines,
/^Quantity Sold:\s*\((\d+\.\d{3})0*\)$/i,
);
Comment thread
denversc marked this conversation as resolved.
Comment thread
denversc marked this conversation as resolved.
if (isDocumentParseError(sharesSold)) {
return sharesSold;
}
Comment thread
denversc marked this conversation as resolved.

const salePrice = stringFromLines(
lines,
/shares at (\$\d+\.\d{4})0* per share/i,
);
Comment thread
denversc marked this conversation as resolved.
Comment thread
denversc marked this conversation as resolved.
if (isDocumentParseError(salePrice)) {
return salePrice;
}
Comment thread
denversc marked this conversation as resolved.

return {
type: "MorganStanleyRelease",
awardId,
settlementDate,
vestedValue,
saleAmount,
sharesSold,
salePrice,
};
}
}

export const morganStanleyRelease: Readonly<
Document<ParsedMorganStanleyRelease, "MorganStanleyRelease">
> = Object.freeze(new MorganStanleyRelease());
Loading
Loading