@@ -114,6 +114,22 @@ impl ColumnMapping {
114114 }
115115 }
116116
117+ /// TD Bank CSV format (no header, date/description/debit/credit/balance)
118+ pub fn td_bank ( ) -> Self {
119+ Self {
120+ date_column : 0 ,
121+ amount_column : None ,
122+ outflow_column : Some ( 2 ) ,
123+ inflow_column : Some ( 3 ) ,
124+ payee_column : Some ( 1 ) ,
125+ memo_column : None ,
126+ date_format : "%Y-%m-%d" . to_string ( ) ,
127+ has_header : false ,
128+ delimiter : ',' ,
129+ invert_amounts : false ,
130+ }
131+ }
132+
117133 /// Set the date format
118134 pub fn with_date_format ( mut self , format : & str ) -> Self {
119135 self . date_format = format. to_string ( ) ;
@@ -346,8 +362,43 @@ impl<'a> ImportService<'a> {
346362 Err ( format ! ( "Could not parse date: '{}'" , s) )
347363 }
348364
365+ /// Check if a record looks like data (not headers)
366+ /// Returns true if first column parses as a date
367+ fn looks_like_data_row ( & self , record : & StringRecord ) -> bool {
368+ if let Some ( first) = record. get ( 0 ) {
369+ let first = first. trim ( ) ;
370+ // Try to parse as a date - if it succeeds, this is data not a header
371+ let date_formats = [
372+ "%Y-%m-%d" , "%m/%d/%Y" , "%m/%d/%y" , "%d/%m/%Y" , "%d/%m/%y" ,
373+ ] ;
374+ for format in date_formats {
375+ if NaiveDate :: parse_from_str ( first, format) . is_ok ( ) {
376+ return true ;
377+ }
378+ }
379+ }
380+ false
381+ }
382+
349383 /// Detect column mapping from CSV header record
350384 pub fn detect_mapping_from_headers ( & self , headers : & StringRecord ) -> ColumnMapping {
385+ // First, check if this looks like a data row (no headers)
386+ if self . looks_like_data_row ( headers) {
387+ // This is likely a headerless CSV like TD Bank
388+ // Check if it matches TD Bank format: date, desc, debit, credit, balance
389+ if headers. len ( ) >= 4 {
390+ // Verify column 2 or 3 looks like a number (debit/credit)
391+ let col2 = headers. get ( 2 ) . map ( |s| s. trim ( ) ) . unwrap_or ( "" ) ;
392+ let col3 = headers. get ( 3 ) . map ( |s| s. trim ( ) ) . unwrap_or ( "" ) ;
393+ let col2_is_num = col2. is_empty ( ) || col2. parse :: < f64 > ( ) . is_ok ( ) ;
394+ let col3_is_num = col3. is_empty ( ) || col3. parse :: < f64 > ( ) . is_ok ( ) ;
395+
396+ if col2_is_num && col3_is_num {
397+ return ColumnMapping :: td_bank ( ) ;
398+ }
399+ }
400+ }
401+
351402 let mut mapping = ColumnMapping :: new ( ) ;
352403
353404 for ( idx, header) in headers. iter ( ) . enumerate ( ) {
0 commit comments