Repository: ranjan-mohanty/vfs-appointment-bot (language: python)

Issue:
Feature/dropdown select



Discussion / hints:

Fix patch:
diff --git a/.flake8 b/.flake8
new file mode 100644
index 0000000..5f2b2f4
--- /dev/null
+++ b/.flake8
@@ -0,0 +1,12 @@
+[flake8]
+exclude = 
+    .git,
+    .github,
+    __pycache__,
+    venv,
+    dist,
+    config,
+    build
+
+max-complexity = 10
+max-line-length = 120
\ No newline at end of file
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..8823009
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,1 @@
+github: ranjan-mohanty
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000..dd84ea7
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,38 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**To Reproduce**
+Steps to reproduce the behavior:
+1. Go to '...'
+2. Click on '....'
+3. Scroll down to '....'
+4. See error
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**Screenshots**
+If applicable, add screenshots to help explain your problem.
+
+**Desktop (please complete the following information):**
+ - OS: [e.g. iOS]
+ - Browser [e.g. chrome, safari]
+ - Version [e.g. 22]
+
+**Smartphone (please complete the following information):**
+ - Device: [e.g. iPhone6]
+ - OS: [e.g. iOS8.1]
+ - Browser [e.g. stock browser, safari]
+ - Version [e.g. 22]
+
+**Additional context**
+Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 0000000..bbcbbe7
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,20 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..42b556f
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,11 @@
+version: 2
+updates:
+  - package-ecosystem: "pip"
+    directory: "/"
+    schedule:
+      interval: "weekly"
+
+  - package-ecosystem: github-actions
+    directory: /
+    schedule:
+      interval: daily
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 0000000..44def4e
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,37 @@
+## Pull Request Template
+
+**Thank you for contributing to the VFS appointment bot project!**
+
+To streamline the review process, please fill out the details below when creating a pull request.
+
+**Title:**
+
+- Briefly describe your changes here
+
+**Description:**
+
+- What changes did you make?
+  - Explain the purpose of your changes and how they address an issue or improve the scraper.
+  - If applicable, mention any new features you've implemented.
+  - Include steps to reproduce any bug fixes (if applicable).
+- How did you test your changes?
+  - Did you add new unit tests?
+  - Did you manually test with different scenarios?
+
+**Additional Information:**
+
+- Did you add any dependencies or external libraries?
+- Did you modify the documentation? If so, how?
+
+**Checklist:**
+
+- [ ] I followed the Coding Style Guide: [https://peps.python.org/pep-0008/](https://peps.python.org/pep-0008/).
+- [ ] I added clear and concise comments to my code.
+- [ ] I added unit tests for new features or bug fixes (if applicable).
+- [ ] I ensured my changes do not introduce regressions.
+
+**Optional:**
+
+- Issue reference (if related to an existing issue): # (issue number)
+
+**By providing these details, you'll help us review your pull request efficiently. We appreciate your contributions!**
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000..94c9165
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,37 @@
+name: Build
+on:
+  workflow_call:
+  push:
+    branches:
+      - "**"
+    tags-ignore:
+      - "**"
+
+permissions:
+  contents: read
+
+jobs:
+  build:
+    name: Build distribution
+    runs-on: ubuntu-latest
+
+    steps:
+      - name: Harden Runner
+        uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0
+        with:
+          egress-policy: audit
+
+      - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
+      - name: Set up Python
+        uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0
+        with:
+          python-version: "3.x"
+      - name: Install pypa/build
+        run: python3 -m pip install build --user
+      - name: Build a binary wheel and a source tarball
+        run: python3 -m build
+      - name: Store the distribution packages
+        uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
+        with:
+          name: python-package-distributions
+          path: dist/
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
new file mode 100644
index 0000000..1519be1
--- /dev/null
+++ b/.github/workflows/codeql.yml
@@ -0,0 +1,78 @@
+# For most projects, this workflow file will not need changing; you simply need
+# to commit it to your repository.
+#
+# You may wish to alter this file to override the set of languages analyzed,
+# or to provide custom queries or build logic.
+#
+# ******** NOTE ********
+# We have attempted to detect the languages in your repository. Please check
+# the `language` matrix defined below to confirm you have the correct set of
+# supported CodeQL languages.
+#
+name: "CodeQL"
+
+on:
+  push:
+    branches: ["main"]
+  pull_request:
+    # The branches below must be a subset of the branches above
+    branches: ["main"]
+  schedule:
+    - cron: "0 0 * * 1"
+
+permissions:
+  contents: read
+
+jobs:
+  analyze:
+    name: Analyze
+    runs-on: ubuntu-latest
+    permissions:
+      actions: read
+      contents: read
+      security-events: write
+
+    strategy:
+      fail-fast: false
+      matrix:
+        language: ["python"]
+        # CodeQL supports [ $supported-codeql-languages ]
+        # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
+
+    steps:
+      - name: Harden Runner
+        uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0
+        with:
+          egress-policy: audit
+
+      - name: Checkout repository
+        uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
+
+      # Initializes the CodeQL tools for scanning.
+      - name: Initialize CodeQL
+        uses: github/codeql-action/init@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9
+        with:
+          languages: ${{ matrix.language }}
+          # If you wish to specify custom queries, you can do so here or in a config file.
+          # By default, queries listed here will override any specified in a config file.
+          # Prefix the list here with "+" to use these queries and those in the config file.
+
+      # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
+      # If this step fails, then you should remove it and run the build manually (see below)
+      - name: Autobuild
+        uses: github/codeql-action/autobuild@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9
+
+      # ℹ️ Command-line programs to run using the OS shell.
+      # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
+
+      #   If the Autobuild fails above, remove it and uncomment the following three lines.
+      #   modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
+
+      # - run: |
+      #   echo "Run, Build Application using script"
+      #   ./location_of_script_within_repo/buildscript.sh
+
+      - name: Perform CodeQL Analysis
+        uses: github/codeql-action/analyze@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9
+        with:
+          category: "/language:${{matrix.language}}"
diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml
new file mode 100644
index 0000000..86ed11e
--- /dev/null
+++ b/.github/workflows/dependency-review.yml
@@ -0,0 +1,27 @@
+# Dependency Review Action
+#
+# This Action will scan dependency manifest files that change as part of a Pull Request,
+# surfacing known-vulnerable versions of the packages declared or updated in the PR.
+# Once installed, if the workflow run is marked as required,
+# PRs introducing known-vulnerable packages will be blocked from merging.
+#
+# Source repository: https://github.com/actions/dependency-review-action
+name: 'Dependency Review'
+on: [pull_request]
+
+permissions:
+  contents: read
+
+jobs:
+  dependency-review:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Harden Runner
+        uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0
+        with:
+          egress-policy: audit
+
+      - name: 'Checkout Repository'
+        uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
+      - name: 'Dependency Review'
+        uses: actions/dependency-review-action@0fa40c3c10055986a88de3baa0d6ec17c5a894b3 # v4.2.3
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
new file mode 100644
index 0000000..5a9fe56
--- /dev/null
+++ b/.github/workflows/publish.yml
@@ -0,0 +1,78 @@
+name: Publish - PyPI
+on:
+  push:
+    tags-ignore:
+      - "*-rc*"
+
+permissions:
+  contents: read
+
+jobs:
+  build:
+    uses: ./.github/workflows/build.yml
+  publish-to-pypi:
+    name: Publish to PyPI
+    needs:
+      - build
+    runs-on: ubuntu-latest
+    environment:
+      name: pypi
+      url: https://pypi.org/p/vfs-appointment-bot
+    permissions:
+      id-token: write
+
+    steps:
+      - name: Harden Runner
+        uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0
+        with:
+          egress-policy: audit
+
+      - name: Download all the dists
+        uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
+        with:
+          name: python-package-distributions
+          path: dist/
+      - name: Publish distribution to PyPI
+        uses: pypa/gh-action-pypi-publish@81e9d935c883d0b210363ab89cf05f3894778450 # release/v1
+
+  github-release:
+    name: Release
+    needs:
+      - publish-to-pypi
+    runs-on: ubuntu-latest
+
+    permissions:
+      contents: write
+      id-token: write
+
+    steps:
+      - name: Harden Runner
+        uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0
+        with:
+          egress-policy: audit
+
+      - name: Download all the dists
+        uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
+        with:
+          name: python-package-distributions
+          path: dist/
+      - name: Sign the dists with Sigstore
+        uses: sigstore/gh-action-sigstore-python@61f6a500bbfdd9a2a339cf033e5421951fbc1cd2 # v2.1.1
+        with:
+          inputs: >-
+            ./dist/*.tar.gz
+            ./dist/*.whl
+      - name: Create GitHub Release
+        env:
+          GITHUB_TOKEN: ${{ github.token }}
+        run: >-
+          gh release create
+          '${{ github.ref_name }}'
+          --repo '${{ github.repository }}'
+      - name: Upload artifact signatures to GitHub Release
+        env:
+          GITHUB_TOKEN: ${{ github.token }}
+        run: >-
+          gh release upload
+          '${{ github.ref_name }}' dist/**
+          --repo '${{ github.repository }}'
diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml
new file mode 100644
index 0000000..9d7effa
--- /dev/null
+++ b/.github/workflows/scorecard.yml
@@ -0,0 +1,70 @@
+# This workflow uses actions that are not certified by GitHub. They are provided
+# by a third-party and are governed by separate terms of service, privacy
+# policy, and support documentation.
+
+name: Scorecard Security
+on:
+  push:
+    branches: ["main"]
+
+# Declare default permissions as read only.
+permissions: read-all
+
+jobs:
+  analysis:
+    name: Scorecard analysis
+    runs-on: ubuntu-latest
+    permissions:
+      # Needed to upload the results to code-scanning dashboard.
+      security-events: write
+      # Needed to publish results and get a badge (see publish_results below).
+      id-token: write
+      # Uncomment the permissions below if installing in a private repository.
+      # contents: read
+      # actions: read
+
+    steps:
+      - name: Harden Runner
+        uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0
+        with:
+          egress-policy: audit
+
+      - name: "Checkout code"
+        uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
+        with:
+          persist-credentials: false
+
+      - name: "Run analysis"
+        uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1
+        with:
+          results_file: results.sarif
+          results_format: sarif
+          # (Optional) "write" PAT token. Uncomment the `repo_token` line below if:
+          # - you want to enable the Branch-Protection check on a *public* repository, or
+          # - you are installing Scorecard on a *private* repository
+          # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat.
+          # repo_token: ${{ secrets.SCORECARD_TOKEN }}
+
+          # Public repositories:
+          #   - Publish results to OpenSSF REST API for easy access by consumers
+          #   - Allows the repository to include the Scorecard badge.
+          #   - See https://github.com/ossf/scorecard-action#publishing-results.
+          # For private repositories:
+          #   - `publish_results` will always be set to `false`, regardless
+          #     of the value entered here.
+          publish_results: true
+
+      # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
+      # format to the repository Actions tab.
+      - name: "Upload artifact"
+        uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
+        with:
+          name: SARIF file
+          path: results.sarif
+          retention-days: 5
+
+      # Upload the results to GitHub's code scanning dashboard.
+      - name: "Upload to code-scanning"
+        uses: github/codeql-action/upload-sarif@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9
+        with:
+          sarif_file: results.sarif
diff --git a/.gitignore b/.gitignore
index b6e4761..e03df20 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,129 +1,32 @@
-# Byte-compiled / optimized / DLL files
-__pycache__/
-*.py[cod]
-*$py.class
-
-# C extensions
-*.so
-
 # Distribution / packaging
-.Python
-build/
-develop-eggs/
 dist/
-downloads/
 eggs/
-.eggs/
-lib/
-lib64/
-parts/
-sdist/
-var/
-wheels/
-pip-wheel-metadata/
-share/python-wheels/
 *.egg-info/
 .installed.cfg
 *.egg
 MANIFEST
-
-# PyInstaller
-#  Usually these files are written by a python script from a template
-#  before PyInstaller builds the exe, so as to inject date/other infos into it.
-*.manifest
-*.spec
-
-# Installer logs
-pip-log.txt
-pip-delete-this-directory.txt
+poetry.lock
 
 # Unit test / coverage reports
-htmlcov/
-.tox/
-.nox/
 .coverage
 .coverage.*
-.cache
-nosetests.xml
-coverage.xml
-*.cover
-*.py,cover
-.hypothesis/
 .pytest_cache/
 
 # Translations
 *.mo
 *.pot
 
-# Django stuff:
-*.log
-local_settings.py
-db.sqlite3
-db.sqlite3-journal
-
-# Flask stuff:
-instance/
-.webassets-cache
-
-# Scrapy stuff:
-.scrapy
-
-# Sphinx documentation
-docs/_build/
-
-# PyBuilder
-target/
-
-# Jupyter Notebook
-.ipynb_checkpoints
-
-# IPython
-profile_default/
-ipython_config.py
-
-# pyenv
-.python-version
-
-# pipenv
-#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
-#   However, in case of collaboration, if having platform-specific dependencies or dependencies
-#   having no cross-platform support, pipenv may install dependencies that don't work, or not
-#   install all needed dependencies.
-#Pipfile.lock
-
-# PEP 582; used by e.g. github.com/David-OConnor/pyflow
-__pypackages__/
-
-# Celery stuff
-celerybeat-schedule
-celerybeat.pid
-
-# SageMath parsed files
-*.sage.py
+# IDEs/Build Tools (consider including if you use them)
+.idea/
+.vscode/
+build/
 
-# Environments
-.env
-.venv
+# Virtual Environments
+.venv/
 env/
-venv/
-ENV/
-env.bak/
-venv.bak/
-
-# Spyder project settings
-.spyderproject
-.spyproject
-
-# Rope project settings
-.ropeproject
-
-# mkdocs documentation
-/site
+venv
 
-# mypy
-.mypy_cache/
-.dmypy.json
-dmypy.json
+#Logs
+app.log
 
-# Pyre type checker
-.pyre/
+config/
\ No newline at end of file
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..23a6b8f
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,22 @@
+repos:
+- repo: https://github.com/gitleaks/gitleaks
+  rev: v8.16.3
+  hooks:
+  - id: gitleaks
+- repo: https://github.com/pre-commit/pre-commit-hooks
+  rev: v4.4.0
+  hooks:
+  - id: end-of-file-fixer
+  - id: trailing-whitespace
+- repo: https://github.com/pylint-dev/pylint
+  rev: v2.17.2
+  hooks:
+  - id: pylint
+- repo: https://github.com/pycqa/flake8
+  rev: ^4.0.1
+  hooks:
+  - id: flake8
+- repo: https://github.com/psf/black
+  rev: ^22.3.2
+  hooks:
+  - id: black
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..4837d0a
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,128 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+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.
+
+## Our Standards
+
+Examples of behavior that contributes to a positive environment for our
+community include:
+
+- 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
+
+Examples of unacceptable behavior include:
+
+- 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
+
+## Enforcement Responsibilities
+
+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.
+
+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.
+
+## Scope
+
+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.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported to the community leaders responsible for enforcement at
+ranjan@duck.com.
+All complaints will be reviewed and investigated promptly and fairly.
+
+All community leaders are obligated to respect the privacy and security of the
+reporter of any incident.
+
+## 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:
+
+### 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.
+
+### 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.
+
+### 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.
+
+### 4. Permanent Ban
+
+**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
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage],
+version 2.0, available at
+https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
+
+Community Impact Guidelines were inspired by [Mozilla's code of conduct
+enforcement ladder](https://github.com/mozilla/diversity).
+
+[homepage]: https://www.contributor-covenant.org
+
+For answers to common questions about this code of conduct, see the FAQ at
+https://www.contributor-covenant.org/faq. Translations are available at
+https://www.contributor-covenant.org/translations.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..db0e76e
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,42 @@
+## Welcome to Amazon Product Details Scraper contributing guide
+
+Thank you for investing your time in contributing to our project! We welcome contributions to this project! Here's how you can get involved:
+
+**1. Issues:**
+
+- **Bug reports:** If you encounter a bug, please search existing issues first to avoid duplicates. If you can't find a relevant issue, create a new one with a clear description of the problem, including steps to reproduce it.
+- **Feature requests:** If you have an idea for a new feature, feel free to create an issue to discuss it. Explain the functionality you'd like to see and how it would benefit the project.
+
+**2. Pull Requests:**
+
+- **Fork the repository:** Create a fork of the repository on GitHub.
+- **Clone your fork:** Clone your forked repository to your local machine.
+- **Create a new branch:** Create a new branch for your changes (e.g., `feature/your_feature_name`).
+- **Implement your changes:** Make your modifications to the code.
+- **Write clear commit messages:** Use descriptive commit messages that explain your changes.
+- **Test your changes:** Ensure your changes don't introduce regressions. Consider adding unit tests if applicable.
+- **Push to your branch:** Push your changes to your branch on your forked repository.
+- **Create a pull request:** Create a pull request from your branch to the main branch of the upstream repository.
+- **Address feedback:** We will review your pull request and provide feedback. Be prepared to address any comments or suggestions.
+
+**3. Coding Style:**
+
+- Follow PEP 8 style guide for Python code ([https://peps.python.org/pep-0008/](https://peps.python.org/pep-0008/)).
+- Use consistent formatting and indentation.
+
+**4. License:**
+
+- This project is licensed under the MIT License. Please ensure your contributions are compatible with the license.
+
+**5. Code Documentation:**
+
+- Add comments to document your code, especially complex sections.
+- Consider using docstrings to explain functions and classes.
+
+**Thank you for your contributions!**
+
+**Additional Notes:**
+
+- You are not obligated to include all the sections mentioned above. Adapt them to your project's specific needs.
+- Feel free to add a section about setting up development environment if your project has dependencies.
+- Consider including a code of conduct to promote a positive and inclusive community.
diff --git a/README.md b/README.md
index 4a7f12c..82ae67b 100644
--- a/README.md
+++ b/README.md
@@ -1,68 +1,204 @@
-# VFS Germany Appointment Bot
-[![GitHub license](https://img.shields.io/github/license/ranjan-mohanty/vfs-appointment-bot)](https://github.com/ranjan-mohanty/vfs-appointment-bot/blob/main/LICENSE)
-[![GitHub forks](https://img.shields.io/github/forks/ranjan-mohanty/vfs-appointment-bot)](https://github.com/ranjan-mohanty/vfs-appointment-bot/network)
-[![GitHub stars](https://img.shields.io/github/stars/ranjan-mohanty/vfs-appointment-bot)](https://github.com/ranjan-mohanty/vfs-appointment-bot/stargazers)
-[![GitHub issues](https://img.shields.io/github/issues/ranjan-mohanty/vfs-appointment-bot)](https://github.com/ranjan-mohanty/vfs-appointment-bot/issues)
+# VFS Appointment Bot
+
+[![GitHub License](https://img.shields.io/github/license/ranjan-mohanty/vfs-appointment-bot)](https://github.com/ranjan-mohanty/vfs-appointment-bot/blob/main/LICENSE)
+[![GitHub forks](https://img.shields.io/github/forks/ranjan-mohanty/vfs-appointment-bot)](https://github.com/ranjan-mohanty/vfs-appointment-bot/forks)
+[![GitHub Repo stars](https://img.shields.io/github/stars/ranjan-mohanty/vfs-appointment-bot)](https://github.com/ranjan-mohanty/vfs-appointment-bot/stargazers)
+
+<!-- [![GitHub Release](https://img.shields.io/github/v/release/ranjan-mohanty/vfs-appointment-bot)](https://github.com/ranjan-mohanty/vfs-appointment-bot/releases)
+[![PyPI - Version](https://img.shields.io/pypi/v/vfs-appointment-bot)](https://pypi.org/project/vfs-appointment-bot/)
+[![Downloads](https://static.pepy.tech/badge/vfs-appointment-bot)](https://pepy.tech/project/vfs-appointment-bot) -->
+
+[![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/ranjan-mohanty/vfs-appointment-bot/build.yml)](https://github.com/ranjan-mohanty/vfs-appointment-bot/actions/workflows/build.yml)
+[![Codacy Badge](https://app.codacy.com/project/badge/Grade/21f1ecd428ec4342980020a6ef383439)](https://app.codacy.com/gh/ranjan-mohanty/vfs-appointment-bot/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)
+[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/ranjan-mohanty/vfs-appointment-bot/badge)](https://securityscorecards.dev/viewer/?uri=github.com/ranjan-mohanty/vfs-appointment-bot)
+[![GitHub Issues or Pull Requests](https://img.shields.io/github/issues/ranjan-mohanty/vfs-appointment-bot)](https://github.com/ranjan-mohanty/vfs-appointment-bot/issues)
+![Libraries.io dependency status for GitHub repo](https://img.shields.io/librariesio/github/ranjan-mohanty/vfs-appointment-bot)
 [![Twitter](https://img.shields.io/twitter/url?style=social&url=https%3A%2F%2Fgithub.com%2Franjan-mohanty%2Fvfs-appointment-bot)](https://twitter.com/intent/tweet?text=Check%20this%20out%20&url=https%3A%2F%2Fgithub.com%2Franjan-mohanty%2Fvfs-appointment-bot)
 
+This Python script(**vfs-appointment-bot**) automates checking for appointments at VFS Global portal in a specified country.
+
+## Installation
+
+<!-- The `vfs-appointment-bot` script can be installed using two methods:
+
+**1. Using pip:**
+
+It is the preferred method for installing `vfs-appointment-bot`. Here's how to do it:
+
+1.  **Install using pip:**
+
+    ```bash
+    pip install vfs-appointment-bot
+    ```
+
+    This will download and install the `vfs-appointment-bot` package and its dependencies into your Python environment.
+
+**Manual Installation:**
+
+If you prefer a more traditional approach, you can clone the source code from the project repository and install it manually: -->
+
+1.  **Clone the repository:**
+
+    ```bash
+    git clone https://github.com/ranjan-mohanty/vfs-appointment-bot
+    ```
+
+2.  **Navigate to the project directory:**
+
+    ```bash
+    cd vfs-appointment-bot
+    ```
+
+3.  **Create a virtual environment (Recommended):**
+
+    ```bash
+    python3 -m venv venv
+    ```
+
+    This creates a virtual environment named `venv` to isolate project dependencies from your system-wide Python installation (**recommended**).
+
+4.  **Activate the virtual environment:**
+
+    **Linux/macOS:**
+
+    ```bash
+    source venv/bin/activate
+    ```
+
+    **Windows:**
+
+    ```bash
+    venv\Scripts\activate
+    ```
+
+5.  **Install dependencies:**
+
+    ```bash
+    pip install poetry
+    poetry install
+    ```
+
+6.  **Install playwright dependencies:**
+
+    ```bash
+    playwright install
+    ```
+
+## Usage
+
+> **Pre-configuration:**  
+> Before running the script, ensure your update the vfs credentials and notification channel preferences in the config.ini file. See the [Notification Channels](#notification-channels) section for details on configuring email, Twilio, and Telegram notifications.
+
+1. **Command-Line Argument:**
+
+   The script requires the source and destination country code ([as per ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2#Officially_assigned_code_elements)) to be provided as a command-line argument using the `-sc` or `--source-country-code` and `-dc` or `--destination-country-code` option.
+
+2. **Running the Script:**
+
+   There are two ways to provide required appointment details:
+
+   - **Responding to User Prompts (recommended):**
+
+     ```bash
+     vfs-appointment-bot -sc IN -dc DE
+     ```
+
+     The script will prompt you to enter the required apponitment parameters for the specified country.
+
+   - **Using `-ap` or `--appointment-params`:**
+
+     Specify appointment details in a comma-separated (**not space-separated**) key-value format:
+
+     ```bash
+     vfs-appointment-bot -sc IN -dc DE -ap visa_center=X,visa_category=Y,visa_sub_category=Z
+     ```
+
+   The script will then connect to the VFS Global website for the specified country, search for available appointments using the provided or entered parameters, and potentially send notifications (depending on your configuration).
+
+## Notification Channels
+
+It currently supports three notification channels to keep you informed about appointment availability:
+
+- **Email:** Sends notifications via a Gmail account.
+- **Twilio (SMS & Voice Call):** Enables alerts through text messages and phone calls using Twilio's services.
+- **Telegram:** Sends notifications directly to your Telegram account through a bot.
+
+**Configuring Notifications:**
+
+**Email:**
+
+1. **Email Account:** You'll need a **Gmail account** for sending notifications.
+2. **App Password:** Generate an app password for your Gmail account instead of your regular password. Refer to Google's guide for generating app passwords: [https://support.google.com/accounts/answer/185833?hl=en](https://support.google.com/accounts/answer/185833?hl=en).
+3. **Configuration File:** Update your application's configuration file (`config.ini`) with the following details:
 
-A script to check the appointment slots.
+   - **`email` (Required):** Your Gmail address.
+   - **`password` (Required):** Your generated Gmail app password.
 
-By default, it runs every 2 minutes and check for visa slots at VFS website and notifies the user by SMS/call/Telegram. <br/>
-The interval can be changed in the config.
+**Twilio:**
 
-## How to use
-1. Clone the repo: `git clone https://github.com/ranjan-mohanty/vfs-appointment-bot.git` <br/>
-2. Move into the repo: `cd vfs_appointment_bot` <br/>
-3. Update the config file (`config/config.ini`) with VFS, Twilio, Telegram credentials. Note that you can use either telegram, or twilio, or both. This can be specified with `use_telegram` and `use_twilio` config flags in same file.
-3. Create a new virtual environment: `python3 -m venv venv` or by using conda `conda create --name venv python=3.8`<br/>
-4. Activate the environment (might differ a bit for windows and MacOS): `source venv/bin/activate` / `conda activate venv` <br/>
-5. Install the dependencies: `pip install -r requirements.txt` <br/>
-6. Run the script:
+1. **Create a Twilio Account (if needed):** Sign up for a free Twilio account at [https://www.twilio.com/en-us](https://www.twilio.com/en-us) to obtain account credentials and phone numbers.
+2. **Retrieve Credentials:** Locate your account SID, auth token, and phone numbers within your Twilio account dashboard.
+3. **Configuration File:** Update your application's configuration file (`config.ini`) with:
 
-`python vfs_appointment_bot/vfs_appointment_bot.py '<vfs_centre>' '<visa_category>' '<visa_subcategory>'`
+   - `auth_token` (Required): Your Twilio auth token
+   - `account_sid` (Required): Your Twilio account SID
+   - `sms_enabled` (Optional): Enables SMS notifications (default: True)
+   - `call_enabled` (Optional): Enables voice call notifications (default: False)
+   - `url` (Optional): Twilio API URL (Only needed if call is enabled)
+   - `to_num` (Required): Recipient phone number for notifications
+   - `from_num` (Required): Twilio phone number you'll use for sending messages
 
-OR
+**Telegram:**
 
-`python vfs_appointment_bot/vfs_appointment_bot.py`
+1. **Create a Telegram Bot:** Visit [https://telegram.me/BotFather](https://telegram.me/BotFather) to create a Telegram bot. Follow the on-screen instructions to obtain your bot's token.
+2. **Configuration File:** Update your application's configuration file (`config.ini`) with:
 
-It will take the values as input from the user
+   - **`bot_token` (Required):** Your Telegram bot token obtained from BotFather.
+   - **`chat_id` (Optional):** The specific Telegram chat ID where you want to receive notifications. If omitted, the bot will send notifications to the chat where it was messaged from. To find your chat ID, you can create a group chat with just yourself and then use the `/my_id` command within the bot.
 
-** Please refer to the screenshot for more details regarding the inputs.
+## Supported Countries and Appointment Parameters
 
-![VFS Appointment Form Screenshot](./assets/vfs-appointment-form.png)
+The following table lists currently supported countries and their corresponding appointment parameters:
 
-## Dependency
+| Country                 | Appointment Parameters                        |
+| ----------------------- | --------------------------------------------- |
+| India(IN) - Germany(DE) | visa_category, visa_sub_category, visa_center |
+| Iraq(IQ) - Germany(DE)  | visa_category, visa_sub_category, visa_center |
 
-1. Install Firefox Browser on your machine if not already installed.
-2. `geckodriver` (instructions to install geckodriver are written below)
-3. Setup client for Twilio/Telegram or both:
-    - Create an account on Twilio to get text and call alerts. Sign up [here](https://www.twilio.com/try-twilio) for a trial account to get credits upto worth $10, OR
-    - Create a new bot via Telegram and add it to a chat group where you want it to post messages to notify you. Check [this simple tutorial out](https://medium.com/codex/using-python-to-send-telegram-messages-in-3-simple-steps-419a8b5e5e2) if you don't know how to create a new bot and get its credentials. Once bot is created you need to add its credentials in `config/config.ini` file.
+**Notes:**
 
+- Appointment parameters might vary depending on the specific country and visa type. Always consult VFS Global's website for the latest information.
 
-## How to install geckodriver
+## Known Issues
 
-1. Run these the commands:
+**1. Login Failures After Frequent Requests:**  
+If the bot makes login requests to the VFS website too frequently, the VFS system might temporarily block your access due to suspected automation. This can lead to login failures.
 
-    - Linux (as an example) : `wget https://github.com/mozilla/geckodriver/releases/download/v0.18.0/geckodriver-v0.18.0-linux64.tar.gz`
+- **Workaround:**
+  - **Reduce request frequency:** Consider increasing the delay between bot runs to avoid triggering VFS's blocking mechanisms. You can adjust the interval in the configuration or code.
+  - **Retry after 2 hours:** If you encounter a login failure, wait for at least 2 hours before retrying. The VFS block should expire within this timeframe.
 
-    (You can find the download URL to the latest release of geckodriver on Github. Check out [their latest release here](https://github.com/mozilla/geckodriver/releases) for your machine.)
+**2. Occasional Captcha Verification:**  
+The VFS website requires a CAPTCHA verification step during login. Currently, the bot does not have a built-in CAPTCHA solver.
 
-2. Extract the file with
+- **Workaround:**
+  - **Wait and Retry:** Sometimes, CAPTCHAs appear due to temporary website issues. Wait for a while and try again later.
+  - **Retry in another browser:** CAPTCHAs are often solved automatically in the Firefox browser. If it still fails, retry the login process in another browser by setting `browser_type` to `"chromium" or "webkit"` in your `config.ini` file.
 
-    `tar -xvzf geckodriver*`
+**Note:** We are constantly working to improve the bot's functionality. Future updates might include integrated CAPTCHA solving capabilities.
 
-3. Make it executable (note this shouldn't be necessary, unless the unzipped file doesn't have the execute bits set):
+## Extending Country Support
 
-    `chmod +x geckodriver`
+This script is currently designed to work with the VFS Global website for Germany. It might be possible to extend support for other countries by modifying the script to handle potential variations in website structure and parameter requirements across different VFS Global country pages.
 
-4. Add the driver to your PATH in ~/.bashrc so other tools can find it:
+## Contributing
 
-    `export PATH=$PATH:/path-to-extracted-file/geckodriver`
+We welcome contributions from the community to improve this project! Here's how you can get involved:
 
+- **Report issues:** If you encounter any bugs or problems with the script, please create an issue on the project's repository.
+- **Suggest improvements:** Do you have ideas for making the script more user-friendly or feature-rich? Feel free to create an issue or pull request on the repository.
+- **Submit pull requests:** If you've made code changes that you think would benefit the project, create a pull request on the repository. Please follow any contribution guidelines outlined in a CONTRIBUTING.md file.
 
-## Contributors
+## Disclaimer
 
-- [Ranjan Mohanty](https://github.com/ranjan-mohanty/)
-- [Bhavul Gauri](https://github.com/bhavul/)
+This script is provided as-is and is not affiliated with VFS Global. It's your responsibility to ensure you're complying with VFS Global's terms and conditions when using this script. Be aware that website structures and appointment availability mechanisms might change over time.
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..bd07b85
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,50 @@
+## Security Policy for VFS Appointment Bot
+
+This document outlines the security policy for the VFS Appointment Bot project.
+
+**1. Reporting Vulnerabilities:**
+
+We appreciate your help in keeping this project secure. If you discover a security vulnerability, please report it responsibly by following these steps:
+
+**1.1 Public Reporting:**
+
+- If the vulnerability can be disclosed publicly without compromising security, you can create a public issue report on the project's GitHub repository.
+
+**1.2 Private Reporting:**
+
+- **We have enabled private vulnerability reporting on GitHub.** For vulnerabilities that should be kept confidential until a fix is released, please follow the steps outlined in the GitHub documentation: https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability
+
+- **Detailed Description:** Provide a detailed description of the vulnerability, including steps to reproduce it and potential impact.
+- **Confidentiality:** Keep the vulnerability confidential until a fix is released to prevent exploitation.
+
+We will acknowledge your report and work on a fix with the following goals:
+
+- **Timely Response:** We will address reported vulnerabilities as quickly as possible.
+- **Transparency:** We will keep you informed of the progress towards a fix and its estimated release date.
+- **Fix Release:** We will release a fix for the vulnerability in a timely manner.
+
+**2. Secure Coding Practices:**
+
+The script development follows best practices for secure coding to minimize vulnerabilities. These practices include:
+
+- **Input Validation:** User input is sanitized to prevent injection attacks (e.g., SQL injection, XSS).
+- **Dependency Management:** Dependencies are kept up-to-date to address known vulnerabilities in external libraries.
+- **Secret Handling:** Sensitive information (if any) is not stored in plain text.
+
+**3. Supported Versions:**
+
+We will only provide security fixes for the most recent versions of the bot. Users are encouraged to stay up-to-date with the latest releases to benefit from the latest security improvements.
+
+**4. Disclaimer:**
+
+While we strive to maintain the security of this script through development practices, it's provided as-is and we cannot guarantee that it is completely free of vulnerabilities. Users are encouraged to exercise caution when using any automated tools that interact with websites.
+
+**5. Responsible Use:**
+
+This script is intended for automating appointment checks on a public website. Users are responsible for using the script in a compliant and ethical manner, respecting robots.txt and terms of service of VFS Global's website.
+
+**6. Reporting Abuses:**
+
+If you suspect any misuse of this script for malicious purposes, please contact the project maintainer immediately.
+
+We appreciate your cooperation in using this script responsibly!
diff --git a/assets/vfs-appointment-form.png b/assets/vfs-appointment-form.png
deleted file mode 100644
index aeb1c9c..0000000
Binary files a/assets/vfs-appointment-form.png and /dev/null differ
diff --git a/config/config.ini b/config/config.ini
index 0eb5056..5a0d06d 100644
--- a/config/config.ini
+++ b/config/config.ini
@@ -1,23 +1,30 @@
-[DEFAULT]
-interval = 120
-use_telegram = False
-use_twilio = True
+[default]
+interval = 180
 
-[VFS]
-vfs_login_url = https://visa.vfsglobal.com/ind/en/deu/login
-vfs_email = email-id
-vfs_password = password
+[browser]
+type = firefox
 
-[TELEGRAM]
-chat_id = -12345678
+[notification]
+channels = email
+
+[vfs-credential]
+email = email
+password = password
+
+[telegram]
+chat_id = chat_id
 parse_mode = Markdown
-bot_token = 98765432:ABCdeFghIjkLMnoPQRSTuv123456789
+bot_token = bot_token
+
+[twilio]
+url = twilio-url
+auth_token = auth-token
+account_sid = account-sid
+sms_enabled = True
+call_enabled = False
+to_num = to-number
+from_num = from-number
 
-[TWILIO]
-twilio_account_sid = account-sid
-twilio_auth_token = auth-token
-twilio_to_num = to-number
-twilio_from_num = from-number
-twilio_url = twilio-url
-twilio_sms_enabled = True
-twilio_call_enabled = True
+[email]
+email = gmail
+password = app-password
\ No newline at end of file
diff --git a/config/logging.ini b/config/logging.ini
deleted file mode 100644
index 0649e4f..0000000
--- a/config/logging.ini
+++ /dev/null
@@ -1,32 +0,0 @@
-[loggers]
-keys=root
-
-[logger_root]
-level=DEBUG
-handlers=screen,file
-
-[formatters]
-keys=simple,verbose
-
-[formatter_simple]
-format=%(asctime)s: %(message)s
-
-[formatter_verbose]
-format=[%(asctime)s] %(levelname)s [%(filename)s %(name)s %(funcName)s (%(lineno)d)]: %(message)s
-
-[handlers]
-keys=file,screen
-
-[handler_file]
-class=handlers.TimedRotatingFileHandler
-interval=midnight
-backupCount=5
-formatter=verbose
-level=DEBUG
-args=('app.log',)
-
-[handler_screen]
-class=StreamHandler
-formatter=simple
-level=INFO
-args=(sys.stdout,)
diff --git a/config/vfs_urls.ini b/config/vfs_urls.ini
new file mode 100644
index 0000000..c7d61f3
--- /dev/null
+++ b/config/vfs_urls.ini
@@ -0,0 +1,3 @@
+[vfs-url]
+IN-DE = https://visa.vfsglobal.com/ind/en/deu/login
+IQ-DE = https://visa.vfsglobal.com/irq/en/deu/login
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..77c8e4b
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,35 @@
+# Project metadata
+[tool.poetry]
+name = "vfs-appointment-bot"
+version = "1.0.0-rc1"
+description = "VFS Appointment Bot - This script automates checking for appointments at VFS Global offices in a specified country."
+authors = ["Ranjan Mohanty <ranjan@duck.com>"]
+license = "MIT"
+readme = "README.md"
+keywords = ["vfs", "vfs-bot", "vfs-appointment-bot", "visa-appointment-bot"]
+
+# URLs
+[tool.poetry.urls]
+repository = "https://github.com/ranjan-mohanty/vfs-appointment-bot/blob/main/README.md"
+homepage = "https://github.com/ranjan-mohanty/vfs-appointment-bot/blob/main"
+
+# Build System
+[build-system]
+requires = ["poetry-core"]
+build-backend = "poetry.core.masonry.api"
+
+# Dependencies
+[tool.poetry.dependencies]
+python = "^3.9"
+playwright = "^1.43.0"
+playwright-stealth = "^1.0.6"
+twilio = "^9.0.4"
+tqdm = "^4.66.2"
+
+[tool.poetry.dev-dependencies]
+flake8 = "^4.0.1"
+black = "^22.3.2"
+
+#Entry points
+[tool.poetry.scripts]
+vfs-appointment-bot = "vfs_appointment_bot.main:main"
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index 15d560b..0000000
--- a/requirements.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-selenium==3.141.0
-twilio==7.2.0
\ No newline at end of file
diff --git a/vfs_appointment_bot/_ConfigReader.py b/vfs_appointment_bot/_ConfigReader.py
deleted file mode 100644
index bce30e0..0000000
--- a/vfs_appointment_bot/_ConfigReader.py
+++ /dev/null
@@ -1,19 +0,0 @@
-import logging
-
-from configparser import ConfigParser
-
-class _ConfigReader:
-    
-    def __init__(self):
-        self.config_object = ConfigParser()
-        self.config_object.read("config/config.ini")
-
-    def read_prop(self, section_header, prop_name):
-        value = self.config_object.get(section_header,prop_name)
-        logging.debug("{}:{}".format(prop_name, value))
-        return value
-
-    def read_bool_prop(self, section_header, prop_name):
-        value = self.config_object.getboolean(section_header,prop_name)
-        logging.debug("{}:{}".format(prop_name, value))
-        return value
\ No newline at end of file
diff --git a/vfs_appointment_bot/_TelegramClient.py b/vfs_appointment_bot/_TelegramClient.py
deleted file mode 100644
index c163490..0000000
--- a/vfs_appointment_bot/_TelegramClient.py
+++ /dev/null
@@ -1,24 +0,0 @@
-import logging
-import requests
-from _ConfigReader import _ConfigReader
-
-class _TelegramClient:
-
-    def __init__(self):
-        _config_reader = _ConfigReader()
-        _section_header = "TELEGRAM"
-        self.chat_id = _config_reader.read_prop(_section_header, "chat_id")
-        self.parse_mode = _config_reader.read_prop(_section_header, "parse_mode")
-        self.bot_token = _config_reader.read_prop(_section_header,"bot_token")
-
-    def send_message(self, message):
-        url = f"https://api.telegram.org/bot{self.bot_token}/sendMessage?chat_id={self.chat_id}&parse_mode={self.parse_mode}&text={message}"
-
-        logging.debug("Sending message")
-        try:
-            # sends message
-            response_json = requests.get(url).json()
-            logging.debug(str(response_json))
-            logging.debug("Message sent")
-        except Exception as e:
-            print("Some problem occured in sending message via telegram: {}".format(e))
diff --git a/vfs_appointment_bot/_Timer.py b/vfs_appointment_bot/_Timer.py
deleted file mode 100644
index c820b33..0000000
--- a/vfs_appointment_bot/_Timer.py
+++ /dev/null
@@ -1,8 +0,0 @@
-import time
-
-def countdown(t):
-    while t > -1:
-        timer = 'Retry after {:02d} seconds'.format(t)
-        print(timer, end="\r")
-        time.sleep(1)
-        t -= 1
\ No newline at end of file
diff --git a/vfs_appointment_bot/_TwilioClient.py b/vfs_appointment_bot/_TwilioClient.py
deleted file mode 100644
index c28acad..0000000
--- a/vfs_appointment_bot/_TwilioClient.py
+++ /dev/null
@@ -1,44 +0,0 @@
-import logging
-
-from twilio.rest import Client
-from _ConfigReader import _ConfigReader
-
-class _TwilioClient:
-
-    def __init__(self):
-        _config_reader = _ConfigReader()
-        _section_header = "TWILIO"
-        self.to_num = _config_reader.read_prop(_section_header, "twilio_to_num")
-        self.from_num = _config_reader.read_prop(_section_header, "twilio_from_num")
-        self.account_sid = _config_reader.read_prop(_section_header,"twilio_account_sid")
-        self.auth_token = _config_reader.read_prop(_section_header, "twilio_auth_token")
-        self.url = _config_reader.read_prop(_section_header,"twilio_url")
-        self.sms_enabled = _config_reader.read_bool_prop(_section_header, "twilio_sms_enabled")
-        self.call_enabled = _config_reader.read_bool_prop(_section_header, "twilio_call_enabled")
-
-    def send_message(self, message):
-        if(not self.sms_enabled):
-            logging.debug("SMS is not enabled")
-            return
-
-        logging.debug("Sending message")
-        try:
-            client = Client(self.account_sid, self.auth_token)
-            client.messages.create(to=self.to_num, from_=self.from_num, body=message)
-            logging.debug("Message sent")
-        except Exception as e:
-            print("Some problem occured in sending message via Twilio: {}".format(e))
-
-
-    def call(self):
-        if(not self.call_enabled):
-            logging.debug("Call is not enabled")
-            return
-
-        logging.debug("Calling")
-        try:
-            client = Client(self.account_sid, self.auth_token)
-            call = client.calls.create(from_=self.from_num, to=self.to_num, url=self.url)
-            logging.debug("Call initiated")
-        except Exception as e:
-            print("Some problem occured in calling via Twilio: {}".format(e))
diff --git a/vfs_appointment_bot/_VfsClient.py b/vfs_appointment_bot/_VfsClient.py
deleted file mode 100644
index db6c8f1..0000000
--- a/vfs_appointment_bot/_VfsClient.py
+++ /dev/null
@@ -1,170 +0,0 @@
-from cmath import exp
-import email
-import time
-import logging
-import datetime
-
-from _ConfigReader import _ConfigReader
-from _TwilioClient import _TwilioClient
-from _TelegramClient import _TelegramClient
-
-
-from selenium import webdriver
-from selenium.webdriver.firefox.options import Options
-from selenium.common.exceptions import NoSuchElementException
-
-class _VfsClient:
-
-    def __init__(self):
-        self._twilio_client = _TwilioClient()
-        self._telegram_client = _TelegramClient()
-        self._config_reader = _ConfigReader()
-
-        self._use_telegram = self._config_reader.read_prop("DEFAULT", "use_telegram")
-        self._use_twilio = self._config_reader.read_prop("DEFAULT", "use_twilio")
-        logging.debug("Will use Telegram : {}".format(self._use_telegram))
-        logging.debug("Will use Twilio : {}".format(self._use_twilio))
-
-    def _init_web_driver(self):
-        firefox_options = Options()
-
-        # open in headless mode to run in background
-        firefox_options.headless = True
-        # firefox_options.add_argument("start-maximized")
-
-        # following options reduce the RAM usage
-        firefox_options.add_argument("disable-infobars")
-        firefox_options.add_argument("--disable-extensions")
-        firefox_options.add_argument("--no-sandbox")
-        firefox_options.add_argument("--disable-application-cache")
-        firefox_options.add_argument("--disable-gpu")
-        firefox_options.add_argument("--disable-dev-shm-usage")
-        self._web_driver = webdriver.Firefox(options=firefox_options)
-
-        # make sure that the browser is full screen,
-        # else some buttons will not be visible to selenium
-        self._web_driver.maximize_window()
-
-    def _login(self):
-
-        _section_header = "VFS"
-        _email = self._config_reader.read_prop(_section_header, "vfs_email");
-        _password = self._config_reader.read_prop(_section_header, "vfs_password");
-
-        logging.debug("Logging in with email: {}".format(_email))
-
-        # logging in
-        time.sleep(10)
-
-        # sleep provides sufficient time for all the elements to get visible
-        _email_input = self._web_driver.find_element_by_xpath("//input[@id='mat-input-0']")
-        _email_input.send_keys(_email)
-        _password_input = self._web_driver.find_element_by_xpath("//input[@id='mat-input-1']")
-        _password_input.send_keys(_password)
-        _login_button = self._web_driver.find_element_by_xpath("//button/span")
-        _login_button.click()
-        time.sleep(10)
-
-    def _validate_login(self):
-        try:
-            _new_booking_button = self._web_driver.find_element_by_xpath("//section/div/div[2]/button/span")
-            if _new_booking_button == None:
-                logging.debug("Unable to login. VFS website is not responding")
-                raise Exception("Unable to login. VFS website is not responding")
-            else:
-                logging.debug("Logged in successfully")
-        except:
-            logging.debug("Unable to login. VFS website is not responding")
-            raise Exception("Unable to login. VFS website is not responding")
-
-    def _get_appointment_date(self, visa_centre, category, sub_category):
-        logging.info("Getting appointment date: Visa Centre: {}, Category: {}, Sub-Category: {}".format(visa_centre, category, sub_category))
-        # select from drop down
-        _new_booking_button = self._web_driver.find_element_by_xpath(
-            "//section/div/div[2]/button/span"
-        )
-        _new_booking_button.click()
-        time.sleep(5)
-        _visa_centre_dropdown = self._web_driver.find_element_by_xpath(
-            "//mat-form-field/div/div/div[3]"
-        )
-        _visa_centre_dropdown.click()
-        time.sleep(2)
-
-        try:
-            _visa_centre = self._web_driver.find_element_by_xpath(
-                "//mat-option[starts-with(@id,'mat-option-')]/span[contains(text(), '{}')]".format(visa_centre)
-            )
-        except NoSuchElementException:
-            raise Exception("Visa centre not found: {}".format(visa_centre))
-
-        logging.debug("VFS Centre: " + _visa_centre.text)
-        self._web_driver.execute_script("arguments[0].click();", _visa_centre)
-        time.sleep(5)
-
-        _category_dropdown = self._web_driver.find_element_by_xpath(
-            "//div[@id='mat-select-value-3']"
-        )
-        _category_dropdown.click()
-        time.sleep(5)
-
-        try:
-            _category = self._web_driver.find_element_by_xpath(
-                "//mat-option[starts-with(@id,'mat-option-')]/span[contains(text(), '{}')]".format(category)
-            )
-        except NoSuchElementException:
-            raise Exception("Category not found: {}".format(category))
-
-        logging.debug("Category: " + _category.text)
-        self._web_driver.execute_script("arguments[0].click();", _category)
-        time.sleep(5)
-
-        _subcategory_dropdown = self._web_driver.find_element_by_xpath(
-            "//div[@id='mat-select-value-5']"
-        )
-
-        self._web_driver.execute_script("arguments[0].click();", _subcategory_dropdown)
-        time.sleep(5)
-
-        try:
-            _subcategory = self._web_driver.find_element_by_xpath(
-                "//mat-option[starts-with(@id,'mat-option-')]/span[contains(text(), '{}')]".format(sub_category)
-            )
-        except NoSuchElementException:
-            raise Exception("Sub-category not found: {}".format(sub_category))
-
-        self._web_driver.execute_script("arguments[0].click();", _subcategory)
-        logging.debug("Sub-Cat: " + _subcategory.text)
-        time.sleep(5)
-
-        # read contents of the text box
-        return self._web_driver.find_element_by_xpath("//div[4]/div")
-
-    def check_slot(self, visa_centre, category, sub_category):
-        self._init_web_driver()
-
-        # open the webpage
-        self.vfs_login_url = self._config_reader.read_prop("VFS", "vfs_login_url")
-        self._web_driver.get(self.vfs_login_url)
-
-        self._login()
-        self._validate_login()
-
-        _message = self._get_appointment_date(visa_centre, category, sub_category)
-        logging.debug("Message: " + _message.text)
-
-        if len(_message.text) != 0 and _message.text != "No appointment slots are currently available" and _message.text != "Currently No slots are available for selected category, please confirm waitlist\nTerms and Conditions":
-            logging.info("Appointment slots available: {}".format(_message.text))
-            ts = time.time()
-            st = datetime.datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M:%S")
-            message = "{} at {}".format(_message.text, st)
-            if eval(self._use_telegram):
-                self._telegram_client.send_message(message)
-            if eval(self._use_twilio):
-                self._twilio_client.send_message(message)
-                self._twilio_client.call()
-        else:
-            logging.info("No slots available")
-        # Close the browser
-        self._web_driver.close()
-        self._web_driver.quit()
diff --git a/vfs_appointment_bot/main.py b/vfs_appointment_bot/main.py
new file mode 100644
index 0000000..c3ca6d6
--- /dev/null
+++ b/vfs_appointment_bot/main.py
@@ -0,0 +1,122 @@
+import argparse
+import logging
+import sys
+from typing import Dict
+
+from vfs_appointment_bot.utils.config_reader import get_config_value
+from vfs_appointment_bot.utils.timer import countdown
+from vfs_appointment_bot.vfs_bot.vfs_bot import LoginError
+from vfs_appointment_bot.vfs_bot.vfs_bot_factory import (
+    UnsupportedCountryError,
+    get_vfs_bot,
+)
+
+
+class KeyValueAction(argparse.Action):
+    """Custom action class for parsing appointment parameters.
+
+    This class handles parsing comma-separated key-value pairs provided through
+    the `--appointment-params` argument. It ensures the format is valid (key=value)
+    and stores the parsed parameters as a dictionary.
+    """
+
+    def __call__(self, parser, namespace, values, option_string=None):
+        try:
+            appointment_params: Dict[str, str] = {
+                key.strip(): value.strip()
+                for key, value in (item.split("=") for item in values.split(","))
+            }
+            setattr(namespace, "appointment_params", appointment_params)
+        except ValueError:
+            parser.error(
+                f"Invalid value format for {option_string}, use key=value pairs"
+            )
+
+
+def main() -> None:
+    """
+    Entry point for the VFS Appointment Bot.
+
+    This function sets up logging, parses command-line arguments, and runs the VFS appointment
+    checking process in a continuous loop. It catches exceptions for unsupported countries and
+    unexpected errors, logging them appropriately.
+
+    Raises:
+        UnsupportedCountryError: If the provided country code is not supported by the bot.
+        Exception: For any other unexpected errors encountered during execution.
+    """
+    initialize_logger()
+
+    parser = argparse.ArgumentParser(
+        description="VFS Appointment Bot: Checks for appointments at VFS Global"
+    )
+    required_args = parser.add_argument_group("required arguments")
+    required_args.add_argument(
+        "-sc",
+        "--source-country-code",
+        type=str,
+        help="The ISO 3166-1 alpha-2 source country code (refer to README)",
+        metavar="<country_code>",
+        required=True,
+    )
+
+    required_args.add_argument(
+        "-dc",
+        "--destination-country-code",
+        type=str,
+        help="The ISO 3166-1 alpha-2 destination country code (refer to README)",
+        metavar="<country_code>",
+        required=True,
+    )
+
+    parser.add_argument(
+        "-ap",
+        "--appointment-params",
+        type=str,
+        default=None,
+        help="Comma-separated key-value pairs for additional appointment details (refer to VFS website)",
+        action=KeyValueAction,
+        metavar="<key1=value1,key2=value2,...>",
+    )
+
+    args = parser.parse_args()
+    source_country_code = args.source_country_code
+    destination_country_code = args.destination_country_code
+    try:
+        while True:
+            vfs_bot = get_vfs_bot(source_country_code, destination_country_code)
+            appointment_found = vfs_bot.run(args)
+            if appointment_found:
+                break
+            countdown(
+                get_config_value("default", "interval"), "Next appointment check in"
+            )
+
+    except (UnsupportedCountryError, LoginError) as e:
+        logging.error(e)
+    except Exception as e:
+        logging.exception(e)
+
+
+def initialize_logger():
+    file_handler = logging.FileHandler("app.log", mode="a")
+    file_handler.setFormatter(
+        logging.Formatter(
+            "[%(asctime)s] %(levelname)s [%(filename)s:%(lineno)d] %(message)s"
+        )
+    )
+
+    stream_handler = logging.StreamHandler(sys.stdout)
+    stream_handler.setFormatter(logging.Formatter("[%(asctime)s] %(message)s"))
+    logging.basicConfig(
+        level=logging.INFO,
+        format="[%(asctime)s] %(levelname)s [%(filename)s:%(lineno)d] %(message)s",
+        handlers=[
+            file_handler,
+            stream_handler,
+        ],
+    )
+
+
+if __name__ == "__main__":
+    main()
diff --git a/vfs_appointment_bot/notification/email_client.py b/vfs_appointment_bot/notification/email_client.py
new file mode 100644
index 0000000..bda5278
--- /dev/null
+++ b/vfs_appointment_bot/notification/email_client.py
@@ -0,0 +1,53 @@
+import smtplib
+import logging
+
+from vfs_appointment_bot.notification.notification_client import NotificationClient
+
+
+class EmailClient(NotificationClient):
+    def __init__(self):
+        """
+        Initializes the email client with configuration data.
+
+        This constructor retrieves configuration settings from the designated
+        section (e.g., `"email"`) of the application configuration and
+        validates them using the base class validation logic.
+        """
+        required_keys = ["email", "password"]
+        super().__init__("email", required_keys)
+
+    def send_notification(self, message: str) -> None:
+        """
+        Sends a notification message through the email channel.
+
+        This method sends an email notification using the provided message content.
+        It connects securely to the configured SMTP server (e.g., Gmail's SMTP),
+        authenticates with the provided credentials, and constructs a well-formatted
+        email before sending it.
+
+        Args:
+            message (str): The message content to be included in the email.
+        """
+        email: str = self.config.get("email")
+        password: str = self.config.get("password")
+        email_text = self.__construct_email_text(email, message)
+
+        smtp_server = smtplib.SMTP_SSL("smtp.gmail.com", 465)
+        smtp_server.ehlo()
+        smtp_server.login(email, password)
+        smtp_server.sendmail(email, email, email_text)
+        smtp_server.close()
+        logging.info("Email sent successfully!")
+
+    def __construct_email_text(self, email: str, message: str) -> str:
+        """
+        Constructs a formatted email text with sender, receiver, subject,
+        and message body.
+
+        Args:
+            message (str): The message content to be included in the email body.
+
+        Returns:
+            str: The formatted email text ready for sending.
+        """
+        return f"From: {email}\nTo: {email}\nSubject: VFS Appointment Bot Notification\n\n{message}"
diff --git a/vfs_appointment_bot/notification/notification_client.py b/vfs_appointment_bot/notification/notification_client.py
new file mode 100644
index 0000000..a1126d5
--- /dev/null
+++ b/vfs_appointment_bot/notification/notification_client.py
@@ -0,0 +1,73 @@
+from abc import ABC, abstractmethod
+from typing import List
+
+from vfs_appointment_bot.utils.config_reader import get_config_section
+
+
+class NotificationClient(ABC):
+    """Abstract base class for notification clients.
+
+    This class defines the common interface for notification clients used
+    throughout the application. Subclasses must implement the `send_notification`
+    method to provide specific notification sending functionality for their
+    respective channels.
+    """
+
+    def __init__(self, config_section: str, required_config_keys: List[str]):
+        """
+        Initializes the client with configuration data.
+
+        Args:
+            config_section (str): The name of the configuration section
+                containing client-specific settings.
+            required_config_keys (list[str]): A list of required keys that must be
+                present in the configuration section.
+        """
+        self.required_keys = required_config_keys
+        self.config = get_config_section(config_section)
+        self._validate_config(required_config_keys)
+
+    @abstractmethod
+    def send_notification(self, message: str) -> None:
+        """
+        Sends a notification message to the recipient.
+
+        This method is abstract and must be implemented by subclasses to
+        provide the specific logic for sending notifications through their
+        respective channels.
+
+        Args:
+            message (str): The message content to be sent.
+        """
+
+    def _validate_config(self, required_config_keys: list[str]):
+        """
+        Validates the configuration of the notification client.
+
+        This method checks if all required configuration keys are present
+        and have non-null values. If any validation errors are found,
+        appropriate exceptions are raised.
+
+        Args:
+            required_config_keys (list[str]): A list of required keys that must be
+                present in the configuration section.
+        """
+        missing_keys = required_config_keys - self.config.keys()
+        if missing_keys:
+            raise NotificationClientConfigValidationError(
+                f"Missing required configuration keys: {', '.join(missing_keys)}"
+            )
+
+        for key in self.required_keys:
+            if self.config.get(key) is None:
+                raise NotificationClientConfigValidationError(
+                    f"Value for key '{key}' cannot be null."
+                )
+
+
+class NotificationClientConfigValidationError(Exception):
+    """Exception raised when notification client configuration validation fails."""
+
+
+class NotificationClientError(Exception):
+    """Exception raised when an error occurs during notification sending."""
diff --git a/vfs_appointment_bot/notification/notification_client_factory.py b/vfs_appointment_bot/notification/notification_client_factory.py
new file mode 100644
index 0000000..677edea
--- /dev/null
+++ b/vfs_appointment_bot/notification/notification_client_factory.py
@@ -0,0 +1,41 @@
+from vfs_appointment_bot.notification.notification_client import NotificationClient
+
+
+class UnsupportedNotificationChannelError(Exception):
+    """Raised when an unsupported notification channel is provided."""
+
+
+def get_notification_client(channel: str) -> NotificationClient:
+    """Retrieves the appropriate notification client for a given channel.
+
+    This function creates an instance of a notification client class based on the
+    provided channel string. Currently supported channels include "telegram" and
+    "slack". If an unsupported channel is provided, a `ValueError` exception is
+    raised.
+
+    Args:
+        channel (str): The notification channel name.
+
+    Returns:
+        NotificationClient: An instance of the `NotificationClient` sub class specific to the provided channel.
+
+    Raises:
+        UnsupportedNotificationChannelError: If the provided notification channel is not supported.
+    """
+
+    if channel == "telegram":
+        from .telegram_client import TelegramClient
+
+        return TelegramClient()
+    elif channel == "slack":
+        from .twilio_client import TwilioClient
+
+        return TwilioClient()
+    elif channel == "email":
+        from .email_client import EmailClient
+
+        return EmailClient()
+    else:
+        raise UnsupportedNotificationChannelError(
+            f"Notification channel '{channel}' is not supported"
+        )
diff --git a/vfs_appointment_bot/notification/telegram_client.py b/vfs_appointment_bot/notification/telegram_client.py
new file mode 100644
index 0000000..bde5d02
--- /dev/null
+++ b/vfs_appointment_bot/notification/telegram_client.py
@@ -0,0 +1,49 @@
+import logging
+
+import requests
+
+from vfs_appointment_bot.notification.notification_client import NotificationClient
+
+
+class TelegramClient(NotificationClient):
+    """Concrete implementation of NotificationClient for the Telegram channel.
+
+    This class provides functionality for sending notifications through the Telegram
+    messaging platform. It inherits from the abstract `NotificationClient` class
+    and implements the required `send_notification` method for Telegram-specific
+    notification sending logic.
+    """
+
+    def __init__(self):
+        """
+        Initializes the Telegram client with configuration data.
+
+        This constructor retrieves configuration settings from the "telegram"
+        section of the application configuration and validates them using the
+        base class validation logic.
+        """
+        required_keys = ["bot_token", "chat_id", "parse_mode"]
+        super().__init__("telegram", required_keys)
+
+    def send_notification(self, message: str) -> None:
+        """
+        Sends a notification message through the Telegram channel.
+
+        This method constructs a Telegram API request URL using the retrieved
+        configuration settings (bot token, chat ID, and parse mode) and sends a
+        GET request to the Telegram API with the message content. The response
+        from the Telegram API is logged for debugging purposes.
+
+        Args:
+            message (str): The message content to be sent as a Telegram notification.
+        """
+        bot_token: str = self.config.get("bot_token")
+        chat_id: str = self.config.get("chat_id")
+        parse_mode: str = self.config.get("parse_mode")
+
+        url = (
+            f"https://api.telegram.org/bot{bot_token}/sendMessage?"
+            + f"chat_id={chat_id}&parse_mode={parse_mode}&text={message}"
+        )
+        requests.get(url, timeout=3000).json()
+        logging.info("Telegram message sent successfully!")
diff --git a/vfs_appointment_bot/notification/twilio_client.py b/vfs_appointment_bot/notification/twilio_client.py
new file mode 100644
index 0000000..fdfab83
--- /dev/null
+++ b/vfs_appointment_bot/notification/twilio_client.py
@@ -0,0 +1,113 @@
+import logging
+from typing import Optional
+
+from twilio.rest import Client
+
+from vfs_appointment_bot.notification.notification_client import NotificationClient
+
+
+class TwilioClient(NotificationClient):
+    """Concrete implementation of NotificationClient for the Twilio channel.
+
+    This class provides functionality for sending notifications through the Twilio
+    communication platform. It inherits from the abstract `NotificationClient` class
+    and implements the required `send_notification` method for Twilio-specific
+    notification sending logic, including SMS messages and calls (if enabled).
+    """
+
+    def __init__(self):
+        """
+        Initializes the Twilio client with configuration data.
+
+        This constructor retrieves configuration settings from the "twilio"
+        section of the application configuration and validates them using the
+        base class validation logic.
+        """
+        required_config_keys = [
+            "to_num",
+            "from_num",
+            "account_sid",
+            "auth_token",
+            "url",
+            "call_enabled",
+        ]
+        super().__init__("twilio", required_config_keys)
+
+    def send_notification(self, message: str) -> None:
+        """
+        Sends a notification message through the Twilio channel.
+
+        This method sends an SMS message using the provided message content.
+        Optionally, if the "call_enabled" flag is set to True in the
+        configuration, it also initiates a call to the specified phone number
+        using a pre-recorded URL (provided by the "url" configuration option).
+
+        Args:
+            message (str): The message content to be sent as a Twilio SMS.
+        """
+        url: Optional[str] = self.config.get("url")
+        auth_token: str = self.config.get("auth_token")
+        account_sid: str = self.config.get("account_sid")
+        to_num: str = self.config.get("to_num")
+        from_num: str = self.config.get("from_num")
+        call_enabled: bool = self.config.get("call_enabled", False)
+
+        self.__send_message(message, auth_token, account_sid, to_num, from_num)
+
+        if call_enabled:
+            self.__call(url, auth_token, account_sid, to_num, from_num)
+
+    def __send_message(
+        self,
+        message: str,
+        auth_token: str,
+        account_sid: str,
+        to_num: str,
+        from_num: str,
+    ) -> None:
+        """
+        Sends an SMS message using the Twilio API.
+
+        This private helper method creates a Twilio client instance and uses it
+        to send an SMS message with the provided content to the specified recipient
+        phone number.
+
+        Args:
+            message (str): The message content to be sent.
+            auth_token (str): The Twilio account authentication token.
+            account_sid (str): The Twilio account SID.
+            to_num (str): The recipient phone number.
+            from_num (str): The Twilio phone number used to send the message.
+        """
+        client = Client(account_sid, auth_token)
+        client.messages.create(to=to_num, from_=from_num, body=message)
+        logging.info("Message sent successfully!")
+
+    def __call(
+        self,
+        url: Optional[str],
+        auth_token: str,
+        account_sid: str,
+        to_num: str,
+        from_num: str,
+    ) -> None:
+        """
+        Initiates a call using the Twilio API (if URL is provided).
+
+        This private helper method creates a Twilio client instance and uses it
+        to initiate a call to the specified recipient phone number, using a
+        pre-recorded URL for the call content (if provided in the configuration).
+
+        Args:
+            url (Optional[str]): The URL for the pre-recorded call content.
+            auth_token (str): The Twilio account authentication token.
+            account_sid (str): The Twilio account SID.
+            to_num (str): The recipient phone number.
+            from_num (str): The Twilio phone number used to initiate the call.
+        """
+        if url:
+            client = Client(account_sid, auth_token)
+            client.calls.create(from_=from_num, to=to_num, url=url)
+            logging.info("Call request sent successfully!")
+        else:
+            logging.warning("No URL provided for call request!")
diff --git a/vfs_appointment_bot/utils/config_reader.py b/vfs_appointment_bot/utils/config_reader.py
new file mode 100644
index 0000000..3552394
--- /dev/null
+++ b/vfs_appointment_bot/utils/config_reader.py
@@ -0,0 +1,64 @@
+import os
+from configparser import ConfigParser
+from typing import Dict
+
+_config = None
+
+
+def get_config_parser(config_dir="config"):
+    """
+    Reads all INI configuration files in a directory and caches the result.
+
+    Args:
+        config_dir: The directory containing configuration files (default: "config").
+
+    Returns:
+        A ConfigParser object loaded with configuration data.
+    """
+    global _config
+    if not _config:
+        _config = ConfigParser()
+        for entry in os.scandir(config_dir):
+            if entry.is_file() and entry.name.endswith(".ini"):
+                config_file_path = os.path.join(config_dir, entry.name)
+                _config.read(config_file_path)
+    return _config
+
+
+def get_config_section(section: str, default: Dict = None) -> Dict:
+    """
+    Get a configuration section as a dictionary.
+
+    Args:
+        section: The name of the section to retrieve.
+        default: A dictionary containing default values for the section (optional).
+
+    Returns:
+        A dictionary containing the configuration for the specified section,
+        or the provided default dictionary if the section is not found.
+    """
+    config = get_config_parser()
+    if config.has_section(section):
+        return dict(config[section])
+    else:
+        return default or {}
+
+
+def get_config_value(section: str, key: str, default: str = None) -> str:
+    """
+    Get a specific configuration value.
+
+    Args:
+        section: The name of the section containing the value.
+        key: The name of the key to retrieve.
+        default: The default value to return if the section or key is not found (optional).
+
+    Returns:
+        The value associated with the given key within the specified section,
+        or the provided default value if the section or key does not exist.
+    """
+    config = get_config_parser()
+    if config.has_section(section) and config.has_option(section, key):
+        return config[section][key]
+    else:
+        return default
diff --git a/vfs_appointment_bot/utils/date_utils.py b/vfs_appointment_bot/utils/date_utils.py
new file mode 100644
index 0000000..a0eab11
--- /dev/null
+++ b/vfs_appointment_bot/utils/date_utils.py
@@ -0,0 +1,10 @@
+import re
+
+
+def extract_date_from_string(text):
+    pattern = r"(\d{4}-\d{2}-\d{2}|\d{2}-\d{2}-\d{4}|\d{2}-\d{2}-\d{2})"
+    match = re.search(pattern, text)
+    if match:
+        return match.group()
+    else:
+        return None
diff --git a/vfs_appointment_bot/utils/timer.py b/vfs_appointment_bot/utils/timer.py
new file mode 100644
index 0000000..a4fc5a4
--- /dev/null
+++ b/vfs_appointment_bot/utils/timer.py
@@ -0,0 +1,21 @@
+import time
+
+from tqdm import tqdm
+
+
+def countdown(t, message="Countdown", unit="seconds"):
+    """
+    Implements a countdown timer
+
+    Args:
+        t (int): The initial countdown time in the specified unit (e.g., seconds, minutes).
+        message (str, optional): The message to display during the countdown.
+                                Defaults to "Countdown".
+        unit (str, optional): The unit of time for the countdown. Defaults to "seconds".
+    """
+    with tqdm(
+        total=t, desc=message, unit=unit, bar_format="{desc}: {remaining}"
+    ) as pbar:
+        for _ in range(t):
+            time.sleep(1)
+            pbar.update(1)
diff --git a/vfs_appointment_bot/vfs_appointment_bot.py b/vfs_appointment_bot/vfs_appointment_bot.py
deleted file mode 100644
index 4c3256a..0000000
--- a/vfs_appointment_bot/vfs_appointment_bot.py
+++ /dev/null
@@ -1,54 +0,0 @@
-import sys
-import logging
-
-from logging.config import fileConfig
-from _Timer import countdown
-from _ConfigReader import _ConfigReader
-from _VfsClient import _VfsClient
-
-def _input():
-    print("Enter the visa centre: ")
-    visa_centre = input()
-
-    print("Enter the category: ")
-    category = input()
-
-    print("Enter the sub category: ")
-    sub_category = input()
-
-    logging.debug("Visa centre: {}, Category: {}, Sub-Category: {}".format(visa_centre, category, sub_category))
-
-    return visa_centre, category, sub_category
-
-def _read_command_line_args():
-    if len(sys.argv) != 4:
-        return _input()
-    return sys.argv[1], sys.argv[2], sys.argv[3]
-
-
-if __name__ == "__main__":
-    count = 1
-    fileConfig('config/logging.ini')
-    logging = logging.getLogger(__name__);
-
-    _vfs_client = _VfsClient()
-    _config_reader = _ConfigReader()
-    _interval = _config_reader.read_prop("DEFAULT", "interval")
-    logging.debug("Interval: {}".format(_interval))
-
-    visa_centre, category, sub_category = _read_command_line_args()
-
-    logging.info("Starting VFS Appointment Bot")
-    while True:
-        try:
-            logging.info("Running VFS Appointment Bot: Attempt#{}".format(count))
-            _vfs_client.check_slot(visa_centre=visa_centre, category=category, sub_category=sub_category)
-            logging.debug("Sleeping for {} seconds".format(_interval))
-            countdown(int(_interval))
-        except Exception as e:
-            logging.info(e.args[0] + ". Please check the logs for more details")
-            logging.debug(e, exc_info=True, stack_info=True)
-            countdown(int(60))
-            pass
-        print("\n")
-        count += 1
diff --git a/vfs_appointment_bot/vfs_bot/vfs_bot.py b/vfs_appointment_bot/vfs_bot/vfs_bot.py
new file mode 100644
index 0000000..aa1b57e
--- /dev/null
+++ b/vfs_appointment_bot/vfs_bot/vfs_bot.py
@@ -0,0 +1,218 @@
+import argparse
+import logging
+from abc import ABC, abstractmethod
+from typing import Dict, List
+
+import playwright
+from playwright.sync_api import sync_playwright
+from playwright_stealth import stealth_sync
+
+from vfs_appointment_bot.utils.config_reader import get_config_value
+from vfs_appointment_bot.notification.notification_client_factory import (
+    get_notification_client,
+)
+
+
+class LoginError(Exception):
+    """Exception raised when login fails."""
+
+
+class VfsBot(ABC):
+    """
+    Abstract base class for VfsBot
+
+    Provides common functionalities like login, pre-login steps, appointment checking, and notification.
+    Subclasses are responsible for implementing country-specific login and appointment checking logic.
+    """
+
+    def __init__(self):
+        """
+        Initializes a VfsBot instance for a specific country.
+
+        """
+        self.source_country_code = None
+        self.destination_country_code = None
+        self.appointment_param_keys: List[str] = []
+
+    def run(self, args: argparse.Namespace = None) -> bool:
+        """
+        Starts the VFS bot for appointment checking and notification.
+
+        This method reads configuration values, performs login, checks for
+        appointments based on provided arguments, and sends notifications if
+        appointments are found.
+
+        Args:
+            args (argparse.Namespace, optional): Namespace object containing parsed
+                command-line arguments. Defaults to None.
+
+        Returns:
+            bool: True if appointments were found, False otherwise.
+        """
+
+        logging.info(
+            f"Starting VFS Bot for {self.source_country_code.upper()}-{self.destination_country_code.upper()}"
+        )
+
+        # Configuration values
+        try:
+            browser_type = get_config_value("browser", "type", "firefox")
+            url_key = self.source_country_code + "-" + self.destination_country_code
+            vfs_url = get_config_value("vfs-url", url_key)
+        except KeyError as e:
+            logging.error(f"Missing configuration value: {e}")
+            return
+
+        email_id = get_config_value("vfs-credential", "email")
+        password = get_config_value("vfs-credential", "password")
+
+        appointment_params = self.get_appointment_params(args)
+
+        # Launch browser and perform actions
+        with sync_playwright() as p:
+            browser = getattr(p, browser_type).launch(headless=False)
+            page = browser.new_page()
+            stealth_sync(page)
+
+            page.goto(vfs_url)
+            self.pre_login_steps(page)
+
+            try:
+                self.login(page, email_id, password)
+                logging.info("Logged in successfully")
+            except Exception:
+                browser.close()
+                raise LoginError(
+                    "\033[1;31mLogin failed. "
+                    + "Please verify your username and password by logging in to the browser and try again.\033[0m"
+                )
+
+            logging.info(f"Checking appointments for {appointment_params}")
+            appointment_found = False
+            try:
+                dates = self.check_for_appontment(page, appointment_params)
+                if dates:
+                    # Log successful appointment finding
+                    logging.info(
+                        f"\033[1;32mFound appointments on: {', '.join(dates)} \033[0m"
+                    )
+                    self.notify_appointment(appointment_params, dates)
+                    appointment_found = True
+                else:
+                    # Log no appointments found
+                    logging.info(
+                        "\033[1;33mNo appointments found for the specified criteria.\033[0m"
+                    )
+            except Exception as e:
+                logging.error(f"Appointment check failed: {e}")
+            browser.close()
+            return appointment_found
+
+    def get_appointment_params(self, args: argparse.Namespace) -> Dict[str, str]:
+        """
+        Collects appointment parameters from command-line arguments or user input.
+
+        This method iterates through pre-defined `appointment_param_keys` (replace
+        with relevant keys) and retrieves values either from provided arguments
+        or prompts the user for input if values are missing.
+
+        Args:
+            args (argparse.Namespace): Namespace object containing parsed command-line arguments.
+
+        Returns:
+            Dict[str, str]: A dictionary containing appointment parameters.
+        """
+        appointment_params = {}
+        for key in self.appointment_param_keys:
+            if (
+                getattr(args, "appointment_params") is not None
+                and args.appointment_params[key] is not None
+            ):
+                appointment_params[key] = args.appointment_params[key]
+            else:
+                key_name = key.replace("_", " ")
+                appointment_params[key] = input(f"Enter the {key_name}: ")
+        return appointment_params
+
+    def notify_appointment(self, appointment_params: Dict[str, str], dates: List[str]):
+        """
+        Sends appointment dates notification to the user.
+
+        This method is responsible for notifying the appointment dates to the user configured channels
+
+        Args:
+            dates (List[str]): A list of appointment dates.
+            appointment_params (Dict[str, str]): A dictionary containing appointment search criteria.
+        """
+        message = f"Found appointment(s) for {', '.join(appointment_params.values())} on {', '.join(dates)}"
+        channels = get_config_value("notification", "channels")
+        if len(channels) == 0:
+            logging.warning(
+                "No notification channels configured. Skipping notification."
+            )
+            return
+
+        for channel in channels.split(","):
+            client = get_notification_client(channel)
+            try:
+                client.send_notification(message)
+            except Exception:
+                logging.error(f"Failed to send {channel} notification")
+
+    @abstractmethod
+    def login(
+        self, page: playwright.sync_api.Page, email_id: str, password: str
+    ) -> None:
+        """
+        Performs login steps specific to the VFS website for the bot's country.
+
+        This abstract method needs to be implemented by subclasses to handle
+        country-specific login procedures (e.g., filling login form elements, handling
+        CAPTCHAs). It should interact with the Playwright `page` object to achieve
+        login functionality.
+
+        Args:
+            page (playwright.sync_api.Page): The Playwright page object used for browser interaction.
+            email_id (str): The user's email address for VFS login.
+            password (str): The user's password for VFS login.
+
+        Raises:
+            Exception: If login fails due to unexpected errors.
+        """
+        raise NotImplementedError("Subclasses must implement login logic")
+
+    @abstractmethod
+    def pre_login_steps(self, page: playwright.sync_api.Page) -> None:
+        """
+        Performs any pre-login steps required by the VFS website for the bot's country.
+
+        This abstract method allows subclasses to implement country-specific actions
+        that need to be done before login (e.g., cookie acceptance, language selection).
+        It should interact with the Playwright `page` object to perform these actions.
+
+        Args:
+            page (playwright.sync_api.Page): The Playwright page object used for browser interaction.
+        """
+
+    @abstractmethod
+    def check_for_appontment(
+        self, page: playwright.sync_api.Page, appointment_params: Dict[str, str]
+    ) -> List[str]:
+        """
+        Checks for appointments based on provided parameters on the VFS website.
+
+        This abstract method needs to be implemented by subclasses to interact with
+        the VFS website and search for appointments based on the given `appointment_params`
+        dictionary. It should use the Playwright `page` object to navigate the website
+        and extract appointment dates.
+
+        Args:
+            page (playwright.sync_api.Page): The Playwright page object used for browser interaction.
+            appointment_params (Dict[str, str]): A dictionary containing appointment search criteria.
+
+        Returns:
+            List[str]: A list of available appointment dates (empty list if none found).
+        """
+        raise NotImplementedError(
+            "Subclasses must implement appointment checking logic"
+        )
diff --git a/vfs_appointment_bot/vfs_bot/vfs_bot_de.py b/vfs_appointment_bot/vfs_bot/vfs_bot_de.py
new file mode 100644
index 0000000..7290743
--- /dev/null
+++ b/vfs_appointment_bot/vfs_bot/vfs_bot_de.py
@@ -0,0 +1,143 @@
+import logging
+from typing import Dict, List, Optional
+
+from playwright.sync_api import Page
+
+from vfs_appointment_bot.utils.date_utils import extract_date_from_string
+from vfs_appointment_bot.vfs_bot.vfs_bot import VfsBot
+
+
+class VfsBotDe(VfsBot):
+    """Concrete implementation of VfsBot for Germany (DE).
+
+    This class inherits from the base `VfsBot` class and implements
+    country-specific logic for interacting with the VFS website for Germany.
+    It overrides the following methods to handle German website specifics:
+
+    - `login`: Fills the login form elements with email and password.
+    - `pre_login_steps`: Rejects all cookie policies if presented.
+    - `check_for_appontment`: Performs appointment search based on provided
+        parameters and extracts available dates from the website.
+    """
+
+    def __init__(self, source_country_code: str):
+        """
+        Initializes a VfsBotDe instance for Germany.
+
+        This constructor sets the source country code and the destination country
+        code "de"(Germany). It also defines appointment parameter keys specific
+        to the destination country's VFS website.
+
+        Args:
+            source_country_code (str): The country code where you're applying from.
+        """
+        super().__init__()
+        self.source_country_code = source_country_code
+        self.destination_country_code = "de"
+        self.appointment_param_keys = [
+            "visa_center",
+            "visa_category",
+            "visa_sub_category",
+        ]
+
+    def login(self, page: Page, email_id: str, password: str) -> None:
+        """
+        Performs login steps specific to the German VFS website.
+
+        This method fills the email and password input fields on the login form
+        and clicks the "Sign In" button. It raises an exception if the login fails
+        (e.g., if the "Start New Booking" button is not found after login).
+
+        Args:
+            page (playwright.sync_api.Page): The Playwright page object used for browser interaction.
+            email_id (str): The user's email address for VFS login.
+            password (str): The user's password for VFS login.
+
+        Raises:
+            Exception: If login fails due to unexpected errors or missing "Start New Booking" button.
+        """
+        email_input = page.locator("#mat-input-0")
+        password_input = page.locator("#mat-input-1")
+
+        email_input.fill(email_id)
+        password_input.fill(password)
+
+        page.get_by_role("button", name="Sign In").click()
+        page.wait_for_selector("role=button >> text=Start New Booking")
+
+    def pre_login_steps(self, page: Page) -> None:
+        """
+        Performs pre-login steps specific to the German VFS website.
+
+        This method checks for a "Reject All" button for cookie policies and
+        clicks it if found.
+
+        Args:
+            page (playwright.sync_api.Page): The Playwright page object used for browser interaction.
+        """
+        policies_reject_button = page.get_by_role("button", name="Reject All")
+        if policies_reject_button is not None:
+            policies_reject_button.click()
+            logging.debug("Rejected all cookie policies")
+
+    def check_for_appontment(
+        self, page: Page, appointment_params: Dict[str, str]
+    ) -> Optional[List[str]]:
+        """
+        Checks for appointments on the German VFS website based on provided parameters.
+
+        This method clicks the "Start New Booking" button, selects the specified
+        visa center, category, and subcategory based on the `appointment_params`
+        dictionary. It then extracts the available appointment dates from the
+        website and returns them as a list. If no appointments are found, it
+        returns None.
+
+        Args:
+            page (playwright.sync_api.Page): The Playwright page object used for browser interaction.
+            appointment_params (Dict[str, str]): A dictionary containing appointment search criteria.
+
+        Returns:
+            Optional[List[str]]: A list of available appointment dates (empty list if none found),
+                including a timestamp of the check, or None if no appointments found.
+        """
+        page.get_by_role("button", name="Start New Booking").click()
+
+        # Select Visa Centre
+
+        visa_centre_dropdown = page.wait_for_selector("mat-form-field")
+        visa_centre_dropdown.click()
+        visa_centre_dropdown_option = page.wait_for_selector(
+            f'mat-option:has-text("{appointment_params.get("visa_center")}")'
+        )
+        visa_centre_dropdown_option.click()
+
+        # Select Visa Category
+        visa_category_dropdown = page.query_selector_all("mat-form-field")[1]
+        visa_category_dropdown.click()
+        visa_category_dropdown_option = page.wait_for_selector(
+            f'mat-option:has-text("{appointment_params.get("visa_category")}")'
+        )
+        visa_category_dropdown_option.click()
+
+        # Select Subcategory
+        visa_subcategory_dropdown = page.query_selector_all("mat-form-field")[2]
+        visa_subcategory_dropdown.click()
+        visa_subcategory_dropdown_option = page.wait_for_selector(
+            f'mat-option:has-text("{appointment_params.get("visa_sub_category")}")'
+        )
+        visa_subcategory_dropdown_option.click()
+
+        try:
+            page.wait_for_selector("div.alert")
+            appointment_date_elements = page.query_selector_all("div.alert")
+            appointment_dates = []
+            for appointment_date_element in appointment_date_elements:
+                appointment_date_text = appointment_date_element.text_content()
+                appointment_date = extract_date_from_string(appointment_date_text)
+                if appointment_date is not None and len(appointment_date) > 0:
+                    appointment_dates.append(appointment_date)
+            return appointment_dates
+        except Exception:
+            return None
+
+        return None
diff --git a/vfs_appointment_bot/vfs_bot/vfs_bot_factory.py b/vfs_appointment_bot/vfs_bot/vfs_bot_factory.py
new file mode 100644
index 0000000..5ac8e80
--- /dev/null
+++ b/vfs_appointment_bot/vfs_bot/vfs_bot_factory.py
@@ -0,0 +1,35 @@
+from vfs_appointment_bot.vfs_bot.vfs_bot import VfsBot
+
+
+class UnsupportedCountryError(Exception):
+    """Raised when an unsupported country code is provided."""
+
+
+def get_vfs_bot(source_country_code: str, destination_country_code: str) -> VfsBot:
+    """Retrieves the appropriate VfsBot class for a given country.
+
+    This function searches for a matching subclass of `VfsBot` based on the
+    provided destination country code (ISO 3166-1 alpha-2).
+    If no matching class is found, an `UnsupportedCountryError` exception is raised.
+
+    Args:
+        source_country_code (str): The ISO 3166-1 alpha-2 country code where you're applying from.
+        destination_country_code (str): The ISO 3166-1 alpha-2 country code where the appointment is needed.
+
+    Returns:
+        VfsBot: An instance of the `VfsBot` subclass specific to the provided country.
+
+    Raises:
+        UnsupportedCountryError: If the provided country is not supported.
+    """
+
+    country_lower = destination_country_code.lower()
+
+    if country_lower == "de":
+        from .vfs_bot_de import VfsBotDe
+
+        return VfsBotDe(source_country_code)
+    else:
+        raise UnsupportedCountryError(
+            f"Country {destination_country_code} is not supported"
+        )


Test patch:
diff --git a/.github/workflows/publish-testpypi.yml b/.github/workflows/publish-testpypi.yml
new file mode 100644
index 0000000..ec0d170
--- /dev/null
+++ b/.github/workflows/publish-testpypi.yml
@@ -0,0 +1,83 @@
+name: Publish - TestPyPI
+on:
+  push:
+    tags:
+      - "**"
+
+permissions:
+  contents: read
+
+jobs:
+  build:
+    uses: ./.github/workflows/build.yml
+  publish-to-testpypi:
+    name: Publish to TestPyPI
+    needs:
+      - build
+    runs-on: ubuntu-latest
+
+    environment:
+      name: testpypi
+      url: https://test.pypi.org/p/vfs-appointment-bot
+
+    permissions:
+      id-token: write
+
+    steps:
+      - name: Harden Runner
+        uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0
+        with:
+          egress-policy: audit
+
+      - name: Download all the dists
+        uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
+        with:
+          name: python-package-distributions
+          path: dist/
+      - name: Publish distribution to TestPyPI
+        uses: pypa/gh-action-pypi-publish@81e9d935c883d0b210363ab89cf05f3894778450 # release/v1
+        with:
+          repository-url: https://test.pypi.org/legacy/
+  github-release:
+    if: ${{contains(github.ref, '-rc')}}
+    name: Release
+    needs:
+      - publish-to-testpypi
+    runs-on: ubuntu-latest
+
+    permissions:
+      contents: write
+      id-token: write
+
+    steps:
+      - name: Harden Runner
+        uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0
+        with:
+          egress-policy: audit
+
+      - name: Download all the dists
+        uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
+        with:
+          name: python-package-distributions
+          path: dist/
+      - name: Sign the dists with Sigstore
+        uses: sigstore/gh-action-sigstore-python@61f6a500bbfdd9a2a339cf033e5421951fbc1cd2 # v2.1.1
+        with:
+          inputs: >-
+            ./dist/*.tar.gz
+            ./dist/*.whl
+      - name: Create GitHub Release
+        env:
+          GITHUB_TOKEN: ${{ github.token }}
+        run: >-
+          gh release create
+          '${{ github.ref_name }}'
+          --repo '${{ github.repository }}'
+          --prerelease
+      - name: Upload artifact signatures to GitHub Release
+        env:
+          GITHUB_TOKEN: ${{ github.token }}
+        run: >-
+          gh release upload
+          '${{ github.ref_name }}' dist/**
+          --repo '${{ github.repository }}'