diff --git a/bot/common.py b/bot/common.py index 2c96622..af14957 100644 --- a/bot/common.py +++ b/bot/common.py @@ -34,4 +34,5 @@ class ConversationState(Enum): CPF, BACKLOG, ADD_BACKLOG_ENTRY, - ) = range(26) + NOTIFICATION, + ) = range(27) diff --git a/bot/database_service/firestore_service.py b/bot/database_service/firestore_service.py index beacf17..4e96f9b 100644 --- a/bot/database_service/firestore_service.py +++ b/bot/database_service/firestore_service.py @@ -1,6 +1,7 @@ from bot.database_service.auth import get_db_client from datetime import datetime, timedelta import pytz +from bot.error_handler import DatabaseError class FirestoreService: @@ -14,25 +15,37 @@ def __init__(self, collection_name="users"): # New user setup def new_user_setup(self, telegram_id, sheet_id, telegram_username): - user_ref = self.db.collection(self.collection_name).document(str(telegram_id)) - timestamp = datetime.now(pytz.timezone("Asia/Singapore")) - user_ref.set( - { - "sheet_id": sheet_id, - "datetime_created": timestamp, - "username": telegram_username, - "usage_count": 0, - "last_accessed": timestamp, - "hourly_accessed": timestamp, - "overusage_count": 0, - } - ) + try: + user_ref = self.db.collection(self.collection_name).document( + str(telegram_id) + ) + timestamp = datetime.now(pytz.timezone("Asia/Singapore")) + user_ref.set( + { + "sheet_id": sheet_id, + "datetime_created": timestamp, + "username": telegram_username, + "usage_count": 0, + "last_accessed": timestamp, + "hourly_accessed": timestamp, + "overusage_count": 0, + } + ) + except Exception as e: + raise DatabaseError(message="Error setting up new user", extra_info=str(e)) # Check if user exists def check_if_user_exists(self, telegram_id): - user_ref = self.db.collection(self.collection_name).document(str(telegram_id)) - user_doc = user_ref.get() - return user_doc.exists + try: + user_ref = self.db.collection(self.collection_name).document( + str(telegram_id) + ) + user_doc = user_ref.get() + return user_doc.exists + except Exception as e: + raise DatabaseError( + message="Error checking if user exists", extra_info=str(e) + ) # Get user sheet id def get_user_sheet_id(self, telegram_id, telegram_username): @@ -76,19 +89,18 @@ def get_user_sheet_id(self, telegram_id, telegram_username): ) return user_doc.get("sheet_id") except Exception as e: - raise e - return None + raise DatabaseError( + message="Error getting user sheet id", extra_info=str(e) + ) + raise DatabaseError( + message="User does not exist", extra_info="User does not exist" + ) # Get all user IDs def get_all_user_id(self): - users_ref = self.db.collection(self.collection_name) - user_ids = [int(user.id) for user in users_ref.stream()] - return user_ids - - # Get all sheet IDs - def get_all_sheet_id(self): - users_ref = self.db.collection(self.collection_name) - sheet_ids = [] - for user in users_ref.stream(): - sheet_ids.append(user.get("sheet_id")) - return sheet_ids + try: + users_ref = self.db.collection(self.collection_name) + user_ids = [int(user.id) for user in users_ref.stream()] + return user_ids + except Exception as e: + raise DatabaseError(message="Error getting all user ids", extra_info=str(e)) diff --git a/bot/error_handler.py b/bot/error_handler.py new file mode 100644 index 0000000..4d308af --- /dev/null +++ b/bot/error_handler.py @@ -0,0 +1,28 @@ +import logging + + +logging.basicConfig(level=logging.ERROR, filename='telegram_bot.log', + format='%(asctime)s - %(levelname)s - %(message)s') + +class BaseError(Exception): + """Base class for other exceptions""" + def __init__(self, error_class, message="An error occurred", extra_info=""): + self.message = message + self.extra_info = extra_info + self.error_class = error_class + super().__init__(f"{self.error_class}: {self.message}") + +class GoogleSheetError(BaseError): + """Exception raised for errors in the Google Sheet service.""" + def __init__(self, message="", extra_info=""): + super().__init__("GoogleSheetError", message, extra_info) + +class TelegramBotError(BaseError): + """Exception raised for errors in the Telegram bot operations.""" + def __init__(self, message="", extra_info=""): + super().__init__("TelegramBotError", message, extra_info) + +class DatabaseError(BaseError): + """Exception raised for errors in database operations.""" + def __init__(self, message="", extra_info=""): + super().__init__("DatabaseError", message, extra_info) \ No newline at end of file diff --git a/bot/google_sheet_service.py b/bot/google_sheet_service.py index 1230195..75e76da 100644 --- a/bot/google_sheet_service.py +++ b/bot/google_sheet_service.py @@ -3,6 +3,7 @@ from bot.common import EntryType import os import json +from bot.error_handler import GoogleSheetError GOOGLE_JSON = os.getenv("GOOGLE_JSON") google_service = json.loads(GOOGLE_JSON) @@ -44,23 +45,27 @@ def get_main_dropdown_value(spreadsheet_id, entry_type): range = others_main_range else: range = payment_main_range - results = ( - sheets_api.spreadsheets() - .values() - .get(spreadsheetId=spreadsheet_id, range=range) - .execute() - ) - values = results.get("values", []) + try: + results = ( + sheets_api.spreadsheets() + .values() + .get(spreadsheetId=spreadsheet_id, range=range) + .execute() + ) + values = results.get("values", []) - if entry_type == EntryType.TRANSPORT: - results_list = [] - for sublist in values: - for item in sublist: - results_list.append(item) - return results_list + if entry_type == EntryType.TRANSPORT: + results_list = [] + for sublist in values: + for item in sublist: + results_list.append(item) + return results_list + + return values[0] + except Exception as e: + raise GoogleSheetError(message=f"Error with retrieving values for main dropdown value for entrytype: {entry_type}", extra_info=str(e)) - return values[0] def get_sub_dropdown_value(spreadsheet_id, main_value, entry_type): @@ -69,52 +74,62 @@ def get_sub_dropdown_value(spreadsheet_id, main_value, entry_type): range = others_sub_range else: range = payment_sub_range - results = ( - sheets_api.spreadsheets() - .values() - .batchGet(spreadsheetId=spreadsheet_id, ranges=range) - .execute() - ) - value_ranges = results.get("valueRanges", []) + try: + results = ( + sheets_api.spreadsheets() + .values() + .batchGet(spreadsheetId=spreadsheet_id, ranges=range) + .execute() + ) + + value_ranges = results.get("valueRanges", []) - dropdown = [] - for value in value_ranges: - if value.get("values", []): - if main_value == value.get("values", [])[0][0]: - dropdown.append(value.get("values", [])) - pass + dropdown = [] + for value in value_ranges: + if value.get("values", []): + if main_value == value.get("values", [])[0][0]: + dropdown.append(value.get("values", [])) + pass - flat_list = [item for sublist in dropdown[0] for item in sublist] - return flat_list + flat_list = [item for sublist in dropdown[0] for item in sublist] + return flat_list + except Exception as e: + raise GoogleSheetError(message=f"Error with retrieving values for sub dropdown value for entrytype: {entry_type}", extra_info=str(e)) def update_prev_day(spreadsheet_id, month, first_row, last_row=0): month = month.title() - - if last_row == 0: - last_row = get_last_entered_row(spreadsheet_id, month) - body = {"values": [[f"=SUM(C{first_row}:H{last_row})"]]} - range_name = f"{month}!B{first_row}" - sheets_api.spreadsheets().values().update( - spreadsheetId=spreadsheet_id, - range=range_name, - valueInputOption="USER_ENTERED", - body=body, - ).execute() + try: + if last_row == 0: + last_row = get_last_entered_row(spreadsheet_id, month) + body = {"values": [[f"=SUM(C{first_row}:H{last_row})"]]} + range_name = f"{month}!B{first_row}" + sheets_api.spreadsheets().values().update( + spreadsheetId=spreadsheet_id, + range=range_name, + valueInputOption="USER_ENTERED", + body=body, + ).execute() + except GoogleSheetError as e: + raise e + except Exception as e: + raise GoogleSheetError(message=f"Error with calculating sum for the previous day", extra_info=str(e)) def get_last_entered_row(spreadsheet_id, month): month = month.title() - - result = ( - sheets_api.spreadsheets() - .values() - .get(spreadsheetId=spreadsheet_id, range=f"{month}!A:K") - .execute() - ) - values = result.get("values", []) - return len(values) + try: + result = ( + sheets_api.spreadsheets() + .values() + .get(spreadsheetId=spreadsheet_id, range=f"{month}!A:K") + .execute() + ) + values = result.get("values", []) + return len(values) + except Exception as e: + raise GoogleSheetError(message=f"Error getting the last entered row for the month of {month}", extra_info=str(e)) def create_date(spreadsheet_id, day, month, first_row): @@ -122,12 +137,15 @@ def create_date(spreadsheet_id, day, month, first_row): body = {"values": [[day]]} range_name = f"{month}!A{first_row}" - sheets_api.spreadsheets().values().update( - spreadsheetId=spreadsheet_id, - range=range_name, - valueInputOption="USER_ENTERED", - body=body, - ).execute() + try: + sheets_api.spreadsheets().values().update( + spreadsheetId=spreadsheet_id, + range=range_name, + valueInputOption="USER_ENTERED", + body=body, + ).execute() + except Exception as e: + raise GoogleSheetError(message=f"Fail to create new day for {day} {month}", extra_info=str(e)) def create_entry(spreadsheet_id, month, row_tracker, row_data): @@ -148,30 +166,35 @@ def create_entry(spreadsheet_id, month, row_tracker, row_data): sheet_column_end = "G" data = [price] + remarks_list + [category, payment] - body = {"values": [data]} - range_name = ( - f"{month}!{sheet_column_start}{row_tracker}:{sheet_column_end}{row_tracker}" - ) - sheets_api.spreadsheets().values().update( - spreadsheetId=spreadsheet_id, - range=range_name, - valueInputOption="USER_ENTERED", - body=body, - ).execute() + try: + body = {"values": [data]} + range_name = ( + f"{month}!{sheet_column_start}{row_tracker}:{sheet_column_end}{row_tracker}" + ) + sheets_api.spreadsheets().values().update( + spreadsheetId=spreadsheet_id, + range=range_name, + valueInputOption="USER_ENTERED", + body=body, + ).execute() + except Exception as e: + raise GoogleSheetError(message=f"Fail to create new entry for {data}", extra_info=str(e)) def get_sheet_id_by_title(spreadsheet_id, title_to_find): - sheet_metadata = ( - sheets_api.spreadsheets().get(spreadsheetId=spreadsheet_id).execute() - ) - sheets = sheet_metadata.get("sheets", "") - - for sheet in sheets: - title = sheet.get("properties", {}).get("title") - if title == title_to_find: - return sheet.get("properties", {}).get("sheetId") + try: + sheet_metadata = ( + sheets_api.spreadsheets().get(spreadsheetId=spreadsheet_id).execute() + ) + sheets = sheet_metadata.get("sheets", "") - return None + for sheet in sheets: + title = sheet.get("properties", {}).get("title") + if title == title_to_find: + return sheet.get("properties", {}).get("sheetId") + return None + except Exception as e: + raise GoogleSheetError(message=f"Fail to retrieve sheet id by title: {title_to_find}", extra_info=str(e)) def create_backlog_entry(spreadsheet_id, backlog_day, backlog_month, row_data): @@ -182,177 +205,206 @@ def create_backlog_entry(spreadsheet_id, backlog_day, backlog_month, row_data): payment = row_data[4].strip() backlog_month = backlog_month.title() - day_first_entry_index = get_day_first_entry_index( - spreadsheet_id, backlog_month, backlog_day - ) - row_to_move = int(get_first_row_to_move(spreadsheet_id, backlog_month, backlog_day)) - last_row_to_move = int(get_last_entered_row(spreadsheet_id, backlog_month)) - new_entry_row = row_to_move - sheet_id = get_sheet_id_by_title(spreadsheet_id, backlog_month.title()) + try: + day_first_entry_index = get_day_first_entry_index( + spreadsheet_id, backlog_month, backlog_day + ) + row_to_move = int(get_first_row_to_move(spreadsheet_id, backlog_month, backlog_day)) + last_row_to_move = int(get_last_entered_row(spreadsheet_id, backlog_month)) + new_entry_row = row_to_move + sheet_id = get_sheet_id_by_title(spreadsheet_id, backlog_month.title()) - if row_to_move is None: - new_entry_row = last_row_to_move + 1 - else: - requests = [ - { - "copyPaste": { - "source": { - "sheetId": sheet_id, - "startRowIndex": row_to_move - 1, - "endRowIndex": last_row_to_move, - "startColumnIndex": start_column_index, - "endColumnIndex": end_column_index, - }, - "destination": { - "sheetId": sheet_id, - "startRowIndex": row_to_move, - "endRowIndex": last_row_to_move + 1, - "startColumnIndex": start_column_index, - "endColumnIndex": end_column_index, - }, - "pasteType": "PASTE_NORMAL", - "pasteOrientation": "NORMAL", + if row_to_move is None: + new_entry_row = last_row_to_move + 1 + else: + requests = [ + { + "copyPaste": { + "source": { + "sheetId": sheet_id, + "startRowIndex": row_to_move - 1, + "endRowIndex": last_row_to_move, + "startColumnIndex": start_column_index, + "endColumnIndex": end_column_index, + }, + "destination": { + "sheetId": sheet_id, + "startRowIndex": row_to_move, + "endRowIndex": last_row_to_move + 1, + "startColumnIndex": start_column_index, + "endColumnIndex": end_column_index, + }, + "pasteType": "PASTE_NORMAL", + "pasteOrientation": "NORMAL", + } } - } - ] - - sheets_api.spreadsheets().batchUpdate( - spreadsheetId=spreadsheet_id, body={"requests": requests} - ).execute() - - clear_range = f"{backlog_month}!A{new_entry_row}:K{new_entry_row}" - sheets_api.spreadsheets().values().clear( - spreadsheetId=spreadsheet_id, range=clear_range - ).execute() - - if day_first_entry_index is None: - create_date(spreadsheet_id, backlog_day, backlog_month, new_entry_row) - day_first_entry_index = new_entry_row - - data = [price, remarks, category, payment] - sheet_column_start = "H" - sheet_column_end = "K" - if entry_type == EntryType.TRANSPORT: - remarks_list = [remark.strip() for remark in remarks.split(",")] - sheet_column_start = "C" - sheet_column_end = "G" - data = [price] + remarks_list + [category, payment] - - body = {"values": [data]} - range_name = f"{backlog_month}!{sheet_column_start}{new_entry_row}:{sheet_column_end}{new_entry_row}" - sheets_api.spreadsheets().values().update( - spreadsheetId=spreadsheet_id, - range=range_name, - valueInputOption="USER_ENTERED", - body=body, - ).execute() - - update_prev_day(spreadsheet_id, backlog_month, day_first_entry_index, new_entry_row) + ] + try: + sheets_api.spreadsheets().batchUpdate( + spreadsheetId=spreadsheet_id, body={"requests": requests} + ).execute() + + clear_range = f"{backlog_month}!A{new_entry_row}:K{new_entry_row}" + sheets_api.spreadsheets().values().clear( + spreadsheetId=spreadsheet_id, range=clear_range + ).execute() + + update_prev_day(spreadsheet_id, backlog_month, day_first_entry_index, new_entry_row) + except GoogleSheetError as e: + raise e + except Exception as e: + raise GoogleSheetError(message=f"Fail to move existing entry down", extra_info=str(e)) + + if day_first_entry_index is None: + create_date(spreadsheet_id, backlog_day, backlog_month, new_entry_row) + day_first_entry_index = new_entry_row + + data = [price, remarks, category, payment] + sheet_column_start = "H" + sheet_column_end = "K" + if entry_type == EntryType.TRANSPORT: + remarks_list = [remark.strip() for remark in remarks.split(",")] + sheet_column_start = "C" + sheet_column_end = "G" + data = [price] + remarks_list + [category, payment] + + try: + body = {"values": [data]} + range_name = f"{backlog_month}!{sheet_column_start}{new_entry_row}:{sheet_column_end}{new_entry_row}" + sheets_api.spreadsheets().values().update( + spreadsheetId=spreadsheet_id, + range=range_name, + valueInputOption="USER_ENTERED", + body=body, + ).execute() + except Exception as e: + raise GoogleSheetError(message=f"Fail to create backlog entry", extra_info=str(e)) + except GoogleSheetError as e: + raise e + except Exception as e: + raise GoogleSheetError(message=f"Fail to create backlog entry", extra_info=str(e)) def get_trackers(spreadsheet_id): - result = ( - sheets_api.spreadsheets() - .values() - .get(spreadsheetId=spreadsheet_id, range=tracker_range) - .execute() - ) - values = result.get("values", []) - if values: - return values[0] - else: - return + try: + result = ( + sheets_api.spreadsheets() + .values() + .get(spreadsheetId=spreadsheet_id, range=tracker_range) + .execute() + ) + values = result.get("values", []) + if values: + return values[0] + else: + return + except Exception as e: + raise GoogleSheetError(message=f"Fail to retrieve trackers", extra_info=str(e)) -def update_rows(spreadsheet_id, day, new_row, first_row): - values = [[day] + [new_row] * 2 + [first_row]] - range_name = tracker_range - body = {"values": values} - request = ( - sheets_api.spreadsheets() - .values() - .update( - spreadsheetId=spreadsheet_id, - range=range_name, - valueInputOption="USER_ENTERED", - body=body, +def update_tracker_values(spreadsheet_id, day, new_row, first_row): + try: + values = [[day] + [new_row] * 2 + [first_row]] + range_name = tracker_range + body = {"values": values} + request = ( + sheets_api.spreadsheets() + .values() + .update( + spreadsheetId=spreadsheet_id, + range=range_name, + valueInputOption="USER_ENTERED", + body=body, + ) ) - ) - request.execute() + request.execute() + except Exception as e: + raise GoogleSheetError(message=f"Fail to update values for tracker", extra_info=str(e)) def row_incremental(spreadsheet_id, entry_type): range_name = tracker_range - response = ( - sheets_api.spreadsheets() - .values() - .get(spreadsheetId=spreadsheet_id, range=range_name, majorDimension="ROWS") - .execute() - ) - - values = response.get("values", []) - if values: - row_values = values[0] - if entry_type == EntryType.OTHERS: - row_values[1] = str(int(row_values[1]) + 1) # Increment others count - elif entry_type == EntryType.TRANSPORT: - row_values[2] = str(int(row_values[2]) + 1) # Increment transport count + try: + response = ( + sheets_api.spreadsheets() + .values() + .get(spreadsheetId=spreadsheet_id, range=range_name, majorDimension="ROWS") + .execute() + ) - body = {"values": [row_values]} - sheets_api.spreadsheets().values().update( - spreadsheetId=spreadsheet_id, - range=range_name, - valueInputOption="USER_ENTERED", - body=body, - ).execute() + values = response.get("values", []) + if values: + row_values = values[0] + if entry_type == EntryType.OTHERS: + row_values[1] = str(int(row_values[1]) + 1) # Increment others count + elif entry_type == EntryType.TRANSPORT: + row_values[2] = str(int(row_values[2]) + 1) # Increment transport count + + body = {"values": [row_values]} + sheets_api.spreadsheets().values().update( + spreadsheetId=spreadsheet_id, + range=range_name, + valueInputOption="USER_ENTERED", + body=body, + ).execute() + + except Exception as e: + raise GoogleSheetError(message=f"Fail to do tracker incremental update for {entry_type}", extra_info=str(e)) def row_incremental_all(spreadsheet_id): range_name = tracker_range - response = ( - sheets_api.spreadsheets() - .values() - .get(spreadsheetId=spreadsheet_id, range=range_name, majorDimension="ROWS") - .execute() - ) - - values = response.get("values", []) - if values: - row_values = values[0] - row_values[1] = str(int(row_values[1]) + 1) # Increment others count - row_values[2] = str(int(row_values[2]) + 1) # Increment transport count - row_values[3] = str(int(row_values[3]) + 1) # Increment first row count - - body = {"values": [row_values]} - sheets_api.spreadsheets().values().update( - spreadsheetId=spreadsheet_id, - range=range_name, - valueInputOption="USER_ENTERED", - body=body, - ).execute() - + try: + response = ( + sheets_api.spreadsheets() + .values() + .get(spreadsheetId=spreadsheet_id, range=range_name, majorDimension="ROWS") + .execute() + ) + values = response.get("values", []) + if values: + row_values = values[0] + row_values[1] = str(int(row_values[1]) + 1) # Increment others count + row_values[2] = str(int(row_values[2]) + 1) # Increment transport count + row_values[3] = str(int(row_values[3]) + 1) # Increment first row count + + body = {"values": [row_values]} + sheets_api.spreadsheets().values().update( + spreadsheetId=spreadsheet_id, + range=range_name, + valueInputOption="USER_ENTERED", + body=body, + ).execute() + except Exception as e: + raise GoogleSheetError(message=f"Fail to do tracker incremental update for all values", extra_info=str(e)) + +# This is used to retrieve the first quick add settings when there is only one def get_quick_add_settings(spreadsheet_id, entry_type): range_name = quick_add_range - response = ( - sheets_api.spreadsheets() - .values() - .get(spreadsheetId=spreadsheet_id, range=range_name, majorDimension="ROWS") - .execute() - ) - - values = response.get("values", []) - if values: - if entry_type == EntryType.TRANSPORT: - transport_payment = values[0][0] if len(values[0]) > 0 else None - transport_type = values[0][1] if len(values[0]) > 1 else None - return transport_payment, transport_type - else: - others_payment = values[0][2] if len(values[0]) > 2 else None - others_type = values[0][3] if len(values[0]) > 3 else None - return others_payment, others_type + try: + response = ( + sheets_api.spreadsheets() + .values() + .get(spreadsheetId=spreadsheet_id, range=range_name, majorDimension="ROWS") + .execute() + ) - return None + values = response.get("values", []) + if values: + if entry_type == EntryType.TRANSPORT: + transport_payment = values[0][0] if len(values[0]) > 0 else None + transport_type = values[0][1] if len(values[0]) > 1 else None + return transport_payment, transport_type + else: + others_payment = values[0][2] if len(values[0]) > 2 else None + others_type = values[0][3] if len(values[0]) > 3 else None + return others_payment, others_type + + return None + + except Exception as e: + raise GoogleSheetError(message=f"Fail to do retrieve quick add settings for {entry_type}", extra_info=str(e)) def update_quick_add_settings(spreadsheet_id, entry_type, payment, type): @@ -362,27 +414,29 @@ def update_quick_add_settings(spreadsheet_id, entry_type, payment, type): else: range_1 = tracker_others_1 range_2 = tracker_others_2 - - last_row = ( - sheets_api.spreadsheets() - .values() - .get( - spreadsheetId=spreadsheet_id, - range=f"Tracker!{range_1}:{range_2}", + try: + last_row = ( + sheets_api.spreadsheets() + .values() + .get( + spreadsheetId=spreadsheet_id, + range=f"Tracker!{range_1}:{range_2}", + ) + .execute() + .get("values", []) ) - .execute() - .get("values", []) - ) - last_row = len(last_row) + 1 - range_name = f"Tracker!{range_1}{last_row}:{range_2}{last_row}" - new_row = [payment, type] - body = {"values": [new_row]} - sheets_api.spreadsheets().values().update( - spreadsheetId=spreadsheet_id, - range=range_name, - valueInputOption="USER_ENTERED", - body=body, - ).execute() + last_row = len(last_row) + 1 + range_name = f"Tracker!{range_1}{last_row}:{range_2}{last_row}" + new_row = [payment, type] + body = {"values": [new_row]} + sheets_api.spreadsheets().values().update( + spreadsheetId=spreadsheet_id, + range=range_name, + valueInputOption="USER_ENTERED", + body=body, + ).execute() + except Exception as e: + raise GoogleSheetError(message=f"Fail to do update quick add settings for {entry_type}", extra_info=str(e)) def get_quick_add_list(spreadsheet_id, entry_type): @@ -390,115 +444,139 @@ def get_quick_add_list(spreadsheet_id, entry_type): range_name = quick_transport_range else: range_name = quick_others_range - response = ( - sheets_api.spreadsheets() - .values() - .get(spreadsheetId=spreadsheet_id, range=range_name) - .execute() - ) + try: + response = ( + sheets_api.spreadsheets() + .values() + .get(spreadsheetId=spreadsheet_id, range=range_name) + .execute() + ) - values = response.get("values", []) - settings_list = [] - for other in values: - merged_str = ", ".join(other) - settings_list.append(merged_str) - return settings_list + values = response.get("values", []) + settings_list = [] + for other in values: + merged_str = ", ".join(other) + settings_list.append(merged_str) + return settings_list + except Exception as e: + raise GoogleSheetError(message=f"Fail to do retrieve quick add settings list for {entry_type}", extra_info=str(e)) def get_day_transaction(spreadsheet_id, month, date): month = month.title() - result = ( - sheets_api.spreadsheets() - .values() - .get(spreadsheetId=spreadsheet_id, range=f"{month}!A:A") - .execute() - ) - values = result.get("values", []) - flat_list = [item for sublist in values for item in sublist or [""]] - if (date) not in flat_list: - return None, None, None - first_row = flat_list.index(date) - first_row += 1 - last_row = ( - flat_list.index(str(int(date) + 1)) - if str(int(date) + 1) in flat_list - else first_row + 10 - ) - result = ( - sheets_api.spreadsheets() - .values() - .batchGet( - spreadsheetId=spreadsheet_id, - ranges=[ - f"{month}!B{first_row}", - f"{month}!C{first_row}:G{last_row}", - f"{month}!H{first_row}:K{last_row}", - ], - ) - .execute() - ) - value_ranges = result.get("valueRanges", []) - total_spend = value_ranges[0].get("values", []) if len(value_ranges) > 0 else [] - transport_values = ( - value_ranges[1].get("values", []) if len(value_ranges) > 0 else [] - ) - other_values = value_ranges[2].get("values", []) if len(value_ranges) > 1 else [] - - return total_spend, transport_values, other_values + try: + try: + result = ( + sheets_api.spreadsheets() + .values() + .get(spreadsheetId=spreadsheet_id, range=f"{month}!A:A") + .execute() + ) + values = result.get("values", []) + flat_list = [item for sublist in values for item in sublist or [""]] + if (date) not in flat_list: + return None, None, None + first_row = flat_list.index(date) + first_row += 1 + last_row = ( + flat_list.index(str(int(date) + 1)) + if str(int(date) + 1) in flat_list + else first_row + 10 + ) + except Exception as e: + raise GoogleSheetError(message=f"Fail to retrieve the starting and ending rows for {month}", extra_info=str(e)) + + try: + result = ( + sheets_api.spreadsheets() + .values() + .batchGet( + spreadsheetId=spreadsheet_id, + ranges=[ + f"{month}!B{first_row}", + f"{month}!C{first_row}:G{last_row}", + f"{month}!H{first_row}:K{last_row}", + ], + ) + .execute() + ) + value_ranges = result.get("valueRanges", []) + total_spend = value_ranges[0].get("values", []) if len(value_ranges) > 0 else [] + transport_values = ( + value_ranges[1].get("values", []) if len(value_ranges) > 0 else [] + ) + other_values = value_ranges[2].get("values", []) if len(value_ranges) > 1 else [] + + return total_spend, transport_values, other_values + except Exception as e: + raise GoogleSheetError(message=f"Fail to retrieve the transaction for {date} {month}", extra_info=str(e)) + except GoogleSheetError as e: + raise e + except Exception as e: + raise GoogleSheetError(message=f"Fail to retrieve transaction for {date} {month}", extra_info=str(e)) def get_first_row_to_move(spreadsheet_id, month, date): month = month.title() - - result = ( - sheets_api.spreadsheets() - .values() - .get(spreadsheetId=spreadsheet_id, range=f"{month}!A:A") - .execute() - ) - values = result.get("values", []) - flat_list = [item for sublist in values for item in sublist or [""]] - next_date = str(int(date) + 1) - while next_date not in flat_list and int(next_date) < 32: - next_date = str(int(next_date) + 1) - try: - last_row = flat_list.index(next_date) - except ValueError: - return get_last_entered_row(spreadsheet_id, month) + 1 - return last_row + 1 + result = ( + sheets_api.spreadsheets() + .values() + .get(spreadsheetId=spreadsheet_id, range=f"{month}!A:A") + .execute() + ) + values = result.get("values", []) + flat_list = [item for sublist in values for item in sublist or [""]] + next_date = str(int(date) + 1) + while next_date not in flat_list and int(next_date) < 32: + next_date = str(int(next_date) + 1) + + try: + last_row = flat_list.index(next_date) + except ValueError: + return get_last_entered_row(spreadsheet_id, month) + 1 + return last_row + 1 + except GoogleSheetError as e: + raise e + except Exception as e: + raise GoogleSheetError(message=f"Fail to find the first row to move", extra_info=str(e)) def get_day_first_entry_index(spreadsheet_id, month, date): month = month.title() + try: + result = ( + sheets_api.spreadsheets() + .values() + .get(spreadsheetId=spreadsheet_id, range=f"{month}!A:A") + .execute() + ) + values = result.get("values", []) + flat_list = [item for sublist in values for item in sublist or [""]] + if (date) not in flat_list: + return None + first_row = flat_list.index(date) + first_row += 1 - result = ( - sheets_api.spreadsheets() - .values() - .get(spreadsheetId=spreadsheet_id, range=f"{month}!A:A") - .execute() - ) - values = result.get("values", []) - flat_list = [item for sublist in values for item in sublist or [""]] - if (date) not in flat_list: - return None - first_row = flat_list.index(date) - first_row += 1 - - return first_row + return first_row + except Exception as e: + raise GoogleSheetError(message=f"Fail to retrieve the index for first entry", extra_info=str(e)) def get_work_place(spreadsheet_id): - result = ( - sheets_api.spreadsheets() - .values() - .get(spreadsheetId=spreadsheet_id, range=income_range) - .execute() - ) - values = result.get("values", []) - flattened_list = [item for sublist in values for item in sublist] + try: + result = ( + sheets_api.spreadsheets() + .values() + .get(spreadsheetId=spreadsheet_id, range=income_range) + .execute() + ) + values = result.get("values", []) + flattened_list = [item for sublist in values for item in sublist] - return flattened_list + return flattened_list + except Exception as e: + raise GoogleSheetError(message=f"Fail to retrieve income sources", extra_info=str(e)) def update_income(spreadsheet_id, month, row_data): @@ -508,44 +586,48 @@ def update_income(spreadsheet_id, month, row_data): data_r = [row_data[-1]] body_mo = {"values": [data_mo]} body_r = {"values": [data_r]} + try: + result = ( + sheets_api.spreadsheets() + .values() + .get(spreadsheetId=spreadsheet_id, range=f"{month}!M5:M10") + .execute() + ) + values = result.get("values", []) + last_row = len(values) + 5 + if last_row > 10: + return False - result = ( - sheets_api.spreadsheets() - .values() - .get(spreadsheetId=spreadsheet_id, range=f"{month}!M5:M10") - .execute() - ) - values = result.get("values", []) - last_row = len(values) + 5 - if last_row > 10: - return False - - range_name_mo = f"{month}!M{last_row}:O{last_row}" - range_name_r = f"{month}!R{last_row}:R{last_row}" - sheets_api.spreadsheets().values().update( - spreadsheetId=spreadsheet_id, - range=range_name_mo, - valueInputOption="USER_ENTERED", - body=body_mo, - ).execute() - - body_r = {"values": [data_r]} - sheets_api.spreadsheets().values().update( - spreadsheetId=spreadsheet_id, - range=range_name_r, - valueInputOption="USER_ENTERED", - body=body_r, - ).execute() - return True + range_name_mo = f"{month}!M{last_row}:O{last_row}" + range_name_r = f"{month}!R{last_row}:R{last_row}" + sheets_api.spreadsheets().values().update( + spreadsheetId=spreadsheet_id, + range=range_name_mo, + valueInputOption="USER_ENTERED", + body=body_mo, + ).execute() + body_r = {"values": [data_r]} + sheets_api.spreadsheets().values().update( + spreadsheetId=spreadsheet_id, + range=range_name_r, + valueInputOption="USER_ENTERED", + body=body_r, + ).execute() + return True + except Exception as e: + raise GoogleSheetError(message=f"Fail to update income for {month}", extra_info=str(e)) def get_overall(spreadsheet_id, month): month = month.title() + try: + result = ( + sheets_api.spreadsheets() + .values() + .get(spreadsheetId=spreadsheet_id, range=f"{month}{overall_range}") + .execute() + ) + return result.get("values", []) - result = ( - sheets_api.spreadsheets() - .values() - .get(spreadsheetId=spreadsheet_id, range=f"{month}{overall_range}") - .execute() - ) - return result.get("values", []) + except Exception as e: + raise GoogleSheetError(message=f"Fail to retrieve overall for {month}", extra_info=str(e)) \ No newline at end of file diff --git a/bot/sheet_service/sheets_services.py b/bot/sheet_service/sheets_services.py index 83621f4..82648a7 100644 --- a/bot/sheet_service/sheets_services.py +++ b/bot/sheet_service/sheets_services.py @@ -7,8 +7,8 @@ # sheets_api.py from googleapiclient.discovery import build from bot.common import EntryType -from bot.google_sheet_service.auth import get_credentials -from bot.google_sheet_service.sheets_range import * +from bot.sheet_service.auth import get_credentials +from bot.sheet_service.sheets_range import * class GoogleSheetsClient: diff --git a/bot/telegram_bot.py b/bot/telegram_bot.py index 89ddea9..30b1de9 100644 --- a/bot/telegram_bot.py +++ b/bot/telegram_bot.py @@ -19,6 +19,8 @@ from bot.database_service import firestore_service import bot.utils as utils +from bot.error_handler import TelegramBotError, GoogleSheetError, DatabaseError + db = firestore_service.FirestoreService() timezone = pytz.timezone("Asia/Singapore") MASTER_TELE_ID = os.environ.get("MASTER_TELE_ID") @@ -27,18 +29,28 @@ def get_category_text(sheet_id, entry_type): msg = "" markup_list = [] - if entry_type == EntryType.TRANSPORT: - msg = DEFAULT_TRANSPORT_TEXT - markup_list = gs.get_main_dropdown_value(sheet_id, EntryType.TRANSPORT) - elif entry_type == EntryType.OTHERS: - msg = DEFAULT_CATEGORY_TEXT - markup_list = gs.get_main_dropdown_value(sheet_id, EntryType.OTHERS) - return msg, markup_list + try: + if entry_type == EntryType.TRANSPORT: + msg = DEFAULT_TRANSPORT_TEXT + markup_list = gs.get_main_dropdown_value(sheet_id, EntryType.TRANSPORT) + elif entry_type == EntryType.OTHERS: + msg = DEFAULT_CATEGORY_TEXT + markup_list = gs.get_main_dropdown_value(sheet_id, EntryType.OTHERS) + return msg, markup_list + except GoogleSheetError as e: + raise e + except Exception as e: + raise TelegramBotError(message="Error getting category text", extra_info=str(e)) def get_payment_text(sheet_id): - payment_list = gs.get_main_dropdown_value(sheet_id, "Payment") - return payment_list + try: + payment_list = gs.get_main_dropdown_value(sheet_id, "Payment") + return payment_list + except GoogleSheetError as e: + raise e + except Exception as e: + raise TelegramBotError(message="Error getting payment text", extra_info=str(e)) def start(update, context): @@ -64,7 +76,7 @@ def start(update, context): update.message.reply_text( ERROR_TEXT + "\nError:\n" + utils.sanitize_error_message(str(e)) ) - return ConversationHandler.END + return ConversationHandler.END def set_up(update, context) -> int: @@ -96,25 +108,33 @@ def set_up(update, context) -> int: gs.update_prev_day(sheet_id, prev_month, first_row) new_row = gs.get_last_entered_row(sheet_id, month) first_row = new_row + 1 - gs.update_rows(sheet_id, day, new_row, first_row) + gs.update_tracker_values(sheet_id, day, new_row, first_row) gs.create_date(sheet_id, day, month, first_row) elif day == 1: new_row = 4 first_row = 5 - gs.update_rows(sheet_id, day, new_row, first_row) + gs.update_tracker_values(sheet_id, day, new_row, first_row) gs.create_date(sheet_id, day, month, first_row) else: # New sheet new_row = 4 first_row = 5 - gs.update_rows( + gs.update_tracker_values( sheet_id, day, new_row, first_row ) # New users start from row 5 gs.create_date(sheet_id, day, month, first_row) update.message.reply_text(SUCCESS_LINK_TEXT) - return ConversationHandler.END + + except GoogleSheetError as e: + update.message.reply_text( + ERROR_TEXT + "\nError:\n" + utils.sanitize_error_message(str(e)) + ) + except DatabaseError as e: + update.message.reply_text( + ERROR_TEXT + "\nError:\n" + utils.sanitize_error_message(str(e)) + ) except Exception as e: update.message.reply_text(GSHEET_ERROR_TEXT) - return ConversationHandler.END + return ConversationHandler.END else: update.message.reply_text(GSHEET_WRONG_TEXT) return CS.SET_UP @@ -189,7 +209,7 @@ def config_handler(update, context) -> int: update.callback_query.message.reply_text( ERROR_TEXT + "\nError:\n" + utils.sanitize_error_message(str(e)) ) - return ConversationHandler.END + return ConversationHandler.END def config_setup(update, context) -> int: @@ -207,7 +227,6 @@ def config_setup(update, context) -> int: ) return CS.CONFIG_CATEGORY except Exception as e: - update.callback_query.message.reply_text( ERROR_TEXT + "\nError:\n" + utils.sanitize_error_message(str(e)) ) @@ -244,7 +263,6 @@ def config_category(update, context) -> int: ) return CS.CONFIG_SUBCATEGORY except Exception as e: - update.callback_query.reply_text( ERROR_TEXT + "\nError:\n" + utils.sanitize_error_message(str(e)) ) @@ -269,7 +287,6 @@ def config_subcategory(update, context) -> int: ) return CS.CONFIG_PAYMENT except Exception as e: - update.callback_query.message.reply_text( ERROR_TEXT + "\nError:\n" + utils.sanitize_error_message(str(e)) ) @@ -291,7 +308,6 @@ def config_payment(update, context) -> int: ) return CS.CONFIG_SUBPAYMENT except Exception as e: - update.callback_query.message.reply_text( ERROR_TEXT + "\nError:\n" + utils.sanitize_error_message(str(e)) ) @@ -319,7 +335,6 @@ def config_subpayment(update, context) -> int: ) return ConversationHandler.END except Exception as e: - update.callback_query.message.reply_text( ERROR_TEXT + "\nError:\n" + utils.sanitize_error_message(str(e)) ) @@ -330,14 +345,22 @@ def add_entry(update, context): context.user_data.clear() telegram_id = update.effective_user.id telegram_username = update.effective_user.username - context.user_data["sheet_id"] = db.get_user_sheet_id(telegram_id, telegram_username) - update.message.reply_text( - ENTRY_TYPE_TEXT, - reply_markup=utils.create_inline_markup( - [entry_type.value for entry_type in EntryType] - ), - ) - return CS.ENTRY + try: + context.user_data["sheet_id"] = db.get_user_sheet_id( + telegram_id, telegram_username + ) + update.message.reply_text( + ENTRY_TYPE_TEXT, + reply_markup=utils.create_inline_markup( + [entry_type.value for entry_type in EntryType] + ), + ) + return CS.ENTRY + except Exception as e: + update.message.reply_text( + ERROR_TEXT + "\nError:\n" + utils.sanitize_error_message(str(e)) + ) + return ConversationHandler.END def entry(update, context) -> int: @@ -375,18 +398,18 @@ def remarks(update: Update, context) -> int: if reply.count(",") != 1: update.message.reply_text(TRANSPORT_DEFAULT_TEXT) return CS.REMARKS - msg, markup_list = get_category_text(sheet_id, entry_type) + try: + msg, markup_list = get_category_text(sheet_id, entry_type) update.message.reply_text( msg, reply_markup=utils.create_inline_markup(markup_list) ) return CS.CATEGORY except Exception as e: - update.message.reply_text( ERROR_TEXT + "\nError:\n" + utils.sanitize_error_message(str(e)) ) - return ConversationHandler.END + return ConversationHandler.END def category(update, context) -> int: @@ -429,11 +452,10 @@ def category(update, context) -> int: ) return CS.PAYMENT except Exception as e: - update.callback_query.reply_text( ERROR_TEXT + "\nError:\n" + utils.sanitize_error_message(str(e)) ) - return ConversationHandler.END + return ConversationHandler.END def subcategory(update, context) -> int: @@ -441,13 +463,13 @@ def subcategory(update, context) -> int: sheet_id = context.user_data["sheet_id"] entry_type = context.user_data["entry_type"] update.callback_query.answer() - if reply == BACK_TEXT: - msg, markup_list = get_category_text(sheet_id, entry_type) - update.callback_query.edit_message_text( - msg, reply_markup=utils.create_inline_markup(markup_list) - ) - return CS.CATEGORY try: + if reply == BACK_TEXT: + msg, markup_list = get_category_text(sheet_id, entry_type) + update.callback_query.edit_message_text( + msg, reply_markup=utils.create_inline_markup(markup_list) + ) + return CS.CATEGORY context.user_data["category"] = f'{context.user_data["category"]} - {reply}' update.callback_query.edit_message_text( f'Category type: {context.user_data["category"]}', reply_markup=None @@ -458,11 +480,10 @@ def subcategory(update, context) -> int: ) return CS.PAYMENT except Exception as e: - update.callback_query.message.reply_text( ERROR_TEXT + "\nError:\n" + utils.sanitize_error_message(str(e)) ) - return ConversationHandler.END + return ConversationHandler.END def payment(update, context) -> int: @@ -490,26 +511,26 @@ def payment(update, context) -> int: else: log_transaction(context.user_data, update) update.callback_query.message.reply_text("Transaction logged.") - return ConversationHandler.END except Exception as e: - update.callback_query.message.reply_text( ERROR_TEXT + "\nError:\n" + utils.sanitize_error_message(str(e)) ) - return ConversationHandler.END + return ConversationHandler.END def subpayment(update, context) -> int: reply = update.callback_query.data update.callback_query.answer() sheet_id = context.user_data["sheet_id"] - if reply == BACK_TEXT: - payment_list = get_payment_text(sheet_id) - update.callback_query.edit_message_text( - DEFAULT_PAYMENT_TEXT, reply_markup=utils.create_inline_markup(payment_list) - ) - return CS.PAYMENT + try: + if reply == BACK_TEXT: + payment_list = get_payment_text(sheet_id) + update.callback_query.edit_message_text( + DEFAULT_PAYMENT_TEXT, + reply_markup=utils.create_inline_markup(payment_list), + ) + return CS.PAYMENT context.user_data["payment"] = f'{context.user_data["payment"]} - {reply}' update.callback_query.edit_message_text( f'Payment type: {context.user_data["payment"]}', reply_markup=None @@ -519,14 +540,11 @@ def subpayment(update, context) -> int: else: log_transaction(context.user_data, update) update.callback_query.message.reply_text("Transaction logged.") - return ConversationHandler.END - except Exception as e: - update.callback_query.message.reply_text( ERROR_TEXT + "\nError:\n" + utils.sanitize_error_message(str(e)) ) - return ConversationHandler.END + return ConversationHandler.END def log_transaction(user_data, update): @@ -553,46 +571,51 @@ def log_transaction(user_data, update): row_data = [entry_type, price, remarks, category, payment] msg = "" - # start new date if date elapsed - if day_tracker != day: - msg = f"New entry for {day} {month}" - prev_month = month - # this should fix the bug regarding if first day of month not keyed in, but not tested - if day == 1 | day < day_tracker: - prev_month = (current_datetime - dt.timedelta(days=1)).strftime("%b") - # update prev day - msg = f"{msg}\nCreating sum for day {day_tracker}" - gs.update_prev_day(sheet_id, prev_month, first_row) - if day == 1 | day < day_tracker: - new_row = 4 - first_row = 5 - gs.update_rows(sheet_id, 1, new_row, first_row) - else: - new_row = gs.get_last_entered_row(sheet_id, month) + try: + # start new date if date elapsed + if day_tracker != day: + msg = f"New entry for {day} {month}" + prev_month = month + # this should fix the bug regarding if first day of month not keyed in, but not tested + if day == 1 | day < day_tracker: + prev_month = (current_datetime - dt.timedelta(days=1)).strftime("%b") + # update prev day + msg = f"{msg}\nCreating sum for day {day_tracker}" + gs.update_prev_day(sheet_id, prev_month, first_row) + if day == 1 | day < day_tracker: + new_row = 4 + first_row = 5 + gs.update_tracker_values(sheet_id, 1, new_row, first_row) + else: + new_row = gs.get_last_entered_row(sheet_id, month) + first_row = new_row + 1 + gs.update_tracker_values(sheet_id, day, new_row, first_row) + if update.callback_query and update.callback_query.message: + update.callback_query.message.reply_text(msg) + elif update.message: + update.message.reply_text(msg) + + transport_row_tracker = new_row first_row = new_row + 1 - gs.update_rows(sheet_id, day, new_row, first_row) - if update.callback_query and update.callback_query.message: - update.callback_query.message.reply_text(msg) - elif update.message: - update.message.reply_text(msg) - - transport_row_tracker = new_row - first_row = new_row + 1 - other_row_tracker = new_row - # enter date into cell - gs.create_date(sheet_id, day, month, first_row) + other_row_tracker = new_row + # enter date into cell + gs.create_date(sheet_id, day, month, first_row) - # update row + 1 - gs.row_incremental(sheet_id, entry_type) - if entry_type == EntryType.TRANSPORT: - transport_row_tracker += 1 - else: - other_row_tracker += 1 - # create entry - if entry_type == EntryType.TRANSPORT: - gs.create_entry(sheet_id, month, transport_row_tracker, row_data) - else: - gs.create_entry(sheet_id, month, other_row_tracker, row_data) + # update row + 1 + gs.row_incremental(sheet_id, entry_type) + if entry_type == EntryType.TRANSPORT: + transport_row_tracker += 1 + else: + other_row_tracker += 1 + # create entry + if entry_type == EntryType.TRANSPORT: + gs.create_entry(sheet_id, month, transport_row_tracker, row_data) + else: + gs.create_entry(sheet_id, month, other_row_tracker, row_data) + except GoogleSheetError as e: + raise e + except Exception as e: + raise TelegramBotError(message="Error logging transaction", extra_info=str(e)) def backlog_transaction(user_data, update): @@ -605,20 +628,27 @@ def backlog_transaction(user_data, update): current_datetime = dt.datetime.now(timezone) month = current_datetime.strftime("%b") - # if backlog month is current month, need to move all one down - if backlog_month.title() == month: - gs.row_incremental_all(sheet_id) - - # user input data - entry_type = user_data["entry_type"] - payment = user_data["payment"] - price = user_data["price"] - category = user_data["category"] - remarks = user_data["remarks"] - row_data = [entry_type, price, remarks, category, payment] - - # create backlog entry - gs.create_backlog_entry(sheet_id, backlog_day, backlog_month, row_data) + try: + # if backlog month is current month, need to move all one down + if backlog_month.title() == month: + gs.row_incremental_all(sheet_id) + + # user input data + entry_type = user_data["entry_type"] + payment = user_data["payment"] + price = user_data["price"] + category = user_data["category"] + remarks = user_data["remarks"] + row_data = [entry_type, price, remarks, category, payment] + + # create backlog entry + gs.create_backlog_entry(sheet_id, backlog_day, backlog_month, row_data) + except GoogleSheetError as e: + raise e + except Exception as e: + raise TelegramBotError( + message="Error logging backlog transaction", extra_info=str(e) + ) def cancel(update, context): @@ -640,33 +670,39 @@ def add_transport(update, context): context.user_data["sheet_id"], EntryType.TRANSPORT ) except Exception as e: - update.callback_query.message.reply_text( ERROR_TEXT + "\nError:\n" + utils.sanitize_error_message(str(e)) ) return ConversationHandler.END + if not setting_list or not setting_list[0]: update.message.reply_text(QUICK_SETUP_TRANSPORT) return ConversationHandler.END else: - setting_list = gs.get_quick_add_list( - context.user_data["sheet_id"], context.user_data["entry_type"] - ) - if len(setting_list) == 1: - payment, category = setting_list[0].split(",") - context.user_data["payment"] = payment - context.user_data["category"] = category - update.message.reply_text( - f"Quick Add Transport\nDefault Payment: {payment}\nDefault Type: {category}" - + "\n\nPlease enter as follow: [price],[start],[end]\n e.g. 2.11, Home, Work" + try: + setting_list = gs.get_quick_add_list( + context.user_data["sheet_id"], context.user_data["entry_type"] ) - return CS.QUICK_ADD - else: + if len(setting_list) == 1: + payment, category = setting_list[0].split(",") + context.user_data["payment"] = payment + context.user_data["category"] = category + update.message.reply_text( + f"Quick Add Transport\nDefault Payment: {payment}\nDefault Type: {category}" + + "\n\nPlease enter as follow: [price],[start],[end]\n e.g. 2.11, Home, Work" + ) + return CS.QUICK_ADD + else: + update.message.reply_text( + "Quick Add Transport, please choose your category.", + reply_markup=utils.create_inline_markup(setting_list), + ) + return CS.QUICK_ADD_TRANSPORT + except Exception as e: update.message.reply_text( - "Quick Add Transport, please choose your category.", - reply_markup=utils.create_inline_markup(setting_list), + ERROR_TEXT + "\nError:\n" + utils.sanitize_error_message(str(e)) ) - return CS.QUICK_ADD_TRANSPORT + return ConversationHandler.END def add_others(update, context): @@ -682,22 +718,27 @@ def add_others(update, context): context.user_data["sheet_id"], EntryType.OTHERS ) except Exception as e: - update.callback_query.message.reply_text( ERROR_TEXT + "\nError:\n" + utils.sanitize_error_message(str(e)) ) return ConversationHandler.END + if not setting_list or not setting_list[0]: update.message.reply_text(QUICK_SETUP_OTHER) return ConversationHandler.END else: - setting_list = gs.get_quick_add_list( - context.user_data["sheet_id"], context.user_data["entry_type"] - ) - update.message.reply_text( - "Quick Add Others, please choose your category.", - reply_markup=utils.create_inline_markup(setting_list), - ) + try: + setting_list = gs.get_quick_add_list( + context.user_data["sheet_id"], context.user_data["entry_type"] + ) + update.message.reply_text( + "Quick Add Others, please choose your category.", + reply_markup=utils.create_inline_markup(setting_list), + ) + except Exception as e: + update.message.reply_text( + ERROR_TEXT + "\nError:\n" + utils.sanitize_error_message(str(e)) + ) return CS.QUICK_ADD_CATEGORY @@ -738,7 +779,6 @@ def quick_add(update, context) -> int: ) return ConversationHandler.END except Exception as e: - update.message.reply_text("Please follow the format and try again.") return CS.QUICK_ADD @@ -758,7 +798,6 @@ def get_day_transaction(update, context): update.message.reply_text(GET_TRANSACTION_TEXT) return CS.HANDLE_GET_TRANSACTION except Exception as e: - update.message.reply_text( ERROR_TEXT + "\nError:\n" + utils.sanitize_error_message(str(e)) ) @@ -776,11 +815,10 @@ def get_overall(update, context): update.message.reply_text(GET_OVERALL_TEXT) return CS.HANDLE_GET_OVERALL except Exception as e: - update.message.reply_text( ERROR_TEXT + "\nError:\n" + utils.sanitize_error_message(str(e)) ) - return ConversationHandler.END + return ConversationHandler.END def handle_get_transaction(update, context): @@ -821,11 +859,10 @@ def handle_get_transaction(update, context): update.message.reply_text(GET_TRANSACTION_TEXT) return CS.HANDLE_GET_TRANSACTION except Exception as e: - update.message.reply_text( ERROR_TEXT + "\nError:\n" + utils.sanitize_error_message(str(e)) ) - return ConversationHandler.END + return ConversationHandler.END def handle_get_overall(update, context): @@ -860,11 +897,10 @@ def handle_get_overall(update, context): update.message.reply_text(GET_OVERALL_TEXT) return CS.HANDLE_GET_OVERALL except Exception as e: - update.message.reply_text( ERROR_TEXT + "\nError:\n" + utils.sanitize_error_message(str(e)) ) - return ConversationHandler.END + return ConversationHandler.END def add_income(update, context): @@ -876,13 +912,12 @@ def add_income(update, context): telegram_id, telegram_username ) update.message.reply_text(ADD_INCOME_TEXT) + return CS.INCOME except Exception as e: - update.message.reply_text( ERROR_TEXT + "\nError:\n" + utils.sanitize_error_message(str(e)) ) - return ConversationHandler.END - return CS.INCOME + return ConversationHandler.END def income(update, context) -> int: @@ -908,11 +943,10 @@ def income(update, context) -> int: ) return CS.WORK_PLACE except Exception as e: - update.message.reply_text( ERROR_TEXT + "\nError:\n" + utils.sanitize_error_message(str(e)) ) - return ConversationHandler.END + return ConversationHandler.END def work_place(update, context) -> int: @@ -942,13 +976,11 @@ def cpf(update, context) -> int: update.callback_query.message.reply_text("Income has been added!") else: update.callback_query.message.reply_text(INCOME_LIMIT_TEXT) - return ConversationHandler.END except Exception as e: - update.callback_query.message.reply_text( ERROR_TEXT + "\nError:\n" + utils.sanitize_error_message(str(e)) ) - return ConversationHandler.END + return ConversationHandler.END def backlog(update, context) -> int: @@ -973,41 +1005,55 @@ def add_backlog_entry(update, context) -> int: telegram_id = update.effective_user.id telegram_username = update.effective_user.username - context.user_data["sheet_id"] = db.get_user_sheet_id(telegram_id, telegram_username) - update.message.reply_text( - ENTRY_TYPE_TEXT, - reply_markup=utils.create_inline_markup( - [entry_type.value for entry_type in EntryType] - ), - ) - return CS.ENTRY + try: + context.user_data["sheet_id"] = db.get_user_sheet_id( + telegram_id, telegram_username + ) + update.message.reply_text( + ENTRY_TYPE_TEXT, + reply_markup=utils.create_inline_markup( + [entry_type.value for entry_type in EntryType] + ), + ) + return CS.ENTRY + except DatabaseError as e: + raise e + except Exception as e: + raise TelegramBotError(message="Error adding backlog entry", extra_info=str(e)) def send_new_feature_message(context, new_feature_message): - users = db.get_all_user_id() - no_of_users = 0 - no_of_error_users = 0 - errors = [] + try: + users = db.get_all_user_id() + no_of_users = 0 + no_of_error_users = 0 + errors = [] - for user_id in users: - try: - context.bot.send_message( - chat_id=user_id, - text=new_feature_message, - parse_mode=ParseMode.HTML, - ) - no_of_users += 1 - except Exception as e: + for user_id in users: try: - chat = context.bot.get_chat(chat_id=user_id) - username = chat.username if chat.username else "?" + context.bot.send_message( + chat_id=user_id, + text=new_feature_message, + parse_mode=ParseMode.HTML, + ) + no_of_users += 1 except Exception: - username = "?" - no_of_error_users += 1 - errors.append(f"Username @{username} (ID: {user_id}): {e}") - - error_message = "\n".join(errors) - return no_of_users, no_of_error_users, error_message + try: + chat = context.bot.get_chat(chat_id=user_id) + username = chat.username if chat.username else "?" + except Exception: + username = "?" + no_of_error_users += 1 + errors.append(f"Username @{username} (ID: {user_id}): {e}") + + error_message = "\n".join(errors) + return no_of_users, no_of_error_users, error_message + except DatabaseError as e: + raise e + except Exception as e: + raise TelegramBotError( + message="Fail to send new feature message", extra_info=str(e) + ) def notify_all(update, context): @@ -1028,6 +1074,7 @@ def notify_all(update, context): f"Preview:\n{new_feature_message}", reply_markup=reply_markup, ) + return CS.NOTIFICATION def notify_preview(update, context): @@ -1037,17 +1084,27 @@ def notify_preview(update, context): text=f"Sending message to all in progress...", reply_markup=InlineKeyboardMarkup([]), ) - if query.data == "confirm_send": - new_feature_message = query.message.text.partition("\n")[2] - no_of_users, no_of_error_users, error_message = send_new_feature_message( - context, new_feature_message + try: + if ( + query.data == "confirm_send" + and str(update.effective_user.id) == MASTER_TELE_ID + ): + new_feature_message = query.message.text.partition("\n")[2] + no_of_users, no_of_error_users, error_message = send_new_feature_message( + context, new_feature_message + ) + + response = f"Message sent to {no_of_users} users.\n{no_of_error_users} users failed to receive the message." + if error_message: + response += f"\nErrors:\n{error_message}" + query.edit_message_text(text=response) + elif query.data == "cancel_send": + query.edit_message_text(text="Message sending cancelled.") + except Exception as e: + query.edit_message_text( + text=ERROR_TEXT + "\nError:\n" + utils.sanitize_error_message(str(e)) ) - response = f"Message sent to {no_of_users} users.\n{no_of_error_users} users failed to receive the message." - if error_message: - response += f"\nErrors:\n{error_message}" - query.edit_message_text(text=response) - elif query.data == "cancel_send": - query.edit_message_text(text="Message sending cancelled.") + return ConversationHandler.END def setup_handlers(dispatcher): @@ -1082,6 +1139,11 @@ def setup_handlers(dispatcher): CS.QUICK_ADD_TRANSPORT: [CallbackQueryHandler(quick_add_transport)], } + # Notify all users (admin) + notification_states = { + CS.NOTIFICATION: [CallbackQueryHandler(notify_preview)], + } + # Retrieve transaction-related states and handlers get_transaction_states = { CS.HANDLE_GET_TRANSACTION: [ @@ -1108,6 +1170,7 @@ def setup_handlers(dispatcher): CommandHandler("getdaytransaction", get_day_transaction), CommandHandler("getoverall", get_overall), CommandHandler("backlog", backlog), + CommandHandler("notifyall", notify_all), ], states={ CS.SET_UP: [MessageHandler(Filters.text & ~Filters.command, set_up)], @@ -1117,6 +1180,7 @@ def setup_handlers(dispatcher): **quick_add_states, **get_transaction_states, **add_income_states, + **notification_states, }, fallbacks=[CommandHandler("cancel", cancel)], ) @@ -1124,9 +1188,3 @@ def setup_handlers(dispatcher): help_handler = CommandHandler("help", help) dispatcher.add_handler(help_handler) - - # Notify all users (admin) - notify_all_handler = CommandHandler("notifyall", notify_all) - - dispatcher.add_handler(CallbackQueryHandler(notify_preview)) - dispatcher.add_handler(notify_all_handler) diff --git a/bot/text_str.py b/bot/text_str.py index 092173d..cbedad4 100644 --- a/bot/text_str.py +++ b/bot/text_str.py @@ -14,7 +14,7 @@ + "6. Copy your entire Google Sheet URL and send it over\n" + "Example: https://docs.google.com/spreadsheets/d/abcd1234/edit\n" ) -ERROR_TEXT = "There seems to be an error, please try again later. If the problem persists, please report it at github.com/brucewzj99/tele-tracker-v2/issues" +ERROR_TEXT = "There seems to be an error, please try again later. If the problem persists, please report it at github.com/brucewzj99/tele-tracker-v2/issues\nwith the error message.\n" SUCCESS_LINK_TEXT = "Google sheet successfully linked! Please proceed to configure your Dropdown sheet.\nOnce completed, type /addentry to add your first entry!" GSHEET_ERROR_TEXT = f"There seems to be an error linking your google sheet, have you shared your Google Sheet with {GOOGLE_API_EMAIL} yet?" GSHEET_WRONG_TEXT = ( diff --git a/release_notes.md b/release_notes.md index b570c1a..5281994 100644 --- a/release_notes.md +++ b/release_notes.md @@ -1,10 +1,17 @@ # Release Notes +## Version 2.4 - Date 6 May 2024 +### Enhancement 🔥 +- Clearer error messages for users + +### For Developer 🧑‍💻 +- Added more test case + ## Version 2.3.7 - Date 3 May 2024 -### Quick Fix 🛠️ +### For Developer 🧑‍💻 - Add number of error users for /notifyall command ## Version 2.3.6 - Date 3 May 2024 -### Quick Fix 🛠️ +### For Developer 🧑‍💻 - Fix parse mode for /notifyall command ## Version 2.3.5 - Date 3 May 2024 diff --git a/test/test_database.py b/test/test_database.py index 60be571..b857742 100644 --- a/test/test_database.py +++ b/test/test_database.py @@ -1,7 +1,6 @@ from bot.database_service import firestore_service - from unittest import TestCase - +from bot.error_handler import DatabaseError class TestFirestoreService(TestCase): @classmethod @@ -9,6 +8,7 @@ def setUpClass(cls): super().setUpClass() cls.db = firestore_service.FirestoreService() cls.telegram_id = "123456" + cls.fake_telegram_id = "123" cls.sheet_id = "sheet123" cls.telegram_username = "test_user" @@ -19,9 +19,26 @@ def test_check_if_user_exists(self): # Assert self.assertTrue(user_exists) + + def test_check_if_user_dont_exists(self): + # Act + user_exists = self.db.check_if_user_exists(self.fake_telegram_id) + + # Assert + self.assertFalse(user_exists) + def test_get_user_sheet_id(self): # Act sheet_id = self.db.get_user_sheet_id(self.telegram_id, self.telegram_username) # Assert self.assertEqual(sheet_id, self.sheet_id) + + + def test_get_dont_exist_user_sheet_id(self): + # Act & Assert + with self.assertRaises(DatabaseError) as context: + self.db.get_user_sheet_id(self.fake_telegram_id, self.telegram_username) + + # Check if the error message is as expected + self.assertIn("User does not exist", context.exception.extra_info) \ No newline at end of file diff --git a/test/test_errorhandler.py b/test/test_errorhandler.py new file mode 100644 index 0000000..c431d8f --- /dev/null +++ b/test/test_errorhandler.py @@ -0,0 +1,35 @@ +import unittest +from unittest.mock import patch +from bot.error_handler import GoogleSheetError, TelegramBotError, DatabaseError + +class TestErrorHandling(unittest.TestCase): + + def test_google_sheet_error_raises_correctly(self): + # Define the message and extra_info + message = "Test failure in Google Sheets" + extra_info = "Invalid Range" + + # Check if the exception is raised with the correct message + with self.assertRaises(GoogleSheetError) as context: + raise GoogleSheetError(message, extra_info) + + # Verify that the message in the exception is as expected + self.assertEqual(str(context.exception), f"GoogleSheetError: {message}") + + def test_telegram_bot_error_raises_correctly(self): + message = "Failed to send message" + extra_info = "User not found" + + with self.assertRaises(TelegramBotError) as context: + raise TelegramBotError(message, extra_info) + + self.assertEqual(str(context.exception), f"TelegramBotError: {message}") + + def test_database_error_raises_correctly(self): + message = "Database connection failed" + extra_info = "Timeout occurred" + + with self.assertRaises(DatabaseError) as context: + raise DatabaseError(message, extra_info) + + self.assertEqual(str(context.exception), f"DatabaseError: {message}") \ No newline at end of file