Implement multi-batch invoice generation

- Add read_csv_file_by_batch() to group transactions by batch_number
- Modify generate command to create separate directories per batch
- Each batch directory contains index.html and customer_XXX.html files
- Add 3 unit tests for batch grouping logic
- Fixes issue #1: CSV with multiple batches now generates separate invoices

Closes: #1
This commit is contained in:
2026-04-02 12:34:14 +02:00
parent 1e9af16325
commit e9cf2e031f
2 changed files with 240 additions and 57 deletions
+67 -56
View File
@@ -8,7 +8,7 @@ use chrono::{NaiveDateTime, Utc};
use config::{Config, Env};
use csv::ReaderBuilder;
use db::{create_pool, Repository};
use invoice_generator::{group_by_customer, read_csv_file, Customer};
use invoice_generator::{group_by_customer, read_csv_file_by_batch, Customer};
use std::collections::HashMap;
use std::env;
use std::fs;
@@ -330,72 +330,83 @@ async fn main() -> anyhow::Result<()> {
let temp_cleaned_path =
base_output_dir.join(format!("{}.temp.csv", filename.trim_end_matches(".txt")));
let batch_number = clean_csv_file(input_path, &temp_cleaned_path)?;
clean_csv_file(input_path, &temp_cleaned_path)?;
let output_dir = base_output_dir.join(&batch_number);
fs::create_dir_all(&output_dir)?;
fs::copy(input_path, output_dir.join(format!("{}.txt", batch_number)))?;
fs::rename(
&temp_cleaned_path,
output_dir.join(format!("{}.csv", batch_number)),
)?;
println!(
"Converted {} transactions",
fs::read_to_string(output_dir.join(format!("{}.csv", batch_number)))?
.lines()
.count()
- 1
);
let batch = read_csv_file(&output_dir.join(format!("{}.csv", batch_number)))?;
println!("Loaded {} transactions", batch.transactions.len());
let first_date = batch.transactions.first().map(|t| t.date).unwrap();
let last_date = batch.transactions.last().map(|t| t.date).unwrap();
let period = format!(
"{} - {}",
first_date.format("%Y-%m-%d"),
last_date.format("%Y-%m-%d")
);
let customers = group_by_customer(&[batch]);
let index_customers: Vec<(String, usize)> = customers
.iter()
.map(|(num, c)| (num.clone(), c.cards.len()))
.collect();
let html = IndexTemplate {
customers: index_customers.clone(),
period: period.clone(),
}
.render()
.unwrap();
fs::write(output_dir.join("index.html"), html)?;
let mut batches = read_csv_file_by_batch(&temp_cleaned_path)?;
println!("Found {} batches in CSV", batches.len());
let mut total_customers = 0usize;
let generated_date = Utc::now().format("%Y-%m-%d %H:%M").to_string();
let customer_count = customers.len();
for (customer_num, customer) in customers {
let prepared = PreparedCustomer::from_customer(customer);
let customer_html = CustomerTemplate {
customer: prepared,
batch_number: batch_number.clone(),
let mut batch_numbers: Vec<_> = batches.keys().cloned().collect();
batch_numbers.sort();
for batch_number in batch_numbers {
let batch = batches.remove(&batch_number).unwrap();
let output_dir = base_output_dir.join(&batch_number);
fs::create_dir_all(&output_dir)?;
let csv_path = output_dir.join(format!("{}.csv", batch_number));
let txt_path = output_dir.join(format!("{}.txt", batch_number));
fs::copy(&temp_cleaned_path, &csv_path)?;
fs::copy(input_path, &txt_path)?;
println!(
"Batch {}: {} transactions",
batch_number,
batch.transactions.len()
);
let first_date = batch.transactions.first().map(|t| t.date).unwrap();
let last_date = batch.transactions.last().map(|t| t.date).unwrap();
let period = format!(
"{} - {}",
first_date.format("%Y-%m-%d"),
last_date.format("%Y-%m-%d")
);
let customers = group_by_customer(&[batch]);
let index_customers: Vec<(String, usize)> = customers
.iter()
.map(|(num, c)| (num.clone(), c.cards.len()))
.collect();
let html = IndexTemplate {
customers: index_customers.clone(),
period: period.clone(),
generated_date: generated_date.clone(),
}
.render()
.unwrap();
let filename = format!("customer_{}.html", customer_num);
fs::write(output_dir.join(&filename), customer_html)?;
println!("Generated {}", filename);
fs::write(output_dir.join("index.html"), html)?;
for (customer_num, customer) in customers {
let prepared = PreparedCustomer::from_customer(customer);
let customer_html = CustomerTemplate {
customer: prepared,
batch_number: batch_number.clone(),
period: period.clone(),
generated_date: generated_date.clone(),
}
.render()
.unwrap();
let customer_filename = format!("customer_{}.html", customer_num);
fs::write(output_dir.join(&customer_filename), customer_html)?;
println!(" Generated {}", customer_filename);
}
total_customers += index_customers.len();
}
fs::remove_file(temp_cleaned_path)?;
println!(
"\nGenerated {} customer invoices in {:?}",
customer_count, output_dir
"\nGenerated {} customer invoices across {} batches in {:?}",
total_customers,
batches.len(),
base_output_dir
);
}
"db" => {