Skip to content

Commit

Permalink
feat: add validate-csv command for validating a claims CSV before
Browse files Browse the repository at this point in the history
submission

Fixes #7.
  • Loading branch information
timrogers committed Aug 1, 2023
1 parent f83dbad commit 938e022
Show file tree
Hide file tree
Showing 4 changed files with 143 additions and 4 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,9 @@ You can submit multiple claims at once by generating a template CSV, filling it
1. Make sure you're logged in - for more details, see "Connecting to your Forma account" above.
2. Run `formanator generate-template-csv` to generate a CSV template. By default, the template will be saved as `claims.csv`. Optionally, you can specify the `--output-path` argument to choose where to save the template.
3. Update the template, filling in the columns for each of your claims. To get valid `benefit` and `category` values, use the `formanator benefits` and `formanator categories --benefit <benefit>` commands documented in "Submitting a single claim, specifying the benefit and category yourself" above.
4. Submit your claims by running `formanator submit-claims-from-csv --input-path claims.csv`.
5. Your claims will be submitted. If there are any validation errors with any of the rows, or if anything goes wrong during submission, an error message will be displayed, but the tool will continue submitting other claims.
4. Validate the CSV up-front by running `formanator validate-csv --input-path claims.csv`.
5. Submit your claims by running `formanator submit-claims-from-csv --input-path claims.csv`.
6. Your claims will be submitted. If there are any validation errors with any of the rows, or if anything goes wrong during submission, an error message will be displayed, but the tool will continue submitting other claims.

## Contributing

Expand Down
45 changes: 44 additions & 1 deletion src/claims.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { existsSync } from 'fs';
import { createReadStream, existsSync } from 'fs';

import { type CreateClaimOptions, getCategoriesForBenefitName } from './forma.js';
import { parse } from '@fast-csv/parse';

export interface Claim {
category: string;
Expand Down Expand Up @@ -58,3 +59,45 @@ export const claimParamsToCreateClaimOptions = async (
subcategoryValue: matchingCategory.subcategory_value,
};
};

const EXPECTED_CSV_HEADERS = [
'category',
'benefit',
'amount',
'merchant',
'purchaseDate',
'description',
'receiptPath',
];

export const readClaimsFromCsv = async (inputPath: string): Promise<Claim[]> => {
const claims: Claim[] = [];

return await new Promise((resolve, reject) => {
createReadStream(inputPath, 'utf8')
.pipe(parse({ headers: true }))
.on('error', reject)
.on('data', (row) => {
const rowHeaders = Object.keys(row);

if (
rowHeaders.length !== EXPECTED_CSV_HEADERS.length ||
!rowHeaders.every((header) => EXPECTED_CSV_HEADERS.includes(header))
) {
reject(
new Error(
'Invalid CSV headers. Please use a template CSV generated by the `generate-template-csv` command.',
),
);
}

const receiptPath = row.receiptPath.split(',').map((path) => path.trim());
const claim: Claim = { ...row, receiptPath };

claims.push(claim);
})
.on('end', () => {
resolve(claims);
});
});
};
93 changes: 93 additions & 0 deletions src/commands/validate-csv.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import * as commander from 'commander';
import { existsSync } from 'fs';
import chalk from 'chalk';

import { actionRunner, serializeError } from '../utils.js';
import { getBenefitsWithCategories } from '../forma.js';
import { claimParamsToCreateClaimOptions, readClaimsFromCsv } from '../claims.js';
import VERSION from '../version.js';
import { getAccessToken } from '../config.js';

const command = new commander.Command();

interface Arguments {
accessToken?: string;
inputPath: string;
}

command
.name('validate-csv')
.version(VERSION)
.description(
'Validate a completed CSV before submitting it with `submit-claims-from-csv`',
)
.requiredOption('--input-path <input_path>', 'The path to the CSV to read claims from')
.action(
actionRunner(async (opts: Arguments) => {
const accessToken = opts.accessToken ?? getAccessToken();

if (!accessToken) {
throw new Error(
"You aren't logged in to Forma. Please run `formanator login` first.",
);
}

if (!existsSync(opts.inputPath)) {
throw new Error(`File '${opts.inputPath}' doesn't exist.`);
}

const claims = await readClaimsFromCsv(opts.inputPath);

if (!claims.length) {
throw new Error(
"Your CSV doesn't seem to contain any claims. Have you filled out the template?",
);
}

const benefitsWithCategories = await getBenefitsWithCategories(accessToken);

for (const [index, claim] of claims.entries()) {
const rowNumber = index + 2;
console.log(`Validating claim ${index + 1}/${claims.length} (row ${rowNumber})`);

try {
if (claim.benefit !== '' && claim.category !== '') {
await claimParamsToCreateClaimOptions(claim, accessToken);
} else {
// Fill in the category with any value, just to skip that part of the validation
const benefit = benefitsWithCategories[0].name;
const category = benefitsWithCategories[0].categories[0].subcategory_name;

await claimParamsToCreateClaimOptions(
{ ...claim, benefit, category },
accessToken,
);

console.log(
chalk.yellow(
`Claim ${index + 1}/${
claims.length
} (row ${rowNumber}) doesn't have a benefit and/or category. This will have to be inferred using OpenAI when the claims are submitted`,
),
);
}

console.log(
chalk.green(
`Validated claim ${index + 1}/${claims.length} (row ${rowNumber})`,
),
);
} catch (e) {
console.error(
chalk.red(
`Error submitting claim ${index + 1}/${claims.length}: ${serializeError(
e,
)} (row ${rowNumber})`,
),
);
}
}
}),
);

export default command;
4 changes: 3 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import categories from './commands/categories.js';
import submitClaim from './commands/submit-claim.js';
import generateTemplateCsv from './commands/generate-template-csv.js';
import submitClaimsFromCsv from './commands/submit-claims-from-csv.js';
import validateCsv from './commands/validate-csv.js';
import VERSION from './version.js';

const program = new commander.Command();
Expand All @@ -19,6 +20,7 @@ program
.addCommand(categories)
.addCommand(submitClaim)
.addCommand(generateTemplateCsv)
.addCommand(submitClaimsFromCsv);
.addCommand(submitClaimsFromCsv)
.addCommand(validateCsv);

program.parse();

0 comments on commit 938e022

Please sign in to comment.