8 Комити a7d5a2b99b ... 0e4bb842eb

Аутор SHA1 Порука Датум
  Slava Barinov 0e4bb842eb [Telegram] Tried to make item names unique пре 1 година
  Slava Barinov 9d57e0a8e4 [Convert] read_file test added пре 1 година
  Slava Barinov 914284ebe7 [Import] Unit tests added пре 1 година
  Slava Barinov fc5be23e0f [User] Log error instead of panic on save пре 1 година
  Slava Barinov b081d34f61 [Build] Set RUST_BACKTRACE to 1 for Github Actions пре 1 година
  Slava Barinov 620e0a040c [Telegram] clippy warnings fixed пре 1 година
  Slava Barinov 7489ee0029 [Receipt] clippy warning fixed пре 1 година
  Slava Barinov a56ce1e6d8 [Telegram] Temporary comment out the format_categories пре 1 година
6 измењених фајлова са 138 додато и 93 уклоњено
  1. 2 0
      .github/workflows/rust.yml
  2. 10 0
      src/convert.rs
  3. 35 0
      src/import.rs
  4. 1 1
      src/receipt.rs
  5. 87 91
      src/telegram.rs
  6. 3 1
      src/user.rs

+ 2 - 0
.github/workflows/rust.yml

@@ -52,6 +52,8 @@ jobs:
         with:
         with:
           command: test
           command: test
           args: --release --features monitoring
           args: --release --features monitoring
+        env:
+          RUST_BACKTRACE: 1
 
 
       - name: Collect test coverage
       - name: Collect test coverage
         uses: actions-rs/tarpaulin@v0.1
         uses: actions-rs/tarpaulin@v0.1

+ 10 - 0
src/convert.rs

@@ -127,4 +127,14 @@ mod tests {
         assert_eq!(result[0].name, "СИДР 0.5 MAGNERS APP");
         assert_eq!(result[0].name, "СИДР 0.5 MAGNERS APP");
         assert_eq!(result[0].sum, 17713);
         assert_eq!(result[0].sum, 17713);
     }
     }
+
+    #[test]
+    fn test_read_file() {
+        let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
+        p.push("tests/resources/test.json");
+        let full_path = p.to_string_lossy();
+
+        let result = read_file(&full_path);
+        assert!(!result.items.is_empty());
+    }
 }
 }

+ 35 - 0
src/import.rs

@@ -15,3 +15,38 @@ pub fn read_accounts(path: &Path) -> Result<Vec<String>, Box<dyn Error>> {
     }
     }
     Ok(result)
     Ok(result)
 }
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use std::fs::{remove_file, File};
+    use std::io::Write;
+    use std::path::PathBuf;
+
+    fn create_test_file() -> PathBuf {
+        let file_path = PathBuf::from("test_accounts.csv");
+        let mut file = File::create(&file_path).unwrap();
+
+        writeln!(file, "Type,Name").unwrap();
+        writeln!(file, "EXPENSE,Coffee").unwrap();
+        writeln!(file, "INCOME,Salary").unwrap();
+        writeln!(file, "EXPENSE,Books").unwrap();
+        writeln!(file, "ASSET,Bank").unwrap();
+
+        file_path
+    }
+
+    #[test]
+    fn test_read_accounts() {
+        let file_path = create_test_file();
+        let accounts = read_accounts(&file_path).unwrap();
+        assert_eq!(accounts, vec!["Coffee", "Books"]);
+        remove_file(file_path).unwrap();
+    }
+
+    #[test]
+    fn test_read_accounts_error() {
+        let path = Path::new("non_existing_file.csv");
+        assert!(read_accounts(path).is_err());
+    }
+}

+ 1 - 1
src/receipt.rs

@@ -54,7 +54,7 @@ struct Ticket {
 
 
 mod custom_date_format {
 mod custom_date_format {
     use chrono::{DateTime, NaiveDateTime, Utc};
     use chrono::{DateTime, NaiveDateTime, Utc};
-    use serde::{self, Deserialize, Deserializer};
+    use serde::{Deserialize, Deserializer};
 
 
     /// The format seems alike to RFC3339 but is not compliant
     /// The format seems alike to RFC3339 but is not compliant
     const FORMAT: &str = "%Y-%m-%dT%H:%M:%S";
     const FORMAT: &str = "%Y-%m-%dT%H:%M:%S";

+ 87 - 91
src/telegram.rs

@@ -13,11 +13,13 @@ use derive_more::From;
 use std::fmt::Debug;
 use std::fmt::Debug;
 use std::str::FromStr;
 use std::str::FromStr;
 use std::sync::Arc;
 use std::sync::Arc;
-use teloxide::types::*;
-use teloxide::types::{InlineKeyboardButton, InlineKeyboardMarkup};
+use teloxide::types::{
+    InlineKeyboardButton, InlineKeyboardButtonKind, InlineKeyboardMarkup, InputFile, MediaKind,
+    MessageKind, ReplyMarkup,
+};
 use teloxide::{
 use teloxide::{
     dispatching::dialogue::InMemStorage, net::Download, prelude::*, types::File as TgFile,
     dispatching::dialogue::InMemStorage, net::Download, prelude::*, types::File as TgFile,
-    utils::command::BotCommands, Bot, DownloadError, RequestError,
+    utils::command::BotCommands, DownloadError, RequestError,
 };
 };
 use thiserror::Error;
 use thiserror::Error;
 use tokio::fs::File;
 use tokio::fs::File;
@@ -292,6 +294,7 @@ async fn handle_idle(bot: Bot, dialogue: QIFDialogue, msg: Message) -> HandlerRe
     Ok(())
     Ok(())
 }
 }
 
 
+/*
 /// List all the items' categories when the all the ticket is processed
 /// List all the items' categories when the all the ticket is processed
 fn format_categories(catitems: &HashMap<String, String>) -> String {
 fn format_categories(catitems: &HashMap<String, String>) -> String {
     catitems
     catitems
@@ -311,24 +314,36 @@ fn format_categories(catitems: &HashMap<String, String>) -> String {
             acc
             acc
         })
         })
 }
 }
+ */
 
 
 fn create_categories_keyboard(catitems: &HashMap<String, String>) -> InlineKeyboardMarkup {
 fn create_categories_keyboard(catitems: &HashMap<String, String>) -> InlineKeyboardMarkup {
     let mut keyboard = InlineKeyboardMarkup::default(); // Use default to initialize
     let mut keyboard = InlineKeyboardMarkup::default(); // Use default to initialize
-    let mut buttons = Vec::new();
 
 
-    for (item, category) in catitems.iter() {
-        let button_text = format!("{}: {}", item, category);
-        let callback_data = format!("edit_{}", item); // Assuming `item` is a unique identifier
+    for (index, (item, category)) in catitems.iter().enumerate() {
+        let parts: Vec<&str> = category.split(':').collect();
+        let shortened_category = if parts.len() > 1 {
+            parts[..parts.len() - 1]
+                .iter()
+                .map(|&part| {
+                    part.chars()
+                        .next()
+                        .unwrap_or_default()
+                        .to_uppercase()
+                        .collect::<String>()
+                })
+                .chain(std::iter::once(parts.last().unwrap().to_string()))
+                .collect::<Vec<String>>()
+                .join(":")
+        } else {
+            category.clone()
+        };
 
 
-        // Create a button and push it to the row
-        let button = InlineKeyboardButton::callback(button_text, callback_data);
-        buttons.push(button); // Add button to the current row
+        let button_text = format!("{}: {}", item, shortened_category);
+        let callback_data = format!("edit_{}_{}", item, index);
 
 
-        // Assuming you want each button in its own row for simplicity
-        keyboard = keyboard.append_row(buttons.clone());
-        buttons.clear(); // Clear the vector for the next iteration
+        let button = InlineKeyboardButton::callback(button_text, callback_data);
+        keyboard = keyboard.append_row(vec![button]);
     }
     }
-
     keyboard
     keyboard
 }
 }
 
 
@@ -688,89 +703,70 @@ async fn handle_qif_ready(
 }
 }
 
 
 async fn callback_handler(q: CallbackQuery, bot: Bot, dialogue: QIFDialogue) -> HandlerResult {
 async fn callback_handler(q: CallbackQuery, bot: Bot, dialogue: QIFDialogue) -> HandlerResult {
-    if let Some(version) = q.data {
-        if version.starts_with("edit_") {
-            let item_id = version.strip_prefix("edit_").unwrap(); // Extract the item ID or number
-
-            dialogue
-                .update(State::NewJson {
-                    filename: String::new(),
-                })
-                .await?;
-
-            // Process the selection, e.g., by updating the dialogue state or responding to the user
-            let response_message = format!("Editing item {}", item_id);
-            if let Some(chat_id) = q.message.clone().map(|msg| msg.chat.id) {
-                bot.send_message(chat_id, response_message).await?;
+    if let Some(data) = q.data {
+        if data.starts_with("edit_") {
+            let data = data.strip_prefix("edit_").unwrap();
+            let parts: Vec<&str> = data.split('_').collect();
+
+            if parts.len() == 2 {
+                let item = parts[0].to_string(); // Convert item to String
+                let index = parts[1]; // Index is parsed but not used further in this snippet
+
+                dialogue
+                    .update(State::NewJson {
+                        filename: String::new(),
+                    })
+                    .await?;
+
+                let response_message = format!("Editing item {}: {}", item, index);
+                if let Some(chat_id) = q.message.clone().map(|msg| msg.chat.id) {
+                    bot.send_message(chat_id, response_message).await?;
+                }
             }
             }
         }
         }
 
 
-        let text = format!("You chose: {}", version);
-
-        match q.message {
-            Some(Message { id, chat, .. }) => {
-                bot.edit_message_text(chat.id, id, text.clone()).await?;
-                let state = dialogue.get().await?;
-                if let Some(data) = state {
-                    log::info!("Data: {}", data);
-                    if let State::SubCategorySelect {
-                        filename,
-                        item,
-                        category: _,
-                        mut items_left,
-                        mut items_processed,
-                    } = data
-                    {
-                        log::info!("SubCategory match!");
-                        bot.send_message(
-                            chat.id,
-                            format!("Item {} is ready for caterogy {}", item, version),
-                        )
+        let text = format!("You chose: {}", data);
+        if let Some(Message { id, chat, .. }) = q.message {
+            bot.edit_message_text(chat.id, id, text.clone()).await?;
+            let state = dialogue.get().await?;
+            if let Some(State::SubCategorySelect {
+                filename,
+                item,
+                category: _,
+                mut items_left,
+                mut items_processed,
+            }) = state
+            {
+                items_processed.insert(item, data.to_string()); // Ensure the correct type is inserted
+                if let Some(newitem) = items_left.pop() {
+                    bot.send_message(chat.id, format!("Input category to search for {}", newitem))
+                        .await?;
+                    dialogue
+                        .update(State::CategorySelect {
+                            filename,
+                            item: newitem,
+                            items_left,
+                            items_processed,
+                        })
+                        .await?;
+                } else {
+                    bot.send_message(chat.id, "This was the last item!").await?;
+                    bot.send_message(chat.id, "Items are categorized and categories are updated")
+                        .await?;
+                    // Assuming create_categories_keyboard is defined elsewhere and correctly processes items_processed
+                    bot.send_message(chat.id, "Enter the memo line").await?;
+                    dialogue
+                        .update(State::Ready {
+                            filename,
+                            item_categories: items_processed,
+                        })
                         .await?;
                         .await?;
-                        items_processed.insert(item, version);
-                        if let Some(newitem) = items_left.pop() {
-                            bot.send_message(
-                                chat.id,
-                                format!("Input category to search for {}", newitem),
-                            )
-                            .await?;
-                            dialogue
-                                .update(State::CategorySelect {
-                                    filename,
-                                    item: newitem,
-                                    items_left,
-                                    items_processed,
-                                })
-                                .await?;
-                        } else {
-                            bot.send_message(chat.id, "This was the last item!".to_string())
-                                .await?;
-                            bot.send_message(
-                                chat.id,
-                                "Items are categorized and categories are updated".to_string(),
-                            )
-                            .reply_markup(create_categories_keyboard(&items_processed))
-                            .await?;
-
-                            bot.send_message(chat.id, "Enter the memo line".to_string())
-                                .await?;
-                            dialogue
-                                .update(State::Ready {
-                                    filename,
-                                    item_categories: items_processed,
-                                })
-                                .await?;
-                        }
-                    } else {
-                        log::info!("No SubCategory match!");
-                    }
-                }
-            }
-            None => {
-                if let Some(id) = q.inline_message_id {
-                    bot.edit_message_text_inline(id, text).await?;
                 }
                 }
+            } else {
+                log::info!("No SubCategory match!");
             }
             }
+        } else if let Some(id) = q.inline_message_id {
+            bot.edit_message_text_inline(id, text).await?;
         }
         }
     }
     }
 
 

+ 3 - 1
src/user.rs

@@ -41,7 +41,9 @@ pub enum UserError {
 
 
 impl Drop for User {
 impl Drop for User {
     fn drop(&mut self) {
     fn drop(&mut self) {
-        self.save_data().unwrap();
+        self.save_data().unwrap_or_else(|err| {
+            log::error!("Can't save database for uid {} due to {:?}", self.uid, err)
+        });
     }
     }
 }
 }