formanator 3.1.0

Submit Forma <https://joinforma.com> benefit claims from the command line, with support for AI-powered receipt analysis via OpenAI or GitHub Models
Documentation
use anyhow::{Result, bail};
use colored::Colorize;

use crate::claims::{ClaimInput, claim_input_to_create_options, read_claims_from_csv};
use crate::cli::ValidateCsvArgs;
use crate::config::resolve_access_token;
use crate::forma::get_benefits_with_categories;
use crate::verbose;

pub fn run(args: ValidateCsvArgs) -> Result<()> {
    verbose::set(args.verbose);
    let access_token = resolve_access_token(args.access_token.as_deref())?;

    if !args.input_path.exists() {
        bail!("File '{}' doesn't exist.", args.input_path.display());
    }

    let claims = read_claims_from_csv(&args.input_path)?;
    if claims.is_empty() {
        bail!("Your CSV doesn't seem to contain any claims. Have you filled out the template?");
    }

    let benefits = get_benefits_with_categories(&access_token)?;
    if benefits.is_empty() {
        bail!("Your account does not have any benefits, so claims cannot be validated.");
    }

    let total = claims.len();
    for (index, claim) in claims.into_iter().enumerate() {
        let row_number = index + 2;
        println!(
            "Validating claim {}/{} (row {row_number})",
            index + 1,
            total
        );

        let result = (|| -> Result<()> {
            let benefit_category_empty = claim.benefit.is_empty() && claim.category.is_empty();
            let other_details_empty = claim.amount.is_empty()
                && claim.merchant.is_empty()
                && claim.purchase_date.is_empty()
                && claim.description.is_empty();

            if !claim.benefit.is_empty() && !claim.category.is_empty() {
                claim_input_to_create_options(&claim, &access_token).map(|_| ())
            } else if benefit_category_empty && other_details_empty {
                if claim.receipt_path.is_empty() {
                    bail!(
                        "Row leaves every column blank except `receiptPath`, but `receiptPath` is also empty. Provide at least one receipt path so claim details can be inferred at submission time."
                    );
                }
                for path in &claim.receipt_path {
                    if !path.exists() {
                        bail!("Receipt path '{}' does not exist.", path.display());
                    }
                }
                println!(
                    "{}",
                    format!(
                        "Claim {}/{} (row {row_number}) leaves every column except `receiptPath` blank. All claim details will be inferred from the receipt at submission time.",
                        index + 1,
                        total
                    )
                    .yellow()
                );
                Ok(())
            } else {
                let placeholder = benefits
                    .iter()
                    .find(|b| !b.categories.is_empty())
                    .ok_or_else(|| anyhow::anyhow!(
                        "Your account does not have any benefits with categories, so claims with a missing benefit/category cannot be validated."
                    ))?;
                let placeholder_benefit = &placeholder.benefit.name;
                let placeholder_category = &placeholder.categories[0].subcategory_name;
                let test_claim = ClaimInput {
                    benefit: placeholder_benefit.clone(),
                    category: placeholder_category.clone(),
                    ..claim.clone()
                };
                claim_input_to_create_options(&test_claim, &access_token).map(|_| ())?;
                println!(
                    "{}",
                    format!(
                        "Claim {}/{} (row {row_number}) doesn't have a benefit and/or category. This will have to be inferred when the claims are submitted.",
                        index + 1,
                        total
                    )
                    .yellow()
                );
                Ok(())
            }
        })();

        match result {
            Ok(()) => println!(
                "{}",
                format!("Validated claim {}/{} (row {row_number})", index + 1, total).green()
            ),
            Err(e) => eprintln!(
                "{}",
                format!(
                    "Error validating claim {}/{}: {e} (row {row_number})",
                    index + 1,
                    total
                )
                .red()
            ),
        }
    }

    Ok(())
}