use chrono::NaiveDateTime; use csv::ReaderBuilder; use std::collections::BTreeMap; use std::fs; use std::path::Path; #[derive(Debug, Clone)] pub struct Transaction { pub date: NaiveDateTime, pub batch_number: String, pub amount: f64, pub volume: f64, pub price: f64, pub quality: i32, pub quality_name: String, pub card_number: String, pub card_type: String, pub customer_number: String, pub station: String, pub terminal: String, pub pump: String, pub receipt: String, pub card_report_group_number: String, pub control_number: String, } #[derive(Debug, Clone)] pub struct Batch { pub filename: String, pub transactions: Vec, } #[derive(Debug, Clone)] pub struct Customer { pub customer_number: String, pub cards: BTreeMap>, } fn get_field(record: &csv::StringRecord, index: usize) -> &str { record.get(index).unwrap_or("") } impl Transaction { pub fn from_record(record: &csv::StringRecord) -> Option { let date_str = get_field(record, 0); let date = NaiveDateTime::parse_from_str(date_str, "%m/%d/%Y %I:%M:%S %p") .or_else(|_| NaiveDateTime::parse_from_str(date_str, "%Y-%m-%d %H:%M:%S")) .ok()?; Some(Transaction { date, batch_number: get_field(record, 1).to_string(), amount: get_field(record, 2).parse().unwrap_or(0.0), volume: get_field(record, 3).parse().unwrap_or(0.0), price: get_field(record, 4).parse().unwrap_or(0.0), quality: get_field(record, 5).parse().unwrap_or(0), quality_name: get_field(record, 6).to_string(), card_number: get_field(record, 7).to_string(), card_type: get_field(record, 8).to_string(), customer_number: get_field(record, 9).to_string(), station: get_field(record, 10).to_string(), terminal: get_field(record, 11).to_string(), pump: get_field(record, 12).to_string(), receipt: get_field(record, 13).to_string(), card_report_group_number: get_field(record, 14).to_string(), control_number: get_field(record, 15).to_string(), }) } } pub fn read_csv_file(path: &Path) -> Result> { let filename = path .file_name() .and_then(|n| n.to_str()) .unwrap_or("unknown") .to_string(); let file = fs::File::open(path)?; let mut rdr = ReaderBuilder::new() .delimiter(b'\t') .has_headers(true) .flexible(true) .from_reader(file); let mut transactions = Vec::new(); for result in rdr.records() { let record = result?; if let Some(tx) = Transaction::from_record(&record) { if tx.amount > 0.0 && !tx.customer_number.is_empty() { transactions.push(tx); } } } transactions.sort_by(|a, b| a.date.cmp(&b.date)); Ok(Batch { filename, transactions, }) } pub fn group_by_customer(batches: &[Batch]) -> BTreeMap { let mut customers: BTreeMap = BTreeMap::new(); for batch in batches { for tx in &batch.transactions { let customer = customers .entry(tx.customer_number.clone()) .or_insert_with(|| Customer { customer_number: tx.customer_number.clone(), cards: BTreeMap::new(), }); let card_txs = customer.cards.entry(tx.card_number.clone()).or_default(); card_txs.push(tx.clone()); } } for customer in customers.values_mut() { for card_txs in customer.cards.values_mut() { card_txs.sort_by(|a, b| a.date.cmp(&b.date)); } } customers }