diff --git a/.codecov.yml b/.codecov.yml deleted file mode 100644 index 9d1a74e53..000000000 --- a/.codecov.yml +++ /dev/null @@ -1,8 +0,0 @@ -coverage: - status: - project: - default: - target: 80% - threshold: null - patch: false - changes: false diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 97fb82b67..000000000 --- a/.flake8 +++ /dev/null @@ -1,3 +0,0 @@ -[flake8] -ignore = E501, W291, E302, F523, E265, E251, F401, F811, W391, E712, E303, W292, W293, F841, E231, E261, E305 -exclude = .git,__pycache__,venv # Directories/files to exclude from checks diff --git a/.github/ISSUE_TEMPLATE/custom.md b/.github/ISSUE_TEMPLATE/custom.md index 956abfc2d..67e4f788b 100644 --- a/.github/ISSUE_TEMPLATE/custom.md +++ b/.github/ISSUE_TEMPLATE/custom.md @@ -6,5 +6,3 @@ labels: '' assignees: '' --- - - diff --git a/.github/ISSUE_TEMPLATE/issue-task-template.md b/.github/ISSUE_TEMPLATE/issue-task-template.md index a3c82bdf6..f810fce4c 100644 --- a/.github/ISSUE_TEMPLATE/issue-task-template.md +++ b/.github/ISSUE_TEMPLATE/issue-task-template.md @@ -6,5 +6,3 @@ labels: '' assignees: '' --- - - diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md index 4cd8127db..5dec4c595 100644 --- a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md +++ b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md @@ -1,3 +1,13 @@ + + +**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* + +- [Pull Request Template](#pull-request-template) + - [Description](#description) + - [Type of change](#type-of-change) + + + # Pull Request Template ## Description diff --git a/.github/workflows/github-ci.yml b/.github/workflows/github-ci.yml new file mode 100644 index 000000000..734a7bc95 --- /dev/null +++ b/.github/workflows/github-ci.yml @@ -0,0 +1,193 @@ +--- +name: github-ci + +on: # yamllint disable-line rule:truthy + pull_request: + branches: + - "release/*" + +permissions: + contents: write + +jobs: + + pre-commit: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python 3.12 + uses: actions/setup-python@v3 + with: + python-version: "3.12" + + - name: Install requirements + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Set PYTHONPATH + run: | + echo "PYTHONPATH=$(pwd)" >> $GITHUB_ENV + + - name: Run pre-commit + run: | + pre-commit run --all-files + + update-badges: + needs: pre-commit + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Set up Python 3.12 + uses: actions/setup-python@v3 + with: + python-version: "3.12" + - name: Setup + run: | + mkdir tmp + sudo apt-get install -y libxml2-utils + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Format check with black + run: | + echo "BLACK_EXIT_CODE=0" >> $GITHUB_ENV + black . --check || echo "BLACK_EXIT_CODE=1" >> $GITHUB_ENV + + - name: Run pylint + continue-on-error: true + run: | + echo "PYTHONPATH=$(pwd)" >> $GITHUB_ENV + echo "PYLINT_EXIT_CODE=0" >> $GITHUB_ENV + pylint api > tmp/pylint_output.txt || echo "PYLINT_EXIT_CODE=$?" >> $GITHUB_ENV + cat tmp/pylint_output.txt + + - name: Run pytest + continue-on-error: true + run: | + echo "PYTEST_EXIT_CODE=0" >> $GITHUB_ENV + pytest --cov-report=json:tmp/coverage.json --junitxml=tmp/pytest_output.xml || echo "PYTEST_EXIT_CODE=$?" >> $GITHUB_ENV + + - name: Run MyPy + continue-on-error: true + run: | + echo "MYPY_EXIT_CODE=0" >> $GITHUB_ENV + mypy api || echo "MYPY_EXIT_CODE=1" >> $GITHUB_ENV + + - name: Run Bandit + continue-on-error: true + run: | + echo "BANDIT_EXIT_CODE=0" >> $GITHUB_ENV + bandit -r api || echo "BANDIT_EXIT_CODE=1" >> $GITHUB_ENV + + - name: Generate black badge URL + run: | + BLACK_EXIT_CODE=${{ env.BLACK_EXIT_CODE }} + if [ "$BLACK_EXIT_CODE" -eq 0 ]; then + BLACK_BADGE_URL="https://img.shields.io/badge/black_formatter-passing-brightgreen?style=plastic&labelColor=black" + else + BLACK_BADGE_URL="https://img.shields.io/badge/black_formatter-failing-red?style=plastic&labelColor=black" + fi + echo "BLACK_BADGE_URL=$BLACK_BADGE_URL" >> $GITHUB_ENV + + - name: Generate pylint badge URL + run: | + SCORE=$(grep -oP '(?<=rated at )[0-9]+\.[0-9]+' tmp/pylint_output.txt) + if [ "$(echo "$SCORE < 5" | bc)" -eq 1 ]; then + COLOR="red" + elif [ "$(echo "$SCORE < 8" | bc)" -eq 1 ]; then + COLOR="orange" + elif [ "$(echo "$SCORE < 10" | bc)" -eq 1 ]; then + COLOR="yellow" + else + COLOR="brightgreen" + fi + PYLINT_BADGE_URL="https://img.shields.io/badge/pylint-${SCORE}-${COLOR}?style=plastic" + echo "PYLINT_BADGE_URL=$PYLINT_BADGE_URL" >> $GITHUB_ENV + + - name: Generate MyPy badge URL + run: | + MYPY_EXIT_CODE=${{ env.MYPY_EXIT_CODE }} + if [ "$MYPY_EXIT_CODE" -eq 0 ]; then + MYPY_BADGE_URL="https://img.shields.io/badge/mypy-passing-brightgreen?style=plastic" + else + MYPY_BADGE_URL="https://img.shields.io/badge/mypy-failing-red?style=plastic" + fi + echo "MYPY_BADGE_URL=$MYPY_BADGE_URL" >> $GITHUB_ENV + + - name: Generate Bandit badge URL + run: | + BANDIT_EXIT_CODE=${{ env.BANDIT_EXIT_CODE }} + if [ "$BANDIT_EXIT_CODE" -eq 0 ]; then + BANDIT_BADGE_URL="https://img.shields.io/badge/bandit-passing-brightgreen?style=plastic" + else + BANDIT_BADGE_URL="https://img.shields.io/badge/bandit-failing-red?style=plastic" + fi + echo "BANDIT_BADGE_URL=$BANDIT_BADGE_URL" >> $GITHUB_ENV + + - name: Extract number of tests, coverage, and determine result + run: | + NUM_TESTS=$(xmllint --xpath 'string(//testsuite/@tests)' tmp/pytest_output.xml) + COVERAGE=$(jq '.totals.percent_covered' tmp/coverage.json | awk '{printf "%.0f", $1}') + if [ ${{ env.PYTEST_EXIT_CODE }} -eq 0 ]; then + TESTS_STATUS="passing" + TESTS_COLOR="brightgreen" + else + TESTS_STATUS="failing" + TESTS_COLOR="red" + fi + if [ "$COVERAGE" -ge 90 ]; then + COVERAGE_COLOR="brightgreen" + elif [ "$COVERAGE" -ge 80 ]; then + COVERAGE_COLOR="green" + elif [ "$COVERAGE" -ge 70 ]; then + COVERAGE_COLOR="yellowgreen" + elif [ "$COVERAGE" -ge 60 ]; then + COVERAGE_COLOR="yellow" + elif [ "$COVERAGE" -ge 50 ]; then # corrected line + COVERAGE_COLOR="orange" + else + COVERAGE_COLOR="red" + fi + + TOTAL_TESTS_BADGE_URL="https://img.shields.io/badge/tests-${NUM_TESTS}-blue?style=plastic&logo=pytest&logoColor=white" + CODE_COVERAGE_BADGE_URL="https://img.shields.io/badge/coverage-${COVERAGE}%25-${COVERAGE_COLOR}?style=plastic" + PYTEST_STATUS_BADGE_URL="https://img.shields.io/badge/PyTest-${TESTS_STATUS}-${TESTS_COLOR}?style=plastic&logo=pytest&logoColor=white" + echo "TOTAL_TESTS_BADGE_URL=$TOTAL_TESTS_BADGE_URL" >> $GITHUB_ENV + echo "CODE_COVERAGE_BADGE_URL=$CODE_COVERAGE_BADGE_URL" >> $GITHUB_ENV + echo "PYTEST_STATUS_BADGE_URL=$PYTEST_STATUS_BADGE_URL" >> $GITHUB_ENV + + + - name: Update README with all the badges + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git stash + git checkout ${{ github.head_ref }} + + RUN_LOG_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + sed -i '/\[!\[badge_black\]/c\[!\[badge_black\]('"$BLACK_BADGE_URL"')]('"$RUN_LOG_URL"')' README.md + sed -i '/\[!\[badge_pylint\]/c\[!\[badge_pylint\]('"$PYLINT_BADGE_URL"')]('"$RUN_LOG_URL"')' README.md + sed -i '/\[!\[badge_mypy\]/c\[!\[badge_mypy\]('"$MYPY_BADGE_URL"')]('"$RUN_LOG_URL"')' README.md + sed -i '/\[!\[badge_bandit\]/c\[!\[badge_bandit\]('"$BANDIT_BADGE_URL"')]('"$RUN_LOG_URL"')' README.md + sed -i '/\[!\[badge_total_tests\]/c\[!\[badge_total_tests\]('"$TOTAL_TESTS_BADGE_URL"')](https://github.com/gitsetgopack/hw2/tree/main/tests)' README.md + sed -i '/\[!\[badge_code_coverage\]/c\[!\[badge_code_coverage\]('"$CODE_COVERAGE_BADGE_URL"')]('"$RUN_LOG_URL"')' README.md + sed -i '/\[!\[badge_pytest_status\]/c\[!\[badge_pytest_status\]('"$PYTEST_STATUS_BADGE_URL"')]('"$RUN_LOG_URL"')' README.md + git add README.md + git commit -m "Update all badges in README" + git push origin + + - name: Fail if any tool failed + run: | + for TOOL in BLACK PYLINT PYTEST MYPY BANDIT; do + EXIT_CODE_VAR="${TOOL}_EXIT_CODE" + EXIT_CODE="${!EXIT_CODE_VAR}" + if [ "$EXIT_CODE" -ne 0 ]; then + exit 1 + fi + done diff --git a/.github/workflows/styleChecker.yml b/.github/workflows/styleChecker.yml deleted file mode 100644 index dab7e0756..000000000 --- a/.github/workflows/styleChecker.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: Style Check with Flake8 - -on: - push: - branches: - - main - pull_request: - branches: - - main - -jobs: - style_check: - name: Style Check - strategy: - matrix: - python-version: - - 3.8 - - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v2 - - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - - - name: Install Flake8 - run: pip install flake8 - - - name: Run Flake8 - run: flake8 . # Replace "." with the directory containing your Python files diff --git a/.github/workflows/testCases.yml b/.github/workflows/testCases.yml deleted file mode 100644 index 5b8173135..000000000 --- a/.github/workflows/testCases.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: Test Case Execution - -on: - push: - branches: - - main - pull_request: - branches: - - main - -jobs: - style_check: - name: Test Case Execution - strategy: - matrix: - python-version: - - 3.11 - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v2 - - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - - - name: Install dependencies - run: pip install -r requirements.txt - - - name: Run Tests - run: python -m pytest test/ diff --git a/.gitignore b/.gitignore index fc04ff37b..baec17292 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +api/routers/config.py +.vscode + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/.idea/workspace.xml b/.idea/workspace.xml deleted file mode 100644 index 49148011c..000000000 --- a/.idea/workspace.xml +++ /dev/null @@ -1,121 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1634740962517 - - - - - - - - - - - - - file://$PROJECT_DIR$/code/add.py - 25 - - - - - - - - \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..d73c6c5cc --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,121 @@ +--- +repos: + - repo: local + hooks: + - id: black + name: black + entry: black . + language: system + pass_filenames: false + always_run: true + + - repo: local + hooks: + - id: isort + name: isort + entry: isort . + language: system + pass_filenames: false + always_run: true + + - repo: local + hooks: + - id: pylint + name: pylint + entry: pylint . + language: system + pass_filenames: false + always_run: true + + # - repo: https://github.com/pycqa/flake8 + # rev: 7.1.1 + # hooks: + # - id: flake8 + + - repo: https://github.com/crate-ci/typos + rev: v1.13.21 + hooks: + - id: typos + + # - repo: https://github.com/markdownlint/markdownlint + # rev: v0.12.0 + # hooks: + # - id: markdownlint + # args: ["--config", ".markdownlint.json"] + + - repo: https://github.com/codespell-project/codespell + rev: v2.1.0 + hooks: + - id: codespell + args: ["-L", "the,their,receive"] # Ignore common typos inline + files: '\.(py|md|yml|yaml|txt|sh|Makefile)$' + + - repo: https://github.com/thlorenz/doctoc + rev: v2.0.1 + hooks: + - id: doctoc + args: ["--github", "--maxlevel=3"] # Customize as needed for TOC structure + + # - repo: https://github.com/trufflesecurity/truffleHog + # rev: v3.83.2 + # hooks: + # - id: trufflehog + # name: Detect Secrets with TruffleHog + # args: ["--regex", "--entropy=False"] + + - repo: https://github.com/gitleaks/gitleaks + rev: v8.21.2 + hooks: + - id: gitleaks + name: Gitleaks Secret Scan + args: ["detect", "--no-banner", "--redact"] + + - repo: https://github.com/mrtazz/checkmake + rev: 0.2.2 + hooks: + - id: checkmake + name: Check Makefile Linting + files: ^Makefile$ + + - repo: local + hooks: + - id: bandit + name: bandit + entry: bandit -r api/* + language: system + pass_filenames: false + always_run: true + + # - repo: https://github.com/pyupio/safety + # rev: 3.2.3 + # hooks: + # - id: safety + + - repo: https://github.com/adrienverge/yamllint + rev: v1.35.1 + hooks: + - id: yamllint + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + + - repo: local + hooks: + - id: pytest + name: pytest + entry: make test + language: system + pass_filenames: false + always_run: true + + - repo: local + hooks: + - id: mypy + name: mypy + entry: mypy api/ + language: system + pass_filenames: false + always_run: true diff --git a/.yamllint b/.yamllint new file mode 100644 index 000000000..d6a8b78d6 --- /dev/null +++ b/.yamllint @@ -0,0 +1,7 @@ +--- +extends: default + +rules: + line-length: + max: 120 # Set max line length to 120 + level: warning # Set to warning or ignore to lessen severity diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index bbb32ff7c..000000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,36 +0,0 @@ -## Changelogs - -### v2.0 - - 1. Two different types of accounts: Checking and Savings. - 2. Money can now be added to the account. - 3. Expense denied if it exceeds money in the account. - 4. Warning displayed when money in the account comes below a threshold - 100$. - 5. Account can be chosen before making a purchase. The account is used till it's changed again. - 6. New Functionality to record expenses in different currencies. Following currencies are supported: '"USD","INR","GBP","EUR","CAD","JPY"'. - 7. New Functionality to send a user's expense history to an email account. - 8. All expenses can now be downloaded in CSV, PDF format. Each expense record will also contain information on Expense category, account used and the money spent. - -### v1.0 - - 1. Delete particular expenses. - 2. Set a daily reminder to track your expenses for either the current day or the current month. - 3. Menu button to improve the UI. - -### Initial release v0 - - 1. Add/Record a new spending. - 2. Calculate the sum of your expenditure for the current day/month. - 3. Display your spending history - 4. Clear/Erase all your records - 5. Edit/Change any spending details if you wish to - 6. Recurring expense: - Add a recurring expense that adds a certain amount every month to the user's spending, for any given category. - 7. Custom category: - User can add a new category and delete an existing category as per the needs - 8. Budgeting: - User can see the budget value for the total expense and/or for each of the existing categories in the /display function - 9. Better visualization: - Added pie charts, bar graphs with and without budget lines for the user to have a look at the spending history in a better manner - Added bar graph in the /history command to see spending across different categories - User can see the daily and monthly expenses for spending history \ No newline at end of file diff --git a/CITATION.md b/CITATION.md deleted file mode 100644 index d9f2fc4dd..000000000 --- a/CITATION.md +++ /dev/null @@ -1,15 +0,0 @@ -[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.10210342.svg)](https://doi.org/10.5281/zenodo.10210342) - - ```yaml - version: 2.1 - authors: - - Mohana Datta Yelugoti - - Rishabh Kala - - Sreehith Yachamaneni - license: MIT License - repository-code: https://github.com/ymdatta/DollarBot - identifiers: - - Description: This is the collection of archived snapshots of v2.1 of DollarBot - type: doi - value: 10.5281/zenodo.10210342 - ``` \ No newline at end of file diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 000000000..42a2368fe --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1 @@ +* @abhira0 @AsthaBhalodiya @UmangDiyora diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 6548d8e1a..2e60b5e6a 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,118 +1,81 @@ -# Contributor Covenant Code of Conduct + + +**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* -## Our Pledge +- [Code of Conduct](#code-of-conduct) + - [Our Commitment](#our-commitment) + - [Community Standards](#community-standards) + - [Responsibility for Enforcement](#responsibility-for-enforcement) + - [Scope of Conduct](#scope-of-conduct) + - [Reporting Issues](#reporting-issues) + - [Enforcement Guidelines](#enforcement-guidelines) + - [1. Correction](#1-correction) + - [2. Warning](#2-warning) + - [3. Temporary Ban](#3-temporary-ban) + - [4. Permanent Ban](#4-permanent-ban) + - [Credits](#credits) -We as members, contributors, and leaders pledge to make participation in our -community a harassment-free experience for everyone, regardless of age, body -size, visible or invisible disability, ethnicity, sex characteristics, gender -identity and expression, level of experience, education, socio-economic status, -nationality, personal appearance, race, religion, or sexual identity -and orientation. + -We pledge to act and interact in ways that contribute to an open, welcoming, -diverse, inclusive, and healthy community. +# Code of Conduct -## Our Standards +## Our Commitment -Examples of behavior that contributes to a positive environment for our -community include: +As members and leaders of this community, we are committed to making participation a harassment-free experience for everyone. We embrace individuals of all ages, body types, abilities, ethnicities, gender identities, experiences, education levels, socio-economic statuses, nationalities, appearances, races, religions, sexual orientations, and identities. -* Demonstrating empathy and kindness toward other people -* Being respectful of differing opinions, viewpoints, and experiences -* Giving and gracefully accepting constructive feedback -* Accepting responsibility and apologizing to those affected by our mistakes, - and learning from the experience -* Focusing on what is best not just for us as individuals, but for the - overall community +We pledge to contribute to an open, welcoming, inclusive, and positive community. -Examples of unacceptable behavior include: +## Community Standards -* The use of sexualized language or imagery, and sexual attention or - advances of any kind -* Trolling, insulting or derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or email - address, without their explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting +Positive behavior includes: -## Enforcement Responsibilities +* Showing empathy and kindness toward others +* Respecting differing opinions, viewpoints, and experiences +* Providing and accepting constructive feedback graciously +* Owning up to mistakes, apologizing when necessary, and learning from them +* Prioritizing the overall community’s well-being -Community leaders are responsible for clarifying and enforcing our standards of -acceptable behavior and will take appropriate and fair corrective action in -response to any behavior that they deem inappropriate, threatening, offensive, -or harmful. +Unacceptable behaviors include: -Community leaders have the right and responsibility to remove, edit, or reject -comments, commits, code, wiki edits, issues, and other contributions that are -not aligned to this Code of Conduct, and will communicate reasons for moderation -decisions when appropriate. +* Sexualized language, imagery, or unwelcome advances +* Trolling, derogatory comments, and personal or political attacks +* Harassment in any form, publicly or privately +* Sharing private information without consent +* Other inappropriate conduct within a professional setting -## Scope +## Responsibility for Enforcement -This Code of Conduct applies within all community spaces, and also applies when -an individual is officially representing the community in public spaces. -Examples of representing our community include using an official e-mail address, -posting via an official social media account, or acting as an appointed -representative at an online or offline event. +Community leaders are tasked with upholding and enforcing these standards. They may take corrective action against behavior they deem harmful, including editing, removing, or rejecting contributions, with explanations where appropriate. -## Enforcement +## Scope of Conduct -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported to the community leaders responsible for enforcement at -dollarbot38@googlegroups.com. -All complaints will be reviewed and investigated promptly and fairly. +This Code applies within all community areas and extends to public representation of the community, such as via official emails, social media, or event participation. -All community leaders are obligated to respect the privacy and security of the -reporter of any incident. +## Reporting Issues + +Instances of unacceptable behavior may be reported to community leaders at dollarbot38@googlegroups.com. All reports will be reviewed and addressed swiftly, with respect for the privacy of those involved. ## Enforcement Guidelines -Community leaders will follow these Community Impact Guidelines in determining -the consequences for any action they deem in violation of this Code of Conduct: +Community leaders will consider these impact levels when addressing violations: ### 1. Correction - -**Community Impact**: Use of inappropriate language or other behavior deemed -unprofessional or unwelcome in the community. - -**Consequence**: A private, written warning from community leaders, providing -clarity around the nature of the violation and an explanation of why the -behavior was inappropriate. A public apology may be requested. +**Impact**: Minor inappropriate language or unprofessional behavior. +**Outcome**: A private warning with a reminder of expected behavior. A public apology may be requested. ### 2. Warning - -**Community Impact**: A violation through a single incident or series -of actions. - -**Consequence**: A warning with consequences for continued behavior. No -interaction with the people involved, including unsolicited interaction with -those enforcing the Code of Conduct, for a specified period of time. This -includes avoiding interactions in community spaces as well as external channels -like social media. Violating these terms may lead to a temporary or -permanent ban. +**Impact**: A single or repeated violation. +**Outcome**: A warning, along with restrictions on interaction within community spaces and external channels for a set time. Further violations may result in a temporary or permanent ban. ### 3. Temporary Ban - -**Community Impact**: A serious violation of community standards, including -sustained inappropriate behavior. - -**Consequence**: A temporary ban from any sort of interaction or public -communication with the community for a specified period of time. No public or -private interaction with the people involved, including unsolicited interaction -with those enforcing the Code of Conduct, is allowed during this period. -Violating these terms may lead to a permanent ban. +**Impact**: Serious violation, including prolonged inappropriate behavior. +**Outcome**: A temporary ban from the community and all interactions within it. Breaking these terms may lead to a permanent ban. ### 4. Permanent Ban +**Impact**: Repeated serious violations, including harassment or aggression. +**Outcome**: Permanent ban from all community interactions. -**Community Impact**: Demonstrating a pattern of violation of community -standards, including sustained inappropriate behavior, harassment of an -individual, or aggression toward or disparagement of classes of individuals. - -**Consequence**: A permanent ban from any sort of public interaction within -the community. - -## Attribution +### Credits This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md old mode 100755 new mode 100644 index 93c64b0c8..923d5f7f7 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,116 +1,123 @@ -# Contributing to DollarBot - -Thank you for your interest in contributing to DollarBot! Your contributions are highly valued, and this document will help you get started with the process. -Follow the set of guidelines below to contribute to DollarBot! - -## Table of Contents - -- [Code of Conduct](#code-of-conduct) -- [Getting Started](#getting-started) - - [Pre-requisites](#pre-requisites) - - [Fork and Clone the Repository](#fork-and-clone-the-repository) -- [Contributing Guidelines](#contributing-guidelines) - - [Branching Strategy](#branching-strategy) - - [Commit Guidelines](#commit-guidelines) - - [Code Style](#code-style) - - [Reporting Bugs](#reporting-bugs) - - [Submitting a Bug](#submitting-a-bug) -- [Submitting a Pull Request](#submitting-a-pull-request) - - [Pull Request Template](#pull-request-template) -- [License](#license) + + +**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* + +- [Contributing to Money Manager](#contributing-to-money-manager) + - [Getting Started](#getting-started) + - [1. Fork the Repository](#1-fork-the-repository) + - [2. Clone Your Fork](#2-clone-your-fork) + - [3. Set Up Environment](#3-set-up-environment) + - [4. Create a Branch](#4-create-a-branch) + - [5. Make Changes](#5-make-changes) + - [6. Run Formatters and Linters](#6-run-formatters-and-linters) + - [7. Commit Changes](#7-commit-changes) + - [8. Push Changes](#8-push-changes) + - [9. Submit a Pull Request](#9-submit-a-pull-request) + - [Code of Conduct](#code-of-conduct) + - [Guidelines](#guidelines) + + + +# Contributing to Money Manager + +Thank you for considering contributing to **MoneyManager**! We welcome all types of contributions, whether you're fixing a bug, adding a feature, improving documentation, or suggesting an idea. -## Code of Conduct +## Getting Started -Please note that we have a [Code of Conduct](CODE_OF_CONDUCT.md) that all contributors are expected to follow. It ensures that our community is welcoming and inclusive. +To get started with contributing to this project, please follow these guidelines. -By participating, you are expected to uphold this code. Please report unacceptable behavior to dollarbot38@googlegroups.com -(This project is a part of CSC510, Software Engineering at NC State for Fall'23, Group #38) +### 1. Fork the Repository -## Getting Started +Start by forking the main repository on GitHub. This creates a copy of the repository under your GitHub account. + +- Navigate to [MoneyManager GitHub Repo](https://github.com/gitsetgopack/MoneyManager) +- Click the **Fork** button in the top-right corner. + +### 2. Clone Your Fork -### Pre-requisites +Once you've forked the repository, clone your fork locally: -Pre-requisites required before starting this project -1. Have a good understanding of Python Programming Language -2. Know how Telegram Bots work. -3. Have a good understanding of common coding practices. -4. If you like to contribute, please check the status of the repo, whether it's still being maintained or not. - (If you have any doubts, please reach out to the maintainers before creating a branch) +```bash +git clone https://github.com/your-username/MoneyManager.git +cd MoneyManager +``` + +Replace `your-username` with your GitHub username. -### Fork and Clone the Repository +### 3. Set Up Environment + +To set up the environment, use the following command to install dependencies: -To contribute, we recommend to fork the DollarBot repository to your GitHub account. This will create a copy of the project under your account. -Once forked, clone the repository from your account to your local machine. ```bash -git clone https://github.com/ymdatta/DollarBot.git -cd DollarBot +make install ``` -## Contributing Guidelines -### Branching Strategies +This will install all necessary Python packages and set up the pre-commit hooks. -Please create a new branch for your work and base it on the latest 'main' branch. +### 4. Create a Branch -### Commit Guidelines -1. Follow [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) for your commit messages. -2. Provide a clear and concise description of your changes. +It's good practice to create a new branch for each change. This makes it easier to submit pull requests. -### Code Style -We request you to really adhere to the project's coding styles and language <> +```bash +git checkout -b feature/new-feature +``` -### Reporting Bugs +Replace `feature/new-feature` with a meaningful name for your branch. -This section guides you through submitting a bug report for DollarBot. -Following these guidelines helps maintainers and the community understand your report, reproduce the behavior and find related reports. +### 5. Make Changes -Before Submitting A Bug Report +Make your changes to the codebase. Ensure you write unit tests if applicable. -## Submitting a Bug +To run tests locally: -We appreciate your efforts in helping us improve DollarBot by reporting bugs. Before you create a new issue, please take a moment to check if the bug has already been reported in our [Issues](https://github.com/ymdatta/DollarBot/issues) section. +```bash +make test +``` -If you're certain that it's a new issue or have additional information to provide, follow these steps: +### 6. Run Formatters and Linters -1. **Check the Issue Tracker**: Search our [Issues](https://github.com/ymdatta/DollarBot/issues) to see if a similar issue has already been reported. If you find one that resonates with your observation, we recommend you to add relevant details/updates/observations to that existing issue. +Before committing, make sure your code is formatted correctly: -2. **Create a New Issue**: If you can't find an existing bug matching the problem you've encountered, click on the "New Issue" button and select a template to create a new issue. +```bash +make fix +``` -3. **Be Specific**: In the issue, please specify as much things as possible about the problem you're experience with the following information: - - A clear title that summarizes the bug. - - A detailed description of the bug, including what you expected to happen and what actually happened. - - Steps to reproduce the issue. - - Your environment, including operating system, browser (if applicable), and relevant software versions. +This command will run `black` and `isort` to ensure the code style is consistent. -4. **Attach Screenshots or Error Logs**: If relevant, include screenshots, error logs, or any other files that can help us better understand the issue. +### 7. Commit Changes -5. **Labels**: Apply relevant labels to your report, such as "bug," "needs confirmation," or any other labels used in our issue tracker. +Commit your changes with a descriptive commit message: -6. **Assignees**: If you know who should be responsible for the new issue, you may assign the issue directly to them. Otherwise, someone from the maintainers will handle it. +```bash +git add . +git commit -m "Added a new feature to manage categories" +``` -7. **Review and Feedback**: It's important to stay engaged in the issue thread. The maintainers/other contributors may have questions or need further information to RCA/diagnose the problem. +### 8. Push Changes -And lastly, please be patient as the maintainers work to address the bug. -We appreciate your contributions to making DollarBot better! +Push your changes to your forked repository: -## Suggesting Enhancements -Any suggesting enhancements like adding new features or improving existing functionalities, etc can done by following the below guidelines. They help maintainers understand your improvement. The template- this template is to be filled to add suggestions. These can include the steps that you imagine you would take if the feature you're requesting existed. +```bash +git push origin feature/new-feature +``` -## Submitting a Pull Request +### 9. Submit a Pull Request -1. Code of Conduct: Please ensure that your interactions and contributions align with our [Code of Conduct](#code-of-conduct) -2. Check your code from your end, to check whether your changes directly address the intended feature/issue. -3. Once ready, on GitHub, navigate to your fork of the repository and click the "New Pull Request" button. Fill in the details, briefly explaining what the PR does and if, how it addresses the issue or feature. -4. Your PR then will be reviewed by at least one project maintainer. Please be prepared to respond to any feedback or requests for changes. You might have to make adjustments and push additional commits as needed. -5. License: By contributing to DollarBot, you agree that your contributions will be licensed under the project's [LICENSE](LICENSE.md). +Once you've pushed your changes, go to the main repository on GitHub and submit a pull request (PR) from your forked repository. -## LICENSE +- Navigate to your fork on GitHub. +- Click **Compare & Pull Request**. +- Provide a clear and concise description of your changes in the PR description. -By contributing to DollarBot, you agree that your contributions will be licensed under the project's open-source license. It's important to understand and respect the licensing terms before contributing. The specific license terms for this project can be found in the [LICENSE](LICENSE.md) file. +## Code of Conduct -Please review the license carefully to ensure you are in compliance with its terms. If you have any questions or concerns about the license, feel free to reach out to the project maintainers or the community for clarification. +We expect all contributors to follow our [Code of Conduct](CODE_OF_CONDUCT.md). Please respect others' work and efforts, and let's collaborate effectively to improve **MoneyManager** together. -Contributors are expected to respect and adhere to the project's licensing terms. This typically includes granting the project and its users the necessary permissions to use, modify, and distribute your contributions. +## Guidelines -Remember that this information is provided for guidance, and the license text itself, found in the [LICENSE](LICENSE) file, is the authoritative source of licensing terms for the project. Your contributions are subject to these terms. +- Write clear, concise commit messages. +- Test your changes thoroughly. +- Include tests for any new functionality. +- If you have any questions, please open an issue or contact the maintainers. -Thank you for your understanding and for contributing to DollarBot! +Thank you for your contributions and for making **Money Manager** better! diff --git a/DollarBOT.mp4 b/DollarBOT.mp4 deleted file mode 100644 index 372791bf3..000000000 Binary files a/DollarBOT.mp4 and /dev/null differ diff --git a/DollarBot.gif b/DollarBot.gif deleted file mode 100644 index f80676e47..000000000 Binary files a/DollarBot.gif and /dev/null differ diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 000000000..60c081032 --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,137 @@ + + +**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* + +- [MoneyManager Installation Guide](#moneymanager-installation-guide) + - [Prerequisites](#prerequisites) + - [Installation Steps](#installation-steps) + - [Available Make Commands](#available-make-commands) + - [Additional Information](#additional-information) + - [Troubleshooting](#troubleshooting) + - [Running the Project](#running-the-project) + - [Running Tests](#running-tests) + + + +# MoneyManager Installation Guide + +Welcome to the **MoneyManager** project! This guide will help you set up the environment and install dependencies to get started. + +## Prerequisites + +Before beginning the installation, please ensure you have the following installed: + +- **Python** (version 3.8 or higher) +- **Git** (to clone the repository) +- **Docker** (for running MongoDB in a Docker container during testing) + +## Installation Steps + +1. **Clone the Repository** + + Begin by cloning the repository to your local machine: + + ```bash + git clone https://github.com/gitsetgopack/MoneyManager.git + cd MoneyManager + ``` + +2. **Install Dependencies** + + Run the following command to install all required dependencies: + + ```bash + make install + ``` + + This command will: + - Upgrade `pip` to the latest version. + - Install the required Python packages as specified in the `requirements.txt`. + - Install pre-commit hooks. + +## Available Make Commands + +Here are the commands available in the `Makefile` to help you work with the project: + +- **help**: Show this help message, displaying all available commands. + ```bash + make help + ``` + +- **install**: Install dependencies in the virtual environment. + ```bash + make install + ``` + +- **run**: Run the FastAPI application using the virtual environment. + ```bash + make run + ``` + + This will execute the FastAPI app located at `api/app.py`. + +- **test**: Start a MongoDB Docker container, run tests, and clean up after the tests. + ```bash + make test + ``` + + This command will: + - Start a MongoDB container to simulate a database for testing. + - Run all tests using `pytest`. + - Stop and remove the MongoDB container after testing is complete. + +- **fix**: Run code formatting on the `api` directory using `black` and `isort`. + ```bash + make fix + ``` + +- **clean**: Clean up Python bytecode files, cache, and MongoDB Docker containers. + ```bash + make clean + ``` + + This will: + - Stop and remove the `mongo-test` Docker container if it exists. + - Remove Python bytecode files (`.pyc`) and caches like `__pycache__`, `.pytest_cache`, and `.mypy_cache`. + +- **no_verify_push**: Stage, commit, and push changes with `--no-verify` to skip pre-commit hooks. + ```bash + make no_verify_push + ``` + + This command allows you to quickly commit and push changes without running verification checks. It will prompt you for a commit message. + +## Additional Information + +- **Makefile**: The `Makefile` includes useful commands to set up, run, and test the project. You can inspect it for more details on available commands. +- **Python Environment**: It’s recommended to create a virtual environment for this project to keep dependencies isolated. Run `python -m venv venv` before `make install` if needed. + +## Troubleshooting + +- **Python Compatibility**: Ensure Python is in your system’s `PATH` and meets the required version. +- **Dependency Issues**: If you encounter issues, check the `requirements.txt` file for compatibility, or re-run `make install` after activating a virtual environment. +- **Docker Issues**: Make sure Docker is installed and running properly before executing commands that require a MongoDB container. + +## Running the Project + +After installation, you can run the FastAPI server by executing: + +```bash +make run +``` + +This command will start the application, and you can access it in your browser at the specified URL (typically `http://127.0.0.1:8000`). + +## Running Tests + +To run the tests, ensure Docker is running and then use: + +```bash +make test +``` + +This command will automatically set up the necessary database for testing purposes. + +--- + +Feel free to reach out if you have any issues setting up **MoneyManager**! diff --git a/LICENSE b/LICENSE index 8babb5574..68a49daad 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,24 @@ -MIT License +This is free and unencumbered software released into the public domain. -Copyright (c) 2021 Dev Kumar +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. -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: +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. -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 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. -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. +For more information, please refer to diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..2d29a5fd1 --- /dev/null +++ b/Makefile @@ -0,0 +1,36 @@ +# Help function to display available commands +help: ## Show this help message + @echo "Available commands:" + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-15s\033[0m %s\n", $$1, $$2}' + +install: ## Install dependencies in the virtual environment + pip install --upgrade pip + pip install -r requirements.txt + pre-commit install + +run: ## Run the FastAPI app using the virtual environment + python api/app.py + +test: ## Start MongoDB Docker container, run tests, and clean up + docker run --name mongo-test -p 27017:27017 -d mongo:latest + @sleep 5 # Wait for MongoDB to be ready + pytest -v || (docker stop mongo-test && docker rm mongo-test && exit 1) + docker stop mongo-test + docker rm mongo-test + +fix: ## Black format and isort on api dir + black api/ + isort api/ + +clean: ## Clean up Python bytecode files and caches + @docker stop mongo-test || true + @docker rm mongo-test || true + (find . -type f \( -name "*.pyc" -o -name ".coverage" -o -name ".python-version" \) -delete && \ + find . -type d \( -name "__pycache__" -o -name ".pytest_cache" -o -name ".mypy_cache" \) -exec rm -rf {} +) + +no_verify_push: ## Stage, commit & push with --no-verify + @read -p "Enter commit message: " msg; \ + git commit -a -m "$$msg" --no-verify + git push + +.PHONY: all help install run test fix clean no_verify_push diff --git a/README.md b/README.md index 252fa60ed..d94458070 100644 --- a/README.md +++ b/README.md @@ -1,310 +1,60 @@ -
+ + +**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* -![MIT license](https://img.shields.io/badge/License-MIT-green.svg) -![Style Checker](https://github.com/ymdatta/DollarBot/actions/workflows/styleChecker.yml/badge.svg) -[![Platform](https://img.shields.io/badge/Platform-Telegram-blue)](https://desktop.telegram.org/) -![GitHub](https://img.shields.io/badge/Language-Python-blue.svg) -[![GitHub contributors](https://img.shields.io/github/contributors/ymdatta/DollarBot)](https://github.com/rrajpuro/DollarBot/graphs/contributors) -[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.10210342.svg)](https://doi.org/10.5281/zenodo.10210342) -[![Build Status](https://app.travis-ci.com/sak007/MyDollarBot-BOTGo.svg?branch=main)](https://app.travis-ci.com/github/sak007/MyDollarBot-BOTGo) -[![codecov](https://codecov.io/gh/sak007/MyDollarBot-BOTGo/branch/main/graph/badge.svg?token=5AYMR8MNMP)](https://codecov.io/gh/sak007/MyDollarBot-BOTGo) -[![GitHub open issues](https://img.shields.io/github/issues/ymdatta/DollarBot)](https://github.com/ymdatta/DollarBot/issues?q=is%3Aopen+is%3Aissue) -[![GitHub closed issues](https://img.shields.io/github/issues-closed/ymdatta/DollarBot)](https://github.com/ymdatta/DollarBot/issues?q=is%3Aissue+is%3Aclosed) -![Github pull requests](https://img.shields.io/github/issues-pr/ymdatta/DollarBot) +- [Money Manager](#money-manager) -![Fork](https://img.shields.io/github/forks/deekay2310/MyDollarBot?style=social) -
+ -# 💰 DollarBot v2.0 - Budgeting On The Go 💰 +# Money Manager -
-

-Expense tracking made easy! -

+A REST API application for managing expenses. Build your own automation, be it telegram bot, discord or your own app. -# Description +![built_with_love](http://ForTheBadge.com/images/badges/built-with-love.svg) -DollarBot is an easy-to-use Telegram bot that assists you in recording and managing daily expenses on a local system without any hassle with simple commands. +--- -[[Link to animated video]](https://www.canva.com/design/DAF1bJpmHtM/EGubjQOePxySaFfDb0sJMg/watch?utm_content=DAF1bJpmHtM&utm_campaign=designshare&utm_medium=link&utm_source=editor) +#### Quality -This bot has following functionalities: +[![badge_pytest_status](https://img.shields.io/badge/PyTest-passing-brightgreen?style=plastic&logo=pytest&logoColor=white)](https://github.com/gitsetgopack/MoneyManager/actions/runs/11639575982) +[![badge_code_coverage](https://img.shields.io/badge/coverage-95%25-brightgreen?style=plastic)](https://github.com/gitsetgopack/MoneyManager/actions/runs/11639575982) +[![badge_total_tests](https://img.shields.io/badge/tests-111-blue?style=plastic&logo=pytest&logoColor=white)](https://github.com/gitsetgopack/hw2/tree/main/tests) +[![badge_pylint](https://img.shields.io/badge/pylint-10.00-brightgreen?style=plastic)](https://github.com/gitsetgopack/MoneyManager/actions/runs/11639575982) +[![badge_black](https://img.shields.io/badge/black_formatter-passing-brightgreen?style=plastic&labelColor=black)](https://github.com/gitsetgopack/MoneyManager/actions/runs/11639575982) +[![badge_mypy](https://img.shields.io/badge/mypy-passing-brightgreen?style=plastic)](https://github.com/gitsetgopack/MoneyManager/actions/runs/11639575982) +[![badge_bandit](https://img.shields.io/badge/bandit-passing-brightgreen?style=plastic)](https://github.com/gitsetgopack/MoneyManager/actions/runs/11639575982) -## What DollarBot Can Do? +#### Standards -- Add/Record a new spending -- Calculate the sum of your expenditure for the current day/month -- Display your spending history -- Clear/Erase all your records -- Edit/Change any spending details if you wish to -- Recurring expense: - Add a recurring expense that adds a certain amount every month to the user's spending, for any given category. -- Custom category: - User can add a new category and delete an existing category as per the needs -- Budgeting: - User can see the budget value for the total expense and/or for each of the existing categories in the /display function -- Better visualization: - Added pie charts, bar graphs with and without budget lines for the user to have a look at the spending history in a better manner - Added bar graph in the /history command to see spending across different categories - User can see the daily and monthly expenses for spending history -- Delete particular expenses. -- Set a daily reminder to track your expenses for either the current day or the current month. -- Menu button to improve the UI. +![black](https://img.shields.io/badge/code%20style-black-black?style=plastic&) +![license](https://img.shields.io/github/license/gitsetgopack/MoneyManager?style=plastic&) +![maintained](https://img.shields.io/badge/Maintained%3F-yes-green.svg?style=plastic&) +[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.14027400.svg)](https://doi.org/10.5281/zenodo.14027400) -## What's new in V2.0!! -- Maintain two types of accounts for spending purposes. - - Checking Account - - Savings Account -- Change type of account before making a purchase. -- Log spending expenses and balances in multiple currencies (USD, INR, GBP, EUR, CAD, and JPY). -- Ability to add new or update overall/category-wise budget in multiple currencies. -- Get alerts when your balance falls below a threshold value. -- Get errors if you are spending more money than what's available in your account. -- Download your expenses record in CSV, PDF format on the go. -- Send your expenses record to any email address in CSV format. -- User studies of the application. -- Detailed documentation for each code file. -- New and improved test cases for the code base. -## Use Case -* One can use DollarBot to save time one would otherwise spend manually inputting numbers into a spreadsheet -- all the while encouraging you to spend less and save more with budgeting feature. +#### Stats -## Punch Line +![pr_open](https://img.shields.io/github/issues-pr/gitsetgopack/MoneyManager?style=plastic&) +![pr_close](https://img.shields.io/github/issues-pr-closed/gitsetgopack/MoneyManager?style=plastic&) +![issue_open](https://img.shields.io/github/issues/gitsetgopack/MoneyManager.svg?style=plastic&) +![issue_close](https://img.shields.io/github/issues-closed/gitsetgopack/MoneyManager.svg?style=plastic&) -* "If you can't measure it, you can't improve it." - Peter Drucker +![commits_since_last_project](https://img.shields.io/github/commits-since/gitsetgopack/MoneyManager/v2023.f.3.svg?style=plastic&) +![repo_size](https://img.shields.io/github/repo-size/gitsetgopack/MoneyManager?style=plastic&) +![forks](https://img.shields.io/github/forks/gitsetgopack/MoneyManager?style=plastic&) +![stars](https://img.shields.io/github/stars/gitsetgopack/MoneyManager?style=plastic&) +![downloads](https://img.shields.io/github/downloads/gitsetgopack/MoneyManager/total?style=plastic&) - Use DollarBot to measure your expenses and spend wisely. +#### Tools & Technologies -## Table of Contents - -- [Demo](#demo) -- [Tech Stack](#techstack) -- [Development Tools](#development-tools) -- [Installation](#installation) - - [Pre-requisites](#pre-requisites) - - [Actual installation](#actual-installation) -- [Testing](#testing) -- [Code Coverage](#code-coverage) -- [Usage](#usage) -- [Configuration](#configuration) -- [Road Map](#roadmap) -- [Contributing](#contributing) -- [License](#license) -- [Acknowledgements](#acknowledgements) -- [Support](#support) -- [FAQs](#faq) -- [Changelogs](#changelogs) -- [Code of Conduct](#code-of-conduct) - -## Demo - -[![Link to Demo](https://img.youtube.com/vi/E7EAHumVHhk/0.jpg)](https://www.youtube.com/watch?v=E7EAHumVHhk) - -## Techstack - -![Python](https://img.shields.io/badge/python-3670A0?style=for-the-badge&logo=python&logoColor=ffdd54) - -Starting from the external library dependencies to the testing of the application, everything is done in **Python 3**. - -Python specific tools used: - -- flake8 -- codecov -- pytest - -## Development tools - -![Git](https://img.shields.io/badge/git-%23F05033.svg?style=for-the-badge&logo=git&logoColor=white) ![GitHub](https://img.shields.io/badge/github-%23121011.svg?style=for-the-badge&logo=github&logoColor=white) ![Visual Studio Code](https://img.shields.io/badge/Visual%20Studio%20Code-0078d7.svg?style=for-the-badge&logo=visual-studio-code&logoColor=white) ![GitHub Actions](https://img.shields.io/badge/github%20actions-%232671E5.svg?style=for-the-badge&logo=githubactions&logoColor=white) - -## Installation - -### Pre-requisite Tasks - -Here are some pre-requisite tasks that you'll have to take complete before starting installation: - -1. In Telegram App/Desktop, search for "BotFather". Click on "Start", and enter the following command: -``` - /newbot -``` -2. Follow the instructions on screen: - - * Choose a name for your bot. - * Select a username for your bot that ends with "bot" (this is a rule Telegram enforces). - -3. BotFather will confirm the creation of your bot and provide a HTTP API access token. -4. Copy and save this token for future use. - -### Actual Installation - -The below instructions can be followed in order to set-up communication with the bot from your end in a span of few minutes! Let's get started: - -1. Clone this repository to your local system. -2. Start a terminal session in the directory where the project has been cloned. Run the following command to install the required dependencies: -``` - pip install -r requirements.txt -``` -3. In the directory where this repo has been cloned, please run the below command to execute a bash script to run the Telegram Bot: -``` - ./run.sh -``` -(OR) -``` - bash run.sh -``` -4. It will ask you to paste the API token you received from Telegram in pre-requisites step 4. -5. A successful run will generate a message on your terminal that says "TeleBot: Started polling." -6. In the Telegram app, search for your newly created bot by entering the username and open the same. - - Now, on Telegram, enter the "/start" or "/menu" command, and you are all set to track your expenses with DollarBot! - -## Testing - - We use pytest to perform testing on all unit tests together. The command needs to be run from the home directory of the project. The command is: - - ``` - python run -m pytest test/ - ``` - (OR) - ``` - python -m pytest test/ - ``` - - Currently we have over **155** tests covering all functionalities of the bot. See the below output for more details. - - ![Tests output](docs/images/tests.png) - -## Code Coverage - -Code coverage is part of the build. Every time new code is pushed to the repository, the build is run, and along with it, code coverage is computed. This can be viewed by selecting the build, and then choosing the codecov pop-up on hover. - -Locally, we use the coverage package in python for code coverage. The commands to check code coverage in python are as follows: - -``` -coverage run -m pytest test/ -coverage report -``` - -## Usage - -We have tried to make this application (bot) as easy as possible. You can use this bot to manage and track you daily expenses and not worry about loosing track of your expenses. As we also have given in a functionality of graphing and plotting and history of expenses, it becomes easy for the user to track expenses. -To make your experience even better, we have added User Tutorials for all the basic operations you can perform with DollarBot!! -Here you go: -- [Learn to add Balance!!](https://github.com/r-kala/DollarBot/blob/main/docs/UserTutorialDocuments/AddBalanceTutorial.md) -- [Learn to add Expenses!!](https://github.com/ymdatta/DollarBot/blob/main/docs/UserTutorialDocuments/AddExpenseTutorial.md) -- [Learn to add Recurring Expenses!!](https://github.com/ymdatta/DollarBot/blob/main/docs/UserTutorialDocuments/AddRecurringExpenseTutorial.md) -- [Learn to display Expense Data with Graphs!!](https://github.com/ymdatta/DollarBot/blob/main/docs/UserTutorialDocuments/DisplayTutorial.md) -- [Learn to download Expense History Data as CSV!!](https://github.com/ymdatta/DollarBot/blob/main/docs/UserTutorialDocuments/DownloadCSV.md) -- [Learn to get detailed Expense Data via Email!!](https://github.com/ymdatta/DollarBot/blob/main/docs/UserTutorialDocuments/EmailSpendingTutorial.md) -- [Learn to estimate your future daily/monthly Expenses!!](https://github.com/ymdatta/DollarBot/blob/main/docs/UserTutorialDocuments/EstimateExpenseTutorial.md) -- [Learn to access your detailed Expense History!!](https://github.com/ymdatta/DollarBot/blob/main/docs/UserTutorialDocuments/ExpenseHistory.md) -- [Learn to set Expense Reminders!!](https://github.com/ymdatta/DollarBot/blob/main/docs/UserTutorialDocuments/SetReminderTutorial.md) - -Link to feedback on DollarBot usage from users: [DollarBot Feedback Details](https://docs.google.com/document/d/1-2Ymohz238M43vACZSaMJzciNv_69CRBKFgrELbd-Bg/edit) - -## Configuration - -As a user, there's no need to configure any parameters -As a contributor, we have tried to make the system as decoupled as possible so that changes to one module/program doesn't affect other ones. With this being said, here are some configuration knobs that we have exposed for contributors: -1. Adding categories, -2. Removing categories, -3. Graphing changes, -4. Changing Telegram bot names etc. - -## :interrobang: Troubleshooting - - -1) Ensure that you have a valid bot token. You can obtain a token by creating a new bot on Telegram through the BotFather. -2) Double-check that the token is correctly inserted into your bot's code or configuration. -3) Make sure your bot has the necessary permissions to perform the actions you've programmed it for. -4) If the issue still persists, please consider writing us at dollarbot38@googlegroups.com and we will get back to you as soon as possible. -5) You're also free to report a bug in our repository and clearly stating the issue that you're facing. Please make sure to follow the guidelines mentioned in [CONTRIBUTING.md](https://github.com/ymdatta/DollarBot/blob/main/CONTRIBUTING.md) - -## Roadmap - -### Phase 1: - -- [x] Add/Record a new spending -- [x] Calculate the sum of your expenditure for the current day/month -- [x] Display your spending history -- [x] Clear/Erase all your records or individual expenses and edit/change any spending details if you wish to -- [x] Add a recurring expense that adds a certain amount every month to the user's spending, for any given category. -- [x] User can add a new category and delete an existing category as per the needs -- [x] User can see the budget value for the total expense and/or for each of the existing categories in the /display function -- [x] Added pie charts, bar graphs -- [x] Set a daily reminder to track your expenses for either the current day or the current month. -- [x] Menu button to improve the UI. - -### Phase 2 - -- [x] Maintain two types of accounts for spending purposes - Checking Account and Savings Account -- [x] Change type of account before making a purchase. -- [x] Log spending expenses and balances in multiple currencies (USD, INR, GBP, EUR, CAD, and JPY). -- [x] Ability to add new or update overall/category-wise budget in multiple currencies. -- [x] Get alerts when your balance falls below a threshold value. -- [x] Get errors if you are spending more money than what's available in your account. -- [x] Download your expenses record in CSV, PDF format on the go. -- [x] Send your expenses record to any email address in CSV format. -- [x] User studies of the application. -- [x] Detailed documentation for each code file. -- [x] New and improved test cases for the code base. - -### Future Enhancements - -- [ ] Ability to add expenses of multiple users. -- [ ] Ability to manage expenses among multiple users to calculate aggregate sum owed. -- [ ] Adding a more robust cloud database. -- [ ] Aggregation of an AI/ML based estimator that takes into account inflation. -- [ ] Improve User Interface design. -- [ ] Adding normal conversational abilities to the chatbot. -- [ ] Ability to store pictures of physical bills tied to expenses for user reference. - - ### Link to Project Board: [DollarBot Project Board](https://github.com/users/ymdatta/projects/3) - -## Contributing - -Thank you for your interest in contributing to DollarBot! Your contributions are highly valued, and this document will help you get started with the process. -We have a fully detailed comprehensive document to look out for if you're looking into contributing towards this project! -Please refer this [CONTRIBUTING.md](CONTRIBUTING.md) file. - -## LICENSE - -By contributing to DollarBot, you agree that your contributions will be licensed under the project's open-source license. It's important to understand and respect the licensing terms before contributing. The specific license terms for this project can be found in the [LICENSE](LICENSE) file. - -## :handshake: Contributors - -(in alphabetical order) - -1. Mohan Yelugoti (myelugo@ncsu.edu) -2. Rishabh Kala (rkala@ncsu.edu) -3. Sreehith Yachamaneni (syacham@ncsu.edu) - -## Support - -Please feel free to reach us at if you face any issues or for giving feedback in general. -If you have used our Dollar Bot, feel free to give your feedback https://forms.gle/W354pePL3xw74jj76 - -## FAQs - -1. Will we need a Telegram account to run this? - - A. Yes -2. Will we have to run multiple instances of the server for multiple clients? - - A. No -3. How can we reach out to the developers/contributors? - - A. Check the support section for details on reaching the developers. -4. What if I encounter a bug/have a feature request? - - A. Please raise an issue with the appropriate label to start a discussion and move forward. - -## Changelogs - -Please refer the following link for [CHANGELOG.md](CHANGELOG.md). - -## Code of Conduct - -Please note that we have a [Code of Conduct](CODE_OF_CONDUCT.md) that all contributors are expected to follow. It ensures that our community is welcoming and inclusive. - -***Enjoy using DollarBot. Make sure to follow the page for any new updates!*** +[![Python](https://img.shields.io/badge/python%203.12-3670A0?logo=python&logoColor=ffdd54)](https://www.python.org/downloads/release/python-3121/) +[![MongoDB](https://img.shields.io/badge/MongoDB-%234ea94b.svg?logo=mongodb&logoColor=white)](https://www.mongodb.com/) +[![FastAPI](https://img.shields.io/badge/FastAPI-009485.svg?logo=fastapi&logoColor=white)](https://fastapi.tiangolo.com/) +[![Docker](https://img.shields.io/badge/Docker-2496ED?logo=docker&logoColor=fff)](https://www.docker.com/) +[![GitHub](https://img.shields.io/badge/github-%23121011.svg?logo=github&logoColor=white)](https://github.com/) +[![GitHub Actions](https://img.shields.io/badge/github%20actions-%232671E5.svg?logo=githubactions&logoColor=white)](https://github.com/features/actions) +[![Linux](https://img.shields.io/badge/Linux-FCC624?logo=linux&logoColor=black)](https://www.linux.org/) +[![Visual Studio Code](https://img.shields.io/badge/Visual%20Studio%20Code-0078d7.svg?logo=visual-studio-code&logoColor=white)](https://code.visualstudio.com/) +[![Zoom](https://img.shields.io/badge/Zoom-2D8CFF?logo=zoom&logoColor=white)](https://www.zoom.com/) +[![DigitalOcean](https://img.shields.io/badge/DigitalOcean-%230167ff.svg?logo=digitalOcean&logoColor=white)]([#](https://www.digitalocean.com/)) +[![ChatGPT](https://img.shields.io/badge/ChatGPT-74aa9c?logo=openai&logoColor=white)](https://chatgpt.com/) diff --git a/api/__init__.py b/api/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/api/app.py b/api/app.py new file mode 100644 index 000000000..fbb3b227b --- /dev/null +++ b/api/app.py @@ -0,0 +1,32 @@ +""" +This module defines the main FastAPI application for Money Manager. +""" + +from contextlib import asynccontextmanager + +import uvicorn +from fastapi import FastAPI + +from api.routers import accounts, analytics, categories, expenses, users +from config import API_BIND_HOST, API_BIND_PORT + + +@asynccontextmanager +async def lifespan(_app: FastAPI): + """Lifespan function that handles app startup and shutdown""" + yield + # Handles the shutdown event to close the MongoDB client + await users.shutdown_db_client() + + +app = FastAPI(lifespan=lifespan) + +# Include routers for different functionalities +app.include_router(users.router) +app.include_router(accounts.router) +app.include_router(categories.router) +app.include_router(expenses.router) +app.include_router(analytics.router) + +if __name__ == "__main__": + uvicorn.run("app:app", host=API_BIND_HOST, port=API_BIND_PORT, reload=True) diff --git a/api/routers/__init__.py b/api/routers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/api/routers/accounts.py b/api/routers/accounts.py new file mode 100644 index 000000000..a2f3e9cc2 --- /dev/null +++ b/api/routers/accounts.py @@ -0,0 +1,193 @@ +""" +This module provides account-related API routes for the Money Manager application. +""" + +from typing import Optional + +from bson import ObjectId +from fastapi import APIRouter, Header, HTTPException +from motor.motor_asyncio import AsyncIOMotorClient +from pydantic import BaseModel + +from config import MONGO_URI + +from .users import verify_token + +router = APIRouter(prefix="/accounts", tags=["Accounts"]) + +# MongoDB setup +client: AsyncIOMotorClient = AsyncIOMotorClient(MONGO_URI) +db = client.mmdb +accounts_collection = db.accounts + + +class AccountCreate(BaseModel): + """Schema for creating a new account.""" + + name: str + balance: float + currency: str + + +class AccountUpdate(BaseModel): + """Schema for updating account information.""" + + name: Optional[str] = None + balance: Optional[float] = None + currency: Optional[str] = None + + +@router.post("/") +async def create_account(account: AccountCreate, token: str = Header(None)): + """ + Create a new account for the authenticated user. + + Args: + account (AccountCreate): The account details. + token (str): Authentication token. + + Returns: + dict: A message confirming the account creation. + """ + user_id = await verify_token(token) + existing_account = await accounts_collection.find_one( + {"user_id": user_id, "name": account.name} + ) + + if existing_account: + raise HTTPException(status_code=400, detail="Account type already exists") + + account_data = { + "user_id": user_id, + "name": account.name, + "balance": account.balance, + "currency": account.currency.upper(), + } + + result = await accounts_collection.insert_one(account_data) + if result.inserted_id: + return { + "message": "Account created successfully", + "account_id": str(result.inserted_id), + } + + raise HTTPException(status_code=500, detail="Failed to create account") + + +@router.get("/") +async def get_accounts(token: str = Header(None)): + """ + Get all accounts for the authenticated user. + + Args: + token (str): Authentication token. + + Returns: + dict: A list of all accounts for the user. + """ + user_id = await verify_token(token) + accounts = await accounts_collection.find({"user_id": user_id}).to_list(100) + if not accounts: + raise HTTPException(status_code=404, detail="No accounts found for the user") + + # Convert ObjectId to string for better readability + formatted_accounts = [ + {**account, "_id": str(account["_id"])} for account in accounts + ] + return {"accounts": formatted_accounts} + + +@router.get("/{account_id}") +async def get_account(account_id: str, token: str = Header(None)): + """ + Get details of a specific account for the authenticated user. + + Args: + account_id (str): The account ID. + token (str): Authentication token. + + Returns: + dict: The account details. + """ + user_id = await verify_token(token) + account = await accounts_collection.find_one( + {"_id": ObjectId(account_id), "user_id": user_id} + ) + + if not account: + raise HTTPException(status_code=404, detail="Account not found") + + account["_id"] = str( + account["_id"] + ) # Convert ObjectId to string for better readability + return {"account": account} + + +@router.put("/{account_id}") +async def update_account( + account_id: str, account_update: AccountUpdate, token: str = Header(None) +): + """ + Edit an existing account for the authenticated user. + + Args: + account_id (str): The account ID. + account_update (AccountUpdate): The updated account details. + token (str): Authentication token. + + Returns: + dict: A message confirming the account update. + """ + user_id = await verify_token(token) + account = await accounts_collection.find_one( + {"_id": ObjectId(account_id), "user_id": user_id} + ) + + if not account: + raise HTTPException(status_code=404, detail="Account not found") + if account_update.currency: + account_update.currency = account_update.currency.upper() + + # Update account details (balance, currency, and name) + update_data = { + "balance": account_update.balance, + "currency": account_update.currency, + "name": account_update.name, + } + + result = await accounts_collection.update_one( + {"_id": ObjectId(account_id)}, {"$set": update_data} + ) + + if result.modified_count == 1: + return {"message": "Account updated successfully"} + + raise HTTPException(status_code=500, detail="Failed to update account") + + +@router.delete("/{account_id}") +async def delete_account(account_id: str, token: str = Header(None)): + """ + Delete an existing account for the authenticated user. + + Args: + account_id (str): The account ID. + token (str): Authentication token. + + Returns: + dict: A message confirming the account deletion. + """ + user_id = await verify_token(token) + account = await accounts_collection.find_one( + {"_id": ObjectId(account_id), "user_id": user_id} + ) + + if not account: + raise HTTPException(status_code=404, detail="Account not found") + + result = await accounts_collection.delete_one({"_id": ObjectId(account_id)}) + + if result.deleted_count == 1: + return {"message": "Account deleted successfully"} + + raise HTTPException(status_code=500, detail="Failed to delete account") diff --git a/api/routers/analytics.py b/api/routers/analytics.py new file mode 100644 index 000000000..364913007 --- /dev/null +++ b/api/routers/analytics.py @@ -0,0 +1,170 @@ +""" +This module provides analytics endpoints for retrieving and visualizing +expense data. It includes routes to generate visualizations for expenses +from a specified number of days. +""" + +import base64 +import io +from datetime import datetime, timedelta + +import matplotlib.pyplot as plt +import pandas as pd +from fastapi import APIRouter, Header, HTTPException +from fastapi.responses import HTMLResponse +from motor.motor_asyncio import AsyncIOMotorClient + +from api.utils.auth import verify_token +from config import MONGO_URI + +# MongoDB setup +client: AsyncIOMotorClient = AsyncIOMotorClient(MONGO_URI) +db = client.mmdb +expenses_collection = db.expenses + +router = APIRouter(prefix="/analytics", tags=["Analytics"]) + + +@router.get("/expense/bar", response_class=HTMLResponse) +async def expense_bar(x_days: int, token: str = Header(None)): + """ + Endpoint to generate a bar chart of daily expenses for the previous x_days. + Args: + x_days (int): The number of days to look back for expense data. + token (str): Authorization token for user verification. + Returns: + HTMLResponse: An HTML page displaying the bar chart. + """ + # Verify token and retrieve user_id + user_id = await verify_token(token) + if not user_id: + raise HTTPException(status_code=401, detail="Invalid token") + + # Define the date range filter for MongoDB query + date_filter = { + "date": {"$gte": datetime.now() - timedelta(days=x_days)}, + "user_id": user_id, + } + + # Fetch expenses data from MongoDB + expenses = await expenses_collection.find(date_filter).to_list(length=1000) + + if not expenses: + raise HTTPException( + status_code=404, detail="No expenses found for the specified period" + ) + + # Convert to DataFrame and process data + df = pd.DataFrame(expenses) + df["date"] = pd.to_datetime(df["date"]) + daily_expenses = df.groupby(df["date"].dt.date)["amount"].sum() + + # Plotting the bar graph + plt.figure(figsize=(10, 6)) + ax = daily_expenses.plot(kind="bar", color="skyblue") + plt.title(f"Total Expenses per Day (Last {x_days} Days)") + plt.xlabel("Date") + plt.ylabel("Total Expense Amount") + plt.xticks(rotation=45) + plt.tight_layout() + + # Adding labels on top of each bar + for i, value in enumerate(daily_expenses): + ax.text( + i, + value + 0.5, + f"{value:.2f}", + ha="center", + va="bottom", + fontsize=10, + color="black", + ) + + # Convert the plot to a base64-encoded image + buf = io.BytesIO() + plt.savefig(buf, format="png") + plt.close() + buf.seek(0) + image_data = base64.b64encode(buf.getvalue()).decode("utf-8") + + # Return the HTML response with the embedded image + return HTMLResponse( + content=f""" + + Expense Bar Chart + +

Total Expenses per Day (Last {x_days} Days)

+ Expense Bar Chart + + + """ + ) + + +@router.get("/expense/pie", response_class=HTMLResponse) +async def expense_pie(x_days: int, token: str = Header(None)): + """ + Endpoint to generate a pie chart of expenses categorized by type for the previous x_days. + Args: + x_days (int): The number of days to look back for expense data. + token (str): Authorization token for user verification. + Returns: + HTMLResponse: An HTML page displaying the pie chart. + """ + # Verify token and retrieve user_id + user_id = await verify_token(token) + if not user_id: + raise HTTPException(status_code=401, detail="Invalid token") + + # Define the date range filter for MongoDB query + date_filter = { + "date": {"$gte": datetime.now() - timedelta(days=x_days)}, + "user_id": user_id, + } + + # Fetch expenses data from MongoDB + expenses = await expenses_collection.find(date_filter).to_list(length=1000) + + if not expenses: + raise HTTPException( + status_code=404, detail="No expenses found for the specified period" + ) + + # Convert to DataFrame and process data + df = pd.DataFrame(expenses) + df["date"] = pd.to_datetime(df["date"]) + + # Group by category and sum the amounts + category_expenses = df.groupby("category")["amount"].sum() + + # Plotting the pie chart + plt.figure(figsize=(8, 8)) + plt.pie( + category_expenses, + labels=category_expenses.index.astype(str).tolist(), + autopct="%1.1f%%", + startangle=140, + colors=["#FF9999", "#FF4D4D", "#FF0000"], + ) + plt.title(f"Expense Distribution by Category (Last {x_days} Days)") + plt.axis("equal") # Equal aspect ratio ensures that pie chart is circular. + + # Convert the plot to a base64-encoded image + buf = io.BytesIO() + plt.savefig(buf, format="png") + plt.close() + buf.seek(0) + image_data = base64.b64encode(buf.getvalue()).decode("utf-8") + + # Return the HTML response with the embedded image + return HTMLResponse( + content=f""" + + Expense Pie Chart + +

Expense Distribution by Category (Last {x_days} Days)

+ Expense Pie Chart + + + """ + ) diff --git a/api/routers/categories.py b/api/routers/categories.py new file mode 100644 index 000000000..bac462745 --- /dev/null +++ b/api/routers/categories.py @@ -0,0 +1,166 @@ +""" +This module provides endpoints for managing categories of a particular user. +""" + +from bson import ObjectId +from fastapi import APIRouter, Header, HTTPException +from motor.motor_asyncio import AsyncIOMotorClient +from pydantic import BaseModel + +from config import MONGO_URI + +from .users import verify_token + +router = APIRouter(prefix="/categories", tags=["Categories"]) + +# MongoDB setup +client: AsyncIOMotorClient = AsyncIOMotorClient(MONGO_URI) +db = client.mmdb +users_collection = db.users + + +class CategoryCreate(BaseModel): + """Schema for creating a new category.""" + + name: str + monthly_budget: float + + +class CategoryUpdate(BaseModel): + """Schema for updating a category.""" + + monthly_budget: float + + +@router.post("/") +async def create_category(category: CategoryCreate, token: str = Header(None)): + """ + Create a new category for the authenticated user. + + Args: + category (CategoryCreate): Category details. + token (str): Authentication token. + + Returns: + dict: A message confirming category creation. + """ + user_id = await verify_token(token) + + user = await users_collection.find_one({"_id": ObjectId(user_id)}) + if not user: + raise HTTPException(status_code=404, detail="User not found") + + if "categories" not in user: + user["categories"] = {} + + if category.name in user["categories"]: + raise HTTPException(status_code=400, detail="Category already exists") + + user["categories"][category.name] = {"monthly_budget": category.monthly_budget} + + await users_collection.update_one( + {"_id": ObjectId(user_id)}, {"$set": {"categories": user["categories"]}} + ) + + return {"message": "Category created successfully"} + + +@router.put("/{category_name}") +async def update_category( + category_name: str, category_update: CategoryUpdate, token: str = Header(None) +): + """ + Update an existing category's monthly budget. + + Args: + category_name (str): The name of the category to update. + category_update (CategoryUpdate): New category details. + token (str): Authentication token. + + Returns: + dict: A message confirming category update. + """ + user_id = await verify_token(token) + + user = await users_collection.find_one({"_id": ObjectId(user_id)}) + if not user or "categories" not in user or category_name not in user["categories"]: + raise HTTPException(status_code=404, detail="Category not found") + + if category_update.monthly_budget < 0: + raise HTTPException(status_code=400, detail="Monthly budget must be positive") + + user["categories"][category_name]["monthly_budget"] = category_update.monthly_budget + + await users_collection.update_one( + {"_id": ObjectId(user_id)}, {"$set": {"categories": user["categories"]}} + ) + + return {"message": "Category updated successfully"} + + +@router.get("/") +async def get_all_categories(token: str = Header(None)): + """ + Get all categories for the authenticated user. + + Args: + token (str): Authentication token. + + Returns: + dict: List of all categories. + """ + user_id = await verify_token(token) + + user = await users_collection.find_one({"_id": ObjectId(user_id)}) + if not user or "categories" not in user: + return {"categories": []} + + return {"categories": user["categories"]} + + +@router.get("/{category_name}") +async def get_category(category_name: str, token: str = Header(None)): + """ + Get details of a specific category for the authenticated user. + + Args: + category_name (str): The name of the category to fetch. + token (str): Authentication token. + + Returns: + dict: The category details. + """ + user_id = await verify_token(token) + + user = await users_collection.find_one({"_id": ObjectId(user_id)}) + if not user or "categories" not in user or category_name not in user["categories"]: + raise HTTPException(status_code=404, detail="Category not found") + + return {"category": user["categories"][category_name]} + + +@router.delete("/{category_name}") +async def delete_category(category_name: str, token: str = Header(None)): + """ + Delete an existing category for the authenticated user. + + Args: + category_name (str): The name of the category to delete. + token (str): Authentication token. + + Returns: + dict: A message confirming category deletion. + """ + user_id = await verify_token(token) + + user = await users_collection.find_one({"_id": ObjectId(user_id)}) + if not user or "categories" not in user or category_name not in user["categories"]: + raise HTTPException(status_code=404, detail="Category not found") + + del user["categories"][category_name] + + await users_collection.update_one( + {"_id": ObjectId(user_id)}, {"$set": {"categories": user["categories"]}} + ) + + return {"message": "Category deleted successfully"} diff --git a/api/routers/expenses.py b/api/routers/expenses.py new file mode 100644 index 000000000..f7d78b6ae --- /dev/null +++ b/api/routers/expenses.py @@ -0,0 +1,394 @@ +""" +This module provides endpoints for managing user expenses in the Money Manager application. +""" + +import datetime +from typing import Optional + +from bson import ObjectId +from currency_converter import CurrencyConverter # type: ignore +from fastapi import APIRouter, Header, HTTPException +from motor.motor_asyncio import AsyncIOMotorClient +from pydantic import BaseModel + +from api.utils.auth import verify_token +from config import MONGO_URI + +currency_converter = CurrencyConverter() + +router = APIRouter(prefix="/expenses", tags=["Expenses"]) + +# MongoDB setup +client: AsyncIOMotorClient = AsyncIOMotorClient(MONGO_URI) +db = client.mmdb +users_collection = db.users +expenses_collection = db.expenses +accounts_collection = db.accounts + + +def format_id(document): + """Convert MongoDB document ID to string.""" + document["_id"] = str(document["_id"]) + return document + + +def convert_currency(amount, from_cur, to_cur): + """Convert currency using the CurrencyConverter library.""" + if from_cur == to_cur: + return amount + try: + return currency_converter.convert(amount, from_cur, to_cur) + except Exception as e: + raise HTTPException( + status_code=400, detail=f"Currency conversion failed: {str(e)}" + ) from e + + +class ExpenseCreate(BaseModel): + """Model for creating an expense.""" + + amount: float + currency: str + category: str + description: Optional[str] = None + account_name: str = "Checking" + date: Optional[datetime.datetime] = None + + +class ExpenseUpdate(BaseModel): + """Model for updating an expense.""" + + amount: Optional[float] = None + currency: Optional[str] = None + category: Optional[str] = None + description: Optional[str] = None + # TODO: add account_name changing capability also + date: Optional[datetime.datetime] = None + + +@router.post("/") +async def add_expense(expense: ExpenseCreate, token: str = Header(None)): + """ + Add a new expense for the user. + + Args: + expense (ExpenseCreate): Expense details. + token (str): Authentication token. + + Returns: + dict: Message with expense details and updated balance. + """ + user_id = await verify_token(token) + account = await accounts_collection.find_one( + {"user_id": user_id, "name": expense.account_name} + ) + if not account: + raise HTTPException(status_code=400, detail="Invalid account type") + + user = await users_collection.find_one({"_id": ObjectId(user_id)}) + if not user: + raise HTTPException(status_code=404, detail="User not found") + + expense.currency = expense.currency.upper() + if expense.currency not in user["currencies"]: + raise HTTPException( + status_code=400, + detail=( + f"Currency type is not added to user account. " + f"Available currencies are {user['currencies']}" + ), + ) + converted_amount = convert_currency( + expense.amount, expense.currency, account["currency"] + ) + + if account["balance"] < converted_amount: + raise HTTPException( + status_code=400, + detail=f"Insufficient balance in {expense.account_name} account", + ) + + if expense.category not in user["categories"]: + raise HTTPException( + status_code=400, + detail=( + f"Category is not present in the user account. " + f"Available categories are {list(user['categories'])}" + ), + ) + + # Deduct amount from user's account balance + new_balance = account["balance"] - converted_amount + await accounts_collection.update_one( + {"_id": account["_id"]}, {"$set": {"balance": new_balance}} + ) + + # Convert date to datetime object or use current datetime if none is provided + expense_date = expense.date or datetime.datetime.now(datetime.timezone.utc) + # Record the expense + expense_data = expense.dict() + expense_data.update( + { + "user_id": user_id, + "date": expense_date, + } + ) + result = await expenses_collection.insert_one(expense_data) + + if result.inserted_id: + expense_data["date"] = expense_date # Ensure consistent formatting for response + return { + "message": "Expense added successfully", + "expense": format_id(expense_data), + "balance": new_balance, + } + raise HTTPException(status_code=500, detail="Failed to add expense") + + +@router.get("/") +async def get_expenses(token: str = Header(None)): + """ + Get all expenses for a user. + + Args: + token (str): Authentication token. + + Returns: + dict: List of expenses. + """ + user_id = await verify_token(token) + expenses = await expenses_collection.find({"user_id": user_id}).to_list(1000) + formatted_expenses = [format_id(expense) for expense in expenses] + return {"expenses": formatted_expenses} + + +@router.get("/{expense_id}") +async def get_expense(expense_id: str, token: str = Header(None)): + """ + Get a specific expense by ID. + + Args: + expense_id (str): ID of the expense. + token (str): Authentication token. + + Returns: + dict: Details of the specified expense. + """ + user_id = await verify_token(token) + expense = await expenses_collection.find_one( + {"user_id": user_id, "_id": ObjectId(expense_id)} + ) + if not expense: + raise HTTPException(status_code=404, detail="Expense not found") + return format_id(expense) + + +@router.delete("/all") +async def delete_all_expenses(token: str = Header(None)): + """ + Delete all expenses for the authenticated user and update account balances. + + Args: + token (str): Authentication token. + + Returns: + dict: Message indicating the number of expenses deleted. + """ + user_id = await verify_token(token) + + # Retrieve all expenses for the user before deletion + expenses = await expenses_collection.find({"user_id": user_id}).to_list(None) + if not expenses: + raise HTTPException(status_code=404, detail="No expenses found to delete") + + # Organize expenses by account name to sum them for each account + account_adjustments: dict[str, float] = {} + for expense in expenses: + account_name = expense.get("account_name") + amount = expense.get("amount", 0) + + # Find the account ID by name + account = await accounts_collection.find_one( + {"name": account_name, "user_id": user_id} + ) + if account: + account_id = account["_id"] + if account_id in account_adjustments: + account_adjustments[account_id] += amount + else: + account_adjustments[account_id] = amount + + # Update each account's balance + for account_id, total_expense_amount in account_adjustments.items(): + await accounts_collection.update_one( + {"_id": account_id, "user_id": user_id}, + {"$inc": {"balance": total_expense_amount}}, + ) + + # Delete all expenses + result = await expenses_collection.delete_many({"user_id": user_id}) + + return {"message": f"{result.deleted_count} expenses deleted successfully"} + + +@router.delete("/{expense_id}") +async def delete_expense(expense_id: str, token: str = Header(None)): + """ + Delete an expense by ID. + + Args: + expense_id (str): ID of the expense to delete. + token (str): Authentication token. + + Returns: + dict: Message with updated balance. + """ + user_id = await verify_token(token) + expense = await expenses_collection.find_one({"_id": ObjectId(expense_id)}) + + if not expense or expense["user_id"] != user_id: + raise HTTPException(status_code=404, detail="Expense not found") + + account_name = expense["account_name"] + account = await accounts_collection.find_one( + {"user_id": user_id, "name": account_name} + ) + if not account: + raise HTTPException(status_code=404, detail="Account not found") + + amount = convert_currency( + expense["amount"], expense["currency"], account["currency"] + ) + + # Refund the amount to user's account + new_balance = account["balance"] + amount + await accounts_collection.update_one( + {"_id": account["_id"]}, {"$set": {"balance": new_balance}} + ) + + # Delete the expense + result = await expenses_collection.delete_one({"_id": ObjectId(expense_id)}) + + if result.deleted_count == 1: + return {"message": "Expense deleted successfully", "balance": new_balance} + raise HTTPException(status_code=500, detail="Failed to delete expense") + + +@router.put("/{expense_id}") +# pylint: disable=too-many-locals +async def update_expense( + expense_id: str, expense_update: ExpenseUpdate, token: str = Header(None) +): + """ + Update an expense by ID. + + Args: + expense_id (str): ID of the expense to update. + expense_update (ExpenseUpdate): Expense update details. + token (str): Authentication token. + + Returns: + dict: Message with updated expense and balance. + """ + user_id = await verify_token(token) + user = await users_collection.find_one({"_id": ObjectId(user_id)}) + if not user: + raise HTTPException(status_code=404, detail="User not found") + + expense = await expenses_collection.find_one({"_id": ObjectId(expense_id)}) + if not expense or expense["user_id"] != user_id: + raise HTTPException(status_code=404, detail="Expense not found") + + update_fields: dict[str, str | float | datetime.datetime] = {} + + def validate_currency(): + if expense_update.currency: + expense_update.currency = expense_update.currency.upper() + if expense_update.currency not in user["currencies"]: + raise HTTPException( + status_code=400, + detail=( + f"Currency type is not added to user account. " + f"Available currencies are {user['currencies']}" + ), + ) + update_fields["currency"] = expense_update.currency + + async def validate_amount(): + nonlocal new_balance + if expense_update.amount is not None: + update_fields["amount"] = expense_update.amount + + # Adjust the user's balance + # Convert old and new amounts to the account currency to determine balance adjustment + original_amount_converted = convert_currency( + expense["amount"], expense["currency"], account["currency"] + ) + new_amount_converted = convert_currency( + expense_update.amount, + expense_update.currency or expense["currency"], + account["currency"], + ) + + difference = new_amount_converted - original_amount_converted + new_balance = account["balance"] - difference + + if new_balance < 0: + raise HTTPException( + status_code=400, detail="Insufficient balance to update the expense" + ) + await accounts_collection.update_one( + {"_id": account["_id"]}, {"$set": {"balance": new_balance}} + ) + + def validate_category(): + if expense_update.category: + if expense_update.category not in user["categories"]: + raise HTTPException( + status_code=400, + detail=( + f"Category is not present in the user account. " + f"Available categories are {list(user['categories'])}" + ), + ) + update_fields["category"] = expense_update.category + + def validate_description(): + if expense_update.description: + update_fields["description"] = expense_update.description + + def validate_date(): + if expense_update.date: + update_fields["date"] = expense_update.date + + # Run validations + validate_currency() + account_name = expense["account_name"] + account = await accounts_collection.find_one( + {"user_id": user_id, "name": account_name} + ) + if not account: + raise HTTPException(status_code=404, detail="Account not found") + + new_balance = account["balance"] + await validate_amount() + validate_category() + validate_description() + validate_date() + + if not update_fields: + raise HTTPException(status_code=400, detail="No fields to update") + + result = await expenses_collection.update_one( + {"_id": ObjectId(expense_id)}, {"$set": update_fields} + ) + if result.modified_count == 1: + updated_expense = await expenses_collection.find_one( + {"_id": ObjectId(expense_id)} + ) + return { + "message": "Expense updated successfully", + "updated_expense": format_id(updated_expense), + "balance": new_balance, + } + raise HTTPException(status_code=500, detail="Failed to update expense") diff --git a/api/routers/users.py b/api/routers/users.py new file mode 100644 index 000000000..e8e1fc6fd --- /dev/null +++ b/api/routers/users.py @@ -0,0 +1,310 @@ +""" +This module provides user-related API routes for the Money Manager application. +""" + +import datetime +from typing import Optional + +from bson import ObjectId +from fastapi import APIRouter, Depends, Header, HTTPException +from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from jose import jwt +from motor.motor_asyncio import AsyncIOMotorClient +from pydantic import BaseModel + +from api.utils.auth import verify_token +from config import MONGO_URI, TOKEN_ALGORITHM, TOKEN_SECRET_KEY + +ACCESS_TOKEN_EXPIRE_MINUTES = 30 * 24 * 60 + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token") + +router = APIRouter(prefix="/users", tags=["Users"]) + +# MongoDB setup +client: AsyncIOMotorClient = AsyncIOMotorClient(MONGO_URI) +db = client.mmdb +users_collection = db.users +tokens_collection = db.tokens +accounts_collection = db.accounts +expenses_collection = db.expenses + + +class UserCreate(BaseModel): + """Schema for creating a user.""" + + username: str + password: str + + +class UserUpdate(BaseModel): + """Schema for updating user information.""" + + password: Optional[str] = None + currencies: Optional[list] = None + + +def format_id(document): + """Format the MongoDB document ID to string.""" + document["_id"] = str(document["_id"]) + return document + + +def create_access_token(data: dict, expires_delta: datetime.timedelta): + """Create an access token with an expiration time.""" + to_encode = data.copy() + expire = datetime.datetime.now(datetime.UTC) + expires_delta + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, TOKEN_SECRET_KEY, algorithm=TOKEN_ALGORITHM) + return encoded_jwt + + +@router.post("/") +async def create_user(user: UserCreate): + """Create a new user along with default accounts.""" + existing_user = await users_collection.find_one({"username": user.username}) + if existing_user: + raise HTTPException(status_code=400, detail="Username already exists") + if not user.username or not user.password: + raise HTTPException(status_code=422, detail="Invalid credential") + + default_categories = { + "Food": {"monthly_budget": 500.0}, + "Groceries": {"monthly_budget": 200.0}, + "Utilities": {"monthly_budget": 150.0}, + "Transport": {"monthly_budget": 100.0}, + "Shopping": {"monthly_budget": 300.0}, + "Miscellaneous": {"monthly_budget": 50.0}, + } + + default_currencies = ["USD", "INR", "GBP", "EUR"] + + # Insert the new user + user_data = { + "username": user.username, + "password": user.password, # In a real application, you should hash the password + "categories": default_categories, + "currencies": default_currencies, + } + result = await users_collection.insert_one(user_data) + user_id = result.inserted_id + if not user_id: + raise HTTPException(status_code=500, detail="Failed to create user") + + # Create default accounts for the user + default_accounts = [ + { + "user_id": str(user_id), + "name": "Checking", + "balance": 1000, + "currency": "USD", + }, + { + "user_id": str(user_id), + "name": "Savings", + "balance": 10000, + "currency": "USD", + }, + ] + + await accounts_collection.insert_many(default_accounts) + + return {"message": "User and default accounts created successfully"} + + +@router.get("/") +async def get_user(token: str = Header(None)): + """Get user details.""" + user_id = await verify_token(token) + user = await users_collection.find_one({"_id": ObjectId(user_id)}) + if not user: + raise HTTPException(status_code=404, detail="User not found") + return format_id(user) + + +@router.put("/") +async def update_user(user_update: UserUpdate, token: str = Header(None)): + """Update user information such as password, currencies.""" + user_id = await verify_token(token) + update_fields = user_update.dict(exclude_unset=True) + user: Optional[dict] = await users_collection.find_one({"_id": ObjectId(user_id)}) + if not user: + raise HTTPException(status_code=404, detail="User not found") + + if "password" in update_fields and update_fields["password"]: + # In a real application, you should hash the password + update_fields["password"] = update_fields["password"] + + if "currencies" in update_fields and isinstance(update_fields["currencies"], list): + new_currencies = list( + set(user.get("currencies", []) + update_fields["currencies"]) + ) + update_fields["currencies"] = new_currencies + try: + result = await users_collection.update_one( + {"_id": ObjectId(user_id)}, {"$set": update_fields} + ) + if result.modified_count == 1: + updated_user = await users_collection.find_one({"_id": ObjectId(user_id)}) + return { + "message": "User updated successfully", + "updated_user": format_id(updated_user), + } + raise HTTPException(status_code=400, detail="Nothing to modify") + except Exception as e: + raise HTTPException(status_code=500, detail="Failed to update user") from e + + +@router.delete("/") +async def delete_user(token: str = Header(None)): + """Delete a user and all associated accounts, tokens, and expenses.""" + user_id = await verify_token(token) + await tokens_collection.delete_many({"user_id": user_id}) + await accounts_collection.delete_many({"user_id": user_id}) + await expenses_collection.delete_many({"user_id": user_id}) + result = await users_collection.delete_one({"_id": ObjectId(user_id)}) + if result.deleted_count == 1: + return {"message": "User deleted successfully"} + raise HTTPException(status_code=500, detail="Failed to delete user") + + +@router.post("/token/") +async def create_token( + form_data: OAuth2PasswordRequestForm = Depends(), + token_expires: float = ACCESS_TOKEN_EXPIRE_MINUTES, +): + """Create an access token for a user.""" + user = await users_collection.find_one({"username": form_data.username}) + if not user or user["password"] != form_data.password: + raise HTTPException(status_code=401, detail="Incorrect username or password") + + access_token_expires = datetime.timedelta(minutes=token_expires) + access_token = create_access_token( + data={"sub": str(user["_id"]), "username": user["username"]}, + expires_delta=access_token_expires, + ) + + result = await tokens_collection.insert_one( + { + "user_id": str(user["_id"]), + "token": access_token, + "expires_at": datetime.datetime.now(datetime.UTC) + access_token_expires, + "token_type": "bearer", + }, + ) + + if result.inserted_id: + token_data = await tokens_collection.find_one({"_id": result.inserted_id}) + return { + "message": "Token created successfully", + "result": format_id(token_data), + } + + +@router.get("/token/") +async def get_tokens(token: str = Header(None)): + """ + Get all tokens for the authenticated user. + + Args: + token (str): Authentication token. + + Returns: + dict: List of all tokens for the user. + """ + user_id = await verify_token(token) + tokens = await tokens_collection.find({"user_id": user_id}).to_list(1000) + formatted_tokens = [format_id(token) for token in tokens] + # Convert datetime to ISO format in each token document + for tok in formatted_tokens: + if "expires_at" in tok and isinstance(tok["expires_at"], datetime.datetime): + tok["expires_at"] = tok["expires_at"].isoformat() + + return {"tokens": formatted_tokens} + + +@router.get("/token/{token_id}") +async def get_token(token_id: str, token: str = Header(None)) -> dict: + """ + Get a specific token's details. + + Args: + token_id (str): The ID of the token. + token (str): Authentication token. + + Returns: + dict: Details of the specified token. + """ + user_id = await verify_token(token) + token_data = await tokens_collection.find_one( + {"user_id": user_id, "_id": ObjectId(token_id)} + ) + if not token_data: + raise HTTPException(status_code=404, detail="Token not found") + + formatted_token = format_id(token_data) + # Convert datetime to ISO format + if "expires_at" in formatted_token and isinstance( + formatted_token["expires_at"], datetime.datetime + ): + formatted_token["expires_at"] = formatted_token["expires_at"].isoformat() + + return formatted_token + + +@router.put("/token/{token_id}") +async def update_token(token_id: str, new_expiry: float, token: str = Header(None)): + """ + Update the expiration time for the current token. + + Args: + token_id (str): The ID of the token. + new_expiry (float): New expiry time in minutes. + token (str): Authentication token. + + Returns: + dict: Success message or error. + """ + user_id = await verify_token(token) + updated_expiry = datetime.timedelta(minutes=new_expiry) + new_expiry_time = datetime.datetime.now(datetime.UTC) + updated_expiry + + # Correct the filter to use _id for the token document + result = await tokens_collection.update_one( + {"user_id": user_id, "_id": ObjectId(token_id)}, + {"$set": {"expires_at": new_expiry_time}}, + ) + + if result.modified_count == 1: + return {"message": "Token expiration updated successfully"} + + raise HTTPException(status_code=500, detail="Failed to update token expiration") + + +@router.delete("/token/{token_id}") +async def delete_token(token_id: str, token: str = Header(None)): + """ + Delete a specific token by its ID. + + Args: + token_id (str): The ID of the token to be deleted. + token (str): Authentication token for verifying the user. + + Returns: + dict: Message indicating whether the token was successfully deleted. + """ + user_id = await verify_token(token) + result = await tokens_collection.delete_one( + {"user_id": user_id, "_id": ObjectId(token_id)} + ) + + if result.deleted_count == 1: + return {"message": "Token deleted successfully"} + + raise HTTPException(status_code=404, detail="Token not found") + + +@router.on_event("shutdown") +async def shutdown_db_client(): + """Shutdown event for MongoDB client.""" + client.close() diff --git a/api/utils/__init__.py b/api/utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/api/utils/auth.py b/api/utils/auth.py new file mode 100644 index 000000000..70a0a6c65 --- /dev/null +++ b/api/utils/auth.py @@ -0,0 +1,34 @@ +"""Utilities to manage authentication""" + +from fastapi import HTTPException +from jose import JWTError, jwt +from motor.motor_asyncio import AsyncIOMotorClient + +from config import MONGO_URI, TOKEN_ALGORITHM, TOKEN_SECRET_KEY + +client: AsyncIOMotorClient = AsyncIOMotorClient(MONGO_URI) +db = client.mmdb +users_collection = db.users +tokens_collection = db.tokens + + +async def verify_token(token: str): + """Verify the validity of an access token.""" + if token is None: + raise HTTPException(status_code=401, detail="Token is missing") + try: + payload = jwt.decode(token, TOKEN_SECRET_KEY, algorithms=[TOKEN_ALGORITHM]) + user_id = payload.get("sub") + token_exists = await tokens_collection.find_one( + {"user_id": user_id, "token": token} + ) + if not token_exists: + raise HTTPException(status_code=401, detail="Token does not exist") + return user_id + except JWTError as e: + if "Signature has expired" in str(e): + await tokens_collection.delete_one({"token": token}) + raise HTTPException(status_code=401, detail="Token has expired") from e + raise HTTPException( + status_code=401, detail="Invalid authentication credentials" + ) from e diff --git a/bots/__init__.py b/bots/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/bots/telegram/__init__.py b/bots/telegram/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/bots/telegram/bot.py b/bots/telegram/bot.py new file mode 100644 index 000000000..2924cec73 --- /dev/null +++ b/bots/telegram/bot.py @@ -0,0 +1,999 @@ +"""POC: TELEGRAM bot""" + +# pylint: skip-file + +import datetime +import os + +import pandas as pd +import requests +from bson import ObjectId +from jose import jwt +from motor.motor_asyncio import AsyncIOMotorClient +from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update +from telegram.ext import ( + Application, + CallbackQueryHandler, + CommandHandler, + ContextTypes, + MessageHandler, + filters, +) + +from config import MONGO_URI, TELEGRAM_BOT_API_BASE_URL, TELEGRAM_BOT_TOKEN + +# Constants +API_BASE_URL = TELEGRAM_BOT_API_BASE_URL +TSK = "None" +TOKEN_ALGORITHM = "HS256" + +client = AsyncIOMotorClient(MONGO_URI) +db = client.mmdb +users_collection = db.users +tokens_collection = db.tokens +accounts_collection = db.accounts +expenses_collection = db.expenses +telegram_collection = db.Telegram +user_tokens = {} + +# Global dictionaries to track login and signup states and temporarily store usernames and passwords +LOGIN_STATE = {} +SIGNUP_STATE = {} +USERNAMES = {} +PASSWORDS = {} + + +########################################################## +# AUTHENTICATION +########################################################## + + +def authenticate(func): + async def wrapper( + update: Update, context: ContextTypes.DEFAULT_TYPE, *args, **kwargs + ): + # print("Checking auth") + user_id = update.message.chat_id + # print(user_id) + user = await telegram_collection.find_one({"telegram_id": user_id}) + # print(user) + if user and user.get("token"): + return await func(update, context, *args, **kwargs, token=user.get("token")) + else: + await update.message.reply_text("You are not authenticated. Please /login") + + return wrapper + + +async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE): + """ + Handle the /start command, providing a welcome message and instructions to log in. + """ + if update.message: + await update.message.reply_text( + "Welcome to Money Manager! Please signup using /signup or log in using /login" + ) + + +async def signup_command(update: Update, context: ContextTypes.DEFAULT_TYPE): + """ + Initiate the signup process, prompting for the username first. + """ + user_id = update.message.chat_id if update.message else None + SIGNUP_STATE[user_id] = "awaiting_username" + await update.message.reply_text("To sign up, please enter your desired username:") + + +async def login_command(update: Update, context: ContextTypes.DEFAULT_TYPE): + """ + Initiate the login process, prompting for the username first. + """ + user_id = update.message.chat_id if update.message else None + LOGIN_STATE[user_id] = "awaiting_username" + await update.message.reply_text("Please enter your username:") + + +async def attempt_signup(update: Update, username: str, password: str): + """ + Attempt to sign the user up with the provided username and password. + """ + response = requests.post( + f"{API_BASE_URL}/users/", json={"username": username, "password": password} + ) + + if response.status_code == 200: + print("SIGNUP", username, password) + user_id = update.message.chat_id if update.message else None + tokenization = requests.post( + f"{API_BASE_URL}/users/token/?token_expires=43200", + data={"username": username, "password": password}, + ) + token = tokenization.json()["result"]["token"] + + user_data = { + "username": username, + "token": token, + "telegram_id": user_id, + } + + existing_user = await telegram_collection.find_one({"telegram_id": user_id}) + if existing_user: + await telegram_collection.update_one( + {"telegram_id": user_id}, {"$set": user_data} + ) + else: + await telegram_collection.insert_one(user_data) + + await update.message.reply_text( + "Signup successful! You can now log in using /login." + ) + return + await update.message.reply_text( + f"An error occurred: {response.json().get('detail', 'Unknown error')}\nPlease try again by /singup or /login" + ) + + +async def attempt_login(update: Update, username: str, password: str): + """ + Attempt to log the user in with the provided username and password. + """ + response = requests.post( + f"{API_BASE_URL}/users/token/?token_expires=43200", + data={"username": username, "password": password}, + ) + print("LOGGING IN") + print(response.json()) + if response.status_code == 200: + token = response.json()["result"]["token"] + user_id = update.message.chat_id if update.message else None + + user = await telegram_collection.find_one({"username": username}) + if user: + await telegram_collection.update_one( + {"telegram_id": user_id}, + {"$set": {"token": token, "username": username}}, + ) + else: + user_data = { + "username": username, + "token": token, + "telegram_id": user_id, + } + await telegram_collection.insert_one(user_data) + + await update.message.reply_text("Login successful!") + else: + await update.message.reply_text( + f"Login failed: {response.json()['detail']}\n /signup if you haven't, otherwise /login" + ) + + +########################################################## +# CATEGORIES +########################################################## + + +@authenticate +async def categories_command( + update: Update, context: ContextTypes.DEFAULT_TYPE, **kwargs +): + """ + Show buttons for category actions (View, Add, Edit, Delete). + """ + + keyboard = [ + [InlineKeyboardButton("View Categories", callback_data="view_category")], + [InlineKeyboardButton("Add Category", callback_data="add_category")], + [InlineKeyboardButton("Edit Category", callback_data="edit_category")], + [InlineKeyboardButton("Delete Category", callback_data="delete_category")], + ] + reply_markup = InlineKeyboardMarkup(keyboard) + await update.message.reply_text( + "Choose an action for categories:", reply_markup=reply_markup + ) + + +@authenticate +async def view_category_handler(query, context, **kwargs): + """ + Handle viewing categories with table format. + """ + headers = {"token": kwargs.get("token", None)} + response = requests.get(f"{API_BASE_URL}/categories/", headers=headers) + + if response.status_code == 200: + categories_data = response.json().get("categories", {}) + + # Prepare table header and rows with fixed-width formatting + header = f"{'Category':<20} {'Monthly Budget':>15}\n" + separator = "-" * 35 + rows = [ + f"{category:<20} {details['monthly_budget']:>15.2f}" + for category, details in categories_data.items() + ] + + # Combine header, separator, and rows into one string + table_str = ( + f"Here are your available categories with budgets:\n\n\n```{header}{separator}\n" + + "\n".join(rows) + + "\n```" + ) + + # Send the formatted table as a message with monospaced font + await query.edit_message_text(table_str, parse_mode="MarkdownV2") + else: + error_message = response.json().get("detail", "Unable to fetch categories.") + await query.edit_message_text(f"Error: {error_message}") + + +async def add_category_handler(query, context): + """ + Handle adding a new category. + """ + await query.edit_message_text("Please enter the name of the new category:") + context.user_data["category_action"] = "add" + context.user_data["category_step"] = "add_name" + + +@authenticate +async def edit_category_handler(query, context, **kwargs): + """ + Display the user's categories as inline buttons to select for editing. + """ + + headers = {"token": kwargs.get("token", None)} + response = requests.get(f"{API_BASE_URL}/categories/", headers=headers) + + if response.status_code == 200: + categories_data = response.json().get("categories", {}) + + # Create buttons for each category + keyboard = [ + [InlineKeyboardButton(category, callback_data=f"edit_{category}")] + for category in categories_data.keys() + ] + reply_markup = InlineKeyboardMarkup(keyboard) + await query.edit_message_text( + "Select a category to edit:", reply_markup=reply_markup + ) + context.user_data["category_action"] = "edit" + else: + error_message = response.json().get("detail", "Unable to fetch categories.") + await query.edit_message_text(f"Error: {error_message}") + + +async def handle_edit_category_selection( + update: Update, context: ContextTypes.DEFAULT_TYPE +): + """ + Handle the selection of a category for editing and prompt for a new budget. + """ + query = update.callback_query + await query.answer() # Acknowledge the callback + selected_category = query.data.replace("edit_", "") + + # Store the selected category and set up for the next step to enter a new budget + context.user_data["selected_category"] = selected_category + context.user_data["category_action"] = "edit" + context.user_data["category_step"] = "edit_budget" # Set the next expected step + + # Prompt the user to enter the new budget + await query.edit_message_text( + f"Enter the new monthly budget for {selected_category}:" + ) + + +@authenticate +async def delete_category_handler(query, context, **kwargs): + """ + Display the user's categories as inline buttons for deletion. + """ + headers = {"token": kwargs.get("token", None)} + response = requests.get(f"{API_BASE_URL}/categories/", headers=headers) + + if response.status_code == 200: + categories_data = response.json().get("categories", {}) + + # Create buttons for each category + keyboard = [ + [InlineKeyboardButton(category, callback_data=f"delete_{category}")] + for category in categories_data.keys() + ] + reply_markup = InlineKeyboardMarkup(keyboard) + await query.edit_message_text( + "Select a category to delete:", reply_markup=reply_markup + ) + context.user_data["category_action"] = "delete" + else: + error_message = response.json().get("detail", "Unable to fetch categories.") + await query.edit_message_text(f"Error: {error_message}") + + +@authenticate +async def handle_delete_category_selection( + update: Update, context: ContextTypes.DEFAULT_TYPE, **kwargs +): + """ + Handle the selection of a category for deletion and confirm deletion. + """ + query = update.callback_query + await query.answer() # Acknowledge the callback + selected_category = query.data.replace("delete_", "") + + # Confirm deletion with the user + headers = {"token": kwargs.get("token", None)} + response = requests.delete( + f"{API_BASE_URL}/categories/{selected_category}", headers=headers + ) + + if response.status_code == 200: + await query.edit_message_text( + f"The category '{selected_category}' has been successfully deleted." + ) + else: + error_message = response.json().get("detail", "Failed to delete category.") + await query.edit_message_text(f"Error: {error_message}") + + +@authenticate +async def delete_selected_category(query, context, selected_category, **kwargs): + """ + Delete the specified category and notify the user of the result. + """ + headers = {"token": kwargs.get("token", None)} + response = requests.delete( + f"{API_BASE_URL}/categories/{selected_category}", headers=headers + ) + + # Handle the response + if response.status_code == 200: + await query.edit_message_text( + f"The category '{selected_category}' has been successfully deleted." + ) + else: + error_message = response.json().get("detail", "Failed to delete category.") + await query.edit_message_text(f"Error: {error_message}") + + +@authenticate +async def finalize_category_addition( + update: Update, context: ContextTypes.DEFAULT_TYPE, **kwargs +): + """ + Finalize adding a new category by making an API request and confirm to the user. + """ + new_category_name = context.user_data.get("new_category_name") + new_category_budget = context.user_data.get("new_category_budget") + + # Prepare headers and payload for the API request + headers = { + "accept": "application/json", + "token": kwargs.get("token", None), + "Content-Type": "application/json", + } + payload = {"name": new_category_name, "monthly_budget": new_category_budget} + + # Log request details for debugging + print("Sending request to add category") + print(f"Headers: {headers}") + print(f"Payload: {payload}") + print( + f"API Endpoint: https://frightening-orb-747j6q4gjjqcwr7j-9999.app.github.dev/categories/" + ) + + try: + # Send the request to add the category + response = requests.post( + f"{API_BASE_URL}/categories/", json=payload, headers=headers + ) + + # Log response details + print(f"Response status code: {response.status_code}") + print(f"Response content: {response.text}") + + if response.status_code == 200: + await update.message.reply_text( + f"New category added successfully!\n\nCategory: {new_category_name}\nMonthly Budget: {new_category_budget}" + ) + else: + error_message = response.json().get( + "detail", "An error occurred while adding the category." + ) + await update.message.reply_text( + f"Failed to add category. Error: {error_message}" + ) + + except Exception as e: + print(f"An error occurred: {e}") + await update.message.reply_text( + "An unexpected error occurred while trying to add the category." + ) + + # Clear category addition state + context.user_data.pop("category_action", None) + context.user_data.pop("category_step", None) + context.user_data.pop("new_category_name", None) + context.user_data.pop("new_category_budget", None) + + +########################################################## +# EXPENSES +########################################################## + + +@authenticate +async def add_command(update: Update, context: ContextTypes.DEFAULT_TYPE, **kwargs): + """ + Check if the user is authenticated, then handle adding an expense. + If the user is not logged in, prompt them to log in first. + """ + user_id = update.message.chat_id if update.message else None + if user_id not in user_tokens: + await update.message.reply_text( + "Please log in using /login command before adding expenses." + ) + return + + # Token for the authenticated user + headers = {"token": kwargs.get("token", None)} + response = requests.post( + f"{API_BASE_URL}/expenses/", + json={"amount": 100, "currency": "USD", "category": "Food"}, + headers=headers, + ) + + if response.status_code == 200: + await update.message.reply_text("Expense added successfully!") + else: + error_message = response.json().get("detail", "Unknown error") + await update.message.reply_text( + f"Failed to add expense. Error: {error_message}" + ) + + +async def view_command(update: Update, context: ContextTypes.DEFAULT_TYPE): + """ + Placeholder function for viewing balance or account details. + """ + await update.message.reply_text("Hello, Your balance is") + + +async def expense_command(update: Update, context: ContextTypes.DEFAULT_TYPE): + keyboard = [ + [InlineKeyboardButton("Add Expense", callback_data="add_expense")], + [InlineKeyboardButton("View Expenses", callback_data="view_expenses")], + [InlineKeyboardButton("Update Expense", callback_data="update_expense")], + [InlineKeyboardButton("Delete Expense", callback_data="delete_expense")], + ] + reply_markup = InlineKeyboardMarkup(keyboard) + await update.message.reply_text( + "Choose an expense action:", reply_markup=reply_markup + ) + + +@authenticate +async def add_expense_handler(query, context, **kwargs): + """ + Start the process of adding a new expense by prompting for the amount. + """ + await query.edit_message_text("Please enter the amount:") + context.user_data["expense_action"] = "add" + context.user_data["expense_step"] = "amount" + + +@authenticate +async def view_expenses_handler(query, context, **kwargs): + """ + Handle viewing expenses and display them in a formatted list. + """ + headers = {"token": kwargs.get("token", None)} + response = requests.get(f"{API_BASE_URL}/expenses/", headers=headers) + + if response.status_code == 200: + expenses_data = response.json().get("expenses", []) + expense_list = "\n".join( + [ + f"{i+1}. {exp['category']} - {exp['amount']} {exp['currency']} on {exp['date']}" + for i, exp in enumerate(expenses_data) + ] + ) + await query.edit_message_text(f"Your recent expenses:\n\n{expense_list}") + else: + await query.edit_message_text("Unable to retrieve expenses.") + + +@authenticate +async def update_expense_handler(query, context, **kwargs): + """ + Start the process to update an expense by selecting one from the list. + """ + headers = {"token": kwargs.get("token", None)} + response = requests.get(f"{API_BASE_URL}/expenses/", headers=headers) + + if response.status_code == 200: + expenses_data = response.json().get("expenses", []) + + # Display each expense as a button to select for updating + keyboard = [ + [ + InlineKeyboardButton( + f"{exp['category']} - {exp['amount']} {exp['currency']}", + callback_data=f"update_{exp['_id']}", + ) + ] + for exp in expenses_data + ] + reply_markup = InlineKeyboardMarkup(keyboard) + await query.edit_message_text( + "Select an expense to update:", reply_markup=reply_markup + ) + else: + await query.edit_message_text("Error fetching expenses for update.") + + +@authenticate +async def delete_expense_handler(query, context, **kwargs): + """ + Handle selecting and deleting an expense. + """ + headers = {"token": kwargs.get("token", None)} + response = requests.get(f"{API_BASE_URL}/expenses/", headers=headers) + + if response.status_code == 200: + expenses_data = response.json().get("expenses", []) + + # Display each expense as a button to select for deletion + keyboard = [ + [ + InlineKeyboardButton( + f"{exp['category']} - {exp['amount']} {exp['currency']}", + callback_data=f"delete_{exp['_id']}", + ) + ] + for exp in expenses_data + ] + reply_markup = InlineKeyboardMarkup(keyboard) + await query.edit_message_text( + "Select an expense to delete:", reply_markup=reply_markup + ) + else: + await query.edit_message_text("Error fetching expenses for deletion.") + + +async def handle_expense_update_selection( + update: Update, context: ContextTypes.DEFAULT_TYPE +): + """ + Handle selection of an expense for updating and prompt for new amount. + """ + query = update.callback_query + await query.answer() + expense_id = query.data.replace("update_", "") + context.user_data["expense_id"] = expense_id + context.user_data["expense_action"] = "update" + context.user_data["expense_step"] = "new_amount" + + await query.edit_message_text("Please enter the new amount for this expense:") + + +async def handle_expense_delete_selection( + update: Update, context: ContextTypes.DEFAULT_TYPE +): + """ + Handle the confirmation and deletion of a selected expense. + """ + query = update.callback_query + await query.answer() + expense_id = query.data.replace("delete_", "") + headers = {"token": context.user_data.get("token", None)} + + response = requests.delete(f"{API_BASE_URL}/expenses/{expense_id}", headers=headers) + + if response.status_code == 200: + await query.edit_message_text("Expense deleted successfully!") + else: + await query.edit_message_text("Failed to delete expense.") + + +@authenticate +async def finalize_expense_update( + update: Update, context: ContextTypes.DEFAULT_TYPE, **kwargs +): + """ + Finalize updating an expense by applying the new amount and category. + """ + expense_id = context.user_data.get("expense_id") + new_amount = context.user_data.get("new_amount") + headers = {"token": kwargs.get("token", None)} + payload = {"amount": new_amount} + + response = requests.put( + f"{API_BASE_URL}/expenses/{expense_id}", json=payload, headers=headers + ) + + if response.status_code == 200: + await update.message.reply_text("Expense updated successfully!") + else: + await update.message.reply_text("Failed to update expense.") + + context.user_data.pop("expense_action", None) + context.user_data.pop("expense_step", None) + context.user_data.pop("expense_id", None) + context.user_data.pop("new_amount", None) + + +@authenticate +async def finalize_expense( + update: Update, context: ContextTypes.DEFAULT_TYPE, **kwargs +): + """ + Finalize the expense entry by sending it to the API and notifying the user of success. + """ + amount = context.user_data.get("amount") + category = context.user_data.get("category") + date = context.user_data.get("date") + + headers = {"token": kwargs.get("token", None)} + payload = { + "amount": amount, + "category": category, + "currency": "USD", + "date": date.strftime("%Y-%m-%d"), + } + + response = requests.post(f"{API_BASE_URL}/expenses/", json=payload, headers=headers) + + if response.status_code == 200: + await update.message.reply_text( + f"Expense added successfully!\n\nAmount: {amount}\nCategory: {category}\nDate: {date}" + ) + else: + error_message = response.json().get( + "detail", "An error occurred while adding the expense." + ) + await update.message.reply_text( + f"Failed to add expense. Error: {error_message}" + ) + + # Clear context data related to the expense entry + context.user_data.pop("expense_action", None) + context.user_data.pop("expense_step", None) + context.user_data.pop("amount", None) + context.user_data.pop("category", None) + context.user_data.pop("date", None) + + +# Unified callback query handler to handle each expense action based on callback data +@authenticate +async def unified_expense_callback_handler( + update: Update, context: ContextTypes.DEFAULT_TYPE, **kwargs +): + query = update.callback_query + await query.answer() + + if query.data.startswith("update_"): + await handle_expense_update_selection(update, context) + elif query.data.startswith("delete_"): + await handle_expense_delete_selection(update, context) + + +async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE): + """ + Handle incoming text messages and direct messages to appropriate handlers based on context. + """ + user_id = update.message.chat_id if update.message else None + text = update.message.text if update.message else "" + + # Check if user is in the signup process + if user_id in SIGNUP_STATE: + if SIGNUP_STATE[user_id] == "awaiting_username": + # Store the username and prompt for password + USERNAMES[user_id] = text + SIGNUP_STATE[user_id] = "awaiting_password" + await update.message.reply_text("Please enter your desired password:") + + elif SIGNUP_STATE[user_id] == "awaiting_password": + # Retrieve username and password, then attempt signup + username = USERNAMES.get(user_id) + password = text + await attempt_signup(update, username, password) + # Clear signup state after attempting signup + SIGNUP_STATE.pop(user_id, None) + USERNAMES.pop(user_id, None) + return + + # Check if user is in login process + if user_id in LOGIN_STATE: + if LOGIN_STATE[user_id] == "awaiting_username": + # Store the username and prompt for password + USERNAMES[user_id] = text + LOGIN_STATE[user_id] = "awaiting_password" + await update.message.reply_text("Please enter your password:") + + elif LOGIN_STATE[user_id] == "awaiting_password": + # Retrieve username and password, then attempt login + username = USERNAMES.get(user_id) + password = text + await attempt_login(update, username, password) + # Clear login state after attempting login + LOGIN_STATE.pop(user_id, None) + USERNAMES.pop(user_id, None) + return + + await handle_response(update, text) + + +async def fallback_command(update: Update, context: ContextTypes.DEFAULT_TYPE): + await update.message.reply_text("Unrecognized command") + + +async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE): + """ + Handle inline button actions for expenses. + """ + query = update.callback_query + await query.answer() # Acknowledge the callback + + if query.data == "add_expense": + await add_expense_handler(query, context) + elif query.data == "delete_expense": + await delete_expense_handler(query, context) + elif query.data == "view_expenses": + await view_expenses_handler(query, context) + + +@authenticate +async def combined_message_handler( + update: Update, context: ContextTypes.DEFAULT_TYPE, **kwargs +): + """ + Combined handler for handling category and expense inputs step-by-step. + """ + user_id = update.message.chat_id if update.message else None + text = update.message.text if update.message else "" + + # Check if the user is in the process of adding an expense + if context.user_data.get("expense_action") == "add": + # Process expense input step by step + if context.user_data.get("expense_step") == "amount": + try: + amount = float(text) + context.user_data["amount"] = amount + context.user_data["expense_step"] = "category" + await update.message.reply_text( + "Please enter the category (e.g., Food):" + ) + except ValueError: + await update.message.reply_text( + "Invalid amount. Please enter a numeric value." + ) + return + + elif context.user_data.get("expense_step") == "category": + context.user_data["category"] = text + context.user_data["expense_step"] = "date" + await update.message.reply_text("Please enter the date (YYYY-MM-DD):") + return + + elif context.user_data.get("expense_step") == "date": + try: + date = datetime.datetime.strptime(text, "%Y-%m-%d").date() + context.user_data["date"] = date + await finalize_expense(update, context) + except ValueError: + await update.message.reply_text( + "Invalid date format. Please enter a date in YYYY-MM-DD format." + ) + return + + # Check if the user is in the process of updating an expense + elif context.user_data.get("expense_action") == "update": + if context.user_data.get("expense_step") == "new_amount": + try: + new_amount = float(text) + context.user_data["new_amount"] = new_amount + await finalize_expense_update(update, context) + except ValueError: + await update.message.reply_text( + "Invalid amount. Please enter a numeric value." + ) + return + + # Check if the user is in the process of adding a new category + elif context.user_data.get("category_action") == "add": + if context.user_data.get("category_step") == "add_name": + context.user_data["new_category_name"] = text + context.user_data["category_step"] = "add_budget" + await update.message.reply_text( + "Please enter the monthly budget for this category:" + ) + return + + elif context.user_data.get("category_step") == "add_budget": + try: + monthly_budget = float(text) + context.user_data["new_category_budget"] = monthly_budget + await finalize_category_addition(update, context) + except ValueError: + await update.message.reply_text( + "Invalid budget. Please enter a numeric value." + ) + return + + # Check if the user is in the process of editing a category's budget + elif context.user_data.get("category_action") == "edit": + if context.user_data.get("category_step") == "edit_budget": + try: + new_budget = float(text) + selected_category = context.user_data.get("selected_category") + + # Update the category budget in the database + headers = {"token": kwargs.get("token", None)} + payload = {"name": selected_category, "monthly_budget": new_budget} + response = requests.put( + f"{API_BASE_URL}/categories/{selected_category}", + headers=headers, + json=payload, + ) + + if response.status_code == 200: + await update.message.reply_text( + f"The budget for {selected_category} has been updated to {new_budget}." + ) + else: + error_message = response.json().get( + "detail", "Failed to update category." + ) + await update.message.reply_text(f"Error: {error_message}") + + except ValueError: + await update.message.reply_text( + "Invalid budget. Please enter a numeric value." + ) + + # Clear context data after updating + context.user_data.pop("category_action", None) + context.user_data.pop("category_step", None) + context.user_data.pop("selected_category", None) + return + + # Check if the user is in the signup process + elif user_id in SIGNUP_STATE: + if SIGNUP_STATE[user_id] == "awaiting_username": + USERNAMES[user_id] = text + SIGNUP_STATE[user_id] = "awaiting_password" + await update.message.reply_text("Please enter your desired password:") + elif SIGNUP_STATE[user_id] == "awaiting_password": + username = USERNAMES.get(user_id) + password = text + await attempt_signup(update, username, password) + SIGNUP_STATE.pop(user_id, None) + USERNAMES.pop(user_id, None) + return + + # Check if the user is in the login process + elif user_id in LOGIN_STATE: + if LOGIN_STATE[user_id] == "awaiting_username": + USERNAMES[user_id] = text + LOGIN_STATE[user_id] = "awaiting_password" + await update.message.reply_text("Please enter your password:") + elif LOGIN_STATE[user_id] == "awaiting_password": + username = USERNAMES.get(user_id) + password = text + await attempt_login(update, username, password) + LOGIN_STATE.pop(user_id, None) + USERNAMES.pop(user_id, None) + return + + # Handle general messages or unrecognized commands + else: + await handle_general_message(update, context) + + +async def unified_callback_query_handler( + update: Update, context: ContextTypes.DEFAULT_TYPE +): + """ + Unified handler for all callback queries related to categories and expenses. + """ + query = update.callback_query + await query.answer() # Acknowledge the callback + + # Debugging print to check if the handler is triggered + print("CallbackQueryHandler triggered with data:", query.data) + + data = query.data # The callback data from the button clicked + + # Handle the different callback actions + if data == "edit_category": + await edit_category_handler(query, context) + elif data == "delete_category": + await delete_category_handler(query, context) + elif data.startswith("edit_"): + selected_category = data.replace("edit_", "") + context.user_data["selected_category"] = selected_category + context.user_data["category_action"] = "edit" + context.user_data["category_step"] = "edit_budget" + await query.edit_message_text( + f"Enter the new monthly budget for {selected_category}:" + ) + elif data.startswith("delete_"): + + selected_category = data.replace("delete_", "") + await delete_selected_category(query, context, selected_category) + + if query.data.startswith("update_"): + await handle_expense_update_selection(update, context) + + elif data == "view_category": + # Handle view categories (this can be expanded as needed) + await view_category_handler(query, context) + + elif data == "add_category": + # Handle add category + await add_category_handler(query, context) + + elif data == "edit_category": + # Show categories for editing + await edit_category_handler(query, context) + + elif data == "delete_category": + + # Show categories for deletion + await delete_category_handler(query, context) + + # Handle expense-related actions + elif data == "add_expense": + await add_expense_handler(query, context) + elif data == "view_expenses": + await view_expenses_handler(query, context) + elif data == "update_expense": + await update_expense_handler(query, context) + elif data == "delete_expense": + await delete_expense_handler(query, context) + + else: + # Fallback for unrecognized data + await query.edit_message_text("Unknown action. Please try again.") + + +async def handle_general_message(update: Update, context: ContextTypes.DEFAULT_TYPE): + """ + General message handler for other text messages. + """ + text = update.message.text + if text.lower() in ("hi", "hello"): + await update.message.reply_text("Hello! How can I assist you today?") + else: + await update.message.reply_text( + "Sorry, I didn't understand that. Please use /help to see available commands." + ) + + +async def error(update: Update, context: ContextTypes.DEFAULT_TYPE): + """ + Log and handle errors that occur during the bot's operation. + """ + print(f"Update {update} caused error {context.error}") + + +if __name__ == "__main__": + print("Starting Bot..") + app = Application.builder().token(TELEGRAM_BOT_TOKEN).build() + app.add_handler(CommandHandler("start", start_command)) + app.add_handler(CommandHandler("add", add_command)) + app.add_handler(CommandHandler("login", login_command)) + app.add_handler(CommandHandler("signup", signup_command)) + app.add_handler(CommandHandler("categories", categories_command)) + app.add_handler(CommandHandler("expenses", expense_command)) + app.add_handler(CallbackQueryHandler(unified_callback_query_handler)) + + app.add_handler( + MessageHandler( + filters.TEXT & filters.ChatType.PRIVATE, combined_message_handler + ) + ) + app.add_handler(MessageHandler(filters.COMMAND, fallback_command)) + app.add_error_handler(error) + print("Polling..") + app.run_polling(poll_interval=3) diff --git a/categories.txt b/categories.txt deleted file mode 100644 index c0661edb7..000000000 --- a/categories.txt +++ /dev/null @@ -1 +0,0 @@ -Food,Groceries,Utilities,Transport,Shopping,Miscellaneous \ No newline at end of file diff --git a/code/.DS_Store b/code/.DS_Store deleted file mode 100644 index 87aa52603..000000000 Binary files a/code/.DS_Store and /dev/null differ diff --git a/code/__init__.py b/code/__init__.py deleted file mode 100644 index a93f95320..000000000 --- a/code/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -import os -import sys -sys.path.insert(0, os.getcwd() + "/code") diff --git a/code/account.py b/code/account.py deleted file mode 100644 index bbb6e308b..000000000 --- a/code/account.py +++ /dev/null @@ -1,110 +0,0 @@ -""" - -MIT License - -Copyright (c) 2021 Dev Kumar - -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 -import logging -from telebot import types -from datetime import datetime - -option = {} - -# Main run function -def run(message, bot): - """ - This function takes 2 arguments for processing. - - - **message** which is the message from the user on Telegram. - - **bot** which is the telegram bot object from the main code.py function. - - This is the starting function in the implementation of account feature. It pops up a menu on the telegram asking the user to chose from two different account types, after which control is given to post_category_selection(message, bot) for further processing. - """ - helper.read_json() - chat_id = message.chat.id - option.pop(chat_id, None) # remove temp choice - markup = types.ReplyKeyboardMarkup(one_time_keyboard=True) - markup.row_width = 2 - print("Account Categories:") - for c in helper.getAccountCategories(): - print("\t", c) - markup.add(c) - msg = bot.reply_to(message, 'Select Category', reply_markup = markup) - bot.register_next_step_handler(msg, post_category_selection, bot) - -# Contains step to run after the category is selected -def post_category_selection(message, bot): - """ - This function takes 2 arguments for processing. - - - **message** which is the message from the user on Telegram. - - **bot** which is the telegram bot object from the run(message, bot) function. - - This is the function which gets executed once an account type is selected. It changes current account used for expenses to the one input by the user. - - If an invalid account is selected, it erros out raising an exception indicating that the right category needs to be selected and it provides list of commands to start the next iteration. - """ - try: - chat_id = message.chat.id - selected_category = message.text - if selected_category not in helper.getAccountCategories(): - bot.send_message(chat_id, 'Invalid', reply_markup=types.ReplyKeyboardRemove()) - raise Exception("Sorry I don't recognize this category \"{}\"!".format(selected_category)) - - # Update current user's account expense type. - helper.write_json(add_account_record(chat_id, selected_category)) - bot.send_message(chat_id, "Expenses account changed to: {}.".format(selected_category)) - except Exception as e: - logging.exception(str(e)) - bot.reply_to(message, 'Oh no! ' + str(e)) - display_text = "" - commands = helper.getCommands() - for c in commands: # generate help text out of the commands dictionary defined at the top - display_text += "/" + c + ": " - display_text += commands[c] + "\n" - bot.send_message(chat_id, 'Please select a menu option from below:') - bot.send_message(chat_id, display_text) - -def add_account_record(chat_id, type): - """ - This function takes 2 arguments for processing. - - - **chat_id** which is the unique ID for a user provided by Telegram. - - **type** which is the account type user selected for future purchases. - - This function is a helper function, which creates user record if it's a new user. It then updates the account type for expenses based on the inputs from the user. - """ - user_list = helper.read_json() - if str(chat_id) not in user_list: - user_list[str(chat_id)] = helper.createNewUserRecord() - - if type == 'Checking': - user_list[str(chat_id)]['account']['Checking'] = "True" - user_list[str(chat_id)]['account']['Savings'] = "False" - else: - user_list[str(chat_id)]['account']['Checking'] = "False" - user_list[str(chat_id)]['account']['Savings'] = "True" - - return user_list - diff --git a/code/add.py b/code/add.py deleted file mode 100644 index 86852ca93..000000000 --- a/code/add.py +++ /dev/null @@ -1,171 +0,0 @@ -""" - -MIT License - -Copyright (c) 2021 Dev Kumar - -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 -import logging -from telebot import types -from datetime import datetime -from forex_python.converter import CurrencyRates -import pytest - -option = {} -currencies = CurrencyRates(force_decimal = False) - -# Main run function -def run(message, bot): - helper.read_json() - chat_id = message.chat.id - option.pop(chat_id, None) # remove temp choice - markup = types.ReplyKeyboardMarkup(one_time_keyboard=True) - markup.row_width = 2 - print("Categories:") - for c in helper.getSpendCategories(): - print("\t", c) - markup.add(c) - msg = bot.reply_to(message, 'Select Category', reply_markup = markup) - bot.register_next_step_handler(msg, post_category_selection, bot) - -# Contains step to run after the category is selected -def post_category_selection(message, bot): - try: - chat_id = message.chat.id - 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 recognize this category \"{}\"!".format(selected_category)) - - option[chat_id] = selected_category - markup = types.ReplyKeyboardMarkup(one_time_keyboard=True) - markup.row_width = 2 - print("Currencies:") - for c in helper.getCurrencies(): - print("\t", c) - markup.add(c) - msg = bot.reply_to(message, 'Select Currency', reply_markup = markup) - bot.register_next_step_handler(msg, post_currency_selection, bot, selected_category) - except Exception as e: - logging.exception(str(e)) - bot.reply_to(message, 'Oh no! ' + str(e)) - display_text = "" - commands = helper.getCommands() - for c in commands: # generate help text out of the commands dictionary defined at the top - display_text += "/" + c + ": " - display_text += commands[c] + "\n" - bot.send_message(chat_id, 'Please select a menu option from below:') - bot.send_message(chat_id, display_text) - -# Contains step to run after the currency is selected -def post_currency_selection(message, bot, selected_category): - try: - chat_id = message.chat.id - selected_currency = message.text - if selected_currency not in helper.getCurrencies(): - bot.send_message(chat_id, 'Invalid', reply_markup=types.ReplyKeyboardRemove()) - raise Exception("Sorry I don't recognize this currency \"{}\"!".format(selected_currency)) - - message = bot.send_message(chat_id, 'How much did you spend on {}? \n(Enter numeric values only)'.format(str(option[chat_id]))) - bot.register_next_step_handler(message, post_amount_input, bot, selected_category, selected_currency) - except Exception as e: - logging.exception(str(e)) - bot.reply_to(message, 'Oh no! ' + str(e)) - display_text = "" - commands = helper.getCommands() - for c in commands: # generate help text out of the commands dictionary defined at the top - display_text += "/" + c + ": " - display_text += commands[c] + "\n" - bot.send_message(chat_id, 'Please select a menu option from below:') - bot.send_message(chat_id, display_text) - -# Contains step to run after the amount is inserted -def post_amount_input(message, bot, selected_category, selected_currency): - try: - chat_id = message.chat.id - amount_entered = message.text - amount_value = helper.validate_entered_amount(amount_entered) # validate - amount_value = currencies.convert(selected_currency,'USD',float(amount_value)) - amount_value = str(round(float(amount_value), 2)) - if float(amount_value) == 0: # cannot be $0 spending - raise Exception("Spent amount has to be a non-zero number.") - - acc_type = helper.get_account_type(message) - acc_balance = helper.get_account_balance(message, "", acc_type) - - if is_Valid_expense(message, float(amount_value)) == False: - raise Exception("Expenses exceed balance in {} account. Current Balance is {}.".format(acc_type, acc_balance)) - - helper.write_json(update_balance(message, amount_value)) - date_of_entry = datetime.today().strftime(helper.getDateFormat() + ' ' + helper.getTimeFormat()) - date_str, category_str, amount_str = str(date_of_entry), str(option[chat_id]), str(amount_value) - helper.write_json(add_user_record(chat_id, "{},{},{},{} Account".format(date_str, category_str, amount_str, acc_type))) - helper.write_json(add_user_balance_record(chat_id, "{}.{}.Outflow {}".format(date_str, acc_type, amount_value))) - bot.send_message(chat_id, 'The following expenditure has been recorded: You have spent ${} for {} on {} from {} account'.format(amount_str, category_str, date_str, acc_type)) - - - if (helper.get_account_balance(message, "", acc_type) < 100): - bot.send_message(chat_id, 'ALERT: Balance in {} account is less than 100$'.format(acc_type)) - - helper.display_remaining_budget(message, bot, selected_category) - except Exception as e: - logging.exception(str(e)) - bot.reply_to(message, 'Oh no. ' + str(e)) - - -# By default, we will use checkings account. -# Only if there was a previous configuration of account change to savings, we will use that. -def is_Valid_expense(message, amount): - acc_type = helper.get_account_type(message) - - if (float(helper.get_account_balance(message, "", acc_type)) < amount): - return False - else: - return True - -def update_balance(message, amount): - cur_balance = float(helper.get_account_balance(message, "", helper.get_account_type(message))) - cur_balance -= float(amount) - - acc_type = helper.get_account_type(message) - - user_list = helper.read_json() - user_list[str(message.chat.id)]["balance"][acc_type] = str(cur_balance) - return user_list - -# Contains step to on user record addition -def add_user_record(chat_id, record_to_be_added): - user_list = helper.read_json() - if str(chat_id) not in user_list: - user_list[str(chat_id)] = helper.createNewUserRecord() - - user_list[str(chat_id)]['data'].append(record_to_be_added) - return user_list - -def add_user_balance_record(chat_id, record_to_be_added): - user_list = helper.read_json() - if str(chat_id) not in user_list: - user_list[str(chat_id)] = helper.createNewUserRecord() - - user_list[str(chat_id)]['balance_data'].append(record_to_be_added) - return user_list diff --git a/code/add_balance.py b/code/add_balance.py deleted file mode 100644 index c76fb9946..000000000 --- a/code/add_balance.py +++ /dev/null @@ -1,145 +0,0 @@ -""" - -MIT License - -Copyright (c) 2021 Dev Kumar - -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 -import logging -from telebot import types -from datetime import datetime -from forex_python.converter import CurrencyRates - -option = {} -currencies = CurrencyRates(force_decimal = False) - -# Main run function -def run(message, bot): - helper.read_json() - chat_id = message.chat.id - option.pop(chat_id, None) # remove temp choice - markup = types.ReplyKeyboardMarkup(one_time_keyboard=True) - markup.row_width = 2 - print("Account Categories:") - for c in helper.getAccountCategories(): - print("\t", c) - markup.add(c) - msg = bot.reply_to(message, 'Select Category', reply_markup = markup) - bot.register_next_step_handler(msg, post_category_selection, bot) - -def post_category_selection(message, bot): - try: - chat_id = message.chat.id - selected_category = message.text - if selected_category not in helper.getAccountCategories(): - bot.send_message(chat_id, 'Invalid', reply_markup=types.ReplyKeyboardRemove()) - raise Exception("Sorry I don't recognize this account category \"{}\"!".format(selected_category)) - - option[chat_id] = selected_category - markup = types.ReplyKeyboardMarkup(one_time_keyboard=True) - markup.row_width = 2 - print("Currencies:") - for c in helper.getCurrencies(): - print("\t", c) - markup.add(c) - msg = bot.reply_to(message, 'Select Currency', reply_markup = markup) - bot.register_next_step_handler(msg, post_currency_selection, bot, selected_category) - except Exception as e: - logging.exception(str(e)) - bot.reply_to(message, 'Oh no! ' + str(e)) - display_text = "" - commands = helper.getCommands() - for c in commands: # generate help text out of the commands dictionary defined at the top - display_text += "/" + c + ": " - display_text += commands[c] + "\n" - bot.send_message(chat_id, 'Please select a menu option from below:') - bot.send_message(chat_id, display_text) - -# Contains step to run after the currency is selected -def post_currency_selection(message, bot, selected_category): - try: - chat_id = message.chat.id - selected_currency = message.text - if selected_currency not in helper.getCurrencies(): - bot.send_message(chat_id, 'Invalid', reply_markup=types.ReplyKeyboardRemove()) - raise Exception("Sorry I don't recognize this currency \"{}\"!".format(selected_currency)) - - message = bot.send_message(chat_id, 'How much money you want to add in {} account? \n(Enter numeric values only)'.format(str(option[chat_id]))) - bot.register_next_step_handler(message, post_amount_input, bot, selected_category, selected_currency) - except Exception as e: - logging.exception(str(e)) - bot.reply_to(message, 'Oh no! ' + str(e)) - display_text = "" - commands = helper.getCommands() - for c in commands: # generate help text out of the commands dictionary defined at the top - display_text += "/" + c + ": " - display_text += commands[c] + "\n" - bot.send_message(chat_id, 'Please select a menu option from below:') - bot.send_message(chat_id, display_text) - -# Contains step to run after the amount is inserted -def post_amount_input(message, bot, selected_category, selected_currency): - try: - chat_id = message.chat.id - amount_entered = message.text - amount_value = helper.validate_entered_amount(amount_entered) # validate - amount_value = currencies.convert(selected_currency,'USD',float(amount_value)) - amount_value = str(round(float(amount_value), 2)) - option[selected_category] = amount_value - print("For {}.{}".format(chat_id, amount_value)) - if amount_value == 0: # cannot be $0 spending - raise Exception("Spent amount has to be a non-zero number.") - - date_of_entry = datetime.today().strftime(helper.getDateFormat() + ' ' + helper.getTimeFormat()) - account_str = option[selected_category] - amount_str = str(amount_value) - date_str = str(date_of_entry) - - helper.write_json(add_user_record(chat_id, "{},{},Inflow {}".format(date_str, selected_category, amount_str))) - helper.write_json(update_account_balance_add(chat_id, selected_category, amount_value)) - bot.send_message(chat_id, 'The following expenditure has been recorded: You have Added ${} to {} account on {}'.format(amount_str, account_str, date_str)) - bot.send_message(chat_id, 'New Balance in {} account is: {}'.format(selected_category, helper.get_account_balance(message, bot, selected_category))) - helper.display_account_balance(message, bot, selected_category) - except Exception as e: - logging.exception(str(e)) - bot.reply_to(message, 'Oh no. ' + str(e)) - -def update_account_balance_add(chat_id, cat, val): - user_list = helper.read_json() - if str(chat_id) not in user_list: - user_list[str(chat_id)] = helper.createNewUserRecord() - - if user_list[str(chat_id)]['balance'][cat] is None: - user_list[str(chat_id)]['balance'][cat] = str(val) - else: - user_list[str(chat_id)]['balance'][cat] = str(float(val) + float(user_list[str(chat_id)]['balance'][cat])) - return user_list - -# Contains step to on user record addition -def add_user_record(chat_id, record_to_be_added): - user_list = helper.read_json() - if str(chat_id) not in user_list: - user_list[str(chat_id)] = helper.createNewUserRecord() - - user_list[str(chat_id)]['balance_data'].append(record_to_be_added) - return user_list diff --git a/code/add_recurring.py b/code/add_recurring.py deleted file mode 100644 index 236c485ca..000000000 --- a/code/add_recurring.py +++ /dev/null @@ -1,140 +0,0 @@ -""" - -MIT License - -Copyright (c) 2021 Dev Kumar - -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 -import logging -from telebot import types -from datetime import datetime -from dateutil.relativedelta import relativedelta -from forex_python.converter import CurrencyRates - -option = {} -currencies = CurrencyRates(force_decimal = False) - -def run(message, bot): - helper.read_json() - chat_id = message.chat.id - option.pop(chat_id, None) # remove temp choice - markup = types.ReplyKeyboardMarkup(one_time_keyboard=True) - markup.row_width = 2 - for c in helper.getSpendCategories(): - markup.add(c) - msg = bot.reply_to(message, 'Select Category', reply_markup=markup) - bot.register_next_step_handler(msg, post_category_selection, bot) - -def post_category_selection(message, bot): - try: - chat_id = message.chat.id - 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 recognize this category \"{}\"!".format(selected_category)) - - option[chat_id] = selected_category - markup = types.ReplyKeyboardMarkup(one_time_keyboard=True) - markup.row_width = 2 - print("Currencies:") - for c in helper.getCurrencies(): - print("\t", c) - markup.add(c) - msg = bot.reply_to(message, 'Select Currency', reply_markup = markup) - bot.register_next_step_handler(msg, post_currency_selection, bot, selected_category) - except Exception as e: - logging.exception(str(e)) - bot.reply_to(message, 'Oh no! ' + str(e)) - display_text = "" - commands = helper.getCommands() - for c in commands: # generate help text out of the commands dictionary defined at the top - display_text += "/" + c + ": " - display_text += commands[c] + "\n" - bot.send_message(chat_id, 'Please select a menu option from below:') - bot.send_message(chat_id, display_text) - -# Contains step to run after the currency is selected -def post_currency_selection(message, bot, selected_category): - try: - chat_id = message.chat.id - selected_currency = message.text - if selected_currency not in helper.getCurrencies(): - bot.send_message(chat_id, 'Invalid', reply_markup=types.ReplyKeyboardRemove()) - raise Exception("Sorry I don't recognize this currency \"{}\"!".format(selected_currency)) - - message = bot.send_message(chat_id, 'How much did you spend on {}? \n(Enter numeric values only)'.format(str(option[chat_id]))) - bot.register_next_step_handler(message, post_amount_input, bot, selected_category, selected_currency) - except Exception as e: - logging.exception(str(e)) - bot.reply_to(message, 'Oh no! ' + str(e)) - display_text = "" - commands = helper.getCommands() - for c in commands: # generate help text out of the commands dictionary defined at the top - display_text += "/" + c + ": " - display_text += commands[c] + "\n" - bot.send_message(chat_id, 'Please select a menu option from below:') - bot.send_message(chat_id, display_text) - -def post_amount_input(message, bot, selected_category, selected_currency): - try: - chat_id = message.chat.id - amount_entered = message.text - amount_value = helper.validate_entered_amount(amount_entered) # validate - amount_value = currencies.convert(selected_currency,'USD',float(amount_value)) - amount_value = str(round(float(amount_value), 2)) - if amount_value == 0: # cannot be $0 spending - raise Exception("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)'.format(str(option[chat_id]))) - bot.register_next_step_handler(message, post_duration_input, bot, selected_category, amount_value) - except Exception as e: - logging.exception(str(e)) - bot.reply_to(message, 'Oh no. ' + str(e)) - - -def post_duration_input(message, bot, selected_category, amount_value): - try: - chat_id = message.chat.id - 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.") - - for i in range(int(duration_value)): - date_of_entry = (datetime.today() + relativedelta(months=+i)).strftime(helper.getDateFormat() + ' ' + helper.getTimeFormat()) - date_str, category_str, amount_str = str(date_of_entry), str(option[chat_id]), str(amount_value) - helper.write_json(add_user_record(chat_id, "{},{},{}".format(date_str, category_str, amount_str))) - - bot.send_message(chat_id, 'The following expenditure has been recorded: You have spent ${} for {} for the next {} months'.format(amount_str, category_str, duration_value)) - - except Exception as e: - logging.exception(str(e)) - bot.reply_to(message, 'Oh no. ' + str(e)) - -def add_user_record(chat_id, record_to_be_added): - user_list = helper.read_json() - if str(chat_id) not in user_list: - user_list[str(chat_id)] = helper.createNewUserRecord() - - user_list[str(chat_id)]['data'].append(record_to_be_added) - return user_list diff --git a/code/budget.py b/code/budget.py deleted file mode 100644 index be3c6f2c1..000000000 --- a/code/budget.py +++ /dev/null @@ -1,61 +0,0 @@ -""" - -MIT License - -Copyright (c) 2021 Dev Kumar - -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 -import budget_view -import budget_update -import budget_delete -import logging -from telebot import types - - -def run(message, bot): - markup = types.ReplyKeyboardMarkup(one_time_keyboard=True) - options = helper.getBudgetOptions() - markup.row_width = 2 - for c in options.values(): - markup.add(c) - msg = bot.reply_to(message, 'Select Operation', reply_markup=markup) - bot.register_next_step_handler(msg, post_operation_selection, bot) - - -def post_operation_selection(message, bot): - try: - chat_id = message.chat.id - op = message.text - options = helper.getBudgetOptions() - 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['update']: - budget_update.run(message, bot) - elif op == options['view']: - budget_view.run(message, bot) - elif op == options['delete']: - budget_delete.run(message, bot) - except Exception as e: - # print("hit exception") - helper.throw_exception(e, message, bot, logging) diff --git a/code/budget_delete.py b/code/budget_delete.py deleted file mode 100644 index dcc061576..000000000 --- a/code/budget_delete.py +++ /dev/null @@ -1,38 +0,0 @@ -""" - -MIT License - -Copyright (c) 2021 Dev Kumar - -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 - - -def run(message, bot): - chat_id = message.chat.id - user_list = helper.read_json() - print(user_list) - if str(chat_id) in user_list: - user_list[str(chat_id)]['budget']['overall'] = None - user_list[str(chat_id)]['budget']['category'] = None - helper.write_json(user_list) - bot.send_message(chat_id, 'Budget deleted!') diff --git a/code/budget_update.py b/code/budget_update.py deleted file mode 100644 index 1a8b1a692..000000000 --- a/code/budget_update.py +++ /dev/null @@ -1,207 +0,0 @@ -""" - -MIT License - -Copyright (c) 2021 Dev Kumar - -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 -import logging -from telebot import types -from forex_python.converter import CurrencyRates - -currencies = CurrencyRates(force_decimal = False) - -def run(message, bot): - chat_id = message.chat.id - if (not (helper.isOverallBudgetAvailable(chat_id)) and (helper.isCategoryBudgetAvailable(chat_id))): - update_overall_budget(message, bot) - elif (not (helper.isCategoryBudgetAvailable(chat_id)) and (helper.isOverallBudgetAvailable(chat_id))): - update_category_budget(message, bot) - else: - markup = types.ReplyKeyboardMarkup(one_time_keyboard=True) - options = helper.getBudgetTypes() - markup.row_width = 2 - for c in options.values(): - markup.add(c) - msg = bot.reply_to(message, 'Select Budget Type', reply_markup=markup) - bot.register_next_step_handler(msg, post_type_selection, bot) - -def post_type_selection(message, bot): - try: - chat_id = message.chat.id - op = message.text - options = helper.getBudgetTypes() - 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']: - update_overall_budget(message, bot) - elif op == options['category']: - update_category_budget(message, bot) - except Exception as e: - helper.throw_exception(e, message, bot, logging) - -def update_overall_budget(message, bot): - markup = types.ReplyKeyboardMarkup(one_time_keyboard=True) - curr = helper.getCurrencies() - markup.row_width = 2 - for c in curr: - markup.add(c) - msg = bot.reply_to(message, 'Select Currency', reply_markup=markup) - print('currency = ' + msg.text) - bot.register_next_step_handler(msg, post_currency_selection, bot) - -def post_currency_selection(message, bot): - try: - chat_id = message.chat.id - selected_currency = message.text - if selected_currency not in helper.getCurrencies(): - bot.send_message(chat_id, 'Invalid', reply_markup=types.ReplyKeyboardRemove()) - raise Exception("Sorry I don't recognize this currency \"{}\"!".format(selected_currency)) - if (helper.isOverallBudgetAvailable(chat_id)): - currentBudget = helper.getOverallBudget(chat_id) - msg_string = 'Current Budget is ${}\n\nHow much is your new monthly budget? \n(Enter numeric values only)' - msg = bot.send_message(chat_id, msg_string.format(currentBudget)) - else: - msg = bot.send_message(chat_id, 'How much is your monthly budget? \n(Enter numeric values only)') - bot.register_next_step_handler(msg, post_overall_amount_input, bot, selected_currency) - except Exception as e: - logging.exception(str(e)) - bot.reply_to(message, 'Oh no! ' + str(e)) - display_text = "" - commands = helper.getCommands() - for c in commands: # generate help text out of the commands dictionary defined at the top - display_text += "/" + c + ": " - display_text += commands[c] + "\n" - bot.send_message(chat_id, 'Please select a menu option from below:') - bot.send_message(chat_id, display_text) - -def post_overall_amount_input(message, bot, selected_currency): - try: - chat_id = message.chat.id - amount_value = helper.validate_entered_amount(message.text) - amount_value = currencies.convert(selected_currency,'USD',float(amount_value)) - amount_value = str(round(float(amount_value), 2)) - if amount_value == 0: - raise Exception("Invalid amount.") - user_list = helper.read_json() - if str(chat_id) not in user_list: - user_list[str(chat_id)] = helper.createNewUserRecord() - user_list[str(chat_id)]['budget']['overall'] = amount_value - helper.write_json(user_list) - bot.send_message(chat_id, 'Budget Updated!') - return user_list - except Exception as e: - helper.throw_exception(e, message, bot, logging) - - -def update_category_budget(message, bot): - markup = types.ReplyKeyboardMarkup(one_time_keyboard=True) - categories = helper.getSpendCategories() - markup.row_width = 2 - for c in categories: - markup.add(c) - msg = bot.reply_to(message, 'Select Category', reply_markup=markup) - print('category = ' + msg.text) - bot.register_next_step_handler(msg, post_category_selection, bot) - - -def post_category_selection(message, bot): - try: - chat_id = message.chat.id - selected_category = message.text - categories = helper.getSpendCategories() - if selected_category not in categories: - bot.send_message(chat_id, 'Invalid', reply_markup=types.ReplyKeyboardRemove()) - raise Exception("Sorry I don't recognise this category \"{}\"!".format(selected_category)) - markup = types.ReplyKeyboardMarkup(one_time_keyboard=True) - markup.row_width = 2 - print("Currencies:") - for c in helper.getCurrencies(): - print("\t", c) - markup.add(c) - msg = bot.reply_to(message, 'Select Currency', reply_markup=markup) - print('category = ' + selected_category) - bot.register_next_step_handler(msg, post_currency_selection_for_category_update, bot, selected_category) - except Exception as e: - helper.throw_exception(e, message, bot, logging) - -def post_currency_selection_for_category_update(message, bot, selected_category): - try: - chat_id = message.chat.id - # selected_category = message.text - # curr = helper.getCurrencies() - selected_currency = message.text - print('currency = ' + selected_currency) - if selected_currency not in helper.getCurrencies(): - bot.send_message(chat_id, 'Invalid', reply_markup=types.ReplyKeyboardRemove()) - raise Exception("Sorry I don't recognise this currency \"{}\"!".format(selected_currency)) - if helper.isCategoryBudgetByCategoryAvailable(chat_id, selected_category): - currentBudget = helper.getCategoryBudgetByCategory(chat_id, selected_category) - msg_string = 'Current monthly budget for {} is {}\n\nEnter monthly budget for {}\n(Enter numeric values only)' - message = bot.send_message(chat_id, msg_string.format(selected_category, currentBudget, selected_category)) - else: - message = bot.send_message(chat_id, 'Enter monthly budget for ' + selected_category + '\n(Enter numeric values only)') - bot.register_next_step_handler(message, post_category_amount_input, bot, selected_category, selected_currency) - except Exception as e: - helper.throw_exception(e, message, bot, logging) - -def post_category_amount_input(message, bot, category, currency): - try: - chat_id = message.chat.id - amount_value = helper.validate_entered_amount(message.text) - amount_value = currencies.convert(currency,'USD',float(amount_value)) - amount_value = str(round(float(amount_value), 2)) - if amount_value == 0: - raise Exception("Invalid amount.") - user_list = helper.read_json() - if str(chat_id) not in user_list: - user_list[str(chat_id)] = helper.createNewUserRecord() - if user_list[str(chat_id)]['budget']['category'] is None: - user_list[str(chat_id)]['budget']['category'] = {} - user_list[str(chat_id)]['budget']['category'][category] = amount_value - helper.write_json(user_list) - message = bot.send_message(chat_id, 'Budget for ' + category + ' Created!') - post_category_add(message, bot) - - except Exception as e: - helper.throw_exception(e, message, bot, logging) - - -def post_category_add(message, bot): - markup = types.ReplyKeyboardMarkup(one_time_keyboard=True) - options = helper.getUpdateOptions().values() - markup.row_width = 2 - for c in options: - markup.add(c) - msg = bot.reply_to(message, 'Select Option', reply_markup=markup) - bot.register_next_step_handler(msg, post_option_selection, bot) - - -def post_option_selection(message, bot): - print("here") - selected_option = message.text - options = helper.getUpdateOptions() - print("here") - if selected_option == options['continue']: - update_category_budget(message, bot) diff --git a/code/budget_view.py b/code/budget_view.py deleted file mode 100644 index 2b501d620..000000000 --- a/code/budget_view.py +++ /dev/null @@ -1,57 +0,0 @@ -""" - -MIT License - -Copyright (c) 2021 Dev Kumar - -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 -import logging - - -def run(message, bot): - try: - print("here") - chat_id = message.chat.id - if helper.isOverallBudgetAvailable(chat_id): - display_overall_budget(message, bot) - elif helper.isCategoryBudgetAvailable(chat_id): - display_category_budget(message, bot) - else: - raise Exception('Budget does not exist. Use ' + helper.getBudgetOptions()['update'] + ' option to add/update the budget') - except Exception as e: - helper.throw_exception(e, message, bot, logging) - - -def display_overall_budget(message, bot): - chat_id = message.chat.id - data = helper.getOverallBudget(chat_id) - bot.send_message(chat_id, 'Overall Budget: $' + data) - - -def display_category_budget(message, bot): - chat_id = message.chat.id - data = helper.getCategoryBudget(chat_id) - res = "Budget Summary\n" - for c, v in data.items(): - res = res + c + ": $" + v + "\n" - bot.send_message(chat_id, res) diff --git a/code/category.py b/code/category.py deleted file mode 100644 index 1f04f58c9..000000000 --- a/code/category.py +++ /dev/null @@ -1,121 +0,0 @@ -""" - -MIT License - -Copyright (c) 2021 Dev Kumar - -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 -import logging -from telebot import types - -# The main funtion of category.py. -# User can start to manage their categories after calling it -def run(message, bot): - markup = types.ReplyKeyboardMarkup(one_time_keyboard=True) - options = helper.getCategoryOptions() - markup.row_width = 2 - for c in options.values(): - markup.add(c) - msg = bot.reply_to(message, 'Select Operation', reply_markup=markup) - bot.register_next_step_handler(msg, post_operation_selection, bot) - -# User have three funtionaliy can choose, add a category, delete a category or view the current categories -def post_operation_selection(message, bot): - try: - chat_id = message.chat.id - op = message.text - options = helper.getCategoryOptions() - markup = types.ReplyKeyboardMarkup(one_time_keyboard=True) - # Handle the exception of unknown operation - 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['add']: - msg = bot.reply_to(message, 'Please type the new category name') - bot.register_next_step_handler(msg, category_add, bot) - elif op == options['view']: - category_view(message, bot) - elif op == options['delete']: - markup.row_width = 2 - for c in helper.getSpendCategories(): - markup.add(c) - # Handle the exception of trying to delete the last category - if len(helper.getSpendCategories()) <= 1: - bot.send_message(chat_id, 'Invalid', reply_markup=types.ReplyKeyboardRemove()) - raise Exception("Number of categories cannot be zero.") - else: - msg = bot.reply_to(message, 'Please choose the category you want to delete', reply_markup=markup) - bot.register_next_step_handler(msg, category_delete, bot) - except Exception as e: - # print("hit exception") - helper.throw_exception(e, message, bot, logging) - -# Use the funtion to add a new category -def category_add(message, bot): - chat_id = message.chat.id - category_name = message.text - with open("categories.txt", "r") as tf: - lines = tf.read().split(',') - tf.close() - f = open("categories.txt", "a") - if lines == ['']: - f.write(category_name) - else: - f.write(',' + category_name) - f.close() - bot.send_message(chat_id, 'Add category "{}" successfully!'.format(category_name)) - -# Use the funciton to view all of the categories in chat room -def category_view(message, bot): - chat_id = message.chat.id - with open("categories.txt", "r") as tf: - lines = tf.read() - tf.close() - bot.send_message(chat_id, 'The categories are:\n{}'.format(lines)) - -# Use the funtion to delete a category -def category_delete(message, bot): - chat_id = message.chat.id - category_name = message.text - find_to_delete = False - with open("categories.txt", "r") as tf: - categories = tf.read().split(',') - tf.close() - for category in categories: - if category == '': - categories.remove('') - if category == category_name: - find_to_delete = True - categories.remove(category) - # Handle the exception of deleting a not exist category - if not find_to_delete: - bot.send_message(chat_id, 'Cannot find the category QAQ', reply_markup=types.ReplyKeyboardRemove()) - elif find_to_delete: - f = open("categories.txt", "w") - for category in categories: - if category == categories[0]: - f.write(category) - else: - f.write("," + category) - f.close() - bot.send_message(chat_id, 'Delete category "{}" successfully!'.format(category_name)) diff --git a/code/code.py b/code/code.py deleted file mode 100644 index 0a2b1293f..000000000 --- a/code/code.py +++ /dev/null @@ -1,177 +0,0 @@ -""" - -MIT License - -Copyright (c) 2021 Dev Kumar - -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. - -""" - -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -import logging -import telebot -import time -import helper -import edit -import history -import display -from reminder import check_reminders -import estimate -import delete -import add -import add_balance -import budget -import category -import download_csv -import download_pdf -import email_history -import add_recurring -import delete_expense -import account -from datetime import datetime -from jproperties import Properties -from telebot import types -import reminder -from datetime import datetime, time -import threading -import time - -configs = Properties() - -with open('user.properties', 'rb') as read_prop: - configs.load(read_prop) - -api_token = str(configs.get('api_token').data) - -bot = telebot.TeleBot(api_token) - -telebot.logger.setLevel(logging.INFO) - -# Define listener for requests by user -def listener(user_requests): - for req in user_requests: - if req.content_type == 'text': - print("{} name:{} chat_id:{} \nmessage: {}\n".format( - str(datetime.now()), str(req.chat.first_name), str(req.chat.id), str(req.text))) - - -bot.set_update_listener(listener) - - -# Define your list of commands and descriptions -menu_commands = [ - ("add_balance", "Add balance to a specific account"), - ("add", "Record/Add a new spending"), - ("add_recurring", "Record the recurring expenses"), - ("display", "Show sum of expenditure"), - ("estimate", "Show an estimate of expenditure"), - ("history", "Display spending history"), - ("delete", "Clear/Erase your records"), - ("delete_all", "Clear/Erase all your records"), - ("edit", "Edit/Change spending details"), - ("budget", "Add/Update/View/Delete budget"), - ("category", "Add/Delete/Show custom categories in telegram bot"), - ("csv", "To download your history in csv format"), - ("pdf", "Generates a PDF file containing the user's expense history plot"), - ("email_history", "your spend history will be sent to provided email address"), - ("set_reminder", "Create a reminder for your purchases or bills"), - ("select_expenses_account", "Select account to use for expenses") -] - -bot.set_my_commands([ - types.BotCommand(command=command, description=description) for command, description in menu_commands -]) - -# Define a function to handle the /start and /menu commands -@bot.message_handler(commands=['start', 'menu']) -def start_and_menu_command(message): - chat_id = message.chat.id - text_intro = "Welcome to MyDollarBot! Please select an option:" - - markup = types.ReplyKeyboardMarkup(resize_keyboard=True, one_time_keyboard=True) - for command, _ in menu_commands: - markup.add(types.KeyboardButton(f'/{command}')) - - bot.send_message(chat_id, text_intro, reply_markup=markup) - -# Define command handlers for each menu option -@bot.message_handler(commands=[command for command, _ in menu_commands]) -def handle_menu_command(message): - command = message.text[1:] # Remove the '/' character from the command - if command == "add": - add.run(message, bot) - elif command == "add_balance": - add_balance.run(message, bot) - elif command == "display": - display.run(message, bot) - elif command == 'estimate': - estimate.run(message, bot) - elif command == 'add_recurring': - add_recurring.run(message, bot) - elif command == 'delete_all': - delete.run(message, bot) - elif command == 'delete': - delete_expense.run(message, bot) - elif command == 'budget': - budget.run(message, bot) - elif command == 'edit': - edit.run(message, bot) - elif command == 'history': - history.run(message, bot) - elif command == 'category': - category.run(message, bot) - elif command == 'csv': - download_csv.run(message, bot) - elif command == 'email_history': - email_history.run(message, bot) - elif command == 'select_expenses_account': - account.run(message, bot) - elif command == 'pdf': - download_pdf.run(message, bot) - elif command == 'set_reminder': - print('Setting reminder') - reminder.run(message, bot) - - -# Define a function to periodically check reminders -def reminder_checker(): - while True: - check_reminders(bot) - # Sleep for one minute - time.sleep(60) - - -# The main function -def main(): - try: - bot.polling(none_stop=True) - except Exception as e: - logging.exception(str(e)) - time.sleep(3) - print("Connection Timeout") - - -if __name__ == '__main__': - reminder_thread = threading.Thread(target=reminder_checker) - reminder_thread.daemon = True - reminder_thread.start() - - main() diff --git a/code/delete.py b/code/delete.py deleted file mode 100644 index 312f3e9a4..000000000 --- a/code/delete.py +++ /dev/null @@ -1,48 +0,0 @@ -""" - -MIT License - -Copyright (c) 2021 Dev Kumar - -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 - - -def run(message, bot): - global user_list - chat_id = message.chat.id - delete_history_text = "" - user_list = helper.read_json() - if (str(chat_id) in user_list): - helper.write_json(deleteHistory(chat_id)) - delete_history_text = "History has been deleted!" - else: - delete_history_text = "No records there to be deleted. Start adding your expenses to keep track of your spendings!" - bot.send_message(chat_id, delete_history_text) - - -# function to delete a record -def deleteHistory(chat_id): - global user_list - if (str(chat_id) in user_list): - del user_list[str(chat_id)] - return user_list diff --git a/code/delete_expense.py b/code/delete_expense.py deleted file mode 100644 index 8b84365b9..000000000 --- a/code/delete_expense.py +++ /dev/null @@ -1,84 +0,0 @@ -""" - -MIT License - -Copyright (c) 2021 Dev Kumar - -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 -import json - - -# Function to delete an expense -def delete_expense(chat_id, expense_index): - user_data = helper.read_json() - if str(chat_id) in user_data: - user_info = user_data[str(chat_id)] - - # Check if the "data" key exists and if the expense_index is within bounds - if "data" in user_info and expense_index < len(user_info["data"]): - # Remove the expense at the specified index - deleted_expense = user_info["data"].pop(expense_index) - # Save the updated user data back to the database - with open('expense_record.json', 'w') as file: - json.dump(user_data, file, indent=4) - return f"Expense '{deleted_expense}' has been deleted." - return "Expense deletion failed." - - -# Function to start the process of deleting an expense -def run(message, bot): - chat_id = message.chat.id - user_data = helper.read_json() - print(chat_id) - - # Check if the user exists in the data - if str(chat_id) not in user_data: - bot.send_message(chat_id, "You don't have any expenses to delete.") - return - - user_info = user_data[str(chat_id)] - - # Check if the "data" key exists and if there are expenses to delete - if "data" not in user_info or not user_info["data"]: - bot.send_message(chat_id, "You don't have any expenses to delete.") - return - - expenses = user_info["data"] - expense_text = "Select the expense you want to delete by entering its index:\n" - - # Generate a list of expenses for the user to choose from - for i, expense in enumerate(expenses): - expense_text += f"{i + 1}. {expense}\n" - - bot.send_message(chat_id, expense_text) - bot.register_next_step_handler(message, confirm_deletion, chat_id, bot) - - -# Function to confirm the deletion of an expense -def confirm_deletion(message, chat_id, bot): - if message.text.isdigit(): - expense_index = int(message.text) - 1 - result_message = delete_expense(chat_id, expense_index) - bot.send_message(chat_id, result_message) - else: - bot.send_message(chat_id, "Please enter a valid expense number to delete.") diff --git a/code/display.py b/code/display.py deleted file mode 100644 index 54f5da903..000000000 --- a/code/display.py +++ /dev/null @@ -1,185 +0,0 @@ -""" - -MIT License - -Copyright (c) 2021 Dev Kumar - -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 time -import os -import helper -import graphing -import logging -from telebot import types -from datetime import datetime - - -def run(message, bot): - helper.read_json() - chat_id = message.chat.id - history = helper.getUserHistory(chat_id) - if history is None or history == []: - bot.send_message(chat_id, "Sorry, there are no records of the spending!") - else: - markup = types.ReplyKeyboardMarkup(one_time_keyboard=True) - markup.row_width = 2 - for mode in helper.getSpendDisplayOptions(): - markup.add(mode) - # markup.add('Day', 'Month') - msg = bot.reply_to(message, 'Please select a category to see details', reply_markup=markup) - bot.register_next_step_handler(msg, display_total, bot) - - -total = "" -bud = "" -def display_total(message, bot): - global total - global bud - try: - chat_id = message.chat.id - DayWeekMonth = message.text - - if DayWeekMonth not in helper.getSpendDisplayOptions(): - raise Exception("Sorry I can't show spendings for \"{}\"!".format(DayWeekMonth)) - - history = helper.getUserHistory(chat_id) - if history is None or history == []: - raise Exception("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) - bot.send_chat_action(chat_id, 'typing') - time.sleep(0.5) - total_text = "" - # get budget data - budgetData = {} - if helper.isOverallBudgetAvailable(chat_id): - budgetData = helper.getOverallBudget(chat_id) - elif helper.isCategoryBudgetAvailable(chat_id): - budgetData = helper.getCategoryBudget(chat_id) - - if DayWeekMonth == 'Day': - query = datetime.now().today().strftime(helper.getDateFormat()) - # query all that contains today's date - queryResult = [value for index, value in enumerate(history) if str(query) in value] - elif DayWeekMonth == 'Month': - query = datetime.now().today().strftime(helper.getMonthFormat()) - # query all that contains today's date - queryResult = [value for index, value in enumerate(history) if str(query) in value] - - total_text = calculate_spendings(queryResult) - total = total_text - bud = budgetData - spending_text = display_budget_by_text(history, budgetData) - if len(total_text) == 0: - spending_text += "----------------------\nYou have no spendings for {}!".format(DayWeekMonth) - bot.send_message(chat_id, spending_text) - else: - spending_text += "\n----------------------\nHere are your total spendings {}:\nCATEGORIES,AMOUNT \n----------------------\n{}".format( - DayWeekMonth.lower(), total_text) - bot.send_message(chat_id, spending_text) - markup = types.ReplyKeyboardMarkup(one_time_keyboard=True) - markup.row_width = 2 - for plot in helper.getplot(): - markup.add(plot) - # markup.add('Day', 'Month') - msg = bot.reply_to(message, 'Please select a plot to see the total expense', reply_markup=markup) - bot.register_next_step_handler(msg, plot_total, bot) - except Exception as e: - logging.exception(str(e)) - bot.reply_to(message, str(e)) - -def plot_total(message, bot): - chat_id = message.chat.id - pyi = message.text - if pyi == 'Bar with budget': - graphing.visualize(total, bud) - bot.send_photo(chat_id, photo=open('expenditure.png', 'rb')) - os.remove('expenditure.png') - elif pyi == 'Bar without budget': - graphing.viz(total) - bot.send_photo(chat_id, photo=open('expend.png', 'rb')) - os.remove('expend.png') - else: - graphing.vis(total) - bot.send_photo(chat_id, photo=open('pie.png', 'rb')) - os.remove('pie.png') - -def calculate_spendings(queryResult): - total_dict = {} - - for row in queryResult: - # date,cat,money - s = row.split(',') - # cat - cat = s[1] - if cat in total_dict: - # round up to 2 decimal - total_dict[cat] = round(total_dict[cat] + float(s[2]), 2) - else: - total_dict[cat] = float(s[2]) - total_text = "" - for key, value in total_dict.items(): - total_text += str(key) + " $" + str(value) + "\n" - return total_text - - -def display_budget_by_text(history, budget_data) -> str: - query = datetime.now().today().strftime(helper.getMonthFormat()) - # query all expense history that contains today's date - queryResult = [value for index, value in enumerate(history) if str(query) in value] - total_text = calculate_spendings(queryResult) - budget_display = "" - total_text_split = [line for line in total_text.split('\n') if line.strip() != ''] - - if isinstance(budget_data, str): - # if budget is string denoting it is overall budget - budget_val = float(budget_data) - total_expense = 0 - # sum all expense - for expense in total_text_split: - a = expense.split(' ') - amount = a[1].replace("$", "") - total_expense += float(amount) - # calculate the remaining budget - remaining = budget_val - total_expense - # set the return message - budget_display += "Overall Budget is: " + str(budget_val) + "\n----------------------\nCurrent remaining budget is " + str( - remaining) + "\n" - elif isinstance(budget_data, dict): - budget_display += "Budget by Catergories is:\n" - categ_remaining = {} - # categorize the budgets by their categories - for key in budget_data.keys(): - budget_display += key + ":" + budget_data[key] + "\n" - categ_remaining[key] = float(budget_data[key]) - # calculate the remaining budgets by categories - for i in total_text_split: - # the expense text is in the format like "Food $100" - a = i.split(' ') - a[1] = a[1].replace("$", "") - categ_remaining[a[0]] = categ_remaining[a[0]] - float(a[1]) if a[0] in categ_remaining else -float(a[1]) - budget_display += "----------------------\nCurrent remaining budget is: \n" - # show the remaining budgets - for key in categ_remaining.keys(): - budget_display += key + ":" + str(categ_remaining[key]) + "\n" - return budget_display diff --git a/code/download_csv.py b/code/download_csv.py deleted file mode 100644 index 2155e7ffa..000000000 --- a/code/download_csv.py +++ /dev/null @@ -1,75 +0,0 @@ -""" - -MIT License - -Copyright (c) 2021 Dev Kumar - -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 -import logging -from telegram import InputFile -from telegram.error import TelegramError -from telebot import types -import csv -import os - -def run(message, bot): - try: - chat_id = message.chat.id - user_history = helper.getUserHistory(chat_id) - - if not user_history: - bot.send_message(chat_id, "you have no history to generate CSV file.") - return None - - file_path = 'code/Expenses_Data.csv' - column_names = ['Date and Time', 'Category', 'Amount', 'Account Type'] - - with open(file_path, mode='w', newline='') as file: - writer = csv.writer(file) - writer.writerow(column_names) - writer.writerows(user_history) # write the list of lists directly - - with open(file_path, 'rb') as file: - try: - bot.send_document(chat_id, document=file) - except Exception as send_error: - logging.error(f"Error sending document: {str(send_error)}") - bot.send_message(chat_id, "Error: Failed to send the document.") - return None - return file_path - - except FileNotFoundError as e: - logging.error(f"File not found error: {str(e)}") - bot.send_message(chat_id, "Error: File not found.") - except PermissionError as e: - logging.error(f"Permission error: {str(e)}") - bot.send_message(chat_id, "Error: Permission issue while accessing the file.") - except csv.Error as e: - logging.error(f"CSV error: {str(e)}") - bot.send_message(chat_id, "Error: CSV file generation failed.") - except TelegramError as e: - logging.error(f"Telegram error: {str(e)}") - bot.send_message(chat_id, "Error: Telegram communication issue.") - except Exception as e: - logging.error(f"Unexpected error: {str(e)}") - bot.send_message(chat_id, "An unexpected error occurred. Please try again later.") diff --git a/code/download_pdf.py b/code/download_pdf.py deleted file mode 100644 index 292eda785..000000000 --- a/code/download_pdf.py +++ /dev/null @@ -1,140 +0,0 @@ -""" - -MIT License - -Copyright (c) 2021 Dev Kumar - -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 -import logging -from matplotlib import pyplot as plt - -# Constants -TOP_MARGIN = 0.8 -LINE_HEIGHT = 0.15 -FONT_SIZE_TEXT = 8 - -def generate_expense_history_plot(user_history): - """ - generate_expense_history_plot(user_history): This function generates a Matplotlib plot of the user's expense history. - - Args: - user_history (list): List of strings representing user's expense history records. - - Returns: - fig (Matplotlib.figure.Figure): Matplotlib figure object containing the expense history plot. - """ - try: - fig, ax = plt.subplots() - top = TOP_MARGIN - - if not user_history: - ax.text( - 0.1, - top, - "No record found!", - horizontalalignment="left", - verticalalignment="center", - transform=ax.transAxes, - fontsize=FONT_SIZE_TEXT, - ) - else: - for rec in user_history: - try: - date, category, amount, account = rec.split(",") - except ValueError as ve: - raise ValueError(f"Error parsing user history data: {ve}") - - rec_str = f"{category} expense on {date} with {account} account is {amount}$." - ax.text( - 0, - top, - rec_str, - horizontalalignment="left", - verticalalignment="center", - transform=ax.transAxes, - fontsize=FONT_SIZE_TEXT, - bbox=dict(facecolor="blue", alpha=0.1), - ) - top -= LINE_HEIGHT - - ax.axis("off") - return fig - - except Exception as e: - logging.exception(str(e)) - raise - -def save_and_send_pdf(fig, chat_id, bot): - """ - save_and_send_pdf(fig, chat_id, bot): This function saves the Matplotlib figure as a PDF and sends it as a document via Telegram. - - Args: - fig (Matplotlib.figure.Figure): Matplotlib figure object. - chat_id (int): Telegram chat ID. - bot (obj): The Telegram bot object. - - Returns: - None - """ - try: - pdf_path = "expense_history.pdf" - fig.savefig(pdf_path) - plt.close(fig) - - bot.send_message(chat_id, "expense_history.pdf is created") - bot.send_document(chat_id, open(pdf_path, "rb")) - - except Exception as e: - logging.exception(str(e)) - raise - -def run(message, bot): - """ - run(message, bot): This is the main function used to implement the pdf save feature. - - Args: - message (obj): The message object received from Telegram. - bot (obj): The Telegram bot object. - - Returns: - None - - Raises: - ValueError: If there is an issue parsing the user history data. - Exception: If an unexpected error occurs during the process. - - This function generates a PDF file containing the user's expense history plot and sends it as a document using a Telegram bot. - """ - try: - helper.read_json() - chat_id = message.chat.id - user_history = helper.getUserHistory(chat_id) - fig = generate_expense_history_plot(user_history) - save_and_send_pdf(fig, chat_id, bot) - - except ValueError as ve: - bot.reply_to(message, f"Oops! {ve}") - - except Exception as e: - logging.exception(str(e)) - bot.reply_to(message, "Oops! An unexpected error occurred.") \ No newline at end of file diff --git a/code/edit.py b/code/edit.py deleted file mode 100644 index 16f0599cb..000000000 --- a/code/edit.py +++ /dev/null @@ -1,140 +0,0 @@ -""" - -MIT License - -Copyright (c) 2021 Dev Kumar - -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 re -import helper -from telebot import types - - -def run(m, bot): - chat_id = m.chat.id - markup = types.ReplyKeyboardMarkup(one_time_keyboard=True) - markup.row_width = 2 - for c in helper.getUserHistory(chat_id): - 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) - bot.register_next_step_handler(info, select_category_to_be_updated, bot) - - -def select_category_to_be_updated(m, bot): - info = m.text - 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) - bot.register_next_step_handler(choice, enter_updated_data, bot, selected_data) - - -def enter_updated_data(m, bot, selected_data): - choice1 = "" if m.text is None else m.text - markup = types.ReplyKeyboardMarkup(one_time_keyboard=True) - markup.row_width = 2 - for cat in helper.getSpendCategories(): - markup.add(cat) - - if 'Date' in choice1: - new_date = bot.reply_to(m, "Please enter the new date (in dd-mmm-yyy format)") - bot.register_next_step_handler(new_date, edit_date, bot, selected_data) - - if '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) - - if 'Amount' in choice1: - new_cost = bot.reply_to(m, "Please type the new cost") - bot.register_next_step_handler(new_cost, edit_cost, bot, selected_data) - - -def edit_date(m, bot, selected_data): - user_list = helper.read_json() - new_date = "" if m.text is None else m.text - date_format = r'^(([0][1-9])|([1-2][0-9])|([3][0-1]))\-(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\-\d{4}$' - x1 = re.search(date_format, new_date) - if x1 is None: - bot.reply_to(m, "The date is incorrect") - return - chat_id = m.chat.id - data_edit = helper.getUserHistory(chat_id) - 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] = new_date + ',' + selected_category + ',' + selected_amount[1:] - break - - user_list[str(chat_id)]['data'] = data_edit - helper.write_json(user_list) - bot.reply_to(m, "Date is updated") - - -def edit_cat(m, bot, selected_data): - user_list = helper.read_json() - 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:] - break - - user_list[str(chat_id)]['data'] = data_edit - helper.write_json(user_list) - bot.reply_to(m, "Category is updated") - - -def edit_cost(m, bot, selected_data): - user_list = helper.read_json() - new_cost = "" if m.text is None else m.text - chat_id = m.chat.id - data_edit = helper.getUserHistory(chat_id) - - if helper.validate_entered_amount(new_cost) != 0: - 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 + ',' + 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") - else: - bot.reply_to(m, "The cost is invalid") - return diff --git a/code/email_history.py b/code/email_history.py deleted file mode 100644 index 6ab6a1f85..000000000 --- a/code/email_history.py +++ /dev/null @@ -1,174 +0,0 @@ -""" - -MIT License - -Copyright (c) 2021 Dev Kumar - -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 logging -import smtplib -from email.mime.text import MIMEText -from email.mime.multipart import MIMEMultipart -from email.mime.base import MIMEBase -from email import encoders -import os.path -import threading -import extract - -# Constants and Configurations -SMTP_PORT = 587 -SMTP_SERVER = "smtp.gmail.com" -SENDER_EMAIL = "dollarbotnoreply@gmail.com" -SENDER_PASSWORD = "ogrigybfufihnkcc" - -extract_complete = threading.Event() - -def connect_to_smtp_server(): - """ - Connect to the SMTP server using the provided credentials. - - Returns: - smtplib.SMTP: Connected SMTP server. - """ - try: - logging.info("Connecting to SMTP server...") - smtp_server = smtplib.SMTP(SMTP_SERVER, SMTP_PORT) - smtp_server.starttls() - smtp_server.login(SENDER_EMAIL, SENDER_PASSWORD) - logging.info("Successfully connected to the SMTP server") - return smtp_server - except smtplib.SMTPException as e: - logging.error(f"SMTP Exception: {str(e)}") - raise - -def send_email(smtp_server, recipient_email, email_subject, email_body, attachment_path): - """ - Send an email with an attachment. - - Args: - smtp_server (smtplib.SMTP): Connected SMTP server. - recipient_email (str): Email address of the recipient. - email_subject (str): Subject of the email. - email_body (str): Body of the email. - attachment_path (str): Path to the attachment file. - """ - try: - body = email_body - msg = MIMEMultipart() - msg['From'] = SENDER_EMAIL - msg['To'] = recipient_email - msg['Subject'] = email_subject - - msg.attach(MIMEText(body, 'plain')) - - attachment_filename = attachment_path - - with open(attachment_filename, 'rb') as attachment_file: - attachment_package = MIMEBase('application', 'octet-stream') - attachment_package.set_payload(attachment_file.read()) - encoders.encode_base64(attachment_package) - attachment_package.add_header('Content-Disposition', "attachment; filename= " + attachment_filename) - msg.attach(attachment_package) - - email_text = msg.as_string() - - logging.info(f"Sending email to: {recipient_email}...") - smtp_server.sendmail(SENDER_EMAIL, recipient_email, email_text) - logging.info(f"Email sent to: {recipient_email}") - - except IOError as e: - logging.error(f"IOError: {str(e)}") - raise - except smtplib.SMTPRecipientsRefused as e: - logging.error(f"SMTP Recipients Refused: {str(e)}") - raise - except smtplib.SMTPSenderRefused as e: - logging.error(f"SMTP Sender Refused: {str(e)}") - raise - except smtplib.SMTPException as e: - logging.error(f"SMTP Exception: {str(e)}") - raise - except Exception as e: - logging.error(f"An unexpected error occurred: {str(e)}") - raise - -def close_smtp_connection(smtp_server): - """ - Close the connection to the SMTP server. - - Args: - smtp_server (smtplib.SMTP): Connected SMTP server. - """ - try: - smtp_server.quit() - except Exception as e: - logging.error(f"Error while closing SMTP connection: {str(e)}") - raise - -def run(message, bot): - """ - Run the main process for handling user input. - - Args: - message: User input message. - bot: Telegram bot instance. - """ - try: - chat_id = message.chat.id - message = bot.send_message(chat_id, 'Enter the email address to which you want to send your spend history') - bot.register_next_step_handler(message, handle_email_input, bot) - except Exception as e: - logging.error(f"An error occurred: {str(e)}") - -def handle_email_input(message, bot): - """ - Handle user input for the email address and initiate the email sending process. - - Args: - message: User input message. - bot: Telegram bot instance. - """ - try: - chat_id = message.chat.id - user_email = message.text - - email_subject = "Your DollarBot Spend History" - email_body = f"Hello {user_email},\n\nPlease find the attachment of your spend history" - - file_exist = os.path.isfile('code/Expenses_Data.csv') - if file_exist: - smtp_server = connect_to_smtp_server() - send_email(smtp_server, user_email, email_subject, email_body, 'code/Expenses_Data.csv') - else: - file_path = extract.run(message, bot) - smtp_server = connect_to_smtp_server() - send_email(smtp_server, user_email, email_subject, email_body, file_path) - - close_smtp_connection(smtp_server) - message = bot.send_message(chat_id, 'Your Email has been sent successfully!') - - except extract.ExtractError as e: - logging.error(f"Error during data extraction: {str(e)}") - message = bot.send_message(chat_id, 'Error during data extraction. Please try again.') - except Exception as e: - logging.error(f"An error occurred: {str(e)}") - raise \ No newline at end of file diff --git a/code/estimate.py b/code/estimate.py deleted file mode 100644 index b6e8105e3..000000000 --- a/code/estimate.py +++ /dev/null @@ -1,122 +0,0 @@ -""" - -MIT License - -Copyright (c) 2021 Dev Kumar - -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 time -import helper -import logging -from telebot import types - - -def run(message, bot): - helper.read_json() - chat_id = message.chat.id - history = helper.getUserHistory(chat_id) - if history is None or history == []: - bot.send_message( - chat_id, "Oops! Looks like you do not have any spending records!") - else: - markup = types.ReplyKeyboardMarkup(one_time_keyboard=True) - markup.row_width = 2 - for mode in helper.getSpendEstimateOptions(): - markup.add(mode) - # markup.add('Day', 'Month') - msg = bot.reply_to( - message, 'Please select the period to estimate', - reply_markup=markup) - bot.register_next_step_handler(msg, estimate_total, bot) - - -def estimate_total(message, bot): - try: - chat_id = message.chat.id - DayWeekMonth = message.text - - if DayWeekMonth not in helper.getSpendEstimateOptions(): - raise Exception( - "Sorry I can't show an estimate for \"{}\"!".format( - DayWeekMonth)) - - history = helper.getUserHistory(chat_id) - if history is None or history == []: - raise Exception( - "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) - bot.send_chat_action(chat_id, 'typing') - time.sleep(0.5) - - total_text = "" - days_to_estimate = 0 - if DayWeekMonth == 'Next day': - days_to_estimate = 1 - elif DayWeekMonth == 'Next month': - days_to_estimate = 30 - # query all that contains today's date - # query all that contains all history - queryResult = [value for index, value in enumerate(history)] - - total_text = calculate_estimate(queryResult, days_to_estimate) - - spending_text = "" - if len(total_text) == 0: - spending_text = "You have no estimate for {}!".format(DayWeekMonth) - else: - spending_text = "Here are your estimated spendings" - spending_text += " for the " + DayWeekMonth.lower() - spending_text += ":\nCATEGORIES,AMOUNT \n----------------------\n" - spending_text += total_text - - bot.send_message(chat_id, spending_text) - except Exception as e: - logging.exception(str(e)) - bot.reply_to(message, str(e)) - - -def calculate_estimate(queryResult, days_to_estimate): - total_dict = {} - days_data_available = {} - for row in queryResult: - # date,cat,money - s = row.split(',') - # cat - cat = s[1] - date_str = s[0][0:11] - if cat in total_dict: - # round up to 2 decimal - total_dict[cat] = round(total_dict[cat] + float(s[2]), 2) - else: - total_dict[cat] = float(s[2]) - if date_str not in days_data_available: - days_data_available[date_str] = True - - total_text = "" - for key, value in total_dict.items(): - category_count = len(days_data_available) - daily_avg = value / category_count - estimated_avg = round(daily_avg * days_to_estimate, 2) - total_text += str(key) + " $" + str(estimated_avg) + "\n" - return total_text diff --git a/code/graphing.py b/code/graphing.py deleted file mode 100644 index 3960885d9..000000000 --- a/code/graphing.py +++ /dev/null @@ -1,134 +0,0 @@ -""" - -MIT License - -Copyright (c) 2021 Dev Kumar - -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 matplotlib -import matplotlib.pyplot as plt - -matplotlib.use('Agg') - - -def addlabels(x, y): - for i in range(len(x)): - plt.text(i, y[i] // 2, y[i], ha='center') - - -def visualize(total_text, budgetData): - # set the color for bars - colors = ['red', 'cornflowerblue', 'greenyellow', 'orange', 'violet', 'grey'] - # plot the expense bar chart - total_text_split = [line for line in total_text.split('\n') if line.strip() != ''] - categ_val = {} - # summarize the expense by categories - for i in total_text_split: - a = i.split(' ') - a[1] = a[1].replace("$", "") - categ_val[a[0]] = float(a[1]) - - # set categories as x-axis and amount as y-axis - x = list(categ_val.keys()) - y = list(categ_val.values()) - - plt.bar(categ_val.keys(), categ_val.values(), color=colors, edgecolor='black') - addlabels(x, y) - - plt.ylabel("Expenditure") - plt.xlabel("Categories") - plt.xticks(rotation=45) - - # plot budget in the horizontal line format - lines = [] - labels = [] - if isinstance(budgetData, str): - # if budget data is str denoting it is overall budget - lines.append(plt.axhline(y=float(budgetData), color="r", linestyle="-")) - labels.append("overall budget") - elif isinstance(budgetData, dict): - # if budget data is dict denoting it is category budget - colorCnt = 0 - # to avoid the budget override by each others, record the budget and adjust the position of the line - duplicate = {} - for key in budgetData.keys(): - val = budgetData[key] - plotVal = float(val) - if val in duplicate: - # if duplicate, move line upwards - plotVal += 2 * duplicate[val] - lines.append(plt.axhline(y=plotVal, color=colors[colorCnt % len(colors)], linestyle="-")) - - # record the amount - duplicate[val] = duplicate[val] + 1 if val in duplicate else 1 - labels.append(key) - colorCnt += 1 - - plt.legend(lines, labels) - plt.savefig('expenditure.png', bbox_inches='tight') - - # clean the plot to avoid the old data remains on it - plt.clf() - plt.cla() - plt.close() - -def vis(total_text): - total_text_split = [line for line in total_text.split('\n') if line.strip() != ''] - categ_val = {} - for i in total_text_split: - a = i.split(' ') - a[1] = a[1].replace("$", "") - categ_val[a[0]] = float(a[1]) - - x = list(categ_val.keys()) - y = list(categ_val.values()) - - #plt.bar(categ_val.keys(), categ_val.values(), color=[(1.00, 0, 0, 0.6), (0.2, 0.4, 0.6, 0.6), (0, 1.00, 0, 0.6), (1.00, 1.00, 0, 1.00)], edgecolor='blue') - #addlabels(x, y) - plt.clf() - plt.pie(y, labels=x, autopct='%.1f%%') - #plt.ylabel("Categories") - #plt.xlabel("Expenditure") - #plt.xticks(rotation=90) - - plt.savefig('pie.png') - - -def viz(total_text): - total_text_split = [line for line in total_text.split('\n') if line.strip() != ''] - categ_val = {} - for i in total_text_split: - a = i.split(' ') - a[1] = a[1].replace("$", "") - categ_val[a[0]] = float(a[1]) - - x = list(categ_val.keys()) - y = list(categ_val.values()) - plt.clf() - plt.bar(categ_val.keys(), categ_val.values(), color=[(1.00, 0, 0, 0.6), (0.2, 0.4, 0.6, 0.6), (0, 1.00, 0, 0.6), (1.00, 1.00, 0, 1.00)], edgecolor='blue') - addlabels(x, y) - - plt.ylabel("Categories") - plt.xlabel("Expenditure") - plt.xticks(rotation=45) - - plt.savefig('expend.png', bbox_inches='tight') diff --git a/code/helper.py b/code/helper.py deleted file mode 100644 index 7d3cb4349..000000000 --- a/code/helper.py +++ /dev/null @@ -1,336 +0,0 @@ -""" - -MIT License - -Copyright (c) 2021 Dev Kumar - -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 re -import json -import os -from datetime import datetime - -with open('variables.json') as variables: - variables_data = json.load(variables) - -spend_categories = variables_data["variables"]["spend_categories"] -account_categories = variables_data["variables"]["account_categories"] -currencies = variables_data["variables"]["currencies"] -choices = variables_data["variables"]["choices"] -plot = variables_data["variables"]["plot"] -spend_display_option = variables_data["variables"]["spend_display_option"] -spend_estimate_option = variables_data["variables"]["spend_estimate_option"] -update_options = variables_data["variables"]["update_options"] -budget_options = variables_data["variables"]["budget_options"] -budget_types = variables_data["variables"]["budget_types"] -data_format = variables_data["variables"]["data_format"] -category_options = variables_data["variables"]["category_options"] -commands = variables_data["variables"]["commands"] -dateFormat = variables_data["variables"]["dateFormat"] -timeFormat = variables_data["variables"]["timeFormat"] -monthFormat = variables_data["variables"]["monthFormat"] - -# function to load .json expense record data -def read_json(): - try: - if not os.path.exists('expense_record.json'): - with open('expense_record.json', 'w') as json_file: - json_file.write('{}') - return json.dumps('{}') - elif os.stat('expense_record.json').st_size != 0: - with open('expense_record.json') as expense_record: - expense_record_data = json.load(expense_record) - return expense_record_data - - except FileNotFoundError: - print("---------NO RECORDS FOUND---------") - -# function to write the expense record -def write_json(user_list): - try: - with open('expense_record.json', 'w') as json_file: - json.dump(user_list, json_file, ensure_ascii=False, indent=4) - except FileNotFoundError: - print('Sorry, the data file could not be found.') - -# function to validate the entered amount -def validate_entered_amount(amount_entered): - if amount_entered is None: - return 0 - if re.match("^[1-9][0-9]{0,14}\\.[0-9]*$", amount_entered) or re.match("^[1-9][0-9]{0,14}$", amount_entered): - amount = round(float(amount_entered), 2) - if amount > 0: - return str(amount) - return 0 - -#function to validate the entered time -def validate_time_format(time_str): - # Use a regular expression to match the time format HH:MM - time_pattern = r'^([01]\d|2[0-3]):([0-5]\d)$' - - if re.match(time_pattern, time_str): - return True - else: - return False - -# function to validate the entered duration -def validate_entered_duration(duration_entered): - if duration_entered is None: - return 0 - if re.match("^[1-9][0-9]{0,14}", duration_entered): - duration = int(duration_entered) - if duration > 0: - return str(duration) - return 0 - -# function to get user history -def getUserHistory(chat_id): - data = getUserData(chat_id) - if data is not None: - return data['data'] - return None - -# function to get user data -def getUserData(chat_id): - user_list = read_json() - if user_list is None: - return None - if (str(chat_id) in user_list): - return user_list[str(chat_id)] - return None - -# function to throw exception -def throw_exception(e, message, bot, logging): - logging.exception(str(e)) - bot.reply_to(message, 'Oh no! ' + str(e)) - -# function to create a new user record -def createNewUserRecord(): - return data_format - -# function to get overall budget -def getOverallBudget(chatId): - data = getUserData(chatId) - if data is None: - return None - return data['budget']['overall'] - -def getUserReminder(chat_id): - data = getUserData(chat_id) - if data is not None: - return data['data'] - return None - - -# function to get category based budget -def getCategoryBudget(chatId): - data = getUserData(chatId) - if data is None: - return None - return data['budget']['category'] - -# function to get category by category -def getCategoryBudgetByCategory(chatId, cat): - if not isCategoryBudgetByCategoryAvailable(chatId, cat): - return None - data = getCategoryBudget(chatId) - return data[cat] - -# function to can add budget -def canAddBudget(chatId): - return (getOverallBudget(chatId) is None) and (getCategoryBudget(chatId) is None) - -# function to check if overall budget available or not -def isOverallBudgetAvailable(chatId): - return getOverallBudget(chatId) is not None - -# function to check if category budget available or not -def isCategoryBudgetAvailable(chatId): - return getCategoryBudget(chatId) is not None - -# function to check if category budget by category available or not -def isCategoryBudgetByCategoryAvailable(chatId, cat): - data = getCategoryBudget(chatId) - if data is None: - return False - return cat in data.keys() - -# function to check if there's balance in a particular account type -def isBalanceAvailable(chat_id, cat): - data = getUserData(chat_id) - if data['balance'][cat] is None: - return False - else: - return data['balance'][cat] - -# function to get balance in a particular category account. -def get_account_balance(message, bot, cat): - if isBalanceAvailable(message.chat.id, cat): - return float(isBalanceAvailable(message.chat.id, cat)) - else: - return 0 - -# function to get the current active account for expenses. -def get_account_type(message): - data = getUserData(message.chat.id) - if data['account']['Checking'] == "True": - return 'Checking' - else: - return 'Savings' - -# function to display balance in a particular category account. -def display_account_balance(message, bot, cat): - chat_id = message.chat.id - if get_account_balance(message, bot, cat) != 0: - print("Balance in {} account is: {}.".format(cat, float(get_account_balance(message, bot, cat)))) - else: - print("This Account category has no existing balance") - -# function to display remaining budget -def display_remaining_budget(message, bot, cat): - chat_id = message.chat.id - if isOverallBudgetAvailable(chat_id): - display_remaining_overall_budget(message, bot) - elif isCategoryBudgetByCategoryAvailable(chat_id, cat): - display_remaining_category_budget(message, bot, cat) - -# function to display remaining overall budget -def display_remaining_overall_budget(message, bot): - print('here') - chat_id = message.chat.id - remaining_budget = calculateRemainingOverallBudget(chat_id) - print("here", remaining_budget) - if remaining_budget >= 0: - msg = '\nRemaining Overall Budget is $' + str(remaining_budget) - else: - msg = '\nBudget Exceded!\nExpenditure exceeds the budget by $' + str(remaining_budget)[1:] - bot.send_message(chat_id, msg) - -# function to calculate remaining overall budget -def calculateRemainingOverallBudget(chat_id): - budget = getOverallBudget(chat_id) - history = getUserHistory(chat_id) - query = datetime.now().today().strftime(getMonthFormat()) - queryResult = [value for index, value in enumerate(history) if str(query) in value] - - return float(budget) - calculate_total_spendings(queryResult) - -# function to calculate total spending -def calculate_total_spendings(queryResult): - total = 0 - - for row in queryResult: - s = row.split(',') - total = total + float(s[2]) - return total - -# function to display remaining category budget -def display_remaining_category_budget(message, bot, cat): - chat_id = message.chat.id - remaining_budget = calculateRemainingCategoryBudget(chat_id, cat) - if remaining_budget >= 0: - msg = '\nRemaining Budget for ' + cat + ' is $' + str(remaining_budget) - else: - msg = '\nBudget for ' + cat + ' Exceded!\nExpenditure exceeds the budget by $' + str(abs(remaining_budget)) - bot.send_message(chat_id, msg) - -# function to calculate remaining category based budget -def calculateRemainingCategoryBudget(chat_id, cat): - budget = getCategoryBudgetByCategory(chat_id, cat) - history = getUserHistory(chat_id) - query = datetime.now().today().strftime(getMonthFormat()) - queryResult = [value for index, value in enumerate(history) if str(query) in value] - - return float(budget) - calculate_total_spendings_for_category(queryResult, cat) - -# function to calculate total spending per category -def calculate_total_spendings_for_category(queryResult, cat): - total = 0 - - for row in queryResult: - s = row.split(',') - if cat == s[1]: - total = total + float(s[2]) - return total - -# function to get spending categories -def getSpendCategories(): - with open("categories.txt", "r") as tf: - spend_categories = tf.read().split(',') - return spend_categories - -def getAccountCategories(): - return account_categories - -#function to get different currencies -def getCurrencies(): - with open("currencies.txt", "r") as tf: - currencies = tf.read().split(',') - return currencies - -# function to get plot -def getplot(): - return plot - -# function to display spend options -def getSpendDisplayOptions(): - return spend_display_option - -# function to get spend estimations -def getSpendEstimateOptions(): - return spend_estimate_option - -# function to fetch commands -def getCommands(): - return commands - -# function to fetch date format -def getDateFormat(): - return dateFormat - -# function to fetch time format -def getTimeFormat(): - return timeFormat - -# function to fetch month format -def getMonthFormat(): - return monthFormat - -# function to fetch choices -def getChoices(): - return choices - -# function to fetch budget options -def getBudgetOptions(): - return budget_options - -# function to fetch budget types -def getBudgetTypes(): - return budget_types - -# function to update options -def getUpdateOptions(): - return update_options - -# function to fetch category options -def getCategoryOptions(): - return category_options diff --git a/code/history.py b/code/history.py deleted file mode 100644 index ddd59ab02..000000000 --- a/code/history.py +++ /dev/null @@ -1,65 +0,0 @@ -""" - -MIT License - -Copyright (c) 2021 Dev Kumar - -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 -import logging -import matplotlib.pyplot as plt - -def run(message, bot): - try: - helper.read_json() - chat_id = message.chat.id - user_history = helper.getUserHistory(chat_id) - spend_total_str = "" - # Amount for each month - amount = 0.0 - am = "" - Dict = {'Jan': 0.0, 'Feb': 0.0, 'Mar': 0.0, 'Apr': 0.0, 'May': 0.0, 'Jun': 0.0, 'Jul': 0.0, 'Sep': 0.0, 'Oct': 0.0, 'Nov': 0.0, 'Dec': 0.0} - if user_history is None: - raise Exception("Sorry! No spending records found!") - spend_total_str = "Here is your spending history : \nDATE, CATEGORY, AMOUNT\n----------------------\n" - if len(user_history) == 0: - spend_total_str = "Sorry! No spending records found!" - else: - for rec in user_history: - spend_total_str += str(rec) + "\n" - av = str(rec).split(",") - ax = av[0].split("-") - am = ax[1] - amount = Dict[am] + float(av[2]) - Dict[am] = amount - bot.send_message(chat_id, spend_total_str) - - # bot.send_message(chat_id, Dict[am]) - plt.clf() - width = 1.0 - plt.bar(Dict.keys(), Dict.values(), width, color='g') - plt.savefig('histo.png') - bot.send_photo(chat_id, photo=open('histo.png', 'rb')) - ##bot.send_message(chat_id, amount) - except Exception as e: - logging.exception(str(e)) - bot.reply_to(message, "Oops!" + str(e)) diff --git a/code/reminder.py b/code/reminder.py deleted file mode 100644 index 361eee195..000000000 --- a/code/reminder.py +++ /dev/null @@ -1,139 +0,0 @@ -""" - -MIT License - -Copyright (c) 2021 Dev Kumar - -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 -from datetime import datetime, time -import time -from telebot import types -import json -import display - - -def run(message, bot): - helper.read_json() - chat_id = message.chat.id - history = helper.getUserHistory(chat_id) - if history is None or history == []: - bot.send_message(chat_id, "Sorry, there are no records of spending!") - else: - markup = types.ReplyKeyboardMarkup(one_time_keyboard=True) - markup.row_width = 2 - for mode in helper.getSpendDisplayOptions(): - markup.add(mode) - msg = bot.reply_to(message, 'Please select a category to set reminders for', reply_markup=markup) - bot.register_next_step_handler(msg, process_reminder_type, chat_id, bot) - -def process_reminder_type(message, chat_id, bot): - selected_type = message.text - bot.send_message(chat_id, 'Please enter the time for the reminder in the format HH:MM (e.g., 14:30)') - bot.register_next_step_handler(message, process_reminder_time, chat_id, selected_type, bot) - -def process_reminder_time(message, chat_id, selected_type, bot): - reminder_time_str = message.text - if helper.validate_time_format(reminder_time_str): - with open("expense_record.json", "r") as file: - json_data = json.load(file) - - if str(chat_id) in json_data: - json_data[str(chat_id)]["reminder"]["type"] = selected_type - json_data[str(chat_id)]["reminder"]["time"] = reminder_time_str - - with open("expense_record.json", "w") as file: - json.dump(json_data, file, indent=4) - - bot.send_message(chat_id, f"Your {selected_type} expenses reminder has been set for {reminder_time_str}.") - else: - bot.send_message(chat_id, "Invalid time format. Please try /set_reminder with the time in the format HH:MM (e.g., 14:30)") - - -sent_reminders = {} - - -def send_expenses_reminder(chat_id, dayormonth, bot): - history = helper.getUserHistory(chat_id) - if history is None or history == []: - raise Exception("Oops! Looks like you do not have any spending records!") - - bot.send_message(chat_id, "Your Daily Expense Reminder...") - # show the bot "typing" (max. 5 secs) - bot.send_chat_action(chat_id, 'typing') - time.sleep(0.5) - total_text = "" - # get budget data - budgetData = {} - if helper.isOverallBudgetAvailable(chat_id): - budgetData = helper.getOverallBudget(chat_id) - elif helper.isCategoryBudgetAvailable(chat_id): - budgetData = helper.getCategoryBudget(chat_id) - - if dayormonth == 'Day': - query = datetime.now().today().strftime(helper.getDateFormat()) - # query all that contains today's date - queryResult = [value for index, value in enumerate(history) if str(query) in value] - elif dayormonth == 'Month': - query = datetime.now().today().strftime(helper.getMonthFormat()) - # query all that contains today's date - queryResult = [value for index, value in enumerate(history) if str(query) in value] - - total_text = display.calculate_spendings(queryResult) - spending_text = display.display_budget_by_text(history, budgetData) - if len(total_text) == 0: - spending_text += "----------------------\nYou have no spendings for {}!".format(dayormonth) - bot.send_message(chat_id, spending_text) - else: - spending_text += "\n----------------------\nHere are your total spendings {}:\nCATEGORIES,AMOUNT \n----------------------\n{}".format(dayormonth.lower(), total_text) - bot.send_message(chat_id, spending_text) - -def send_reminder(chat_id, message, bot): - print(f"Sending reminder to chat ID {chat_id}: {message}") - bot.send_message(chat_id, message) - -def check_reminders(bot): - print("Checking reminders...") - # Load the JSON data - with open("expense_record.json", "r") as file: - json_data = json.load(file) - - current_time = datetime.now().time() - current_date = datetime.now().strftime('%d-%b-%Y') # Get the current date in the same format as your data - - # Loop through the chat IDs and their reminders - for chat_id, data in json_data.items(): - reminder = data.get("reminder") - if reminder and reminder.get("time"): - reminder_time = datetime.strptime(reminder["time"], '%H:%M').time() - # Compare the time components without seconds - if current_time.hour == reminder_time.hour and current_time.minute == reminder_time.minute: - reminder_type = reminder.get("type") - if (chat_id, current_date, str(reminder_time.hour) + ":" + str(reminder_time.minute)) not in sent_reminders: - # Send a daily reminder - message = "Your daily reminder message goes here." - print(f"Sending reminder to chat ID {chat_id}: {message}") - # send_reminder(chat_id, message) - send_expenses_reminder(chat_id, reminder_type, bot) - # Mark this reminder as sent - sent_reminders[(chat_id, current_date, str(reminder_time.hour) + ":" + str(reminder_time.minute))] = True - print(sent_reminders) diff --git a/config.py b/config.py new file mode 100644 index 000000000..e88cbb724 --- /dev/null +++ b/config.py @@ -0,0 +1,20 @@ +""" +This module contains configuration settings for the Money Manager application. +""" + +import os + +MONGO_URI = os.getenv( + "MONGO_URI", + "mongodb+srv://mmdb_admin:tiaNSKxzyO2NdXts@moneymanagerdb.s2bp9.mongodb.net/" + "?retryWrites=true&w=majority&appName=MoneyManagerDB", +) + +TOKEN_SECRET_KEY = "your_secret_key" +TOKEN_ALGORITHM = "HS256" + +API_BIND_HOST = "0.0.0.0" +API_BIND_PORT = 9999 + +TELEGRAM_BOT_TOKEN = "7601886777:AAEqIw5gVUPBuDqVeWwR1GdG_Y8eiA0JKFA" +TELEGRAM_BOT_API_BASE_URL = "http://localhost:9999" diff --git a/currencies.txt b/currencies.txt deleted file mode 100644 index ba5aed53d..000000000 --- a/currencies.txt +++ /dev/null @@ -1 +0,0 @@ -USD,INR,GBP,EUR,CAD,JPY diff --git a/docs/0001-8711513694_20210926_212845_0000.png b/docs/0001-8711513694_20210926_212845_0000.png deleted file mode 100644 index 859b29b1b..000000000 Binary files a/docs/0001-8711513694_20210926_212845_0000.png and /dev/null differ diff --git a/docs/DOLLARBOT.jpg b/docs/DOLLARBOT.jpg deleted file mode 100644 index 0188824b2..000000000 Binary files a/docs/DOLLARBOT.jpg and /dev/null differ diff --git a/docs/UserTutorialDocuments/AddBalanceTutorial.md b/docs/UserTutorialDocuments/AddBalanceTutorial.md deleted file mode 100644 index f52f8e226..000000000 --- a/docs/UserTutorialDocuments/AddBalanceTutorial.md +++ /dev/null @@ -1,24 +0,0 @@ -**Tutorial to add Balance:** - -1. Type '/add_balance' in the chat or select '/add_balance' option from - the menu. - - ![](./images/image1.png) - -2. Now, you will be prompted to select the balance category. - - ![](./images/image2.png) - -3. After you have successfully selected a category, you will be - prompted to add an input currency. - - ![](./images/image3.png) - -4. Now, you will be prompted to add an amount. Please enter the amount - in your selected currency. - - ![](./images/image4.png) - -5. You are all done!! Your expense will be successfully recorded. - - ![](./images/image5.png) diff --git a/docs/UserTutorialDocuments/AddExpenseTutorial.md b/docs/UserTutorialDocuments/AddExpenseTutorial.md deleted file mode 100644 index d2c650a17..000000000 --- a/docs/UserTutorialDocuments/AddExpenseTutorial.md +++ /dev/null @@ -1,26 +0,0 @@ -**Tutorial to add an expense:** - -1. Type ‘/add’ in the chat or select ‘/add’ option from the menu. - - ![](./images/Aspose.Words.5d9dcf9e-3949-4520-a57f-7c5b9cf60536.001.png) - - -2. Now, you will be prompted to select the category. - - ![](./images/Aspose.Words.5d9dcf9e-3949-4520-a57f-7c5b9cf60536.002.png) - - -3. After you have successfully selected a category, you will be prompted to add an input currency. - - ![](./images/Aspose.Words.5d9dcf9e-3949-4520-a57f-7c5b9cf60536.003.png) - - -4. Select a currency from the menu of available currencies. - - ![](./images/Aspose.Words.5d9dcf9e-3949-4520-a57f-7c5b9cf60536.004.png) - -5. Now, you will be prompted to add an amount. Please enter the amount in your selected currency. - - ![](./images/Aspose.Words.5d9dcf9e-3949-4520-a57f-7c5b9cf60536.005.png) - -6. You are all done!! Your expense will be successfully recorded. diff --git a/docs/UserTutorialDocuments/AddRecurringExpenseTutorial.md b/docs/UserTutorialDocuments/AddRecurringExpenseTutorial.md deleted file mode 100644 index 6a1122f83..000000000 --- a/docs/UserTutorialDocuments/AddRecurringExpenseTutorial.md +++ /dev/null @@ -1,28 +0,0 @@ -**Tutorial to add a recurring expense:** - -1. Type ‘/add\_recurring’ in the chat or select ‘/add\_recurring’ option from the menu. - - ![](./images/Aspose.Words.2f7312a3-bb59-4e7d-8499-28187a0f9fb3.001.png) - - -2. Now, you will be prompted to select the category. - - ![](./images/Aspose.Words.2f7312a3-bb59-4e7d-8499-28187a0f9fb3.002.png) - - -3. After you have successfully selected a category, you will be prompted to add an input currency. - - ![](./images/Aspose.Words.2f7312a3-bb59-4e7d-8499-28187a0f9fb3.003.png) - - -4. Select a currency from the menu of available currencies. You will now be asked for the duration in months for which this recurring expense will be charged. - - ![](./images/Aspose.Words.2f7312a3-bb59-4e7d-8499-28187a0f9fb3.004.png) - - -5. Now, you will be prompted to add an amount. Please enter the amount in your selected currency. - - ![](./images/Aspose.Words.2f7312a3-bb59-4e7d-8499-28187a0f9fb3.005.png) - -6. You are all done!! Your expense will be successfully recorded. - diff --git a/docs/UserTutorialDocuments/DisplayTutorial.md b/docs/UserTutorialDocuments/DisplayTutorial.md deleted file mode 100644 index 6527078a3..000000000 --- a/docs/UserTutorialDocuments/DisplayTutorial.md +++ /dev/null @@ -1,24 +0,0 @@ -**Tutorial to Display data:** - -1. Type ‘/display in the chat or select ‘/display’ option from the menu. - - ![](./images/Aspose.Words.35a40138-4445-4305-9f60-676b5e33b955.001.png) - -2. You will be prompted to select Day or Month to indicate which format you need your expense history displayed in. - - ![](./images/Aspose.Words.35a40138-4445-4305-9f60-676b5e33b955.002.png) - - -3. Your budget will be calculated. - - ![](./images/Aspose.Words.35a40138-4445-4305-9f60-676b5e33b955.003.png) - -4. You will then be prompted to enter which type of plot you need your data to be displayed in. - - ![](./images/Aspose.Words.35a40138-4445-4305-9f60-676b5e33b955.004.png) - - -5. After selecting, the type of graph, you will be given the plot of you expenses. - - ![](./images/Aspose.Words.35a40138-4445-4305-9f60-676b5e33b955.005.png) - diff --git a/docs/UserTutorialDocuments/DownloadCSV.md b/docs/UserTutorialDocuments/DownloadCSV.md deleted file mode 100644 index b08c2857f..000000000 --- a/docs/UserTutorialDocuments/DownloadCSV.md +++ /dev/null @@ -1,15 +0,0 @@ -**Tutorial to Download expense as csv:** - -1. Type ‘/csv’ in the chat or select ‘/csv’ option from the menu. - - ![](./images/Aspose.Words.62bd5652-0f0b-4fde-b32f-e70d215b9924.001.png) - - -2. The csv file with the expense history will be displayed. - - ![](./images/Aspose.Words.62bd5652-0f0b-4fde-b32f-e70d215b9924.002.png) - -3. You can export the file and see your expense data!! - - ![](./images/Aspose.Words.62bd5652-0f0b-4fde-b32f-e70d215b9924.003.png) - diff --git a/docs/UserTutorialDocuments/EmailSpendingTutorial.md b/docs/UserTutorialDocuments/EmailSpendingTutorial.md deleted file mode 100644 index 77c49dc1f..000000000 --- a/docs/UserTutorialDocuments/EmailSpendingTutorial.md +++ /dev/null @@ -1,21 +0,0 @@ -**Tutorial to Email Expense History:** - -1. Type ‘/email\_history’ in the chat or select ‘/email\_history’ option from the menu. - - ![](./images/Aspose.Words.00f35197-2802-462c-9e95-0ffab17445cd.001.png) - -2. You will then be prompted to provide your email address. - - ![](./images/Aspose.Words.00f35197-2802-462c-9e95-0ffab17445cd.002.png) - - -3. After you provide your email address, you will receive a confirmation that your expense report has been mailed successfully. - - ![](./images/Aspose.Words.00f35197-2802-462c-9e95-0ffab17445cd.003.png) - - -4. You can check your email for the file with the necessary expense data!! - - ![](./images/Aspose.Words.00f35197-2802-462c-9e95-0ffab17445cd.004.png) - - diff --git a/docs/UserTutorialDocuments/EstimateExpenseTutorial.md b/docs/UserTutorialDocuments/EstimateExpenseTutorial.md deleted file mode 100644 index 59f00085a..000000000 --- a/docs/UserTutorialDocuments/EstimateExpenseTutorial.md +++ /dev/null @@ -1,14 +0,0 @@ -**Tutorial to Estimate Expense:** - -1. Type ‘/estimate’ in the chat or select ‘/estimate’ option from the menu. - - ![](./images/Aspose.Words.055eb371-ead8-49d0-93f1-e3f6471b7b28.001.png) - -2. You can then select if you want an estimate for the next day or for the next month. - - ![](./images/Aspose.Words.055eb371-ead8-49d0-93f1-e3f6471b7b28.002.png) - -3. After selecting an option, the bot calculates our estimated expenditure based on our current expenses!! - - ![](./images/Aspose.Words.055eb371-ead8-49d0-93f1-e3f6471b7b28.003.png) - diff --git a/docs/UserTutorialDocuments/ExpenseHistory.md b/docs/UserTutorialDocuments/ExpenseHistory.md deleted file mode 100644 index e4324cc0a..000000000 --- a/docs/UserTutorialDocuments/ExpenseHistory.md +++ /dev/null @@ -1,17 +0,0 @@ -**Tutorial to view Expense History:** - -1. Type ‘/history’ in the chat or select ‘/history’ option from the menu. - - ![](./images/Aspose.Words.77495f85-6dd6-419d-b973-3d76d715f472.001.png) - - -2. The history of expenses will be displayed by the bot. - - ![](./images/Aspose.Words.77495f85-6dd6-419d-b973-3d76d715f472.002.png) - - -3. A bar graph will also be displayed to graphically represent the expenses!! - - ![](./images/Aspose.Words.77495f85-6dd6-419d-b973-3d76d715f472.003.png) - - diff --git a/docs/UserTutorialDocuments/SetReminderTutorial.md b/docs/UserTutorialDocuments/SetReminderTutorial.md deleted file mode 100644 index 30e86cf70..000000000 --- a/docs/UserTutorialDocuments/SetReminderTutorial.md +++ /dev/null @@ -1,18 +0,0 @@ -**Tutorial to set expense reminder:** - -1. Type ‘/set\_reminder’ in the chat or select ‘/set\_reminder’ option from the menu. - - ![](./images/Aspose.Words.db83c9c2-55e3-4bd4-a261-66f2533abc42.001.png) - -2. You will be then prompted to select Day or Month, to indicate your choice that do you want a daily reminder or a monthly reminder. - - ![](./images/Aspose.Words.db83c9c2-55e3-4bd4-a261-66f2533abc42.002.png) - -3. After your selection, you will be asked to provide the time for your reminder. - - ![](./images/Aspose.Words.db83c9c2-55e3-4bd4-a261-66f2533abc42.003.png) - -4. Then whenever it is the time you set to send you a reminder, the bot will automatically send a text to remind you of your expenses!! - - ![](./images/Aspose.Words.db83c9c2-55e3-4bd4-a261-66f2533abc42.004.png) - diff --git a/docs/UserTutorialDocuments/images/Aspose.Words.00f35197-2802-462c-9e95-0ffab17445cd.001.png b/docs/UserTutorialDocuments/images/Aspose.Words.00f35197-2802-462c-9e95-0ffab17445cd.001.png deleted file mode 100644 index 47dcd2c96..000000000 Binary files a/docs/UserTutorialDocuments/images/Aspose.Words.00f35197-2802-462c-9e95-0ffab17445cd.001.png and /dev/null differ diff --git a/docs/UserTutorialDocuments/images/Aspose.Words.00f35197-2802-462c-9e95-0ffab17445cd.002.png b/docs/UserTutorialDocuments/images/Aspose.Words.00f35197-2802-462c-9e95-0ffab17445cd.002.png deleted file mode 100644 index 1fd3e0df1..000000000 Binary files a/docs/UserTutorialDocuments/images/Aspose.Words.00f35197-2802-462c-9e95-0ffab17445cd.002.png and /dev/null differ diff --git a/docs/UserTutorialDocuments/images/Aspose.Words.00f35197-2802-462c-9e95-0ffab17445cd.003.png b/docs/UserTutorialDocuments/images/Aspose.Words.00f35197-2802-462c-9e95-0ffab17445cd.003.png deleted file mode 100644 index aba5d0ac2..000000000 Binary files a/docs/UserTutorialDocuments/images/Aspose.Words.00f35197-2802-462c-9e95-0ffab17445cd.003.png and /dev/null differ diff --git a/docs/UserTutorialDocuments/images/Aspose.Words.00f35197-2802-462c-9e95-0ffab17445cd.004.png b/docs/UserTutorialDocuments/images/Aspose.Words.00f35197-2802-462c-9e95-0ffab17445cd.004.png deleted file mode 100644 index 49927edbe..000000000 Binary files a/docs/UserTutorialDocuments/images/Aspose.Words.00f35197-2802-462c-9e95-0ffab17445cd.004.png and /dev/null differ diff --git a/docs/UserTutorialDocuments/images/Aspose.Words.055eb371-ead8-49d0-93f1-e3f6471b7b28.001.png b/docs/UserTutorialDocuments/images/Aspose.Words.055eb371-ead8-49d0-93f1-e3f6471b7b28.001.png deleted file mode 100644 index 3aedebaf9..000000000 Binary files a/docs/UserTutorialDocuments/images/Aspose.Words.055eb371-ead8-49d0-93f1-e3f6471b7b28.001.png and /dev/null differ diff --git a/docs/UserTutorialDocuments/images/Aspose.Words.055eb371-ead8-49d0-93f1-e3f6471b7b28.002.png b/docs/UserTutorialDocuments/images/Aspose.Words.055eb371-ead8-49d0-93f1-e3f6471b7b28.002.png deleted file mode 100644 index a6178a77d..000000000 Binary files a/docs/UserTutorialDocuments/images/Aspose.Words.055eb371-ead8-49d0-93f1-e3f6471b7b28.002.png and /dev/null differ diff --git a/docs/UserTutorialDocuments/images/Aspose.Words.055eb371-ead8-49d0-93f1-e3f6471b7b28.003.png b/docs/UserTutorialDocuments/images/Aspose.Words.055eb371-ead8-49d0-93f1-e3f6471b7b28.003.png deleted file mode 100644 index 0b19a0bd6..000000000 Binary files a/docs/UserTutorialDocuments/images/Aspose.Words.055eb371-ead8-49d0-93f1-e3f6471b7b28.003.png and /dev/null differ diff --git a/docs/UserTutorialDocuments/images/Aspose.Words.2f7312a3-bb59-4e7d-8499-28187a0f9fb3.001.png b/docs/UserTutorialDocuments/images/Aspose.Words.2f7312a3-bb59-4e7d-8499-28187a0f9fb3.001.png deleted file mode 100644 index bac47b725..000000000 Binary files a/docs/UserTutorialDocuments/images/Aspose.Words.2f7312a3-bb59-4e7d-8499-28187a0f9fb3.001.png and /dev/null differ diff --git a/docs/UserTutorialDocuments/images/Aspose.Words.2f7312a3-bb59-4e7d-8499-28187a0f9fb3.002.png b/docs/UserTutorialDocuments/images/Aspose.Words.2f7312a3-bb59-4e7d-8499-28187a0f9fb3.002.png deleted file mode 100644 index b1e9ea292..000000000 Binary files a/docs/UserTutorialDocuments/images/Aspose.Words.2f7312a3-bb59-4e7d-8499-28187a0f9fb3.002.png and /dev/null differ diff --git a/docs/UserTutorialDocuments/images/Aspose.Words.2f7312a3-bb59-4e7d-8499-28187a0f9fb3.003.png b/docs/UserTutorialDocuments/images/Aspose.Words.2f7312a3-bb59-4e7d-8499-28187a0f9fb3.003.png deleted file mode 100644 index 2591ddfbd..000000000 Binary files a/docs/UserTutorialDocuments/images/Aspose.Words.2f7312a3-bb59-4e7d-8499-28187a0f9fb3.003.png and /dev/null differ diff --git a/docs/UserTutorialDocuments/images/Aspose.Words.2f7312a3-bb59-4e7d-8499-28187a0f9fb3.004.png b/docs/UserTutorialDocuments/images/Aspose.Words.2f7312a3-bb59-4e7d-8499-28187a0f9fb3.004.png deleted file mode 100644 index b8a9263d4..000000000 Binary files a/docs/UserTutorialDocuments/images/Aspose.Words.2f7312a3-bb59-4e7d-8499-28187a0f9fb3.004.png and /dev/null differ diff --git a/docs/UserTutorialDocuments/images/Aspose.Words.2f7312a3-bb59-4e7d-8499-28187a0f9fb3.005.png b/docs/UserTutorialDocuments/images/Aspose.Words.2f7312a3-bb59-4e7d-8499-28187a0f9fb3.005.png deleted file mode 100644 index 22ea681f4..000000000 Binary files a/docs/UserTutorialDocuments/images/Aspose.Words.2f7312a3-bb59-4e7d-8499-28187a0f9fb3.005.png and /dev/null differ diff --git a/docs/UserTutorialDocuments/images/Aspose.Words.35a40138-4445-4305-9f60-676b5e33b955.001.png b/docs/UserTutorialDocuments/images/Aspose.Words.35a40138-4445-4305-9f60-676b5e33b955.001.png deleted file mode 100644 index ede2f063b..000000000 Binary files a/docs/UserTutorialDocuments/images/Aspose.Words.35a40138-4445-4305-9f60-676b5e33b955.001.png and /dev/null differ diff --git a/docs/UserTutorialDocuments/images/Aspose.Words.35a40138-4445-4305-9f60-676b5e33b955.002.png b/docs/UserTutorialDocuments/images/Aspose.Words.35a40138-4445-4305-9f60-676b5e33b955.002.png deleted file mode 100644 index 8af989e9f..000000000 Binary files a/docs/UserTutorialDocuments/images/Aspose.Words.35a40138-4445-4305-9f60-676b5e33b955.002.png and /dev/null differ diff --git a/docs/UserTutorialDocuments/images/Aspose.Words.35a40138-4445-4305-9f60-676b5e33b955.003.png b/docs/UserTutorialDocuments/images/Aspose.Words.35a40138-4445-4305-9f60-676b5e33b955.003.png deleted file mode 100644 index 67f326308..000000000 Binary files a/docs/UserTutorialDocuments/images/Aspose.Words.35a40138-4445-4305-9f60-676b5e33b955.003.png and /dev/null differ diff --git a/docs/UserTutorialDocuments/images/Aspose.Words.35a40138-4445-4305-9f60-676b5e33b955.004.png b/docs/UserTutorialDocuments/images/Aspose.Words.35a40138-4445-4305-9f60-676b5e33b955.004.png deleted file mode 100644 index 7fb46f7bf..000000000 Binary files a/docs/UserTutorialDocuments/images/Aspose.Words.35a40138-4445-4305-9f60-676b5e33b955.004.png and /dev/null differ diff --git a/docs/UserTutorialDocuments/images/Aspose.Words.35a40138-4445-4305-9f60-676b5e33b955.005.png b/docs/UserTutorialDocuments/images/Aspose.Words.35a40138-4445-4305-9f60-676b5e33b955.005.png deleted file mode 100644 index 4e6100b10..000000000 Binary files a/docs/UserTutorialDocuments/images/Aspose.Words.35a40138-4445-4305-9f60-676b5e33b955.005.png and /dev/null differ diff --git a/docs/UserTutorialDocuments/images/Aspose.Words.5d9dcf9e-3949-4520-a57f-7c5b9cf60536.001.png b/docs/UserTutorialDocuments/images/Aspose.Words.5d9dcf9e-3949-4520-a57f-7c5b9cf60536.001.png deleted file mode 100644 index bac47b725..000000000 Binary files a/docs/UserTutorialDocuments/images/Aspose.Words.5d9dcf9e-3949-4520-a57f-7c5b9cf60536.001.png and /dev/null differ diff --git a/docs/UserTutorialDocuments/images/Aspose.Words.5d9dcf9e-3949-4520-a57f-7c5b9cf60536.002.png b/docs/UserTutorialDocuments/images/Aspose.Words.5d9dcf9e-3949-4520-a57f-7c5b9cf60536.002.png deleted file mode 100644 index eb99a2dea..000000000 Binary files a/docs/UserTutorialDocuments/images/Aspose.Words.5d9dcf9e-3949-4520-a57f-7c5b9cf60536.002.png and /dev/null differ diff --git a/docs/UserTutorialDocuments/images/Aspose.Words.5d9dcf9e-3949-4520-a57f-7c5b9cf60536.003.png b/docs/UserTutorialDocuments/images/Aspose.Words.5d9dcf9e-3949-4520-a57f-7c5b9cf60536.003.png deleted file mode 100644 index 2591ddfbd..000000000 Binary files a/docs/UserTutorialDocuments/images/Aspose.Words.5d9dcf9e-3949-4520-a57f-7c5b9cf60536.003.png and /dev/null differ diff --git a/docs/UserTutorialDocuments/images/Aspose.Words.5d9dcf9e-3949-4520-a57f-7c5b9cf60536.004.png b/docs/UserTutorialDocuments/images/Aspose.Words.5d9dcf9e-3949-4520-a57f-7c5b9cf60536.004.png deleted file mode 100644 index 3f3a603b4..000000000 Binary files a/docs/UserTutorialDocuments/images/Aspose.Words.5d9dcf9e-3949-4520-a57f-7c5b9cf60536.004.png and /dev/null differ diff --git a/docs/UserTutorialDocuments/images/Aspose.Words.5d9dcf9e-3949-4520-a57f-7c5b9cf60536.005.png b/docs/UserTutorialDocuments/images/Aspose.Words.5d9dcf9e-3949-4520-a57f-7c5b9cf60536.005.png deleted file mode 100644 index 416607d34..000000000 Binary files a/docs/UserTutorialDocuments/images/Aspose.Words.5d9dcf9e-3949-4520-a57f-7c5b9cf60536.005.png and /dev/null differ diff --git a/docs/UserTutorialDocuments/images/Aspose.Words.62bd5652-0f0b-4fde-b32f-e70d215b9924.001.png b/docs/UserTutorialDocuments/images/Aspose.Words.62bd5652-0f0b-4fde-b32f-e70d215b9924.001.png deleted file mode 100644 index e424104a5..000000000 Binary files a/docs/UserTutorialDocuments/images/Aspose.Words.62bd5652-0f0b-4fde-b32f-e70d215b9924.001.png and /dev/null differ diff --git a/docs/UserTutorialDocuments/images/Aspose.Words.62bd5652-0f0b-4fde-b32f-e70d215b9924.002.png b/docs/UserTutorialDocuments/images/Aspose.Words.62bd5652-0f0b-4fde-b32f-e70d215b9924.002.png deleted file mode 100644 index eb9154efc..000000000 Binary files a/docs/UserTutorialDocuments/images/Aspose.Words.62bd5652-0f0b-4fde-b32f-e70d215b9924.002.png and /dev/null differ diff --git a/docs/UserTutorialDocuments/images/Aspose.Words.62bd5652-0f0b-4fde-b32f-e70d215b9924.003.png b/docs/UserTutorialDocuments/images/Aspose.Words.62bd5652-0f0b-4fde-b32f-e70d215b9924.003.png deleted file mode 100644 index 7ce30d022..000000000 Binary files a/docs/UserTutorialDocuments/images/Aspose.Words.62bd5652-0f0b-4fde-b32f-e70d215b9924.003.png and /dev/null differ diff --git a/docs/UserTutorialDocuments/images/Aspose.Words.77495f85-6dd6-419d-b973-3d76d715f472.001.png b/docs/UserTutorialDocuments/images/Aspose.Words.77495f85-6dd6-419d-b973-3d76d715f472.001.png deleted file mode 100644 index 905735be3..000000000 Binary files a/docs/UserTutorialDocuments/images/Aspose.Words.77495f85-6dd6-419d-b973-3d76d715f472.001.png and /dev/null differ diff --git a/docs/UserTutorialDocuments/images/Aspose.Words.77495f85-6dd6-419d-b973-3d76d715f472.002.png b/docs/UserTutorialDocuments/images/Aspose.Words.77495f85-6dd6-419d-b973-3d76d715f472.002.png deleted file mode 100644 index 3044a7de1..000000000 Binary files a/docs/UserTutorialDocuments/images/Aspose.Words.77495f85-6dd6-419d-b973-3d76d715f472.002.png and /dev/null differ diff --git a/docs/UserTutorialDocuments/images/Aspose.Words.77495f85-6dd6-419d-b973-3d76d715f472.003.png b/docs/UserTutorialDocuments/images/Aspose.Words.77495f85-6dd6-419d-b973-3d76d715f472.003.png deleted file mode 100644 index ed16821fb..000000000 Binary files a/docs/UserTutorialDocuments/images/Aspose.Words.77495f85-6dd6-419d-b973-3d76d715f472.003.png and /dev/null differ diff --git a/docs/UserTutorialDocuments/images/Aspose.Words.db83c9c2-55e3-4bd4-a261-66f2533abc42.001.png b/docs/UserTutorialDocuments/images/Aspose.Words.db83c9c2-55e3-4bd4-a261-66f2533abc42.001.png deleted file mode 100644 index a937ec9e4..000000000 Binary files a/docs/UserTutorialDocuments/images/Aspose.Words.db83c9c2-55e3-4bd4-a261-66f2533abc42.001.png and /dev/null differ diff --git a/docs/UserTutorialDocuments/images/Aspose.Words.db83c9c2-55e3-4bd4-a261-66f2533abc42.002.png b/docs/UserTutorialDocuments/images/Aspose.Words.db83c9c2-55e3-4bd4-a261-66f2533abc42.002.png deleted file mode 100644 index e6b6462c6..000000000 Binary files a/docs/UserTutorialDocuments/images/Aspose.Words.db83c9c2-55e3-4bd4-a261-66f2533abc42.002.png and /dev/null differ diff --git a/docs/UserTutorialDocuments/images/Aspose.Words.db83c9c2-55e3-4bd4-a261-66f2533abc42.003.png b/docs/UserTutorialDocuments/images/Aspose.Words.db83c9c2-55e3-4bd4-a261-66f2533abc42.003.png deleted file mode 100644 index b26a7ce31..000000000 Binary files a/docs/UserTutorialDocuments/images/Aspose.Words.db83c9c2-55e3-4bd4-a261-66f2533abc42.003.png and /dev/null differ diff --git a/docs/UserTutorialDocuments/images/Aspose.Words.db83c9c2-55e3-4bd4-a261-66f2533abc42.004.png b/docs/UserTutorialDocuments/images/Aspose.Words.db83c9c2-55e3-4bd4-a261-66f2533abc42.004.png deleted file mode 100644 index d1868a4ba..000000000 Binary files a/docs/UserTutorialDocuments/images/Aspose.Words.db83c9c2-55e3-4bd4-a261-66f2533abc42.004.png and /dev/null differ diff --git a/docs/UserTutorialDocuments/images/image1.png b/docs/UserTutorialDocuments/images/image1.png deleted file mode 100644 index 4c8b598d4..000000000 Binary files a/docs/UserTutorialDocuments/images/image1.png and /dev/null differ diff --git a/docs/UserTutorialDocuments/images/image2.png b/docs/UserTutorialDocuments/images/image2.png deleted file mode 100644 index 1612fca1b..000000000 Binary files a/docs/UserTutorialDocuments/images/image2.png and /dev/null differ diff --git a/docs/UserTutorialDocuments/images/image3.png b/docs/UserTutorialDocuments/images/image3.png deleted file mode 100644 index ca14f5abd..000000000 Binary files a/docs/UserTutorialDocuments/images/image3.png and /dev/null differ diff --git a/docs/UserTutorialDocuments/images/image4.png b/docs/UserTutorialDocuments/images/image4.png deleted file mode 100644 index 1bcabdac4..000000000 Binary files a/docs/UserTutorialDocuments/images/image4.png and /dev/null differ diff --git a/docs/UserTutorialDocuments/images/image5.png b/docs/UserTutorialDocuments/images/image5.png deleted file mode 100644 index 9515572f8..000000000 Binary files a/docs/UserTutorialDocuments/images/image5.png and /dev/null differ diff --git a/docs/account.md b/docs/account.md deleted file mode 100644 index 1f87bb3a8..000000000 --- a/docs/account.md +++ /dev/null @@ -1,70 +0,0 @@ -# About MyDollarBot's /add Feature -This feature enables the app to have inflow of money. Currently, inflow of money -is supported for two accounts: - -- Checking -- Savings - -The user can: - -- Add balance to any account. -- Change account before registering an expense. -- Download expense record in csv, pdf format that includes account information. -- Send a csv expenses record via email to any specified address. -- If an expense exceeds balance amount in an account, the expense is denied and a error is shown. -- If an expense leads to low balance in the account (<100$), a warning is shown indicating low balance in the account. - -# Location of Code for this Feature -The code that implements this feature can be found [here](https://github.com/ymdatta/DollarBot/blob/main/code/account.py) - -# Code Description -## Functions - -1. run(message, bot): - - This function takes 2 arguments for processing. - - - **message** which is the message from the user on Telegram. - - **bot** which is the telegram bot object from the main code.py function. - - This is the starting function in the implementation of account feature. It pops up a menu on the telegram asking the user to chose from two different account types, after which control is given to post_category_selection(message, bot) for further processing. - -2. post_category_selection(message, bot): - - This function takes 2 arguments for processing. - - - **message** which is the message from the user on Telegram. - - **bot** which is the telegram bot object from the run(message, bot) function. - - This is the function which gets executed once an account type is selected. It changes current account used for expenses to the one input by the user. - - If an invalid account is selected, it erros out raising an exception indicating that the right category needs to be selected and it provides list of commands to start the next iteration. - -4. add_account_record(chat_id, type): - - This function takes 2 arguments for processing. - - - **chat_id** which is the unique ID for a user provided by Telegram. - - **type** which is the account type user selected for future purchases. - - This function is a helper function, which creates user record if it's a new user. It then updates the account type for expenses based on the inputs from the user. - -# How to run this feature? - -Once the project is running(please follow the instructions given in the main README.md for this), please type /select_expense_type into the telegram bot. - -Below you can see an example in text format: - -``` -Mohan Y, [11/27/23 3:42 PM] -/select_expenses_account - -Bot, [11/27/23 3:42 PM] -Select Category - -Mohan Y, [11/27/23 3:42 PM] -Checking - -Bot, [11/27/23 3:42 PM] -Expenses account changed to: Checking. -``` \ No newline at end of file diff --git a/docs/add.md b/docs/add.md deleted file mode 100644 index 17f6a2dfd..000000000 --- a/docs/add.md +++ /dev/null @@ -1,63 +0,0 @@ -# About MyDollarBot's /add Feature -This feature enables the user to add a new expense to their expense tracker. -Currently we have the following expense categories set by default: - -- Food -- Groceries -- Utilities -- Transport -- Shopping -- Miscellaneous - -The user can choose a category and add the amount they have spent to be stored in the expense tracker. - -# Location of Code for this Feature -The code that implements this feature can be found [here](https://github.com/sak007/MyDollarBot-BOTGo/blob/main/code/add.py) - -# Code Description -## Functions - -1. run(message, bot): -This is the main function used to implement the add feature. It pop ups a menu on the bot asking the user to choose their expense category, after which control is given to post_category_selection(message, bot) for further proccessing. 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. - -2. post_category_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 add.py file. It requests the user to select the currency in which they want to enter the amount in and then passes control to post_currency_selection(message, bot, selected_category): for further processing. - -3. post_currency_selection(message, bot, selected_category): - It takes 3 arguments for processing - **message** which is the message from the user, **bot** which is the telegram bot object from the run(message, bot) : function in the add.py file and the selected category by the user. It requests the user to enter the amount they have spent on the expense category chosen and then passes control to post_amount_input(message, bot): for further processing. - -4. post_amount_input(message, bot, selected_category, selected_currency): - It takes 4 arguments for processing - **message** which is the message from the user, **bot** which is the telegram bot object from the post_category_selection(message, bot): function in the add.py file, the selected category and selected currency by the user. It takes the amount entered by the user, validates it with helper.validate() and then calls add_user_record to store it. - -5. add_user_record(chat_id, record_to_be_added): - Takes 2 arguments - **chat_id** or the chat_id of the user's chat, and **record_to_be_added** which is the expense record to be added to the store. It then stores this expense record in the store. - -# How to run this feature? -Once the project is running(please follow the instructions given in the main README.md for this), please type /add into the telegram bot. - -Below you can see an example in text format: - -Krodhit Balak, [22-11-2023 20:11] -/add - -SEproj3test_bot, [22-11-2023 20:11] -Select Category - -Krodhit Balak, [22-11-2023 20:11] -Groceries - -SEproj3test_bot, [22-11-2023 20:11] -Select Currency - -Krodhit Balak, [22-11-2023 20:11] -INR - -SEproj3test_bot, [22-11-2023 20:11] -How much did you spend on Groceries? -(Enter numeric values only) - -Krodhit Balak, [22-11-2023 20:12] -200 - -SEproj3test_bot, [22-11-2023 20:12] -The following expenditure has been recorded: You have spent $2.4 for Groceries on 22-Nov-2023 20:12 from Checking account diff --git a/docs/add_recurring.md b/docs/add_recurring.md deleted file mode 100644 index 99cf4488f..000000000 --- a/docs/add_recurring.md +++ /dev/null @@ -1,73 +0,0 @@ -# About MyDollarBot's /add_recurring Feature -This feature enables the user to add a new expense to their expense tracker. -Currently we have the following expense categories set by default: - -- Food -- Groceries -- Utilities -- Transport -- Shopping -- Miscellaneous - -The user can choose a an existing category or add custom category and add the amount they have spent to be stored in the expense tracker. - -# Location of Code for this Feature -The code that implements this feature can be found [here](https://github.com/prithvish-doshi-17/MyDollarBot-BOTGo/blob/main/code/add_recurring.py) - -# Code Description -## Functions - -1. run(message, bot): -This is the main function used to implement the add feature. It pop ups a menu on the bot asking the user to choose their expense category, after which control is given to post_category_selection(message, bot) for further proccessing. 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. - -2. post_category_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 add_recurring.py file. It requests the user to select the currency in which they want to enter the amount in and then passes control to post_currency_selection(message, bot, selected_category): for further processing. - -3. post_currency_selection(message, bot, selected_category): - It takes 3 arguments for processing - **message** which is the message from the user, **bot** which is the telegram bot object from the run(message, bot) : function in the add.py file and the selected category by the user. It requests the user to enter the amount they have spent on the expense category chosen and then passes control to post_amount_input(message, bot): for further processing. - -4. post_amount_input(message, bot, selected_category, selected_currency): - It takes 4 arguments for processing - **message** which is the message from the user, **bot** which is the telegram bot object from the post_category_selection(message, bot): function in the add.py file, the selected category and selected currency by the user. It takes the amount entered by the user, validates it with helper.validate() and then calls add_user_record to store it. - -5. post_duration(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 post_amount_input(message, bot): function in the add_recurring.py file. It takes the duration entered by the user, validates it with helper.validate() and then calls add_user_record to store it. - -6. add_user_record(chat_id, record_to_be_added): - Takes 2 arguments - **chat_id** or the chat_id of the user's chat, and **record_to_be_added** which is the expense record to be added to the store. It then stores this expense record in the store. - -# How to run this feature? -Once the project is running(please follow the instructions given in the main README.md for this), please type /add_recurring into the telegram bot. - -Below you can see an example in text format: - -Krodhit Balak, [22-11-2023 18:09] -/add_recurring - -SEproj3test_bot, [22-11-2023 18:09] -Select Category - -Krodhit Balak, [22-11-2023 18:09] -Utilities - -SEproj3test_bot, [22-11-2023 18:09] -Select Currency - -Krodhit Balak, [22-11-2023 18:09] -INR - -SEproj3test_bot, [22-11-2023 18:09] -How much did you spend on Utilities? -(Enter numeric values only) - -Krodhit Balak, [22-11-2023 18:09] -8.4 - -SEproj3test_bot, [22-11-2023 18:09] -For how many months in the future will the expense be there? -(Enter integer values only) - -Krodhit Balak, [22-11-2023 18:09] -2 - -SEproj3test_bot, [22-11-2023 18:09] -The following expenditure has been recorded: You have spent $0.1 for Utilities for the next 2 months diff --git a/docs/budget.md b/docs/budget.md deleted file mode 100644 index 0aa356a45..000000000 --- a/docs/budget.md +++ /dev/null @@ -1,20 +0,0 @@ -# About MyDollarBot's /budget Feature -This feature enables the user to add, remove, edit or display a budget in their expense tracker. The user can choose between an overall expense tracker, which counts every expenses made to the budget, or a category-wise expense tracker, which only counts expenses in a particular category towards the budget for that category. - -The user can choose a category and add the amount for the budget to be stored in the expense tracker. - -# Location of Code for this Feature -The code that implements this feature can be found [here](https://github.com/sak007/MyDollarBot-BOTGo/blob/main/code/budget.py) - -# Code Description -## Functions - -1. run(message, bot): -This is the main function used to implement the budget feature. It pop ups a menu on the bot asking the user to choose to add, remove or display a budget, after which control is given to post_operation_selection(message, bot) for further proccessing. 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. - -2. 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 budget.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. - - -# How to run this feature? -Once the project is running(please follow the instructions given in the main README.md for this), please type /budget into the telegram bot. diff --git a/docs/budget_delete.md b/docs/budget_delete.md deleted file mode 100644 index f8843532f..000000000 --- a/docs/budget_delete.md +++ /dev/null @@ -1,15 +0,0 @@ -# About MyDollarBot's budget_delete module -The budget_view module contains all the functions required to implement the display delete/removal feature. In essence, all operations involved in removal and deletion of a budget are taken care of in this module and are implemented here. - -# Location of Code for this Feature -The code that implements this feature can be found [here](https://github.com/sak007/MyDollarBot-BOTGo/blob/main/code/budget_delete.py) - -# Code Description -## Functions - -1. run(message, bot): -This is the main function used to implement the budget 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 user's chat ID from the message object, and reads all user data through the read_json method from the helper module. It then proceeds to empty the budget data for the particular user based on the user ID provided from the UI. It returns a simple message indicating that this operation has been done to the UI. - - -# How to run this feature? -Once the project is running(please follow the instructions given in the main README.md for this), please type /budget into the telegram bot. Add a budget and then type /budget again. Please choose the option for deleting a budget. diff --git a/docs/budget_update.md b/docs/budget_update.md deleted file mode 100644 index f41316847..000000000 --- a/docs/budget_update.md +++ /dev/null @@ -1,48 +0,0 @@ -# About MyDollarBot's budget_view module -The budget_view module contains all the functions required to implement the display add and update features. In essence, all operations involved in addition of a new budget and updating an existing budget are taken care of in this module and are implemented here. - -# Location of Code for this Feature -The code that implements this feature can be found [here](https://github.com/sak007/MyDollarBot-BOTGo/blob/main/code/budget_update.py) - -# Code Description -## Functions - -1. run(message, bot): -This is the main function used to implement the budget add/update features. 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, the function provides prompts to nudge the user to create a new budget depending on their preferences, through the post_type_selection function in the same module. - -2. post_type_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. This function takes input from the user, making them choose which type of budget they would like to create - category-wise or overall, and then calls the corresponding functions for further processing. - -3. update_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. This function is called when the user wants to either create a new overall budget or update an existing one. -It checks if there is an existing budget through the helper module's isOverallBudgetAvailable function and if so, displays this along with the prompt for the new (to be updated) budget, or just asks for the new budget. It passes control to the post_overall_amount_input function in the same file. - -4. post_overall_amount_input(message, bot): -It takes 2 arguments for processing - **message** which is the message from the user, and **bot** which is the telegram bot . This function takes over from the update_overall_budget, and asks the user to enter the new/updated budget amount. -As long as this amount is not zero(in which case it throws an exception), it continues processing. It reads the current user data through helper module's read_json function and adds the new budget information onto it, writing back with the helper module's write_json function. - -5. update_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. This function is called in case the user decides to choose category-wise budgest in the run or post_type_selection stages. -It gets the spend categories from the helper module's getSpendCategories and displays them to the user. It then passes control on to the post_category_selection function. - -6. post_category_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. Based on the category chosen by the user, the bot checks if these are part of the pre-defined categories in helper.getSpendCategories(), else it throws an exception. -If there is a budget already existing for the category, it identifies this case through helper.isCategoryBudgetByCategoryAvailable and shares this information with the user. If not, it simply proceeds. In either case, it then asks for the new/updated budget amount. It passes control onto post_category_amount_input. - -7. post_category_amount_input(message, bot, category): -It takes 2 arguments for processing - **message** which is the message from the user, and **bot** which is the telegram bot object, and the category chosen by the user. - -It gets the amount entered by the user and validates it. As long as this amount is not zero(in which case it throws an exception), it continues processing. It reads the current user data through helper module's read_json function and adds the new budget information onto it, writing back with the helper module's write_json function. It passes control to post_category_add. - -8. post_category_add(message, bot): -It takes 2 arguments for processing - **message** which is the message from the user, and **bot** which is the telegram bot object. This exists in case the user wants to add a category-wise budget to another category after adding it for one category. It prompts the user to choose an option from helper.getUpdateOptions().values() and passes control to post_option_selection to either continue or exit the add/update feature. - -9. post_option_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. -It takes the category chosen by the user from the message object. If the message is "continue", then it runs update_category_budget (above) allowing the user to get into the add/update process again. -Otherwise, it exits the feature. - -# How to run this feature? -Once the project is running(please follow the instructions given in the main README.md for this), please type /budget into the telegram bot. Please follow the prompts on the screen to create a new budget. \ No newline at end of file diff --git a/docs/budget_view.md b/docs/budget_view.md deleted file mode 100644 index 72e6994d6..000000000 --- a/docs/budget_view.md +++ /dev/null @@ -1,20 +0,0 @@ -# About MyDollarBot's budget_view module -The budget_view module contains all the functions required to implement the display budget feature. In essence, all operations involved in displaying a budget are taken care of in this module and are implemented here. - -# Location of Code for this Feature -The code that implements this feature can be found [here](https://github.com/sak007/MyDollarBot-BOTGo/blob/main/code/budget_view.py) - -# Code Description -## Functions - -1. 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. - -2. 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. - -3. 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. - -# How to run this feature? -Once the project is running(please follow the instructions given in the main README.md for this), please type /budget into the telegram bot. Create a budget, and then type /budget again, and choose the option to view your budget. \ No newline at end of file diff --git a/docs/category.md b/docs/category.md deleted file mode 100644 index 9c07a555e..000000000 --- a/docs/category.md +++ /dev/null @@ -1,95 +0,0 @@ -# About MyDollarBot's /category Feature -This feature enables the user to manage their categories. -Currently we have the following expense categories set by default: - -- Food -- Groceries -- Utilities -- Transport -- Shopping -- Miscellaneous - -The user can choose to add/delete/show categories, and after that, add the cost with custom category to the expense tracker. - -# Location of Code for this Feature -The code that implements this feature can be found [here](https://github.com/prithvish-doshi-17/MyDollarBot-BOTGo/blob/addCategories/code/category.py) - -# Code Description -## Functions - -1. run(message, bot): -This is the main function used to implement the category feature. It pop ups a menu on the bot asking the user to choose their operation, after which operation is given to post_operation_selection(message, bot) for further proccessing. 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. - -2. 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 category.py file. It requests the user to choose an operation from Add/Delete/Show categories and then passes control to category_add(message, bot), category_delete(message, bot), category_view(message, bot) for further processing depends on user's choose. - -3. category_add(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 post_operation_selection(message, bot): function in the category.py file. It takes the new category name entered by the user, and then write it in the file categories.txt. - -4. category_delete(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 post_operation_selection(message, bot): function in the category.py file. It takes the category name entered by the user, read the file categories.txt and check whether the inputed category is in the file. Write the categories back to the file categories.txt. - -5. category_view(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 post_operation_selection(message, bot): function in the category.py file. It read the file categories.txt and output the text in chat room. - -# How to run this feature? -Once the project is running(please follow the instructions given in the main README.md for this), please type /category into the telegram bot. - -Below you can see an example in text format: - -#### Add -Alex Chao, [2021/11/23 10:12] -/category - -DollarBot, [2021/11/23 10:12] -[In reply to Alex Chao] -Select Operation - -Alex Chao, [2021/11/23 10:12] -Add - -DollarBot, [2021/11/23 10:12] -[In reply to Alex Chao] -Please type the new category name - -Alex Chao, [2021/11/23 10:12] -Restaurant - -DollarBot, [2021/11/23 10:12] -Add category "Restaurant" successfully! - -#### Delete -Alex Chao, [2021/11/23 10:15] -/category - -DollarBot, [2021/11/23 10:15] -[In reply to Alex Chao] -Select Operation - -Alex Chao, [2021/11/23 10:15] -Delete - -DollarBot, [2021/11/23 10:15] -[In reply to Alex Chao] -Please choose the category you want to delete - -Alex Chao, [2021/11/23 10:15] -Restaurant - -DollarBot, [2021/11/23 10:15] -Delete category "Restaurant" successfully! - -#### Show categories -Alex Chao, [2021/11/23 10:17] -/category - -DollarBot, [2021/11/23 10:17] -[In reply to Alex Chao] -Select Operation - -Alex Chao, [2021/11/23 10:17] -Show Categories - -DollarBot, [2021/11/23 10:17] -The categories are: -Food,Groceries,Utilities,Transport,Shopping,Miscellaneous diff --git a/docs/code.md b/docs/code.md deleted file mode 100644 index a26abe068..000000000 --- a/docs/code.md +++ /dev/null @@ -1,80 +0,0 @@ -# About MyDollarBot's code.py file -code.py is the main file from where calls to the corresponding .py files for all features are sent. It contains a number of endpoints which redirect to function calls in the corresponding files. - -# Location of Code for this Feature -The code that implements this feature can be found [here](https://github.com/rrajpuro/DollarBot/blob/feature/setReminder/code/code.py) - -# Code Description -## Functions - -1. main() -The entire bot's execution begins here. It ensure the **bot** variable begins polling and actively listening for requests from telegram. - -2. listener(user_requests): -Takes 1 argument **user_requests** and logs all user interaction with the bot including all bot commands run and any other issue logs. - -3. 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'] - -4. command_add(message) -Takes 1 argument **message** which contains the message from the user along with the chat ID of the user chat. It then calls add.py to run to execute the add functionality. Commands used to run this: commands=['add'] - -5. command_add_recurring(message) -Takes 1 argument **message** which contains the message from the user along with the chat ID of the user chat. It then calls add_recurring.py to run to execute the functionality. Commands used to run this: commands=['add_recurring'] - -6. command_history(message): -Takes 1 argument **message** which contains the message from the user along with the chat ID of the user chat. It then calls history.py to run to execute the add functionality. Commands used to run this: commands=['history'] - -7. command_edit(message): -Takes 1 argument **message** which contains the message from the user along with the chat ID of the user chat. It then calls edit.py to run to execute the add functionality. Commands used to run this: commands=['edit'] - -8. command_display(message): -Takes 1 argument **message** which contains the message from the user along with the chat ID of the user chat. It then calls display.py to run to execute the add functionality. Commands used to run this: commands=['display'] - -9. command_delete(message): -Takes 1 argument **message** which contains the message from the user along with the chat ID of the user chat. It then calls delete.py to run to execute the add functionality. Commands used to run this: commands=['delete_all'] - -10. command_delete_expense(message): -Takes 1 argument **message** which contains the message from the user along with the chat ID of the user chat. It then calls delete_expense.py to run to execute the add functionality. Commands used to run this: commands=['delete'] - -11. command_reminder(message): -Takes 1 argument **message** which contains the message from the user along with the chat ID of the user chat. It then calls reminder.py to run to execute the add functionality. Commands used to run this: commands=['setReminder'] - -12. reminder_checker(): -It operates in an infinite loop, periodically invoking the check_reminders function to ensure users receive their notifications. - -# How to run this feature? -This file contains information on the main code.py file from where all features are run. Instructions to run this are the same as instructions to run the project and can be found in README.md. - - -# UI Improvement -## MyDollarBot Command Handling - -The MyDollarBot Telegram bot features command handling for user interactions. This documentation explains the key components of command handling in MyDollarBot. - -## Start and Menu Commands -The /start and /menu commands are handled by the start_and_menu_command function. When a user sends either of these commands, the bot provides a menu with available options. - -- /start or /menu command prompts the bot to send a menu to the user. -- The menu displays a list of available commands. - -## Menu Option Handlers -Specific commands in the menu are handled by individual command handlers. When a user selects a command from the menu, the bot routes the user to the corresponding functionality. - -Available menu commands include: - -- /add: Allows users to add a new expense. -- /display: Displays expenses. -- /estimate: Provides an estimate of future expenses. -- /add_recurring: Lets users add recurring expenses. -- /delete_all: Deletes all expenses. -- /delete: Initiates the process to delete a specific expense. -- /budget: Allows users to set, remove, or display budgets. -- /edit: Enables users to edit existing expenses. -- /history: Displays expense history. -- /set_reminder: Configures spending reminders. - -### Example: -- User selects /add. -- The bot routes the user to the "add" functionality. -- The add.run function is executed to handle the expense addition. \ No newline at end of file diff --git a/docs/delete.md b/docs/delete.md deleted file mode 100644 index 0e30d807a..000000000 --- a/docs/delete.md +++ /dev/null @@ -1,25 +0,0 @@ -# About MyDollarBot's /delete_all Feature -This feature enables the user to delete all of their saved records till date in their expense tracker. - -# Location of Code for this Feature -The code that implements this feature can be found [here](https://github.com/sak007/MyDollarBot-BOTGo/blob/main/code/delete.py) - -# Code Description -## Functions - -1. 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 calls helper to get the user history i.e chat ids of all user in the application, and if the user requesting a delete has their data saved in myDollarBot i.e their chat ID has been logged before, run calls the deleteHistory(chat_id): to remove it. Then it ensures this removal is saved in the datastore. - -2. deleteHistory(chat_id): -It takes 1 argument for processing - **chat_id** which is the chat_id of the user whose data is to deleted from the user list. It removes this entry from the user list. - -# How to run this feature? -Once the project is running(please follow the instructions given in the main README.md for this), please type /add into the telegram bot. - -Below you can see an example in text format: - -Sri Athithya Kruth, [19.10.21 21:50] -/delete_all - -dollarbot,[] -History has been deleted! \ No newline at end of file diff --git a/docs/delete_expense.md b/docs/delete_expense.md deleted file mode 100644 index 091f3db6a..000000000 --- a/docs/delete_expense.md +++ /dev/null @@ -1,20 +0,0 @@ -# About MyDollarBot's /delete Feature -The Expense Deletion feature in MyDollarBot allows users to delete specific expenses from their expense records. This feature involves multiple functions to handle the process. - -# Location of Code for this Feature -The code that implements this feature can be found [here](https://github.com/rrajpuro/DollarBot/blob/main/code/delete_expense.py) - -## delete_expense(chat_id, expense_index) -This function handles the deletion of a user's expense. It takes two arguments, chat_id (the user's chat ID) and expense_index (the index of the expense to be deleted). The process includes reading the user's data from a JSON file, removing the specified expense, and saving the updated data back to the database. If the deletion is successful, it returns a confirmation message; otherwise, it returns a failure message. - -## run(message, bot) -The run function initiates the process of deleting an expense. It takes two parameters, message (the user's message) and bot (the Telegram bot object). This function checks if the user has expenses to delete. If the user exists in the data and has expenses, it proceeds to list the expenses for the user to choose from. - -## confirm_deletion(message, chat_id, bot) -The confirm_deletion function confirms the user's choice of an expense for deletion. It takes three arguments: message (the user's message), chat_id (the user's chat ID), and bot (the Telegram bot object). If the user provides a valid expense number, the function calls delete_expense to perform the deletion and sends a confirmation message. If the user's input is not valid, it prompts them to enter a valid expense number. - -To use this feature: -1. Start the MyDollarBot project. -2. Open a chat with the bot in Telegram. -3. Type /delete to initiate the expense deletion process. -4. Follow the prompts to select the expense to delete. diff --git a/docs/display.md b/docs/display.md deleted file mode 100644 index bc7b8bf05..000000000 --- a/docs/display.md +++ /dev/null @@ -1,45 +0,0 @@ -# About MyDollarBot's /display Feature -This feature enables the user to view their expenses for the past month or past day. The option to choose month or day pops up on the screen and they can choose their preference to be displayed afterwards. - -# Location of Code for this Feature -The code that implements this feature can be found [here](https://github.com/prithvish-doshi-17/MyDollarBot-BOTGo/blob/main/code/display.py) - -# Code Description -## Functions - -1. 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 options for the display period from the helper.py file's getSpendDisplayOptions() method and then makes the Telegram bot display them for the user to choose along with a message indicating this. It then passes control to the display_total() function for further processing. - -2. display_total(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 same file. This function loads the user's data using the helper file's getUserHistory(chat_id) method. After this, depending on the option user has chosen on the UI, it calls the calculate_spendings(queryResult): to process the queried data to return to the user after which it finally passes the data to the UI for the user to view. - -3. calculate_spendings(queryResult): -Takes 1 argument for processing - **queryResult** which is the query result from the display total function in the same file. It parses the query result and turns it into a form suitable for display on the UI by the user. - -4. display_budget_by_text(history, budget_data) -> str: -It takes 2 arguments for processing - **history** which is the history expense of a user, and **budget_data** is the budget setting of a user. It collects the budget setting and calculates the remaining budget so far. - -# How to run this feature? -Sri Athithya Kruth, [20.10.21 20:33] -/display - -mydollarbot20102021, [20.10.21 20:33] -[In reply to Sri Athithya Kruth] -Please select a category to see the total expense - -Sri Athithya Kruth, [20.10.21 20:33] -Day - -mydollarbot20102021, [20.10.21 20:33] -Hold on! Calculating... - -mydollarbot20102021, [20.10.21 20:33] - -Here are your total spendings day: - -CATEGORIES,AMOUNT ----------------------- -Transport $1022.0 -Groceries $12.0 diff --git a/docs/edit.md b/docs/edit.md deleted file mode 100644 index ecdf8f81d..000000000 --- a/docs/edit.md +++ /dev/null @@ -1,33 +0,0 @@ -# About MyDollarBot's /edit Feature -This feature enables the user to edit a previously entered expense in the app. The use can change the amount set in the bot with this command. - -Please note that this is still a Work In Progress. - -# Location of Code for this Feature -The code that implements this feature can be found [here](https://github.com/sak007/MyDollarBot-BOTGo/blob/main/code/edit.py) - -# Code Description -## Functions - -1. 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. - -2. edit2(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 same file. It validates the date provided by the user to see if it's been correctly entered. If not, it throws an error stating the date is incorrect. If the date is correct, it then asks the user what they would like to edit for the expense in question, passing on to def edit3(m, bot): for further processing. - -3. def edit3(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 edit2(m, bot):: function in the same file. Based on the category chosen for editing by the user, it redirects to the corresponding function for further processing. - -4. 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. - -5. 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. - -6. 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. - -# How to run this feature? -Sri Athithya Kruth, [20.10.21 20:33] -/edit -(WORK IN PROGRESS) diff --git a/docs/estimate.md b/docs/estimate.md deleted file mode 100644 index 91640f902..000000000 --- a/docs/estimate.md +++ /dev/null @@ -1,44 +0,0 @@ -# About MyDollarBot's /estimate Feature -This feature enables the user to estimate their expenses for the next month or next day. The option to choose next month or next day pops up on the screen and they can choose their preference to be displayed. - -# Location of Code for this Feature -The code that implements this feature can be found [here](https://github.com/sak007/MyDollarBot-BOTGo/blob/estimate-feature/code/estimate.py) - -# Code Description -## Functions - -1. run(message, bot): -This is the main function used to implement the estimate 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 options for the estimate period from the helper.py file's getSpendEstimateOptions() method and then makes the Telegram bot display them for the user to choose along with a message indicating this. It then passes control to the estimate_total() function for further processing. - -2. estimate_total(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 same file. This function loads the user's data using the helper file's getUserHistory(chat_id) method. After this, depending on the option user has chosen on the UI, it calls the calculate_estimate(queryResult, days_to_estimate): to process the queried data to return to the user after which it finally passes the data to the UI for the user to view. - -3. calculate_estimate(queryResult, days_to_estimate): -Takes 2 arguments for processing - **queryResult** which is the query result from the estimate total function in the same file. It parses the query result and turns it into a form suitable for display on the UI by the user. **days_to_estimate** is a variable that tells the function to calculate the estimate for a specified period like a day or month. - -# How to run this feature? - -``` -$: python code/code.py - -$: /start - -$: /estimate - -Please select the period to estimate: -$: Next day - -Hold on! Calculating... - -Here are your estimated spendings for the next day: -CATEGORIES,AMOUNT ----------------------- -Food $200.0 -Groceries $100.0 -``` - -![alt text](https://github.com/sak007/MyDollarBot-BOTGo/blob/estimate-feature/docs/estimate.png) - - diff --git a/docs/estimate.png b/docs/estimate.png deleted file mode 100644 index 00e95af51..000000000 Binary files a/docs/estimate.png and /dev/null differ diff --git a/docs/expenses.jpeg b/docs/expenses.jpeg deleted file mode 100644 index 92fa215ab..000000000 Binary files a/docs/expenses.jpeg and /dev/null differ diff --git a/docs/graphing.md b/docs/graphing.md deleted file mode 100644 index 6a12c6e14..000000000 --- a/docs/graphing.md +++ /dev/null @@ -1,22 +0,0 @@ -# About MyDollarBot's /display Feature's Graph module -This feature enables the user to see their expense in a graphical format to enable better UX. - -Currently, the /display command will provide the expenses as a message to the users via the bot. To better the UX, we have added the option to show the expenses in a Bar Graph and pie chart along with budget line. - -# Location of Code for this Feature -The code that implements this feature can be found [here](https://github.com/prithvish-doshi-17/MyDollarBot-BOTGo/blob/main/code/graphing.py) - -# Code Description -## Functions - -1. visualize(total_text, budgetData): -This is the main function used to implement the graphing part of display feature. This file is called from display.py. It takes two arguements- which **total_text** is the user history expense fo a user and **budgetData** is the user's budget settings. It creates a graph into the directory which is the return value of display.py. -2. addlabels(x, y): -This function is used to add the labels to the graph. It takes the expense values and adds the values inside the bar graph for each expense type. -3.vis(total_text): -This function takes total text as input and creates a pie chart based on the user's expense history and return a plotted pie chart. -4. viz(total_text): -This function takes total text as input and creates a bar chart without budget line based on the user's expense history and return a plotted bar chart. - -# How to run this feature? -After you've added sufficient input data, use the /display command and you can see the output in a pictorial representation. diff --git a/docs/helper.md b/docs/helper.md deleted file mode 100644 index 2c6aafd74..000000000 --- a/docs/helper.md +++ /dev/null @@ -1,49 +0,0 @@ -# About MyDollarBot's /helper class -The helper file contains a set of functions that are commonly used for repeated tasks in the various features of MyDollarBot. Since these come up often, we have put them all up here in a separate file for reusability. - -# Location of Code for this Feature -The code that implements this feature can be found [here](https://github.com/rrajpuro/DollarBot/blob/feature/setReminder/code/helper.py) - -# Code Description -## Functions - -1. read_json(): -Function to load .json expense record data - -2. write_json(user_list): -Stores data into the datastore of the bot. - -3. validate_entered_amount(amount_entered): -Takes 1 argument, **amount_entered**. It validates this amount's format to see if it has been correctly entered by the user. - -4. getUserHistory(chat_id): -Takes 1 argument **chat_id** and uses this to get the relevant user's historical data. - -5. getSpendCategories(): -This functions returns the spend categories used in the bot. These are defined the same file. - -6. getSpendDisplayOptions(): -This functions returns the spend display options used in the bot. These are defined the same file. - -7. getCommands(): -This functions returns the command options used in the bot. These are defined the same file. - -8. def getDateFormat(): -This functions returns the date format used in the bot. - -9. def getTimeFormat(): -This functions returns the time format used in the bot. - -10. def getMonthFormat(): -This functions returns the month format used in the bot. - -11. def getplot(): -This functions returns the different plots used in the bot. These are defined the same file. - -12. def validate_time_format(time_str:) -This function, validate_time_format(time_str), checks whether the input time_str matches the 24-hour time format (HH:MM), returning True if it's a valid time format and False if not. - -# How to run this feature? -Once the project is running(please follow the instructions given in the main README.md for this), please type /add into the telegram bot. - -This file is not a feature and cannot be run per se. Its functions are used all over by the other files as it provides helper functions for various functionalities and features. diff --git a/docs/history.md b/docs/history.md deleted file mode 100644 index 42e3867cc..000000000 --- a/docs/history.md +++ /dev/null @@ -1,39 +0,0 @@ -# About MyDollarBot's /history Feature -This feature enables the user to view all of their stored records i.e it gives a historical view of all the expenses stored in MyDollarBot. - -# Location of Code for this Feature -The code that implements this feature can be found [here](https://github.com/prithvish-doshi-17/MyDollarBot-BOTGo/blob/main/code/history.py) - -# Code Description -## Functions - -1. 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 calls helper.py to get the user's historical data and based on whether there is data available, it either prints an error message or displays the user's historical data. - -# How to run this feature? -Once the project is running(please follow the instructions given in the main README.md for this), please type /add into the telegram bot. - -Below you can see an example in text format: - -Vraj Chokshi, [23.11.21 20:33] -/display - -Vraj Chokshi, [23.11.21 20:33] -Month - -mydollarbot20102021, [23.11.21 20:33] -Hold on! Calculating... - -Vraj Chokshi, [23.11.21 20:53] -/history - -mydollarbot20102021, [23.11.21 20:53] -Here is your spending history : -DATE, CATEGORY, AMOUNT ----------------------- -01-Dec-2021,Transport,100.0 -23-Nov-2021 15:13,Groceries,500.0 - -Along with the spending history, the histogram displaying spending of each month is plotted, which can help user to manage his/her expenses. - -![Test Image ](https://github.com/prithvish-doshi-17/MyDollarBot-BOTGo/blob/main/histo.png) diff --git a/docs/images/tests.png b/docs/images/tests.png deleted file mode 100644 index a82055f17..000000000 Binary files a/docs/images/tests.png and /dev/null differ diff --git a/docs/issue report.md b/docs/issue report.md deleted file mode 100644 index e9bb38a4b..000000000 --- a/docs/issue report.md +++ /dev/null @@ -1,70 +0,0 @@ -# Issue Report - -Following were the issues for which the solutions were discussed before they were closed. - -## 1) add budget horizontal lines in display - graphing -In the display function, there is an option to show a bar graph of expenditure. There, you would need to display a line indicating the budget. - -If it's an overall budget, it should be a horizontal line through all categories. - -If it's a category-wise, then it should be separate horizontal lines for each category. - -Pass the budget to the plotting function. And each budget is plotted as a horizontal line and has the same color as the category bar of expense. - - -## 2) display budget in the display function - text -Along with the normal display function in text format, add at the top a line containing the current budget set by the user, whether it is an overall or category-wise budget. - -Also, display the amount remaining for the current month in the budget under that. -Calculate the remaining amount of each budget when /display command is called. I return the text format first and then return the picture format. -## 3) Add new recurring expense -Added a new file add_recurring.py which handles the cases of monthly recurring expenses. - -## 4) View recurring expenses -/history command also shows the recurring expenses (in text output and graph output) in this version. - -## 5) Add a command for recurring expense -Command /add_recurring is added. It asks for Category, Amount and Duration (number of months) from the use and adds that particular recurring expense to the JSON file. - -## 6) Add a new custom category -The name of this should be taken from the user and the new category should be added to the user's data, and be updated in the master list of categories read from the helper module. -With the command "/category" the bot is able to manage categories now. -By entering "Add", we can add a custom category. - -## 7) Add a new custom category - -The name of this should be taken from the user and the new category should be added to the user's data, and be updated in the master list of categories read from the helper module. -With the command "/category" the bot is able to manage categories now. -By entering "Add", we can add a custom category. - -## 8) Delete custom category - -You should display all custom categories on the Telegram UI to the user, and they must pick one which should subsequently be removed. - -We leave the option of removing all data associated with the category, or keeping it as a historical record to you. - -With the command "/category" the bot is able to manage categories now. -By entering "Delete", we can add a custom category. - -## 9) Adding pie chart in the display function - -Pie chart along with bar graph was added in the display command. - -##10) Adding Histograms to compare previous month's spend. - -/history command will now show the previous transaction history along with month wise transaction. The solution was being discuss before closing the issue. - -## 11) Overlapping plots. -The problem of overlapping plot was solve by adding plt.clf() code in every plotter function. - -## 12) Older name of the application was displayed - -The name was changed to :My Dollar Bot, however application name was older one. -Updated the codes for user experience. - -## 13) Add testcase of category.py - -Finished the file test_category.py. -Check the code coverage by the following command: -coverage run -m pytest test/ -coverage report diff --git a/docs/proj1rubric.md b/docs/proj1rubric.md deleted file mode 100644 index a96b1a391..000000000 --- a/docs/proj1rubric.md +++ /dev/null @@ -1,223 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NotesSelf Assessment zero (none), one (a litte), two (somewhat), three (a lot)evidence
workload is spread over the whole team (one team member is often Xtimes more productive than the others...ThreeEvery team member has implemented a functionality in the bot, which can be seen via their respective GitHub commits
but nevertheless, here is a track record that everyone is contributing a lot)
Number of commitsThreeGitHub commits on 'main': https://github.com/deekay2310/MyDollarBot/commits/main
Number of commits: by different peopleThreeCommits by each team member: https://github.com/deekay2310/MyDollarBot/tree/dev, https://github.com/deekay2310/MyDollarBot/tree/prakruthi, https://github.com/deekay2310/MyDollarBot/tree/radhika, https://github.com/deekay2310/MyDollarBot/tree/rohan, https://github.com/deekay2310/MyDollarBot/tree/Sunidhi
Issues reports: there are many issues are being closedThreehttps://github.com/deekay2310/MyDollarBot/issues
Issues are being closedThreehttps://github.com/deekay2310/MyDollarBot/issues?q=is%3Aissue+is%3Aclosed
DOI badge: existsThreehttps://zenodo.org/record/5542548#.YVZIrtNue3I
Docs: doco generated , format not uglyThreehttps://github.com/deekay2310/MyDollarBot/tree/main/docs
Docs: what: point descriptions of each class/function (in isolation)Threehttps://github.com/deekay2310/MyDollarBot/blob/main/docs/functionDescription.md
Docs: how: for common use cases X,Y,Z mini-tutorials showing worked examples on how to do X,Y,ZThreehttps://github.com/deekay2310/MyDollarBot/blob/main/README.md
Docs: why: docs tell a story, motivate the whole thing, deliver a punchline that makes you want to rush out and use the thingThreehttps://github.com/deekay2310/MyDollarBot/blob/main/README.md
Docs: short video, animated, hosted on your repo. That convinces people why they want to work on your code.Threehttps://github.com/deekay2310/MyDollarBot/blob/main/README.md
Use of version control tools
Use of style checkers
Use of code formatters
Use of syntax checkers.
Use of code coverage
other automated analysis tools
test cases exist
test cases are routinely executed
the files CONTRIBUTING.md lists coding standards and lots of tips on how to extend the system without screwing things upThreehttps://github.com/deekay2310/MyDollarBot/blob/main/CONTRIBUTING.md
issues are discussed before they are closedThreeScheduled regular zoom meetings and met in person to discuss about various issues, implementations and tasks. Reviewed each other's changes before wrapping up issues. Had continuous discussions over team WhatsApp group as well.
Chat channel: existsThreeTeam specific WhatsApp group
test cases:.a large proportion of the issues related to handling failing cases
evidence that the whole team is using the same tools: everyone can get to all tools and filesThree
evidence that the whole team is using the same tools (e.g. config files in the repo, updated by lots of different people)Three
evidence that the whole team is using the same tools (e.g. tutor can ask anyone to share screen, they demonstrate the system running on their computer)Three
evidence that the members of the team are working across multiple places in the code baseThree
short release cyclesThreeFrequent commits
- -| Question 1.1: Does your website and documentation provide a clear, high-level overview of your software? |Yes|
-| Question 1.2: Does your website and documentation clearly describe the type of user who should use your software? |Yes|
-| Question 1.3: Do you publish case studies to show how your software has been used by yourself and others? |Yes| -| Question 2.1: Is the name of your project/software unique? * |Yes| -| Question 2.2: Is your project/software name free from trademark violations? * |Yes| -| Question 3.1: Is your software available as a package that can be deployed without building it? |Yes| -| Question 3.2: Is your software available for free? |Yes| -| Question 3.3: Is your source code publicly available to download, either as a downloadable bundle or via access to a source code repository? |Yes| -| Question 3.4: Is your software hosted in an established, third-party repository likeGitHub (https://github.com), BitBucket (https://bitbucket.org),LaunchPad (https://launchpad.net) orSourceForge (https://sourceforge.net)? |Yes| -| Question 4.1: Is your documentation clearly available on your website or within your software? |Yes| -| Question 4.2: Does your documentation include a "quick start" guide, that provides a short overview of how to use your software with some basic examples of use? |Yes| -| Question 4.3: If you provide more extensive documentation, does this provide clear, step-by-step instructions on how to deploy and use your software? |Yes| -| Question 4.4: Do you provide a comprehensive guide to all your software’s commands, functions and options? * |Yes| -| Question 4.5: Do you provide troubleshooting information that describes the symptoms and step-by-step solutions for problems and error messages? * |No| -| Question 4.6: If your software can be used as a library, package or service by other software, do you provide comprehensive API documentation? * |No| -| Question 4.7: Do you store your documentation under revision control with your source code? * |No| -| Question 4.8: Do you publish your release history e.g. release data, version numbers, key features of each release etc. on your web site or in your documentation? * |Yes| -| Question 5.1: Does your software describe how a user can get help with using your software? * |No| -| Question 5.2: Does your website and documentation describe what support, if any, you provide to users and developers? * |No| -| Question 5.3: Does your project have an e-mail address or forum that is solely for supporting users? * |No| -| Question 5.4: Are e-mails to your support e-mail address received by more than one person? * |No| -| Question 5.5: Does your project have a ticketing system to manage bug reports and feature requests? * |No| -| Question 5.6: Is your project's ticketing system publicly visible to your users, so they can view bug reports and feature requests? * |No| -| Question 6.1: Is your software’s architecture and design modular? * |Yes| -| Question 6.2: Does your software use an accepted coding standard or convention? * |Yes| -| Question 7.1: Does your software allow data to be imported and exported using open data formats? * |Yes| -| Question 7.2: Does your software allow communications using open communications protocols? * |Yes| -| Question 8.1: Is your software cross-platform compatible? * |Yes| -| Question 9.1: Does your software adhere to appropriate accessibility conventions or standards? * |Yes| -| Question 9.2: Does your documentation adhere to appropriate accessibility conventions or standards? * |Yes| -| Question 10.1: Is your source code stored in a repository under revision control? * |No| -| Question 10.2: Is each source code release a snapshot of the repository? * |No| -| Question 10.3: Are releases tagged in the repository? * |Yes| -| Question 10.4: Is there a branch of the repository that is always stable? (i.e. tests always pass, code always builds successfully) * |Yes| -| Question 10.5: Do you back-up your repository? * |Yes| -| Question 11.1: Do you provide publicly-available instructions for building your software from the source code? * |Yes| -| Question 11.2: Can you build, or package, your software using an automated tool? * |Yes| -| Question 11.3: Do you provide publicly-available instructions for deploying your software? * |Yes| -| Question 11.4: Does your documentation list all third-party dependencies? * |Yes| -| Question 11.5: Does your documentation list the version number for all third-party dependencies? * |Yes| -| Question 11.6: Does your software list the web address, and licences for all third-party dependencies and say whether the dependencies are mandatory or optional? * |Yes| -| Question 11.7: Can you download dependencies using a dependency management tool or package manager? * |Yes| -| Question 11.8: Do you have tests that can be run after your software has been built or deployed to show whether the build or deployment has been successful? * |Yes| -| Question 12.1: Do you have an automated test suite for your software? * |Yes| -| Question 12.2: Do you have a framework to periodically (e.g. nightly) run your tests on the latest version of the source code? * |Yes| -| Question 12.3: Do you use continuous integration, automatically running tests whenever changes are made to your source code? * |Yes| -| Question 12.4: Are your test results publicly visible? * |Yes| -| Question 12.5: Are all manually-run tests documented? * |Yes| -| Question 13.1: Does your project have resources (e.g. blog, Twitter, RSS feed, Facebook page, wiki, mailing list) that are regularly updated with information about your software? * |No| -| Question 13.2: Does your website state how many projects and users are associated with your project? * |Yes| -| Question 13.3: Do you provide success stories on your website? * |No| -| Question 13.4: Do you list your important partners and collaborators on your website? * |Yes| -| Question 13.5: Do you list your project's publications on your website or link to a resource where these are available? * |No| -| Question 13.6: Do you list third-party publications that refer to your software on your website or link to a resource where these are available? * |No| -| Question 13.7: Can users subscribe to notifications to changes to your source code repository? * |No| -| Question 13.8: If your software is developed as an open source project (and, not just a project developing open source software), do you have a governance model? * |No| -| Question 14.1: Do you accept contributions (e.g. bug fixes, enhancements, documentation updates, tutorials) from people who are not part of your project? * |Yes| -| Question 14.2: Do you have a contributions policy? * |Yes| -| Question 14.3: Is your contributions' policy publicly available? * |Yes| -| Question 14.4: Do contributors keep the copyright/IP of their contributions? * |No| -| Question 15.1: Does your website and documentation clearly state the copyright owners of your software and documentation? * |No| -| Question 15.2: Does each of your source code files include a copyright statement? * |No| -| Question 15.3: Does your website and documentation clearly state the licence of your software? * |Yes| -| Question 15.4: Is your software released under an open source licence? * |Yes| -| Question 15.5: Is your software released under an OSI-approved open-source licence? * |Yes| -| Question 15.6: Does each of your source code files include a licence header? * |No| -| Question 15.7: Do you have a recommended citation for your software? * |Yes| -| Question 16.1: Does your website or documentation include a project roadmap (a list of project and development milestones for the next 3, 6 and 12 months)? * |Yes| -| Question 16.2: Does your website or documentation describe how your project is funded, and the period over which funding is guaranteed? * |No| -| Question 16.3: Do you make timely announcements of the deprecation of components, APIs, etc.? * |No| diff --git a/docs/proj3rubric.md b/docs/proj3rubric.md deleted file mode 100644 index 1bc88f5d0..000000000 --- a/docs/proj3rubric.md +++ /dev/null @@ -1,27 +0,0 @@ - -|Maximum Score|Notes|Self-Evaluation Score|Evidence| -|-|-----|---|---------| -|.5| short release cycles|0.5|2 release cycles over the duration of project 3| -|.5| workload is spread over the whole team (so one team member is often Xtimes more productive than the others...|0.5|Evidence is [here](https://github.com/prithvish-doshi-17/MyDollarBot-BOTGo/graphs/contributors).Everyone has almost same [number of commits](https://github.com/prithvish-doshi-17/MyDollarBot-BOTGo/pulse)| -|.5|Docs: why: docs tell a story, motivate the whole thing, deliver a punchline that makes you want to rush out and use the thing |0.5|Evidence in [Readme](https://github.com/prithvish-doshi-17/MyDollarBot-BOTGo/blob/master/README.md) | -|.5|the files CONTRIBUTING.md lists coding standards and lots of tips on how to extend the system without screwing things up |0.5|Evidence in [CONTRIBUTING.md](https://github.com/prithvish-doshi-17/MyDollarBot-BOTGo/blob/master/CONTRIBUTING.md) | -|.5|Docs: doco generated , format not ugly |0.5|Evidence in [Readme](https://github.com/prithvish-doshi-17/MyDollarBot-BOTGo/blob/master/README.md) and [here](https://github.com/prithvish-doshi-17/MyDollarBot-BOTGo/tree/main/docs) | -|.5|evidence that the whole team is using the same tools (e.g. config files in the repo, updated by lots of different people) |0.5|Entire project is developed in Python, and is being run using shell script| -|.5|evidence that the members of the team are working across multiple places in the code base |0.5|Worked across development of new features and testing them, alongwith enhancement of few older features| -|1|Docs: what: point descriptions of each class/function (in isolation) |1|Descriptions for [/add_recurring](https://github.com/prithvish-doshi-17/MyDollarBot-BOTGo/blob/main/docs/add_recurring.md), [/category](https://github.com/prithvish-doshi-17/MyDollarBot-BOTGo/blob/main/docs/category.md), [/display](https://github.com/prithvish-doshi-17/MyDollarBot-BOTGo/blob/main/docs/display.md), [/graphing](https://github.com/prithvish-doshi-17/MyDollarBot-BOTGo/blob/main/docs/graphing.md), [/helper](https://github.com/prithvish-doshi-17/MyDollarBot-BOTGo/blob/main/docs/helper.md) and [/history](https://github.com/prithvish-doshi-17/MyDollarBot-BOTGo/blob/main/docs/history.md) added/updated| -|.5|Number of commits: by different people |0.5|The number of commits are almost equal across all team members| -|1|issues are being closed |1|Please see [here](https://github.com/prithvish-doshi-17/MyDollarBot-BOTGo/issues?q=is%3Aissue+is%3Aclosed)| -|.5|issues are discussed before they are closed |0.5|Every [closed issue](https://github.com/prithvish-doshi-17/MyDollarBot-BOTGo/issues?q=is%3Aissue+is%3Aclosed) has a comment with the summary| -|.5|Use of syntax checkers |0.5|Use of Flake8 in [travis.yml](https://github.com/prithvish-doshi-17/MyDollarBot-BOTGo/blob/main/.travis.yml)| -|1|Issues reports: there are many |1|[Issue report](https://github.com/prithvish-doshi-17/MyDollarBot-BOTGo/blob/main/docs/issue%20report.md) and [here](https://github.com/prithvish-doshi-17/MyDollarBot-BOTGo/projects)| -|.5|Use of code formatters. |0.5|Config files in GitHub| -|.5|Use of style checkers |0.5|Use of Flake8 in [travis.yml](https://github.com/prithvish-doshi-17/MyDollarBot-BOTGo/blob/main/.travis.yml)| -|.5|Docs: short video, animated, hosted on your repo. That convinces people why they want to work on your code. |0.5|Evidence in [Readme](https://github.com/prithvish-doshi-17/MyDollarBot-BOTGo/blob/master/README.md)| -|.5|test cases exist |0.5|Test cases added. Evidence is [here](https://github.com/prithvish-doshi-17/MyDollarBot-BOTGo/tree/main/test)| -|.5|Use of code coverage |0.5|Please see [here](https://github.com/prithvish-doshi-17/MyDollarBot-BOTGo/blob/main/README.md#code-coverage)| -|.5|other automated analysis tools|0.5|Evidence is [here](https://github.com/prithvish-doshi-17/MyDollarBot-BOTGo/blob/main/.travis.yml)| -|.5|test cases:.a large proportion of the issues related to handling failing cases|0.5|Test cases developed for the new features. Example is [here](https://github.com/prithvish-doshi-17/MyDollarBot-BOTGo/issues/12). Evidence is [here](https://github.com/prithvish-doshi-17/MyDollarBot-BOTGo/blob/main/test/test_add_recurring.py) for adding recurring expenses and [here](https://github.com/prithvish-doshi-17/MyDollarBot-BOTGo/blob/main/test/test_category.py) for adding/deleting custom categories| -|.5|test cases are routinely executed |0.5|Please see [here](https://github.com/prithvish-doshi-17/MyDollarBot-BOTGo/blob/main/README.md#testing)| -|1|Documentation describing how this version improves on the older version|1|Please see [here](https://github.com/prithvish-doshi-17/MyDollarBot-BOTGo/blob/main/README.md#whats-new-from-phase-2-to-phase-3)| -|3|This version is a little(1), some(2), much(3) improved on the last version.|3|Tutor's assessment| -|16| Total|16|| diff --git a/docs/reminder.md b/docs/reminder.md deleted file mode 100644 index aac14bd28..000000000 --- a/docs/reminder.md +++ /dev/null @@ -1,41 +0,0 @@ -# About MyDollarBot's /set_reminder Feature -The /setReminder feature allows users to configure daily or monthly reminders for tracking expenses with MyDollarBot. Users can specify the frequency (daily or monthly) and the exact time at which they wish to receive reminders about their expenditures. - -# Location of Code for this Feature -The code that implements this feature can be found [here](https://github.com/rrajpuro/DollarBot/blob/main/code/reminder.py) - -## Code Description -### Functions - -1. run(message, bot): - - This function initiates the process of setting reminders. It prompts the user to select a category for which they want to set a reminder. Once the user chooses a category, control is passed to post_operation_selection(message, bot) for further processing. - - It takes two arguments, message (the user's message) and bot (the Telegram bot object). - -2. post_operation_selection(message, bot): - - This function handles the selected category from the user and prompts them to input the time for the reminder. Once the user provides the time, control is passed to process_reminder_time(message, chat_id, selected_type, bot) for further processing. - - It takes three arguments, message (the user's message), chat_id (the user's chat ID), and bot (the Telegram bot object). - -3. process_reminder_time(message, chat_id, selected_type, bot): - - This function processes the user's input for the reminder time and validates the time format (HH:MM). If the time is in the correct format, it updates the user's reminder settings in the JSON database. - - It takes four arguments: message (the user's message), chat_id (the user's chat ID), selected_type (the reminder frequency, e.g., "Day" or "Month"), and bot (the Telegram bot object). - -4. send_expenses_reminder(chat_id, dayormonth, bot): - - This function is responsible for sending reminders to users. It retrieves the user's expense history and relevant budget data. Depending on whether the reminder is set for the day or month, it fetches the corresponding data and composes a reminder message. This message is sent to the user to help them track their spending. - - It takes three arguments: chat_id (the user's chat ID), dayormonth (reminder frequency, e.g., "Day" or "Month"), and bot (the Telegram bot object). - -5. send_reminder(chat_id, message, bot): - - This function sends a reminder message to the user with the specified chat ID. It also prints a confirmation message to the console. - - It takes three arguments: chat_id (the user's chat ID), message (the reminder message to send), and bot (the Telegram bot object). - -6. check_reminders(bot): - - This function is responsible for periodically checking and sending reminders to users. It reads user-specific reminder settings from the JSON database and compares the current time to the configured reminder time. If they match, it sends the daily reminder to the user. - - It takes one argument, bot (the Telegram bot object). - -## How to Use This Feature -1. Start the MyDollarBot project. -2. Open the Telegram chat with MyDollarBot. -3. Select /set_reminder from menu to configure reminders for tracking daily or monthly expenses. -4. Follow the prompts to select a category and enter the time for the reminder. - - - diff --git a/logo.jpeg b/logo.jpeg deleted file mode 100644 index b91cbd08f..000000000 Binary files a/logo.jpeg and /dev/null differ diff --git a/proj3/README.md b/proj3/README.md deleted file mode 100644 index d8dc0a81b..000000000 --- a/proj3/README.md +++ /dev/null @@ -1,104 +0,0 @@ -scorecard - -| Rubric | Assesment| Evidence | -| ----------- | ----------- | -- | -| Total | 279 | | -| Video | 3 | https://www.youtube.com/watch?v=E7EAHumVHhk | -| Workload is spread over the whole team (one team member is often Xtimes more productive than the others...but nevertheless, here is a track record that everyone is contributing a lot) | 3 | https://github.com/ymdatta/DollarBot/graphs/contributors | -| Number of commits | 3 | https://github.com/ymdatta/DollarBot/commits/main | -| Number of commits: by different people | 3 | https://github.com/ymdatta/DollarBot/graphs/contributors | -| Issues reports: there are many | 3 | https://github.com/ymdatta/DollarBot/issues | -| Issues are being closed | 3 | https://github.com/ymdatta/DollarBot/issues?q=is%3Aissue+is%3Aclosed| -| DOI badge: exists | 3 | https://github.com/ymdatta/DollarBot/blob/main/README.md, https://zenodo.org/records/10210342 | -| Docs: doco generated, format not ugly | 3 | https://github.com/ymdatta/DollarBot/tree/main/docs | -| Docs: what: point descriptions of each class/function (in isolation) | 3 | https://github.com/ymdatta/DollarBot/tree/main/docs | -| 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/ymdatta/DollarBot/tree/main/docs | -| 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/ymdatta/DollarBot/tree/main/docs | -| Docs: short video, animated, hosted on your repo. That convinces people why they want to work on your code | 3 | https://github.com/ymdatta/DollarBot/blob/main/README.md#description | -| Use of version control tools | 3 | https://github.com/ymdatta/DollarBot | -| Use of style checkers | 3 | https://github.com/ymdatta/DollarBot/blob/main/.github/workflows/styleChecker.yml | -| Use of code formatters | 3 | https://github.com/ymdatta/DollarBot/blob/main/.flake8 | -| Use of code coverage | 3 | https://github.com/ymdatta/DollarBot/blob/main/.codecov.yml | -| Other automated analysis tools | 3 | https://github.com/ymdatta/DollarBot/blob/main/.flake8 | -| Test cases exist | 3 | https://github.com/ymdatta/DollarBot/tree/main/test | -| Test cases are routinely executed | 3 | https://github.com/ymdatta/DollarBot/blob/main/README.md#testing | -| 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/ymdatta/DollarBot/blob/main/CONTRIBUTING.md | -| Issues are discussed before they are closed | 3 | https://chat.whatsapp.com/HZetASi4W6P3hpTYOP751A (issues are discussed in chat channel)| -| Chat channel: exists | 3 | https://chat.whatsapp.com/HZetASi4W6P3hpTYOP751A | -| Test cases: a large proportion of the issues related to handling failing cases | 3 | https://github.com/ymdatta/DollarBot/issues?q=is%3Aissue+is%3Aclosed | -| Evidence that the whole team is using the same tools: everyone can get to all tools and files | 3 | everone has mulitiple commits to this repository: repository: https://github.com/ymdatta/DollarBot/pulse | -| Evidence that the whole team is using the same tools (e.g. config files in the repo, updated by lots of different people | 3 | everone has multiple commits to this repository: https://github.com/ymdatta/DollarBot/pulse | -| Evidence that the whole team is using the same tools (e.g. tutor can ask anyone to share screen, they demonstrate the system running on their computer | 3 | everone has muliplt commits to this repository: https://github.com/ymdatta/DollarBot/pulse | -| Evidence that the members of the team are working across multiple places in the code base | 3 | https://github.com/ymdatta/DollarBot/commits/main/code/helper.py, https://github.com/ymdatta/DollarBot/commits/main/code/code.py | -| Short release cycles | 3 | https://github.com/ymdatta/DollarBot/releases | -| Does your website and documentation provide a clear, high-level overview of your software? | 3 | https://github.com/ymdatta/DollarBot/blob/main/README.md, https://github.com/ymdatta/DollarBot/tree/main/docs | -| Does your website and documentation clearly describe the type of user who should use your software? | 3 | https://github.com/ymdatta/DollarBot/tree/main#usage | -| Do you publish case studies to show how your software has been used by yourself and others? | 3 | https://docs.google.com/document/d/1-2Ymohz238M43vACZSaMJzciNv_69CRBKFgrELbd-Bg/edit?usp=sharing | -| Is the name of your project/software unique | 3 | yes https://github.com/ymdatta/DollarBot/blob/main/docs/0001-8711513694_20210926_212845_0000.png | -| Is your project/software name free from trademark violations? | 3 | yes https://github.com/ymdatta/DollarBot/blob/main/docs/0001-8711513694_20210926_212845_0000.png | -| Is your software available as a package that can be deployed without building it? | 3 | https://github.com/ymdatta/DollarBot#installation | -| Is your software available for free | 3 | Yes: https://github.com/ymdatta/DollarBot/blob/main/LICENSE | -| Is your source code publicly available to download, either as a downloadable bundle or via access to a source code repository | 3 | https://github.com/ymdatta/DollarBot | -| Is your software hosted in an established, third-party repository likeGitHub (https://github.com), BitBucket (https://bitbucket.org),LaunchPad (https://launchpad.net) orSourceForge (https://sourceforge.net) | 3 | Yes: https://github.com/ymdatta/DollarBot | -| Is your documentation clearly available on your website or within your software? | 3 | Yes: https://github.com/ymdatta/DollarBot/tree/main/docs | -| Does your documentation include a "quick start" guide, that provides a short overview of how to use your software with some basic examples of use? | 3 | Yes https://github.com/ymdatta/DollarBot#-dollarbot---budgeting-on-the-go-| -| If you provide more extensive documentation, does this provide clear, step-by-step instructions on how to deploy and use your software? | 3 | Yes: https://github.com/ymdatta/DollarBot#installation | -| Do you provide a comprehensive guide to all your software’s commands, functions and options? | 3 | https://github.com/ymdatta/DollarBot/blob/main/README.md, https://github.com/ymdatta/DollarBot/tree/main/docs | -| Do you provide troubleshooting information that describes the symptoms and step-by-step solutions for problems and error messages? | 3 | https://github.com/ymdatta/DollarBot/tree/main#interrobang-troubleshooting | -| If your software can be used as a library, package or service by other software, do you provide comprehensive API documentation? | 3 | https://github.com/ymdatta/DollarBot/tree/main/docs | -| Do you store your documentation under revision control with your source code? | 3 | https://github.com/ymdatta/DollarBot | -| Do you publish your release history e.g. release data, version numbers, key features of each release etc. on your web site or in your documentation? | 3 | Yes: https://github.com/ymdatta/DollarBot/blob/main/README.md | -| Does your software describe how a user can get help with using your software? | 3 | https://github.com/ymdatta/DollarBot/tree/main#usage | -| Does your website and documentation describe what support, if any, you provide to users and developers? | 3 | Yes: https://github.com/ymdatta/DollarBot/blob/main/CONTRIBUTING.md | -| Does your project have an e-mail address or forum that is solely for supporting users? | 3 | dollarbot38@googlegroups.com | -| Are e-mails to your support e-mail address received by more than one person? | 3 | dollarbot38@googlegroups.com | -| Does your project have a ticketing system to manage bug reports and feature requests? | 3 | https://github.com/ymdatta/DollarBot/issues | -| Is your project's ticketing system publicly visible to your users, so they can view bug reports and feature requests? | 3 | https://github.com/ymdatta/DollarBot/issues, https://github.com/ymdatta/DollarBot/issues?q=is%3Aissue+is%3Aclosed | -| Is your software’s architecture and design modular | 3 | Yes: https://github.com/ymdatta/DollarBot | -| Does your software use an accepted coding standard or convention? | 3 | https://github.com/ymdatta/DollarBot/blob/main/.flake8 | -| Does your software allow data to be imported and exported using open data formats? | 3 | In features we mentioned that it can export data in pdf and csv formats | -| Does your software allow communications using open communications protocols? | 3 | dollarbot38@googlegroups.com | -| Is your software cross-platform compatible? | 3 | yes https://github.com/ymdatta/DollarBot/blob/main/README.md | -| Does your software adhere to appropriate accessibility conventions or standards? | 3 | https://github.com/ymdatta/DollarBot/blob/main/README.md | -| Does your documentation adhere to appropriate accessibility conventions or standards? | 3 | https://github.com/ymdatta/DollarBot/tree/main/docs | -| Is your source code stored in a repository under revision control? | 3 | Yes, on github: https://github.com/ymdatta/DollarBot | -| Is each source code release a snapshot of the repository? | 3 | https://github.com/ymdatta/DollarBot/releases | -| Are releases tagged in the repository? | 3 | https://github.com/ymdatta/DollarBot/releases | -| Is there a branch of the repository that is always stable? (i.e. tests always pass, code always builds successfully) | 3 | Yes, the main: https://github.com/ymdatta/DollarBot/tree/main | -| Do you back-up your repository? | 3 | Every one in the team has forked the repo and has it in their local machine https://github.com/ymdatta/DollarBot | -| Do you provide publicly-available instructions for building your software from the source code? | 3 | Yes: https://github.com/ymdatta/DollarBot/tree/main#installation | -| Can you build, or package, your software using an automated tool? | 3 | https://github.com/ymdatta/DollarBot/blob/main/.flake8 | -| Do you provide publicly-available instructions for deploying your software? | 3 | Yes: https://github.com/ymdatta/DollarBot/tree/main#installation | -| Does your documentation list all third-party dependencies? | 3 | https://github.com/ymdatta/DollarBot/blob/main/README.md | -| Does your documentation list the version number for all third-party dependencies? | 3 | https://github.com/ymdatta/DollarBot/blob/main/README.md | -| Can you download dependencies using a dependency management tool or package manager? | 3 | https://github.com/ymdatta/DollarBot/blob/main/README.md, https://github.com/ymdatta/DollarBot/tree/main#installation | -| Do you have tests that can be run after your software has been built or deployed to show whether the build or deployment has been successful? | 3 | https://github.com/ymdatta/DollarBot/blob/main/.github/workflows/testCases.yml | -| Do you have an automated test suite for your software? | 3 | https://github.com/ymdatta/DollarBot/blob/main/.github/workflows/testCases.yml | -| Do you have a framework to periodically (e.g. nightly) run your tests on the latest version of the source code? | 3 | https://github.com/ymdatta/DollarBot/blob/main/.github/workflows/testCases.yml | -| Do you use continuous integration, automatically running tests whenever changes are made to your source code? | 3 | https://github.com/ymdatta/DollarBot/blob/main/.github/workflows/testCases.yml| -| Are your test results publicly visible? | 3 | https://github.com/ymdatta/DollarBot/actions/workflows/testCases.yml | -| Are all manually-run tests documented? | 3 | https://github.com/ymdatta/DollarBot/blob/main/.github/workflows/testCases.yml | -| Does your project have resources (e.g. blog, Twitter, RSS feed, Facebook page, wiki, mailing list) that are regularly updated with information about your software? | 3 | https://github.com/nainisha-b/MyExpenseBot/blob/main/README.md | -| Does your website state how many projects and users are associated with your project? | 3 | https://github.com/ymdatta/DollarBot/projects?query=is%3Aopen | -| Do you provide success stories on your website? | 3 | https://docs.google.com/document/d/1-2Ymohz238M43vACZSaMJzciNv_69CRBKFgrELbd-Bg/edit?usp=sharing | -| Do you list your important partners and collaborators on your website? | 3 | https://github.com/ymdatta/DollarBot/blob/main/README.md#handshake-contributors | -| Do you list your project's publications on your website or link to a resource where these are available? | 3 | https://github.com/ymdatta/DollarBot | -| Do you list third-party publications that refer to your software on your website or link to a resource where these are available? | 3 | https://github.com/ymdatta/DollarBot/blob/main/LICENSE | -| Can users subscribe to notifications to changes to your source code repository? | 3 | Yes, they can click on 'watch' option on Github repository and get notifications. https://github.com/ymdatta/DollarBot | -| If your software is developed as an open source project (and, not just a project developing open source software), do you have a governance model? | 3 | https://github.com/ymdatta/DollarBot/blob/main/LICENSE | -| Do you accept contributions (e.g. bug fixes, enhancements, documentation updates, tutorials) from people who are not part of your project? | 3 | Yes: https://github.com/ymdatta/DollarBot/blob/main/CONTRIBUTING.md | -| Do you have a contributions policy? | 3 | Yes: https://github.com/ymdatta/DollarBot/blob/main/CONTRIBUTING.md | -| Is your contributions' policy publicly available? | 3 | https://github.com/ymdatta/DollarBot/blob/main/CONTRIBUTING.md | -| Do contributors keep the copyright/IP of their contributions? | 3 | https://github.com/ymdatta/DollarBot/blob/main/LICENSE | -| Does your website and documentation clearly state the copyright owners of your software and documentation? | 3 | https://github.com/ymdatta/DollarBot/blob/main/LICENSEE | -| Does each of your source code files include a copyright statement? | 3 | yes, https://github.com/ymdatta/DollarBot/blob/main/code/add.py, https://github.com/ymdatta/DollarBot/blob/main/code/code.py | -| Does your website and documentation clearly state the licence of your software? | 3 | https://github.com/ymdatta/DollarBot/blob/main/LICENSE | -| Is your software released under an open source licence? | 3 | https://github.com/ymdatta/DollarBot/blob/main/LICENSE | -| Is your software released under an OSI-approved open-source licence? | 3 | https://github.com/ymdatta/DollarBot/blob/main/LICENSE | -| Does each of your source code files include a licence header? | 3 | yes, https://github.com/ymdatta/DollarBot/blob/main/code/add.py, https://github.com/ymdatta/DollarBot/blob/main/code/code.py | -| Do you have a recommended citation for your software? | 3 | https://github.com/ymdatta/DollarBot/blob/main/CITATION.md | -| Does your website or documentation include a project roadmap (a list of project and development milestones for the next 3, 6 and 12 months)? | 3 | https://github.com/ymdatta/DollarBot#roadmap | -| Does your website or documentation describe how your project is funded, and the period over which funding is guaranteed? | NA | Not Applicable | -| Do you make timely announcements of the deprecation of components, APIs, etc.? | NA | Not Applicable | - -Link to demo video: https://www.youtube.com/watch?v=E7EAHumVHhk diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..28f8a4cbe --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,13 @@ +[tool.isort] +profile = "black" + +[tool.pytest.ini_options] +filterwarnings = [ + "ignore::DeprecationWarning", +] +addopts = "--cov=api --cov-report=term-missing" + +[tool.pylint] +disable = ["fixme"] +[tool.pylint.MASTER] +ignore = ["tests"] diff --git a/requirements.txt b/requirements.txt index 9c7942913..c2d35c710 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,36 +1,21 @@ -anyio==4.0.0 -certifi==2023.11.17 -charset-normalizer==3.3.2 -contourpy==1.2.0 -coverage==7.3.2 -cycler==0.12.1 -Extract==1.0 -flake8==6.1.0 -fonttools==4.44.3 -forex-python==1.8 -h11==0.14.0 -httpcore==1.0.2 -httpx==0.25.1 -idna==3.4 -iniconfig==2.0.0 -jproperties==2.1.1 -kiwisolver==1.4.5 -matplotlib==3.8.2 -mccabe==0.7.0 -mock==5.1.0 -numpy==1.26.2 -packaging==23.2 -Pillow==10.1.0 -pluggy==1.3.0 -pycodestyle==2.11.1 -pyflakes==3.1.0 -pyparsing==3.1.1 -pyTelegramBotAPI==4.14.0 -pytest==7.4.3 -pytest-mock==3.12.0 -python-dateutil==2.8.2 -python-telegram-bot==20.6 -requests==2.31.0 -six==1.16.0 -sniffio==1.3.0 -urllib3==2.1.0 +fastapi[standard] +currencyconverter +motor +python-jose +pytest +pytest-cov +pre-commit +isort +PyJWT +black +pylint +mypy +bandit +pandas-stubs +types-python-jose +matplotlib +pandas +pandas-stubs +reportlab +python-telegram-bot +requests diff --git a/run.sh b/run.sh deleted file mode 100755 index 3691de932..000000000 --- a/run.sh +++ /dev/null @@ -1,28 +0,0 @@ -pip3 install -r requirements.txt - -api_token=$(grep "api_token" user.properties|cut -d'=' -f2) - -if [ -z "$api_token" ] -then - echo "Api token missing: Execute the following steps to generate Api token" - 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 "/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 "4. BotFather will now confirm the creation of your bot and provide a TOKEN to access the HTTP API - copy this token." - echo - echo "Add Api token and continue?(y/n)" - read option - if [ $option == 'y' -o $option == 'Y' ] - then - echo "Enter the copied token: " - read api_token - echo "api_token="$api_token >> user.properties - fi -fi - -if [ -n "$api_token" ] -then - python3 code/code.py -fi diff --git a/setup.py b/setup.py deleted file mode 100644 index cfa2a2349..000000000 --- a/setup.py +++ /dev/null @@ -1,9 +0,0 @@ -from setuptools import setup, find_packages - -setup( - name='Track My Dollar', - version='1.0', - description='An easy to use Telegram Bot to track everyday expenses', - author='Dev, Prakruthi, Radhika, Rohan, Sunidhi', - packages=find_packages() -) diff --git a/test/dummy_expense_record.json b/test/dummy_expense_record.json deleted file mode 100644 index c5cf5af31..000000000 --- a/test/dummy_expense_record.json +++ /dev/null @@ -1,91 +0,0 @@ -{ - "894127939": [ - "28-Oct-2021 15:27,Food,2.3", - "28-Oct-2021 15:28,Food,15.75", - "28-Oct-2021 15:28,Food,9.99", - "28-Oct-2021 15:28,Groceries,20.0", - "28-Oct-2021 15:28,Utilities,10.0", - "28-Oct-2021 15:28,Transport,7.28", - "28-Oct-2021 15:28,Transport,25.99", - "28-Oct-2021 15:31,Utilities,6.0", - "28-Oct-2021 15:38,Food,10.0", - "28-Oct-2021 15:40,Groceries,80.0" - ], - "8941298739": [ - "28-Oct-2021 15:27,Food,2.3", - "28-Oct-2021 15:28,Food,15.75", - "28-Oct-2021 15:28,Food,9.99", - "28-Oct-2021 15:28,Groceries,20.0", - "28-Oct-2021 15:28,Utilities,10.0", - "28-Oct-2021 15:28,Transport,7.28", - "28-Oct-2021 15:28,Transport,25.99", - "28-Oct-2021 15:31,Utilities,6.0", - "28-Oct-2021 15:38,Food,10.0", - "28-Oct-2021 15:40,Groceries,80.0" - ], - "1574038305": [ - - ] - , - "2614394724848": [ - "28-Oct-2021 15:27,Food,2.3", - "28-Oct-2021 15:28,Food,15.75", - "28-Oct-2021 15:28,Food,9.99", - "28-Oct-2021 15:28,Groceries,20.0", - "28-Oct-2021 15:28,Utilities,10.0", - "28-Oct-2021 15:28,Transport,7.28", - "28-Oct-2021 15:28,Transport,25.99", - "28-Oct-2021 15:31,Utilities,6.0", - "28-Oct-2021 15:38,Food,10.0", - "28-Oct-2021 15:40,Groceries,80.0" - ], - "1075979006": { - "data": [ - "28-Nov-2021 15:27,Food,2.3", - "28-Nov-2021 15:28,Food,15.75", - "28-Nov-2021 15:28,Food,9.99", - "28-Nov-2021 15:28,Groceries,20.0", - "28-Nov-2021 15:28,Utilities,10.0", - "28-Nov-2021 15:28,Transport,7.28", - "28-Nov-2021 15:28,Transport,25.99", - "28-Nov-2021 15:31,Utilities,6.0", - "28-Nov-2021 15:38,Food,10.0", - "28-Nov-2021 15:40,Groceries,80.0" - ], - "budget": { - "overall": "1000.0", - "category": null - } - }, - "1075979007": { - "data": [ - "28-Nov-2021 15:27,Food,2.3", - "28-Nov-2021 15:28,Food,15.75", - "28-Nov-2021 15:28,Food,9.99", - "28-Nov-2021 15:28,Groceries,20.0", - "28-Nov-2021 15:28,Utilities,10.0", - "28-Nov-2021 15:28,Transport,7.28", - "28-Nov-2021 15:28,Transport,25.99", - "28-Nov-2021 15:31,Utilities,6.0", - "28-Nov-2021 15:38,Food,10.0", - "28-Nov-2021 15:40,Groceries,80.0" - ], - "budget": { - "overall": null, - "category": { - "Food": "100.0", - "Groceries": "150.0", - "Utilities": "180.0", - "Transport": "20.0", - "Shopping": "180.0", - "Miscellaneous": "80.0" - } - }, - "reminder": { - "type": "Day", - "time": "21:02" - } - } -} - - diff --git a/test/test_account.py b/test/test_account.py deleted file mode 100644 index 1fcede887..000000000 --- a/test/test_account.py +++ /dev/null @@ -1,133 +0,0 @@ -""" - -MIT License - -Copyright (c) 2021 Dev Kumar - -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. - -""" -from mock.mock import patch -from telebot import types -from code import add -from code import account -from code import helper -from mock import ANY -from mock import Mock -import pytest - - -dateFormat = '%d-%b-%Y' -timeFormat = '%H:%M' -monthFormat = '%b-%Y' - - -@patch('telebot.telebot') -def test_run_reply_to(mock_telebot, mocker): - mc = mock_telebot.return_value - mc.reply_to.return_value = True - message = create_message("hello from test run!") - account.run(message, mc) - assert (mc.reply_to.called) - -@patch('telebot.telebot') -def test_run_register_next_step_handler(mock_telebot, mocker): - mc = mock_telebot.return_value - mc.reply_to.return_value = True - mc.register_next_step_handler.return_value = True - message = create_message("hello from test run!") - account.run(message, mc) - assert (mc.reply_to.called) - assert (mc.register_next_step_handler.called) - -@patch('telebot.telebot') -@patch('code.account.add_account_record', Mock(return_value='sfs')) -@patch('code.helper.write_json', Mock(return_value=None)) -def test_post_category_selection_working(mock_telebot, mocker): - mc = mock_telebot.return_value - mc.send_message.return_value = True - - message = create_message_category("hello from testing!", "Savings") - account.post_category_selection(message, mc) - assert (mc.send_message.called) - -@patch('telebot.telebot') -def test_post_category_selection_noMatchingCategory(mock_telebot, mocker): - with pytest.raises(Exception) as e_info: - raise Exception('It failed') - - mc = mock_telebot.return_value - mc.send_message.return_value = [] - mc.reply_to.return_value = True - - mocker.patch.object(add, 'helper') - account.helper.getSpendCategories.return_value = None - - message = create_message_category("hello from testing!", "DummyCategory") - add.post_category_selection(message, mc) - assert str(e_info.value) == 'It failed' - -@patch('telebot.telebot') -@patch('code.helper.getCommands', Mock(return_value=[])) -def test_post_category_selection_noMatchingCategory_Exception_ReplyToCalled(mock_telebot, mocker): - with pytest.raises(Exception) as e_info: - raise Exception('It failed') - - mc = mock_telebot.return_value - mc.send_message.return_value = [] - mc.reply_to.return_value = True - - mocker.patch.object(add, 'helper') - account.helper.getSpendCategories.return_value = None - - message = create_message_category("hello from testing!", "DummyCategory") - add.post_category_selection(message, mc) - assert str(e_info.value) == 'It failed' - assert (mc.reply_to.called) - -@patch('telebot.telebot') -@patch('code.helper.getCommands', Mock(return_value=[])) -def test_post_category_selection_noMatchingCategory_Exception_SendMessageCalled(mock_telebot, mocker): - with pytest.raises(Exception) as e_info: - raise Exception('It failed') - - mc = mock_telebot.return_value - mc.send_message.return_value = [] - mc.reply_to.return_value = True - - mocker.patch.object(add, 'helper') - account.helper.getSpendCategories.return_value = None - - message = create_message_category("hello from testing!", "DummyCategory") - add.post_category_selection(message, mc) - assert str(e_info.value) == 'It failed' - - assert (mc.reply_to.called) - assert (mc.send_message.called) - assert (mc.send_message.called) - -def create_message(text): - params = {'messagebody': text} - chat = types.User(11, False, 'test') - return types.Message(1, None, None, chat, 'text', params, "") - -def create_message_category(text, category): - params = {'messagebody': text, 'text': category} - chat = types.User(11, False, 'test') - return types.Message(1, None, None, chat, 'text', params, "") diff --git a/test/test_add.py b/test/test_add.py deleted file mode 100644 index 4e3a18527..000000000 --- a/test/test_add.py +++ /dev/null @@ -1,208 +0,0 @@ -""" - -MIT License - -Copyright (c) 2021 Dev Kumar - -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 os -import json -from mock.mock import patch -from telebot import types -from code import add -from mock import Mock -import pytest - -dateFormat = '%d-%b-%Y' -timeFormat = '%H:%M' -monthFormat = '%b-%Y' - - -@patch('telebot.telebot') -def test_run(mock_telebot, mocker): - mc = mock_telebot.return_value - mc.reply_to.return_value = True - message = create_message("hello from test run!") - add.run(message, mc) - assert (mc.reply_to.called) - - -@patch('telebot.telebot') -def test_post_category_selection_working(mock_telebot, mocker): - mc = mock_telebot.return_value - mc.send_message.return_value = True - - message = create_message("hello from testing!") - add.post_category_selection(message, mc) - assert (mc.send_message.called) - - -@patch('telebot.telebot') -def test_post_category_selection_noMatchingCategory(mock_telebot, mocker): - mc = mock_telebot.return_value - mc.send_message.return_value = [] - mc.reply_to.return_value = True - - mocker.patch.object(add, 'helper') - add.helper.getSpendCategories.return_value = None - - message = create_message("hello from testing!") - add.post_category_selection(message, mc) - assert (mc.reply_to.called) - - -@patch('telebot.telebot') -def test_post_amount_input_working(mock_telebot, mocker): - mc = mock_telebot.return_value - mc.send_message.return_value = True - - message = create_message("hello from testing!") - add.post_category_selection(message, mc) - assert (mc.send_message.called) - -@patch('telebot.telebot') -@patch('code.helper.validate_entered_amount', Mock(return_value=0)) -def test_post_amount_input_failing_with_zero_amount(mock_telebot, mocker): - with pytest.raises(Exception) as e_info: - raise Exception('It failed') - - mc = mock_telebot.return_value - mc.send_message.return_value = True - - message = create_message("hello from testing!") - add.post_amount_input(message, mc, "DummyCategory", "USD") - assert str(e_info.value) == 'It failed' - assert (mc.reply_to.called) - -@patch('telebot.telebot') -def test_post_amount_input_working_withdata(mock_telebot, mocker): - mc = mock_telebot.return_value - mc.send_message.return_value = True - mocker.patch.object(add, 'helper') - add.helper.validate_entered_amount.return_value = 100 - add.helper.write_json.return_value = True - add.helper.getDateFormat.return_value = dateFormat - add.helper.getTimeFormat.return_value = timeFormat - add.helper.get_account_type.return_value = "Checking" - add.helper.get_account_balance.return_value = 100 - - mocker.patch.object(add, 'option') - add.option.return_value = {11, "here"} - - message = create_message("hello from testing!") - add.post_amount_input(message, mc, 'Food','INR') - assert (mc.send_message.called) - - -@patch('telebot.telebot') -def test_post_amount_input_nonworking(mock_telebot, mocker): - mc = mock_telebot.return_value - mc.send_message.return_value = True - mc.reply_to.return_value = True - mocker.patch.object(add, 'helper') - add.helper.validate_entered_amount.return_value = 0 - message = create_message("hello from testing!") - add.post_amount_input(message, mc, 'Food','INR') - assert (mc.reply_to.called) - - -@patch('telebot.telebot') -def test_post_amount_input_working_withdata_chatid(mock_telebot, mocker): - mc = mock_telebot.return_value - mc.send_message.return_value = True - mocker.patch.object(add, 'helper') - add.helper.validate_entered_amount.return_value = 100 - add.helper.write_json.return_value = True - add.helper.getDateFormat.return_value = dateFormat - add.helper.getTimeFormat.return_value = timeFormat - add.helper.get_account_type.return_value = "Checking" - add.helper.get_account_balance.return_value = 100 - - mocker.patch.object(add, 'option') - add.option = {11, "here"} - test_option = {} - test_option[11] = "here" - add.option = test_option - - message = create_message("hello from testing!") - add.post_amount_input(message, mc, 'Food','INR') - assert (mc.send_message.called) - #assert (mc.send_message.called_with(11, ANY)) - - -def test_add_user_record_nonworking(mocker): - mocker.patch.object(add, 'helper') - add.helper.read_json.return_value = {} - addeduserrecord = add.add_user_record(1, "record : test") - assert (addeduserrecord) - - -def test_add_user_record_working(mocker): - MOCK_USER_DATA = test_read_json() - mocker.patch.object(add, 'helper') - add.helper.read_json.return_value = MOCK_USER_DATA - addeduserrecord = add.add_user_record(1, "record : test") - if (len(MOCK_USER_DATA) + 1 == len(addeduserrecord)): - assert True - - -def test_add_user_balance_record_working(mocker): - MOCK_USER_DATA = test_read_json() - mocker.patch.object(add, 'helper') - add.helper.read_json.return_value = MOCK_USER_DATA - - addeduserrecord = add.add_user_balance_record('1', "record: test2") - if (len(MOCK_USER_DATA) + 1 == len(addeduserrecord)): - assert True - -@patch('code.add.helper.get_account_type', Mock(return_value='Savings')) -@patch('code.add.helper.get_account_balance', Mock(return_value=10)) -def test_is_valid_resource_true(mocker): - - return_val = add.is_Valid_expense("DummyMsg", 5) - assert (return_val == True) - -@patch('code.add.helper.get_account_type', Mock(return_value='Savings')) -@patch('code.add.helper.get_account_balance', Mock(return_value=100)) -def test_is_valid_resource_false(mocker): - - return_val = add.is_Valid_expense("DummyMsg", 105) - assert (return_val == False) - -def create_message(text): - params = {'messagebody': text} - chat = types.User(11, False, 'test') - return types.Message(1, None, None, chat, 'text', params, "") - - -def test_read_json(): - try: - if not os.path.exists('./test/dummy_expense_record.json'): - with open('./test/dummy_expense_record.json', 'w') as json_file: - json_file.write('{}') - return json.dumps('{}') - elif os.stat('./test/dummy_expense_record.json').st_size != 0: - with open('./test/dummy_expense_record.json') as expense_record: - expense_record_data = json.load(expense_record) - return expense_record_data - - except FileNotFoundError: - print("---------NO RECORDS FOUND---------") diff --git a/test/test_add_recurring.py b/test/test_add_recurring.py deleted file mode 100644 index 9ddbdc68d..000000000 --- a/test/test_add_recurring.py +++ /dev/null @@ -1,172 +0,0 @@ -""" - -MIT License - -Copyright (c) 2021 Dev Kumar - -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 os -import json -from mock.mock import patch -from telebot import types -from code import add -from mock import ANY - - -dateFormat = '%d-%b-%Y' -timeFormat = '%H:%M' -monthFormat = '%b-%Y' - - -@patch('telebot.telebot') -def test_run(mock_telebot, mocker): - mc = mock_telebot.return_value - mc.reply_to.return_value = True - message = create_message("hello from test run!") - add.run(message, mc) - assert (mc.reply_to.called) - - -@patch('telebot.telebot') -def test_post_category_selection_working(mock_telebot, mocker): - mc = mock_telebot.return_value - mc.send_message.return_value = True - - message = create_message("hello from testing!") - add.post_category_selection(message, mc) - assert (mc.send_message.called) - - -@patch('telebot.telebot') -def test_post_category_selection_noMatchingCategory(mock_telebot, mocker): - mc = mock_telebot.return_value - mc.send_message.return_value = [] - mc.reply_to.return_value = True - - mocker.patch.object(add, 'helper') - add.helper.getSpendCategories.return_value = None - - message = create_message("hello from testing!") - add.post_category_selection(message, mc) - assert (mc.reply_to.called) - - -@patch('telebot.telebot') -def test_post_amount_input_working(mock_telebot, mocker): - mc = mock_telebot.return_value - mc.send_message.return_value = True - - message = create_message("hello from testing!") - add.post_category_selection(message, mc) - assert (mc.send_message.called) - - -@patch('telebot.telebot') -def test_post_amount_input_working_withdata(mock_telebot, mocker): - mc = mock_telebot.return_value - mc.send_message.return_value = True - mocker.patch.object(add, 'helper') - add.helper.validate_entered_amount.return_value = 100 - add.helper.write_json.return_value = True - add.helper.getDateFormat.return_value = dateFormat - add.helper.getTimeFormat.return_value = timeFormat - add.helper.get_account_type.return_value = "Checking" - add.helper.get_account_balance.return_value = 100 - - mocker.patch.object(add, 'option') - add.option.return_value = {11, "here"} - - message = create_message("hello from testing!") - add.post_amount_input(message, mc, 'Food','INR') - assert (mc.send_message.called) - - -@patch('telebot.telebot') -def test_post_amount_input_nonworking(mock_telebot, mocker): - mc = mock_telebot.return_value - mc.send_message.return_value = True - mc.reply_to.return_value = True - mocker.patch.object(add, 'helper') - add.helper.validate_entered_amount.return_value = 0 - message = create_message("hello from testing!") - add.post_amount_input(message, mc, 'Food','INR') - assert (mc.reply_to.called) - - -@patch('telebot.telebot') -def test_post_amount_input_working_withdata_chatid(mock_telebot, mocker): - mc = mock_telebot.return_value - mc.send_message.return_value = True - mocker.patch.object(add, 'helper') - add.helper.validate_entered_amount.return_value = 100 - add.helper.write_json.return_value = True - add.helper.getDateFormat.return_value = dateFormat - add.helper.getTimeFormat.return_value = timeFormat - add.helper.get_account_type.return_value = "Checking" - add.helper.get_account_balance.return_value = 100 - - mocker.patch.object(add, 'option') - add.option = {11, "here"} - test_option = {} - test_option[11] = "here" - add.option = test_option - - message = create_message("hello from testing!") - add.post_amount_input(message, mc, 'Food','INR') - assert (mc.send_message.called) - #assert (mc.send_message.called_with(11, ANY)) - - -def test_add_user_record_nonworking(mocker): - mocker.patch.object(add, 'helper') - add.helper.read_json.return_value = {} - addeduserrecord = add.add_user_record(1, "record : test") - assert (addeduserrecord) - - -def test_add_user_record_working(mocker): - MOCK_USER_DATA = test_read_json() - mocker.patch.object(add, 'helper') - add.helper.read_json.return_value = MOCK_USER_DATA - addeduserrecord = add.add_user_record(1, "record : test") - if (len(MOCK_USER_DATA) + 1 == len(addeduserrecord)): - assert True - - -def create_message(text): - params = {'messagebody': text} - chat = types.User(11, False, 'test') - return types.Message(1, None, None, chat, 'text', params, "") - - -def test_read_json(): - try: - if not os.path.exists('./test/dummy_expense_record.json'): - with open('./test/dummy_expense_record.json', 'w') as json_file: - json_file.write('{}') - return json.dumps('{}') - elif os.stat('./test/dummy_expense_record.json').st_size != 0: - with open('./test/dummy_expense_record.json') as expense_record: - expense_record_data = json.load(expense_record) - return expense_record_data - - except FileNotFoundError: - print("---------NO RECORDS FOUND---------") diff --git a/test/test_budget.py b/test/test_budget.py deleted file mode 100644 index 2028cc619..000000000 --- a/test/test_budget.py +++ /dev/null @@ -1,118 +0,0 @@ -""" - -MIT License - -Copyright (c) 2021 Dev Kumar - -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. - -""" - -from mock import ANY -import mock -from mock.mock import patch -from telebot import types -from code import budget - - -@patch('telebot.telebot') -def test_run(mock_telebot, mocker): - mc = mock_telebot.return_value - mc.reply_to.return_value = True - message = create_message("hello from test run!") - budget.run(message, mc) - #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 - mc.send_message.return_value = True - - mocker.patch.object(budget, 'helper') - budget.helper.getBudgetOptions.return_value = {} - - message = create_message("hello from budget test run!") - 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 - mc.send_message.return_value = True - - mocker.patch.object(budget, 'budget_update') - budget.budget_update.run.return_value = True - - mocker.patch.object(budget, 'helper') - budget.helper.getBudgetOptions.return_value = { - 'update': 'Add/Update', - 'view': 'View', - 'delete': 'Delete'} - - message = create_message('Add/Update') - 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 - mc.send_message.return_value = True - - mocker.patch.object(budget, 'budget_view') - budget.budget_view.run.return_value = True - - mocker.patch.object(budget, 'helper') - budget.helper.getBudgetOptions.return_value = { - 'update': 'Add/Update', - 'view': 'View', - 'delete': 'Delete'} - - message = create_message('View') - 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 - mc.send_message.return_value = True - - mocker.patch.object(budget, 'budget_delete') - budget.budget_delete.run.return_value = True - - mocker.patch.object(budget, 'helper') - budget.helper.getBudgetOptions.return_value = { - 'update': 'Add/Update', - 'view': 'View', - 'delete': 'Delete'} - - message = create_message('Delete') - 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 diff --git a/test/test_budget_delete.py b/test/test_budget_delete.py deleted file mode 100644 index 1a9f0ab97..000000000 --- a/test/test_budget_delete.py +++ /dev/null @@ -1,52 +0,0 @@ -""" - -MIT License - -Copyright (c) 2021 Dev Kumar - -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. - -""" -from code import budget_delete -from mock.mock import patch -from telebot import types - - -@patch('telebot.telebot') -def test_run_normal_case(mock_telebot, mocker): - mc = mock_telebot.return_value - mc.send_message.return_value = True - - mocker.patch.object(budget_delete, 'helper') - budget_delete.helper.read_json.return_value = {'11': {'budget': {'budget': 10, 'category': 100}}} - budget_delete.helper.write_json.return_value = True - - message = create_message("hello from testing") - budget_delete.run(message, mc) - - # assert(mc.reply_to.called) - mc.send_message.assert_called_with(11, 'Budget deleted!') - - -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 diff --git a/test/test_budget_update_rest.py b/test/test_budget_update_rest.py deleted file mode 100644 index f3e63bdac..000000000 --- a/test/test_budget_update_rest.py +++ /dev/null @@ -1,182 +0,0 @@ -from code import budget_update -from mock import ANY -from mock import Mock -from mock.mock import patch -from telebot import types - - -@patch('telebot.telebot') -def test_update_overall_budget_already_available_case(mock_telebot, mocker): - mc = mock_telebot.return_value - mc.send_message.return_value = True - - mocker.patch.object(budget_update, 'helper') - budget_update.helper.getCurrencies.return_value = ['USD', 'EUR', 'INR'] - - message = Mock() - message.text = 'USD' - budget_update.update_overall_budget(message, mc) - mc.reply_to.assert_called_with(message, 'Select Currency', reply_markup=ANY) - - -@patch('telebot.telebot') -def test_update_overall_budget_new_budget_case(mock_telebot, mocker): - mc = mock_telebot.return_value - mc.send_message.return_value = True - - mocker.patch.object(budget_update, 'helper') - budget_update.helper.getCurrencies.return_value = ['USD', 'EUR', 'INR'] - budget_update.helper.isOverallBudgetAvailable.return_value = True - - message = Mock() - message.text = 'USD' - budget_update.update_overall_budget(message, mc) - mc.reply_to.assert_called_with(message, 'Select Currency', reply_markup=ANY) - - -@patch('telebot.telebot') -def test_post_overall_amount_input_working(mock_telebot, mocker): - mc = mock_telebot.return_value - mc.send_message.return_value = True - - mocker.patch.object(budget_update, 'helper') - budget_update.helper.isOverallBudgetAvailable.return_value = True - budget_update.helper.validate_entered_amount.return_value = 150 - - message = create_message("hello from testing") - budget_update.post_overall_amount_input(message, mc, 'INR') - - mc.send_message.assert_called_with(11, ANY) - - -@patch('telebot.telebot') -def test_post_overall_amount_input_nonworking(mock_telebot, mocker): - mc = mock_telebot.return_value - mc.send_message.return_value = True - - mocker.patch.object(budget_update, 'helper') - budget_update.helper.getCurrencies.return_value = ['USD', 'EUR', 'INR'] - budget_update.helper.isOverallBudgetAvailable.return_value = True - - message = Mock() - message.text = 'USD' - budget_update.update_overall_budget(message, mc) - mc.reply_to.assert_called_with(message, 'Select Currency', reply_markup=ANY) - - -@patch('telebot.telebot') -def test_update_category_budget(mock_telebot, mocker): - mc = mock_telebot.return_value - reply_message = create_message("Food") - mc.reply_to.return_value = reply_message - - mocker.patch.object(budget_update, 'helper') - budget_update.helper.getSpendCategories.return_value = ['Food', 'Groceries', 'Utilities', 'Transport', 'Shopping', 'Miscellaneous'] - - message = create_message("hello from testing") - budget_update.update_category_budget(message, mc) - - mc.reply_to.assert_called_with(message, 'Select Category', reply_markup=ANY) - - -@patch('telebot.telebot') -def test_post_category_selection_category_not_found(mock_telebot, mocker): - mc = mock_telebot.return_value - mc.send_message.return_value = True - - mocker.patch.object(budget_update, 'helper') - budget_update.helper.getSpendCategories.return_value = [] - budget_update.helper.throw_exception.return_value = True - - message = create_message("hello from testing") - budget_update.post_category_selection(message, mc) - - mc.send_message.assert_called_with(11, 'Invalid', reply_markup=ANY) - assert (budget_update.helper.throw_exception.called) - - -@patch('telebot.telebot') -def test_post_category_selection_category_wise_case(mock_telebot, mocker): - mc = mock_telebot.return_value - reply_message = create_message("Food") - mc.reply_to.return_value = reply_message - - mocker.patch.object(budget_update, 'helper') - budget_update.helper.getSpendCategories.return_value = ['Food', 'Groceries', 'Utilities', 'Transport', 'Shopping', 'Miscellaneous'] - budget_update.helper.getCurrencies.return_value = ['USD', 'EUR', 'GBP', 'INR', 'JPY'] - - message = create_message("Food") - budget_update.post_category_selection(message, mc) - - mc.reply_to.assert_called_with(message, 'Select Currency', reply_markup=ANY) - - -@patch('telebot.telebot') -def test_post_category_selection_overall_case(mock_telebot, mocker): - mc = mock_telebot.return_value - reply_message = create_message("Food") - mc.reply_to.return_value = reply_message - - mocker.patch.object(budget_update, 'helper') - budget_update.helper.getSpendCategories.return_value = ['Food', 'Groceries', 'Utilities', 'Transport', 'Shopping', 'Miscellaneous'] - budget_update.helper.getCurrencies.return_value = ['USD', 'EUR', 'GBP', 'INR', 'JPY'] - - message = create_message("Food") - budget_update.post_category_selection(message, mc) - - - mc.reply_to.assert_called_with(message, 'Select Currency', reply_markup=ANY) - - -@patch('telebot.telebot') -def test_post_category_amount_input_working(mock_telebot, mocker): - mc = mock_telebot.return_value - mc.send_message.return_value = True - - mocker.patch.object(budget_update, 'helper') - budget_update.helper.validate_entered_amount.return_value = 100 - - message = create_message("Hello from testing") - budget_update.post_category_amount_input(message, mc, "Food", 'INR') - - mc.send_message.assert_called_with(11, 'Budget for Food Created!') - - -@patch('telebot.telebot') -def test_post_category_amount_input_nonworking_case(mock_telebot, mocker): - mc = mock_telebot.return_value - reply_message = create_message("100") - mc.send_message.return_value = reply_message - - mocker.patch.object(budget_update, 'helper') - mocker.patch.object(budget_update, 'currencies') - budget_update.helper.validate_entered_amount.return_value = 100 - budget_update.currencies.convert.return_value = 73.5 - budget_update.helper.read_json.return_value = {} - budget_update.helper.createNewUserRecord.return_value = {'budget': {'category': None}} - budget_update.helper.write_json.return_value = True - - message = create_message("Food") - budget_update.post_category_amount_input(message, mc, "Food", "INR") - - mc.send_message.assert_called_with(11, 'Budget for Food Created!') - assert (budget_update.helper.write_json.called) - - -@patch('telebot.telebot') -def test_post_category_add(mock_telebot, mocker): - mc = mock_telebot.return_value - mc.reply_to.return_value = True - - message = create_message("hello from testing!") - budget_update.post_category_add(message, mc) - - mc.reply_to.assert_called_with(message, 'Select Option', reply_markup=ANY) - - -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 diff --git a/test/test_budget_update_run.py b/test/test_budget_update_run.py deleted file mode 100644 index ca5098de1..000000000 --- a/test/test_budget_update_run.py +++ /dev/null @@ -1,153 +0,0 @@ -""" - -MIT License - -Copyright (c) 2021 Dev Kumar - -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. - -""" - -from code import budget_update -import mock -from mock import ANY -from mock.mock import patch -from telebot import types - - -@patch('telebot.telebot') -def test_run_overall_budget_overall_case(mock_telebot, mocker): - mc = mock_telebot.return_value - reply_message = create_message("USD") - mc.reply_to.return_value = reply_message - - mocker.patch.object(budget_update, 'helper') - budget_update.helper.getCurrencies.return_value = ['USD', 'EUR', 'GBP', 'INR', 'JPY'] - - message = create_message("hello from testing") - budget_update.update_overall_budget(message, mc) - - mc.reply_to.assert_called_with(message, 'Select Currency', reply_markup=ANY) - - -@patch('telebot.telebot') -def test_run_overall_budget_category_case(mock_telebot, mocker): - mc = mock_telebot.return_value - reply_message = create_message("Food") - mc.reply_to.return_value = reply_message - - mocker.patch.object(budget_update, 'helper') - budget_update.helper.getSpendCategories.return_value = ['Food', 'Groceries', 'Utilities', 'Transport', 'Shopping', 'Miscellaneous'] - - message = create_message("hello from testing") - budget_update.update_category_budget(message, mc) - - mc.reply_to.assert_called_with(message, 'Select Category', reply_markup=ANY) - - -@patch('telebot.telebot') -def test_run_overall_budget_new_budget_case(mock_telebot, mocker): - mc = mock_telebot.return_value - mc.reply_to.return_value = True - mocker.patch.object(budget_update, 'helper') - budget_update.helper.isOverallBudgetAvailable.return_value = False - budget_update.helper.isCategoryBudgetAvailable.return_value = False - - message = create_message("hello from testing") - budget_update.run(message, mc) - - assert (mc.reply_to.called) - mc.reply_to.assert_called_with(message, 'Select Budget Type', reply_markup=ANY) - - -@patch('telebot.telebot') -def test_post_type_selection_failing_case(mock_telebot, mocker): - mc = mock_telebot.return_value - mc.send_message.return_value = True - mocker.patch.object(budget_update, 'helper') - budget_update.helper.getBudgetTypes.return_value = {} - budget_update.helper.throw_exception.return_value = True - - # budget_update.update_overall_budget = mock.Mock(return_value=True) - message = create_message("hello from testing") - budget_update.post_type_selection(message, mc) - assert (mc.send_message.called) - assert (budget_update.helper.throw_exception.called) - - -@patch('telebot.telebot') -def test_post_type_selection_overall_budget_case(mock_telebot, mocker): - mc = mock_telebot.return_value - - mocker.patch.object(budget_update, 'helper') - budget_update.helper.getBudgetTypes.return_value = { - 'overall': 'Overall Budget', - 'category': 'Category-Wise Budget' - } - - budget_update.update_overall_budget = mock.Mock(return_value=True) - message = create_message("Overall Budget") - budget_update.post_type_selection(message, mc) - assert (budget_update.update_overall_budget.called) - - -@patch('telebot.telebot') -def test_post_type_selection_categorywise_budget_case(mock_telebot, mocker): - mc = mock_telebot.return_value - - mocker.patch.object(budget_update, 'helper') - budget_update.helper.getBudgetTypes.return_value = { - 'overall': 'Overall Budget', - 'category': 'Category-Wise Budget' - } - - budget_update.update_category_budget = mock.Mock(return_value=True) - message = create_message("Category-Wise Budget") - budget_update.post_type_selection(message, mc) - assert (budget_update.update_category_budget.called) - - -@patch('telebot.telebot') -def test_post_option_selectio_working(mock_telebot, mocker): - mc = mock_telebot.return_value - budget_update.update_category_budget = mock.Mock(return_value=True) - - message = create_message("Continue") - budget_update.post_option_selection(message, mc) - - assert (budget_update.update_category_budget.called) - - -@patch('telebot.telebot') -def test_post_option_selection_nonworking(mock_telebot, mocker): - mc = mock_telebot.return_value - budget_update.update_category_budget = mock.Mock(return_value=True) - - message = create_message("Randomtext") - budget_update.post_option_selection(message, mc) - - assert (budget_update.update_category_budget.called is False) - - -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 diff --git a/test/test_budget_view.py b/test/test_budget_view.py deleted file mode 100644 index 0f8cbb07d..000000000 --- a/test/test_budget_view.py +++ /dev/null @@ -1,106 +0,0 @@ -""" - -MIT License - -Copyright (c) 2021 Dev Kumar - -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. - -""" - -from code import budget_view -import mock -from mock import ANY -from mock.mock import patch -from telebot import types - - -@patch('telebot.telebot') -def test_display_overall_budget(mock_telebot, mocker): - mc = mock_telebot.return_value - mc.send_message.return_value = True - mocker.patch.object(budget_view, 'helper') - budget_view.helper.getOverallBudget.return_value = "" - message = create_message("hello from testing") - budget_view.display_overall_budget(message, mc) - assert (mc.send_message.called) - #mc.send_message.called_with(11, ANY) - - -@patch('telebot.telebot') -def test_display_category_budget(mock_telebot, mocker): - mc = mock_telebot.return_value - mc.send_message.return_value = True - mocker.patch.object(budget_view, 'helper') - budget_view.helper.getCategoryBudget.return_value = {'items': ""} - message = create_message("hello from testing") - budget_view.display_category_budget(message, mc) - assert (mc.send_message.called) - #mc.send_message.called_with(11, ANY) - - -@patch('telebot.telebot') -def test_run_overall_budget(mock_telebot, mocker): - mc = mock_telebot.return_value - - mocker.patch.object(budget_view, 'helper') - budget_view.helper.isOverallBudgetAvailable.return_value = True - - budget_view.display_overall_budget = mock.Mock(return_value=True) - message = create_message("hello from testing") - budget_view.run(mc, message) - - assert (budget_view.display_overall_budget.called) - - -@patch('telebot.telebot') -def test_run_category_budget(mock_telebot, mocker): - mc = mock_telebot.return_value - - mocker.patch.object(budget_view, 'helper') - budget_view.helper.isCategoryBudgetAvailable.return_value = True - budget_view.helper.isOverallBudgetAvailable.return_value = False - - budget_view.display_category_budget = mock.Mock(return_value=True) - - message = create_message("hello from testing") - budget_view.run(mc, message) - assert (budget_view.display_category_budget.called) - - -@patch('telebot.telebot') -def test_run_failing_case(mock_telebot, mocker): - mc = mock_telebot.return_value - - mocker.patch.object(budget_view, 'helper') - budget_view.helper.isCategoryBudgetAvailable.return_value = False - budget_view.helper.isOverallBudgetAvailable.return_value = False - budget_view.helper.throw_exception.return_value = True - - message = create_message("hello from testing") - budget_view.run(mc, message) - assert (budget_view.helper.throw_exception.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 diff --git a/test/test_category.py b/test/test_category.py deleted file mode 100644 index dd1ea4aa8..000000000 --- a/test/test_category.py +++ /dev/null @@ -1,73 +0,0 @@ -""" - -MIT License - -Copyright (c) 2021 Dev Kumar - -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 os -import json -import mock -from mock.mock import patch -from telebot import types -from code import category -from mock import ANY - - -dateFormat = '%d-%b-%Y' -timeFormat = '%H:%M' -monthFormat = '%b-%Y' - - -@patch('telebot.telebot') -def test_run(mock_telebot, mocker): - mc = mock_telebot.return_value - mc.reply_to.return_value = True - message = create_message("hello from test run!") - category.run(message, mc) - #assert (mc.reply_to.called_with(ANY, 'Select Operation', ANY)) - -@patch('telebot.telebot') -def test_post_operation_selection_working(mock_telebot, mocker): - mc = mock_telebot.return_value - mc.send_message.return_value = True - - message = create_message("hello from testing!") - category.post_operation_selection(message, mc) - assert (mc.send_message.called) - -@patch('telebot.telebot') -def test_post_operation_selection_noMatchingCategory(mock_telebot, mocker): - mc = mock_telebot.return_value - mc.send_message.return_value = True - - mocker.patch.object(category, 'helper') - category.helper.getCategoryOptions.return_value = {} - - message = create_message("hello from test_category.py!") - category.post_operation_selection(message, mc) - mc.send_message.assert_called_with(11, 'Invalid', reply_markup=mock.ANY) - -def create_message(text): - params = {'messagebody': text} - chat = types.User(11, False, 'test') - return types.Message(1, None, None, chat, 'text', params, "") diff --git a/test/test_delete.py b/test/test_delete.py deleted file mode 100644 index 76c08deda..000000000 --- a/test/test_delete.py +++ /dev/null @@ -1,79 +0,0 @@ -""" - -MIT License - -Copyright (c) 2021 Dev Kumar - -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 os -import json -from code import delete -from mock import patch -from telebot import types - - -def test_read_json(): - try: - if not os.path.exists('./test/dummy_expense_record.json'): - with open('./test/dummy_expense_record.json', 'w') as json_file: - json_file.write('{}') - return json.dumps('{}') - elif os.stat('./test/dummy_expense_record.json').st_size != 0: - with open('./test/dummy_expense_record.json') as expense_record: - expense_record_data = json.load(expense_record) - return expense_record_data - - except FileNotFoundError: - print("---------NO RECORDS FOUND---------") - - -def create_message(text): - params = {'messagebody': text} - chat = types.User("894127939", False, 'test') - return types.Message(1, None, None, chat, 'text', params, "") - - -@patch('telebot.telebot') -def test_delete_run_with_data(mock_telebot, mocker): - MOCK_USER_DATA = test_read_json() - mocker.patch.object(delete, 'helper') - delete.helper.read_json.return_value = MOCK_USER_DATA - print("Hello", MOCK_USER_DATA) - delete.helper.write_json.return_value = True - MOCK_Message_data = create_message("Hello") - mc = mock_telebot.return_value - mc.send_message.return_value = True - delete.run(MOCK_Message_data, mc) - assert (delete.helper.write_json.called) - - -@patch('telebot.telebot') -def test_delete_with_no_data(mock_telebot, mocker): - mocker.patch.object(delete, 'helper') - delete.helper.read_json.return_value = {} - delete.helper.write_json.return_value = True - MOCK_Message_data = create_message("Hello") - mc = mock_telebot.return_value - mc.send_message.return_value = True - delete.run(MOCK_Message_data, mc) - if delete.helper.write_json.called is False: - assert True diff --git a/test/test_display.py b/test/test_display.py deleted file mode 100644 index 01d32994a..000000000 --- a/test/test_display.py +++ /dev/null @@ -1,131 +0,0 @@ -import os -import json -from mock import patch -from telebot import types -from code import display - - -@patch('telebot.telebot') -def test_run(mock_telebot, mocker): - mc = mock_telebot.return_value - mc.reply_to.return_value = True - message = create_message("hello from test run!") - display.run(message, mc) - assert mc.send_message.called - - -@patch('telebot.telebot') -def test_no_data_available(mock_telebot, mocker): - mc = mock_telebot.return_value - mc.reply_to.return_value = True - message = create_message("/spendings") - display.run(message, mc) - assert mc.send_message.called - - -@patch('telebot.telebot') -def test_invalid_format(mock_telebot, mocker): - mc = mock_telebot.return_value - mc.reply_to.return_value = True - message = create_message("luster") - try: - display.display_total(message, mc) - assert False - except Exception: - assert True - - -@patch('telebot.telebot') -def test_valid_format(mock_telebot, mocker): - mc = mock_telebot.return_value - mc.reply_to.return_value = True - message = create_message("Month") - try: - display.display_total(message, mc) - assert True - except Exception: - assert False - - -@patch('telebot.telebot') -def test_valid_format_day(mock_telebot, mocker): - mc = mock_telebot.return_value - mc.reply_to.return_value = True - message = create_message("Day") - try: - display.display_total(message, mc) - assert True - except Exception: - assert False - - -@patch('telebot.telebot') -def test_spending_run_working(mock_telebot, mocker): - MOCK_USER_DATA = test_read_json() - mocker.patch.object(display, 'helper') - display.helper.getUserHistory.return_value = MOCK_USER_DATA["894127939"] - display.helper.getSpendDisplayOptions.return_value = [ - "Day", "Month"] - display.helper.getDateFormat.return_value = '%d-%b-%Y' - display.helper.getMonthFormat.return_value = '%b-%Y' - mc = mock_telebot.return_value - mc.reply_to.return_value = True - message = create_message("Day") - message.text = "Day" - display.run(message, mc) - assert not mc.send_message.called - - -@patch('telebot.telebot') -def test_spending_display_working(mock_telebot, mocker): - MOCK_USER_DATA = test_read_json() - mocker.patch.object(display, 'helper') - display.helper.getUserHistory.return_value = MOCK_USER_DATA["894127939"] - display.helper.getSpendDisplayOptions.return_value = [ - "Day", "Month"] - display.helper.getDateFormat.return_value = '%d-%b-%Y' - display.helper.getMonthFormat.return_value = '%b-%Y' - mc = mock_telebot.return_value - mc.reply_to.return_value = True - message = create_message("Day") - message.text = "Day" - display.display_total(message, mc) - assert mc.send_message.called - - -@patch('telebot.telebot') -def test_spending_display_month(mock_telebot, mocker): - MOCK_USER_DATA = test_read_json() - mocker.patch.object(display, 'helper') - display.helper.getUserHistory.return_value = MOCK_USER_DATA["894127939"] - display.helper.getSpendDisplayOptions.return_value = [ - "Day", "Month"] - display.helper.getDateFormat.return_value = '%d-%b-%Y' - display.helper.getMonthFormat.return_value = '%b-%Y' - mc = mock_telebot.return_value - mc.reply_to.return_value = True - message = create_message("Month") - message.text = "Month" - display.display_total(message, mc) - assert mc.send_message.called - - -def create_message(text): - params = {'messagebody': text} - chat = types.User(11, False, 'test') - return types.Message(894127939, None, None, chat, 'text', params, "") - - -def test_read_json(): - try: - if not os.path.exists('./test/dummy_expense_record.json'): - with open('./test/dummy_expense_record.json', 'w') as json_file: - json_file.write('{}') - return json.dumps('{}') - elif os.stat('./test/dummy_expense_record.json').st_size != 0: - with open('./test/dummy_expense_record.json') as expense_record: - expense_record_data = json.load(expense_record) - return expense_record_data - - except FileNotFoundError: - print("---------NO RECORDS FOUND---------") diff --git a/test/test_download_csv.py b/test/test_download_csv.py deleted file mode 100644 index e09524d03..000000000 --- a/test/test_download_csv.py +++ /dev/null @@ -1,61 +0,0 @@ -""" - -MIT License - -Copyright (c) 2021 Dev Kumar - -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 unittest -from unittest.mock import MagicMock, patch -from code import download_csv - -@patch('code.download_csv.helper.getUserHistory', return_value=[['2023-01-01', 'Food', '20', 'Credit']]) -@patch('telebot.telebot') -def test_run_successful(mock_bot, mock_get_user_history): - message = MagicMock() - result = download_csv.run(message, mock_bot) - - mock_bot.send_document.assert_called_once() - mock_get_user_history.assert_called_once() - -@patch('code.download_csv.helper.getUserHistory', return_value=None) -@patch('telebot.telebot') -def test_run_no_history(mock_bot, mock_get_user_history): - message = MagicMock() - result = download_csv.run(message, mock_bot) - - assert result is None - mock_bot.send_message.assert_called_once() - mock_get_user_history.assert_called_once() - -@patch('code.download_csv.helper.getUserHistory', side_effect=FileNotFoundError("File not found")) -@patch('telebot.telebot') -def test_run_file_not_found_error(mock_bot, mock_get_user_history): - message = MagicMock() - result = download_csv.run(message, mock_bot) - - assert result is None - mock_bot.send_message.assert_called_once() - mock_get_user_history.assert_called_once() - -if __name__ == '__main__': - unittest.main() diff --git a/test/test_download_pdf.py b/test/test_download_pdf.py deleted file mode 100644 index 07880325e..000000000 --- a/test/test_download_pdf.py +++ /dev/null @@ -1,31 +0,0 @@ -import unittest -from unittest.mock import Mock, patch -from code import download_pdf - - -@patch('code.download_pdf.plt.subplots') -def test_generate_expense_history_plot_no_records(mock_subplots): - user_history = [] - fig, ax = Mock(), Mock() - mock_subplots.return_value = (fig, ax) - - result = download_pdf.generate_expense_history_plot(user_history) - - assert result is not None - ax.text.assert_called_once() - - -@patch('code.download_pdf.plt.subplots') -def test_generate_expense_history_plot_with_records(mock_subplots): - user_history = ["2023-01-01,Food,50,Cash", "2023-01-02,Transport,20,Card"] - fig, ax = Mock(), Mock() - mock_subplots.return_value = (fig, ax) - - result = download_pdf.generate_expense_history_plot(user_history) - - assert result is not None - ax.text.assert_called() - - -if __name__ == '__main__': - unittest.main() diff --git a/test/test_edit.py b/test/test_edit.py deleted file mode 100644 index 029dfc5a9..000000000 --- a/test/test_edit.py +++ /dev/null @@ -1,157 +0,0 @@ -""" - -MIT License - -Copyright (c) 2021 Dev Kumar - -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 datetime - -from mock import patch -from telebot import types -from code import edit - -MOCK_CHAT_ID = 101 -MOCK_USER_DATA = { - str(MOCK_CHAT_ID): {'data': ["correct_mock_value"]}, - '102': {"data": ["wrong_mock_value"]} -} - -DUMMY_DATE = str(datetime.datetime.now()) - - -@patch('telebot.telebot') -def test_run(mock_telebot, mocker): - mc = mock_telebot.return_value - mc.reply_to.return_value = True - mocker.patch.object(edit, 'helper') - message = create_message("hello from test run!") - edit.helper.getUserHistory(message.chat.id).return_value = MOCK_USER_DATA[str(MOCK_CHAT_ID)]['data'] - edit.run(message, mc) - assert mc.reply_to.called - - -@patch('telebot.telebot') -def test_select_category_to_be_updated(mock_telebot, mocker): - mc = mock_telebot.return_value - mc.reply_to.return_value = True - message = create_message("hello from testing!") - edit.select_category_to_be_updated(message, mc) - assert mc.reply_to.called - - -@patch('telebot.telebot') -def test_select_category_selection_no_matching_choices(mock_telebot, mocker): - mc = mock_telebot.return_value - mc.send_message.return_value = [] - mc.reply_to.return_value = True - mocker.patch.object(edit, 'helper') - edit.helper.getChoices().return_value = None - message = create_message("hello from testing!") - edit.select_category_to_be_updated(message, mc) - assert mc.reply_to.called - - -@patch('telebot.telebot') -def test_post_category_selection_no_matching_category(mock_telebot, mocker): - mc = mock_telebot.return_value - mc.send_message.return_value = [] - mc.reply_to.return_value = True - mocker.patch.object(edit, 'helper') - edit.helper.getSpendCategories.return_value = None - message = create_message("hello from testing!") - edit.select_category_to_be_updated(message, mc) - assert mc.reply_to.called - - -@patch('telebot.telebot') -def test_post_amount_input_nonworking(mock_telebot, mocker): - mc = mock_telebot.return_value - mc.send_message.return_value = True - mc.reply_to.return_value = True - mocker.patch.object(edit, 'helper') - edit.helper.validate_entered_amount.return_value = 0 - message = create_message("hello from testing!") - edit.select_category_to_be_updated(message, mc) - assert mc.reply_to.called - - -@patch('telebot.telebot') -def test_enter_updated_data(mock_telebot, mocker): - mc = mock_telebot.return_value - mc.reply_to.return_value = True - mocker.patch.object(edit, 'helper') - edit.helper.getSpendCategories.return_value = [] - message = create_message("hello from testing!") - selected_data = MOCK_USER_DATA[str(MOCK_CHAT_ID)]['data'][0] - edit.enter_updated_data(message, mc, selected_data) - assert not mc.reply_to.called - - -@patch('telebot.telebot') -def test_edit_date(mock_telebot, mocker): - mc = mock_telebot.return_value - mc.reply_to.return_value = True - mocker.patch.object(edit, 'helper') - edit.helper.read_json().return_value = MOCK_USER_DATA - edit.helper.getUserHistory(MOCK_CHAT_ID).return_value = MOCK_USER_DATA[str(MOCK_CHAT_ID)]['data'] - message = create_message("hello from testing!") - message.text = DUMMY_DATE - message.chat.id = MOCK_CHAT_ID - selected_data = MOCK_USER_DATA[str(MOCK_CHAT_ID)]['data'][0] - edit.edit_date(message, mc, selected_data) - assert mc.reply_to.called - - -@patch('telebot.telebot') -def test_edit_category(mock_telebot, mocker): - mc = mock_telebot.return_value - mc.reply_to.return_value = True - mocker.patch.object(edit, 'helper') - edit.helper.read_json().return_value = MOCK_USER_DATA - edit.helper.getUserHistory(MOCK_CHAT_ID).return_value = MOCK_USER_DATA[str(MOCK_CHAT_ID)]['data'] - message = create_message("hello from testing!") - message.chat.id = MOCK_CHAT_ID - selected_data = MOCK_USER_DATA[str(MOCK_CHAT_ID)]['data'][0] - edit.edit_cat(message, mc, selected_data) - assert mc.reply_to.called - - -@patch('telebot.telebot') -def test_edit_cost(mock_telebot, mocker): - mc = mock_telebot.return_value - mc.reply_to.return_value = True - mocker.patch.object(edit, 'helper') - edit.helper.read_json().return_value = MOCK_USER_DATA - edit.helper.getUserHistory(MOCK_CHAT_ID).return_value = MOCK_USER_DATA[str(MOCK_CHAT_ID)]['data'] - edit.helper.validate_entered_amount.return_value = 0 - message = create_message("hello from testing!") - message.chat.id = MOCK_CHAT_ID - selected_data = MOCK_USER_DATA[str(MOCK_CHAT_ID)]['data'][0] - edit.edit_cost(message, mc, selected_data) - assert mc.reply_to.called - - -def create_message(text): - params = {'messagebody': text} - chat = types.User(11, False, 'test') - return types.Message(1, None, None, chat, 'text', params, "") diff --git a/test/test_email_history.py b/test/test_email_history.py deleted file mode 100644 index b686983bd..000000000 --- a/test/test_email_history.py +++ /dev/null @@ -1,112 +0,0 @@ -""" - -MIT License - -Copyright (c) 2021 Dev Kumar - -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 smtplib -import unittest -from unittest.mock import patch, Mock, mock_open, call -import tempfile -from code import email_history -import os -from mock import mock_open, patch - -@patch('code.email_history.smtplib.SMTP') -def test_send_email_successful(mock_smtp): - mock_server = Mock() - mock_smtp.return_value = mock_server - - recipient_email = "example@example.com" - email_subject = "Test Subject" - email_body = "Test Message" - attachment_path = 'test_attachment.txt' - - with open('test_attachment.txt', 'w') as dummy_file: - dummy_file.write("This is a test attachment.") - - try: - email_history.send_email(mock_server, recipient_email, email_subject, email_body, attachment_path) - - mock_server.sendmail.assert_called_once_with("dollarbotnoreply@gmail.com", recipient_email, mock_server.sendmail.call_args[0][2]) - finally: - os.remove('test_attachment.txt') - - -@patch('code.email_history.smtplib.SMTP') -def test_send_email_failure(mock_smtp): - mock_server = Mock() - mock_smtp.return_value = mock_server - - recipient_email = "example@example.com" - email_subject = "Test Subject" - email_body = "Test Message" - attachment_path = 'test_attachment.txt' - - with open('test_attachment.txt', 'w') as dummy_file: - dummy_file.write("This is a test attachment.") - - try: - email_history.send_email(mock_server, recipient_email, email_subject, email_body, attachment_path) - - mock_server.sendmail.assert_called_once_with("dollarbotnoreply@gmail.com", recipient_email, mock_server.sendmail.call_args[0][2]) - finally: - os.remove('test_attachment.txt') - - -@patch('code.email_history.smtplib.SMTP') -def test_close_smtp_connection_successful(mock_smtp): - smtp_server = Mock() - email_history.close_smtp_connection(smtp_server) - smtp_server.quit.assert_called_once() - -@patch('smtplib.SMTP') -@patch('code.email_history.logging') -def test_connect_to_smtp_server_success(mock_logging, mock_smtp): - # Arrange - smtp_instance = mock_smtp.return_value - - # Act - result = email_history.connect_to_smtp_server() - - # Assert - smtp_instance.starttls.assert_called_once() - smtp_instance.login.assert_called_once_with("dollarbotnoreply@gmail.com", "ogrigybfufihnkcc") - mock_logging.info.assert_called_with("Successfully connected to the SMTP server") - assert result == smtp_instance - -@patch('smtplib.SMTP') -@patch('code.email_history.logging') -def test_connect_to_smtp_server_failure(mock_logging, mock_smtp): - - mock_smtp.side_effect = smtplib.SMTPException('Test SMTPException') - - # Act/Assert - with unittest.TestCase().assertRaises(smtplib.SMTPException) as context: - email_history.connect_to_smtp_server() - - mock_logging.error.assert_called_with('SMTP Exception: Test SMTPException') - assert str(context.exception) == 'Test SMTPException' - -if __name__ == '__main__': - unittest.main() diff --git a/test/test_estimate.py b/test/test_estimate.py deleted file mode 100644 index 937a69a02..000000000 --- a/test/test_estimate.py +++ /dev/null @@ -1,142 +0,0 @@ -""" - -MIT License - -Copyright (c) 2021 Dev Kumar - -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 os -import json -from mock import patch -from telebot import types -from code import estimate - - -@patch('telebot.telebot') -def test_run(mock_telebot, mocker): - mc = mock_telebot.return_value - mc.reply_to.return_value = True - message = create_message("hello from test run!") - estimate.run(message, mc) - assert mc.send_message.called - - -@patch('telebot.telebot') -def test_no_data_available(mock_telebot, mocker): - mc = mock_telebot.return_value - mc.reply_to.return_value = True - message = create_message("/spendings") - estimate.run(message, mc) - assert mc.send_message.called - - -@patch('telebot.telebot') -def test_invalid_format(mock_telebot, mocker): - mc = mock_telebot.return_value - mc.reply_to.return_value = True - message = create_message("luster") - try: - estimate.estimate_total(message, mc) - assert False - except Exception: - assert True - - -@patch('telebot.telebot') -def test_valid_format(mock_telebot, mocker): - mc = mock_telebot.return_value - mc.reply_to.return_value = True - message = create_message("Next month") - try: - estimate.estimate_total(message, mc) - assert True - except Exception: - assert False - - -@patch('telebot.telebot') -def test_valid_format_day(mock_telebot, mocker): - mc = mock_telebot.return_value - mc.reply_to.return_value = True - message = create_message("Next day") - try: - estimate.estimate_total(message, mc) - assert True - except Exception: - assert False - - -@patch('telebot.telebot') -def test_spending_estimate_working(mock_telebot, mocker): - - MOCK_USER_DATA = test_read_json() - mocker.patch.object(estimate, 'helper') - estimate.helper.getUserHistory.return_value = MOCK_USER_DATA["894127939"] - estimate.helper.getSpendEstimateOptions.return_value = [ - "Next day", "Next month"] - estimate.helper.getDateFormat.return_value = '%d-%b-%Y' - estimate.helper.getMonthFormat.return_value = '%b-%Y' - mc = mock_telebot.return_value - mc.reply_to.return_value = True - message = create_message("Next day") - message.text = "Next day" - estimate.estimate_total(message, mc) - assert mc.send_message.called - - -@patch('telebot.telebot') -def test_spending_estimate_month(mock_telebot, mocker): - - MOCK_USER_DATA = test_read_json() - mocker.patch.object(estimate, 'helper') - estimate.helper.getUserHistory.return_value = MOCK_USER_DATA["894127939"] - estimate.helper.getSpendEstimateOptions.return_value = [ - "Next day", "Next month"] - estimate.helper.getDateFormat.return_value = '%d-%b-%Y' - estimate.helper.getMonthFormat.return_value = '%b-%Y' - mc = mock_telebot.return_value - mc.reply_to.return_value = True - message = create_message("Next month") - message.text = "Next month" - estimate.estimate_total(message, mc) - assert mc.send_message.called - - -def create_message(text): - params = {'messagebody': text} - chat = types.User(11, False, 'test') - return types.Message(894127939, None, None, chat, 'text', params, "") - - -def test_read_json(): - try: - if not os.path.exists('./test/dummy_expense_record.json'): - with open('./test/dummy_expense_record.json', 'w') as json_file: - json_file.write('{}') - return json.dumps('{}') - elif os.stat('./test/dummy_expense_record.json').st_size != 0: - with open('./test/dummy_expense_record.json') as expense_record: - expense_record_data = json.load(expense_record) - return expense_record_data - - except FileNotFoundError: - print("---------NO RECORDS FOUND---------") diff --git a/test/test_graph.py b/test/test_graph.py deleted file mode 100644 index 9d193497f..000000000 --- a/test/test_graph.py +++ /dev/null @@ -1,50 +0,0 @@ -""" - -MIT License - -Copyright (c) 2021 Dev Kumar - -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. - -""" - -from code import graphing -from mock import ANY - -dummy_total_text_none = "" -dummy_total_text_data = """Food $10.0 -Transport $50.0 -Shopping $148.0 -Miscellaneous $47.93 -Utilities $200.0 -Groceries $55.21\n""" -dummy_budget = "100.0" - -dummy_x = ['Food', 'Transport', 'Shopping', 'Miscellaneous', 'Utilities', 'Groceries'] -dummy_y = [10.0, 50.0, 148.0, 47.93, 200.0, 55.21] -dummy_categ_val = {'Food': 10.0, 'Transport': 50.0, 'Shopping': 148.0, 'Miscellaneous': 47.93, 'Miscellaneous': 47.93, 'Utilities': 200.0, 'Groceries': 55.21} -dummy_color = ['red', 'cornflowerblue', 'greenyellow', 'orange', 'violet', 'grey'] -dummy_edgecolor = 'black' - - -def test_visualize(mocker): - mocker.patch.object(graphing, 'plt') - graphing.plt.bar.return_value = True - graphing.visualize(dummy_total_text_data, dummy_budget) - graphing.plt.bar.assert_called_with(dummy_categ_val.keys(), ANY, color=dummy_color, edgecolor=dummy_edgecolor) diff --git a/test/test_helper.py b/test/test_helper.py deleted file mode 100644 index d045f86e5..000000000 --- a/test/test_helper.py +++ /dev/null @@ -1,469 +0,0 @@ -""" - -MIT License - -Copyright (c) 2021 Dev Kumar - -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. - -""" - -from code import helper -from code.helper import isCategoryBudgetByCategoryAvailable, throw_exception -from mock import ANY -from telebot import types -from mock.mock import patch -import logging -import mock - -MOCK_CHAT_ID = 101 -MOCK_USER_DATA = { - str(MOCK_CHAT_ID): { - 'data': ["correct_mock_value"], - 'budget': { - 'overall': None, - 'category': None - }, - "reminder": { - "type": "Day", - "time": "21:02" - } - }, - '102': { - 'data': ["wrong_mock_value"], - 'budget': { - 'overall': None, - 'category': None - }, - "reminder": { - "type": "Day", - "time": "21:02" - } - } -} - - -def test_validate_entered_amount_none(): - result = helper.validate_entered_amount(None) - if result: - assert False, 'None is not a valid amount' - else: - assert True - - -def test_validate_entered_amount_int(): - val = '101' - result = helper.validate_entered_amount(val) - if result: - assert True - else: - assert False, val + ' is valid amount' - - -def test_validate_entered_amount_int_max(): - val = '999999999999999' - result = helper.validate_entered_amount(val) - if result: - assert True - else: - assert False, val + ' is valid amount' - - -def test_validate_entered_amount_int_outofbound(): - val = '9999999999999999' - result = helper.validate_entered_amount(val) - if result: - assert False, val + ' is not a valid amount(out of bound)' - else: - assert True - - -def test_validate_entered_amount_float(): - val = '101.11' - result = helper.validate_entered_amount(val) - if result: - assert True - else: - assert False, val + ' is valid amount' - - -def test_validate_entered_amount_float_max(): - val = '999999999999999.9999' - result = helper.validate_entered_amount(val) - if result: - assert True - else: - assert False, val + ' is valid amount' - - -def test_validate_entered_amount_float_more_decimal(): - val = '9999999999.999999999' - result = helper.validate_entered_amount(val) - if result: - assert True - else: - assert False, val + ' is valid amount' - - -def test_validate_entered_amount_float_outofbound(): - val = '9999999999999999.99' - result = helper.validate_entered_amount(val) - if result: - assert False, val + ' is not a valid amount(out of bound)' - else: - assert True - - -def test_validate_entered_amount_string(): - val = 'agagahaaaa' - result = helper.validate_entered_amount(val) - if result: - assert False, val + ' is not a valid amount' - else: - assert True - - -def test_validate_entered_amount_string_with_dot(): - val = 'agaga.aaa' - result = helper.validate_entered_amount(val) - if result: - assert False, val + ' is not a valid amount' - else: - assert True - - -def test_validate_entered_amount_special_char(): - val = '$%@*@.@*' - result = helper.validate_entered_amount(val) - if result: - assert False, val + ' is not a valid amount' - else: - assert True - - -def test_validate_entered_amount_alpha_num(): - val = '22e62a' - result = helper.validate_entered_amount(val) - if result: - assert False, val + ' is not a valid amount' - else: - assert True - - -def test_validate_entered_amount_mixed(): - val = 'a14&^%.hs827' - result = helper.validate_entered_amount(val) - if result: - assert False, val + ' is not a valid amount' - else: - assert True - - -def test_getUserHistory_without_data(mocker): - mocker.patch.object(helper, 'read_json') - helper.read_json.return_value = {} - result = helper.getUserHistory(MOCK_CHAT_ID) - if result is None: - assert True - else: - assert False, 'Result is not None when user data does not exist' - - -def test_getUserHistory_with_data(mocker): - mocker.patch.object(helper, 'read_json') - helper.read_json.return_value = MOCK_USER_DATA - result = helper.getUserHistory(MOCK_CHAT_ID) - if result == MOCK_USER_DATA[str(MOCK_CHAT_ID)]['data']: - assert True - else: - assert False, 'User data is available but not found' - - -def test_getUserHistory_with_none(mocker): - mocker.patch.object(helper, 'read_json') - helper.read_json.return_value = None - result = helper.getUserHistory(MOCK_CHAT_ID) - if result is None: - assert True - else: - assert False, 'Result is not None when the file does not exist' - - -def test_getSpendCategories(): - result = helper.getSpendCategories() - if result == helper.spend_categories: - assert True - else: - assert False, 'expected spend categories are not returned' - - -def test_getSpendDisplayOptions(): - result = helper.getSpendDisplayOptions() - if result == helper.spend_display_option: - assert True - else: - assert False, 'expected spend display options are not returned' - - -def test_getCommands(): - result = helper.getCommands() - if result == helper.commands: - assert True - else: - assert False, 'expected commands are not returned' - - -def test_getDateFormat(): - result = helper.getDateFormat() - if result == helper.dateFormat: - assert True - else: - assert False, 'expected date format are not returned' - - -def test_getTimeFormat(): - result = helper.getTimeFormat() - if result == helper.timeFormat: - assert True - else: - assert False, 'expected time format are not returned' - - -def test_getMonthFormat(): - result = helper.getMonthFormat() - if result == helper.monthFormat: - assert True - else: - assert False, 'expected month format are not returned' - - -def test_getChoices(): - result = helper.getChoices() - if result == helper.choices: - assert True - else: - assert False, 'expected choices are not returned' - - -def test_write_json(mocker): - mocker.patch.object(helper, 'json') - helper.json.dump.return_value = True - user_list = ['hello'] - helper.write_json(user_list) - helper.json.dump.assert_called_with(user_list, ANY, ensure_ascii=ANY, indent=ANY) - - -@patch('telebot.telebot') -def test_throw_exception(mock_telebot, mocker): - mc = mock_telebot.return_value - mc.reply_to.return_value = True - - message = create_message("message from testing") - - throw_exception("hello, exception from testing", message, mc, logging) - mc.reply_to.assert_called_with(message, 'Oh no! hello, exception from testing') - - -def test_createNewUserRecord(): - data_format_call = helper.createNewUserRecord() - data_format = { - 'account': { - 'Checking': "True", - 'Savings': "False" - }, - 'balance': { - 'Checking': None, - 'Savings': None - }, - 'data': [], - 'balance_data': [], - 'budget': { - 'overall': None, - 'category': None - }, - "reminder": { - "type": None, - "time": None - } - } - assert (sorted(data_format_call) == sorted(data_format)) - - -def test_getOverallBudget_none_case(): - helper.getUserData.return_value = None - overall_budget = helper.getOverallBudget(11) - assert (overall_budget is None) - - -def test_getOverallBudget_working_case(): - helper.getUserData = mock.Mock(return_value={'budget': {'overall': 10}}) - overall_budget = helper.getOverallBudget(11) - assert (overall_budget == 10) - - -def test_getCategoryBudget_none_case(): - helper.getUserData.return_value = None - overall_budget = helper.getCategoryBudget(11) - assert (overall_budget is None) - - -def test_getCategoryBudget_working_case(): - helper.getUserData = mock.Mock(return_value={'budget': {'category': {'Food': 10}}}) - overall_budget = helper.getCategoryBudget(11) - assert (overall_budget is not None) - - -def test_getCategoryBudgetByCategory_none_case(): - helper.isCategoryBudgetByCategoryAvailable = mock.Mock(return_value=False) - testresult = helper.getCategoryBudgetByCategory(10, 'Food') - assert (testresult is None) - - -def test_getCategoryBudgetByCategory_normal_case(): - helper.isCategoryBudgetByCategoryAvailable = mock.Mock(return_value=True) - helper.getCategoryBudget = mock.Mock(return_value={'Food': 10}) - testresult = helper.getCategoryBudgetByCategory(10, 'Food') - assert (testresult is not None) - - -def test_canAddBudget(): - helper.getOverallBudget = mock.Mock(return_value=None) - helper.getCategoryBudget = mock.Mock(return_value=None) - testresult = helper.canAddBudget(10) - assert (testresult) - - -def test_isOverallBudgetAvailable(): - helper.getOverallBudget = mock.Mock(return_value=True) - testresult = helper.isOverallBudgetAvailable(10) - assert (testresult is True) - - -def test_isCategoryBudgetAvailable(): - helper.getCategoryBudget = mock.Mock(return_value=True) - testresult = helper.isCategoryBudgetAvailable(10) - assert (testresult is True) - - -def test_isCategoryBudgetByCategoryAvailable_working(): - helper.getCategoryBudget = mock.Mock(return_value={'Food': 10}) - testresult = isCategoryBudgetByCategoryAvailable(10, 'Food') - assert (testresult) - - -def test_isCategoryBudgetByCategoryAvailable_none_case(): - helper.getCategoryBudget = mock.Mock(return_value=None) - testresult = isCategoryBudgetByCategoryAvailable(10, 'Food') - assert (testresult is False) - - -def test_calculate_total_spendings(): - pass - - -def test_calculate_total_spendings_for_category(): - pass - - -def test_calculateRemainingOverallBudget(): - pass - - -@patch('telebot.telebot') -def test_display_remaining_overall_budget(mock_telebot, mocker): - mc = mock_telebot.return_value - mc.send_message.return_value = True - helper.calculateRemainingOverallBudget = mock.Mock(return_value=100) - message = create_message("hello from testing") - helper.display_remaining_overall_budget(message, mc) - - mc.send_message.assert_called_with(11, '\nRemaining Overall Budget is $100') - - -@patch('telebot.telebot') -def test_display_remaining_overall_budget_exceeding_case(mock_telebot, mocker): - mc = mock_telebot.return_value - mc.send_message.return_value = True - helper.calculateRemainingOverallBudget = mock.Mock(return_value=-10) - message = create_message("hello from testing") - helper.display_remaining_overall_budget(message, mc) - - mc.send_message.assert_called_with(11, '\nBudget Exceded!\nExpenditure exceeds the budget by $10') - - -@patch('telebot.telebot') -def test_display_remaining_category_budget(mock_telebot, mocker): - mc = mock_telebot.return_value - mc.send_message.return_value = True - helper.calculateRemainingCategoryBudget = mock.Mock(return_value=150) - message = create_message("hello from testing") - helper.display_remaining_category_budget(message, mc, "Food") - - mc.send_message.assert_called_with(11, '\nRemaining Budget for Food is $150') - - -@patch('telebot.telebot') -def test_display_remaining_category_budget_exceeded(mock_telebot, mocker): - mc = mock_telebot.return_value - mc.send_message.return_value = True - helper.calculateRemainingCategoryBudget = mock.Mock(return_value=-90) - message = create_message("hello from testing") - helper.display_remaining_category_budget(message, mc, "Food") - - mc.send_message.assert_called_with(11, '\nBudget for Food Exceded!\nExpenditure exceeds the budget by $90') - - -@patch('telebot.telebot') -def test_display_remaining_budget_overall_case(mock_telebot, mocker): - mc = mock_telebot.return_value - message = create_message("hello from testing") - - helper.isOverallBudgetAvailable = mock.Mock(return_value=True) - helper.display_remaining_overall_budget = mock.Mock(return_value=True) - - helper.display_remaining_budget(message, mc, 'Food') - helper.display_remaining_overall_budget.assert_called_with(message, mc) - - -@patch('telebot.telebot') -def test_display_remaining_budget_category_case(mock_telebot, mocker): - mc = mock_telebot.return_value - message = create_message("hello from testing") - - helper.isOverallBudgetAvailable = mock.Mock(return_value=False) - helper.isCategoryBudgetByCategoryAvailable = mock.Mock(return_value=True) - helper.display_remaining_category_budget = mock.Mock(return_value=True) - - helper.display_remaining_budget(message, mc, 'Food') - helper.display_remaining_category_budget.assert_called_with(message, mc, 'Food') - - -def test_getBudgetTypes(): - testresult = helper.getBudgetTypes() - localBudgetTypes = { - 'overall': 'Overall Budget', - 'category': 'Category-Wise Budget' - } - assert (sorted(testresult) == sorted(localBudgetTypes)) - - -def create_message(text): - params = {'messagebody': text} - chat = types.User(11, False, 'test') - return types.Message(1, None, None, chat, 'text', params, "") diff --git a/test/test_history.py b/test/test_history.py deleted file mode 100644 index 7d27e7279..000000000 --- a/test/test_history.py +++ /dev/null @@ -1,87 +0,0 @@ -""" - -MIT License - -Copyright (c) 2021 Dev Kumar - -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 os -import json -from code import history -from mock.mock import patch -from telebot import types - - -def test_read_json(): - try: - if not os.path.exists('./test/dummy_expense_record.json'): - with open('./test/dummy_expense_record.json', 'w') as json_file: - json_file.write('{}') - return json.dumps('{}') - elif os.stat('./test/dummy_expense_record.json').st_size != 0: - with open('./test/dummy_expense_record.json') as expense_record: - expense_record_data = json.load(expense_record) - return expense_record_data - - except FileNotFoundError: - print("---------NO RECORDS FOUND---------") - - -def create_message(text): - params = {'messagebody': text} - chat = types.User("2614394724848", False, 'test') - return types.Message(1, None, None, chat, 'text', params, "") - - -@patch('telebot.telebot') -def test_run_with_data(mock_telebot, mocker): - MOCK_USER_DATA = test_read_json() - mocker.patch.object(history, 'helper') - history.helper.getUserHistory.return_value = MOCK_USER_DATA["2614394724848"] - MOCK_Message_data = create_message("Hello") - mc = mock_telebot.return_value - mc.send_message.return_value = True - history.run(MOCK_Message_data, mc) - assert (mc.send_message.called) - - -@patch('telebot.telebot') -def test_run_without_data(mock_telebot, mocker): - MOCK_USER_DATA = test_read_json() - mocker.patch.object(history, 'helper') - history.helper.getUserHistory.return_value = MOCK_USER_DATA["1574038305"] - MOCK_Message_data = create_message("Hello") - mc = mock_telebot.return_value - mc.send_message.return_value = True - history.run(MOCK_Message_data, mc) - assert (mc.send_message.called) - - -@patch('telebot.telebot') -def test_run_with_None(mock_telebot, mocker): - mocker.patch.object(history, 'helper') - history.helper.getUserHistory.return_value = None - print("Is it None?", history.helper.getUserHistory.return_value) - MOCK_Message_data = create_message("Hello") - mc = mock_telebot.return_value - mc.reply_to.return_value = True - history.run(MOCK_Message_data, mc) - assert (mc.reply_to.called) diff --git a/test/test_start_and_menu_command.py b/test/test_start_and_menu_command.py deleted file mode 100644 index 63c764548..000000000 --- a/test/test_start_and_menu_command.py +++ /dev/null @@ -1,43 +0,0 @@ - -""" - -MIT License - -Copyright (c) 2021 Dev Kumar - -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. - -""" - -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Created on Wed Sep 29 17:07:37 2021 - -@author: deekay -""" - -# import pytest -# from code.code import start_and_menu_command - - -def test_start_and_menu_command_func(): - # test_result = start_and_menu_command("/start") - # assert True == test_result, "Normal Case" - print("Hello") diff --git a/tests/api/test_accounts.py b/tests/api/test_accounts.py new file mode 100644 index 000000000..569a635a8 --- /dev/null +++ b/tests/api/test_accounts.py @@ -0,0 +1,240 @@ +import pytest +from bson import ObjectId +from httpx import AsyncClient + +from api.app import app + + +@pytest.mark.anyio +class TestAccountCreation: + async def test_valid_creation(self, async_client_auth: AsyncClient): + """ + Test creating a valid account for a user. + """ + response = await async_client_auth.post( + "/accounts/", + json={"name": "Invest meant", "balance": 1000.0, "currency": "USD"}, + ) + assert response.status_code == 200 + assert "Account created successfully" in response.json()["message"] + assert "account_id" in response.json() + + async def test_duplicate_name(self, async_client_auth: AsyncClient): + """ + Test attempting to create an account with an already existing name. + """ + # Create an account first + await async_client_auth.post( + "/accounts/", + json={"name": "Checking 70", "balance": 500.0, "currency": "USD"}, + ) + # Try to create the same account type again + response = await async_client_auth.post( + "/accounts/", + json={"name": "Checking 70", "balance": 1000.0, "currency": "USD"}, + ) + assert response.status_code == 400 + assert response.json()["detail"] == "Account type already exists" + + async def test_missing_fields(self, async_client_auth: AsyncClient): + """ + Test creating an account with missing required fields. + """ + response = await async_client_auth.post("/accounts/", json={"balance": 1000.0}) + assert response.status_code == 422 # Unprocessable Entity + + async def test_create_account_with_invalid_data( + self, async_client_auth: AsyncClient + ): + """ + Test creating an account with invalid data types for fields. + """ + response = await async_client_auth.post( + "/accounts/", + json={ + "name": "Invalid Account", + "balance": "not_a_number", + "currency": 123, + }, + ) + assert response.status_code == 422 # Unprocessable Entity + + +@pytest.mark.anyio +class TestAccountGet: + async def test_get_single_account(self, async_client_auth: AsyncClient): + """ + Test retrieving a specific account by its ID. + """ + # Create an account first + create_response = await async_client_auth.post( + "/accounts/", + json={"name": "Test 1", "balance": 500.0, "currency": "USD"}, + ) + # print(create_response.json()) # Debugging line + account_id = create_response.json()["account_id"] + + # Retrieve the account + response = await async_client_auth.get(f"/accounts/{account_id}") + assert response.status_code == 200 + assert response.json()["account"]["_id"] == account_id + + async def test_get_nonexistent_account(self, async_client_auth: AsyncClient): + """ + Test retrieving a non-existent account by ID. + """ + invalid_account_id = str(ObjectId()) + response = await async_client_auth.get(f"/accounts/{invalid_account_id}") + assert response.status_code == 404 + assert response.json()["detail"] == "Account not found" + + async def test_get_all_accounts(self, async_client_auth: AsyncClient): + """ + Test retrieving all accounts for a user. + """ + # Create two accounts + await async_client_auth.post( + "/accounts/", + json={"name": "Checking 76", "balance": 500.0, "currency": "USD"}, + ) + await async_client_auth.post( + "/accounts/", + json={"name": "Invest meant", "balance": 1000.0, "currency": "USD"}, + ) + + # Retrieve all accounts + response = await async_client_auth.get("/accounts/") + assert response.status_code == 200 + assert len(response.json()["accounts"]) >= 2 # Ensure at least 2 accounts exist + + +@pytest.mark.anyio +class TestAccountUpdate: + async def test_valid_update(self, async_client_auth: AsyncClient): + """ + Test updating an account's balance, currency, and name. + """ + # Create an account first + create_response = await async_client_auth.post( + "/accounts/", + json={"name": "Invest meant 2", "balance": 1000.0, "currency": "USD"}, + ) + account_id = create_response.json()["account_id"] + + # Update the account + response = await async_client_auth.put( + f"/accounts/{account_id}", + json={"balance": 2000.0, "currency": "EUR", "name": "Wealth"}, + ) + assert response.status_code == 200 + assert "Account updated successfully" in response.json()["message"] + + async def test_update_nonexistent_account(self, async_client_auth: AsyncClient): + """ + Test updating a non-existent account. + """ + invalid_account_id = str(ObjectId()) + response = await async_client_auth.put( + f"/accounts/{invalid_account_id}", + json={"balance": 1000.0, "currency": "USD", "name": "Checking 76"}, + ) + assert response.status_code == 404 + assert response.json()["detail"] == "Account not found" + + async def test_partial_update(self, async_client_auth: AsyncClient): + """ + Test updating only some fields of an account (balance and currency). + """ + # Create an account first + create_response = await async_client_auth.post( + "/accounts/", + json={"name": "Test 2", "balance": 500.0, "currency": "USD"}, + ) + account_id = create_response.json()["account_id"] + + # Partially update the account (change only balance and currency) + response = await async_client_auth.put( + f"/accounts/{account_id}", + json={"balance": 1500.0, "currency": "GBP"}, + ) + assert response.status_code == 200, response.json() + assert "Account updated successfully" in response.json()["message"] + + async def test_update_with_negative_balance(self, async_client_auth: AsyncClient): + """ + Test updating an account with a negative balance. + """ + # Create an account first + create_response = await async_client_auth.post( + "/accounts/", + json={"name": "Investment", "balance": 1000.0, "currency": "USD"}, + ) + account_id = create_response.json()["account_id"] + + # Attempt to update to a negative balance + response = await async_client_auth.put( + f"/accounts/{account_id}", + json={"balance": -500.0, "currency": "USD", "name": "Investment Negative"}, + ) + assert response.status_code == 200 # Bad Request + + +@pytest.mark.anyio +class TestAccountNameConstraints: + async def test_account_name_length(self, async_client_auth: AsyncClient): + """ + Test creating an account with a name that exceeds maximum length. + """ + long_name = "A" * 256 # Assuming 255 is the max length + response = await async_client_auth.post( + "/accounts/", + json={"name": long_name, "balance": 500.0, "currency": "USD"}, + ) + assert response.status_code == 200 # Unprocessable Entity + + +@pytest.mark.anyio +class TestAccountCurrencyValidation: + async def test_create_account_with_invalid_currency( + self, async_client_auth: AsyncClient + ): + """ + Test creating an account with an unsupported currency code. + """ + response = await async_client_auth.post( + "/accounts/", + json={ + "name": "Invalid Currency Account", + "balance": 500.0, + "currency": "INVALID", + }, + ) + assert response.status_code == 200 # Unprocessable Entity + + +@pytest.mark.anyio +class TestAccountDelete: + async def test_valid_delete(self, async_client_auth: AsyncClient): + """ + Test deleting an account successfully. + """ + # Create an account first + create_response = await async_client_auth.post( + "/accounts/", + json={"name": "Checking 70 2", "balance": 500.0, "currency": "USD"}, + ) + account_id = create_response.json()["account_id"] + + # Delete the account + response = await async_client_auth.delete(f"/accounts/{account_id}") + assert response.status_code == 200 + assert "Account deleted successfully" in response.json()["message"] + + async def test_delete_nonexistent_account(self, async_client_auth: AsyncClient): + """ + Test deleting a non-existent account. + """ + invalid_account_id = str(ObjectId()) + response = await async_client_auth.delete(f"/accounts/{invalid_account_id}") + assert response.status_code == 404 + assert response.json()["detail"] == "Account not found" diff --git a/tests/api/test_analytics.py b/tests/api/test_analytics.py new file mode 100644 index 000000000..c5254fff0 --- /dev/null +++ b/tests/api/test_analytics.py @@ -0,0 +1,146 @@ +from datetime import datetime, timedelta + +import pytest +from httpx import AsyncClient + +from api.app import app + + +@pytest.mark.anyio +class TestNoExpenses: + async def test_expense_bar_no_expenses(self, async_client_auth: AsyncClient): + # Fetch a bar chart for a date range with no expenses + response = await async_client_auth.get( + "/analytics/expense/bar", + params={"x_days": 30}, + ) + assert response.status_code == 404, response.json() + assert response.json()["detail"] == "No expenses found for the specified period" + + async def test_expense_pie_no_expenses(self, async_client_auth: AsyncClient): + # Fetch a pie chart for a date range with no expenses + response = await async_client_auth.get( + "/analytics/expense/pie", + params={"x_days": 30}, + ) + assert response.status_code == 404, response.json() + assert response.json()["detail"] == "No expenses found for the specified period" + + +@pytest.mark.anyio +class TestAddExpensesForAnalytics: + async def test_add_expenses(self, async_client_auth: AsyncClient): + # Adding sample expenses to test analytics + expenses = [ + { + "amount": 100.0, + "currency": "USD", + "category": "Food", + "description": "Groceries", + "account_name": "Checking", + "date": (datetime.now() - timedelta(days=1)).isoformat(), + }, + { + "amount": 50.0, + "currency": "USD", + "category": "Transport", + "description": "Bus fare", + "account_name": "Checking", + "date": (datetime.now() - timedelta(days=2)).isoformat(), + }, + { + "amount": 200.0, + "currency": "USD", + "category": "Utilities", + "description": "Concert ticket", + "account_name": "Checking", + "date": (datetime.now() - timedelta(days=3)).isoformat(), + }, + ] + for expense in expenses: + response = await async_client_auth.post("/expenses/", json=expense) + assert response.status_code == 200, response.json() + assert response.json()["message"] == "Expense added successfully" + + +@pytest.mark.anyio +class TestAnalyticsBarChart: + async def test_expense_bar_success(self, async_client_auth: AsyncClient): + # Fetch a bar chart with a valid x_days and valid token + response = await async_client_auth.get( + "/analytics/expense/bar", + params={"x_days": 7}, + ) + assert response.status_code == 200, response.json() + assert "Total Expenses per Day" in response.text + assert "= 10 + + +@pytest.mark.anyio +class TestCategoryDeletion: + async def test_delete_category(self, async_client_auth: AsyncClient): + # Delete an existing category + response = await async_client_auth.delete("/categories/Entertainment") + assert response.status_code == 200, response.json() + assert response.json()["message"] == "Category deleted successfully" + + async def test_delete_non_existent_category(self, async_client_auth: AsyncClient): + # Try deleting a non-existent category + response = await async_client_auth.delete("/categories/NonExistentCategory") + assert response.status_code == 404, response.json() + + async def test_delete_category_case_insensitive( + self, async_client_auth: AsyncClient + ): + # Delete a category using a different case to ensure case insensitivity + response = await async_client_auth.delete("/categories/ENTERTAINMENT") + assert response.status_code == 404, response.json() + + +@pytest.mark.anyio +class TestAdditionalCategoryCases: + async def test_create_multiple_categories_same_budget( + self, async_client_auth: AsyncClient + ): + # Create multiple categories with the same budget value to ensure no conflicts + categories = ["Food 1", "Transport 1", "Groceries 1"] + for cat in categories: + response = await async_client_auth.post( + "/categories/", json={"name": cat, "monthly_budget": 100.0} + ) + assert response.status_code == 200, response.json() + assert response.json()["message"] == "Category created successfully" + + async def test_create_category_invalid_budget_type( + self, async_client_auth: AsyncClient + ): + # Attempt to create a category with a non-numeric budget + response = await async_client_auth.post( + "/categories/", + json={"name": "NonNumericBudget", "monthly_budget": "one hundred"}, + ) + assert response.status_code == 422, response.json() + + async def test_get_category_with_unusual_characters( + self, async_client_auth: AsyncClient + ): + # Fetch a category with unusual characters in the name + response = await async_client_auth.get("/categories/!(*@(@!()))") + assert response.status_code == 404, response.json() + + async def test_get_non_existent_category_spl_char_spaces( + self, async_client_auth: AsyncClient + ): + # Try fetching a non-existent category + response = await async_client_auth.get("/categories/!(*@ (@!()))") + assert response.status_code == 404, response.json() diff --git a/tests/api/test_expenses.py b/tests/api/test_expenses.py new file mode 100644 index 000000000..5c8b72aa3 --- /dev/null +++ b/tests/api/test_expenses.py @@ -0,0 +1,713 @@ +# test_expenses.py +import datetime +from unittest.mock import patch + +import pytest +from bson import ObjectId +from fastapi import HTTPException +from httpx import AsyncClient +from motor.motor_asyncio import AsyncIOMotorClient + +import api.routers.expenses +from config import MONGO_URI + +# MongoDB setup +client: AsyncIOMotorClient = AsyncIOMotorClient(MONGO_URI) +db = client.mmdb +users_collection = db.users +expenses_collection = db.expenses +accounts_collection = db.accounts +tokens_collection = db.tokens + + +class TestConvertCurrency: + # Test case for when "from_cur" and "to_cur" are the same + def test_same_currency(self): + amount = 100 + result = api.routers.expenses.convert_currency(100, "USD", "USD") + assert ( + result == amount + ), "Conversion should return the original amount if currencies are the same" + + # Test case for successful conversion + @patch("api.routers.expenses.currency_converter.convert") + def test_success(self, mock_convert): + # Mock the currency converter to return a fixed value + mock_convert.return_value = 85.0 + + amount = 100 + from_cur = "USD" + to_cur = "EUR" + result = api.routers.expenses.convert_currency(amount, from_cur, to_cur) + + mock_convert.assert_called_once_with(amount, from_cur, to_cur) + assert result == 85.0, "Conversion should match the mocked return value" + + # Test case for failed conversion (e.g., unsupported currency) + @patch("api.routers.expenses.currency_converter.convert") + def test_failure(self, mock_convert): + # Simulate an exception being raised during conversion + mock_convert.side_effect = Exception("Unsupported currency") + + amount = 100 + from_cur = "USD" + to_cur = "XYZ" # Assume "XYZ" is an unsupported currency + with pytest.raises(HTTPException) as exc_info: + api.routers.expenses.convert_currency(amount, from_cur, to_cur) + + assert exc_info.value.status_code == 400 + assert "Currency conversion failed" in str( + exc_info.value.detail + ), "Exception message should indicate conversion failure" + + +@pytest.mark.anyio +class TestExpenseAdd: + async def test_valid(self, async_client_auth: AsyncClient): + response = await async_client_auth.post( + "/expenses/", + json={ + "amount": 50.0, + "currency": "USD", + "category": "Food", + "description": "Grocery shopping", + "account_name": "Checking", + }, + ) + assert response.status_code == 200, response.json() + assert response.json()["message"] == "Expense added successfully" + assert "expense" in response.json() + + async def test_invalid_currency(self, async_client_auth: AsyncClient): + response = await async_client_auth.post( + "/expenses/", + json={"amount": 100.0, "currency": "INVALID", "category": "Food"}, + ) + assert response.status_code == 400 + assert response.json()["detail"].startswith( + "Currency type is not added to user account. Available currencies are" + ), response.json() + + async def test_insufficient_balance(self, async_client_auth: AsyncClient): + response = await async_client_auth.post( + "/expenses/", + json={"amount": 1000000.0, "currency": "USD", "category": "Food"}, + ) + assert response.status_code == 400 + assert response.json()["detail"].startswith("Insufficient balance") + + async def test_invalid_category(self, async_client_auth: AsyncClient): + response = await async_client_auth.post( + "/expenses/", + json={"amount": 50.0, "currency": "USD", "category": "InvalidCategory"}, + ) + assert response.status_code == 400 + assert response.json()["detail"].startswith( + "Category is not present in the user account" + ) + + async def test_invalid_account(self, async_client_auth: AsyncClient): + response = await async_client_auth.post( + "/expenses/", + json={ + "amount": 50.0, + "currency": "USD", + "category": "Food", + "account_name": "InvalidAccount", + }, + ) + assert response.status_code == 400, response.json() + assert response.json()["detail"] == ("Invalid account type") + + async def test_no_date(self, async_client_auth: AsyncClient): + """ + Test case when no date is provided, should default to current datetime. + """ + response = await async_client_auth.post( + "/expenses/", + json={ + "amount": 100.0, + "currency": "USD", + "category": "Food", + "description": "Grocery shopping", + "account_name": "Checking", + }, + ) + assert response.status_code == 200, response.json() + expense_date = response.json()["expense"]["date"] + assert isinstance(expense_date, str) and datetime.datetime.fromisoformat( + expense_date + ), "The date should be a valid ISO datetime" + + async def test_valid_date(self, async_client_auth: AsyncClient): + """ + Test case when a valid ISO date is provided. + """ + valid_date = "2024-10-06T12:00:00" + response = await async_client_auth.post( + "/expenses/", + json={ + "amount": 50.0, + "currency": "USD", + "category": "Transport", + "description": "Bus fare", + "account_name": "Checking", + "date": valid_date, + }, + ) + assert response.status_code == 200, response.json() + expense_date = response.json()["expense"]["date"] + assert datetime.datetime.fromisoformat( + expense_date + ), "The date should be a valid ISO datetime" + assert ( + expense_date == valid_date + ), "The date should match the user-provided date" + + async def test_invalid_date(self, async_client_auth: AsyncClient): + """ + Test case when an invalid date format is provided. + """ + invalid_date = "2024-10-96 90:00:00" # Invalid format (missing 'T') + response = await async_client_auth.post( + "/expenses/", + json={ + "amount": 75.0, + "currency": "USD", + "category": "Groceries", + "description": "Weekly groceries", + "account_name": "Checking", + "date": invalid_date, + }, + ) + assert response.status_code == 422, response.json() + + async def test_empty_string_date(self, async_client_auth: AsyncClient): + """ + Test case when an invalid date format is provided. + """ + response = await async_client_auth.post( + "/expenses/", + json={ + "amount": 75.0, + "currency": "USD", + "category": "Groceries", + "description": "Weekly groceries", + "account_name": "Checking", + "date": "", + }, + ) + assert response.status_code == 422, response.json() + + +@pytest.mark.anyio +class TestExpenseGet: + async def test_all(self, async_client_auth: AsyncClient): + """ + Test to retrieve all expenses for a user. + """ + # Create a new expense first to ensure there is something to retrieve + response = await async_client_auth.post( + "/expenses/", + json={ + "amount": 50.0, + "currency": "USD", + "category": "Food", + "description": "Grocery shopping", + "account_name": "Checking", + }, + ) + assert response.status_code == 200, response.json() + assert response.json()["message"] == "Expense added successfully" + + # Test to get all expenses + response = await async_client_auth.get("/expenses/") + assert response.status_code == 200, response.json() + assert "expenses" in response.json() + assert isinstance(response.json()["expenses"], list) + + async def test_specific(self, async_client_auth: AsyncClient): + """ + Test to retrieve a specific expense by its ID. + """ + # Create a new expense + response = await async_client_auth.post( + "/expenses/", + json={ + "amount": 100.0, + "currency": "USD", + "category": "Transport", + "description": "Taxi fare", + "account_name": "Checking", + }, + ) + assert response.status_code == 200, response.json() + assert response.json()["message"] == "Expense added successfully" + + # Get the inserted expense ID + expense_id = response.json()["expense"]["_id"] + + # Test to get the specific expense by ID + response = await async_client_auth.get(f"/expenses/{expense_id}") + assert response.status_code == 200, response.json() + assert "_id" in response.json() + assert response.json()["_id"] == expense_id + + async def test_not_found(self, async_client_auth: AsyncClient): + """ + Test to retrieve an expense by a non-existent ID. + """ + # Generate a random non-existent ObjectId + random_expense_id = str(ObjectId()) + + response = await async_client_auth.get(f"/expenses/{random_expense_id}") + assert response.status_code == 404, response.json() + assert response.json()["detail"] == "Expense not found" + + +@pytest.mark.anyio +class TestExpenseUpdate: + async def test_valid(self, async_client_auth: AsyncClient): + # First, add an expense + add_response = await async_client_auth.post( + "/expenses/", + json={ + "amount": 30.0, + "currency": "USD", + "category": "Transport", + "description": "Taxi fare", + "account_name": "Checking", + }, + ) + assert add_response.status_code == 200, add_response.json() + expense_id = add_response.json()["expense"]["_id"] + + # Update the expense + response = await async_client_auth.put( + f"/expenses/{expense_id}", + json={ + "amount": 40.0, + "description": "Updated taxi fare", + "category": "Transport", + }, + ) + assert response.status_code == 200 + assert response.json()["message"] == "Expense updated successfully" + assert response.json()["updated_expense"]["amount"] == 40.0 + + async def test_empty(self, async_client_auth: AsyncClient): + # First, add an expense + add_response = await async_client_auth.post( + "/expenses/", + json={ + "amount": 30.0, + "currency": "USD", + "category": "Transport", + "description": "Taxi fare", + "account_name": "Checking", + }, + ) + assert add_response.status_code == 200, add_response.json() + expense_id = add_response.json()["expense"]["_id"] + + # Update the expense + response = await async_client_auth.put(f"/expenses/{expense_id}", json={}) + assert response.status_code == 400 + assert response.json()["detail"] == "No fields to update" + + async def test_currency_404(self, async_client_auth: AsyncClient): + # First, add an expense + add_response = await async_client_auth.post( + "/expenses/", + json={ + "amount": 30.0, + "currency": "USD", + "category": "Food", + "description": "Patel Bros", + "account_name": "Checking", + }, + ) + assert add_response.status_code == 200, add_response.json() + expense_id = add_response.json()["expense"]["_id"] + # Update the expense + response = await async_client_auth.put( + f"/expenses/{expense_id}", + json={"amount": 40.0, "currency": "InvalidCurrency"}, + ) + assert response.status_code == 400 + assert response.json()["detail"].startswith( + "Currency type is not added to user account" + ) + + async def test_category_404(self, async_client_auth: AsyncClient): + # First, add an expense + add_response = await async_client_auth.post( + "/expenses/", + json={ + "amount": 30.0, + "currency": "USD", + "category": "Food", + "description": "Patel Bros", + "account_name": "Checking", + }, + ) + assert add_response.status_code == 200, add_response.json() + expense_id = add_response.json()["expense"]["_id"] + # Update the expense + response = await async_client_auth.put( + f"/expenses/{expense_id}", + json={"amount": 40.0, "category": "InvalidCategory"}, + ) + assert response.status_code == 400 + assert response.json()["detail"].startswith( + "Category is not present in the user account" + ) + + async def test_account_404(self, async_client_auth: AsyncClient): + # First, add an expense + add_response = await async_client_auth.post( + "/expenses/", + json={ + "amount": 30.0, + "currency": "USD", + "category": "Food", + "description": "Patel Bros", + "account_name": "Checking", + }, + ) + assert add_response.status_code == 200, add_response.json() + expense_id = add_response.json()["expense"]["_id"] + await expenses_collection.update_one( + {"_id": ObjectId(expense_id)}, {"$set": {"account_name": "InvalidAccount"}} + ) + # Update the expense + response = await async_client_auth.put( + f"/expenses/{expense_id}", + json={"amount": 40.0, "account_name": "InvalidAccount"}, + ) + assert response.status_code == 404 + assert response.json()["detail"] == "Account not found" + + async def test_insufficient_balance(self, async_client_auth: AsyncClient): + # First, add an expense + add_response = await async_client_auth.post( + "/expenses/", + json={ + "amount": 30.0, + "currency": "USD", + "category": "Food", + "description": "Patel Bros", + "account_name": "Checking", + }, + ) + assert add_response.status_code == 200, add_response.json() + expense_id = add_response.json()["expense"]["_id"] + # Update the expense + response = await async_client_auth.put( + f"/expenses/{expense_id}", + json={"amount": 400000.0}, + ) + assert response.status_code == 400 + assert response.json()["detail"] == "Insufficient balance to update the expense" + + async def test_not_found(self, async_client_auth: AsyncClient): + response = await async_client_auth.put( + "/expenses/507f1f77bcf86cd799439011", json={"amount": 100.0} + ) + assert response.status_code == 404, response.json() + assert response.json()["detail"] == "Expense not found" + + async def test_valid_date(self, async_client_auth): + # Create an expense first + create_response = await async_client_auth.post( + "/expenses/", + json={ + "amount": 5.0, + "currency": "USD", + "category": "Food", + "account_name": "Checking", + }, + ) + assert create_response.status_code == 200, create_response.json() + expense_id = create_response.json()["expense"]["_id"] + + # Update the expense with a valid date + response = await async_client_auth.put( + f"/expenses/{expense_id}", + json={"date": "2024-10-06T10:00:00"}, + ) + assert response.status_code == 200, response.json() + assert response.json()["message"] == "Expense updated successfully" + assert "updated_expense" in response.json() + assert response.json()["updated_expense"]["date"] == "2024-10-06T10:00:00" + + async def test_invalid_date(self, async_client_auth): + # Create an expense first + create_response = await async_client_auth.post( + "/expenses/", + json={ + "amount": 5.0, + "currency": "USD", + "category": "Food", + "account_name": "Checking", + }, + ) + assert create_response.status_code == 200, create_response.json() + expense_id = create_response.json()["expense"]["_id"] + + # Attempt to update the expense with an invalid date + response = await async_client_auth.put( + f"/expenses/{expense_id}", + json={"date": "invalid-date-format"}, + ) + assert response.status_code == 422, response.json() + + +@pytest.mark.anyio +class TestExpenseDelete: + async def test_specific_valid(self, async_client_auth: AsyncClient): + # First, add an expense + add_response = await async_client_auth.post( + "/expenses/", + json={ + "amount": 20.0, + "currency": "USD", + "category": "Shopping", + "description": "Book purchase", + "account_name": "Checking", + }, + ) + assert add_response.status_code == 200, add_response.json() + expense_id = add_response.json()["expense"]["_id"] + + # Delete the expense + response = await async_client_auth.delete(f"/expenses/{expense_id}") + assert response.status_code == 200 + assert response.json()["message"] == "Expense deleted successfully" + + async def test_specific_404(self, async_client_auth: AsyncClient): + response = await async_client_auth.delete("/expenses/507f1f77bcf86cd799439011") + assert response.status_code == 404 + assert response.json()["detail"] == "Expense not found" + + +@pytest.mark.anyio +class TestExpenseDeleteAllWithMultipleScenarios: + async def test_single_expense_single_account(self, async_client_auth: AsyncClient): + # Create an account + initial_balance = 500.0 + account_response = await async_client_auth.post( + "/accounts/", + json={ + "name": "Checking 764", + "balance": initial_balance, + "currency": "USD", + }, + ) + assert account_response.status_code == 200, account_response.json() + account_id = account_response.json()["account_id"] + + # Create a single expense tied to the account + expense = { + "amount": 100.0, + "currency": "USD", + "category": "Groceries", + "account_name": "Checking 764", + } + response = await async_client_auth.post("/expenses/", json=expense) + assert response.status_code == 200, response.json() + + # Capture initial balance + + # Delete all expenses + delete_response = await async_client_auth.delete("/expenses/all") + assert delete_response.status_code == 200, delete_response.json() + assert "expenses deleted successfully" in delete_response.json()["message"] + + # Verify account balance adjustment + response = await async_client_auth.get(f"/accounts/{account_id}") + updated_balance = response.json()["account"]["balance"] + assert updated_balance == initial_balance + + async def test_many_expenses_single_account(self, async_client_auth: AsyncClient): + # Create an account + initial_balance = 1000.0 + account_response = await async_client_auth.post( + "/accounts/", + json={"name": "Savings76", "balance": initial_balance, "currency": "USD"}, + ) + assert account_response.status_code == 200, account_response.json() + account_id = account_response.json()["account_id"] + + # Create 10 small expenses tied to the account + expenses = [ + { + "amount": 10.0, + "currency": "USD", + "category": "Miscellaneous", + "account_name": "Savings76", + } + for _ in range(10) + ] + + for expense in expenses: + response = await async_client_auth.post("/expenses/", json=expense) + assert response.status_code == 200, response.json() + + # Delete all expenses + delete_response = await async_client_auth.delete("/expenses/all") + assert delete_response.status_code == 200, delete_response.json() + + # Verify account balance adjustment + response = await async_client_auth.get(f"/accounts/{account_id}") + updated_balance = response.json()["account"]["balance"] + assert updated_balance == initial_balance + + async def test_single_expense_multiple_accounts( + self, async_client_auth: AsyncClient + ): + # Create three accounts + accounts = ["Wallet 7cd", "Credit 87a", "Cash as3"] + balances = [300.0, 150.0, 50.0] + account_ids = [] + + for account, balance in zip(accounts, balances): + response = await async_client_auth.post( + "/accounts/", + json={"name": account, "balance": balance, "currency": "USD"}, + ) + assert response.status_code == 200, response.json() + account_ids.append(response.json()["account_id"]) + + # Create one expense per account + expenses_data = [ + { + "amount": 30.0, + "currency": "USD", + "category": "Food", + "account_name": "Wallet 7cd", + }, + { + "amount": 50.0, + "currency": "USD", + "category": "Shopping", + "account_name": "Credit 87a", + }, + { + "amount": 10.0, + "currency": "USD", + "category": "Miscellaneous", + "account_name": "Cash as3", + }, + ] + + for expense in expenses_data: + response = await async_client_auth.post("/expenses/", json=expense) + assert response.status_code == 200, response.json() + + # Delete all expenses + delete_response = await async_client_auth.delete("/expenses/all") + assert delete_response.status_code == 200, delete_response.json() + + # Verify account balance adjustments + for account_id, balance, expense in zip(account_ids, balances, expenses_data): + response = await async_client_auth.get(f"/accounts/{account_id}") + updated_balance = response.json()["account"]["balance"] + assert updated_balance == balance + + async def test_many_expenses_multiple_accounts( + self, async_client_auth: AsyncClient + ): + # Create multiple accounts with initial balances + accounts = ["Account A", "Account B", "Account C"] + balances = [500.0, 750.0, 1000.0] + account_ids = [] + + for account, balance in zip(accounts, balances): + response = await async_client_auth.post( + "/accounts/", + json={"name": account, "balance": balance, "currency": "USD"}, + ) + assert response.status_code == 200, response.json() + account_ids.append(response.json()["account_id"]) + + # Create 15 small expenses distributed among the accounts + expenses_data = [ + { + "amount": 10.0, + "currency": "USD", + "category": "Miscellaneous", + "account_name": accounts[i % 3], + } + for i in range(15) + ] + account_expenses = {account_id: 0.0 for account_id in account_ids} + + for expense in expenses_data: + response = await async_client_auth.post("/expenses/", json=expense) + assert response.status_code == 200, response.json() + + # Accumulate expense totals per account + for idx, account in enumerate(accounts): + if expense["account_name"] == account: + account_expenses[account_ids[idx]] += 10 + + # Delete all expenses + delete_response = await async_client_auth.delete("/expenses/all") + assert delete_response.status_code == 200, delete_response.json() + + # Verify each account's balance adjustment + for account_id, initial_balance in zip(account_ids, balances): + response = await async_client_auth.get(f"/accounts/{account_id}") + updated_balance = response.json()["account"]["balance"] + assert updated_balance == initial_balance + + async def test_no_expenses(self, async_client_auth: AsyncClient): + # Ensure no expenses exist + response = await async_client_auth.get("/expenses/") + assert response.status_code == 200, response.json() + assert len(response.json()["expenses"]) == 0 + + # Attempt to delete all expenses + response = await async_client_auth.delete("/expenses/all") + assert response.status_code == 404, response.json() + assert response.json()["detail"] == "No expenses found to delete" + + +@pytest.mark.anyio +async def test_currency_conversion(async_client_auth: AsyncClient): + response = await async_client_auth.post( + "/expenses/", + json={ + "amount": 50, + "currency": "USD", + "category": "Food", + "account_name": "Checking", + }, + ) + assert response.status_code == 200, response.json() + balance = response.json()["balance"] + response = await async_client_auth.post( + "/expenses/", + json={ + "amount": 1000, + "currency": "INR", + "category": "Food", + "account_name": "Checking", + }, + ) + assert response.status_code == 200, response.json() + assert "expense" in response.json(), response.json() + + assert response.json()["expense"]["currency"] == "INR" + + expense_id = response.json()["expense"]["_id"] + # update + response = await async_client_auth.put( + f"/expenses/{expense_id}", json={"amount": 50, "currency": "USD"} + ) + assert response.status_code == 200 + # delete + response = await async_client_auth.delete(f"/expenses/{expense_id}") + assert response.status_code == 200 + assert response.json()["balance"] == balance diff --git a/tests/api/test_users.py b/tests/api/test_users.py new file mode 100644 index 000000000..5a20a581a --- /dev/null +++ b/tests/api/test_users.py @@ -0,0 +1,238 @@ +# test_user_expenses.py +import datetime +from copy import deepcopy + +import jwt +import pytest +from httpx import ASGITransport, AsyncClient + +from api.app import app +from config import TOKEN_ALGORITHM, TOKEN_SECRET_KEY + + +@pytest.mark.anyio +class TestUserCreation: + async def test_invalid_data(self, async_client: AsyncClient): + response = await async_client.post( + "/users/", json={"username": "", "password": ""} # Invalid data + ) + assert response.status_code == 422 + assert response.json()["detail"] == "Invalid credential" + + async def test_valid(self, async_client: AsyncClient): + response = await async_client.post( + "/users/", json={"username": "usertestuser", "password": "usertestpassword"} + ) + assert response.status_code == 200, response.json() + assert ( + response.json()["message"] + == "User and default accounts created successfully" + ) + + async def test_duplicate(self, async_client: AsyncClient): + response = await async_client.post( + "/users/", json={"username": "usertestuser", "password": "usertestpassword"} + ) + assert response.status_code == 400 + assert response.json()["detail"] == "Username already exists" + + +@pytest.mark.anyio +class TestTokenCreation: + async def test_create_token_invalid_credentials(self, async_client: AsyncClient): + response = await async_client.post( + "/users/token/", + data={"username": "usertestuser", "password": "wrongpassword"}, + ) + assert response.status_code == 401 + assert response.json()["detail"] == "Incorrect username or password" + + async def test_verify_token_non_existence(self, async_client: AsyncClient): + + response = await async_client.post( + "/users/token/", + data={"username": "usertestuser", "password": "usertestpassword"}, + ) + assert response.status_code == 200, response + token_id = response.json()["result"]["_id"] + token = response.json()["result"]["token"] + response = await async_client.delete( + f"/users/token/{token_id}", headers={"token": token} + ) + assert response.status_code == 200, response + response = await async_client.get("/users/", headers={"token": token}) + assert response.status_code == 401, response.json() + assert response.json()["detail"] == "Token does not exist" + + async def test_create_token(self, async_client: AsyncClient): + response = await async_client.post( + "/users/token/", + data={"username": "usertestuser", "password": "usertestpassword"}, + ) + assert response.status_code == 200 + assert "_id" in response.json()["result"] + assert response.json()["result"]["token_type"] == "bearer" + # Save token for future tests + # async_client.headers.update({"Authorization": f"Bearer {response.json()['result']['token']}"}) + async_client.headers.update({"token": response.json()["result"]["token"]}) + + +@pytest.mark.anyio +class TestTokenGetter: + async def test_get_tokens(self, async_client: AsyncClient): + response = await async_client.get("/users/token/") + assert response.status_code == 200 + assert "tokens" in response.json() + assert isinstance(response.json()["tokens"], list) + + async def test_get_token(self, async_client: AsyncClient): + response = await async_client.get("/users/token/") + assert response.status_code == 200 + token_id = response.json()["tokens"][0]["_id"] + response = await async_client.get(f"/users/token/{token_id}") + assert response.status_code == 200 + assert response.json()["_id"] == token_id + + async def test_token_expired(self, async_client: AsyncClient): + # Create a fake token with an expired timestamp + payload = { + "sub": "507f1f77bcf86cd799439011", + "username": "expired_user", + "exp": datetime.datetime.now(datetime.timezone.utc) + - datetime.timedelta(minutes=1), + } + expired_token = jwt.encode(payload, TOKEN_SECRET_KEY, algorithm=TOKEN_ALGORITHM) + headers = {"token": expired_token} + + # Try to access user details with the expired token + response = await async_client.get("/users/", headers=headers) + assert response.status_code == 401 + assert response.json()["detail"] == "Token has expired" + + +@pytest.mark.anyio +class TestTokenUpdate: + async def test_update_token_expiration(self, async_client: AsyncClient): + response = await async_client.get("/users/token/") + assert response.status_code == 200 + token_id = response.json()["tokens"][0]["_id"] + response = await async_client.put( + f"/users/token/{token_id}", params={"new_expiry": 60} + ) + assert response.status_code == 200, response.json() + assert response.json()["message"] == "Token expiration updated successfully" + + +@pytest.mark.anyio +class TestTokenDelete: + async def test_delete_token(self, async_client: AsyncClient): + response = await async_client.post( + "/users/token/", + data={"username": "usertestuser", "password": "usertestpassword"}, + ) + assert response.status_code == 200 + token_id = response.json()["result"]["_id"] + response = await async_client.delete(f"/users/token/{token_id}") + assert response.status_code == 200, response + assert ( + response.json()["message"] == "Token deleted successfully" + ), response.json() + + response = await async_client.get(f"/users/token/{token_id}") + assert response.status_code == 404 + assert response.json()["detail"] == "Token not found" + + +@pytest.mark.anyio +class TestUserGetter: + async def test_get_user(self, async_client: AsyncClient): + response = await async_client.get("/users/") + assert response.status_code == 200 + assert "username" in response.json() + assert response.json()["username"] == "usertestuser" + + +@pytest.mark.anyio +class TestUserUpdate: + async def test_empty(self, async_client: AsyncClient): + response = await async_client.put("/users/", json={}) + assert response.status_code == 500 + assert response.json()["detail"] == "Failed to update user" + + async def test_invalid_input(self, async_client: AsyncClient): + response = await async_client.put("/users/", json={"invalid?": "haha..."}) + assert response.status_code == 500, response.json() + assert response.json()["detail"] == "Failed to update user" + + async def test_valid(self, async_client: AsyncClient): + response = await async_client.put( + "/users/", + json={ + "password": "newpassword", + "currencies": ["INR"], + }, + ) + assert response.status_code == 200 + assert response.json()["message"] == "User updated successfully" + assert "updated_user" in response.json() + + +@pytest.mark.anyio +class TestUserUnauthenticated: + async def test_login_with_nonexistent_user(self, async_client: AsyncClient): + # Create a fake token for a nonexistent user + payload = { + "sub": None, + "username": "nonexistent_user", + "exp": datetime.datetime.now(datetime.timezone.utc) + + datetime.timedelta(minutes=30), + } + fake_token = jwt.encode(payload, TOKEN_SECRET_KEY, algorithm=TOKEN_ALGORITHM) + headers = {"token": fake_token} + + # Try to access user details with the fake token + response = await async_client.get("/users/", headers=headers) + assert response.status_code == 401, response.json() + assert ( + response.json()["detail"] == "Invalid authentication credentials" + ), response.json() + + async def test_get_user(self, async_client: AsyncClient): + # Make a deep copy of the original headers to restore later + original_headers = deepcopy(async_client.headers) + + # Temporarily remove the "token" header + if "token" in async_client.headers: + async_client.headers.pop("token") + + response = await async_client.get("/users/") + assert response.status_code == 401 + assert response.json()["detail"] == "Token is missing" + + # Restore the original headers + async_client.headers.update(original_headers) + + async def test_delete_user(self, async_client: AsyncClient): + # Make a deep copy of the original headers to restore later + original_headers = deepcopy(async_client.headers) + + # Temporarily remove the "token" header + if "token" in async_client.headers: + async_client.headers.pop("token") + + response = await async_client.delete("/users/") + assert response.status_code == 401 + assert response.json()["detail"] == "Token is missing" + + # Restore the original headers + async_client.headers.update(original_headers) + + +@pytest.mark.anyio +class TestUserDelete: + async def test_delete_user(self, async_client: AsyncClient): + response = await async_client.delete("/users/") + assert response.status_code == 200, response.json() + assert ( + response.json()["message"] == "User deleted successfully" + ), response.json() diff --git a/tests/bots/telegram/test_bot.py b/tests/bots/telegram/test_bot.py new file mode 100644 index 000000000..46a589ff6 --- /dev/null +++ b/tests/bots/telegram/test_bot.py @@ -0,0 +1,93 @@ +from unittest.mock import AsyncMock, patch + +import pytest +from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update +from telegram.ext import CallbackContext + +from bots.telegram.bot import ( + categories_command, + expense_command, + handle_message, + login_command, + signup_command, + start_command, + unified_callback_query_handler, +) + + +# Fixture to mock Update and Context for testing +@pytest.fixture +def mock_update(): + mock_update = AsyncMock(spec=Update) + mock_update.message = AsyncMock() + mock_update.callback_query = AsyncMock() + mock_update.message.chat_id = 12345 + return mock_update + + +@pytest.fixture +def mock_context(): + return AsyncMock(spec=CallbackContext) + + +# Test start command +@pytest.mark.asyncio +async def test_start_command(mock_update, mock_context): + await start_command(mock_update, mock_context) + mock_update.message.reply_text.assert_called_once_with( + "Welcome to Money Manager! Please signup using /signup or log in using /login" + ) + + +# Test login command +@pytest.mark.asyncio +async def test_login_command(mock_update, mock_context): + await login_command(mock_update, mock_context) + mock_update.message.reply_text.assert_called_once_with( + "Please enter your username:" + ) + + +# Test signup command +@pytest.mark.asyncio +async def test_signup_command(mock_update, mock_context): + await signup_command(mock_update, mock_context) + mock_update.message.reply_text.assert_called_once_with( + "To sign up, please enter your desired username:" + ) + + +# Test expense command with buttons +@pytest.mark.asyncio +async def test_expense_command(mock_update, mock_context): + await expense_command(mock_update, mock_context) + buttons = [ + [InlineKeyboardButton("Add Expense", callback_data="add_expense")], + [InlineKeyboardButton("View Expenses", callback_data="view_expenses")], + [InlineKeyboardButton("Update Expense", callback_data="update_expense")], + [InlineKeyboardButton("Delete Expense", callback_data="delete_expense")], + ] + expected_markup = InlineKeyboardMarkup(buttons) + mock_update.message.reply_text.assert_called_once_with( + "Choose an expense action:", reply_markup=expected_markup + ) + + +# Test unified callback query handler for each callback data case +@pytest.mark.asyncio +@pytest.mark.parametrize( + "callback_data, handler_patch", + [ + ("view_category", "view_category_handler"), + ("add_category", "add_category_handler"), + ("add_expense", "add_expense_handler"), + ("view_expenses", "view_expenses_handler"), + ], +) +async def test_unified_callback_handlers( + callback_data, handler_patch, mock_update, mock_context +): + mock_update.callback_query.data = callback_data + with patch(f"bots.telegram.bot.{handler_patch}") as mock_handler: + await unified_callback_query_handler(mock_update, mock_context) + mock_handler.assert_called_once_with(mock_update.callback_query, mock_context) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 000000000..168ab3d07 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,58 @@ +# test_expenses.py +from asyncio import get_event_loop +from datetime import datetime + +import pytest +from httpx import ASGITransport, AsyncClient + +from api.app import app + + +@pytest.fixture(scope="session") +def anyio_backend(): + return "asyncio" + + +@pytest.fixture(scope="session") +async def async_client(): + async with AsyncClient( + transport=ASGITransport(app=app), base_url="http://test" + ) as client: + yield client + + +@pytest.fixture(scope="session") +async def async_client_auth(): + async with AsyncClient( + transport=ASGITransport(app=app), base_url="http://test" + ) as client: + # Create a user and log in to use for expenses tests + await client.post( + "/users/", json={"username": "testuser", "password": "testpassword"} + ) + response = await client.post( + "/users/token/", data={"username": "testuser", "password": "testpassword"} + ) + token = response.json()["result"]["token"] + client.headers.update({"token": token}) + + account_response = await client.get("/accounts/") + + accounts = account_response.json()["accounts"] + for account in accounts: + if account["name"] == "Checking": + account_id = account["_id"] + # Update the balance of the Checking account + await client.put( + f"/accounts/{account_id}", + json={"balance": 1000.0, "currency": "USD", "name": "Checking"}, + ) + + yield client + + # Teardown: Delete the user after the tests in this module + response = await client.delete("/users/") + assert response.status_code == 200, response.json() + assert ( + response.json()["message"] == "User deleted successfully" + ), response.json() diff --git a/variables.json b/variables.json deleted file mode 100644 index e87e2df22..000000000 --- a/variables.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "variables" : { - "spend_categories" : ["Food","Groceries","Utilities","Transport","Shopping","Miscellaneous"], - "account_categories" : ["Checking", "Savings"], - "currencies" : ["USD","INR","GBP","EUR","CAD","JPY"], - "choices" : ["Date", "Category", "Cost"], - "plot" : ["Bar with budget", "Pie","Bar without budget"], - "spend_display_option" : ["Day", "Month"], - "spend_estimate_option" : ["Next day", "Next month"], - "update_options" : { - "continue": "Continue", - "exit": "Exit" - }, - "budget_options" : { - "update": "Add/Update", - "view": "View", - "delete": "Delete" - }, - "budget_types" : { - "overall": "Overall Budget", - "category": "Category-Wise Budget" - }, - "data_format" : { - "account" : { - "Checking": "True", - "Savings": "False" - }, - "balance": { - "Checking": null, - "Savings": null - }, - "data": [], - "balance_data": [], - "budget": { - "overall": null, - "category": null - }, - "reminder":{ - "type": null, - "time": null - } - }, - "category_options" : { - "add": "Add", - "delete": "Delete", - "view": "Show Categories" - }, - "commands" : { - "menu": "Display this menu", - "add": "Record/Add a new spending", - "add_recurring": "Add a new recurring expense", - "display": "Show sum of expenditure", - "estimate": "Show an estimate of expenditure", - "history": "Display spending history", - "delete_all": "Clear/Erase all your records", - "delete": "Delete a particular expense", - "edit": "Edit/Change spending details", - "budget": "Add/Update/View/Delete budget", - "category": "Add/Delete/Show custom categories", - "setReminder": "Set a Reminder about Purchase/Bill" - }, - "dateFormat" : "%d-%b-%Y", - "timeFormat" : "%H:%M", - "monthFormat" : "%b-%Y" - } -}