Skip to content

paystub.ts added#9

Merged
denversc merged 7 commits into
mainfrom
paystub
Jun 15, 2026
Merged

paystub.ts added#9
denversc merged 7 commits into
mainfrom
paystub

Conversation

@denversc

Copy link
Copy Markdown
Owner

No description provided.

@denversc denversc merged commit 7076654 into main Jun 15, 2026
3 checks passed
@denversc denversc deleted the paystub branch June 15, 2026 02:39

@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 introduces a new command-line utility in finances/paystub.ts for parsing, classifying, and renaming Google pay stub PDFs. Key feedback on this implementation includes correcting a logic bug in classifyText where auxiliary benefits are checked before primary pay types, removing the unused MONTH_MAP constant and its associated ESLint suppression comment, defensively handling missing GSU amounts to prevent 'undefined' in filenames, and avoiding redundant file renaming operations when the filename is already correct.

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/paystub.ts
Comment on lines +39 to +87
function classifyText(text: string): Classification {
// Split on "Deductions" to only process the Earnings section
const parts = text.split(/\r?\nDeductions/);
const earningsSection = parts[0] || text;

const mealMatches = earningsSection.matchAll(
/Meal Bene[fi\s]+t\s+(?:([\d.]+)\s+\$([\d.]+)\s+)?\$([\d,.]+)\s+\$([\d,.]+)/gi,
);
for (const match of mealMatches) {
if (match[3]) {
const currentVal = parseFloat(match[3].replace(/,/g, ""));
if (currentVal > 0) {
return "meal";
}
}
}

const shuttleMatch = earningsSection.match(
/CA Shuttle Bus\s+(?:([\d.]+)\s+\$([\d.]+)\s+)?\$([\d,.]+)\s+\$([\d,.]+)/i,
);
if (shuttleMatch && shuttleMatch[3]) {
const currentVal = parseFloat(shuttleMatch[3].replace(/,/g, ""));
if (currentVal > 0) {
return "shuttle";
}
}

const gsuMatch = earningsSection.match(
/Google Stock Un\s+([\d,.]+)\s+\$([\d,.]+)\s+\$([\d,.]+)\s+\$([\d,.]+)/i,
);
if (gsuMatch && gsuMatch[3]) {
const currentVal = parseFloat(gsuMatch[3].replace(/,/g, ""));
if (currentVal > 0) {
return "gsu";
}
}

const regPayMatch = earningsSection.match(
/Regular Pay\s+([\d,.]+)\s+\$([\d,.]+)\s+\$([\d,.]+)\s+\$([\d,.]+)/i,
);
if (regPayMatch && regPayMatch[3]) {
const currentVal = parseFloat(regPayMatch[3].replace(/,/g, ""));
if (currentVal > 0) {
return "regpay";
}
}

throw new Error("Unable to determine pay stub classification");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

In classifyText, the checks are ordered such that auxiliary benefits like Meal Benefit and CA Shuttle Bus are evaluated before primary classifications like Regular Pay and Google Stock Un (GSU). Since a regular pay stub or GSU vest pay stub can also contain meal or shuttle benefits, checking the auxiliary benefits first will cause regular pay stubs to be misclassified as 'meal' or 'shuttle'.

Reorder the checks so that primary classifications (regpay and gsu) are evaluated first.

function classifyText(text: string): Classification {
  // Split on "Deductions" to only process the Earnings section
  const parts = text.split(/\r?\nDeductions/);
  const earningsSection = parts[0] || text;

  const regPayMatch = earningsSection.match(
    /Regular Pay\s+([\d,.]+)\s+\$([\d,.]+)\s+\$([\d,.]+)\s+\$([\d,.]+)/i,
  );
  if (regPayMatch && regPayMatch[3]) {
    const currentVal = parseFloat(regPayMatch[3].replace(/,/g, ""));
    if (currentVal > 0) {
      return "regpay";
    }
  }

  const gsuMatch = earningsSection.match(
    /Google Stock Un\s+([\d,.]+)\s+\$([\d,.]+)\s+\$([\d,.]+)\s+\$([\d,.]+)/i,
  );
  if (gsuMatch && gsuMatch[3]) {
    const currentVal = parseFloat(gsuMatch[3].replace(/,/g, ""));
    if (currentVal > 0) {
      return "gsu";
    }
  }

  const shuttleMatch = earningsSection.match(
    /CA Shuttle Bus\s+(?:([\d.]+)\s+\$([\d.]+)\s+)?\$([\d,.]+)\s+\$([\d,.]+)/i,
  );
  if (shuttleMatch && shuttleMatch[3]) {
    const currentVal = parseFloat(shuttleMatch[3].replace(/,/g, ""));
    if (currentVal > 0) {
      return "shuttle";
    }
  }

  const mealMatches = earningsSection.matchAll(
    /Meal Bene[fi\s]+t\s+(?:([\d.]+)\s+\$([\d.]+)\s+)?\$([\d,.]+)\s+\$([\d,.]+)/gi,
  );
  for (const match of mealMatches) {
    if (match[3]) {
      const currentVal = parseFloat(match[3].replace(/,/g, ""));
      if (currentVal > 0) {
        return "meal";
      }
    }
  }

  throw new Error("Unable to determine pay stub classification");
}

Comment thread finances/paystub.ts
Comment on lines +1 to +2
/* eslint-disable @typescript-eslint/no-unused-vars */
import * as fs from "fs";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The eslint-disable comment is used to suppress unused variable warnings, but the only unused variable in this file is MONTH_MAP. It is better to remove the unused code and this suppression comment to keep the codebase clean and maintainable.

Suggested change
/* eslint-disable @typescript-eslint/no-unused-vars */
import * as fs from "fs";
import * as fs from "fs";

Comment thread finances/paystub.ts
Comment on lines +22 to +35
const MONTH_MAP: Record<string, string> = {
jan: "01",
feb: "02",
mar: "03",
apr: "04",
may: "05",
jun: "06",
jul: "07",
aug: "08",
sep: "09",
oct: "10",
nov: "11",
dec: "12",
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The MONTH_MAP constant is defined but never used in this file. It should be removed to clean up the code.

Comment thread finances/paystub.ts
Comment on lines +137 to +148
let gsuAmount: string | undefined;
if (classification === "gsu") {
// Split on "Deductions" to only process the Earnings section
const parts = result.text.split(/\r?\nDeductions/);
const earningsSection = parts[0] || result.text;
const gsuMatch = earningsSection.match(
/Google Stock Un\s+([\d,.]+)\s+\$([\d,.]+)\s+\$([\d,.]+)\s+\$([\d,.]+)/i,
);
if (gsuMatch && gsuMatch[3]) {
gsuAmount = `$${gsuMatch[3]}`;
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

If the classification is 'gsu', but the gsuMatch fails to extract the amount (e.g., due to unexpected PDF formatting), gsuAmount will remain undefined. This will result in a filename containing 'undefined' (e.g., ... GSU Vest undefined CAD). We should throw an error or handle this defensively if gsuAmount cannot be parsed for a GSU classification.

  let gsuAmount: string | undefined;
  if (classification === "gsu") {
    // Split on "Deductions" to only process the Earnings section
    const parts = result.text.split(/\r?\nDeductions/);
    const earningsSection = parts[0] || result.text;
    const gsuMatch = earningsSection.match(
      /Google Stock Un\s+([\d,.]+)\s+\$([\d,.]+)\s+\$([\d,.]+)\s+\$([\d,.]+)/i,
    );
    if (gsuMatch && gsuMatch[3]) {
      gsuAmount = `$${gsuMatch[3]}`;
    } else {
      throw new Error("Failed to parse GSU amount for GSU pay stub");
    }
  }

Comment thread finances/paystub.ts
Comment on lines +285 to +292
try {
console.log(`Renaming ${item.filePath} to ${newFilename}`);
fs.renameSync(item.filePath, newPath);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error(`Error: ${message}`);
process.exit(1);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

If the file is already named correctly, item.filePath and newPath will be identical. Calling fs.renameSync in this case is unnecessary and can cause redundant log output or potential OS-level errors. We should check if the paths are different before renaming.

    try {
      if (item.filePath !== newPath) {
        console.log(`Renaming ${item.filePath} to ${newFilename}`);
        fs.renameSync(item.filePath, newPath);
      } else {
        console.log(`File ${item.filePath} is already correctly named`);
      }
    } catch (error) {
      const message = error instanceof Error ? error.message : String(error);
      console.error(`Error: ${message}`);
      process.exit(1);
    }

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