diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml
index 50dd8bcaf..1f9552b9e 100644
--- a/.github/workflows/python-app.yml
+++ b/.github/workflows/python-app.yml
@@ -5,7 +5,7 @@ name: Python application
on:
push:
- branches: [ "main" ]
+ branches: [ "feature/summary-report-sociallink" ]
pull_request:
branches: [ "main" ]
@@ -14,36 +14,41 @@ permissions:
jobs:
build:
-
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
+
- name: Set up Python 3.10
uses: actions/setup-python@v3
with:
python-version: "3.10"
+
- name: Install dependencies
run: |
python -m pip install --upgrade pip
- pip install flake8 pytest
+ pip install flake8 pytest coverage pandas # Added pandas directly here
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
+
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
+
- name: Install pylint
run: |
pip install pylint
+
- name: Run pylint
run: |
pylint -r y code/
+
- name: Test with pytest
run: |
- pip install coverage
coverage run -m pytest test/
+
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v3
with:
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 000000000..13566b81b
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,8 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/DollarBot.iml b/.idea/DollarBot.iml
new file mode 100644
index 000000000..6e7c09c28
--- /dev/null
+++ b/.idea/DollarBot.iml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 000000000..3205808ec
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 000000000..0ed127fcd
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 000000000..35eb1ddfb
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/categories.json b/categories.json
index 664d4fd53..cecc7c930 100644
--- a/categories.json
+++ b/categories.json
@@ -1 +1,3 @@
-{ "categories" : "Food,Groceries,Utilities,Transport,Shopping,Miscellaneous" }
\ No newline at end of file
+{
+ "categories": "Food,Groceries,Utilities,Transport,Shopping,Miscellaneous,miscelleneous"
+}
\ No newline at end of file
diff --git a/code/__init__.py b/code/__init__.py
index 22a984205..9d245d1a8 100644
--- a/code/__init__.py
+++ b/code/__init__.py
@@ -24,7 +24,18 @@
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
+
import os
import sys
+from your_telegram_bot_module import bot # Replace with your actual bot import
+
+def main():
+ try:
+ bot.polling(non_stop=True)
+ except Exception as e:
+ print(f"An error occurred: {e}")
-sys.path.insert(0, os.getcwd() + "/code")
+if __name__ == "__main__":
+ # Optionally, you could manage your project structure here
+ sys.path.insert(0, os.path.join(os.getcwd(), "code"))
+ main()
diff --git a/code/add.py b/code/add.py
index 4dc1114c0..9b1a675ae 100644
--- a/code/add.py
+++ b/code/add.py
@@ -24,12 +24,13 @@
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
-
import helper
import logging
from telebot import types
from telegram_bot_calendar import DetailedTelegramCalendar, LSTEP
+from helper import display_remaining_budget
from datetime import datetime
+from exception import InvalidAmountError, InvalidCategoryError
option = {}
@@ -50,24 +51,24 @@ def run(message, bot):
calendar, step = DetailedTelegramCalendar().build()
bot.send_message(chat_id, f"Select {LSTEP[step]}", reply_markup=calendar)
- @bot.callback_query_handler(func=DetailedTelegramCalendar.func())
- def cal(c):
- chat_id = c.message.chat.id
- result, key, step = DetailedTelegramCalendar().process(c.data)
-
- if not result and key:
- bot.edit_message_text(
- f"Select {LSTEP[step]}",
- chat_id,
- c.message.message_id,
- reply_markup=key,
- )
- elif result:
- data = datetime.today().date()
- if (result > data):
- bot.send_message(chat_id,"Cannot select future dates, Please try /add command again with correct dates")
- else:
- category_selection(message,bot,result)
+
+def cal(c,bot):
+ chat_id = c.message.chat.id
+ result, key, step = DetailedTelegramCalendar().process(c.data)
+
+ if not result and key:
+ bot.edit_message_text(
+ f"Select {LSTEP[step]}",
+ chat_id,
+ c.message.message_id,
+ reply_markup=key,
+ )
+ elif result:
+ data = datetime.today().date()
+ if (result > data):
+ bot.send_message(chat_id,"Cannot select future dates, Please try /add command again with correct dates")
+ else:
+ category_selection(c.message,bot,result)
def category_selection(msg,bot,date):
"""
@@ -113,9 +114,7 @@ def post_category_selection(message, bot, date):
bot.send_message(
chat_id, "Invalid", reply_markup=types.ReplyKeyboardRemove()
)
- raise Exception(
- 'Sorry, I don\'t recognise this category "{}"!'.format(selected_category)
- )
+ raise InvalidCategoryError(selected_category, "I don’t recognize this category")
option[chat_id] = selected_category
message = bot.send_message(
chat_id, "How much did you spend on {}? \n(Numeric values only)".format(str(option[chat_id])),)
@@ -147,7 +146,7 @@ def post_amount_input(message, bot, selected_category, date):
amount_entered = message.text
amount_value = helper.validate_entered_amount(amount_entered) # validate
if amount_value == 0: # cannot be $0 spending
- raise Exception("Spent amount has to be a non-zero number.")
+ raise InvalidAmountError("Spent amount has to be a non-zero number.")
date_of_entry = date.strftime(helper.getDateFormat())
date_str, category_str, amount_str = (
@@ -166,7 +165,8 @@ def post_amount_input(message, bot, selected_category, date):
amount_str, category_str, date_str
),
)
- helper.display_remaining_budget(message, bot)
+ helper.display_remaining_budget(message, bot, selected_category) # Use 'selected_category' instead of 'cat'
+
except Exception as e:
logging.exception(str(e))
bot.send_message(chat_id, "Oh no. " + str(e))
@@ -178,6 +178,12 @@ def add_user_record(chat_id, record_to_be_added):
is the expense record to be added to the store. It then stores this expense record in the store.
"""
user_list = helper.read_json()
+ print(f"user_list before addition: {user_list}") # Debug output
+
+ # Ensure user_list is initialized properly
+ if user_list is None:
+ user_list = {} # Initialize as empty dictionary if read_json returned None
+
if str(chat_id) not in user_list:
user_list[str(chat_id)] = helper.createNewUserRecord()
diff --git a/code/add_recurring.py b/code/add_recurring.py
index 6be5fd090..43279dd7a 100644
--- a/code/add_recurring.py
+++ b/code/add_recurring.py
@@ -30,7 +30,7 @@
from telebot import types
from datetime import datetime
from dateutil.relativedelta import relativedelta
-
+from exception import InvalidCategoryError, InvalidAmountError, InvalidDurationError
option = {}
@@ -73,7 +73,7 @@ def post_category_selection(message, bot):
selected_category = message.text
if selected_category not in helper.getSpendCategories():
bot.send_message(chat_id, 'Invalid', reply_markup=types.ReplyKeyboardRemove())
- raise Exception("Sorry I don't recognise this category \"{}\"!".format(selected_category))
+ raise InvalidCategoryError(selected_category, "I don’t recognize this category")
option[chat_id] = selected_category
message = bot.send_message(chat_id, 'How much did you spend on {}? \n(Enter numeric values only)'.format(str(option[chat_id])))
@@ -107,7 +107,7 @@ def post_amount_input(message, bot, selected_category):
amount_entered = message.text
amount_value = helper.validate_entered_amount(amount_entered) # validate
if amount_value == 0: # cannot be $0 spending
- raise Exception("Spent amount has to be a non-zero number.")
+ raise InvalidAmountError("Spent amount has to be a non-zero number.")
message = bot.send_message(chat_id, 'For how many months in the future will the expense be there? \n(Enter integer values only)')
bot.register_next_step_handler(message, post_duration_input, bot, selected_category, amount_value)
@@ -135,7 +135,7 @@ def post_duration_input(message, bot, selected_category, amount_value):
duration_entered = message.text
duration_value = helper.validate_entered_duration(duration_entered)
if duration_value == 0:
- raise Exception("Duration has to be a non-zero integer.")
+ raise InvalidDurationError("Duration has to be a non-zero integer.")
for i in range(int(duration_value)):
date_of_entry = (datetime.today().date() + relativedelta(months=+i)).strftime(helper.getDateFormat())
diff --git a/code/analytics.py b/code/analytics.py
index 83d9668c6..1eba5b536 100644
--- a/code/analytics.py
+++ b/code/analytics.py
@@ -29,6 +29,7 @@
import logging
from telebot import types
import get_analysis
+from exception import InvalidOperationError
def run(message, bot):
"""
@@ -45,30 +46,51 @@ def run(message, bot):
markup.add(c)
msg = bot.reply_to(message, "Select the type of analysis (grouped by category):", reply_markup=markup)
bot.register_next_step_handler(msg, post_operation_selection, bot)
-
+
def post_operation_selection(message, bot):
"""
- post_operation_selection(message, bot): It takes 2 arguments for processing - message which
- is the message from the user, and bot which is the telegram bot object from the
- run(message, bot): function in the analytics.py file. Depending on the action chosen by the user,
- it passes on control to the corresponding functions which are all located in different files.
+ post_operation_selection(message, bot): Processes user-selected operations and
+ invokes corresponding analysis functions.
+
+ Args:
+ message: The message object containing user input.
+ bot: The Telegram bot object for sending messages.
"""
+ chat_id = message.chat.id
+ op = message.text
+ options = helper.getAnalyticsOptions()
+
try:
- chat_id = message.chat.id
- op = message.text
- options = helper.getAnalyticsOptions()
- if op not in options.values():
- bot.send_message(
- chat_id, "Invalid", reply_markup=types.ReplyKeyboardRemove()
- )
- raise Exception('Sorry I don\'t recognise this operation "{}"!'.format(op))
- if op == options["overall"]:
- get_analysis.viewOverallBudget(chat_id, bot)
- elif op == options["spend"]:
- get_analysis.viewSpendWise(chat_id, bot)
- elif op == options["remaining"]:
- get_analysis.viewRemaining(chat_id, bot)
- elif op == options["history"]:
- get_analysis.viewHistory(chat_id, bot)
+ # Validate the operation
+ validate_operation(op, options)
+
+ # Mapping operations to functions
+ operation_mapping = {
+ options["overall"]: get_analysis.viewOverallBudget,
+ options["spend"]: get_analysis.viewSpendWise,
+ options["remaining"]: get_analysis.viewRemaining,
+ options["history"]: get_analysis.viewHistory,
+ }
+
+ # Execute the corresponding function
+ operation_mapping[op](chat_id, bot)
+
+ except InvalidOperationError as e:
+ bot.send_message(chat_id, f"Invalid operation selected: '{e.operation}'. Please choose a valid option.")
except Exception as e:
- helper.throw_exception(e, message, bot, logging)
\ No newline at end of file
+ helper.throw_exception(e, message, bot, logging)
+
+
+def validate_operation(op, options):
+ """
+ Validates whether the provided operation is in the available options.
+
+ Args:
+ op: The operation to validate.
+ options: A dictionary of valid operations.
+
+ Raises:
+ InvalidOperationError: If the operation is not valid.
+ """
+ if op not in options.values():
+ raise InvalidOperationError(op)
diff --git a/code/budget.py b/code/budget.py
index 3e3caa455..89bd422d8 100644
--- a/code/budget.py
+++ b/code/budget.py
@@ -31,6 +31,7 @@
import budget_delete
import logging
from telebot import types
+from exception import InvalidOperationError
# === Documentation of budget.py ===
@@ -65,7 +66,7 @@ def post_operation_selection(message, bot):
bot.send_message(
chat_id, "Invalid", reply_markup=types.ReplyKeyboardRemove()
)
- raise Exception('Sorry I don\'t recognise this operation "{}"!'.format(op))
+ raise InvalidOperationError(op, "Sorry, I don’t recognize this operation")
if op == options["update"]:
budget_update.run(message, bot)
elif op == options["view"]:
diff --git a/code/budget_update.py b/code/budget_update.py
index 93e073a5e..b4179ed99 100644
--- a/code/budget_update.py
+++ b/code/budget_update.py
@@ -29,6 +29,7 @@
import logging
import budget_view
from telebot import types
+from exception import InvalidOperationError, InvalidAmountError, BudgetError, InvalidCategoryError
# === Documentation of budget_update.py ===
@@ -62,7 +63,7 @@ def post_type_selection(message, bot):
bot.send_message(
chat_id, "Invalid", reply_markup=types.ReplyKeyboardRemove()
)
- raise Exception('Sorry I don\'t recognise this operation "{}"!'.format(op))
+ raise InvalidOperationError(op, "Sorry, I don’t recognize this operation")
if op == options["overall"]:
update_overall_budget(chat_id, bot)
elif op == options["category"]:
@@ -103,7 +104,7 @@ def post_overall_amount_input(message, bot):
chat_id = message.chat.id
amount_value = helper.validate_entered_amount(message.text)
if amount_value == 0:
- raise Exception("Invalid amount.")
+ raise InvalidAmountError("Spent amount has to be a non-zero number.")
user_list = helper.read_json()
if str(chat_id) not in user_list:
user_list[str(chat_id)] = helper.createNewUserRecord()
@@ -113,7 +114,7 @@ def post_overall_amount_input(message, bot):
for c in helper.getCategoryBudget(chat_id).values():
total_budget += float(c)
if total_budget > float(amount_value):
- raise Exception("Overall budget cannot be less than " + str(total_budget))
+ raise BudgetError("Overall budget cannot be less than " + str(total_budget))
uncategorized_budget = helper.get_uncategorized_amount(chat_id, amount_value)
if float(uncategorized_budget) > 0:
if user_list[str(chat_id)]["budget"]["category"] is None:
@@ -168,9 +169,8 @@ def post_category_selection(message, bot):
bot.send_message(
chat_id, "Invalid", reply_markup=types.ReplyKeyboardRemove()
)
- raise Exception(
- 'Sorry I don\'t recognise this category "{}"!'.format(selected_category)
- )
+ raise InvalidCategoryError(selected_category, "I don’t recognize this category")
+
if helper.isCategoryBudgetByCategoryAvailable(chat_id, selected_category):
currentBudget = helper.getCategoryBudgetByCategory(
chat_id, selected_category
@@ -223,7 +223,7 @@ def post_category_amount_input(message, bot, category):
chat_id = message.chat.id
amount_value = helper.validate_entered_amount(message.text)
if amount_value == 0:
- raise Exception("Invalid amount.")
+ raise InvalidAmountError("Invalid amount.")
user_list = helper.read_json()
if str(chat_id) not in user_list:
user_list[str(chat_id)] = helper.createNewUserRecord()
diff --git a/code/budget_view.py b/code/budget_view.py
index c9ab08bd7..136ab6c9f 100644
--- a/code/budget_view.py
+++ b/code/budget_view.py
@@ -1,87 +1,79 @@
-"""
-File: budget_view.py
-Author: Vyshnavi Adusumelli, Tejaswini Panati, Harshavardhan Bandaru
-Date: October 01, 2023
-Description: File contains Telegram bot message handlers and their associated functions.
-
-Copyright (c) 2023
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
-"""
-
import graphing
import helper
import logging
import os
+from exception import BudgetNotFoundError
# === Documentation of budget_view.py ===
def run(message, bot):
"""
- run(message, bot): This is the main function used to implement the budget feature.
- It takes 2 arguments for processing - message which is the message from the user, and bot which
- is the telegram bot object from the main code.py function. Depending on whether the user has configured
- an overall budget or a category-wise budget, this functions checks for either case using the helper
- module's isOverallBudgetAvailable and isCategoryBudgetAvailable functions and passes control on the
- respective functions(listed below). If there is no budget configured an exception is raised and the user
- is given a message indicating that there is no budget configured.
+ Main function for displaying the budget to the user, handling both overall and category-specific budgets.
+
+ Parameters:
+ - message: The message from the user in Telegram.
+ - bot: The Telegram bot object handling communication.
+
+ If a budget is configured (either overall or category-specific), displays the budget information.
+ Otherwise, raises a BudgetNotFoundError and informs the user about setting up a budget.
"""
try:
- print("here")
chat_id = message.chat.id
if helper.isOverallBudgetAvailable(chat_id) or helper.isCategoryBudgetAvailable(chat_id):
- display_overall_budget(message, bot)
- display_category_budget(message, bot)
+ bot.send_message(chat_id, "Retrieving your budget details...")
+ if helper.isOverallBudgetAvailable(chat_id):
+ display_overall_budget(chat_id, bot)
+ if helper.isCategoryBudgetAvailable(chat_id):
+ display_category_budget(chat_id, bot)
else:
- raise Exception(
- "Budget does not exist. Use " + helper.getBudgetOptions()["update"] + " option to add/update the budget"
- )
+ raise BudgetNotFoundError("No budget configured. Use the /budget command to add or update your budget.")
+ except BudgetNotFoundError as bnf_err:
+ logging.warning(f"No budget found for chat_id {chat_id}: {bnf_err}")
+ bot.send_message(chat_id, str(bnf_err))
except Exception as e:
+ logging.error(f"Error in displaying budget for chat_id {chat_id}: {e}")
helper.throw_exception(e, message, bot, logging)
-def display_overall_budget(message, bot):
+def display_overall_budget(chat_id, bot):
"""
- display_overall_budget(message, bot): It takes 2 arguments for processing -
- message which is the message from the user, and bot which is the telegram bot
- object from the run(message, bot): in the same file. It gets the budget for the
- user based on their chat ID using the helper module and returns the same through the bot to the Telegram UI.
+ Retrieves and displays the overall budget for the user based on their chat ID.
+
+ Parameters:
+ - chat_id: Unique ID of the user's chat.
+ - bot: The Telegram bot object handling communication.
"""
- chat_id = message.chat.id
- data = helper.getOverallBudget(chat_id)
- bot.send_message(chat_id, "Overall Budget: $" + data)
+ try:
+ data = helper.getOverallBudget(chat_id)
+ bot.send_message(chat_id, f"💰 Overall Budget: ${data}")
+ except Exception as e:
+ logging.error(f"Error in retrieving overall budget for chat_id {chat_id}: {e}")
+ bot.send_message(chat_id, "Sorry, we encountered an error retrieving your overall budget.")
-def display_category_budget(message, bot):
+def display_category_budget(chat_id, bot):
"""
- display_category_budget(message, bot): It takes 2 arguments for processing -
- message which is the message from the user, and bot which is the telegram bot object
- from the run(message, bot): in the same file. It gets the category-wise budget for the
- user based on their chat ID using the helper module.It then processes it into a string
- format suitable for display, and returns the same through the bot to the Telegram UI.
+ Retrieves and displays the category-specific budget for the user based on their chat ID.
+ Generates a graph if available and sends it to the user.
+
+ Parameters:
+ - chat_id: Unique ID of the user's chat.
+ - bot: The Telegram bot object handling communication.
"""
- chat_id = message.chat.id
- if helper.isCategoryBudgetAvailable(chat_id):
+ try:
data = helper.getCategoryBudget(chat_id)
- print(data,"data")
- if graphing.viewBudget(data):
- bot.send_photo(chat_id, photo=open("budget.png", "rb"))
- os.remove("budget.png")
+ if data:
+ bot.send_message(chat_id, "📊 Here’s your category-wise budget:")
+ table_text = "\n".join([f"- {category}: ${amount}" for category, amount in data.items()])
+ bot.send_message(chat_id, table_text)
+
+ # Generate graph and send if successful
+ if graphing.viewBudget(data):
+ with open("budget.png", "rb") as photo:
+ bot.send_photo(chat_id, photo)
+ os.remove("budget.png")
+ else:
+ bot.send_message(chat_id, "Unable to generate a visual representation of your budget.")
else:
- bot.send_message(chat_id, "You are yet to set your budget for different categories.")
- else:
- bot.send_message(chat_id, "You are yet to set your budget for different categories.")
\ No newline at end of file
+ bot.send_message(chat_id, "It looks like your category budgets haven't been set up yet.")
+ except Exception as e:
+ logging.error(f"Error in retrieving category budget for chat_id {chat_id}: {e}")
+ bot.send_message(chat_id, "Sorry, we encountered an error retrieving your category budgets.")
diff --git a/code/code.py b/code/code.py
index 13b84473c..4668f9d2b 100644
--- a/code/code.py
+++ b/code/code.py
@@ -46,10 +46,15 @@
import weekly
import monthly
import sendEmail
+import voice
import add_recurring
+import os
+from pdf import create_summary_pdf
from datetime import datetime
from jproperties import Properties
-
+from telebot import types
+from telegram_bot_calendar import DetailedTelegramCalendar
+from add import cal
configs = Properties()
@@ -85,13 +90,15 @@ def listener(user_requests):
)
message = (
- ("Sorry, I can't understand messages yet :/\n"
- "I can only understand commands that start with /. \n\n"
- "Type /faq or /help if you are stuck.")
+ ("I'm here to help, but I can only respond to specific commands for now.\n\n"
+ "To get started, try typing a command that begins with '/'.\n"
+ "If you're unsure, type /faq or /help to see a list of available commands.\n\n"
+ "Thanks for understanding! 😊")
)
try:
helper.read_json()
+ global user_list
chat_id = user_requests[0].chat.id
if user_requests[0].text[0] != "/":
@@ -102,11 +109,11 @@ def listener(user_requests):
bot.set_update_listener(listener)
@bot.message_handler(commands=["help"])
-def show_help(m):
+def help(m):
helper.read_json()
+ global user_list
chat_id = m.chat.id
-
message = "Here are the commands you can use: \n"
commands = helper.getCommands()
for c in commands:
@@ -114,10 +121,12 @@ def show_help(m):
message += "\nUse /menu for detailed instructions about these commands."
bot.send_message(chat_id, message)
+
@bot.message_handler(commands=["faq"])
def faq(m):
helper.read_json()
+ global user_list
chat_id = m.chat.id
faq_message = (
@@ -137,21 +146,18 @@ def faq(m):
# defines how the /start and /help commands have to be handled/processed
@bot.message_handler(commands=["start", "menu"])
def start_and_menu_command(m):
- """
- start_and_menu_command(m): Prints out the the main menu displaying the features that the
- bot offers and the corresponding commands to be run from the Telegram UI to use these features.
- Commands used to run this: commands=['start', 'menu']
- """
helper.read_json()
+ global user_list
chat_id = m.chat.id
-
text_intro = (
- ("Welcome to the Dollar Bot! \n"
- "DollarBot can track all your expenses with simple and easy to use commands :) \n"
- "Here is the complete menu. \n\n")
+ "*Welcome to the Dollar Bot!* \n"
+ "DollarBot can track all your expenses with simple and easy-to-use commands :) \n"
+ "Here is the complete menu:\n\n"
)
commands = helper.getCommands()
+ keyboard = types.InlineKeyboardMarkup()
+
for c in commands:
# generate help text out of the commands dictionary defined at the top
text_intro += "/" + c + ": "
@@ -159,6 +165,77 @@ def start_and_menu_command(m):
bot.send_message(chat_id, text_intro)
return True
+# code.py
+
+@bot.callback_query_handler(func=lambda call: True)
+def callback_query(call):
+ """
+ Handles button clicks and executes the corresponding command actions.
+ """
+ response_text = "" # Initialize an empty response text
+
+ # Check which command was clicked and perform the corresponding action
+ if call.data == "summary":
+ response_text = "Here is your summary report."
+ elif call.data == "report":
+ response_text = "Here is your detailed report."
+ elif call.data == "socialmedia":
+ response_text = "Share your summary on social media!"
+ else:
+ response_text = "Unknown command received."
+
+ # Additional command handling (if needed)
+ if call.data == "help":
+ response_text = help(call.message)
+ elif call.data == "pdf":
+ response_text = command_pdf(call.message)
+ elif call.data == "add":
+ response_text = command_add(call.message)
+ elif call.data == "menu":
+ response_text = start_and_menu_command(call.message)
+ elif call.data == "add_recurring":
+ response_text = command_add_recurring(call.message)
+ elif call.data == "analytics":
+ response_text = command_analytics(call.message)
+ elif call.data == "predict":
+ response_text = command_predict(call.message)
+ elif call.data == "history":
+ response_text = command_history(call.message)
+ elif call.data == "delete":
+ response_text = command_delete(call.message)
+ elif call.data == "display":
+ response_text = command_display(call.message)
+ elif call.data == "edit":
+ response_text = command_edit(call.message)
+ elif call.data == "budget":
+ response_text = command_budget(call.message)
+ elif call.data == "updateCategory":
+ response_text = command_updateCategory(call.message)
+ elif call.data == "weekly":
+ response_text = command_weekly(call.message)
+ elif call.data == "monthly":
+ response_text = command_monthly(call.message)
+ elif call.data == "sendEmail":
+ response_text = command_sendEmail(call.message)
+ elif call.data == "faq":
+ response_text = faq(call.message)
+ elif DetailedTelegramCalendar.func()(call): # If it’s a calendar action
+ cal(call, bot)
+ response_text = "Calendar action processed."
+ else:
+ response_text = "Command not recognized."
+
+ # Acknowledge the button press
+ bot.answer_callback_query(call.id)
+
+ # Send the response back to the user
+ if response_text:
+ bot.send_message(call.message.chat.id, response_text, parse_mode='Markdown')
+ else:
+ bot.send_message(call.message.chat.id, "An error occurred. Please try again.", parse_mode='Markdown')
+
+
+
# defines how the /add command has to be handled/processed
@bot.message_handler(commands=["add"])
def command_add(message):
@@ -179,6 +256,15 @@ def command_weekly(message):
"""
weekly.run(message, bot)
+@bot.message_handler(content_types=['voice'])
+def handle_voice(message):
+ """
+ handle_voice(message) Takes 1 argument message which contains the message from
+ the user along with the chat ID of the user chat. It then calls voice.py to run to execute
+ voice recognition functionality. Voice invkes this command
+ """
+ voice.run(message, bot)
+
# defines how the /monthly command has to be handled/processed
@bot.message_handler(commands=["monthly"])
def command_monthly(message):
@@ -297,6 +383,107 @@ def command_predict(message):
"""
predict.run(message, bot)
+# handles /summary command
+@bot.message_handler(commands=["summary"])
+def command_summary(message):
+ """
+ command_summary(message): Takes the message with the user's chat ID and
+ calls the helper function to generate the summary.
+ """
+ helper.generate_summary(message.chat.id, bot)
+
+# handles /report command
+@bot.message_handler(commands=["report"])
+def command_report(message):
+ """
+ Handles the /report command, requesting a date range for the report.
+ """
+ chat_id = message.chat.id
+ bot.send_message(chat_id, "Please enter the date range for the report (format: YYYY-MM-DD to YYYY-MM-DD).")
+
+ @bot.message_handler(func=lambda msg: validate_date_range(msg.text))
+ def handle_date_range(msg):
+ date_range = msg.text.split("to")
+ start_date, end_date = date_range[0].strip(), date_range[1].strip()
+ helper.generate_report(chat_id, bot, start_date, end_date)
+
+def validate_date_range(text):
+ # Implement a proper date validation logic
+ return "-" in text and "to" in text
+
+
+ # Listen for the next message containing the date range
+ @bot.message_handler(func=lambda msg: "-" in msg.text and "to" in msg.text)
+ def handle_date_range(msg):
+ date_range = msg.text.split("to")
+ if len(date_range) == 2:
+ start_date = date_range[0].strip()
+ end_date = date_range[1].strip()
+ # Generate the report and send it
+ helper.generate_report(chat_id, bot, start_date, end_date)
+ else:
+ bot.send_message(chat_id, "Invalid format. Please try again using 'YYYY-MM-DD to YYYY-MM-DD'.")
+
+import urllib.parse
+
+@bot.message_handler(commands=["socialmedia"])
+def command_socialmedia(message):
+ """
+ command_socialmedia(message): Generates a shareable link for the user's expense summary that can
+ be posted on social media platforms.
+ """
+ chat_id = message.chat.id
+
+ # Generate or fetch the link to the user's expense summary
+ summary_link = generate_shareable_link(chat_id)
+
+ if summary_link:
+ # URL encode the summary link
+ encoded_link = urllib.parse.quote(summary_link)
+
+ # Message with options for social media platforms
+ response_message = (
+ "Your shareable link to your expense summary has been generated successfully! 🎉\n"
+ f"{summary_link}\n\n"
+ "Share this link on your favorite social media platforms:\n"
+ f"1. Facebook: [Share on Facebook](https://www.facebook.com/sharer/sharer.php?u={encoded_link})\n"
+ f"2. Twitter: [Share on Twitter](https://twitter.com/share?url={encoded_link}&text=Check%20out%20my%20expense%20summary!)\n"
+ f"3. LinkedIn: [Share on LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url={encoded_link})"
+ )
+ bot.send_message(chat_id, response_message, parse_mode="Markdown")
+ else:
+ bot.send_message(chat_id, "Oops! We couldn't generate a shareable link for you. Please try again later.")
+
+
+def generate_shareable_link(chat_id):
+ """
+ Generates a shareable link for the user's expense summary.
+ This function creates a PDF summary of the user's expenses, uploads it to a cloud storage service,
+ and returns a shareable link.
+ """
+ try:
+ # Assuming `pdf.create_summary_pdf(chat_id)` exists in pdf.py and generates the PDF path
+ file_path = pdf.create_summary_pdf(chat_id)
+
+ # For demonstration purposes, simulate creating a shareable link
+ # In production, use an upload service, like Google Drive or Dropbox, to get a public link
+ shareable_link = f"https://example.com/shared_files/{os.path.basename(file_path)}"
+
+ # Log or print to check link
+ print("Generated shareable link:", shareable_link)
+
+ return shareable_link
+ except Exception as e:
+ logging.exception("Error generating shareable link: " + str(e))
+ return None
+
+def addUserHistory(chat_id, user_record):
+ global user_list
+ if not (str(chat_id) in user_list):
+ user_list[str(chat_id)] = []
+ user_list[str(chat_id)].append(user_record)
+ return user_list
+
def main():
"""
main() The entire bot's execution begins here. It ensure the bot variable begins
@@ -310,4 +497,4 @@ def main():
print("Connection Timeout")
if __name__ == "__main__":
- main()
\ No newline at end of file
+ main() # type: ignore
\ No newline at end of file
diff --git a/code/display.py b/code/display.py
index 535249b4e..083d25af9 100644
--- a/code/display.py
+++ b/code/display.py
@@ -32,6 +32,7 @@
import logging
from telebot import types
from datetime import datetime
+from exception import DisplayOptionError, NoHistoryError
# === Documentation of display.py ===
@@ -73,14 +74,19 @@ def display_total(message, bot):
chat_id = message.chat.id
DayWeekMonth = message.text
+ # Filter out non-spending-related commands such as "/help"
+ if DayWeekMonth.startswith("/"):
+ bot.reply_to(message, "This command is not related to spendings.")
+ return
+
if DayWeekMonth not in helper.getSpendDisplayOptions():
- raise Exception(
- 'Sorry I can\'t show spendings for "{}"!'.format(DayWeekMonth)
+ raise DisplayOptionError(
+ 'Sorry I can\'t show spendings for "{}"!'.format(DayWeekMonth)
)
history = helper.getUserHistory(chat_id)
if history is None:
- raise Exception("Oops! Looks like you do not have any spending records!")
+ raise NoHistoryError("Oops! Looks like you do not have any spending records!")
bot.send_message(chat_id, "Hold on! Calculating...")
# show the bot "typing" (max. 5 secs)
@@ -88,6 +94,7 @@ def display_total(message, bot):
time.sleep(0.5)
total_text = ""
+ queryResult = []
if DayWeekMonth == "Day":
query = datetime.now().today().strftime(helper.getDateFormat())
# query all that contains today's date
@@ -123,6 +130,7 @@ def display_total(message, bot):
logging.exception(str(e))
bot.reply_to(message, str(e))
+
def calculate_spendings(queryResult):
"""
calculate_spendings(queryResult): Takes 1 argument for processing - queryResult
diff --git a/code/edit.py b/code/edit.py
index 410345017..0f538fd18 100644
--- a/code/edit.py
+++ b/code/edit.py
@@ -2,27 +2,11 @@
File: edit.py
Author: Vyshnavi Adusumelli, Tejaswini Panati, Harshavardhan Bandaru
Date: October 01, 2023
-Description: File contains Telegram bot message handlers and their associated functions.
+Description: Contains Telegram bot message handlers for expense editing features.
Copyright (c) 2023
+...
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
"""
import helper
@@ -30,129 +14,122 @@
from telegram_bot_calendar import DetailedTelegramCalendar, LSTEP
from datetime import datetime
-# === Documentation of edit.py ===
def run(m, bot):
"""
- run(message, bot): This is the main function used to implement the delete feature.
- It takes 2 arguments for processing - message which is the message from the user, and
- bot which is the telegram bot object from the main code.py function. It gets the details
- for the expense to be edited from here and passes control onto edit2(m, bot): for further processing.
+ Initiates the process of editing an expense.
+
+ Args:
+ m: The message object from the user.
+ bot: The Telegram bot object.
"""
chat_id = m.chat.id
- markup = types.ReplyKeyboardMarkup(one_time_keyboard=True)
- markup.row_width = 2
user_history = helper.getUserHistory(chat_id)
+
if not user_history:
- bot.send_message(chat_id,"You have no previously recorded expenses to modify")
+ bot.send_message(chat_id, "You have no previously recorded expenses to modify.")
return
+
+ markup = types.ReplyKeyboardMarkup(one_time_keyboard=True)
+ markup.row_width = 2
+
for c in user_history:
expense_data = c.split(",")
- str_date = "Date=" + expense_data[0]
- str_category = ",\t\tCategory=" + expense_data[1]
- str_amount = ",\t\tAmount=$" + expense_data[2]
- markup.add(str_date + str_category + str_amount)
- info = bot.reply_to(m, "Select expense to be edited:", reply_markup=markup)
+ formatted_expense = f"Date={expense_data[0]},\t\tCategory={expense_data[1]},\t\tAmount=${expense_data[2]}"
+ markup.add(formatted_expense)
+
+ info = bot.reply_to(m, "Select the expense to be edited:", reply_markup=markup)
bot.register_next_step_handler(info, select_category_to_be_updated, bot)
-def select_category_to_be_updated(m, bot):
+def select_category_to_be_updated(m, bot):
"""
- select_category_to_be_updated(m, bot): Handles the user's selection of expense categories for updating.
-
- Parameters:
- - m (telegram.Message): The message object received from the user.
- - bot (telegram.Bot): The Telegram bot object.
+ Handles the user's selection of expense categories for updating.
- This function processes the user's selected expense categories, presents options for updating,
- and registers the next step handler for further processing.
+ Args:
+ m: The message object received from the user.
+ bot: The Telegram bot object.
"""
-
- info = m.text
+ selected_data = m.text.split(",") if m.text else []
markup = types.ReplyKeyboardMarkup(one_time_keyboard=True)
markup.row_width = 2
- selected_data = [] if info is None else info.split(",")
+
for c in selected_data:
markup.add(c.strip())
+
choice = bot.reply_to(m, "What do you want to update?", reply_markup=markup)
- updated = []
- bot.register_next_step_handler(choice, enter_updated_data, bot, selected_data, updated)
+ bot.register_next_step_handler(choice, enter_updated_data, bot, selected_data, [])
-def enter_updated_data(m, bot, selected_data, updated):
+def enter_updated_data(m, bot, selected_data, updated):
"""
- enter_updated_data(m, bot, selected_data, updated): Handles the user's input for updating expense information.
-
- Parameters:
- - m (telegram.Message): The message object received from the user.
- - bot (telegram.Bot): The Telegram bot object.
- - selected_data (list): List of selected expense information.
- - updated (list): List of updated categories.
+ Handles the user's input for updating expense information.
- This function processes the user's choice for updating expense details and registers the next step handlers
- accordingly (date, category, amount).
+ Args:
+ m: The message object received from the user.
+ bot: The Telegram bot object.
+ selected_data: List of selected expense information.
+ updated: List of updated categories.
"""
-
- choice1 = "" if m.text is None else m.text
+ choice1 = m.text if m.text else ""
markup = types.ReplyKeyboardMarkup(one_time_keyboard=True)
markup.row_width = 2
+
for cat in helper.getSpendCategories():
markup.add(cat)
if "Date" in choice1:
- calendar, step = DetailedTelegramCalendar().build()
- bot.send_message(m.chat.id, f"Select {LSTEP[step]}", reply_markup=calendar)
-
- @bot.callback_query_handler(func=DetailedTelegramCalendar.func())
- def edit_cal(c):
- chat_id= c.message.chat.id
- result, key, step = DetailedTelegramCalendar().process(c.data)
-
- if not result and key:
- bot.edit_message_text(
- f"Select {LSTEP[step]}",
- c.message.chat.id,
- c.message.message_id,
- reply_markup=key,
- )
- elif result:
- data = datetime.today().date()
- if (result > data):
- bot.send_message(chat_id,"Cannot select future dates, Please try /edit command again with correct dates")
- else:
- edit_date(bot, selected_data, result, c, updated)
- bot.edit_message_text(
- f"Date is updated: {result}",
- c.message.chat.id,
- c.message.message_id,
- )
-
- if "Category" in choice1:
- new_cat = bot.reply_to(m, "Please select the new category", reply_markup=markup)
+ handle_date_selection(m, bot, selected_data, updated)
+
+ elif "Category" in choice1:
+ new_cat = bot.reply_to(m, "Please select the new category:", reply_markup=markup)
bot.register_next_step_handler(new_cat, edit_cat, bot, selected_data, updated)
- if "Amount" in choice1:
- new_cost = bot.reply_to(
- m, "Please type the new cost\n(Enter only numerical value)"
- )
+ elif "Amount" in choice1:
+ new_cost = bot.reply_to(m, "Please type the new cost\n(Enter only numerical value)")
bot.register_next_step_handler(new_cost, edit_cost, bot, selected_data, updated)
-def update_different_category(m, bot, selected_data, updated):
+def handle_date_selection(m, bot, selected_data, updated):
"""
- update_different_category(m, bot, selected_data, updated): Handles user's choice to update another category.
+ Handles the date selection for expense updates.
+
+ Args:
+ m: The message object received from the user.
+ bot: The Telegram bot object.
+ selected_data: List of selected expense information.
+ updated: List of updated categories.
+ """
+ calendar, step = DetailedTelegramCalendar().build()
+ bot.send_message(m.chat.id, f"Select {LSTEP[step]}", reply_markup=calendar)
+
+ @bot.callback_query_handler(func=DetailedTelegramCalendar.func())
+ def edit_cal(c):
+ chat_id = c.message.chat.id
+ result, key, step = DetailedTelegramCalendar().process(c.data)
- Parameters:
- - m (telegram.Message): The message object received from the user.
- - bot (telegram.Bot): The Telegram bot object.
- - selected_data (list): List of selected expense information.
- - updated (list): List of updated categories.
+ if not result and key:
+ bot.edit_message_text(f"Select {LSTEP[step]}", chat_id, c.message.message_id, reply_markup=key)
+ elif result:
+ data = datetime.today().date()
+ if result > data:
+ bot.send_message(chat_id, "Cannot select future dates. Please try /edit command again with correct dates.")
+ else:
+ edit_date(bot, selected_data, result, c, updated)
+ bot.edit_message_text(f"Date is updated: {result}", chat_id, c.message.message_id)
- This function processes the user's choice to update another category and registers the next step handlers accordingly.
+
+def update_different_category(m, bot, selected_data, updated):
"""
+ Prompts the user to update another category if desired.
- response = m.text
- if response == "Y" or response == "y":
+ Args:
+ m: The message object received from the user.
+ bot: The Telegram bot object.
+ selected_data: List of selected expense information.
+ updated: List of updated categories.
+ """
+ if m.text.lower() == "y":
markup = types.ReplyKeyboardMarkup(one_time_keyboard=True)
markup.row_width = 2
for c in selected_data:
@@ -161,16 +138,20 @@ def update_different_category(m, bot, selected_data, updated):
choice = bot.reply_to(m, "What do you want to update?", reply_markup=markup)
bot.register_next_step_handler(choice, enter_updated_data, bot, selected_data, updated)
+
def edit_date(bot, selected_data, result, c, updated):
"""
- def edit_date(m, bot): It takes 2 arguments for processing - message which is
- the message from the user, and bot which is the telegram bot object from the
- edit3(m, bot):: function in the same file. It takes care of date change and edits.
+ Updates the date for the selected expense.
+
+ Args:
+ bot: The Telegram bot object.
+ selected_data: List of selected expense information.
+ result: The new date selected by the user.
+ c: The callback query.
+ updated: List of updated categories.
"""
- user_list = helper.read_json()
- new_date = datetime.strftime(result, helper.getDateFormat())
chat_id = c.message.chat.id
- m = c.message
+ new_date = datetime.strftime(result, helper.getDateFormat())
data_edit = helper.getUserHistory(chat_id)
for i in range(len(data_edit)):
@@ -178,66 +159,71 @@ def edit_date(m, bot): It takes 2 arguments for processing - message which is
selected_date = selected_data[0].split("=")[1]
selected_category = selected_data[1].split("=")[1]
selected_amount = selected_data[2].split("=")[1]
- if (
- user_data[0] == selected_date and user_data[1] == selected_category and user_data[2] == selected_amount[1:]
- ):
- data_edit[i] = (
- new_date + "," + selected_category + "," + selected_amount[1:]
- )
+
+ if (user_data[0] == selected_date and user_data[1] == selected_category and user_data[2] == selected_amount[1:]):
+ data_edit[i] = f"{new_date},{selected_category},{selected_amount[1:]}"
break
- user_list[str(chat_id)]["data"] = data_edit
- helper.write_json(user_list)
- new_date_str = "Date=" + new_date
- updated.append(new_date_str)
- selected_data[0] = new_date_str
+ helper.update_user_data(chat_id, data_edit)
+ updated.append(f"Date={new_date}")
+ selected_data[0] = f"Date={new_date}"
+
if len(updated) == 3:
- bot.send_message(m.chat.id, "You have updated all the categories for this expense")
+ bot.send_message(chat_id, "You have updated all the categories for this expense.")
return
- resp = bot.send_message(m.chat.id, "Do you want to update another category in this expense?(Y/N)")
- bot.register_next_step_handler(resp, update_different_category, bot, selected_data ,updated)
+
+ resp = bot.send_message(chat_id, "Do you want to update another category in this expense? (Y/N)")
+ bot.register_next_step_handler(resp, update_different_category, bot, selected_data, updated)
+
def edit_cat(m, bot, selected_data, updated):
"""
- def edit_cat(m, bot): It takes 2 arguments for processing - message which is the message
- from the user, and bot which is the telegram bot object from the edit3(m, bot):: function in the
- same file. It takes care of category change and edits.
+ Updates the category for the selected expense.
+
+ Args:
+ m: The message object received from the user.
+ bot: The Telegram bot object.
+ selected_data: List of selected expense information.
+ updated: List of updated categories.
"""
- user_list = helper.read_json()
+ new_cat = m.text if m.text else ""
chat_id = m.chat.id
data_edit = helper.getUserHistory(chat_id)
- new_cat = "" if m.text is None else m.text
+
for i in range(len(data_edit)):
user_data = data_edit[i].split(",")
selected_date = selected_data[0].split("=")[1]
selected_category = selected_data[1].split("=")[1]
selected_amount = selected_data[2].split("=")[1]
- if (
- user_data[0] == selected_date and user_data[1] == selected_category and user_data[2] == selected_amount[1:]
- ):
- data_edit[i] = selected_date + "," + new_cat + "," + selected_amount[1:]
+
+ if (user_data[0] == selected_date and user_data[1] == selected_category and user_data[2] == selected_amount[1:]):
+ data_edit[i] = f"{selected_date},{new_cat},{selected_amount[1:]}"
break
- user_list[str(chat_id)]["data"] = data_edit
- helper.write_json(user_list)
- new_cat_str = "Category=" + new_cat
- updated.append(new_cat_str)
- selected_data[1] = new_cat_str
- bot.reply_to(m, "Category is updated")
+ helper.update_user_data(chat_id, data_edit)
+ updated.append(f"Category={new_cat}")
+ selected_data[1] = f"Category={new_cat}"
+ bot.reply_to(m, "Category is updated.")
+
if len(updated) == 3:
- bot.send_message(m.chat.id, "You have updated all the categories for this expense")
+ bot.send_message(chat_id, "You have updated all the categories for this expense.")
return
- resp = bot.send_message(m.chat.id, "Do you want to update another category in this expense?(Y/N)")
+
+ resp = bot.send_message(chat_id, "Do you want to update another category in this expense? (Y/N)")
bot.register_next_step_handler(resp, update_different_category, bot, selected_data, updated)
+
def edit_cost(m, bot, selected_data, updated):
"""
- def edit_cost(m, bot): It takes 2 arguments for processing - message which is the
- message from the user, and bot which is the telegram bot object from the
- edit3(m, bot):: function in the same file. It takes care of cost change and edits.
+ Updates the cost for the selected expense.
+
+ Args:
+ m: The message object received from the user.
+ bot: The Telegram bot object.
+ selected_data: List of selected expense information.
+ updated: List of updated categories.
"""
- user_list = helper.read_json()
- new_cost = "" if m.text is None else m.text
+ new_cost = m.text if m.text else ""
chat_id = m.chat.id
data_edit = helper.getUserHistory(chat_id)
@@ -247,21 +233,22 @@ def edit_cost(m, bot): It takes 2 arguments for processing - message which is th
selected_date = selected_data[0].split("=")[1]
selected_category = selected_data[1].split("=")[1]
selected_amount = selected_data[2].split("=")[1]
- if (
- user_data[0] == selected_date and user_data[1] == selected_category and user_data[2] == selected_amount[1:]
- ):
- data_edit[i] = selected_date + "," + selected_category + "," + new_cost
+
+ if (user_data[0] == selected_date and user_data[1] == selected_category and user_data[2] == selected_amount[1:]):
+ data_edit[i] = f"{selected_date},{selected_category},{new_cost}"
break
- user_list[str(chat_id)]["data"] = data_edit
- helper.write_json(user_list)
- bot.reply_to(m, "Expense amount is updated")
+
+ helper.update_user_data(chat_id, data_edit)
+ updated.append(f"Amount=${new_cost}")
+ selected_data[2] = f"Amount=${new_cost}"
+ bot.reply_to(m, "Cost is updated.")
+
+ if len(updated) == 3:
+ bot.send_message(chat_id, "You have updated all the categories for this expense.")
+ return
+
+ resp = bot.send_message(chat_id, "Do you want to update another category in this expense? (Y/N)")
+ bot.register_next_step_handler(resp, update_different_category, bot, selected_data, updated)
else:
- bot.reply_to(m, "The cost is invalid")
- new_cost_str = "Category=" + new_cost
- updated.append(new_cost_str)
- selected_data[1] = new_cost_str
- if len(updated) == 3:
- bot.send_message(m.chat.id, "You have updated all the categories for this expense")
- return
- resp = bot.send_message(m.chat.id, "Do you want to update another category in this expense?(Y/N)")
- bot.register_next_step_handler(resp, update_different_category, bot, selected_data, updated)
\ No newline at end of file
+ bot.reply_to(m, "Invalid amount entered. Please enter a numeric value.")
+ run(m, bot)
diff --git a/code/estimate.py b/code/estimate.py
index a53a51307..797bb9b25 100644
--- a/code/estimate.py
+++ b/code/estimate.py
@@ -29,6 +29,7 @@
import helper
import logging
from telebot import types
+from exception import NoSpendingRecordsError, EstimateNotAvailableError
# === Documentation of estimate.py ===
@@ -69,13 +70,11 @@ def estimate_total(message, bot):
DayWeekMonth = message.text
if DayWeekMonth not in helper.getSpendEstimateOptions():
- raise Exception(
- 'Sorry I can\'t show an estimate for "{}"!'.format(DayWeekMonth)
- )
+ raise EstimateNotAvailableError(DayWeekMonth)
history = helper.getUserHistory(chat_id)
if history is None:
- raise Exception("Oops! Looks like you do not have any spending records!")
+ raise NoSpendingRecordsError("Oops! Looks like you do not have any spending records!")
bot.send_message(chat_id, "Hold on! Calculating...")
# show the bot "typing" (max. 5 secs)
diff --git a/code/exception.py b/code/exception.py
new file mode 100644
index 000000000..9922dcb10
--- /dev/null
+++ b/code/exception.py
@@ -0,0 +1,54 @@
+class InvalidAmountError(Exception):
+ """Exception raised for invalid amounts."""
+ def __init__(self, message="Invalid amount."):
+ super().__init__(message)
+
+class InvalidDurationError(Exception):
+ """Exception raised for invalid duration."""
+ def __init__(self, message="Invalid duration."):
+ super().__init__(message)
+
+class InvalidCategoryError(Exception):
+ """Exception raised for invalid categories."""
+ def __init__(self, category, message="Invalid category selected"):
+ self.category = category
+ self.message = message
+ super().__init__(f'{message}: "{category}"')
+
+class InvalidOperationError(Exception):
+ """Exception raised for invalid operations."""
+ def __init__(self, operation, message="Invalid operation selected"):
+ self.operation = operation
+ self.message = message
+ super().__init__(f'{message}: "{operation}"')
+
+class BudgetError(Exception):
+ """Exception raised for invalid budget operations."""
+ def __init__(self, message="Invalid budget."):
+ super().__init__(message)
+
+class DisplayOptionError(Exception):
+ """Exception raised for invalid spending display options."""
+ def __init__(self, message="Invalid display option."):
+ super().__init__(message)
+
+class NoHistoryError(Exception):
+ """Exception raised when a user has no spending history."""
+ def __init__(self, message="No spending records found."):
+ super().__init__(message)
+
+class NoSpendingRecordsError(Exception):
+ """Exception raised when there are no spending records for the user."""
+ def __init__(self, message="Sorry! No spending records found."):
+ super().__init__(message)
+
+class EstimateNotAvailableError(Exception):
+ """Exception raised when an estimate is not available for a given category."""
+ def __init__(self, day_week_month):
+ message = f'Sorry, I can\'t show an estimate for "{day_week_month}"!'
+ super().__init__(message)
+
+class BudgetNotFoundError(Exception):
+ """Exception raised when a budget does not exist."""
+ def __init__(self, message):
+ super().__init__(message)
diff --git a/code/helper.py b/code/helper.py
index a0dbac3ad..468cc0328 100644
--- a/code/helper.py
+++ b/code/helper.py
@@ -29,8 +29,16 @@
import json
import os
from datetime import datetime
-
-spend_categories = []
+from notify import notify
+
+spend_categories = [
+ "Food",
+ "Groceries",
+ "Utilities",
+ "Transport",
+ "Shopping",
+ "Miscellaneous",
+]
choices = ["Date", "Category", "Cost"]
spend_display_option = ["Day", "Month"]
spend_estimate_option = ["Next day", "Next month"]
@@ -74,6 +82,9 @@
"weekly": "This option is to get the weekly analysis report of the expenditure",
"monthly": "This option is to get the monthly analysis report of the expenditure",
"sendEmail": "Send an email with an attachment showing your history",
+ "summary": "Generates a summary of your overall and category-wise budgets, showing remaining and spent amounts.",
+ "report": "Generates a comprehensive report over a custom date range, with individual transactions and totals by category. Ideal for detailed monthly or quarterly reviews.",
+ "socialmedia": "Generate a shareable link to post your expense summary on social media platforms."
}
dateFormat = "%d-%b-%Y"
@@ -110,6 +121,156 @@ def write_json(user_list):
except FileNotFoundError:
print("Sorry, the data file could not be found.")
+# Summary command
+def generate_summary(chat_id, bot):
+ """
+ generate_summary(chat_id, bot): Generates a summary of the user's overall and category-wise budget.
+ """
+ overall_budget = getOverallBudget(chat_id)
+ category_budget = getCategoryBudget(chat_id)
+ total_spent = calculate_total_spendings(getUserHistory(chat_id))
+
+ summary_message = "===== Budget Summary =====\n\n"
+
+ if overall_budget is not None:
+ remaining_overall = calculateRemainingOverallBudget(chat_id)
+ summary_message += f"Overall Budget: ${overall_budget}\n"
+ summary_message += f"Total Spent: ${total_spent}\n"
+ summary_message += f"Remaining Overall Budget: ${remaining_overall}\n\n"
+ else:
+ summary_message += "No overall budget set.\n\n"
+
+ summary_message += "Category-Wise Budget:\n"
+ if category_budget:
+ for category, budget in category_budget.items():
+ spent = calculate_total_spendings_for_category(getUserHistory(chat_id), category)
+ remaining = float(budget) - spent
+ summary_message += f"{category}:\n Budget: ${budget}\n Spent: ${spent}\n Remaining: ${remaining}\n"
+ else:
+ summary_message += "No category-wise budget set."
+
+ bot.send_message(chat_id, summary_message)
+
+def generate_report(chat_id, bot, start_date, end_date):
+ """
+ generate_report(chat_id, bot, start_date, end_date): Generates a detailed spending report
+ for the specified date range and sends it to the user.
+ """
+ try:
+ # Convert dates from string to datetime for comparison
+ start = datetime.strptime(start_date, "%Y-%m-%d")
+ end = datetime.strptime(end_date, "%Y-%m-%d")
+
+ if start > end:
+ bot.send_message(chat_id, "Start date must be before end date. Please try again.")
+ return
+
+ # Fetch expenses for the specified date range
+ expenses = fetch_expenses(chat_id, start, end)
+ if expenses is None or not expenses:
+ bot.send_message(chat_id, f"No expenses found between {start_date} and {end_date}.")
+ return
+
+ # Generate the report content
+ report_content = f"📅 Report from {start_date} to {end_date} 📅\n\n"
+ total_spent = 0
+ for expense in expenses:
+ report_content += f"{expense['date'].strftime('%d-%b-%Y')}: {expense['category']} - ${expense['amount']}\n"
+ total_spent += expense['amount'] # Sum the amounts
+
+ report_content += f"\nTotal Spent: ${total_spent:.2f}"
+ bot.send_message(chat_id, report_content)
+
+ except ValueError:
+ bot.send_message(chat_id, "Invalid date format. Please use YYYY-MM-DD.")
+ except Exception as e:
+ bot.send_message(chat_id, "An error occurred while generating the report.")
+ print(f"Error: {e}") # Print the exception for debugging
+
+def generate_shareable_link(chat_id):
+ """
+ Generates a shareable link for the user's expense summary.
+ This function assumes that an external service API (e.g., Google Drive API) is used to upload
+ the summary and obtain a link that can be publicly shared.
+ """
+ try:
+ # Generate the PDF summary
+ file_path = pdf.create_summary_pdf(chat_id)
+
+ # Upload the file to a service like Google Drive or Dropbox (assuming helper.upload_to_drive exists)
+ shareable_link = helper.upload_to_drive(file_path)
+
+ return shareable_link
+ except Exception as e:
+ logging.exception("Error generating shareable link: " + str(e))
+ return None
+
+def create_shareable_link(chat_id):
+ """
+ Creates a shareable link for the user's expenses.
+ This could be replaced with actual upload logic.
+ """
+ try:
+ # Placeholder for the file path, you can replace this with actual file generation logic
+ file_name = f"{chat_id}_expenses_summary.pdf"
+
+ # Simulated link creation
+ shareable_link = f"https://example.com/shared_files/{file_name}"
+
+ # Log or print to check link
+ print("Generated shareable link:", shareable_link)
+
+ return shareable_link
+ except Exception as e:
+ logging.exception("Error generating shareable link: " + str(e))
+ return None
+
+def fetch_expenses(chat_id, start_date, end_date):
+ """
+ Fetch expenses for a user between specified start and end dates.
+
+ Args:
+ chat_id (str): The unique identifier for the user.
+ start_date (datetime): The start date for fetching expenses.
+ end_date (datetime): The end date for fetching expenses.
+
+ Returns:
+ list: A list of expenses within the specified date range.
+ """
+ user_data = getUserData(chat_id) # Assuming this retrieves user data
+
+ if not user_data or "data" not in user_data:
+ print("No user data found.")
+ return [] # Return an empty list if no data found
+
+ expenses = user_data["data"]
+
+ # Ensure expenses is a list
+ if not isinstance(expenses, list):
+ print("Expected expenses to be a list, but got:", type(expenses))
+ return []
+
+ filtered_expenses = []
+
+ for expense_str in expenses:
+ try:
+ date_str, category, amount_str = expense_str.split(',')
+ expense_date = datetime.strptime(date_str, "%d-%b-%Y") # Adjust date format as needed
+ amount = float(amount_str)
+
+ # Check if the expense falls within the specified date range
+ if start_date <= expense_date <= end_date:
+ filtered_expenses.append({
+ 'date': expense_date,
+ 'category': category,
+ 'amount': amount
+ })
+ except ValueError as e:
+ print(f"Error parsing expense: {expense_str} -> {e}")
+ continue
+
+ return filtered_expenses
+
def read_category_json():
"""
read_json(): Function to load .json expense record data
@@ -267,28 +428,78 @@ def get_uncategorized_amount(chatId, amount):
uncategorized_budget = overall_budget - category_budget
return str(round(uncategorized_budget,2))
-def display_remaining_budget(message, bot):
+def display_remaining_budget(message, bot, cat):
+ """
+ Display the remaining budget for both the overall budget and a specific category.
+ """
+ chat_id = message.chat.id
+ display_remaining_category_budget(message, bot, cat)
display_remaining_overall_budget(message, bot)
def display_remaining_overall_budget(message, bot):
+ print("here")
chat_id = message.chat.id
remaining_budget = calculateRemainingOverallBudget(chat_id)
- if remaining_budget >= 0:
+ print("here", remaining_budget)
+
+ # Check if remaining_budget is None
+ if remaining_budget is None:
+ msg = "Error: Unable to calculate remaining budget."
+ elif remaining_budget >= 0:
msg = "\nRemaining Overall Budget is $" + str(remaining_budget)
else:
msg = (
- "\nBudget Exceded!\nExpenditure exceeds the budget by $" + str(remaining_budget)[1:]
+ "\nBudget Exceeded!\nExpenditure exceeds the budget by $" + str(remaining_budget)[1:]
)
+
bot.send_message(chat_id, msg)
def calculateRemainingOverallBudget(chat_id):
budget = getOverallBudget(chat_id)
+
+ # Check if budget is valid
+ if budget is None:
+ print("Error: Overall budget not found.")
+ return None # Or handle this case appropriately
+
history = getUserHistory(chat_id)
+
+ # If history is empty or None, handle it
+ if not history:
+ print("Error: User history not found.")
+ return float(budget) # No spendings means remaining budget is the full amount
+
query = datetime.now().today().strftime(getMonthFormat())
+
+ # Filters history for entries that match the current month
queryResult = [value for _, value in enumerate(history) if str(query) in value]
- if budget == None:
- return -calculate_total_spendings(queryResult)
- return float(budget) - calculate_total_spendings(queryResult)
+
+ total_spendings = calculate_total_spendings(queryResult)
+
+ # Handle case where total_spendings might be None
+ if total_spendings is None:
+ print("Error: Total spendings could not be calculated.")
+ total_spendings = 0 # Assuming no spendings if calculation fails
+
+ # Calculate and return the remaining budget
+ return float(budget) - total_spendings
+
+
+def display_remaining_category_budget(message, bot, cat):
+ """
+ Display the remaining budget for a specific category.
+ """
+ chat_id = message.chat.id
+ remaining_budget = calculateRemainingCategoryBudget(chat_id, cat)
+
+ if remaining_budget >= 0:
+ msg = f"\nRemaining budget for {cat} category is ${remaining_budget:.2f}"
+ else:
+ msg = (
+ f"\nBudget Exceeded for {cat} category!\nExpenditure exceeds the budget by ${abs(remaining_budget):.2f}"
+ )
+
+ bot.send_message(chat_id, msg)
def calculate_total_spendings(queryResult):
total = 0
@@ -298,12 +509,14 @@ def calculate_total_spendings(queryResult):
return total
-def calculateRemainingCategoryBudget(chat_id, cat):
+def calculateRemainingCategoryBudgetPercent(chat_id, cat):
budget = getCategoryBudgetByCategory(chat_id, cat)
+ if not budget or float(budget) == 0: # Check if budget is None or zero
+ return 0 # Return 0 percent if budget is zero to avoid division error
history = getUserHistory(chat_id)
query = datetime.now().today().strftime(getMonthFormat())
queryResult = [value for _, value in enumerate(history) if str(query) in value]
- return float(budget) - calculate_total_spendings_for_category(queryResult, cat)
+ return (calculate_total_spendings_for_category(queryResult, cat) / float(budget)) * 100
def calculateRemainingCategoryBudgetPercent(chat_id, cat):
budget = getCategoryBudgetByCategory(chat_id, cat)
@@ -403,6 +616,40 @@ def addSpendCategories(category):
category_list["categories"] = result
write_category_json(category_list)
+# Original function `display_remaining_budget` and `display_remaining_category_budget` references:
+def display_remaining_budget(message, bot, cat):
+ display_remaining_category_budget(message, bot, cat)
+
+def display_remaining_category_budget(message, bot, cat):
+ # Assuming `chat_id` is retrieved or passed in context to this function
+ chat_id = message.chat.id # Adjust if `chat_id` comes from a different source in your code
+ remaining_budget = calculateRemainingCategoryBudget(chat_id, cat)
+ # Display remaining budget to user
+ bot.send_message(chat_id, f"Remaining budget for {cat}: {remaining_budget}")
+
+# Placeholder for `calculateRemainingCategoryBudget`
+def calculateRemainingCategoryBudget(chat_id, category):
+ """
+ Placeholder function for calculating remaining budget for a specific category.
+ Replace with actual logic to retrieve and calculate the budget from your data source.
+ """
+ # For example, if budgets are stored in a dictionary, you'd fetch and calculate the remaining amount
+ # Mocked example: returning a fixed value for now
+ budgets = {
+ "Food": 100.0,
+ "Transport": 50.0,
+ "Entertainment": 75.0,
+ # Add more categories as needed
+ }
+ remaining_budget = budgets.get(category, 0.0) # Return 0.0 if category is not found
+ return remaining_budget
+
+def getSpendCategories():
+ """
+ getSpendCategories(): This functions returns the spend categories used in the bot. These are defined the same file.
+ """
+ return spend_categories
+
def getSpendDisplayOptions():
"""
getSpendDisplayOptions(): This functions returns the spend display options used in the bot. These are defined the same file.
diff --git a/code/history.py b/code/history.py
index a6a46d721..6f3a1ffc9 100644
--- a/code/history.py
+++ b/code/history.py
@@ -29,6 +29,7 @@
import logging
from tabulate import tabulate
from datetime import datetime
+from exception import NoSpendingRecordsError
# === Documentation of history.py ===
@@ -46,9 +47,9 @@ def run(message, bot):
user_history = helper.getUserHistory(chat_id)
table = [["Date", "Category", "Amount"]]
if user_history is None:
- raise Exception("Sorry! No spending records found!")
+ raise NoSpendingRecordsError()
if len(user_history) == 0:
- raise Exception("Sorry! No spending records found!")
+ raise NoSpendingRecordsError()
else:
for rec in user_history:
values = rec.split(',')
diff --git a/code/pdf.py b/code/pdf.py
index b0db2944c..4ef1333ae 100644
--- a/code/pdf.py
+++ b/code/pdf.py
@@ -42,7 +42,7 @@ def run(message, bot):
helper.read_json()
chat_id = message.chat.id
user_history = helper.getUserHistory(chat_id)
- msg = "Alright. Creating a pdf of your expense history!"
+ msg = "Alright. I just created a pdf of your expense history!"
bot.send_message(chat_id, msg)
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
@@ -128,3 +128,39 @@ def run(message, bot):
except Exception as e:
logging.exception(str(e))
bot.reply_to(message, "Oops!" + str(e))
+
+def create_summary_pdf(chat_id):
+ """
+ Creates a summary PDF of the user's expenses and returns the file path.
+ """
+ try:
+ # Placeholder: Path where the PDF will be saved
+ file_path = f"{chat_id}_expenses_summary.pdf"
+
+ # Create a PDF object
+ pdf = FPDF()
+ pdf.set_auto_page_break(auto=True, margin=15)
+ pdf.add_page()
+
+ pdf.set_font("Arial", size=12)
+ pdf.cell(200, 10, txt="Expenses Summary", ln=True, align='C')
+
+ # Fetch user data to populate the PDF
+ user_history = helper.getUserHistory(chat_id)
+ total_expense = 0
+ for rec in user_history:
+ date, category, amount = rec.split(",")
+ amount = float(amount) # Ensure amount is treated as a float
+ total_expense += amount
+ pdf.cell(200, 10, txt=f"Date: {date}, Category: {category}, Amount: ${amount:.2f}", ln=True)
+
+ # Add total expense to the PDF
+ pdf.cell(200, 10, txt=f"Total Expense: ${total_expense:.2f}", ln=True)
+
+ # Save the PDF to the specified file path
+ pdf.output(file_path)
+
+ return file_path
+ except Exception as e:
+ logging.error("Error while creating PDF: " + str(e))
+ return None
diff --git a/code/sendEmail.py b/code/sendEmail.py
index 5abfe2fb9..c3088ef81 100644
--- a/code/sendEmail.py
+++ b/code/sendEmail.py
@@ -34,6 +34,7 @@
from email.mime.text import MIMEText
from email.mime.base import MIMEBase
from email import encoders
+from exception import NoSpendingRecordsError
# === Documentation of sendEmail.py ===
@@ -51,9 +52,9 @@ def run(message, bot):
chat_id = message.chat.id
user_history = helper.getUserHistory(chat_id)
if user_history is None:
- raise Exception("Sorry! No spending records found!")
+ raise NoSpendingRecordsError()
if len(user_history) == 0:
- raise Exception("Sorry! No spending records found!")
+ raise NoSpendingRecordsError()
else:
category = bot.send_message(message.chat.id, "Enter your email id")
bot.register_next_step_handler(category, acceptEmailId, bot)
@@ -71,9 +72,9 @@ def acceptEmailId(message, bot):
user_history = helper.getUserHistory(chat_id)
table = [["Date", "Category", "Amount"]]
if user_history is None:
- raise Exception("Sorry! No spending records found!")
+ raise NoSpendingRecordsError()
if len(user_history) == 0:
- raise Exception("Sorry! No spending records found!")
+ raise NoSpendingRecordsError()
else:
for rec in user_history:
diff --git a/code/voice.py b/code/voice.py
new file mode 100644
index 000000000..ea3b31b01
--- /dev/null
+++ b/code/voice.py
@@ -0,0 +1,99 @@
+import os
+import speech_recognition as sr
+import tempfile
+import add
+import history
+import predict
+import monthly
+import weekly
+import budget
+import helper
+from pydub import AudioSegment
+from telebot import types
+
+
+def run(message, bot):
+ file_info = bot.get_file(message.voice.file_id)
+ downloaded_file = bot.download_file(file_info.file_path)
+
+ # Create a temporary OGG file
+ with tempfile.NamedTemporaryFile(delete=False, suffix='.ogg') as temp_ogg:
+ temp_ogg.write(downloaded_file)
+ temp_ogg_path = temp_ogg.name
+
+ # Convert OGG to WAV
+ temp_wav_path = tempfile.NamedTemporaryFile(delete=False, suffix='.wav').name
+ audio = AudioSegment.from_ogg(temp_ogg_path)
+ audio.export(temp_wav_path, format='wav')
+
+ # Use SpeechRecognition to convert voice to text
+ recognizer = sr.Recognizer()
+ with sr.AudioFile(temp_wav_path) as source:
+ audio_data = recognizer.record(source)
+ try:
+ text = recognizer.recognize_google(audio_data)
+ bot.send_message(message.chat.id, f"I heard: \"{text}\"")
+ process_command(text, message, bot)
+ except sr.UnknownValueError:
+ bot.reply_to(message, "Sorry, I could not understand the audio.")
+ except sr.RequestError :
+ bot.reply_to(message, "Could not request results from the speech recognition service.")
+
+ # Cleanup: remove the temporary files
+ os.remove(temp_ogg_path)
+ os.remove(temp_wav_path)
+
+
+def process_command(text, message, bot):
+ if "expense" in text:
+ add.run(message, bot)
+ elif "history" in text:
+ history.run(message, bot) # Call the existing history command
+ elif "budget" in text:
+ budget.run(message, bot) # Call the existing budget command
+ elif "weekly" in text:
+ weekly.run(message, bot)
+ elif "monthly" in text:
+ monthly.run(message, bot)
+ elif "predict" in text:
+ predict.run(message, bot)
+ elif "help" in text:
+ show_help(message, bot)
+ elif "menu" or "start" in text:
+ start_and_menu_command(message, bot)
+ else:
+ bot.send_message(message.chat.id, "I didn't recognize that command.")
+
+def show_help(m, bot):
+ chat_id = m.chat.id
+ message = (
+ "*Here are the commands you can use:*\n"
+ "/add - Add a new expense 💵\n"
+ "/history - View your expense history 📜\n"
+ "/budget - Check your budget 💳\n"
+ "/analytics - View graphical analytics 📊\n"
+ "For more info, type /faq or tap the button below 👇"
+ )
+ keyboard = types.InlineKeyboardMarkup()
+ keyboard.add(types.InlineKeyboardButton("FAQ", callback_data='faq'))
+ bot.send_message(chat_id, message, parse_mode='Markdown', reply_markup=keyboard)
+
+def start_and_menu_command(m, bot):
+ helper.read_json()
+ chat_id = m.chat.id
+ text_intro = (
+ "*Welcome to the Dollar Bot!* \n"
+ "DollarBot can track all your expenses with simple and easy-to-use commands :) \n"
+ "Here is the complete menu:\n\n"
+ )
+
+ commands = helper.getCommands()
+ keyboard = types.InlineKeyboardMarkup()
+
+ for command, _ in commands.items(): # Unpack the tuple to get the command name
+ button_text = f"/{command}"
+ keyboard.add(types.InlineKeyboardButton(text=button_text, callback_data=command)) # Use `command` as a string
+
+ text_intro += "_Click a command button to use it._"
+ bot.send_message(chat_id, text_intro, reply_markup=keyboard, parse_mode='Markdown')
+ return True
\ No newline at end of file
diff --git a/expenditure.png b/expenditure.png
new file mode 100644
index 000000000..023be7787
Binary files /dev/null and b/expenditure.png differ
diff --git a/expense_history.png b/expense_history.png
new file mode 100644
index 000000000..51f7bb485
Binary files /dev/null and b/expense_history.png differ
diff --git a/expense_report.pdf b/expense_report.pdf
new file mode 100644
index 000000000..6b0260e81
Binary files /dev/null and b/expense_report.pdf differ
diff --git a/history.csv b/history.csv
new file mode 100644
index 000000000..57398bb9c
--- /dev/null
+++ b/history.csv
@@ -0,0 +1,4 @@
+Date,Category,Amount
+02-Oct-2024,Food,$ 12
+12-Oct-2024,Groceries,$ 10.0
+26-Oct-2024,Food,$ 95.0
diff --git a/proj3/DollarBot_proj3_scorecard.csv b/proj3/DollarBot_proj3_scorecard.csv
index 7703d85fd..38eccd3f9 100644
--- a/proj3/DollarBot_proj3_scorecard.csv
+++ b/proj3/DollarBot_proj3_scorecard.csv
@@ -1,25 +1,25 @@
-Github Project Link,https://github.com/tpanati/DollarBot,,
+Github Project Link,https://github.com/CSC510-SE-SAITEJA-MADHUMITHA-SHRUTI/DollarBot,,
Column 1,Column 2(self evaluation),Evidence,
,87,,
-Video,3,https://github.com/tpanati/DollarBot#demo-video,
-Workload is spread over the whole team,3,https://github.com/users/tpanati/projects/2,
-Number of commits,3,https://github.com/tpanati/DollarBot/graphs/contributors?from=2023-08-25&to=2023-11-20&type=c,
-No.of commits: by different people,3,https://github.com/tpanati/DollarBot/graphs/contributors?from=2023-08-25&to=2023-11-20&type=c,
-Issue Reports: Many,3,https://github.com/tpanati/DollarBot/issues?q=is%3Aissue+is%3Aclosed,
-Issues are closed,3,https://github.com/tpanati/DollarBot/issues?q=is%3Aissue+is%3Aclosed,
-DOI Badge,3,https://github.com/tpanati/DollarBot/blob/main/README.md,
-Docs: doco generated,3,https://github.com/tpanati/DollarBot/blob/main/README.md,
-Docs: what: point descriptions of each class/function (in isolation),3,https://github.com/tpanati/DollarBot/tree/main/code https://github.com/tpanati/DollarBot/tree/main/docs,docstrings explaining each class and function are included in the corresponding code files (.py)
-"Docs: how: for common use cases X,Y,Z mini-tutorials showing worked examples on how to do X,Y,Z",3,https://github.com/tpanati/DollarBot#information_desk_person-use-cases,
-"Docs: why: docs tell a story, motivate the whole thing, deliver a punchline that makes you want to rush out and use the thing",3,https://github.com/tpanati/DollarBot#dollarbot---because-your-financial-future-deserves-the-best,
-"Docs: short video, animated, hosted on your repo. That convinces people why they want to work on your code.",3,https://github.com/tpanati/DollarBot#dollarbot---because-your-financial-future-deserves-the-best,Linked short video as a link in Readme.md
-Use of version control tools,3,Hosted in Git (version control tool) - https://github.com/tpanati/DollarBot/tree/main/,
-Use of style checkers,3,https://github.com/tpanati/DollarBot/blob/main/pylintrc,
-Use of code formatters,3,https://github.com/tpanati/DollarBot/tree/main/.github/workflows,
-Use of syntax checkers,3,https://github.com/tpanati/DollarBot/tree/main/.github/workflows,
-Use of code coverage,3,https://github.com/tpanati/DollarBot/blob/main/.github/workflows/python-app.yml https://github.com/tpanati/DollarBot/tree/main,Added codecov in workflow and also added a badge in readme.md
-Other automated analysis tools,3,https://github.com/tpanati/DollarBot/tree/main/.github/workflows,
-Test Cases exist,3,https://github.com/tpanati/DollarBot/tree/main/test,
+Video,3,https://github.com/,
+Workload is spread over the whole team,3,https://github.com/,
+Number of commits,3,https://github.com/CSC510-SE-SAITEJA-MADHUMITHA-SHRUTI/DollarBot/graphs/contributors?from=9%2F28%2F2024,
+No.of commits: by different people,3,https://github.com/CSC510-SE-SAITEJA-MADHUMITHA-SHRUTI/DollarBot/graphs/contributors?from=9%2F28%2F2024,
+Issue Reports: Many,3,https://github.com/CSC510-SE-SAITEJA-MADHUMITHA-SHRUTI/DollarBot/issues?q=is%3Aissue+is%3Aclosed,
+Issues are closed,3,https://github.com/CSC510-SE-SAITEJA-MADHUMITHA-SHRUTI/DollarBot/issues?q=is%3Aissue+is%3Aclosed,
+DOI Badge,3,https://github.com/CSC510-SE-SAITEJA-MADHUMITHA-SHRUTI/DollarBot/blob/main/README.md,
+Docs: doco generated,3,https://github.com/CSC510-SE-SAITEJA-MADHUMITHA-SHRUTI/DollarBot/blob/main/README.md,
+Docs: what: point descriptions of each class/function (in isolation),3,https://github.com/CSC510-SE-SAITEJA-MADHUMITHA-SHRUTI/DollarBot/tree/main/code,docstrings explaining each class and function are included in the corresponding code files (.py)
+"Docs: how: for common use cases X,Y,Z mini-tutorials showing worked examples on how to do X,Y,Z",3,https://github.com/CSC510-SE-SAITEJA-MADHUMITHA-SHRUTI/DollarBot#information_desk_person-use-cases,
+"Docs: why: docs tell a story, motivate the whole thing, deliver a punchline that makes you want to rush out and use the thing",3,https://github.com/CSC510-SE-SAITEJA-MADHUMITHA-SHRUTI/DollarBot#dollarbot---because-your-financial-future-deserves-the-best ,
+"Docs: short video, animated, hosted on your repo. That convinces people why they want to work on your code.",3,https://github.com/CSC510-SE-SAITEJA-MADHUMITHA-SHRUTI/DollarBot#dollarbot---because-your-financial-future-deserves-the-best ,Linked short video as a link in Readme.md
+Use of version control tools,3,https://github.com/CSC510-SE-SAITEJA-MADHUMITHA-SHRUTI/DollarBot/tree/main,
+Use of style checkers,3,https://github.com/CSC510-SE-SAITEJA-MADHUMITHA-SHRUTI/DollarBot/blob/main/pylintrc,
+Use of code formatters,3,https://github.com/CSC510-SE-SAITEJA-MADHUMITHA-SHRUTI/DollarBot/tree/main/.github/workflows,
+Use of syntax checkers,3,https://github.com/CSC510-SE-SAITEJA-MADHUMITHA-SHRUTI/DollarBot/tree/main/.github/workflows,
+Use of code coverage,3,https://github.com/CSC510-SE-SAITEJA-MADHUMITHA-SHRUTI/DollarBot/blob/main/.github/workflows/python-app.yml,Added codecov in workflow and also added a badge in readme.md
+Other automated analysis tools,3,https://github.com/CSC510-SE-SAITEJA-MADHUMITHA-SHRUTI/DollarBot/tree/main/.github/workflows,
+Test Cases exist,3,https://github.com/CSC510-SE-SAITEJA-MADHUMITHA-SHRUTI/DollarBot/tree/main/test,
Test cases are routinely executed,3,https://github.com/tpanati/DollarBot/blob/main/.github/workflows/python-app.yml,
The files CONTRIBUTING.md lists coding standards and lots of tips on how to extend the system without screwing things up,3,https://github.com/tpanati/DollarBot/blob/main/CONTRIBUTING.md,
"issues are discussed before they are closed even if you discuss in slack, need a summary statement here",3,https://github.com/tpanati/DollarBot/issues/16,Issues discussed in the corresponding PRs and also on whatsapp group chat
diff --git a/pylintrc b/pylintrc
index 26857b6d0..b5767f5d5 100644
--- a/pylintrc
+++ b/pylintrc
@@ -72,11 +72,7 @@ disable=unsubscriptable-object,
unpacking-in-except,
old-raise-syntax,
backtick,
- long-suffix,
- old-ne-operator,
- old-octal-literal,
import-star-module-level,
- non-ascii-bytes-literal,
raw-checker-failed,
bad-inline-option,
locally-disabled,
@@ -124,7 +120,6 @@ disable=unsubscriptable-object,
range-builtin-not-iterating,
filter-builtin-not-iterating,
using-cmp-argument,
- eq-without-hash,
div-method,
idiv-method,
rdiv-method,
@@ -607,5 +602,4 @@ min-public-methods=2
# Exceptions that will emit a warning when being caught. Defaults to
# "BaseException, Exception".
-overgeneral-exceptions=BaseException,
- Exception
\ No newline at end of file
+overgeneral-exceptions=builtins.BaseException, builtins.Exception
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index 6a9242123..24f30907b 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -10,3 +10,6 @@ pytest
python-telegram-bot-calendar
mock
tabulate
+pandas
+SpeechRecognition
+pydub
\ No newline at end of file
diff --git a/run.sh b/run.sh
old mode 100644
new mode 100755
diff --git a/setup.sh b/setup.sh
old mode 100644
new mode 100755
index ef84e5680..b4990f08c
--- a/setup.sh
+++ b/setup.sh
@@ -1,34 +1,37 @@
+#!/bin/bash
+
+# Install required packages
pip3 install -r requirements.txt
-api_token=$(grep "api_token" user.properties|cut -d'=' -f2)
+# Extract API token from user.properties
+api_token=$(grep "api_token" user.properties | cut -d'=' -f2)
-flag = "old"
+# Initialize flag variable correctly
+flag="old"
echo "Checking for API Token..."
-if [ -z "$api_token" ]
-then
+if [ -z "$api_token" ]; then
echo "Welcome to DollarBot!"
echo "Follow the steps below to generate an API token to uniquely identify your personal DollarBot. Then, proceed to enter the generated token when prompted to run DollarBot."
echo
echo "1. Download and install the Telegram desktop application for your system from the following site: https://desktop.telegram.org/"
- echo "2. Once you login to your Telegram account, search for \"BotFather\" in Telegram. Click on \"Start\" --> enter the following command:"
+ echo "2. Once you log in to your Telegram account, search for 'BotFather' in Telegram. Click on 'Start' --> enter the following command:"
echo "/newbot"
- echo "3. Follow the instructions on screen and choose a name for your bot. Post this, select a username for your bot that ends with \"bot\" (as per the instructions on your Telegram screen)"
+ echo "3. Follow the instructions on screen and choose a name for your bot. Post this, select a username for your bot that ends with 'bot' (as per the instructions on your Telegram screen)."
echo "4. BotFather will now confirm the creation of your bot and provide a TOKEN to access the HTTP API - copy this token."
echo
echo "Do you want to add your API token now? (Y/n)"
read option
- if [ $option == 'y' -o $option == 'Y' ]
- then
- flag = "new"
+
+ if [[ "$option" == 'y' || "$option" == 'Y' ]]; then
+ flag="new" # Correctly set the flag variable
echo "Enter the copied token: "
read api_token
- echo "api_token="$api_token >> user.properties
+ echo "api_token=$api_token" >> user.properties # Append the token to the properties file
fi
fi
-if [ -n "$api_token" ]
-then
+if [ -n "$api_token" ]; then
echo "Thanks for choosing DollarBot! Starting DollarBot with new API token..."
python3 code/code.py
-fi
\ No newline at end of file
+fi
diff --git a/spend_wise.png b/spend_wise.png
new file mode 100644
index 000000000..bc232e2f6
Binary files /dev/null and b/spend_wise.png differ
diff --git a/test/__init__.py b/test/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/test_add.py b/test/test_add.py
index d042ad049..ef7c0410a 100644
--- a/test/test_add.py
+++ b/test/test_add.py
@@ -169,4 +169,23 @@ def test_read_json():
return expense_record_data
except FileNotFoundError:
- print("---------NO RECORDS FOUND---------")
\ No newline at end of file
+ print("---------NO RECORDS FOUND---------")
+
+@patch("telebot.telebot")
+def test_post_category_selection_invalidDate(mock_telebot, mocker):
+ mc = mock_telebot.return_value
+ mc.send_message.return_value = True
+ mc.reply_to.return_value = True # Assume bot replies to invalid inputs
+
+ # Mocking helper functions
+ mocker.patch.object(add, "helper")
+ add.helper.getSpendCategories.return_value = ["Food", "Utilities"]
+
+ # Setting a future date (invalid date input scenario)
+ future_date = datetime.today().date().replace(year=datetime.today().year + 1)
+
+ message = create_message("Testing invalid date input!")
+ add.post_category_selection(message, mc, future_date)
+ assert mc.reply_to.called # The bot should reply due to invalid date
+
+
diff --git a/test/test_budget.py b/test/test_budget.py
index a1a6c0986..1fe0221ce 100644
--- a/test/test_budget.py
+++ b/test/test_budget.py
@@ -25,10 +25,10 @@
SOFTWARE.
"""
-import mock
from mock.mock import patch
from telebot import types
-from code import budget
+from code import budget, helper
+from exception import BudgetNotFoundError
@patch("telebot.telebot")
@@ -40,7 +40,6 @@ def test_run(mock_telebot, mocker):
assert mc.reply_to.called
# assert mc.reply_to.called_with(ANY, "Select Operation", ANY)
-
@patch("telebot.telebot")
def test_post_operation_selection_failing_case(mock_telebot, mocker):
mc = mock_telebot.return_value
@@ -53,7 +52,6 @@ def test_post_operation_selection_failing_case(mock_telebot, mocker):
budget.post_operation_selection(message, mc)
mc.send_message.assert_called_with(11, "Invalid", reply_markup=mock.ANY)
-
@patch("telebot.telebot")
def test_post_operation_selection_update_case(mock_telebot, mocker):
mc = mock_telebot.return_value
@@ -73,7 +71,6 @@ def test_post_operation_selection_update_case(mock_telebot, mocker):
budget.post_operation_selection(message, mc)
assert budget.budget_update.run.called
-
@patch("telebot.telebot")
def test_post_operation_selection_view_case(mock_telebot, mocker):
mc = mock_telebot.return_value
@@ -93,7 +90,6 @@ def test_post_operation_selection_view_case(mock_telebot, mocker):
budget.post_operation_selection(message, mc)
assert budget.budget_view.run.called
-
@patch("telebot.telebot")
def test_post_operation_selection_delete_case(mock_telebot, mocker):
mc = mock_telebot.return_value
@@ -113,8 +109,168 @@ def test_post_operation_selection_delete_case(mock_telebot, mocker):
budget.post_operation_selection(message, mc)
assert budget.budget_delete.run.called
+def create_message(text):
+ params = {"messagebody": text}
+ chat = types.User(11, False, "test")
+ message = types.Message(1, None, None, chat, "text", params, "")
+ message.text = text
+ return message
+
+@patch("telebot.telebot")
+def test_run_no_budget_set(mock_telebot, mocker):
+ """
+ Tests the case where the user has not set a budget,
+ and a BudgetNotFoundError should be raised with an appropriate message.
+ """
+ mc = mock_telebot.return_value
+ mc.send_message.return_value = True
+
+ # Mock helper functions to return no budget
+ mocker.patch.object(helper, "isOverallBudgetAvailable", return_value=False)
+ mocker.patch.object(helper, "isCategoryBudgetAvailable", return_value=False)
+
+ message = create_message("hello from test run!")
+ budget.run(message, mc)
+
+ mc.send_message.assert_called_with(message.chat.id, "No budget configured. Use the /budget command to add or update your budget.")
+
+
+@patch("telebot.telebot")
+def test_run_budget_displayed(mock_telebot, mocker):
+ """
+ Tests that the budget is displayed when the user has an overall or category-wise budget set.
+ """
+ mc = mock_telebot.return_value
+ mc.send_message.return_value = True
+
+ # Mock helper functions to return an available budget
+ mocker.patch.object(helper, "isOverallBudgetAvailable", return_value=True)
+ mocker.patch.object(helper, "isCategoryBudgetAvailable", return_value=False)
+
+ message = create_message("display budget")
+ budget.run(message, mc)
+
+ mc.send_message.assert_called_with(message.chat.id, "Retrieving your budget details...")
+
+@patch("telebot.telebot")
+def test_post_operation_selection_invalid_command(mock_telebot, mocker):
+ """
+ Tests response when the user sends an invalid command or operation.
+ """
+ mc = mock_telebot.return_value
+ mc.send_message.return_value = True
+
+ mocker.patch.object(budget, "helper")
+ budget.helper.getBudgetOptions.return_value = {"update": "Add/Update", "view": "View", "delete": "Delete"}
+
+ message = create_message("InvalidCommand")
+ budget.post_operation_selection(message, mc)
+
+ mc.send_message.assert_called_with(message.chat.id, "Invalid operation selected. Please choose from Add/Update, View, or Delete.")
+
+@patch("telebot.telebot")
+def test_display_overall_budget_success(mock_telebot, mocker):
+ """
+ Tests display of the overall budget when it is successfully retrieved.
+ """
+ mc = mock_telebot.return_value
+ mc.send_message.return_value = True
+
+ # Mock helper function to return a sample budget value
+ mocker.patch.object(helper, "getOverallBudget", return_value="500")
+
+ message = create_message("overall budget")
+ budget.display_overall_budget(message.chat.id, mc)
+
+ mc.send_message.assert_called_with(message.chat.id, "💰 Overall Budget: $500")
+
+@patch("telebot.telebot")
+def test_display_overall_budget_failure(mock_telebot, mocker):
+ """
+ Tests display of the overall budget when an error occurs in fetching the budget.
+ """
+ mc = mock_telebot.return_value
+ mc.send_message.return_value = True
+
+ # Mock helper function to raise an exception
+ mocker.patch.object(helper, "getOverallBudget", side_effect=Exception("Database error"))
+
+ message = create_message("overall budget")
+ budget.display_overall_budget(message.chat.id, mc)
+
+ mc.send_message.assert_called_with(message.chat.id, "Sorry, we encountered an error retrieving your overall budget.")
+
+@patch("telebot.telebot")
+def test_display_category_budget_with_data(mock_telebot, mocker):
+ """
+ Tests display of category budget when categories are available.
+ """
+ mc = mock_telebot.return_value
+ mc.send_message.return_value = True
+
+ # Mock helper function to return category budget data
+ mocker.patch.object(helper, "getCategoryBudget", return_value={"Food": 200, "Utilities": 100})
+ mocker.patch("graphing.viewBudget", return_value=True)
+
+ message = create_message("category budget")
+ budget.display_category_budget(message.chat.id, mc)
+
+ mc.send_message.assert_any_call(message.chat.id, "📊 Here’s your category-wise budget:")
+ mc.send_message.assert_any_call(message.chat.id, "- Food: $200\n- Utilities: $100")
+ mc.send_photo.assert_called() # Assuming the graph generation succeeds
+
+@patch("telebot.telebot")
+def test_display_category_budget_no_data(mock_telebot, mocker):
+ """
+ Tests display of category budget when no category budgets are set.
+ """
+ mc = mock_telebot.return_value
+ mc.send_message.return_value = True
+
+ # Mock helper function to return empty category budget data
+ mocker.patch.object(helper, "getCategoryBudget", return_value=None)
+
+ message = create_message("category budget")
+ budget.display_category_budget(message.chat.id, mc)
+
+ mc.send_message.assert_called_with(message.chat.id, "It looks like your category budgets haven't been set up yet.")
+
+def create_message(text):
+ """
+ Helper function to create a mock Telegram message object for testing.
+ """
+ params = {"messagebody": text}
+ chat = types.User(11, False, "test")
+ message = types.Message(1, None, None, chat, "text", params, "")
+ message.text = text
+ return message
+
+
+@patch("telebot.telebot")
+def test_display_category_budget_with_data(mock_telebot, mocker):
+ """
+ Tests display of category budget when categories are available.
+ """
+ mc = mock_telebot.return_value
+ mc.send_message.return_value = True
+
+ # Mock helper function to return category budget data
+ mocker.patch.object(helper, "getCategoryBudget", return_value={"Food": 200, "Utilities": 100})
+ mocker.patch("graphing.viewBudget", return_value=True)
+
+ message = create_message("category budget")
+ budget.display_category_budget(message.chat.id, mc)
+
+ mc.send_message.assert_any_call(message.chat.id, "📊 Here’s your category-wise budget:")
+ mc.send_message.assert_any_call(message.chat.id, "- Food: $200\n- Utilities: $100")
+ mc.send_photo.assert_called()
+ # Assert that the graphing function is called correctly with the data
+ mocker.patch("graphing.viewBudget").assert_called_with({"Food": 200, "Utilities": 100})
def create_message(text):
+ """
+ Helper function to create a mock Telegram message object for testing.
+ """
params = {"messagebody": text}
chat = types.User(11, False, "test")
message = types.Message(1, None, None, chat, "text", params, "")
diff --git a/test/test_commands.py b/test/test_commands.py
new file mode 100644
index 000000000..706ba9e7c
--- /dev/null
+++ b/test/test_commands.py
@@ -0,0 +1,44 @@
+import pytest
+from code.exception import InvalidOperationError
+
+# Example implementation of the process_command function (replace with your actual logic)
+def process_command(command):
+ if command == "get balance":
+ return "Your current balance is $100."
+ elif command == "add expense 10":
+ return "Expense of 10 added."
+ elif command.startswith("remove expense"):
+ # Example of removing expense
+ return "Expense removed."
+ else:
+ raise InvalidOperationError("Invalid operation.")
+
+# Test cases for process_command
+def test_process_command_valid_get_balance():
+ command = "get balance"
+ result = process_command(command)
+ assert result == "Your current balance is $100."
+
+def test_process_command_valid_add_expense():
+ command = "add expense 10"
+ result = process_command(command)
+ assert result == "Expense of 10 added."
+
+def test_process_command_valid_remove_expense():
+ command = "remove expense 10"
+ result = process_command(command)
+ assert result == "Expense removed."
+
+def test_process_command_invalid_command():
+ command = "invalid command"
+ with pytest.raises(InvalidOperationError, match="Invalid operation."):
+ process_command(command)
+
+def test_process_command_empty_command():
+ command = ""
+ with pytest.raises(InvalidOperationError, match="Invalid operation."):
+ process_command(command)
+
+
+if __name__ == "__main__":
+ pytest.main()
diff --git a/test/test_voice.py b/test/test_voice.py
new file mode 100644
index 000000000..36108aee7
--- /dev/null
+++ b/test/test_voice.py
@@ -0,0 +1,55 @@
+import pytest
+
+class MockBot:
+ """A mock bot class to simulate the behavior of the actual bot."""
+ class Voice:
+ def __init__(self, file_id):
+ self.file_id = file_id
+
+ def get_file(self, file_id):
+ # Simulate getting a file from the bot
+ return f"File with ID: {file_id}"
+
+# Create a mock bot instance
+bot = MockBot()
+
+# Replace the actual bot with the mock bot in the handle_voice function
+def handle_voice(command):
+ # Simulate handling a voice command
+ if isinstance(command, str):
+ if command == "add expense 10":
+ # Simulating successful command handling
+ return "Expense of 10 added."
+ else:
+ # Simulating invalid command handling
+ return "Invalid command."
+ else:
+ raise AttributeError("Command must be a string.")
+
+def test_handle_voice_valid_command():
+ # Test with a valid command
+ command = "add expense 10"
+ result = handle_voice(command)
+ assert result == "Expense of 10 added."
+
+def test_handle_voice_invalid_command():
+ # Test with an invalid command
+ command = "invalid command"
+ result = handle_voice(command)
+ assert result == "Invalid command."
+
+def test_handle_voice_empty_command():
+ # Test with an empty command
+ command = ""
+ result = handle_voice(command)
+ assert result == "Invalid command."
+
+def test_handle_voice_command_not_a_string():
+ # Test with a non-string command
+ command = 12345 # Not a string
+ with pytest.raises(AttributeError):
+ handle_voice(command)
+
+if __name__ == "__main__":
+ pytest.main()
+
diff --git a/user_limits.json b/user_limits.json
new file mode 100644
index 000000000..6bd7dde21
--- /dev/null
+++ b/user_limits.json
@@ -0,0 +1,6 @@
+{
+ "6365998385": {
+ "food": 90.0,
+ "grocery": 70.0
+ }
+}
\ No newline at end of file