Debopam Param
commited on
Commit
·
44af237
1
Parent(s):
0f291df
first commit
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .dockerignore +93 -0
- .env.example +7 -0
- .gitignore +178 -0
- Dockerfile +12 -0
- README.md +33 -11
- Space.yaml +6 -0
- app/ai/__init__.py +0 -0
- app/ai/dispute_analyzer.py +151 -0
- app/ai/gemini_client.py +0 -0
- app/ai/langchain_service.py +132 -0
- app/ai/prompts/__init__.py +0 -0
- app/ai/prompts/dispute_insights.py +29 -0
- app/ai/prompts/dispute_priority.py +30 -0
- app/ai/prompts/followup_questions.py +20 -0
- app/ai/schemas/__init__.py +0 -0
- app/ai/schemas/insights_schema.py +30 -0
- app/ai/schemas/priority_schema.py +12 -0
- app/api/__init__.py +0 -0
- app/api/database.py +140 -0
- app/api/models.py +171 -0
- app/api/routes/__init__.py +0 -0
- app/api/routes/customers.py +181 -0
- app/api/routes/disputes.py +505 -0
- app/api/services/__init__.py +0 -0
- app/api/services/ai_service.py +0 -0
- app/api/services/database_service.py +331 -0
- app/api/services/priority_service.py +40 -0
- app/api/services/recommendation_service.py +25 -0
- app/core/__init__.py +0 -0
- app/core/ai_config.py +20 -0
- app/core/config.py +20 -0
- app/data/__init__.py +0 -0
- app/data/dispute_categories.json +14 -0
- app/data/sample_disputes.json +26 -0
- app/entrypoint.py +34 -0
- app/frontend/__init__.py +0 -0
- app/frontend/components/__init__.py +0 -0
- app/frontend/components/api_popover.py +0 -0
- app/frontend/components/dispute_card.py +52 -0
- app/frontend/components/followup_questions.py +33 -0
- app/frontend/components/insights_panel.py +109 -0
- app/frontend/components/sidebar.py +37 -0
- app/frontend/pages/__init__.py +0 -0
- app/frontend/pages/admin.py +43 -0
- app/frontend/pages/api_docs.py +400 -0
- app/frontend/pages/customer_details.py +416 -0
- app/frontend/pages/dashboard.py +203 -0
- app/frontend/pages/dispute_details.py +544 -0
- app/frontend/pages/dispute_form.py +259 -0
- app/frontend/streamlit_app.py +64 -0
.dockerignore
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
extra.md
|
| 2 |
+
scraped_files.md
|
| 3 |
+
scrape.py
|
| 4 |
+
|
| 5 |
+
# Git
|
| 6 |
+
.git
|
| 7 |
+
.gitignore
|
| 8 |
+
.gitattributes
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
# CI
|
| 12 |
+
.codeclimate.yml
|
| 13 |
+
.travis.yml
|
| 14 |
+
.taskcluster.yml
|
| 15 |
+
|
| 16 |
+
# Docker
|
| 17 |
+
docker-compose.yml
|
| 18 |
+
Dockerfile
|
| 19 |
+
.docker
|
| 20 |
+
.dockerignore
|
| 21 |
+
|
| 22 |
+
# Byte-compiled / optimized / DLL files
|
| 23 |
+
**/__pycache__/
|
| 24 |
+
**/*.py[cod]
|
| 25 |
+
|
| 26 |
+
# C extensions
|
| 27 |
+
*.so
|
| 28 |
+
|
| 29 |
+
# Distribution / packaging
|
| 30 |
+
.Python
|
| 31 |
+
env/
|
| 32 |
+
build/
|
| 33 |
+
develop-eggs/
|
| 34 |
+
dist/
|
| 35 |
+
downloads/
|
| 36 |
+
eggs/
|
| 37 |
+
lib/
|
| 38 |
+
lib64/
|
| 39 |
+
parts/
|
| 40 |
+
sdist/
|
| 41 |
+
var/
|
| 42 |
+
*.egg-info/
|
| 43 |
+
.installed.cfg
|
| 44 |
+
*.egg
|
| 45 |
+
|
| 46 |
+
# PyInstaller
|
| 47 |
+
# Usually these files are written by a python script from a template
|
| 48 |
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
| 49 |
+
*.manifest
|
| 50 |
+
*.spec
|
| 51 |
+
|
| 52 |
+
# Installer logs
|
| 53 |
+
pip-log.txt
|
| 54 |
+
pip-delete-this-directory.txt
|
| 55 |
+
|
| 56 |
+
# Unit test / coverage reports
|
| 57 |
+
htmlcov/
|
| 58 |
+
.tox/
|
| 59 |
+
.coverage
|
| 60 |
+
.cache
|
| 61 |
+
nosetests.xml
|
| 62 |
+
coverage.xml
|
| 63 |
+
|
| 64 |
+
# Translations
|
| 65 |
+
*.mo
|
| 66 |
+
*.pot
|
| 67 |
+
|
| 68 |
+
# Django stuff:
|
| 69 |
+
*.log
|
| 70 |
+
|
| 71 |
+
# Sphinx documentation
|
| 72 |
+
docs/_build/
|
| 73 |
+
|
| 74 |
+
# PyBuilder
|
| 75 |
+
target/
|
| 76 |
+
|
| 77 |
+
# Virtual environment
|
| 78 |
+
.env
|
| 79 |
+
.venv/
|
| 80 |
+
venv/
|
| 81 |
+
|
| 82 |
+
# PyCharm
|
| 83 |
+
.idea
|
| 84 |
+
|
| 85 |
+
# Python mode for VIM
|
| 86 |
+
.ropeproject
|
| 87 |
+
**/.ropeproject
|
| 88 |
+
|
| 89 |
+
# Vim swap files
|
| 90 |
+
**/*.swp
|
| 91 |
+
|
| 92 |
+
# VS Code
|
| 93 |
+
.vscode/
|
.env.example
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# API Keys
|
| 2 |
+
GOOGLE_API_KEY=your_google_api_key_here
|
| 3 |
+
|
| 4 |
+
# App Settings
|
| 5 |
+
DATABASE_URL=sqlite:///./disputes.db
|
| 6 |
+
API_URL=http://localhost:8000
|
| 7 |
+
DEBUG=True
|
.gitignore
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
extra.md
|
| 2 |
+
scraped_files.md
|
| 3 |
+
scrape.py
|
| 4 |
+
|
| 5 |
+
# Byte-compiled / optimized / DLL files
|
| 6 |
+
__pycache__/
|
| 7 |
+
*.py[cod]
|
| 8 |
+
*$py.class
|
| 9 |
+
|
| 10 |
+
# C extensions
|
| 11 |
+
*.so
|
| 12 |
+
|
| 13 |
+
# Distribution / packaging
|
| 14 |
+
.Python
|
| 15 |
+
build/
|
| 16 |
+
develop-eggs/
|
| 17 |
+
dist/
|
| 18 |
+
downloads/
|
| 19 |
+
eggs/
|
| 20 |
+
.eggs/
|
| 21 |
+
lib/
|
| 22 |
+
lib64/
|
| 23 |
+
parts/
|
| 24 |
+
sdist/
|
| 25 |
+
var/
|
| 26 |
+
wheels/
|
| 27 |
+
share/python-wheels/
|
| 28 |
+
*.egg-info/
|
| 29 |
+
.installed.cfg
|
| 30 |
+
*.egg
|
| 31 |
+
MANIFEST
|
| 32 |
+
|
| 33 |
+
# PyInstaller
|
| 34 |
+
# Usually these files are written by a python script from a template
|
| 35 |
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
| 36 |
+
*.manifest
|
| 37 |
+
*.spec
|
| 38 |
+
|
| 39 |
+
# Installer logs
|
| 40 |
+
pip-log.txt
|
| 41 |
+
pip-delete-this-directory.txt
|
| 42 |
+
|
| 43 |
+
# Unit test / coverage reports
|
| 44 |
+
htmlcov/
|
| 45 |
+
.tox/
|
| 46 |
+
.nox/
|
| 47 |
+
.coverage
|
| 48 |
+
.coverage.*
|
| 49 |
+
.cache
|
| 50 |
+
nosetests.xml
|
| 51 |
+
coverage.xml
|
| 52 |
+
*.cover
|
| 53 |
+
*.py,cover
|
| 54 |
+
.hypothesis/
|
| 55 |
+
.pytest_cache/
|
| 56 |
+
cover/
|
| 57 |
+
|
| 58 |
+
# Translations
|
| 59 |
+
*.mo
|
| 60 |
+
*.pot
|
| 61 |
+
|
| 62 |
+
# Django stuff:
|
| 63 |
+
*.log
|
| 64 |
+
local_settings.py
|
| 65 |
+
db.sqlite3
|
| 66 |
+
db.sqlite3-journal
|
| 67 |
+
|
| 68 |
+
# Flask stuff:
|
| 69 |
+
instance/
|
| 70 |
+
.webassets-cache
|
| 71 |
+
|
| 72 |
+
# Scrapy stuff:
|
| 73 |
+
.scrapy
|
| 74 |
+
|
| 75 |
+
# Sphinx documentation
|
| 76 |
+
docs/_build/
|
| 77 |
+
|
| 78 |
+
# PyBuilder
|
| 79 |
+
.pybuilder/
|
| 80 |
+
target/
|
| 81 |
+
|
| 82 |
+
# Jupyter Notebook
|
| 83 |
+
.ipynb_checkpoints
|
| 84 |
+
|
| 85 |
+
# IPython
|
| 86 |
+
profile_default/
|
| 87 |
+
ipython_config.py
|
| 88 |
+
|
| 89 |
+
# pyenv
|
| 90 |
+
# For a library or package, you might want to ignore these files since the code is
|
| 91 |
+
# intended to run in multiple environments; otherwise, check them in:
|
| 92 |
+
# .python-version
|
| 93 |
+
|
| 94 |
+
# pipenv
|
| 95 |
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
| 96 |
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
| 97 |
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
| 98 |
+
# install all needed dependencies.
|
| 99 |
+
#Pipfile.lock
|
| 100 |
+
|
| 101 |
+
# UV
|
| 102 |
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
| 103 |
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
| 104 |
+
# commonly ignored for libraries.
|
| 105 |
+
#uv.lock
|
| 106 |
+
|
| 107 |
+
# poetry
|
| 108 |
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
| 109 |
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
| 110 |
+
# commonly ignored for libraries.
|
| 111 |
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
| 112 |
+
#poetry.lock
|
| 113 |
+
|
| 114 |
+
# pdm
|
| 115 |
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
| 116 |
+
#pdm.lock
|
| 117 |
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
| 118 |
+
# in version control.
|
| 119 |
+
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
| 120 |
+
.pdm.toml
|
| 121 |
+
.pdm-python
|
| 122 |
+
.pdm-build/
|
| 123 |
+
|
| 124 |
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
| 125 |
+
__pypackages__/
|
| 126 |
+
|
| 127 |
+
# Celery stuff
|
| 128 |
+
celerybeat-schedule
|
| 129 |
+
celerybeat.pid
|
| 130 |
+
|
| 131 |
+
# SageMath parsed files
|
| 132 |
+
*.sage.py
|
| 133 |
+
|
| 134 |
+
# Environments
|
| 135 |
+
.env
|
| 136 |
+
.venv
|
| 137 |
+
env/
|
| 138 |
+
venv/
|
| 139 |
+
ENV/
|
| 140 |
+
env.bak/
|
| 141 |
+
venv.bak/
|
| 142 |
+
|
| 143 |
+
# Spyder project settings
|
| 144 |
+
.spyderproject
|
| 145 |
+
.spyproject
|
| 146 |
+
|
| 147 |
+
# Rope project settings
|
| 148 |
+
.ropeproject
|
| 149 |
+
|
| 150 |
+
# mkdocs documentation
|
| 151 |
+
/site
|
| 152 |
+
|
| 153 |
+
# mypy
|
| 154 |
+
.mypy_cache/
|
| 155 |
+
.dmypy.json
|
| 156 |
+
dmypy.json
|
| 157 |
+
|
| 158 |
+
# Pyre type checker
|
| 159 |
+
.pyre/
|
| 160 |
+
|
| 161 |
+
# pytype static type analyzer
|
| 162 |
+
.pytype/
|
| 163 |
+
|
| 164 |
+
# Cython debug symbols
|
| 165 |
+
cython_debug/
|
| 166 |
+
|
| 167 |
+
# PyCharm
|
| 168 |
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
| 169 |
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
| 170 |
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
| 171 |
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
| 172 |
+
#.idea/
|
| 173 |
+
|
| 174 |
+
# Ruff stuff:
|
| 175 |
+
.ruff_cache/
|
| 176 |
+
|
| 177 |
+
# PyPI configuration file
|
| 178 |
+
.pypirc
|
Dockerfile
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
COPY requirements.txt .
|
| 6 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 7 |
+
|
| 8 |
+
COPY . .
|
| 9 |
+
|
| 10 |
+
ENV PYTHONPATH=/app
|
| 11 |
+
|
| 12 |
+
CMD ["python", "app/entrypoint.py"]
|
README.md
CHANGED
|
@@ -1,11 +1,33 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Banking Dispute Resolution System
|
| 2 |
+
|
| 3 |
+
A prototype system for handling banking disputes with AI-powered insights and priority assignment.
|
| 4 |
+
|
| 5 |
+
## Features
|
| 6 |
+
|
| 7 |
+
- AI-powered dispute analysis and priority assignment
|
| 8 |
+
- Streamlit-based user interface for customer service representatives
|
| 9 |
+
- FastAPI backend with SQLite database
|
| 10 |
+
- Deployed on Hugging Face Spaces
|
| 11 |
+
|
| 12 |
+
## Setup
|
| 13 |
+
|
| 14 |
+
1. Clone this repository
|
| 15 |
+
2. Install dependencies: \pip install -r requirements.txt\
|
| 16 |
+
3. Set up environment variables (see \.env.example\)
|
| 17 |
+
4. Run the application: \python app/entrypoint.py\
|
| 18 |
+
|
| 19 |
+
## Docker
|
| 20 |
+
|
| 21 |
+
To run with Docker:
|
| 22 |
+
|
| 23 |
+
\\\
|
| 24 |
+
docker build -t banking-disputes .
|
| 25 |
+
docker run -p 8501:8501 -p 8000:8000 banking-disputes
|
| 26 |
+
\\\
|
| 27 |
+
|
| 28 |
+
## Development
|
| 29 |
+
|
| 30 |
+
All code is in the \pp\ directory:
|
| 31 |
+
- \pi/\ - FastAPI backend
|
| 32 |
+
- \i/\ - AI services using Langchain and Gemini
|
| 33 |
+
- \rontend/\ - Streamlit UI components
|
Space.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
title: Banking Dispute Resolution System
|
| 2 |
+
emoji: ??
|
| 3 |
+
colorFrom: blue
|
| 4 |
+
colorTo: indigo
|
| 5 |
+
sdk: docker
|
| 6 |
+
pinned: false
|
app/ai/__init__.py
ADDED
|
File without changes
|
app/ai/dispute_analyzer.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app/ai/dispute_analyzer.py
|
| 2 |
+
from typing import Dict, Any, List, Tuple
|
| 3 |
+
import json
|
| 4 |
+
from datetime import datetime, timedelta
|
| 5 |
+
|
| 6 |
+
from app.ai.langchain_service import DisputeAIService
|
| 7 |
+
from app.core.ai_config import ai_settings
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class DisputeAnalyzer:
|
| 11 |
+
"""Class for analyzing banking disputes using AI and rule-based approaches"""
|
| 12 |
+
|
| 13 |
+
def __init__(self):
|
| 14 |
+
self.ai_service = DisputeAIService()
|
| 15 |
+
|
| 16 |
+
def analyze_dispute(self, dispute_data: Dict[str, Any]) -> Dict[str, Any]:
|
| 17 |
+
"""
|
| 18 |
+
Comprehensive analysis of a dispute combining AI and rule-based approaches
|
| 19 |
+
|
| 20 |
+
Returns:
|
| 21 |
+
Dict with analysis results including priority, risk score, insights,
|
| 22 |
+
recommended actions, etc.
|
| 23 |
+
"""
|
| 24 |
+
# Get AI analysis
|
| 25 |
+
ai_analysis = self.ai_service.analyze_dispute(dispute_data)
|
| 26 |
+
|
| 27 |
+
# Add rule-based risk scoring
|
| 28 |
+
risk_score, risk_factors = self._calculate_risk_score(dispute_data)
|
| 29 |
+
|
| 30 |
+
# Add recommended next actions
|
| 31 |
+
recommended_actions = self._generate_recommended_actions(
|
| 32 |
+
dispute_data, ai_analysis, risk_score
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
# Combine all analysis
|
| 36 |
+
return {
|
| 37 |
+
**ai_analysis,
|
| 38 |
+
"risk_score": risk_score,
|
| 39 |
+
"risk_factors": risk_factors,
|
| 40 |
+
"recommended_actions": recommended_actions,
|
| 41 |
+
"sla_target": self._calculate_sla_target(
|
| 42 |
+
dispute_data, ai_analysis["priority"]
|
| 43 |
+
),
|
| 44 |
+
"similar_cases_count": 0, # Placeholder for future implementation
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
def _calculate_risk_score(
|
| 48 |
+
self, dispute_data: Dict[str, Any]
|
| 49 |
+
) -> Tuple[float, List[str]]:
|
| 50 |
+
"""Calculate a risk score (0-100) based on dispute characteristics"""
|
| 51 |
+
risk_factors = []
|
| 52 |
+
score = 50
|
| 53 |
+
|
| 54 |
+
# Transaction amount risk
|
| 55 |
+
amount = dispute_data.get("transaction_amount", 0)
|
| 56 |
+
if amount > 10000:
|
| 57 |
+
score += 25
|
| 58 |
+
risk_factors.append("High transaction amount (>$10k)")
|
| 59 |
+
elif amount > 5000:
|
| 60 |
+
score += 15
|
| 61 |
+
risk_factors.append("Medium-high transaction amount (>$5k)")
|
| 62 |
+
elif amount > 1000:
|
| 63 |
+
score += 5
|
| 64 |
+
risk_factors.append("Moderate transaction amount (>$1k)")
|
| 65 |
+
|
| 66 |
+
# Customer history risk
|
| 67 |
+
previous_disputes = dispute_data.get("previous_disputes_count", 0)
|
| 68 |
+
if previous_disputes > 5:
|
| 69 |
+
score += 20
|
| 70 |
+
risk_factors.append("Frequent disputer (>5 disputes)")
|
| 71 |
+
elif previous_disputes > 2:
|
| 72 |
+
score += 10
|
| 73 |
+
risk_factors.append("Multiple previous disputes (>2)")
|
| 74 |
+
|
| 75 |
+
# Account age risk
|
| 76 |
+
account_age = dispute_data.get("customer_account_age_days", 0)
|
| 77 |
+
if account_age < 30:
|
| 78 |
+
score += 15
|
| 79 |
+
risk_factors.append("New account (<30 days)")
|
| 80 |
+
elif account_age < 365:
|
| 81 |
+
score += 5
|
| 82 |
+
risk_factors.append("Relatively new account (<1 year)")
|
| 83 |
+
|
| 84 |
+
# Category risk
|
| 85 |
+
category = dispute_data.get("category", "").lower()
|
| 86 |
+
if "fraud" in category:
|
| 87 |
+
score += 30
|
| 88 |
+
risk_factors.append("Fraud-related category")
|
| 89 |
+
elif "unauthorized" in category:
|
| 90 |
+
score += 20
|
| 91 |
+
risk_factors.append("Unauthorized transaction")
|
| 92 |
+
|
| 93 |
+
# Document status
|
| 94 |
+
if not dispute_data.get("has_supporting_documents"):
|
| 95 |
+
score += 10
|
| 96 |
+
risk_factors.append("Missing supporting documents")
|
| 97 |
+
|
| 98 |
+
# Cap score between 0-100
|
| 99 |
+
score = max(0, min(100, score))
|
| 100 |
+
|
| 101 |
+
return round(score, 2), risk_factors
|
| 102 |
+
|
| 103 |
+
def _generate_recommended_actions(
|
| 104 |
+
self,
|
| 105 |
+
dispute_data: Dict[str, Any],
|
| 106 |
+
ai_analysis: Dict[str, Any],
|
| 107 |
+
risk_score: float,
|
| 108 |
+
) -> List[str]:
|
| 109 |
+
"""Generate recommended actions based on analysis results"""
|
| 110 |
+
actions = []
|
| 111 |
+
|
| 112 |
+
# High priority actions
|
| 113 |
+
if ai_analysis["priority"] >= 4:
|
| 114 |
+
actions.extend(
|
| 115 |
+
[
|
| 116 |
+
"Escalate to senior analyst",
|
| 117 |
+
"Request urgent documentation",
|
| 118 |
+
"Initiate fraud investigation",
|
| 119 |
+
]
|
| 120 |
+
)
|
| 121 |
+
|
| 122 |
+
# Medium priority actions
|
| 123 |
+
elif ai_analysis["priority"] >= 3:
|
| 124 |
+
actions.extend(
|
| 125 |
+
[
|
| 126 |
+
"Schedule customer interview",
|
| 127 |
+
"Verify transaction details with merchant",
|
| 128 |
+
"Review account history",
|
| 129 |
+
]
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
+
# General actions based on risk
|
| 133 |
+
if risk_score > 70:
|
| 134 |
+
actions.append("Flag account for enhanced monitoring")
|
| 135 |
+
if risk_score > 80:
|
| 136 |
+
actions.append("Notify compliance department")
|
| 137 |
+
|
| 138 |
+
# Add AI recommendations
|
| 139 |
+
actions.extend(ai_analysis.get("probable_solutions", []))
|
| 140 |
+
|
| 141 |
+
return list(set(actions))[:5] # Return top 5 unique actions
|
| 142 |
+
|
| 143 |
+
def _calculate_sla_target(
|
| 144 |
+
self, dispute_data: Dict[str, Any], priority: int
|
| 145 |
+
) -> datetime:
|
| 146 |
+
"""Calculate SLA target date based on priority"""
|
| 147 |
+
base_date = datetime.utcnow()
|
| 148 |
+
|
| 149 |
+
sla_days = {1: 14, 2: 10, 3: 7, 4: 3, 5: 1}
|
| 150 |
+
|
| 151 |
+
return base_date + timedelta(days=sla_days.get(priority, 14))
|
app/ai/gemini_client.py
ADDED
|
File without changes
|
app/ai/langchain_service.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app/ai/langchain_service.py
|
| 2 |
+
from langchain_google_genai import ChatGoogleGenerativeAI
|
| 3 |
+
import os
|
| 4 |
+
from typing import Dict, Any, List
|
| 5 |
+
import json
|
| 6 |
+
|
| 7 |
+
from app.ai.schemas.priority_schema import PrioritySchema
|
| 8 |
+
from app.ai.schemas.insights_schema import InsightsSchema
|
| 9 |
+
from app.core.ai_config import ai_settings
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class DisputeAIService:
|
| 13 |
+
"""Service for AI-powered dispute analysis using Langchain and Gemini"""
|
| 14 |
+
|
| 15 |
+
def __init__(self):
|
| 16 |
+
self.llm = ChatGoogleGenerativeAI(
|
| 17 |
+
model=ai_settings.GEMINI_MODEL,
|
| 18 |
+
temperature=ai_settings.TEMPERATURE,
|
| 19 |
+
max_tokens=ai_settings.MAX_TOKENS,
|
| 20 |
+
timeout=None,
|
| 21 |
+
max_retries=ai_settings.MAX_RETRIES,
|
| 22 |
+
# google_api_key=os.environ.get("GOOGLE_API_KEY", ai_settings.GOOGLE_API_KEY),
|
| 23 |
+
google_api_key="AIzaSyBeC3D_rxPbaLyQjgeqCB3PZRQL7kT4mrE",
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
def analyze_dispute(self, dispute_data: Dict[str, Any]) -> Dict[str, Any]:
|
| 27 |
+
"""
|
| 28 |
+
Analyze a dispute and return AI-generated insights
|
| 29 |
+
|
| 30 |
+
Returns:
|
| 31 |
+
Dict with priority, insights, followup_questions,
|
| 32 |
+
probable_solutions, and possible_reasons
|
| 33 |
+
"""
|
| 34 |
+
# Get structured output for priority
|
| 35 |
+
priority_model = self.llm.with_structured_output(PrioritySchema)
|
| 36 |
+
priority_result = priority_model.invoke(
|
| 37 |
+
self._build_priority_prompt(dispute_data)
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
print(self._build_priority_prompt(dispute_data))
|
| 41 |
+
print(type(priority_result))
|
| 42 |
+
print(priority_result)
|
| 43 |
+
|
| 44 |
+
# Get structured output for insights
|
| 45 |
+
insights_model = self.llm.with_structured_output(InsightsSchema)
|
| 46 |
+
insights_result = insights_model.invoke(
|
| 47 |
+
self._build_insights_prompt(dispute_data)
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
print(self._build_insights_prompt(dispute_data))
|
| 51 |
+
print(type(insights_result))
|
| 52 |
+
print(insights_result)
|
| 53 |
+
|
| 54 |
+
# Combine results
|
| 55 |
+
return {
|
| 56 |
+
"priority": (
|
| 57 |
+
priority_result.priority_level if priority_result.priority_level else 0
|
| 58 |
+
),
|
| 59 |
+
"priority_reason": (
|
| 60 |
+
priority_result.priority_reason
|
| 61 |
+
if priority_result.priority_reason
|
| 62 |
+
else ""
|
| 63 |
+
),
|
| 64 |
+
"insights": insights_result.insights if insights_result.insights else "",
|
| 65 |
+
"followup_questions": (
|
| 66 |
+
insights_result.followup_questions
|
| 67 |
+
if insights_result.followup_questions
|
| 68 |
+
else ""
|
| 69 |
+
),
|
| 70 |
+
"probable_solutions": (
|
| 71 |
+
insights_result.probable_solutions
|
| 72 |
+
if insights_result.probable_solutions
|
| 73 |
+
else ""
|
| 74 |
+
),
|
| 75 |
+
"possible_reasons": (
|
| 76 |
+
insights_result.possible_reasons
|
| 77 |
+
if insights_result.possible_reasons
|
| 78 |
+
else ""
|
| 79 |
+
),
|
| 80 |
+
"risk_score": (
|
| 81 |
+
insights_result.risk_score if insights_result.risk_score else 0
|
| 82 |
+
),
|
| 83 |
+
"risk_factors": (
|
| 84 |
+
insights_result.risk_factors if insights_result.risk_factors else ""
|
| 85 |
+
),
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
def _build_priority_prompt(self, dispute_data: Dict[str, Any]) -> str:
|
| 89 |
+
"""Create prompt for priority assignment"""
|
| 90 |
+
return f"""
|
| 91 |
+
You are a banking dispute resolution expert. Analyze this dispute and assign a priority level.
|
| 92 |
+
|
| 93 |
+
Customer profile:
|
| 94 |
+
- Customer name: {dispute_data.get('customer_name')}
|
| 95 |
+
- Customer type: {dispute_data.get('customer_type')}
|
| 96 |
+
- Previous disputes: {dispute_data.get('previous_disputes_count')}
|
| 97 |
+
- Account age (days): {dispute_data.get('customer_account_age_days')}
|
| 98 |
+
|
| 99 |
+
Dispute details:
|
| 100 |
+
- Transaction amount: ${dispute_data.get('transaction_amount')}
|
| 101 |
+
- Description: {dispute_data.get('dispute_description')}
|
| 102 |
+
- Category: {dispute_data.get('category')}
|
| 103 |
+
- Has supporting documents: {dispute_data.get('has_supporting_documents')}
|
| 104 |
+
|
| 105 |
+
Based on this information, assign a priority level (1-5) where:
|
| 106 |
+
1 = Very Low, 2 = Low, 3 = Medium, 4 = High, 5 = Critical
|
| 107 |
+
|
| 108 |
+
Provide the priority level and a brief explanation for this decision.
|
| 109 |
+
"""
|
| 110 |
+
|
| 111 |
+
def _build_insights_prompt(self, dispute_data: Dict[str, Any]) -> str:
|
| 112 |
+
"""Create prompt for generating insights"""
|
| 113 |
+
return f"""
|
| 114 |
+
You are a banking dispute resolution expert. Analyze this dispute and provide insights.
|
| 115 |
+
|
| 116 |
+
Customer profile:
|
| 117 |
+
- Customer name: {dispute_data.get('customer_name')}
|
| 118 |
+
- Customer type: {dispute_data.get('customer_type')}
|
| 119 |
+
- Previous disputes: {dispute_data.get('previous_disputes_count')}
|
| 120 |
+
|
| 121 |
+
Dispute details:
|
| 122 |
+
- Transaction amount: ${dispute_data.get('transaction_amount')}
|
| 123 |
+
- Description: {dispute_data.get('dispute_description')}
|
| 124 |
+
- Category: {dispute_data.get('category')}
|
| 125 |
+
- Transaction date: {dispute_data.get('transaction_date')}
|
| 126 |
+
|
| 127 |
+
Provide:
|
| 128 |
+
1. Key insights about this dispute
|
| 129 |
+
2. Follow-up questions to ask the customer
|
| 130 |
+
3. Probable solutions to resolve this dispute
|
| 131 |
+
4. Possible underlying reasons for this dispute
|
| 132 |
+
"""
|
app/ai/prompts/__init__.py
ADDED
|
File without changes
|
app/ai/prompts/dispute_insights.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Dict, Any
|
| 2 |
+
|
| 3 |
+
# app/ai/prompts/dispute_insights.py
|
| 4 |
+
def get_insights_prompt_template(dispute_data: Dict[str, Any]) -> str:
|
| 5 |
+
return f"""
|
| 6 |
+
As a senior dispute analyst, provide detailed insights for this case:
|
| 7 |
+
|
| 8 |
+
Customer Background:
|
| 9 |
+
- Member since: {dispute_data.get('customer_account_age_days')} days
|
| 10 |
+
- Past disputes: {dispute_data.get('previous_disputes_count')}
|
| 11 |
+
- Account type: {dispute_data.get('customer_type')}
|
| 12 |
+
|
| 13 |
+
Transaction Details:
|
| 14 |
+
- Date: {dispute_data.get('transaction_date')}
|
| 15 |
+
- Merchant: {dispute_data.get('merchant_name')}
|
| 16 |
+
- Amount: ${dispute_data.get('transaction_amount')}
|
| 17 |
+
- Description: {dispute_data.get('dispute_description')}
|
| 18 |
+
|
| 19 |
+
Analysis Requirements:
|
| 20 |
+
1. Identify 3 key patterns or anomalies
|
| 21 |
+
2. List 5 relevant follow-up questions for the customer
|
| 22 |
+
3. Suggest 3 probable resolution paths
|
| 23 |
+
4. Highlight 2-3 potential systemic issues
|
| 24 |
+
|
| 25 |
+
Format Requirements:
|
| 26 |
+
- Use bullet points for each section
|
| 27 |
+
- Avoid markdown formatting
|
| 28 |
+
- Keep responses concise and actionable
|
| 29 |
+
"""
|
app/ai/prompts/dispute_priority.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Dict, Any
|
| 2 |
+
# app/ai/prompts/dispute_priority.py
|
| 3 |
+
def get_priority_prompt_template(dispute_data: Dict[str, Any]) -> str:
|
| 4 |
+
return f"""
|
| 5 |
+
You are a banking dispute resolution expert. Analyze this dispute and assign a priority level.
|
| 6 |
+
|
| 7 |
+
Customer Profile:
|
| 8 |
+
- Name: {dispute_data.get('customer_name')}
|
| 9 |
+
- Type: {dispute_data.get('customer_type')}
|
| 10 |
+
- Previous Disputes: {dispute_data.get('previous_disputes_count')}
|
| 11 |
+
- Account Age: {dispute_data.get('customer_account_age_days')} days
|
| 12 |
+
|
| 13 |
+
Dispute Details:
|
| 14 |
+
- Amount: ${dispute_data.get('transaction_amount')}
|
| 15 |
+
- Category: {dispute_data.get('category')}
|
| 16 |
+
- Description: {dispute_data.get('dispute_description')}
|
| 17 |
+
- Documents Available: {'Yes' if dispute_data.get('has_supporting_documents') else 'No'}
|
| 18 |
+
|
| 19 |
+
Priority Guidelines:
|
| 20 |
+
1 (Very Low) - Routine inquiry, low amount, established customer
|
| 21 |
+
2 (Low) - Minor discrepancy, medium amount
|
| 22 |
+
3 (Medium) - Significant amount, unclear details
|
| 23 |
+
4 (High) - Potential fraud indicators, large amount
|
| 24 |
+
5 (Critical) - Clear fraud pattern, VIP customer, legal implications
|
| 25 |
+
|
| 26 |
+
Required Output:
|
| 27 |
+
- Priority level (1-5 integer)
|
| 28 |
+
- Concise reason for priority assignment
|
| 29 |
+
- Key risk factors identified
|
| 30 |
+
"""
|
app/ai/prompts/followup_questions.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Dict, Any
|
| 2 |
+
# app/ai/prompts/followup_questions.py
|
| 3 |
+
def get_followup_prompt_template(dispute_data: Dict[str, Any]) -> str:
|
| 4 |
+
return f"""
|
| 5 |
+
Generate targeted follow-up questions for this dispute:
|
| 6 |
+
|
| 7 |
+
Context:
|
| 8 |
+
- Customer: {dispute_data.get('customer_name')}
|
| 9 |
+
- Transaction ID: {dispute_data.get('transaction_id')}
|
| 10 |
+
- Dispute Type: {dispute_data.get('category')}
|
| 11 |
+
|
| 12 |
+
Current Information:
|
| 13 |
+
{dispute_data.get('dispute_description')}
|
| 14 |
+
|
| 15 |
+
Requirements:
|
| 16 |
+
- Generate 5-7 specific questions
|
| 17 |
+
- Prioritize questions that help verify transaction legitimacy
|
| 18 |
+
- Include questions about timeline, location, and verification methods
|
| 19 |
+
- Phrase questions in neutral, non-accusatory language
|
| 20 |
+
"""
|
app/ai/schemas/__init__.py
ADDED
|
File without changes
|
app/ai/schemas/insights_schema.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app/ai/schemas/insights_schema.py
|
| 2 |
+
from typing import List
|
| 3 |
+
from pydantic import BaseModel, Field
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class InsightsSchema(BaseModel):
|
| 7 |
+
"""Schema for dispute insights"""
|
| 8 |
+
|
| 9 |
+
insights: str = Field(description="Detailed insights regarding the dispute")
|
| 10 |
+
followup_questions: List[str] = Field(
|
| 11 |
+
description="List of relevant follow-up questions to gather more information"
|
| 12 |
+
)
|
| 13 |
+
probable_solutions: List[str] = Field(
|
| 14 |
+
description="Potential solutions to address the dispute"
|
| 15 |
+
)
|
| 16 |
+
possible_reasons: List[str] = Field(
|
| 17 |
+
description="Possible reasons that might have caused the dispute"
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
risk_score: float = Field(
|
| 21 |
+
description="Risk score for the dispute from 0 (lowest) to 10 (highest)"
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
risk_factors: List[str] = Field(
|
| 25 |
+
description="3 Factors contributing to the calculated risk score, if low risk, keep empty"
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
|
app/ai/schemas/priority_schema.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app/ai/schemas/priority_schema.py
|
| 2 |
+
from pydantic import BaseModel, Field
|
| 3 |
+
from typing import Literal, Optional
|
| 4 |
+
|
| 5 |
+
class PrioritySchema(BaseModel):
|
| 6 |
+
"""Schema for priority assignment"""
|
| 7 |
+
priority_level: int = Field(
|
| 8 |
+
description="Priority level from 1 (lowest) to 10 (highest)"
|
| 9 |
+
)
|
| 10 |
+
priority_reason: str = Field(
|
| 11 |
+
description="Explanation for the assigned priority level"
|
| 12 |
+
)
|
app/api/__init__.py
ADDED
|
File without changes
|
app/api/database.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app/api/database.py
|
| 2 |
+
from datetime import datetime
|
| 3 |
+
from app.core.config import settings
|
| 4 |
+
from sqlalchemy import (
|
| 5 |
+
create_engine,
|
| 6 |
+
Column,
|
| 7 |
+
Integer,
|
| 8 |
+
String,
|
| 9 |
+
Float,
|
| 10 |
+
ForeignKey,
|
| 11 |
+
Text,
|
| 12 |
+
DateTime,
|
| 13 |
+
Boolean,
|
| 14 |
+
case,
|
| 15 |
+
func,
|
| 16 |
+
)
|
| 17 |
+
from sqlalchemy.ext.declarative import declarative_base
|
| 18 |
+
from sqlalchemy.orm import sessionmaker, relationship, Session
|
| 19 |
+
from sqlalchemy import event
|
| 20 |
+
from sqlite3 import Connection as SQLite3Connection
|
| 21 |
+
import uuid
|
| 22 |
+
|
| 23 |
+
# Create SQLite engine
|
| 24 |
+
SQLALCHEMY_DATABASE_URL = settings.DATABASE_URL
|
| 25 |
+
engine = create_engine(
|
| 26 |
+
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
# Create session
|
| 30 |
+
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
| 31 |
+
Base = declarative_base()
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
# Database Models
|
| 35 |
+
class Customer(Base):
|
| 36 |
+
__tablename__ = "customers"
|
| 37 |
+
|
| 38 |
+
id = Column(String, primary_key=True, index=True)
|
| 39 |
+
name = Column(String, index=True)
|
| 40 |
+
email = Column(String, unique=True, index=True)
|
| 41 |
+
account_type = Column(String) # e.g., "Individual", "Business"
|
| 42 |
+
dispute_count = Column(Integer, default=0)
|
| 43 |
+
created_at = Column(DateTime, default=datetime.utcnow)
|
| 44 |
+
|
| 45 |
+
# Relationship
|
| 46 |
+
disputes = relationship("Dispute", back_populates="customer")
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
class Dispute(Base):
|
| 50 |
+
__tablename__ = "disputes"
|
| 51 |
+
|
| 52 |
+
id = Column(String, primary_key=True, index=True)
|
| 53 |
+
customer_id = Column(String, ForeignKey("customers.id"))
|
| 54 |
+
transaction_id = Column(String, index=True)
|
| 55 |
+
merchant_name = Column(String)
|
| 56 |
+
amount = Column(Float)
|
| 57 |
+
description = Column(Text)
|
| 58 |
+
category = Column(String, index=True) # e.g., "Unauthorized", "Duplicate"
|
| 59 |
+
status = Column(String, default="Open") # "Open", "Under Review", "Resolved"
|
| 60 |
+
priority = Column(Integer, nullable=True) # 1-5, with 5 being highest priority
|
| 61 |
+
created_at = Column(DateTime, default=datetime.utcnow)
|
| 62 |
+
resolved_at = Column(DateTime, nullable=True)
|
| 63 |
+
|
| 64 |
+
# Relationship
|
| 65 |
+
customer = relationship("Customer", back_populates="disputes")
|
| 66 |
+
notes = relationship("DisputeNote", back_populates="dispute")
|
| 67 |
+
ai_insights = relationship(
|
| 68 |
+
"DisputeInsight", back_populates="dispute", uselist=False
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
class DisputeNote(Base):
|
| 73 |
+
__tablename__ = "dispute_notes"
|
| 74 |
+
|
| 75 |
+
id = Column(String, primary_key=True, index=True)
|
| 76 |
+
dispute_id = Column(String, ForeignKey("disputes.id"))
|
| 77 |
+
content = Column(Text)
|
| 78 |
+
created_at = Column(DateTime, default=datetime.utcnow)
|
| 79 |
+
|
| 80 |
+
# Relationship
|
| 81 |
+
dispute = relationship("Dispute", back_populates="notes")
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
class DisputeInsight(Base):
|
| 86 |
+
"""SQLAlchemy model for dispute insights"""
|
| 87 |
+
|
| 88 |
+
__tablename__ = "dispute_insights"
|
| 89 |
+
__table_args__ = {"extend_existing": True} # Allows redefining table options
|
| 90 |
+
|
| 91 |
+
id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
|
| 92 |
+
dispute_id = Column(
|
| 93 |
+
String, ForeignKey("disputes.id", ondelete="CASCADE"), nullable=False
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
+
# Core insight fields
|
| 97 |
+
insights = Column(String, nullable=False)
|
| 98 |
+
followup_questions = Column(String, nullable=False)
|
| 99 |
+
probable_solutions = Column(String, nullable=False)
|
| 100 |
+
possible_reasons = Column(String, nullable=False)
|
| 101 |
+
|
| 102 |
+
# Risk assessment
|
| 103 |
+
risk_score = Column(Float, nullable=False)
|
| 104 |
+
risk_factors = Column(String, nullable=False)
|
| 105 |
+
|
| 106 |
+
# Priority fields (from your existing model)
|
| 107 |
+
priority_level = Column(Integer, nullable=False)
|
| 108 |
+
priority_reason = Column(String, nullable=False)
|
| 109 |
+
|
| 110 |
+
# Metadata
|
| 111 |
+
created_at = Column(DateTime, server_default=func.now())
|
| 112 |
+
updated_at = Column(DateTime, onupdate=func.now())
|
| 113 |
+
|
| 114 |
+
# Relationship
|
| 115 |
+
dispute = relationship("Dispute", back_populates="ai_insights")
|
| 116 |
+
|
| 117 |
+
def __repr__(self):
|
| 118 |
+
return f"<DisputeInsight(id={self.id}, dispute_id={self.dispute_id})>"
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
# Create all tables
|
| 122 |
+
Base.metadata.create_all(bind=engine)
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
# Add this event listener
|
| 126 |
+
@event.listens_for(engine, "connect")
|
| 127 |
+
def set_sqlite_pragma(dbapi_connection, connection_record):
|
| 128 |
+
if isinstance(dbapi_connection, SQLite3Connection):
|
| 129 |
+
cursor = dbapi_connection.cursor()
|
| 130 |
+
cursor.execute("PRAGMA foreign_keys=ON")
|
| 131 |
+
cursor.close()
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
# Dependency
|
| 135 |
+
def get_db():
|
| 136 |
+
db = SessionLocal()
|
| 137 |
+
try:
|
| 138 |
+
yield db
|
| 139 |
+
finally:
|
| 140 |
+
db.close()
|
app/api/models.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List, Optional, Dict, Any
|
| 2 |
+
from datetime import datetime
|
| 3 |
+
from pydantic import BaseModel, Field, EmailStr, ConfigDict
|
| 4 |
+
|
| 5 |
+
# Customer Models
|
| 6 |
+
class CustomerBase(BaseModel):
|
| 7 |
+
model_config = ConfigDict(extra="ignore")
|
| 8 |
+
|
| 9 |
+
name: str
|
| 10 |
+
email: EmailStr
|
| 11 |
+
account_type: str = "Individual"
|
| 12 |
+
|
| 13 |
+
class CustomerCreate(CustomerBase):
|
| 14 |
+
pass
|
| 15 |
+
|
| 16 |
+
class Customer(CustomerBase):
|
| 17 |
+
id: str
|
| 18 |
+
dispute_count: int = 0
|
| 19 |
+
created_at: datetime
|
| 20 |
+
|
| 21 |
+
model_config = ConfigDict(from_attributes=True, extra="ignore")
|
| 22 |
+
|
| 23 |
+
# Dispute Models
|
| 24 |
+
class DisputeBase(BaseModel):
|
| 25 |
+
model_config = ConfigDict(extra="ignore")
|
| 26 |
+
|
| 27 |
+
customer_id: str
|
| 28 |
+
transaction_id: str
|
| 29 |
+
merchant_name: str
|
| 30 |
+
amount: float
|
| 31 |
+
description: str
|
| 32 |
+
category: str
|
| 33 |
+
|
| 34 |
+
class DisputeCreate(DisputeBase):
|
| 35 |
+
pass
|
| 36 |
+
|
| 37 |
+
class DisputeUpdate(BaseModel):
|
| 38 |
+
model_config = ConfigDict(extra="ignore")
|
| 39 |
+
|
| 40 |
+
status: Optional[str] = None
|
| 41 |
+
priority: Optional[int] = None
|
| 42 |
+
description: Optional[str] = None
|
| 43 |
+
|
| 44 |
+
class Dispute(DisputeBase):
|
| 45 |
+
id: str
|
| 46 |
+
status: str
|
| 47 |
+
priority: Optional[int] = None
|
| 48 |
+
created_at: datetime
|
| 49 |
+
resolved_at: Optional[datetime] = None
|
| 50 |
+
|
| 51 |
+
model_config = ConfigDict(from_attributes=True, extra="ignore")
|
| 52 |
+
|
| 53 |
+
class DisputeWithCustomer(Dispute):
|
| 54 |
+
customer: Customer
|
| 55 |
+
|
| 56 |
+
model_config = ConfigDict(from_attributes=True, extra="ignore")
|
| 57 |
+
|
| 58 |
+
# Note Models
|
| 59 |
+
class NoteCreate(BaseModel):
|
| 60 |
+
model_config = ConfigDict(extra="ignore")
|
| 61 |
+
|
| 62 |
+
content: str
|
| 63 |
+
dispute_id: str
|
| 64 |
+
|
| 65 |
+
class Note(NoteCreate):
|
| 66 |
+
id: str
|
| 67 |
+
created_at: datetime
|
| 68 |
+
|
| 69 |
+
model_config = ConfigDict(from_attributes=True, extra="ignore")
|
| 70 |
+
|
| 71 |
+
# AI Insight Models
|
| 72 |
+
class FollowupQuestion(BaseModel):
|
| 73 |
+
model_config = ConfigDict(extra="ignore")
|
| 74 |
+
|
| 75 |
+
question: str
|
| 76 |
+
|
| 77 |
+
class ProbableSolution(BaseModel):
|
| 78 |
+
model_config = ConfigDict(extra="ignore")
|
| 79 |
+
|
| 80 |
+
solution: str
|
| 81 |
+
confidence: Optional[float] = None
|
| 82 |
+
|
| 83 |
+
class PossibleReason(BaseModel):
|
| 84 |
+
model_config = ConfigDict(extra="ignore")
|
| 85 |
+
|
| 86 |
+
reason: str
|
| 87 |
+
confidence: Optional[float] = None
|
| 88 |
+
|
| 89 |
+
class InsightCreate(BaseModel):
|
| 90 |
+
model_config = ConfigDict(extra="ignore")
|
| 91 |
+
|
| 92 |
+
dispute_id: str
|
| 93 |
+
priority_level: int
|
| 94 |
+
priority_reason: str
|
| 95 |
+
insights: str
|
| 96 |
+
followup_questions: List[str]
|
| 97 |
+
probable_solutions: List[str]
|
| 98 |
+
possible_reasons: List[str]
|
| 99 |
+
|
| 100 |
+
class Insight(InsightCreate):
|
| 101 |
+
id: str
|
| 102 |
+
created_at: datetime
|
| 103 |
+
|
| 104 |
+
model_config = ConfigDict(from_attributes=True, extra="ignore")
|
| 105 |
+
|
| 106 |
+
# AI Analysis Request/Response
|
| 107 |
+
class DisputeAnalysisResponse(BaseModel):
|
| 108 |
+
model_config = ConfigDict(extra="ignore")
|
| 109 |
+
|
| 110 |
+
dispute_id: str
|
| 111 |
+
analysis: Dict[str, Any]
|
| 112 |
+
|
| 113 |
+
class DashboardMetrics(BaseModel):
|
| 114 |
+
model_config = ConfigDict(extra="ignore")
|
| 115 |
+
|
| 116 |
+
total_disputes: int
|
| 117 |
+
high_priority_count: int
|
| 118 |
+
pending_count: int
|
| 119 |
+
resolved_today: Optional[int] = None
|
| 120 |
+
disputes_by_category: Dict[str, int] = {}
|
| 121 |
+
disputes_by_status: Dict[str, int] = {}
|
| 122 |
+
disputes_by_priority: Dict[str, int] = {}
|
| 123 |
+
average_resolution_time: Optional[str] = None
|
| 124 |
+
|
| 125 |
+
class Insights(BaseModel):
|
| 126 |
+
model_config = ConfigDict(from_attributes=True, extra="ignore")
|
| 127 |
+
|
| 128 |
+
id: str
|
| 129 |
+
dispute_id: str
|
| 130 |
+
insights: str = Field(description="Detailed insights regarding the dispute")
|
| 131 |
+
followup_questions: List[str] = Field(
|
| 132 |
+
description="List of relevant follow-up questions to gather more information"
|
| 133 |
+
)
|
| 134 |
+
probable_solutions: List[str] = Field(
|
| 135 |
+
description="Potential solutions to address the dispute"
|
| 136 |
+
)
|
| 137 |
+
possible_reasons: List[str] = Field(
|
| 138 |
+
description="Possible reasons that might have caused the dispute"
|
| 139 |
+
)
|
| 140 |
+
risk_score: float = Field(
|
| 141 |
+
description="Risk score for the dispute from 0 (lowest) to 10 (highest)"
|
| 142 |
+
)
|
| 143 |
+
risk_factors: List[str] = Field(
|
| 144 |
+
description="Factors contributing to the calculated risk score, if low risk, keep empty"
|
| 145 |
+
)
|
| 146 |
+
priority_level: int
|
| 147 |
+
priority_reason: str
|
| 148 |
+
created_at: datetime
|
| 149 |
+
updated_at: Optional[datetime] = None
|
| 150 |
+
|
| 151 |
+
class InsightsCreate(BaseModel):
|
| 152 |
+
model_config = ConfigDict(extra="ignore")
|
| 153 |
+
|
| 154 |
+
insights: str = Field(description="Detailed insights regarding the dispute")
|
| 155 |
+
followup_questions: List[str] = Field(
|
| 156 |
+
description="List of relevant follow-up questions to gather more information"
|
| 157 |
+
)
|
| 158 |
+
probable_solutions: List[str] = Field(
|
| 159 |
+
description="Potential solutions to address the dispute"
|
| 160 |
+
)
|
| 161 |
+
possible_reasons: List[str] = Field(
|
| 162 |
+
description="Possible reasons that might have caused the dispute"
|
| 163 |
+
)
|
| 164 |
+
risk_score: float = Field(
|
| 165 |
+
description="Risk score for the dispute from 0 (lowest) to 10 (highest)"
|
| 166 |
+
)
|
| 167 |
+
risk_factors: List[str] = Field(
|
| 168 |
+
description="Factors contributing to the calculated risk score, if low risk, keep empty"
|
| 169 |
+
)
|
| 170 |
+
priority_level: int
|
| 171 |
+
priority_reason: str
|
app/api/routes/__init__.py
ADDED
|
File without changes
|
app/api/routes/customers.py
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app/api/routes/customers.py
|
| 2 |
+
import uuid
|
| 3 |
+
from typing import List, Optional
|
| 4 |
+
from fastapi import APIRouter, Depends, HTTPException
|
| 5 |
+
from sqlalchemy.orm import Session
|
| 6 |
+
|
| 7 |
+
from app.api.database import get_db, Customer
|
| 8 |
+
from app.api.models import Dispute as DisputeModel
|
| 9 |
+
from app.api.database import Dispute as DbDispute
|
| 10 |
+
from app.api.models import CustomerCreate, Customer as CustomerModel
|
| 11 |
+
|
| 12 |
+
router = APIRouter()
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
@router.post("/", response_model=CustomerModel, status_code=201)
|
| 16 |
+
async def create_customer(customer: CustomerCreate, db: Session = Depends(get_db)):
|
| 17 |
+
"""Create a new customer"""
|
| 18 |
+
# Check if customer with this email already exists
|
| 19 |
+
db_customer = db.query(Customer).filter(Customer.email == customer.email).first()
|
| 20 |
+
if db_customer:
|
| 21 |
+
raise HTTPException(status_code=400, detail="Email already registered")
|
| 22 |
+
|
| 23 |
+
# Create new customer
|
| 24 |
+
new_customer = Customer(
|
| 25 |
+
id=str(uuid.uuid4()),
|
| 26 |
+
name=customer.name,
|
| 27 |
+
email=customer.email,
|
| 28 |
+
account_type=customer.account_type,
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
# Add to database
|
| 32 |
+
db.add(new_customer)
|
| 33 |
+
db.commit()
|
| 34 |
+
db.refresh(new_customer)
|
| 35 |
+
|
| 36 |
+
# Convert to Pydantic model
|
| 37 |
+
return CustomerModel.model_validate(new_customer.__dict__)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
@router.get("/", response_model=List[CustomerModel])
|
| 41 |
+
async def get_customers(
|
| 42 |
+
db: Session = Depends(get_db),
|
| 43 |
+
skip: int = 0,
|
| 44 |
+
limit: int = 100,
|
| 45 |
+
account_type: str = None,
|
| 46 |
+
):
|
| 47 |
+
"""Get all customers with optional filtering"""
|
| 48 |
+
query = db.query(Customer)
|
| 49 |
+
|
| 50 |
+
# Apply filters
|
| 51 |
+
if account_type:
|
| 52 |
+
query = query.filter(Customer.account_type == account_type)
|
| 53 |
+
|
| 54 |
+
# Apply pagination
|
| 55 |
+
customers = query.offset(skip).limit(limit).all()
|
| 56 |
+
|
| 57 |
+
# Convert to Pydantic models
|
| 58 |
+
return [CustomerModel.model_validate(customer.__dict__) for customer in customers]
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
@router.get("/{customer_id}", response_model=CustomerModel)
|
| 62 |
+
async def get_customer(customer_id: str, db: Session = Depends(get_db)):
|
| 63 |
+
"""Get a specific customer"""
|
| 64 |
+
customer = db.query(Customer).filter(Customer.id == customer_id).first()
|
| 65 |
+
if not customer:
|
| 66 |
+
raise HTTPException(status_code=404, detail="Customer not found")
|
| 67 |
+
|
| 68 |
+
# Convert to Pydantic model
|
| 69 |
+
return CustomerModel.model_validate(customer.__dict__)
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
@router.get("/{customer_id}/disputes", response_model=List[DisputeModel])
|
| 73 |
+
async def get_customer_disputes(
|
| 74 |
+
customer_id: str,
|
| 75 |
+
db: Session = Depends(get_db),
|
| 76 |
+
skip: int = 0,
|
| 77 |
+
limit: int = 100,
|
| 78 |
+
status: Optional[str] = None,
|
| 79 |
+
):
|
| 80 |
+
"""Get all disputes for a specific customer with filtering"""
|
| 81 |
+
try:
|
| 82 |
+
# Check if customer exists
|
| 83 |
+
customer = db.query(Customer).filter(Customer.id == customer_id).first()
|
| 84 |
+
if not customer:
|
| 85 |
+
raise HTTPException(status_code=404, detail="Customer not found")
|
| 86 |
+
|
| 87 |
+
# Build query
|
| 88 |
+
query = db.query(DbDispute).filter(DbDispute.customer_id == customer_id)
|
| 89 |
+
|
| 90 |
+
# Apply status filter if provided
|
| 91 |
+
if status:
|
| 92 |
+
valid_statuses = ["Open", "Under Review", "Resolved"]
|
| 93 |
+
if status not in valid_statuses:
|
| 94 |
+
raise HTTPException(
|
| 95 |
+
status_code=400,
|
| 96 |
+
detail=f"Invalid status. Must be one of: {', '.join(valid_statuses)}",
|
| 97 |
+
)
|
| 98 |
+
query = query.filter(DbDispute.status == status)
|
| 99 |
+
|
| 100 |
+
# Apply sorting and pagination
|
| 101 |
+
query = query.order_by(DbDispute.created_at.desc())
|
| 102 |
+
|
| 103 |
+
# Validate pagination parameters
|
| 104 |
+
if skip < 0:
|
| 105 |
+
skip = 0
|
| 106 |
+
if limit <= 0:
|
| 107 |
+
limit = 100
|
| 108 |
+
elif limit > 500:
|
| 109 |
+
limit = 500
|
| 110 |
+
|
| 111 |
+
disputes = query.offset(skip).limit(limit).all()
|
| 112 |
+
|
| 113 |
+
# Convert to Pydantic models
|
| 114 |
+
return [DisputeModel.model_validate(dispute.__dict__) for dispute in disputes]
|
| 115 |
+
except HTTPException as e:
|
| 116 |
+
raise e # Re-raise HTTPExceptions
|
| 117 |
+
except Exception as e:
|
| 118 |
+
print(f"An unexpected error occurred: {e}")
|
| 119 |
+
import traceback
|
| 120 |
+
|
| 121 |
+
print(traceback.format_exc())
|
| 122 |
+
raise HTTPException(status_code=500, detail=f"Internal Server Error: {str(e)}")
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
@router.put("/{customer_id}", response_model=CustomerModel)
|
| 126 |
+
async def update_customer(
|
| 127 |
+
customer_id: str, customer_update: CustomerCreate, db: Session = Depends(get_db)
|
| 128 |
+
):
|
| 129 |
+
"""Update a customer"""
|
| 130 |
+
customer = db.query(Customer).filter(Customer.id == customer_id).first()
|
| 131 |
+
if not customer:
|
| 132 |
+
raise HTTPException(status_code=404, detail="Customer not found")
|
| 133 |
+
|
| 134 |
+
# Check if email is being changed and if it's already in use
|
| 135 |
+
if customer_update.email != customer.email:
|
| 136 |
+
existing_customer = (
|
| 137 |
+
db.query(Customer).filter(Customer.email == customer_update.email).first()
|
| 138 |
+
)
|
| 139 |
+
if existing_customer:
|
| 140 |
+
raise HTTPException(status_code=400, detail="Email already registered")
|
| 141 |
+
|
| 142 |
+
# Update fields
|
| 143 |
+
customer.name = customer_update.name
|
| 144 |
+
customer.email = customer_update.email
|
| 145 |
+
customer.account_type = customer_update.account_type
|
| 146 |
+
|
| 147 |
+
db.commit()
|
| 148 |
+
db.refresh(customer)
|
| 149 |
+
|
| 150 |
+
# Convert to Pydantic model
|
| 151 |
+
return CustomerModel.model_validate(customer.__dict__)
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
@router.delete("/{customer_id}", response_model=dict)
|
| 155 |
+
async def delete_customer(customer_id: str, db: Session = Depends(get_db)):
|
| 156 |
+
"""Delete a customer with option to cascade delete disputes"""
|
| 157 |
+
customer = db.query(Customer).filter(Customer.id == customer_id).first()
|
| 158 |
+
if not customer:
|
| 159 |
+
raise HTTPException(status_code=404, detail="Customer not found")
|
| 160 |
+
|
| 161 |
+
# Check if customer has disputes
|
| 162 |
+
disputes = db.query(DbDispute).filter(DbDispute.customer_id == customer_id).all()
|
| 163 |
+
|
| 164 |
+
if disputes:
|
| 165 |
+
# Inform user of existing disputes that block deletion
|
| 166 |
+
dispute_count = len(disputes)
|
| 167 |
+
raise HTTPException(
|
| 168 |
+
status_code=400,
|
| 169 |
+
detail=f"Cannot delete customer with existing disputes. The customer has {dispute_count} disputes.",
|
| 170 |
+
)
|
| 171 |
+
|
| 172 |
+
try:
|
| 173 |
+
# Delete the customer
|
| 174 |
+
db.delete(customer)
|
| 175 |
+
db.commit()
|
| 176 |
+
return {"message": "Customer deleted successfully"}
|
| 177 |
+
except Exception as e:
|
| 178 |
+
db.rollback()
|
| 179 |
+
raise HTTPException(
|
| 180 |
+
status_code=500, detail=f"Failed to delete customer: {str(e)}"
|
| 181 |
+
)
|
app/api/routes/disputes.py
ADDED
|
@@ -0,0 +1,505 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app/api/routes/disputes.py
|
| 2 |
+
import uuid
|
| 3 |
+
from typing import List, Optional
|
| 4 |
+
from datetime import datetime
|
| 5 |
+
from fastapi import APIRouter, Depends, HTTPException, Query
|
| 6 |
+
from sqlalchemy.orm import Session
|
| 7 |
+
from sqlalchemy import case
|
| 8 |
+
import json
|
| 9 |
+
from app.api.database import get_db, Dispute, Customer, DisputeNote, DisputeInsight
|
| 10 |
+
from app.api.models import (
|
| 11 |
+
DisputeCreate,
|
| 12 |
+
Dispute as DisputeModel,
|
| 13 |
+
DisputeUpdate,
|
| 14 |
+
DisputeWithCustomer,
|
| 15 |
+
DisputeAnalysisResponse,
|
| 16 |
+
)
|
| 17 |
+
from app.ai.langchain_service import DisputeAIService
|
| 18 |
+
|
| 19 |
+
router = APIRouter()
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
# Dependency for AI service
|
| 23 |
+
def get_ai_service():
|
| 24 |
+
return DisputeAIService()
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
@router.post("/", response_model=DisputeModel, status_code=201)
|
| 28 |
+
async def create_dispute(
|
| 29 |
+
dispute: DisputeCreate,
|
| 30 |
+
db: Session = Depends(get_db),
|
| 31 |
+
):
|
| 32 |
+
"""Create a new dispute"""
|
| 33 |
+
# Check if customer exists
|
| 34 |
+
customer = db.query(Customer).filter(Customer.id == dispute.customer_id).first()
|
| 35 |
+
if not customer:
|
| 36 |
+
raise HTTPException(status_code=404, detail="Customer not found")
|
| 37 |
+
|
| 38 |
+
# Create new dispute with all required fields from DisputeCreate model
|
| 39 |
+
new_dispute = Dispute(
|
| 40 |
+
id=str(uuid.uuid4()),
|
| 41 |
+
customer_id=dispute.customer_id,
|
| 42 |
+
transaction_id=dispute.transaction_id,
|
| 43 |
+
merchant_name=dispute.merchant_name,
|
| 44 |
+
amount=dispute.amount,
|
| 45 |
+
description=dispute.description,
|
| 46 |
+
category=dispute.category,
|
| 47 |
+
status="Open",
|
| 48 |
+
priority=None,
|
| 49 |
+
created_at=datetime.utcnow(),
|
| 50 |
+
resolved_at=None,
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
# Increment customer dispute count
|
| 54 |
+
customer.dispute_count += 1
|
| 55 |
+
|
| 56 |
+
# Add to database
|
| 57 |
+
db.add(new_dispute)
|
| 58 |
+
db.commit()
|
| 59 |
+
db.refresh(new_dispute)
|
| 60 |
+
|
| 61 |
+
# Convert to Pydantic model
|
| 62 |
+
return DisputeModel.model_validate(new_dispute.__dict__, strict=False)
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
@router.get(
|
| 66 |
+
"/",
|
| 67 |
+
response_model=List[DisputeModel],
|
| 68 |
+
)
|
| 69 |
+
async def get_disputes(
|
| 70 |
+
db: Session = Depends(get_db),
|
| 71 |
+
skip: int = 0,
|
| 72 |
+
limit: int = 100,
|
| 73 |
+
status: Optional[str] = None,
|
| 74 |
+
priority: Optional[int] = None,
|
| 75 |
+
category: Optional[str] = None,
|
| 76 |
+
priority_sort: bool = Query(True, description="Sort by priority (high to low)"),
|
| 77 |
+
date_sort: str = Query("desc", description="Sort by date ('asc' or 'desc')"),
|
| 78 |
+
):
|
| 79 |
+
"""Get all disputes with improved filtering and sorting"""
|
| 80 |
+
query = db.query(Dispute)
|
| 81 |
+
|
| 82 |
+
# Apply filters
|
| 83 |
+
if status:
|
| 84 |
+
valid_statuses = ["Open", "Under Review", "Resolved"]
|
| 85 |
+
if status not in valid_statuses:
|
| 86 |
+
raise HTTPException(
|
| 87 |
+
status_code=400,
|
| 88 |
+
detail=f"Invalid status filter. Must be one of: {', '.join(valid_statuses)}",
|
| 89 |
+
)
|
| 90 |
+
query = query.filter(Dispute.status == status)
|
| 91 |
+
|
| 92 |
+
if priority:
|
| 93 |
+
if not 1 <= priority <= 5:
|
| 94 |
+
raise HTTPException(
|
| 95 |
+
status_code=400, detail="Priority filter must be between 1 and 5"
|
| 96 |
+
)
|
| 97 |
+
query = query.filter(Dispute.priority == priority)
|
| 98 |
+
|
| 99 |
+
if category:
|
| 100 |
+
query = query.filter(Dispute.category == category)
|
| 101 |
+
|
| 102 |
+
# Apply sorting
|
| 103 |
+
if priority_sort:
|
| 104 |
+
# Sort by priority descending (nulls last)
|
| 105 |
+
query = query.order_by(
|
| 106 |
+
case({None: 0}, value=Dispute.priority, else_=Dispute.priority).desc()
|
| 107 |
+
)
|
| 108 |
+
|
| 109 |
+
# Apply date sorting
|
| 110 |
+
if date_sort.lower() == "asc":
|
| 111 |
+
query = query.order_by(Dispute.created_at.asc())
|
| 112 |
+
else:
|
| 113 |
+
query = query.order_by(Dispute.created_at.desc())
|
| 114 |
+
|
| 115 |
+
# Apply pagination with validation
|
| 116 |
+
if skip < 0:
|
| 117 |
+
skip = 0
|
| 118 |
+
if limit <= 0:
|
| 119 |
+
limit = 100
|
| 120 |
+
elif limit > 500: # Set a reasonable maximum
|
| 121 |
+
limit = 500
|
| 122 |
+
|
| 123 |
+
# Execute query with pagination
|
| 124 |
+
try:
|
| 125 |
+
disputes = query.offset(skip).limit(limit).all()
|
| 126 |
+
# Convert to Pydantic models
|
| 127 |
+
return [DisputeModel.model_validate(dispute.__dict__) for dispute in disputes]
|
| 128 |
+
except Exception as e:
|
| 129 |
+
raise HTTPException(status_code=500, detail=f"Database query error: {str(e)}")
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
@router.get("/{dispute_id}", response_model=DisputeWithCustomer)
|
| 133 |
+
async def get_dispute(dispute_id: str, db: Session = Depends(get_db)):
|
| 134 |
+
"""Get a specific dispute with customer details"""
|
| 135 |
+
dispute = db.query(Dispute).filter(Dispute.id == dispute_id).first()
|
| 136 |
+
if not dispute:
|
| 137 |
+
raise HTTPException(status_code=404, detail="Dispute not found")
|
| 138 |
+
|
| 139 |
+
# Get the customer
|
| 140 |
+
customer = db.query(Customer).filter(Customer.id == dispute.customer_id).first()
|
| 141 |
+
|
| 142 |
+
# Prepare the result dict
|
| 143 |
+
result = dispute.__dict__.copy()
|
| 144 |
+
result["customer"] = customer.__dict__ if customer else None
|
| 145 |
+
|
| 146 |
+
# Convert to Pydantic model
|
| 147 |
+
return DisputeWithCustomer.model_validate(result)
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
@router.put("/{dispute_id}", response_model=DisputeModel)
|
| 151 |
+
async def update_dispute(
|
| 152 |
+
dispute_id: str, dispute_update: DisputeUpdate, db: Session = Depends(get_db)
|
| 153 |
+
):
|
| 154 |
+
"""Update a dispute"""
|
| 155 |
+
dispute = db.query(Dispute).filter(Dispute.id == dispute_id).first()
|
| 156 |
+
if not dispute:
|
| 157 |
+
raise HTTPException(status_code=404, detail="Dispute not found")
|
| 158 |
+
|
| 159 |
+
# Check if any fields are provided
|
| 160 |
+
update_data = dispute_update.model_dump(exclude_unset=True)
|
| 161 |
+
if not update_data:
|
| 162 |
+
raise HTTPException(status_code=400, detail="No valid fields to update")
|
| 163 |
+
|
| 164 |
+
# Update fields with validation
|
| 165 |
+
if dispute_update.status is not None:
|
| 166 |
+
# Validate status
|
| 167 |
+
valid_statuses = ["Open", "Under Review", "Resolved"]
|
| 168 |
+
if dispute_update.status not in valid_statuses:
|
| 169 |
+
raise HTTPException(
|
| 170 |
+
status_code=400,
|
| 171 |
+
detail=f"Invalid status. Must be one of: {', '.join(valid_statuses)}",
|
| 172 |
+
)
|
| 173 |
+
|
| 174 |
+
dispute.status = dispute_update.status
|
| 175 |
+
if dispute_update.status == "Resolved" and not dispute.resolved_at:
|
| 176 |
+
dispute.resolved_at = datetime.utcnow()
|
| 177 |
+
|
| 178 |
+
if dispute_update.priority is not None:
|
| 179 |
+
# Validate priority
|
| 180 |
+
if not 1 <= dispute_update.priority <= 5:
|
| 181 |
+
raise HTTPException(
|
| 182 |
+
status_code=400, detail="Priority must be between 1 and 5"
|
| 183 |
+
)
|
| 184 |
+
dispute.priority = dispute_update.priority
|
| 185 |
+
|
| 186 |
+
if dispute_update.description is not None:
|
| 187 |
+
dispute.description = dispute_update.description
|
| 188 |
+
|
| 189 |
+
try:
|
| 190 |
+
db.commit()
|
| 191 |
+
db.refresh(dispute)
|
| 192 |
+
# Convert to Pydantic model
|
| 193 |
+
return DisputeModel.model_validate(dispute.__dict__)
|
| 194 |
+
except Exception as e:
|
| 195 |
+
db.rollback()
|
| 196 |
+
raise HTTPException(status_code=500, detail=f"Database error: {str(e)}")
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
# Rest of the code remains the same...
|
| 200 |
+
# Just be sure to add the proper model_validate conversion in each endpoint
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
@router.post(
|
| 204 |
+
"/{dispute_id}/analyze", response_model=DisputeAnalysisResponse, status_code=201
|
| 205 |
+
)
|
| 206 |
+
async def analyze_dispute(
|
| 207 |
+
dispute_id: str,
|
| 208 |
+
db: Session = Depends(get_db),
|
| 209 |
+
ai_service: DisputeAIService = Depends(get_ai_service),
|
| 210 |
+
):
|
| 211 |
+
# The response is already properly converted to a Pydantic model
|
| 212 |
+
# No changes needed
|
| 213 |
+
try:
|
| 214 |
+
# Get dispute from database
|
| 215 |
+
dispute = db.query(Dispute).filter(Dispute.id == dispute_id).first()
|
| 216 |
+
if not dispute:
|
| 217 |
+
raise HTTPException(status_code=404, detail="Dispute not found")
|
| 218 |
+
|
| 219 |
+
# Check if analysis already exists
|
| 220 |
+
existing_insight = (
|
| 221 |
+
db.query(DisputeInsight)
|
| 222 |
+
.filter(DisputeInsight.dispute_id == dispute_id)
|
| 223 |
+
.first()
|
| 224 |
+
)
|
| 225 |
+
if existing_insight:
|
| 226 |
+
# Return existing analysis instead of creating a duplicate
|
| 227 |
+
analysis_result = {
|
| 228 |
+
"priority": existing_insight.priority_level,
|
| 229 |
+
"priority_reason": existing_insight.priority_reason,
|
| 230 |
+
"insights": existing_insight.insights,
|
| 231 |
+
"followup_questions": json.loads(existing_insight.followup_questions),
|
| 232 |
+
"probable_solutions": json.loads(existing_insight.probable_solutions),
|
| 233 |
+
"possible_reasons": json.loads(existing_insight.possible_reasons),
|
| 234 |
+
"risk_score": existing_insight.risk_score,
|
| 235 |
+
"risk_factors": json.loads(existing_insight.risk_factors),
|
| 236 |
+
}
|
| 237 |
+
response_data = {"dispute_id": dispute_id, "analysis": analysis_result}
|
| 238 |
+
return DisputeAnalysisResponse.model_validate(response_data)
|
| 239 |
+
|
| 240 |
+
# Get customer information
|
| 241 |
+
customer = db.query(Customer).filter(Customer.id == dispute.customer_id).first()
|
| 242 |
+
if not customer:
|
| 243 |
+
raise HTTPException(status_code=404, detail="Customer not found")
|
| 244 |
+
|
| 245 |
+
# Prepare data for AI analysis
|
| 246 |
+
dispute_data = {
|
| 247 |
+
"dispute_id": dispute.id,
|
| 248 |
+
"customer_id": dispute.customer_id,
|
| 249 |
+
"customer_name": customer.name,
|
| 250 |
+
"customer_type": customer.account_type,
|
| 251 |
+
"transaction_id": dispute.transaction_id,
|
| 252 |
+
"merchant_name": dispute.merchant_name,
|
| 253 |
+
"transaction_date": dispute.created_at.isoformat(),
|
| 254 |
+
"dispute_date": dispute.created_at.isoformat(),
|
| 255 |
+
"transaction_amount": dispute.amount,
|
| 256 |
+
"dispute_description": dispute.description,
|
| 257 |
+
"category": dispute.category,
|
| 258 |
+
"previous_disputes_count": customer.dispute_count,
|
| 259 |
+
"customer_account_age_days": (
|
| 260 |
+
(datetime.utcnow() - customer.created_at).days
|
| 261 |
+
),
|
| 262 |
+
"has_supporting_documents": False, # Example value
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
# Analyze dispute using AI
|
| 266 |
+
analysis_result = ai_service.analyze_dispute(dispute_data)
|
| 267 |
+
|
| 268 |
+
# Validate required fields in analysis_result
|
| 269 |
+
required_fields = [
|
| 270 |
+
"priority",
|
| 271 |
+
"priority_reason",
|
| 272 |
+
"insights",
|
| 273 |
+
"followup_questions",
|
| 274 |
+
"probable_solutions",
|
| 275 |
+
"possible_reasons",
|
| 276 |
+
"risk_score",
|
| 277 |
+
"risk_factors",
|
| 278 |
+
]
|
| 279 |
+
for field in required_fields:
|
| 280 |
+
if field not in analysis_result:
|
| 281 |
+
raise ValueError(f"AI analysis missing required field: {field}")
|
| 282 |
+
|
| 283 |
+
# Update dispute with priority (from analysis_result, not directly setting it)
|
| 284 |
+
dispute.priority = analysis_result["priority"]
|
| 285 |
+
db.commit()
|
| 286 |
+
|
| 287 |
+
# Store AI insights with proper field validation
|
| 288 |
+
insight = DisputeInsight(
|
| 289 |
+
id=str(uuid.uuid4()),
|
| 290 |
+
dispute_id=dispute_id,
|
| 291 |
+
priority_level=analysis_result["priority"],
|
| 292 |
+
priority_reason=analysis_result["priority_reason"],
|
| 293 |
+
insights=analysis_result["insights"],
|
| 294 |
+
followup_questions=json.dumps(analysis_result["followup_questions"]),
|
| 295 |
+
probable_solutions=json.dumps(analysis_result["probable_solutions"]),
|
| 296 |
+
possible_reasons=json.dumps(analysis_result["possible_reasons"]),
|
| 297 |
+
risk_score=analysis_result["risk_score"],
|
| 298 |
+
risk_factors=json.dumps(analysis_result["risk_factors"]),
|
| 299 |
+
)
|
| 300 |
+
db.add(insight)
|
| 301 |
+
db.commit()
|
| 302 |
+
|
| 303 |
+
# Return analysis results
|
| 304 |
+
response_data = {"dispute_id": dispute_id, "analysis": analysis_result}
|
| 305 |
+
return DisputeAnalysisResponse.model_validate(response_data)
|
| 306 |
+
except ValueError as e:
|
| 307 |
+
# Handle missing fields in AI response
|
| 308 |
+
raise HTTPException(status_code=400, detail=str(e))
|
| 309 |
+
except Exception as e:
|
| 310 |
+
# Log the detailed error for debugging
|
| 311 |
+
import traceback
|
| 312 |
+
|
| 313 |
+
print(f"AI analysis failed: {str(e)}")
|
| 314 |
+
print(traceback.format_exc())
|
| 315 |
+
raise HTTPException(status_code=500, detail=f"AI analysis failed: {str(e)}")
|
| 316 |
+
|
| 317 |
+
|
| 318 |
+
@router.delete("/{dispute_id}", response_model=dict)
|
| 319 |
+
async def delete_dispute(dispute_id: str, db: Session = Depends(get_db)):
|
| 320 |
+
"""Delete a dispute"""
|
| 321 |
+
dispute = db.query(Dispute).filter(Dispute.id == dispute_id).first()
|
| 322 |
+
if not dispute:
|
| 323 |
+
raise HTTPException(status_code=404, detail="Dispute not found")
|
| 324 |
+
|
| 325 |
+
# Check and delete dispute notes if they exist
|
| 326 |
+
if db.query(DisputeNote).filter(DisputeNote.dispute_id == dispute_id).first():
|
| 327 |
+
db.query(DisputeNote).filter(DisputeNote.dispute_id == dispute_id).delete()
|
| 328 |
+
|
| 329 |
+
# Check and delete dispute insights if they exist
|
| 330 |
+
if db.query(DisputeInsight).filter(DisputeInsight.dispute_id == dispute_id).first():
|
| 331 |
+
db.query(DisputeInsight).filter(
|
| 332 |
+
DisputeInsight.dispute_id == dispute_id
|
| 333 |
+
).delete()
|
| 334 |
+
|
| 335 |
+
# Delete the dispute
|
| 336 |
+
db.delete(dispute)
|
| 337 |
+
db.commit()
|
| 338 |
+
|
| 339 |
+
return {"message": "Dispute deleted successfully"}
|
| 340 |
+
|
| 341 |
+
|
| 342 |
+
from app.api.models import Insights, InsightsCreate
|
| 343 |
+
|
| 344 |
+
|
| 345 |
+
@router.post("/{dispute_id}/insights", response_model=Insights, status_code=201)
|
| 346 |
+
async def create_dispute_insights(
|
| 347 |
+
dispute_id: str,
|
| 348 |
+
insight_data: InsightsCreate,
|
| 349 |
+
db: Session = Depends(get_db),
|
| 350 |
+
):
|
| 351 |
+
"""Create new insights for a dispute with proper validation"""
|
| 352 |
+
# Check if dispute exists
|
| 353 |
+
dispute = db.query(Dispute).filter(Dispute.id == dispute_id).first()
|
| 354 |
+
if not dispute:
|
| 355 |
+
raise HTTPException(status_code=404, detail="Dispute not found")
|
| 356 |
+
|
| 357 |
+
# Check if insight already exists for this dispute
|
| 358 |
+
existing_insight = (
|
| 359 |
+
db.query(DisputeInsight).filter(DisputeInsight.dispute_id == dispute_id).first()
|
| 360 |
+
)
|
| 361 |
+
if existing_insight:
|
| 362 |
+
raise HTTPException(
|
| 363 |
+
status_code=400, detail="Insights already exist for this dispute"
|
| 364 |
+
)
|
| 365 |
+
|
| 366 |
+
# Validate risk_score range
|
| 367 |
+
if not 0 <= insight_data.risk_score <= 10:
|
| 368 |
+
raise HTTPException(
|
| 369 |
+
status_code=400, detail="Risk score must be between 0 and 10"
|
| 370 |
+
)
|
| 371 |
+
|
| 372 |
+
# Validate priority_level range
|
| 373 |
+
if not 1 <= insight_data.priority_level <= 5:
|
| 374 |
+
raise HTTPException(
|
| 375 |
+
status_code=400, detail="Priority level must be between 1 and 5"
|
| 376 |
+
)
|
| 377 |
+
|
| 378 |
+
try:
|
| 379 |
+
# Create new insight with proper JSON serialization
|
| 380 |
+
new_insight = DisputeInsight(
|
| 381 |
+
id=str(uuid.uuid4()),
|
| 382 |
+
dispute_id=dispute_id,
|
| 383 |
+
insights=insight_data.insights,
|
| 384 |
+
followup_questions=json.dumps(insight_data.followup_questions),
|
| 385 |
+
probable_solutions=json.dumps(insight_data.probable_solutions),
|
| 386 |
+
possible_reasons=json.dumps(insight_data.possible_reasons),
|
| 387 |
+
risk_score=insight_data.risk_score,
|
| 388 |
+
risk_factors=json.dumps(insight_data.risk_factors),
|
| 389 |
+
priority_level=insight_data.priority_level,
|
| 390 |
+
priority_reason=insight_data.priority_reason,
|
| 391 |
+
)
|
| 392 |
+
|
| 393 |
+
# Update the dispute's priority to match the insight priority
|
| 394 |
+
dispute.priority = insight_data.priority_level
|
| 395 |
+
|
| 396 |
+
# Add insight to database
|
| 397 |
+
db.add(new_insight)
|
| 398 |
+
db.commit()
|
| 399 |
+
db.refresh(new_insight)
|
| 400 |
+
|
| 401 |
+
# Return formatted response
|
| 402 |
+
return _format_insight_response(new_insight)
|
| 403 |
+
except Exception as e:
|
| 404 |
+
db.rollback()
|
| 405 |
+
raise HTTPException(
|
| 406 |
+
status_code=500, detail=f"Error creating insights: {str(e)}"
|
| 407 |
+
)
|
| 408 |
+
|
| 409 |
+
|
| 410 |
+
@router.get("/{dispute_id}/insights", response_model=Insights)
|
| 411 |
+
async def get_dispute_insights(dispute_id: str, db: Session = Depends(get_db)):
|
| 412 |
+
"""Get insights for a specific dispute"""
|
| 413 |
+
# Check if dispute exists
|
| 414 |
+
dispute = db.query(Dispute).filter(Dispute.id == dispute_id).first()
|
| 415 |
+
if not dispute:
|
| 416 |
+
raise HTTPException(status_code=404, detail="Dispute not found")
|
| 417 |
+
|
| 418 |
+
# Get the insights
|
| 419 |
+
insight = (
|
| 420 |
+
db.query(DisputeInsight).filter(DisputeInsight.dispute_id == dispute_id).first()
|
| 421 |
+
)
|
| 422 |
+
if not insight:
|
| 423 |
+
raise HTTPException(
|
| 424 |
+
status_code=404, detail="No insights found for this dispute"
|
| 425 |
+
)
|
| 426 |
+
|
| 427 |
+
# Handle potentially malformed JSON with more detailed error messages
|
| 428 |
+
try:
|
| 429 |
+
return _format_insight_response(insight)
|
| 430 |
+
except json.JSONDecodeError as e:
|
| 431 |
+
# Log the specific JSON error
|
| 432 |
+
print(f"JSON decode error in dispute insights: {str(e)}")
|
| 433 |
+
# Create a response with empty lists for fields that failed to parse
|
| 434 |
+
insight_data = {
|
| 435 |
+
"id": insight.id,
|
| 436 |
+
"dispute_id": insight.dispute_id,
|
| 437 |
+
"insights": insight.insights,
|
| 438 |
+
"followup_questions": [],
|
| 439 |
+
"probable_solutions": [],
|
| 440 |
+
"possible_reasons": [],
|
| 441 |
+
"risk_score": insight.risk_score,
|
| 442 |
+
"risk_factors": [],
|
| 443 |
+
"priority_level": insight.priority_level,
|
| 444 |
+
"priority_reason": insight.priority_reason,
|
| 445 |
+
"created_at": insight.created_at,
|
| 446 |
+
"updated_at": insight.updated_at,
|
| 447 |
+
}
|
| 448 |
+
return Insights.model_validate(insight_data)
|
| 449 |
+
|
| 450 |
+
|
| 451 |
+
@router.put("/{dispute_id}/insights", response_model=Insights)
|
| 452 |
+
async def update_dispute_insights(
|
| 453 |
+
dispute_id: str, insight_data: InsightsCreate, db: Session = Depends(get_db)
|
| 454 |
+
):
|
| 455 |
+
"""Update insights for a specific dispute"""
|
| 456 |
+
# Check if dispute exists
|
| 457 |
+
dispute = db.query(Dispute).filter(Dispute.id == dispute_id).first()
|
| 458 |
+
if not dispute:
|
| 459 |
+
raise HTTPException(status_code=404, detail="Dispute not found")
|
| 460 |
+
|
| 461 |
+
# Get the existing insight
|
| 462 |
+
insight = (
|
| 463 |
+
db.query(DisputeInsight).filter(DisputeInsight.dispute_id == dispute_id).first()
|
| 464 |
+
)
|
| 465 |
+
if not insight:
|
| 466 |
+
raise HTTPException(
|
| 467 |
+
status_code=404, detail="No insights found for this dispute"
|
| 468 |
+
)
|
| 469 |
+
|
| 470 |
+
# Update fields
|
| 471 |
+
insight.insights = insight_data.insights
|
| 472 |
+
insight.followup_questions = json.dumps(insight_data.followup_questions)
|
| 473 |
+
insight.probable_solutions = json.dumps(insight_data.probable_solutions)
|
| 474 |
+
insight.possible_reasons = json.dumps(insight_data.possible_reasons)
|
| 475 |
+
insight.risk_score = insight_data.risk_score
|
| 476 |
+
insight.risk_factors = json.dumps(insight_data.risk_factors)
|
| 477 |
+
insight.priority_level = insight_data.priority_level
|
| 478 |
+
insight.priority_reason = insight_data.priority_reason
|
| 479 |
+
|
| 480 |
+
# Commit changes
|
| 481 |
+
db.commit()
|
| 482 |
+
db.refresh(insight)
|
| 483 |
+
|
| 484 |
+
# Convert the stored JSON strings back to lists for the response
|
| 485 |
+
return _format_insight_response(insight)
|
| 486 |
+
|
| 487 |
+
|
| 488 |
+
# Helper function to format insight response
|
| 489 |
+
def _format_insight_response(insight):
|
| 490 |
+
"""Convert SQLAlchemy model to Pydantic model with JSON parsing"""
|
| 491 |
+
insight_data = {
|
| 492 |
+
"id": insight.id,
|
| 493 |
+
"dispute_id": insight.dispute_id,
|
| 494 |
+
"insights": insight.insights,
|
| 495 |
+
"followup_questions": json.loads(insight.followup_questions),
|
| 496 |
+
"probable_solutions": json.loads(insight.probable_solutions),
|
| 497 |
+
"possible_reasons": json.loads(insight.possible_reasons),
|
| 498 |
+
"risk_score": insight.risk_score,
|
| 499 |
+
"risk_factors": json.loads(insight.risk_factors),
|
| 500 |
+
"priority_level": insight.priority_level,
|
| 501 |
+
"priority_reason": insight.priority_reason,
|
| 502 |
+
"created_at": insight.created_at,
|
| 503 |
+
"updated_at": insight.updated_at if hasattr(insight, "updated_at") else None,
|
| 504 |
+
}
|
| 505 |
+
return Insights.model_validate(insight_data)
|
app/api/services/__init__.py
ADDED
|
File without changes
|
app/api/services/ai_service.py
ADDED
|
File without changes
|
app/api/services/database_service.py
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import uuid
|
| 2 |
+
from datetime import datetime
|
| 3 |
+
from sqlalchemy.orm import Session
|
| 4 |
+
from typing import List, Optional, Dict, Any, Union
|
| 5 |
+
|
| 6 |
+
from ..database import (
|
| 7 |
+
Customer,
|
| 8 |
+
Dispute,
|
| 9 |
+
DisputeAnalysis,
|
| 10 |
+
SupportingDocument,
|
| 11 |
+
DisputeStatus,
|
| 12 |
+
)
|
| 13 |
+
from ..models import (
|
| 14 |
+
CustomerCreate,
|
| 15 |
+
DisputeCreate,
|
| 16 |
+
CustomerUpdate,
|
| 17 |
+
DisputeUpdate,
|
| 18 |
+
DisputeAnalysisCreate,
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
# Customer operations
|
| 23 |
+
def create_customer(db: Session, customer: CustomerCreate) -> Customer:
|
| 24 |
+
"""Create a new customer in the database"""
|
| 25 |
+
db_customer = Customer(
|
| 26 |
+
id=str(uuid.uuid4()),
|
| 27 |
+
name=customer.name,
|
| 28 |
+
email=customer.email,
|
| 29 |
+
phone=customer.phone,
|
| 30 |
+
account_type=customer.account_type,
|
| 31 |
+
)
|
| 32 |
+
db.add(db_customer)
|
| 33 |
+
db.commit()
|
| 34 |
+
db.refresh(db_customer)
|
| 35 |
+
return db_customer
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def get_customer(db: Session, customer_id: str) -> Optional[Customer]:
|
| 39 |
+
"""Get a customer by ID"""
|
| 40 |
+
return db.query(Customer).filter(Customer.id == customer_id).first()
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def get_customer_by_email(db: Session, email: str) -> Optional[Customer]:
|
| 44 |
+
"""Get a customer by email"""
|
| 45 |
+
return db.query(Customer).filter(Customer.email == email).first()
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def get_customers(db: Session, skip: int = 0, limit: int = 100) -> List[Customer]:
|
| 49 |
+
"""Get a list of customers"""
|
| 50 |
+
return db.query(Customer).offset(skip).limit(limit).all()
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def update_customer(
|
| 54 |
+
db: Session, customer_id: str, customer: CustomerUpdate
|
| 55 |
+
) -> Optional[Customer]:
|
| 56 |
+
"""Update a customer's information"""
|
| 57 |
+
db_customer = get_customer(db, customer_id)
|
| 58 |
+
if not db_customer:
|
| 59 |
+
return None
|
| 60 |
+
|
| 61 |
+
# Update only the fields that are provided
|
| 62 |
+
customer_data = customer.dict(exclude_unset=True)
|
| 63 |
+
for key, value in customer_data.items():
|
| 64 |
+
setattr(db_customer, key, value)
|
| 65 |
+
|
| 66 |
+
db_customer.updated_at = datetime.utcnow()
|
| 67 |
+
db.commit()
|
| 68 |
+
db.refresh(db_customer)
|
| 69 |
+
return db_customer
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def delete_customer(db: Session, customer_id: str) -> bool:
|
| 73 |
+
"""Delete a customer"""
|
| 74 |
+
db_customer = get_customer(db, customer_id)
|
| 75 |
+
if not db_customer:
|
| 76 |
+
return False
|
| 77 |
+
|
| 78 |
+
db.delete(db_customer)
|
| 79 |
+
db.commit()
|
| 80 |
+
return True
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def increment_dispute_count(db: Session, customer_id: str) -> Optional[Customer]:
|
| 84 |
+
"""Increment the dispute count for a customer"""
|
| 85 |
+
db_customer = get_customer(db, customer_id)
|
| 86 |
+
if not db_customer:
|
| 87 |
+
return None
|
| 88 |
+
|
| 89 |
+
db_customer.dispute_count += 1
|
| 90 |
+
db_customer.updated_at = datetime.utcnow()
|
| 91 |
+
db.commit()
|
| 92 |
+
db.refresh(db_customer)
|
| 93 |
+
return db_customer
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
# Dispute operations
|
| 97 |
+
def create_dispute(db: Session, dispute: DisputeCreate) -> Dispute:
|
| 98 |
+
"""Create a new dispute in the database"""
|
| 99 |
+
dispute_id = str(uuid.uuid4())
|
| 100 |
+
db_dispute = Dispute(
|
| 101 |
+
id=dispute_id,
|
| 102 |
+
customer_id=dispute.customer_id,
|
| 103 |
+
transaction_id=dispute.transaction_id,
|
| 104 |
+
merchant_name=dispute.merchant_name,
|
| 105 |
+
amount=dispute.amount,
|
| 106 |
+
description=dispute.description,
|
| 107 |
+
category=dispute.category,
|
| 108 |
+
status=DisputeStatus.NEW,
|
| 109 |
+
)
|
| 110 |
+
db.add(db_dispute)
|
| 111 |
+
db.commit()
|
| 112 |
+
db.refresh(db_dispute)
|
| 113 |
+
|
| 114 |
+
# Increment the customer's dispute count
|
| 115 |
+
increment_dispute_count(db, dispute.customer_id)
|
| 116 |
+
|
| 117 |
+
return db_dispute
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def get_dispute(db: Session, dispute_id: str) -> Optional[Dispute]:
|
| 121 |
+
"""Get a dispute by ID"""
|
| 122 |
+
return db.query(Dispute).filter(Dispute.id == dispute_id).first()
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def get_disputes(
|
| 126 |
+
db: Session,
|
| 127 |
+
skip: int = 0,
|
| 128 |
+
limit: int = 100,
|
| 129 |
+
status: Optional[List[DisputeStatus]] = None,
|
| 130 |
+
priority: Optional[List[int]] = None,
|
| 131 |
+
customer_id: Optional[str] = None,
|
| 132 |
+
sort_by: str = "created_at",
|
| 133 |
+
sort_desc: bool = True,
|
| 134 |
+
) -> List[Dispute]:
|
| 135 |
+
"""Get a list of disputes with filtering and sorting options"""
|
| 136 |
+
query = db.query(Dispute)
|
| 137 |
+
|
| 138 |
+
# Apply filters
|
| 139 |
+
if status:
|
| 140 |
+
query = query.filter(Dispute.status.in_(status))
|
| 141 |
+
|
| 142 |
+
if priority:
|
| 143 |
+
query = query.filter(Dispute.priority.in_(priority))
|
| 144 |
+
|
| 145 |
+
if customer_id:
|
| 146 |
+
query = query.filter(Dispute.customer_id == customer_id)
|
| 147 |
+
|
| 148 |
+
# Apply sorting
|
| 149 |
+
if sort_by == "priority" and sort_desc:
|
| 150 |
+
query = query.order_by(
|
| 151 |
+
Dispute.priority.desc().nullslast(), Dispute.created_at.desc()
|
| 152 |
+
)
|
| 153 |
+
elif sort_by == "priority" and not sort_desc:
|
| 154 |
+
query = query.order_by(
|
| 155 |
+
Dispute.priority.asc().nullsfirst(), Dispute.created_at.desc()
|
| 156 |
+
)
|
| 157 |
+
elif sort_by == "amount" and sort_desc:
|
| 158 |
+
query = query.order_by(Dispute.amount.desc(), Dispute.created_at.desc())
|
| 159 |
+
elif sort_by == "amount" and not sort_desc:
|
| 160 |
+
query = query.order_by(Dispute.amount.asc(), Dispute.created_at.desc())
|
| 161 |
+
elif sort_desc:
|
| 162 |
+
query = query.order_by(getattr(Dispute, sort_by).desc())
|
| 163 |
+
else:
|
| 164 |
+
query = query.order_by(getattr(Dispute, sort_by).asc())
|
| 165 |
+
|
| 166 |
+
return query.offset(skip).limit(limit).all()
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
def update_dispute(
|
| 170 |
+
db: Session, dispute_id: str, dispute: DisputeUpdate
|
| 171 |
+
) -> Optional[Dispute]:
|
| 172 |
+
"""Update a dispute's information"""
|
| 173 |
+
db_dispute = get_dispute(db, dispute_id)
|
| 174 |
+
if not db_dispute:
|
| 175 |
+
return None
|
| 176 |
+
|
| 177 |
+
# Update only the fields that are provided
|
| 178 |
+
dispute_data = dispute.dict(exclude_unset=True)
|
| 179 |
+
for key, value in dispute_data.items():
|
| 180 |
+
if key == "status" and value in [
|
| 181 |
+
DisputeStatus.APPROVED,
|
| 182 |
+
DisputeStatus.REJECTED,
|
| 183 |
+
DisputeStatus.CLOSED,
|
| 184 |
+
]:
|
| 185 |
+
# Set resolved_at timestamp when dispute is resolved
|
| 186 |
+
db_dispute.resolved_at = datetime.utcnow()
|
| 187 |
+
setattr(db_dispute, key, value)
|
| 188 |
+
|
| 189 |
+
db_dispute.updated_at = datetime.utcnow()
|
| 190 |
+
db.commit()
|
| 191 |
+
db.refresh(db_dispute)
|
| 192 |
+
return db_dispute
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
def delete_dispute(db: Session, dispute_id: str) -> bool:
|
| 196 |
+
"""Delete a dispute"""
|
| 197 |
+
db_dispute = get_dispute(db, dispute_id)
|
| 198 |
+
if not db_dispute:
|
| 199 |
+
return False
|
| 200 |
+
|
| 201 |
+
db.delete(db_dispute)
|
| 202 |
+
db.commit()
|
| 203 |
+
return True
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
# AI Analysis operations
|
| 207 |
+
def create_dispute_analysis(
|
| 208 |
+
db: Session, dispute_id: str, analysis: DisputeAnalysisCreate
|
| 209 |
+
) -> Optional[DisputeAnalysis]:
|
| 210 |
+
"""Create a new AI analysis for a dispute"""
|
| 211 |
+
# Check if dispute exists
|
| 212 |
+
db_dispute = get_dispute(db, dispute_id)
|
| 213 |
+
if not db_dispute:
|
| 214 |
+
return None
|
| 215 |
+
|
| 216 |
+
# Check if analysis already exists
|
| 217 |
+
existing_analysis = (
|
| 218 |
+
db.query(DisputeAnalysis)
|
| 219 |
+
.filter(DisputeAnalysis.dispute_id == dispute_id)
|
| 220 |
+
.first()
|
| 221 |
+
)
|
| 222 |
+
|
| 223 |
+
if existing_analysis:
|
| 224 |
+
# Update existing analysis
|
| 225 |
+
existing_analysis.priority = analysis.priority
|
| 226 |
+
existing_analysis.priority_reason = analysis.priority_reason
|
| 227 |
+
existing_analysis.insights = analysis.insights
|
| 228 |
+
existing_analysis.possible_reasons = str(analysis.possible_reasons)
|
| 229 |
+
existing_analysis.probable_solutions = str(analysis.probable_solutions)
|
| 230 |
+
existing_analysis.followup_questions = str(analysis.followup_questions)
|
| 231 |
+
existing_analysis.updated_at = datetime.utcnow()
|
| 232 |
+
|
| 233 |
+
db.commit()
|
| 234 |
+
db.refresh(existing_analysis)
|
| 235 |
+
return existing_analysis
|
| 236 |
+
else:
|
| 237 |
+
# Create new analysis
|
| 238 |
+
db_analysis = DisputeAnalysis(
|
| 239 |
+
id=str(uuid.uuid4()),
|
| 240 |
+
dispute_id=dispute_id,
|
| 241 |
+
priority=analysis.priority,
|
| 242 |
+
priority_reason=analysis.priority_reason,
|
| 243 |
+
insights=analysis.insights,
|
| 244 |
+
possible_reasons=str(analysis.possible_reasons),
|
| 245 |
+
probable_solutions=str(analysis.probable_solutions),
|
| 246 |
+
followup_questions=str(analysis.followup_questions),
|
| 247 |
+
)
|
| 248 |
+
|
| 249 |
+
# Update dispute priority
|
| 250 |
+
db_dispute.priority = analysis.priority
|
| 251 |
+
db_dispute.updated_at = datetime.utcnow()
|
| 252 |
+
|
| 253 |
+
db.add(db_analysis)
|
| 254 |
+
db.commit()
|
| 255 |
+
db.refresh(db_analysis)
|
| 256 |
+
return db_analysis
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
def get_dispute_analysis(db: Session, dispute_id: str) -> Optional[DisputeAnalysis]:
|
| 260 |
+
"""Get AI analysis for a dispute"""
|
| 261 |
+
return (
|
| 262 |
+
db.query(DisputeAnalysis)
|
| 263 |
+
.filter(DisputeAnalysis.dispute_id == dispute_id)
|
| 264 |
+
.first()
|
| 265 |
+
)
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
# Dashboard metrics
|
| 269 |
+
def get_dashboard_metrics(db: Session) -> Dict[str, Any]:
|
| 270 |
+
"""Get metrics for the dashboard"""
|
| 271 |
+
# Total disputes
|
| 272 |
+
total_disputes = db.query(Dispute).count()
|
| 273 |
+
|
| 274 |
+
# High priority disputes (4-5)
|
| 275 |
+
high_priority_count = db.query(Dispute).filter(Dispute.priority.in_([4, 5])).count()
|
| 276 |
+
|
| 277 |
+
# Pending disputes
|
| 278 |
+
pending_count = (
|
| 279 |
+
db.query(Dispute)
|
| 280 |
+
.filter(
|
| 281 |
+
Dispute.status.in_(
|
| 282 |
+
[
|
| 283 |
+
DisputeStatus.NEW,
|
| 284 |
+
DisputeStatus.UNDER_REVIEW,
|
| 285 |
+
DisputeStatus.INFO_REQUESTED,
|
| 286 |
+
]
|
| 287 |
+
)
|
| 288 |
+
)
|
| 289 |
+
.count()
|
| 290 |
+
)
|
| 291 |
+
|
| 292 |
+
# Resolved today
|
| 293 |
+
today_start = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0)
|
| 294 |
+
resolved_today = (
|
| 295 |
+
db.query(Dispute).filter(Dispute.resolved_at >= today_start).count()
|
| 296 |
+
)
|
| 297 |
+
|
| 298 |
+
# Disputes by category
|
| 299 |
+
category_query = (
|
| 300 |
+
db.query(Dispute.category, db.func.count(Dispute.id).label("count"))
|
| 301 |
+
.group_by(Dispute.category)
|
| 302 |
+
.all()
|
| 303 |
+
)
|
| 304 |
+
disputes_by_category = {category: count for category, count in category_query}
|
| 305 |
+
|
| 306 |
+
# Disputes by status
|
| 307 |
+
status_query = (
|
| 308 |
+
db.query(Dispute.status, db.func.count(Dispute.id).label("count"))
|
| 309 |
+
.group_by(Dispute.status)
|
| 310 |
+
.all()
|
| 311 |
+
)
|
| 312 |
+
disputes_by_status = {status.name: count for status, count in status_query}
|
| 313 |
+
|
| 314 |
+
# Disputes by priority
|
| 315 |
+
priority_query = (
|
| 316 |
+
db.query(Dispute.priority, db.func.count(Dispute.id).label("count"))
|
| 317 |
+
.filter(Dispute.priority.isnot(None))
|
| 318 |
+
.group_by(Dispute.priority)
|
| 319 |
+
.all()
|
| 320 |
+
)
|
| 321 |
+
disputes_by_priority = {priority: count for priority, count in priority_query}
|
| 322 |
+
|
| 323 |
+
return {
|
| 324 |
+
"total_disputes": total_disputes,
|
| 325 |
+
"high_priority_count": high_priority_count,
|
| 326 |
+
"pending_count": pending_count,
|
| 327 |
+
"resolved_today": resolved_today,
|
| 328 |
+
"disputes_by_category": disputes_by_category,
|
| 329 |
+
"disputes_by_status": disputes_by_status,
|
| 330 |
+
"disputes_by_priority": disputes_by_priority,
|
| 331 |
+
}
|
app/api/services/priority_service.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app/api/services/priority_service.py
|
| 2 |
+
from typing import Dict, Any
|
| 3 |
+
from app.ai.dispute_analyzer import DisputeAnalyzer
|
| 4 |
+
from app.core.ai_config import ai_settings
|
| 5 |
+
|
| 6 |
+
class PriorityService:
|
| 7 |
+
def __init__(self):
|
| 8 |
+
self.analyzer = DisputeAnalyzer()
|
| 9 |
+
|
| 10 |
+
def calculate_priority(self, dispute_data: Dict[str, Any]) -> Dict[str, Any]:
|
| 11 |
+
"""Calculate final priority considering both AI and rule-based factors"""
|
| 12 |
+
analysis = self.analyzer.analyze_dispute(dispute_data)
|
| 13 |
+
|
| 14 |
+
# Combine AI priority with risk-based adjustment
|
| 15 |
+
ai_priority = analysis["priority"]
|
| 16 |
+
risk_priority = self._risk_based_priority(analysis["risk_score"])
|
| 17 |
+
|
| 18 |
+
# Take the higher of the two priorities
|
| 19 |
+
final_priority = max(ai_priority, risk_priority)
|
| 20 |
+
|
| 21 |
+
return {
|
| 22 |
+
"final_priority": final_priority,
|
| 23 |
+
"ai_priority": ai_priority,
|
| 24 |
+
"risk_priority": risk_priority,
|
| 25 |
+
"risk_score": analysis["risk_score"],
|
| 26 |
+
"priority_reason": analysis["priority_reason"],
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
def _risk_based_priority(self, risk_score: float) -> int:
|
| 30 |
+
"""Convert risk score to priority level"""
|
| 31 |
+
if risk_score >= 80:
|
| 32 |
+
return 5
|
| 33 |
+
elif risk_score >= 65:
|
| 34 |
+
return 4
|
| 35 |
+
elif risk_score >= 50:
|
| 36 |
+
return 3
|
| 37 |
+
elif risk_score >= 35:
|
| 38 |
+
return 2
|
| 39 |
+
else:
|
| 40 |
+
return 1
|
app/api/services/recommendation_service.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app/api/services/recommendation_service.py
|
| 2 |
+
from typing import Dict, Any, List
|
| 3 |
+
from app.ai.dispute_analyzer import DisputeAnalyzer
|
| 4 |
+
|
| 5 |
+
class RecommendationService:
|
| 6 |
+
def __init__(self):
|
| 7 |
+
self.analyzer = DisputeAnalyzer()
|
| 8 |
+
|
| 9 |
+
def get_recommendations(self, dispute_data: Dict[str, Any]) -> Dict[str, Any]:
|
| 10 |
+
"""Get comprehensive recommendations for dispute resolution"""
|
| 11 |
+
analysis = self.analyzer.analyze_dispute(dispute_data)
|
| 12 |
+
|
| 13 |
+
return {
|
| 14 |
+
"priority": analysis["priority"],
|
| 15 |
+
"risk_score": analysis["risk_score"],
|
| 16 |
+
"recommended_actions": analysis["recommended_actions"],
|
| 17 |
+
"sla_target": analysis["sla_target"].isoformat(),
|
| 18 |
+
"followup_questions": analysis["followup_questions"],
|
| 19 |
+
"similar_cases": self._find_similar_cases(dispute_data)
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
def _find_similar_cases(self, dispute_data: Dict[str, Any]) -> List[Dict]:
|
| 23 |
+
"""Placeholder for similar case lookup"""
|
| 24 |
+
# TODO: Implement actual similar case search
|
| 25 |
+
return []
|
app/core/__init__.py
ADDED
|
File without changes
|
app/core/ai_config.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app/core/ai_config.py
|
| 2 |
+
from pydantic_settings import BaseSettings
|
| 3 |
+
|
| 4 |
+
class AISettings(BaseSettings):
|
| 5 |
+
# Use the same environment variable name
|
| 6 |
+
GOOGLE_API_KEY: str # No default value
|
| 7 |
+
|
| 8 |
+
# Rest of your existing AI config
|
| 9 |
+
GEMINI_MODEL: str = "gemini-2.0-flash"
|
| 10 |
+
TEMPERATURE: float = 0.2
|
| 11 |
+
MAX_TOKENS: int = 1024
|
| 12 |
+
MAX_RETRIES: int = 2
|
| 13 |
+
RETRY_DELAY: int = 4
|
| 14 |
+
|
| 15 |
+
class Config:
|
| 16 |
+
env_file = ".env"
|
| 17 |
+
extra = "ignore"
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
ai_settings = AISettings()
|
app/core/config.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app/core/config.py
|
| 2 |
+
from pydantic_settings import BaseSettings
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
class Settings(BaseSettings):
|
| 6 |
+
# Keep existing fields
|
| 7 |
+
API_V1_STR: str = "/api/v1"
|
| 8 |
+
PROJECT_NAME: str = "Banking Disputes Resolution System"
|
| 9 |
+
DEBUG: bool = False
|
| 10 |
+
DATABASE_URL: str = "sqlite:///./disputes.db"
|
| 11 |
+
|
| 12 |
+
# Add this to accept Google API key from environment
|
| 13 |
+
GOOGLE_API_KEY: str = None
|
| 14 |
+
|
| 15 |
+
class Config:
|
| 16 |
+
env_file = ".env"
|
| 17 |
+
extra = "ignore"
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
settings = Settings()
|
app/data/__init__.py
ADDED
|
File without changes
|
app/data/dispute_categories.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"categories": [
|
| 3 |
+
"Billing Error",
|
| 4 |
+
"Fraudulent Transaction",
|
| 5 |
+
"Item Not Received",
|
| 6 |
+
"Item Not As Described",
|
| 7 |
+
"Duplicate Charge",
|
| 8 |
+
"Cancelled Service",
|
| 9 |
+
"Refund Not Processed",
|
| 10 |
+
"Subscription Issue",
|
| 11 |
+
"ATM Issue",
|
| 12 |
+
"Other"
|
| 13 |
+
]
|
| 14 |
+
}
|
app/data/sample_disputes.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"disputes": [
|
| 3 |
+
{
|
| 4 |
+
"id": "D1001",
|
| 5 |
+
"customer_id": "C1001",
|
| 6 |
+
"transaction_id": "T12345",
|
| 7 |
+
"merchant_name": "Utility Co",
|
| 8 |
+
"amount": 120.00,
|
| 9 |
+
"category": "Billing Error",
|
| 10 |
+
"description": "Unexpected increase in monthly utility bill without prior notification.",
|
| 11 |
+
"status": "New",
|
| 12 |
+
"created_at": "2024-01-15T10:30:00Z"
|
| 13 |
+
},
|
| 14 |
+
{
|
| 15 |
+
"id": "D1002",
|
| 16 |
+
"customer_id": "C1002",
|
| 17 |
+
"transaction_id": "T67890",
|
| 18 |
+
"merchant_name": "Online Shop",
|
| 19 |
+
"amount": 89.99,
|
| 20 |
+
"category": "Item Not Received",
|
| 21 |
+
"description": "Ordered a headphone set 3 weeks ago but haven't received it yet. Tracking shows no movement for 2 weeks.",
|
| 22 |
+
"status": "New",
|
| 23 |
+
"created_at": "2024-01-18T14:15:00Z"
|
| 24 |
+
}
|
| 25 |
+
]
|
| 26 |
+
}
|
app/entrypoint.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import subprocess
|
| 2 |
+
import sys
|
| 3 |
+
import os
|
| 4 |
+
import threading
|
| 5 |
+
import time
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def run_fastapi():
|
| 9 |
+
print("Starting FastAPI backend...")
|
| 10 |
+
subprocess.run(["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"])
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def run_streamlit():
|
| 14 |
+
print("Starting Streamlit from frontend directory...")
|
| 15 |
+
# Change to the frontend directory first
|
| 16 |
+
os.chdir("app/frontend")
|
| 17 |
+
# Then run streamlit (note the path is now relative to the frontend directory)
|
| 18 |
+
subprocess.run(["streamlit", "run", "streamlit_app.py", "--server.port", "7860", "--server.address", "0.0.0.0"])
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
if __name__ == "__main__":
|
| 22 |
+
# Store the original directory to potentially return to it later if needed
|
| 23 |
+
original_dir = os.getcwd()
|
| 24 |
+
|
| 25 |
+
# Start FastAPI in a separate thread
|
| 26 |
+
api_thread = threading.Thread(target=run_fastapi)
|
| 27 |
+
api_thread.daemon = True
|
| 28 |
+
api_thread.start()
|
| 29 |
+
|
| 30 |
+
# Give FastAPI some time to start up
|
| 31 |
+
time.sleep(3)
|
| 32 |
+
|
| 33 |
+
# Start Streamlit in the main thread
|
| 34 |
+
run_streamlit()
|
app/frontend/__init__.py
ADDED
|
File without changes
|
app/frontend/components/__init__.py
ADDED
|
File without changes
|
app/frontend/components/api_popover.py
ADDED
|
File without changes
|
app/frontend/components/dispute_card.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app/frontend/components/dispute_card.py
|
| 2 |
+
import streamlit as st
|
| 3 |
+
from typing import Dict
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def dispute_card(dispute: Dict):
|
| 7 |
+
priority_colors = {
|
| 8 |
+
1: "#4a86e8",
|
| 9 |
+
2: "#5cb85c",
|
| 10 |
+
3: "#f0ad4e",
|
| 11 |
+
4: "#d9534f",
|
| 12 |
+
5: "#d9534f",
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
with st.container():
|
| 16 |
+
st.markdown(
|
| 17 |
+
f"""
|
| 18 |
+
<div style='
|
| 19 |
+
border-left: 5px solid {priority_colors.get(dispute.get("priority", 1), "#999")};
|
| 20 |
+
padding: 15px;
|
| 21 |
+
margin: 10px 0;
|
| 22 |
+
background: white;
|
| 23 |
+
border-radius: 5px;
|
| 24 |
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
| 25 |
+
'>
|
| 26 |
+
<div style='display: flex; justify-content: space-between;'>
|
| 27 |
+
<h3 style='margin: 0;'>#{dispute['id'][:6]}</h3>
|
| 28 |
+
<div style='
|
| 29 |
+
background: {priority_colors.get(dispute.get("priority", 1))};
|
| 30 |
+
color: white;
|
| 31 |
+
padding: 2px 8px;
|
| 32 |
+
border-radius: 12px;
|
| 33 |
+
font-size: 0.8em;
|
| 34 |
+
'>
|
| 35 |
+
{dispute.get('priority', 'N/A')}
|
| 36 |
+
</div>
|
| 37 |
+
</div>
|
| 38 |
+
<p style='margin: 8px 0; color: #666;'>{dispute['description'][:100]}...</p>
|
| 39 |
+
<div style='display: flex; justify-content: space-between; font-size: 0.9em;'>
|
| 40 |
+
<div>${dispute['amount']}</div>
|
| 41 |
+
<div>{dispute['status']}</div>
|
| 42 |
+
</div>
|
| 43 |
+
</div>
|
| 44 |
+
""",
|
| 45 |
+
unsafe_allow_html=True,
|
| 46 |
+
)
|
| 47 |
+
# st.button(
|
| 48 |
+
# "View Details",
|
| 49 |
+
# on_click=lambda id=dispute["id"]: st.query_params.update(
|
| 50 |
+
# {"page": "dispute_details", "id": id}
|
| 51 |
+
# ),
|
| 52 |
+
# )
|
app/frontend/components/followup_questions.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app/frontend/components/followup_questions.py
|
| 2 |
+
import streamlit as st
|
| 3 |
+
from typing import List, Dict
|
| 4 |
+
|
| 5 |
+
def display_followup_questions(questions: List[str], dispute_id: str):
|
| 6 |
+
"""Displays interactive follow-up questions with response handling"""
|
| 7 |
+
with st.container(border=True):
|
| 8 |
+
st.markdown("### Follow-up Questions")
|
| 9 |
+
|
| 10 |
+
if not questions:
|
| 11 |
+
st.info("No follow-up questions generated for this case")
|
| 12 |
+
return
|
| 13 |
+
|
| 14 |
+
for i, question in enumerate(questions):
|
| 15 |
+
with st.expander(f"Question #{i+1}: {question}", expanded=False):
|
| 16 |
+
response = st.text_area(
|
| 17 |
+
"Agent Response",
|
| 18 |
+
key=f"response_{dispute_id}_{i}",
|
| 19 |
+
placeholder="Enter your response here...",
|
| 20 |
+
height=100
|
| 21 |
+
)
|
| 22 |
+
col1, col2 = st.columns([3, 1])
|
| 23 |
+
with col1:
|
| 24 |
+
if st.button("Save Response", key=f"save_{dispute_id}_{i}"):
|
| 25 |
+
if response.strip():
|
| 26 |
+
# Implement API call to save response
|
| 27 |
+
st.success("Response saved successfully")
|
| 28 |
+
else:
|
| 29 |
+
st.warning("Please enter a response before saving")
|
| 30 |
+
with col2:
|
| 31 |
+
if st.button("Mark as Completed", key=f"complete_{dispute_id}_{i}"):
|
| 32 |
+
# Implement API call to mark question as resolved
|
| 33 |
+
st.success("Question marked as completed")
|
app/frontend/components/insights_panel.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
from typing import Dict, Any, List
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
def ai_insights_panel(analysis: Dict[str, Any], priority: int):
|
| 6 |
+
"""Displays AI-generated insights in a structured panel."""
|
| 7 |
+
|
| 8 |
+
with st.container():
|
| 9 |
+
|
| 10 |
+
# Priority and Risk
|
| 11 |
+
col1, col2 = st.columns(2)
|
| 12 |
+
with col1:
|
| 13 |
+
st.metric("Priority Level", f"{priority}/5")
|
| 14 |
+
with col2:
|
| 15 |
+
st.metric("Risk Score", f"{analysis.get('risk_score', 'N/A')}/10.0")
|
| 16 |
+
|
| 17 |
+
# Priority Reason
|
| 18 |
+
if analysis.get("priority_reason"):
|
| 19 |
+
with st.expander("Priority Reason", expanded=True):
|
| 20 |
+
st.markdown(analysis["priority_reason"])
|
| 21 |
+
|
| 22 |
+
# Key Insights
|
| 23 |
+
if analysis.get("insights"):
|
| 24 |
+
with st.expander("Key Insights", expanded=True):
|
| 25 |
+
# Use markdown instead of write to ensure full text is displayed
|
| 26 |
+
st.markdown("### AI Insights")
|
| 27 |
+
st.markdown(analysis["insights"])
|
| 28 |
+
|
| 29 |
+
# Follow-up Questions
|
| 30 |
+
if analysis.get("followup_questions"):
|
| 31 |
+
with st.expander("Follow-up Questions", expanded=False):
|
| 32 |
+
if isinstance(analysis["followup_questions"], str):
|
| 33 |
+
try:
|
| 34 |
+
# Attempt to parse as a list (in case it's a string representation)
|
| 35 |
+
questions = eval(analysis["followup_questions"])
|
| 36 |
+
if isinstance(questions, list):
|
| 37 |
+
for question in questions:
|
| 38 |
+
st.markdown(f"- {question}")
|
| 39 |
+
else:
|
| 40 |
+
st.markdown(analysis["followup_questions"]) # Display as is
|
| 41 |
+
except:
|
| 42 |
+
st.markdown(
|
| 43 |
+
analysis["followup_questions"]
|
| 44 |
+
) # Display as is if parsing fails
|
| 45 |
+
elif isinstance(analysis["followup_questions"], list):
|
| 46 |
+
for question in analysis["followup_questions"]:
|
| 47 |
+
st.markdown(f"- {question}")
|
| 48 |
+
else:
|
| 49 |
+
st.markdown(analysis["followup_questions"])
|
| 50 |
+
|
| 51 |
+
# Probable Solutions
|
| 52 |
+
if analysis.get("probable_solutions"):
|
| 53 |
+
with st.expander("Probable Solutions", expanded=False):
|
| 54 |
+
if isinstance(analysis["probable_solutions"], str):
|
| 55 |
+
try:
|
| 56 |
+
solutions = eval(analysis["probable_solutions"])
|
| 57 |
+
if isinstance(solutions, list):
|
| 58 |
+
for solution in solutions:
|
| 59 |
+
st.markdown(f"- {solution}")
|
| 60 |
+
else:
|
| 61 |
+
st.markdown(analysis["probable_solutions"])
|
| 62 |
+
except:
|
| 63 |
+
st.markdown(analysis["probable_solutions"])
|
| 64 |
+
elif isinstance(analysis["probable_solutions"], list):
|
| 65 |
+
for solution in analysis[
|
| 66 |
+
"probable_solutions"
|
| 67 |
+
]: # Use analysis["probable_solutions"] instead of solutions
|
| 68 |
+
st.markdown(f"- {solution}")
|
| 69 |
+
else:
|
| 70 |
+
st.markdown(analysis["probable_solutions"])
|
| 71 |
+
|
| 72 |
+
# Possible Reasons
|
| 73 |
+
if analysis.get("possible_reasons"):
|
| 74 |
+
with st.expander("Possible Reasons", expanded=False):
|
| 75 |
+
if isinstance(analysis["possible_reasons"], str):
|
| 76 |
+
try:
|
| 77 |
+
reasons = eval(analysis["possible_reasons"])
|
| 78 |
+
if isinstance(reasons, list):
|
| 79 |
+
for reason in reasons:
|
| 80 |
+
st.markdown(f"- {reason}")
|
| 81 |
+
else:
|
| 82 |
+
st.markdown(analysis["possible_reasons"])
|
| 83 |
+
except:
|
| 84 |
+
st.markdown(analysis["possible_reasons"])
|
| 85 |
+
|
| 86 |
+
elif isinstance(analysis["possible_reasons"], list):
|
| 87 |
+
for reason in analysis["possible_reasons"]:
|
| 88 |
+
st.markdown(f"- {reason}")
|
| 89 |
+
else:
|
| 90 |
+
st.markdown(analysis["possible_reasons"])
|
| 91 |
+
|
| 92 |
+
# Risk Factors
|
| 93 |
+
if analysis.get("risk_factors"):
|
| 94 |
+
with st.expander("Risk Factors", expanded=False):
|
| 95 |
+
if isinstance(analysis["risk_factors"], str):
|
| 96 |
+
try:
|
| 97 |
+
risk_factors = eval(analysis["risk_factors"])
|
| 98 |
+
if isinstance(risk_factors, list):
|
| 99 |
+
for risk_factor in risk_factors:
|
| 100 |
+
st.markdown(f"- {risk_factor}")
|
| 101 |
+
else:
|
| 102 |
+
st.markdown(analysis["risk_factors"])
|
| 103 |
+
except:
|
| 104 |
+
st.markdown(analysis["risk_factors"])
|
| 105 |
+
elif isinstance(analysis["risk_factors"], list):
|
| 106 |
+
for risk_factor in analysis["risk_factors"]:
|
| 107 |
+
st.markdown(f"- {risk_factor}")
|
| 108 |
+
else:
|
| 109 |
+
st.markdown(analysis["risk_factors"])
|
app/frontend/components/sidebar.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app/frontend/components/sidebar.py
|
| 2 |
+
import streamlit as st
|
| 3 |
+
from utils.api_client import DisputeAPIClient
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def sidebar():
|
| 7 |
+
with st.sidebar:
|
| 8 |
+
st.title("Dispute Resolution")
|
| 9 |
+
st.button(
|
| 10 |
+
"🏠 Dashboard",
|
| 11 |
+
on_click=lambda: st.query_params.update({"page": "dashboard"}),
|
| 12 |
+
)
|
| 13 |
+
st.button(
|
| 14 |
+
"➕ New Dispute",
|
| 15 |
+
on_click=lambda: st.query_params.update({"page": "new_dispute"}),
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
# Quick stats
|
| 19 |
+
st.divider()
|
| 20 |
+
st.markdown("### System Status")
|
| 21 |
+
try:
|
| 22 |
+
metrics = DisputeAPIClient.get_dashboard_metrics()
|
| 23 |
+
col1, col2 = st.columns(2)
|
| 24 |
+
with col1:
|
| 25 |
+
st.metric("Total Disputes", metrics.get("total_disputes", 0))
|
| 26 |
+
with col2:
|
| 27 |
+
st.metric("High Priority", metrics.get("high_priority_count", 0))
|
| 28 |
+
|
| 29 |
+
st.metric("Pending Review", metrics.get("pending_count", 0))
|
| 30 |
+
except Exception as e:
|
| 31 |
+
st.error(f"Error loading metrics: {str(e)}")
|
| 32 |
+
|
| 33 |
+
# Admin section
|
| 34 |
+
st.divider()
|
| 35 |
+
st.button(
|
| 36 |
+
"⚙️ Admin Panel", on_click=lambda: st.query_params.update({"page": "admin"})
|
| 37 |
+
)
|
app/frontend/pages/__init__.py
ADDED
|
File without changes
|
app/frontend/pages/admin.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app/frontend/pages/admin.py
|
| 2 |
+
import streamlit as st
|
| 3 |
+
from utils.api_client import DisputeAPIClient
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def display_admin():
|
| 7 |
+
"""Administration panel for system management"""
|
| 8 |
+
|
| 9 |
+
st.title("System Administration")
|
| 10 |
+
|
| 11 |
+
tab1, tab2, tab3 = st.tabs(["Metrics", "Configuration", "Users"])
|
| 12 |
+
|
| 13 |
+
with tab1:
|
| 14 |
+
st.header("System Metrics")
|
| 15 |
+
metrics = DisputeAPIClient.get_dashboard_metrics()
|
| 16 |
+
col1, col2 = st.columns(2)
|
| 17 |
+
with col1:
|
| 18 |
+
st.metric("Total Disputes", metrics.get("total_disputes", 0))
|
| 19 |
+
st.metric("Average Resolution Time", "24h")
|
| 20 |
+
with col2:
|
| 21 |
+
st.metric("API Health", "✅ Operational")
|
| 22 |
+
st.metric("Active Users", 15)
|
| 23 |
+
|
| 24 |
+
with tab2:
|
| 25 |
+
st.header("System Configuration")
|
| 26 |
+
with st.form("config_form"):
|
| 27 |
+
st.number_input("Default SLA Days", value=7)
|
| 28 |
+
st.number_input("High Priority Threshold", value=4)
|
| 29 |
+
st.form_submit_button("Save Configuration")
|
| 30 |
+
|
| 31 |
+
with tab3:
|
| 32 |
+
st.header("User Management")
|
| 33 |
+
# Placeholder table
|
| 34 |
+
st.dataframe(
|
| 35 |
+
{
|
| 36 |
+
"User": ["admin", "analyst1", "reviewer2"],
|
| 37 |
+
"Role": ["Admin", "Analyst", "Reviewer"],
|
| 38 |
+
"Last Login": ["2h ago", "1d ago", "3d ago"],
|
| 39 |
+
}
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
if __name__ == "__main__":
|
| 43 |
+
display_admin()
|
app/frontend/pages/api_docs.py
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
def display_api_docs():
|
| 5 |
+
st.title("API Documentation")
|
| 6 |
+
|
| 7 |
+
st.markdown(
|
| 8 |
+
"""
|
| 9 |
+
## REST API Reference
|
| 10 |
+
Explore our API endpoints for integration with the Banking Dispute Resolution System.
|
| 11 |
+
"""
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
with st.expander("📚 Customer Endpoints", expanded=True):
|
| 15 |
+
st.markdown(
|
| 16 |
+
"""
|
| 17 |
+
### 1. Create Customer
|
| 18 |
+
**POST** `/customers/`
|
| 19 |
+
|
| 20 |
+
**Input:**
|
| 21 |
+
```json
|
| 22 |
+
{
|
| 23 |
+
"name": "John Doe",
|
| 24 |
+
"email": "[email protected]",
|
| 25 |
+
"account_type": "Individual"
|
| 26 |
+
}
|
| 27 |
+
```
|
| 28 |
+
|
| 29 |
+
**Response:** (Status Code: 201)
|
| 30 |
+
```json
|
| 31 |
+
{
|
| 32 |
+
"id": "550e8400-e29b-41d4-a716-446655440000",
|
| 33 |
+
"name": "John Doe",
|
| 34 |
+
"email": "[email protected]",
|
| 35 |
+
"account_type": "Individual",
|
| 36 |
+
"dispute_count": 0,
|
| 37 |
+
"created_at": "2025-03-22T14:30:45.123456"
|
| 38 |
+
}
|
| 39 |
+
```
|
| 40 |
+
|
| 41 |
+
### 2. Get All Customers
|
| 42 |
+
**GET** `/customers/`
|
| 43 |
+
|
| 44 |
+
**Query Parameters:**
|
| 45 |
+
- `skip`: int (default: 0)
|
| 46 |
+
- `limit`: int (default: 100)
|
| 47 |
+
- `account_type`: string (optional)
|
| 48 |
+
|
| 49 |
+
**Response:** (Status Code: 200)
|
| 50 |
+
```json
|
| 51 |
+
[
|
| 52 |
+
{
|
| 53 |
+
"id": "550e8400-e29b-41d4-a716-446655440000",
|
| 54 |
+
"name": "John Doe",
|
| 55 |
+
"email": "[email protected]",
|
| 56 |
+
"account_type": "Individual",
|
| 57 |
+
"dispute_count": 2,
|
| 58 |
+
"created_at": "2025-03-22T14:30:45.123456"
|
| 59 |
+
},
|
| 60 |
+
...
|
| 61 |
+
]
|
| 62 |
+
```
|
| 63 |
+
|
| 64 |
+
### 3. Get Customer by ID
|
| 65 |
+
**GET** `/customers/{customer_id}`
|
| 66 |
+
|
| 67 |
+
**Response:** (Status Code: 200)
|
| 68 |
+
```json
|
| 69 |
+
{
|
| 70 |
+
"id": "550e8400-e29b-41d4-a716-446655440000",
|
| 71 |
+
"name": "John Doe",
|
| 72 |
+
"email": "[email protected]",
|
| 73 |
+
"account_type": "Individual",
|
| 74 |
+
"dispute_count": 2,
|
| 75 |
+
"created_at": "2025-03-22T14:30:45.123456"
|
| 76 |
+
}
|
| 77 |
+
```
|
| 78 |
+
|
| 79 |
+
### 4. Get Customer's Disputes
|
| 80 |
+
**GET** `/customers/{customer_id}/disputes`
|
| 81 |
+
|
| 82 |
+
**Query Parameters:**
|
| 83 |
+
- `skip`: int (default: 0)
|
| 84 |
+
- `limit`: int (default: 100)
|
| 85 |
+
|
| 86 |
+
**Response:** (Status Code: 200)
|
| 87 |
+
```json
|
| 88 |
+
[
|
| 89 |
+
{
|
| 90 |
+
"id": "550e8400-e29b-41d4-a716-446655440010",
|
| 91 |
+
"customer_id": "550e8400-e29b-41d4-a716-446655440000",
|
| 92 |
+
"transaction_id": "T12345",
|
| 93 |
+
"merchant_name": "Example Store",
|
| 94 |
+
"amount": 99.99,
|
| 95 |
+
"description": "Unauthorized charge",
|
| 96 |
+
"category": "Unauthorized",
|
| 97 |
+
"status": "Open",
|
| 98 |
+
"priority": 3,
|
| 99 |
+
"created_at": "2025-03-22T15:30:45.123456",
|
| 100 |
+
"resolved_at": null
|
| 101 |
+
},
|
| 102 |
+
...
|
| 103 |
+
]
|
| 104 |
+
```
|
| 105 |
+
|
| 106 |
+
### 5. Update Customer
|
| 107 |
+
**PUT** `/customers/{customer_id}`
|
| 108 |
+
|
| 109 |
+
**Input:**
|
| 110 |
+
```json
|
| 111 |
+
{
|
| 112 |
+
"name": "John Doe Updated",
|
| 113 |
+
"email": "[email protected]",
|
| 114 |
+
"account_type": "Business"
|
| 115 |
+
}
|
| 116 |
+
```
|
| 117 |
+
|
| 118 |
+
**Response:** (Status Code: 200)
|
| 119 |
+
```json
|
| 120 |
+
{
|
| 121 |
+
"id": "550e8400-e29b-41d4-a716-446655440000",
|
| 122 |
+
"name": "John Doe Updated",
|
| 123 |
+
"email": "[email protected]",
|
| 124 |
+
"account_type": "Business",
|
| 125 |
+
"dispute_count": 2,
|
| 126 |
+
"created_at": "2025-03-22T14:30:45.123456"
|
| 127 |
+
}
|
| 128 |
+
```
|
| 129 |
+
|
| 130 |
+
### 6. Delete Customer
|
| 131 |
+
**DELETE** `/customers/{customer_id}`
|
| 132 |
+
|
| 133 |
+
**Response:** (Status Code: 200)
|
| 134 |
+
```json
|
| 135 |
+
{
|
| 136 |
+
"message": "Customer deleted successfully"
|
| 137 |
+
}
|
| 138 |
+
```
|
| 139 |
+
"""
|
| 140 |
+
)
|
| 141 |
+
|
| 142 |
+
with st.expander("⚖️ Dispute Endpoints", expanded=True):
|
| 143 |
+
st.markdown(
|
| 144 |
+
"""
|
| 145 |
+
### 1. Create Dispute
|
| 146 |
+
**POST** `/disputes/`
|
| 147 |
+
|
| 148 |
+
**Input:**
|
| 149 |
+
```json
|
| 150 |
+
{
|
| 151 |
+
"customer_id": "550e8400-e29b-41d4-a716-446655440000",
|
| 152 |
+
"transaction_id": "T12345",
|
| 153 |
+
"merchant_name": "Example Store",
|
| 154 |
+
"amount": 99.99,
|
| 155 |
+
"description": "Unauthorized charge",
|
| 156 |
+
"category": "Unauthorized"
|
| 157 |
+
}
|
| 158 |
+
```
|
| 159 |
+
|
| 160 |
+
**Response:** (Status Code: 201)
|
| 161 |
+
```json
|
| 162 |
+
{
|
| 163 |
+
"id": "550e8400-e29b-41d4-a716-446655440010",
|
| 164 |
+
"customer_id": "550e8400-e29b-41d4-a716-446655440000",
|
| 165 |
+
"transaction_id": "T12345",
|
| 166 |
+
"merchant_name": "Example Store",
|
| 167 |
+
"amount": 99.99,
|
| 168 |
+
"description": "Unauthorized charge",
|
| 169 |
+
"category": "Unauthorized",
|
| 170 |
+
"status": "Open",
|
| 171 |
+
"priority": null,
|
| 172 |
+
"created_at": "2025-03-22T15:30:45.123456",
|
| 173 |
+
"resolved_at": null
|
| 174 |
+
}
|
| 175 |
+
```
|
| 176 |
+
|
| 177 |
+
### 2. Get All Disputes
|
| 178 |
+
**GET** `/disputes/`
|
| 179 |
+
|
| 180 |
+
**Query Parameters:**
|
| 181 |
+
- `skip`: int (default: 0)
|
| 182 |
+
- `limit`: int (default: 100)
|
| 183 |
+
- `status`: string (optional)
|
| 184 |
+
- `priority_sort`: bool (default: true)
|
| 185 |
+
|
| 186 |
+
**Response:** (Status Code: 200)
|
| 187 |
+
```json
|
| 188 |
+
[
|
| 189 |
+
{
|
| 190 |
+
"id": "550e8400-e29b-41d4-a716-446655440010",
|
| 191 |
+
"customer_id": "550e8400-e29b-41d4-a716-446655440000",
|
| 192 |
+
"transaction_id": "T12345",
|
| 193 |
+
"merchant_name": "Example Store",
|
| 194 |
+
"amount": 99.99,
|
| 195 |
+
"description": "Unauthorized charge",
|
| 196 |
+
"category": "Unauthorized",
|
| 197 |
+
"status": "Open",
|
| 198 |
+
"priority": 3,
|
| 199 |
+
"created_at": "2025-03-22T15:30:45.123456",
|
| 200 |
+
"resolved_at": null
|
| 201 |
+
},
|
| 202 |
+
...
|
| 203 |
+
]
|
| 204 |
+
```
|
| 205 |
+
|
| 206 |
+
### 3. Get Dispute by ID
|
| 207 |
+
**GET** `/disputes/{dispute_id}`
|
| 208 |
+
|
| 209 |
+
**Response:** (Status Code: 200)
|
| 210 |
+
```json
|
| 211 |
+
{
|
| 212 |
+
"id": "550e8400-e29b-41d4-a716-446655440010",
|
| 213 |
+
"customer_id": "550e8400-e29b-41d4-a716-446655440000",
|
| 214 |
+
"transaction_id": "T12345",
|
| 215 |
+
"merchant_name": "Example Store",
|
| 216 |
+
"amount": 99.99,
|
| 217 |
+
"description": "Unauthorized charge",
|
| 218 |
+
"category": "Unauthorized",
|
| 219 |
+
"status": "Open",
|
| 220 |
+
"priority": 3,
|
| 221 |
+
"created_at": "2025-03-22T15:30:45.123456",
|
| 222 |
+
"resolved_at": null,
|
| 223 |
+
"customer": {
|
| 224 |
+
"id": "550e8400-e29b-41d4-a716-446655440000",
|
| 225 |
+
"name": "John Doe",
|
| 226 |
+
"email": "[email protected]",
|
| 227 |
+
"account_type": "Individual",
|
| 228 |
+
"dispute_count": 2,
|
| 229 |
+
"created_at": "2025-03-22T14:30:45.123456"
|
| 230 |
+
}
|
| 231 |
+
}
|
| 232 |
+
```
|
| 233 |
+
|
| 234 |
+
### 4. Update Dispute
|
| 235 |
+
**PUT** `/disputes/{dispute_id}`
|
| 236 |
+
|
| 237 |
+
**Input:**
|
| 238 |
+
```json
|
| 239 |
+
{
|
| 240 |
+
"status": "Under Review",
|
| 241 |
+
"priority": 4,
|
| 242 |
+
"description": "Updated description of the dispute"
|
| 243 |
+
}
|
| 244 |
+
```
|
| 245 |
+
|
| 246 |
+
**Response:** (Status Code: 200)
|
| 247 |
+
```json
|
| 248 |
+
{
|
| 249 |
+
"id": "550e8400-e29b-41d4-a716-446655440010",
|
| 250 |
+
"customer_id": "550e8400-e29b-41d4-a716-446655440000",
|
| 251 |
+
"transaction_id": "T12345",
|
| 252 |
+
"merchant_name": "Example Store",
|
| 253 |
+
"amount": 99.99,
|
| 254 |
+
"description": "Updated description of the dispute",
|
| 255 |
+
"category": "Unauthorized",
|
| 256 |
+
"status": "Under Review",
|
| 257 |
+
"priority": 4,
|
| 258 |
+
"created_at": "2025-03-22T15:30:45.123456",
|
| 259 |
+
"resolved_at": null
|
| 260 |
+
}
|
| 261 |
+
```
|
| 262 |
+
|
| 263 |
+
### 5. Delete Dispute
|
| 264 |
+
**DELETE** `/disputes/{dispute_id}`
|
| 265 |
+
|
| 266 |
+
**Response:** (Status Code: 200)
|
| 267 |
+
```json
|
| 268 |
+
{
|
| 269 |
+
"message": "Dispute deleted successfully"
|
| 270 |
+
}
|
| 271 |
+
```
|
| 272 |
+
|
| 273 |
+
### 6. Analyze Dispute
|
| 274 |
+
**POST** `/disputes/{dispute_id}/analyze`
|
| 275 |
+
|
| 276 |
+
**Response:** (Status Code: 201)
|
| 277 |
+
```json
|
| 278 |
+
{
|
| 279 |
+
"dispute_id": "550e8400-e29b-41d4-a716-446655440010",
|
| 280 |
+
"analysis": {
|
| 281 |
+
"priority": 4,
|
| 282 |
+
"priority_reason": "High value transaction with a new merchant",
|
| 283 |
+
"insights": "This appears to be a legitimate dispute based on transaction history.",
|
| 284 |
+
"followup_questions": ["When did you first notice the charge?", "Have you contacted the merchant?"],
|
| 285 |
+
"probable_solutions": ["Issue chargeback to merchant", "Request more information from customer"],
|
| 286 |
+
"possible_reasons": ["Fraudulent merchant", "Unauthorized use of card"],
|
| 287 |
+
"risk_score": 7.5,
|
| 288 |
+
"risk_factors": ["High transaction amount", "First transaction with merchant"]
|
| 289 |
+
}
|
| 290 |
+
}
|
| 291 |
+
```
|
| 292 |
+
"""
|
| 293 |
+
)
|
| 294 |
+
|
| 295 |
+
with st.expander("🔍 Dispute Insights Endpoints", expanded=True):
|
| 296 |
+
st.markdown(
|
| 297 |
+
"""
|
| 298 |
+
### 1. Create Dispute Insights
|
| 299 |
+
**POST** `/disputes/{dispute_id}/insights`
|
| 300 |
+
|
| 301 |
+
**Input:**
|
| 302 |
+
```json
|
| 303 |
+
{
|
| 304 |
+
"insights": "Detailed analysis of the dispute situation",
|
| 305 |
+
"followup_questions": ["Question 1?", "Question 2?"],
|
| 306 |
+
"probable_solutions": ["Solution 1", "Solution 2"],
|
| 307 |
+
"possible_reasons": ["Reason 1", "Reason 2"],
|
| 308 |
+
"risk_score": 6.5,
|
| 309 |
+
"risk_factors": ["Factor 1", "Factor 2"],
|
| 310 |
+
"priority_level": 4,
|
| 311 |
+
"priority_reason": "Explanation for the priority level"
|
| 312 |
+
}
|
| 313 |
+
```
|
| 314 |
+
|
| 315 |
+
**Response:** (Status Code: 201)
|
| 316 |
+
```json
|
| 317 |
+
{
|
| 318 |
+
"id": "550e8400-e29b-41d4-a716-446655440020",
|
| 319 |
+
"dispute_id": "550e8400-e29b-41d4-a716-446655440010",
|
| 320 |
+
"insights": "Detailed analysis of the dispute situation",
|
| 321 |
+
"followup_questions": ["Question 1?", "Question 2?"],
|
| 322 |
+
"probable_solutions": ["Solution 1", "Solution 2"],
|
| 323 |
+
"possible_reasons": ["Reason 1", "Reason 2"],
|
| 324 |
+
"risk_score": 6.5,
|
| 325 |
+
"risk_factors": ["Factor 1", "Factor 2"],
|
| 326 |
+
"priority_level": 4,
|
| 327 |
+
"priority_reason": "Explanation for the priority level",
|
| 328 |
+
"created_at": "2025-03-22T16:30:45.123456",
|
| 329 |
+
"updated_at": null
|
| 330 |
+
}
|
| 331 |
+
```
|
| 332 |
+
|
| 333 |
+
### 2. Get Dispute Insights
|
| 334 |
+
**GET** `/disputes/{dispute_id}/insights`
|
| 335 |
+
|
| 336 |
+
**Response:** (Status Code: 200)
|
| 337 |
+
```json
|
| 338 |
+
{
|
| 339 |
+
"id": "550e8400-e29b-41d4-a716-446655440020",
|
| 340 |
+
"dispute_id": "550e8400-e29b-41d4-a716-446655440010",
|
| 341 |
+
"insights": "Detailed analysis of the dispute situation",
|
| 342 |
+
"followup_questions": ["Question 1?", "Question 2?"],
|
| 343 |
+
"probable_solutions": ["Solution 1", "Solution 2"],
|
| 344 |
+
"possible_reasons": ["Reason 1", "Reason 2"],
|
| 345 |
+
"risk_score": 6.5,
|
| 346 |
+
"risk_factors": ["Factor 1", "Factor 2"],
|
| 347 |
+
"priority_level": 4,
|
| 348 |
+
"priority_reason": "Explanation for the priority level",
|
| 349 |
+
"created_at": "2025-03-22T16:30:45.123456",
|
| 350 |
+
"updated_at": null
|
| 351 |
+
}
|
| 352 |
+
```
|
| 353 |
+
|
| 354 |
+
### 3. Update Dispute Insights
|
| 355 |
+
**PUT** `/disputes/{dispute_id}/insights`
|
| 356 |
+
|
| 357 |
+
**Input:**
|
| 358 |
+
```json
|
| 359 |
+
{
|
| 360 |
+
"insights": "Updated analysis of the dispute situation",
|
| 361 |
+
"followup_questions": ["Updated question 1?", "Updated question 2?"],
|
| 362 |
+
"probable_solutions": ["Updated solution 1", "Updated solution 2"],
|
| 363 |
+
"possible_reasons": ["Updated reason 1", "Updated reason 2"],
|
| 364 |
+
"risk_score": 8.0,
|
| 365 |
+
"risk_factors": ["Updated factor 1", "Updated factor 2"],
|
| 366 |
+
"priority_level": 5,
|
| 367 |
+
"priority_reason": "Updated explanation for the priority level"
|
| 368 |
+
}
|
| 369 |
+
```
|
| 370 |
+
|
| 371 |
+
**Response:** (Status Code: 200)
|
| 372 |
+
```json
|
| 373 |
+
{
|
| 374 |
+
"id": "550e8400-e29b-41d4-a716-446655440020",
|
| 375 |
+
"dispute_id": "550e8400-e29b-41d4-a716-446655440010",
|
| 376 |
+
"insights": "Updated analysis of the dispute situation",
|
| 377 |
+
"followup_questions": ["Updated question 1?", "Updated question 2?"],
|
| 378 |
+
"probable_solutions": ["Updated solution 1", "Updated solution 2"],
|
| 379 |
+
"possible_reasons": ["Updated reason 1", "Updated reason 2"],
|
| 380 |
+
"risk_score": 8.0,
|
| 381 |
+
"risk_factors": ["Updated factor 1", "Updated factor 2"],
|
| 382 |
+
"priority_level": 5,
|
| 383 |
+
"priority_reason": "Updated explanation for the priority level",
|
| 384 |
+
"created_at": "2025-03-22T16:30:45.123456",
|
| 385 |
+
"updated_at": "2025-03-22T17:45:30.987654"
|
| 386 |
+
}
|
| 387 |
+
```
|
| 388 |
+
"""
|
| 389 |
+
)
|
| 390 |
+
|
| 391 |
+
st.markdown(
|
| 392 |
+
"""
|
| 393 |
+
## Note
|
| 394 |
+
No authentication is required for these API endpoints.
|
| 395 |
+
"""
|
| 396 |
+
)
|
| 397 |
+
|
| 398 |
+
|
| 399 |
+
if __name__ == "__main__":
|
| 400 |
+
display_api_docs()
|
app/frontend/pages/customer_details.py
ADDED
|
@@ -0,0 +1,416 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app/frontend/pages/customer_details.py
|
| 2 |
+
import streamlit as st
|
| 3 |
+
from utils.api_client import DisputeAPIClient
|
| 4 |
+
from components.dispute_card import dispute_card
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def display_customer_details():
|
| 8 |
+
"""Display details for a specific customer"""
|
| 9 |
+
|
| 10 |
+
# Get all customer details
|
| 11 |
+
all_customers = DisputeAPIClient.get_customers()
|
| 12 |
+
|
| 13 |
+
# Get customer ID from query params
|
| 14 |
+
customer_id = st.query_params.get("id")
|
| 15 |
+
if customer := DisputeAPIClient.get_customer(customer_id):
|
| 16 |
+
st.title(f"Customer Profile: {customer.get('name', 'Unknown')}")
|
| 17 |
+
|
| 18 |
+
# Load customer data
|
| 19 |
+
customer = DisputeAPIClient.get_customer(customer_id)
|
| 20 |
+
if not customer:
|
| 21 |
+
st.error("Customer not found")
|
| 22 |
+
st.button(
|
| 23 |
+
"Return to Dashboard",
|
| 24 |
+
on_click=lambda: st.query_params.update({"page": "dashboard"}),
|
| 25 |
+
)
|
| 26 |
+
return
|
| 27 |
+
|
| 28 |
+
# Header Section with return button
|
| 29 |
+
col1, col2, col3 = st.columns([1, 3, 1])
|
| 30 |
+
with col1:
|
| 31 |
+
st.button(
|
| 32 |
+
"← Back", on_click=lambda: st.query_params.update({"page": "dashboard"})
|
| 33 |
+
)
|
| 34 |
+
with col2:
|
| 35 |
+
st.title(f"Customer Profile: {customer.get('name', 'Unknown')}")
|
| 36 |
+
with col3:
|
| 37 |
+
st.write("")
|
| 38 |
+
|
| 39 |
+
# Customer Information
|
| 40 |
+
col1, col2 = st.columns(2)
|
| 41 |
+
with col1:
|
| 42 |
+
with st.container(border=True):
|
| 43 |
+
st.markdown("### Profile Information")
|
| 44 |
+
st.markdown(f"**Name:** {customer.get('name', 'Unknown')}")
|
| 45 |
+
st.markdown(f"**Email:** {customer.get('email', 'Unknown')}")
|
| 46 |
+
st.markdown(f"**Account Type:** {customer.get('account_type', 'Unknown')}")
|
| 47 |
+
st.markdown(f"**Member Since:** {customer.get('created_at', 'Unknown')}")
|
| 48 |
+
|
| 49 |
+
with col2:
|
| 50 |
+
with st.container(border=True):
|
| 51 |
+
st.markdown("### Disputes Summary")
|
| 52 |
+
st.markdown(f"**Total Disputes:** {customer.get('dispute_count', 0)}")
|
| 53 |
+
|
| 54 |
+
# Create button to file new dispute for this customer
|
| 55 |
+
st.button(
|
| 56 |
+
"➕ File New Dispute for This Customer",
|
| 57 |
+
key="new_dispute_for_customer",
|
| 58 |
+
on_click=lambda: st.query_params.update(
|
| 59 |
+
{"page": "new_dispute", "customer_id": customer_id}
|
| 60 |
+
),
|
| 61 |
+
type="primary",
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
# Load customer disputes
|
| 65 |
+
st.markdown("### Customer Disputes")
|
| 66 |
+
disputes = DisputeAPIClient.get_customer_disputes(customer_id)
|
| 67 |
+
|
| 68 |
+
if disputes:
|
| 69 |
+
# Organize disputes in columns (2 per row)
|
| 70 |
+
disputes_per_row = 2
|
| 71 |
+
rows = [
|
| 72 |
+
disputes[i : i + disputes_per_row]
|
| 73 |
+
for i in range(0, len(disputes), disputes_per_row)
|
| 74 |
+
]
|
| 75 |
+
|
| 76 |
+
for row in rows:
|
| 77 |
+
cols = st.columns(disputes_per_row)
|
| 78 |
+
for i, dispute in enumerate(row):
|
| 79 |
+
with cols[i]:
|
| 80 |
+
with st.container(border=True):
|
| 81 |
+
dispute_card(dispute)
|
| 82 |
+
st.button(
|
| 83 |
+
"View Details",
|
| 84 |
+
key=f"view_{dispute['id']}",
|
| 85 |
+
on_click=lambda id=dispute["id"]: st.query_params.update(
|
| 86 |
+
{"page": "dispute_details", "id": id}
|
| 87 |
+
),
|
| 88 |
+
use_container_width=True,
|
| 89 |
+
)
|
| 90 |
+
else:
|
| 91 |
+
st.info("This customer has no disputes on record.")
|
| 92 |
+
|
| 93 |
+
# Customer Management Tab
|
| 94 |
+
with st.expander("Customer Management", expanded=False):
|
| 95 |
+
st.markdown("### Update Customer Information")
|
| 96 |
+
with st.form("update_customer_form"):
|
| 97 |
+
updated_name = st.text_input("Name", value=customer.get("name", ""))
|
| 98 |
+
updated_email = st.text_input("Email", value=customer.get("email", ""))
|
| 99 |
+
updated_account_type = st.selectbox(
|
| 100 |
+
"Account Type",
|
| 101 |
+
options=["Individual", "Business", "Premium", "VIP"],
|
| 102 |
+
index=["Individual", "Business", "Premium", "VIP"].index(
|
| 103 |
+
customer.get("account_type", "Individual")
|
| 104 |
+
),
|
| 105 |
+
)
|
| 106 |
+
|
| 107 |
+
if st.form_submit_button("Update Customer"):
|
| 108 |
+
update_data = {
|
| 109 |
+
"name": updated_name,
|
| 110 |
+
"email": updated_email,
|
| 111 |
+
"account_type": updated_account_type,
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
if DisputeAPIClient.update_customer(customer_id, update_data):
|
| 115 |
+
st.session_state.notifications.append(
|
| 116 |
+
{"type": "success", "message": "Customer updated successfully!"}
|
| 117 |
+
)
|
| 118 |
+
st.rerun()
|
| 119 |
+
else:
|
| 120 |
+
st.error("Failed to update customer information")
|
| 121 |
+
|
| 122 |
+
# Dangerous Zone
|
| 123 |
+
st.markdown("### Dangerous Zone")
|
| 124 |
+
st.warning("These actions are permanent and cannot be undone!")
|
| 125 |
+
|
| 126 |
+
col1, col2 = st.columns(2)
|
| 127 |
+
with col1:
|
| 128 |
+
with st.form("delete_customer_form"):
|
| 129 |
+
st.markdown("**Delete Customer Account**")
|
| 130 |
+
st.markdown(
|
| 131 |
+
"This will remove all customer information and associated disputes"
|
| 132 |
+
)
|
| 133 |
+
confirm_delete = st.checkbox("I understand this cannot be undone")
|
| 134 |
+
|
| 135 |
+
if st.form_submit_button(
|
| 136 |
+
"🚨 Delete Customer", use_container_width=True
|
| 137 |
+
):
|
| 138 |
+
if confirm_delete:
|
| 139 |
+
if DisputeAPIClient.delete_customer(customer_id):
|
| 140 |
+
st.session_state.notifications.append(
|
| 141 |
+
{
|
| 142 |
+
"type": "success",
|
| 143 |
+
"message": "Customer deleted successfully!",
|
| 144 |
+
}
|
| 145 |
+
)
|
| 146 |
+
st.query_params.update({"page": "dashboard"})
|
| 147 |
+
st.rerun()
|
| 148 |
+
else:
|
| 149 |
+
st.error("Failed to delete customer")
|
| 150 |
+
else:
|
| 151 |
+
st.error("Please confirm deletion")
|
| 152 |
+
|
| 153 |
+
with col2:
|
| 154 |
+
with st.form("merge_customer_form"):
|
| 155 |
+
st.markdown("**Merge Customer Accounts**")
|
| 156 |
+
target_customer = st.selectbox(
|
| 157 |
+
"Select target customer",
|
| 158 |
+
options=[
|
| 159 |
+
c["id"]
|
| 160 |
+
for c in DisputeAPIClient.get_customers()
|
| 161 |
+
if c["id"] != customer_id
|
| 162 |
+
],
|
| 163 |
+
)
|
| 164 |
+
confirm_merge = st.checkbox("Confirm account merge")
|
| 165 |
+
|
| 166 |
+
if st.form_submit_button("🔀 Merge Accounts", use_container_width=True):
|
| 167 |
+
if confirm_merge:
|
| 168 |
+
st.info("Account merging functionality coming soon!")
|
| 169 |
+
else:
|
| 170 |
+
st.error("Please confirm merge")
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
def display_customer_details_page():
|
| 175 |
+
"""Display details for a specific customer and a list of all customers"""
|
| 176 |
+
|
| 177 |
+
# Get all customer details
|
| 178 |
+
all_customers = DisputeAPIClient.get_customers()
|
| 179 |
+
|
| 180 |
+
# Initialize session state to store selected customer if not already set
|
| 181 |
+
if 'selected_customer_id' not in st.session_state:
|
| 182 |
+
st.session_state.selected_customer_id = None
|
| 183 |
+
|
| 184 |
+
# Header Section with return button
|
| 185 |
+
col1, col2, col3 = st.columns([1, 3, 1])
|
| 186 |
+
with col1:
|
| 187 |
+
st.button(
|
| 188 |
+
"← Back", on_click=lambda: st.query_params.update({"page": "dashboard"})
|
| 189 |
+
)
|
| 190 |
+
with col2:
|
| 191 |
+
st.title("Customer Details")
|
| 192 |
+
with col3:
|
| 193 |
+
st.write("")
|
| 194 |
+
|
| 195 |
+
# Display all customers section
|
| 196 |
+
with st.container(border=True):
|
| 197 |
+
st.markdown("### Customer List")
|
| 198 |
+
|
| 199 |
+
# Create a searchable filter
|
| 200 |
+
search_term = st.text_input("Search customers by name or email", "")
|
| 201 |
+
|
| 202 |
+
# Filter customers based on search term if provided
|
| 203 |
+
filtered_customers = all_customers
|
| 204 |
+
if search_term:
|
| 205 |
+
filtered_customers = [
|
| 206 |
+
c for c in all_customers
|
| 207 |
+
if search_term.lower() in c.get('name', '').lower()
|
| 208 |
+
or search_term.lower() in c.get('email', '').lower()
|
| 209 |
+
]
|
| 210 |
+
|
| 211 |
+
# Display customers in a table
|
| 212 |
+
if filtered_customers:
|
| 213 |
+
customer_data = {
|
| 214 |
+
"ID": [c.get('id') for c in filtered_customers],
|
| 215 |
+
"Name": [c.get('name', 'Unknown') for c in filtered_customers],
|
| 216 |
+
"Email": [c.get('email', 'Unknown') for c in filtered_customers],
|
| 217 |
+
"Account Type": [c.get('account_type', 'Unknown') for c in filtered_customers],
|
| 218 |
+
"Disputes": [c.get('dispute_count', 0) for c in filtered_customers]
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
customer_df = st.dataframe(
|
| 222 |
+
customer_data,
|
| 223 |
+
use_container_width=True,
|
| 224 |
+
column_config={
|
| 225 |
+
"ID": st.column_config.TextColumn(
|
| 226 |
+
"ID",
|
| 227 |
+
width="small",
|
| 228 |
+
),
|
| 229 |
+
"Disputes": st.column_config.NumberColumn(
|
| 230 |
+
"Disputes",
|
| 231 |
+
format="%d",
|
| 232 |
+
width="small",
|
| 233 |
+
),
|
| 234 |
+
},
|
| 235 |
+
hide_index=True,
|
| 236 |
+
)
|
| 237 |
+
|
| 238 |
+
# Add dropdown to select customer
|
| 239 |
+
selected_idx = 0
|
| 240 |
+
if st.session_state.selected_customer_id:
|
| 241 |
+
selected_idx = next(
|
| 242 |
+
(i for i, c in enumerate(filtered_customers) if c.get('id') == st.session_state.selected_customer_id),
|
| 243 |
+
0
|
| 244 |
+
)
|
| 245 |
+
|
| 246 |
+
selected_customer_id = st.selectbox(
|
| 247 |
+
"Select a customer to view details",
|
| 248 |
+
options=[c.get('id') for c in filtered_customers],
|
| 249 |
+
format_func=lambda x: next((c.get('name', 'Unknown') for c in filtered_customers if c.get('id') == x), 'Unknown'),
|
| 250 |
+
index=selected_idx
|
| 251 |
+
)
|
| 252 |
+
|
| 253 |
+
# Button to select the customer
|
| 254 |
+
if st.button(
|
| 255 |
+
"View Selected Customer",
|
| 256 |
+
type="primary",
|
| 257 |
+
use_container_width=True
|
| 258 |
+
):
|
| 259 |
+
st.session_state.selected_customer_id = selected_customer_id
|
| 260 |
+
st.rerun()
|
| 261 |
+
else:
|
| 262 |
+
st.info("No customers found matching your search criteria.")
|
| 263 |
+
|
| 264 |
+
# If customer is selected, show detailed view
|
| 265 |
+
if st.session_state.selected_customer_id:
|
| 266 |
+
customer = DisputeAPIClient.get_customer(st.session_state.selected_customer_id)
|
| 267 |
+
if not customer:
|
| 268 |
+
st.error("Customer not found")
|
| 269 |
+
st.session_state.selected_customer_id = None # Reset selection if customer not found
|
| 270 |
+
st.rerun()
|
| 271 |
+
else:
|
| 272 |
+
# Display detailed customer information
|
| 273 |
+
st.markdown(f"## Customer Profile: {customer.get('name', 'Unknown')}")
|
| 274 |
+
|
| 275 |
+
# Customer Information
|
| 276 |
+
col1, col2 = st.columns(2)
|
| 277 |
+
with col1:
|
| 278 |
+
with st.container(border=True):
|
| 279 |
+
st.markdown("### Profile Information")
|
| 280 |
+
st.markdown(f"**Name:** {customer.get('name', 'Unknown')}")
|
| 281 |
+
st.markdown(f"**Email:** {customer.get('email', 'Unknown')}")
|
| 282 |
+
st.markdown(f"**Account Type:** {customer.get('account_type', 'Unknown')}")
|
| 283 |
+
st.markdown(f"**Member Since:** {customer.get('created_at', 'Unknown')}")
|
| 284 |
+
|
| 285 |
+
with col2:
|
| 286 |
+
with st.container(border=True):
|
| 287 |
+
st.markdown("### Disputes Summary")
|
| 288 |
+
st.markdown(f"**Total Disputes:** {customer.get('dispute_count', 0)}")
|
| 289 |
+
|
| 290 |
+
# Create button to file new dispute for this customer
|
| 291 |
+
st.button(
|
| 292 |
+
"➕ File New Dispute for This Customer",
|
| 293 |
+
key="new_dispute_for_customer",
|
| 294 |
+
on_click=lambda: st.query_params.update(
|
| 295 |
+
{"page": "new_dispute", "customer_id": st.session_state.selected_customer_id}
|
| 296 |
+
),
|
| 297 |
+
type="primary",
|
| 298 |
+
)
|
| 299 |
+
|
| 300 |
+
# Load customer disputes
|
| 301 |
+
st.markdown("### Customer Disputes")
|
| 302 |
+
disputes = DisputeAPIClient.get_customer_disputes(st.session_state.selected_customer_id)
|
| 303 |
+
|
| 304 |
+
if disputes:
|
| 305 |
+
# Organize disputes in columns (2 per row)
|
| 306 |
+
disputes_per_row = 2
|
| 307 |
+
rows = [
|
| 308 |
+
disputes[i : i + disputes_per_row]
|
| 309 |
+
for i in range(0, len(disputes), disputes_per_row)
|
| 310 |
+
]
|
| 311 |
+
|
| 312 |
+
for row in rows:
|
| 313 |
+
cols = st.columns(disputes_per_row)
|
| 314 |
+
for i, dispute in enumerate(row):
|
| 315 |
+
with cols[i]:
|
| 316 |
+
with st.container(border=True):
|
| 317 |
+
dispute_card(dispute)
|
| 318 |
+
st.button(
|
| 319 |
+
"View Details",
|
| 320 |
+
key=f"view_{dispute['id']}",
|
| 321 |
+
on_click=lambda id=dispute["id"]: st.query_params.update(
|
| 322 |
+
{"page": "dispute_details", "id": id}
|
| 323 |
+
),
|
| 324 |
+
use_container_width=True,
|
| 325 |
+
)
|
| 326 |
+
else:
|
| 327 |
+
st.info("This customer has no disputes on record.")
|
| 328 |
+
|
| 329 |
+
# Customer Management Tab
|
| 330 |
+
with st.expander("Customer Management", expanded=False):
|
| 331 |
+
st.markdown("### Update Customer Information")
|
| 332 |
+
with st.form("update_customer_form"):
|
| 333 |
+
updated_name = st.text_input("Name", value=customer.get("name", ""))
|
| 334 |
+
updated_email = st.text_input("Email", value=customer.get("email", ""))
|
| 335 |
+
updated_account_type = st.selectbox(
|
| 336 |
+
"Account Type",
|
| 337 |
+
options=["Individual", "Business", "Premium", "VIP"],
|
| 338 |
+
index=["Individual", "Business", "Premium", "VIP"].index(
|
| 339 |
+
customer.get("account_type", "Individual")
|
| 340 |
+
),
|
| 341 |
+
)
|
| 342 |
+
|
| 343 |
+
if st.form_submit_button("Update Customer"):
|
| 344 |
+
update_data = {
|
| 345 |
+
"name": updated_name,
|
| 346 |
+
"email": updated_email,
|
| 347 |
+
"account_type": updated_account_type,
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
if DisputeAPIClient.update_customer(st.session_state.selected_customer_id, update_data):
|
| 351 |
+
st.session_state.notifications.append(
|
| 352 |
+
{"type": "success", "message": "Customer updated successfully!"}
|
| 353 |
+
)
|
| 354 |
+
st.rerun()
|
| 355 |
+
else:
|
| 356 |
+
st.error("Failed to update customer information")
|
| 357 |
+
|
| 358 |
+
# Dangerous Zone
|
| 359 |
+
st.markdown("### Dangerous Zone")
|
| 360 |
+
st.warning("These actions are permanent and cannot be undone!")
|
| 361 |
+
|
| 362 |
+
col1, col2 = st.columns(2)
|
| 363 |
+
with col1:
|
| 364 |
+
with st.form("delete_customer_form"):
|
| 365 |
+
st.markdown("**Delete Customer Account**")
|
| 366 |
+
st.markdown(
|
| 367 |
+
"This will remove all customer information and associated disputes"
|
| 368 |
+
)
|
| 369 |
+
confirm_delete = st.checkbox("I understand this cannot be undone")
|
| 370 |
+
|
| 371 |
+
if st.form_submit_button(
|
| 372 |
+
"🚨 Delete Customer", use_container_width=True
|
| 373 |
+
):
|
| 374 |
+
if confirm_delete:
|
| 375 |
+
if DisputeAPIClient.delete_customer(st.session_state.selected_customer_id):
|
| 376 |
+
st.session_state.notifications.append(
|
| 377 |
+
{
|
| 378 |
+
"type": "success",
|
| 379 |
+
"message": "Customer deleted successfully!",
|
| 380 |
+
}
|
| 381 |
+
)
|
| 382 |
+
st.session_state.selected_customer_id = None
|
| 383 |
+
st.query_params.update({"page": "dashboard"})
|
| 384 |
+
st.rerun()
|
| 385 |
+
else:
|
| 386 |
+
st.error("Failed to delete customer")
|
| 387 |
+
else:
|
| 388 |
+
st.error("Please confirm deletion")
|
| 389 |
+
|
| 390 |
+
with col2:
|
| 391 |
+
with st.form("merge_customer_form"):
|
| 392 |
+
st.markdown("**Merge Customer Accounts**")
|
| 393 |
+
target_customer = st.selectbox(
|
| 394 |
+
"Select target customer",
|
| 395 |
+
options=[
|
| 396 |
+
c["id"]
|
| 397 |
+
for c in DisputeAPIClient.get_customers()
|
| 398 |
+
if c["id"] != st.session_state.selected_customer_id
|
| 399 |
+
],
|
| 400 |
+
)
|
| 401 |
+
confirm_merge = st.checkbox("Confirm account merge")
|
| 402 |
+
|
| 403 |
+
if st.form_submit_button("🔀 Merge Accounts", use_container_width=True):
|
| 404 |
+
if confirm_merge:
|
| 405 |
+
st.info("Account merging functionality coming soon!")
|
| 406 |
+
else:
|
| 407 |
+
st.error("Please confirm merge")
|
| 408 |
+
else:
|
| 409 |
+
# If no customer is selected, show a prompt
|
| 410 |
+
st.info("Please select a customer from the list above to view their details.")
|
| 411 |
+
|
| 412 |
+
|
| 413 |
+
if __name__ == "__main__":
|
| 414 |
+
display_customer_details_page()
|
| 415 |
+
|
| 416 |
+
|
app/frontend/pages/dashboard.py
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app/frontend/pages/dashboard.py
|
| 2 |
+
import streamlit as st
|
| 3 |
+
from typing import List, Dict
|
| 4 |
+
from utils.api_client import DisputeAPIClient
|
| 5 |
+
from components.dispute_card import dispute_card
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def display_dashboard():
|
| 9 |
+
"""Main dashboard view with dispute overview and filtering"""
|
| 10 |
+
|
| 11 |
+
st.title("Dispute Management Dashboard")
|
| 12 |
+
|
| 13 |
+
# Metrics Row
|
| 14 |
+
metrics = DisputeAPIClient.get_dashboard_metrics()
|
| 15 |
+
col1, col2, col3, col4 = st.columns(4)
|
| 16 |
+
with col1:
|
| 17 |
+
st.metric(
|
| 18 |
+
"Total Disputes",
|
| 19 |
+
metrics.get("total_disputes", 0),
|
| 20 |
+
)
|
| 21 |
+
with col2:
|
| 22 |
+
st.metric(
|
| 23 |
+
"High Priority",
|
| 24 |
+
metrics.get("high_priority_count", 0),
|
| 25 |
+
)
|
| 26 |
+
with col3:
|
| 27 |
+
st.metric(
|
| 28 |
+
"Pending Review",
|
| 29 |
+
metrics.get("pending_count", 0),
|
| 30 |
+
)
|
| 31 |
+
with col4:
|
| 32 |
+
system_healthy = DisputeAPIClient.check_health()
|
| 33 |
+
st.metric("System Status", "✅ Online" if system_healthy else "❌ Offline")
|
| 34 |
+
|
| 35 |
+
# Store filter state in session state to persist between refreshes
|
| 36 |
+
if "priority_filter" not in st.session_state:
|
| 37 |
+
st.session_state.priority_filter = []
|
| 38 |
+
if "status_filter" not in st.session_state:
|
| 39 |
+
st.session_state.status_filter = []
|
| 40 |
+
if "sort_option" not in st.session_state:
|
| 41 |
+
st.session_state.sort_option = "Priority"
|
| 42 |
+
|
| 43 |
+
# Filters with two columns for better layout
|
| 44 |
+
with st.expander("Filter Disputes", expanded=True):
|
| 45 |
+
col1, col2 = st.columns(2)
|
| 46 |
+
|
| 47 |
+
with col1:
|
| 48 |
+
# Priority filter with more descriptive labels
|
| 49 |
+
priority_filter = st.multiselect(
|
| 50 |
+
"Priority Level",
|
| 51 |
+
options=[1, 2, 3, 4, 5],
|
| 52 |
+
default=st.session_state.priority_filter,
|
| 53 |
+
format_func=lambda x: f"P{x} - {'Critical' if x >= 4 else 'High' if x == 3 else 'Medium' if x == 2 else 'Low'}",
|
| 54 |
+
)
|
| 55 |
+
st.session_state.priority_filter = priority_filter
|
| 56 |
+
|
| 57 |
+
# Status filter with common statuses
|
| 58 |
+
status_filter = st.multiselect(
|
| 59 |
+
"Status",
|
| 60 |
+
options=[
|
| 61 |
+
"Open",
|
| 62 |
+
"Under Review",
|
| 63 |
+
"Info Requested",
|
| 64 |
+
"Resolved",
|
| 65 |
+
"Approved",
|
| 66 |
+
"Rejected",
|
| 67 |
+
],
|
| 68 |
+
default=st.session_state.status_filter,
|
| 69 |
+
)
|
| 70 |
+
st.session_state.status_filter = status_filter
|
| 71 |
+
|
| 72 |
+
with col2:
|
| 73 |
+
# Sort options
|
| 74 |
+
sort_option = st.selectbox(
|
| 75 |
+
"Sort By",
|
| 76 |
+
options=[
|
| 77 |
+
"Priority",
|
| 78 |
+
"Date (Newest)",
|
| 79 |
+
"Date (Oldest)",
|
| 80 |
+
"Amount (High to Low)",
|
| 81 |
+
],
|
| 82 |
+
index=[
|
| 83 |
+
"Priority",
|
| 84 |
+
"Date (Newest)",
|
| 85 |
+
"Date (Oldest)",
|
| 86 |
+
"Amount (High to Low)",
|
| 87 |
+
].index(st.session_state.sort_option),
|
| 88 |
+
)
|
| 89 |
+
st.session_state.sort_option = sort_option
|
| 90 |
+
|
| 91 |
+
# Add a clear filters button
|
| 92 |
+
if st.button("Clear Filters"):
|
| 93 |
+
st.session_state.priority_filter = []
|
| 94 |
+
st.session_state.status_filter = []
|
| 95 |
+
st.session_state.sort_option = "Priority"
|
| 96 |
+
st.rerun()
|
| 97 |
+
|
| 98 |
+
# Create New Dispute Button
|
| 99 |
+
col1, col2 = st.columns([3, 1])
|
| 100 |
+
with col2:
|
| 101 |
+
st.button(
|
| 102 |
+
"➕ Create New Dispute",
|
| 103 |
+
on_click=lambda: st.query_params.update({"page": "new_dispute"}),
|
| 104 |
+
type="primary",
|
| 105 |
+
use_container_width=True,
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
# Load disputes with filters
|
| 109 |
+
sort_mapping = {
|
| 110 |
+
"Priority": "priority",
|
| 111 |
+
"Date (Newest)": "date_desc",
|
| 112 |
+
"Date (Oldest)": "date_asc",
|
| 113 |
+
"Amount (High to Low)": "amount_desc",
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
# Get all disputes
|
| 117 |
+
all_disputes = DisputeAPIClient.get_disputes(
|
| 118 |
+
sort_by=sort_mapping.get(sort_option, "priority")
|
| 119 |
+
)
|
| 120 |
+
|
| 121 |
+
# Apply filters manually (since API filter parameters aren't working)
|
| 122 |
+
if all_disputes:
|
| 123 |
+
filtered_disputes = all_disputes
|
| 124 |
+
|
| 125 |
+
# Apply priority filter if set
|
| 126 |
+
if priority_filter:
|
| 127 |
+
filtered_disputes = [
|
| 128 |
+
d for d in filtered_disputes if d.get("priority") in priority_filter
|
| 129 |
+
]
|
| 130 |
+
|
| 131 |
+
# Apply status filter if set
|
| 132 |
+
if status_filter:
|
| 133 |
+
filtered_disputes = [
|
| 134 |
+
d for d in filtered_disputes if d.get("status") in status_filter
|
| 135 |
+
]
|
| 136 |
+
|
| 137 |
+
# Apply sorting if needed
|
| 138 |
+
if sort_option == "Date (Newest)":
|
| 139 |
+
filtered_disputes.sort(key=lambda x: x.get("created_at", ""), reverse=True)
|
| 140 |
+
elif sort_option == "Date (Oldest)":
|
| 141 |
+
filtered_disputes.sort(key=lambda x: x.get("created_at", ""))
|
| 142 |
+
elif sort_option == "Amount (High to Low)":
|
| 143 |
+
filtered_disputes.sort(
|
| 144 |
+
key=lambda x: float(x.get("amount", 0)), reverse=True
|
| 145 |
+
)
|
| 146 |
+
elif sort_option == "Priority":
|
| 147 |
+
filtered_disputes.sort(key=lambda x: x.get("priority", 1), reverse=True)
|
| 148 |
+
|
| 149 |
+
disputes = filtered_disputes
|
| 150 |
+
else:
|
| 151 |
+
disputes = []
|
| 152 |
+
|
| 153 |
+
# Display disputes
|
| 154 |
+
if disputes:
|
| 155 |
+
st.subheader(f"Disputes ({len(disputes)})")
|
| 156 |
+
|
| 157 |
+
# Organize disputes in columns (2 per row)
|
| 158 |
+
disputes_per_row = 2
|
| 159 |
+
rows = [
|
| 160 |
+
disputes[i : i + disputes_per_row]
|
| 161 |
+
for i in range(0, len(disputes), disputes_per_row)
|
| 162 |
+
]
|
| 163 |
+
|
| 164 |
+
for row in rows:
|
| 165 |
+
cols = st.columns(disputes_per_row)
|
| 166 |
+
for i, dispute in enumerate(row):
|
| 167 |
+
with cols[i]:
|
| 168 |
+
with st.container(border=True):
|
| 169 |
+
dispute_card(dispute)
|
| 170 |
+
|
| 171 |
+
# Show analysis summary if available
|
| 172 |
+
if dispute.get("analysis"):
|
| 173 |
+
st.info(
|
| 174 |
+
f"Priority: P{dispute['analysis'].get('priority', 'N/A')} • Risk: {dispute['analysis'].get('risk_score', 'N/A')}%"
|
| 175 |
+
)
|
| 176 |
+
|
| 177 |
+
# Action buttons
|
| 178 |
+
col1, col2 = st.columns(2)
|
| 179 |
+
with col1:
|
| 180 |
+
st.button(
|
| 181 |
+
"View Details",
|
| 182 |
+
key=f"view_{dispute['id']}",
|
| 183 |
+
on_click=lambda id=dispute["id"]: st.query_params.update(
|
| 184 |
+
{"page": "dispute_details", "id": id} # Add clear params
|
| 185 |
+
),
|
| 186 |
+
use_container_width=True,
|
| 187 |
+
)
|
| 188 |
+
with col2:
|
| 189 |
+
if dispute.get("customer_id"):
|
| 190 |
+
st.button(
|
| 191 |
+
"Customer Info",
|
| 192 |
+
key=f"customer_{dispute['id']}",
|
| 193 |
+
on_click=lambda cid=dispute["customer_id"]: st.query_params.update(
|
| 194 |
+
{"page": "customer_details", "id": cid}
|
| 195 |
+
),
|
| 196 |
+
use_container_width=True,
|
| 197 |
+
)
|
| 198 |
+
else:
|
| 199 |
+
st.info("No disputes found. Adjust your filters or create a new dispute.")
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
if __name__ == "__main__":
|
| 203 |
+
display_dashboard()
|
app/frontend/pages/dispute_details.py
ADDED
|
@@ -0,0 +1,544 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app/frontend/pages/dispute_details.py
|
| 2 |
+
import streamlit as st
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
from utils.api_client import DisputeAPIClient
|
| 5 |
+
from components.insights_panel import ai_insights_panel
|
| 6 |
+
from components.followup_questions import display_followup_questions
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def display_dispute_details():
|
| 10 |
+
"""Detailed view of a single dispute"""
|
| 11 |
+
|
| 12 |
+
# Get all disputes
|
| 13 |
+
all_disputes = DisputeAPIClient.get_disputes()
|
| 14 |
+
|
| 15 |
+
# Get dispute ID from query params
|
| 16 |
+
dispute_id = st.query_params.get("id")
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
if not dispute_id:
|
| 20 |
+
st.error("No dispute selected")
|
| 21 |
+
st.button(
|
| 22 |
+
"Return to Dashboard",
|
| 23 |
+
on_click=lambda: st.query_params.update({"page": "dashboard"}),
|
| 24 |
+
)
|
| 25 |
+
return
|
| 26 |
+
|
| 27 |
+
# Load dispute data
|
| 28 |
+
dispute = DisputeAPIClient.get_dispute(dispute_id)
|
| 29 |
+
if not dispute:
|
| 30 |
+
st.error("Dispute not found")
|
| 31 |
+
st.button(
|
| 32 |
+
"Return to Dashboard",
|
| 33 |
+
on_click=lambda: st.query_params.update({"page": "dashboard"}),
|
| 34 |
+
)
|
| 35 |
+
return
|
| 36 |
+
|
| 37 |
+
# Header Section with return button
|
| 38 |
+
col1, col2, col3 = st.columns([1, 3, 1])
|
| 39 |
+
with col1:
|
| 40 |
+
st.button(
|
| 41 |
+
"← Back", on_click=lambda: st.query_params.update({"page": "dashboard"})
|
| 42 |
+
)
|
| 43 |
+
with col2:
|
| 44 |
+
st.title(f"Dispute #{dispute['id'][:6]}")
|
| 45 |
+
with col3:
|
| 46 |
+
st.write("")
|
| 47 |
+
|
| 48 |
+
# Status bar with styled colors
|
| 49 |
+
status_colors = {
|
| 50 |
+
"Open": "🔵",
|
| 51 |
+
"Under Review": "🟠",
|
| 52 |
+
"Info Requested": "🟡",
|
| 53 |
+
"Resolved": "🟢",
|
| 54 |
+
"Approved": "✅",
|
| 55 |
+
"Rejected": "❌",
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
status = dispute.get("status", "Unknown")
|
| 59 |
+
priority = dispute.get("priority", "N/A")
|
| 60 |
+
|
| 61 |
+
# Status and Priority Indicators
|
| 62 |
+
col1, col2 = st.columns(2)
|
| 63 |
+
with col1:
|
| 64 |
+
st.markdown(f"### Status: {status_colors.get(status, '⚪')} {status}")
|
| 65 |
+
with col2:
|
| 66 |
+
priority_color = (
|
| 67 |
+
"red"
|
| 68 |
+
if priority >= 4
|
| 69 |
+
else "orange" if priority == 3 else "blue" if priority == 2 else "green"
|
| 70 |
+
)
|
| 71 |
+
st.markdown(
|
| 72 |
+
f"### Priority: <span style='color:{priority_color};'>P{priority}</span>",
|
| 73 |
+
unsafe_allow_html=True,
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
# Main Content Tabs
|
| 77 |
+
tab1, tab2, tab3 = st.tabs(["Details", "AI Analysis", "Resolution"])
|
| 78 |
+
|
| 79 |
+
with tab1:
|
| 80 |
+
# Basic Info Columns
|
| 81 |
+
col1, col2 = st.columns(2)
|
| 82 |
+
with col1:
|
| 83 |
+
with st.container(border=True):
|
| 84 |
+
st.markdown("### Customer Information")
|
| 85 |
+
if "customer" in dispute and dispute["customer"]:
|
| 86 |
+
customer = dispute["customer"]
|
| 87 |
+
st.markdown(f"**Name:** {customer.get('name', 'Unknown')}")
|
| 88 |
+
st.markdown(f"**Email:** {customer.get('email', 'Unknown')}")
|
| 89 |
+
st.markdown(
|
| 90 |
+
f"**Account Type:** {customer.get('account_type', 'Unknown')}"
|
| 91 |
+
)
|
| 92 |
+
st.markdown(
|
| 93 |
+
f"**Total Disputes:** {customer.get('dispute_count', 0)}"
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
+
# Customer details button
|
| 97 |
+
st.button(
|
| 98 |
+
"View Customer Profile",
|
| 99 |
+
key="view_customer",
|
| 100 |
+
on_click=lambda: st.query_params.update(
|
| 101 |
+
{"page": "customer_details", "id": customer["id"]}
|
| 102 |
+
),
|
| 103 |
+
use_container_width=True,
|
| 104 |
+
)
|
| 105 |
+
else:
|
| 106 |
+
customer_id = dispute.get("customer_id")
|
| 107 |
+
st.markdown(f"**Customer ID:** {customer_id}")
|
| 108 |
+
st.markdown("_Detailed customer information not available_")
|
| 109 |
+
|
| 110 |
+
with col2:
|
| 111 |
+
with st.container(border=True):
|
| 112 |
+
st.markdown("### Transaction Information")
|
| 113 |
+
st.markdown(f"**Amount:** ${dispute.get('amount', 0):.2f}")
|
| 114 |
+
st.markdown(f"**Merchant:** {dispute.get('merchant_name', 'Unknown')}")
|
| 115 |
+
st.markdown(
|
| 116 |
+
f"**Transaction ID:** {dispute.get('transaction_id', 'Unknown')}"
|
| 117 |
+
)
|
| 118 |
+
st.markdown(f"**Category:** {dispute.get('category', 'Unknown')}")
|
| 119 |
+
st.markdown(f"**Created:** {dispute.get('created_at', 'Unknown')}")
|
| 120 |
+
if dispute.get("resolved_at"):
|
| 121 |
+
st.markdown(f"**Resolved:** {dispute.get('resolved_at')}")
|
| 122 |
+
|
| 123 |
+
# Description Section
|
| 124 |
+
st.markdown("### Dispute Description")
|
| 125 |
+
st.write(dispute.get("description", "No description provided"))
|
| 126 |
+
|
| 127 |
+
with tab2:
|
| 128 |
+
# AI Insights Section
|
| 129 |
+
st.markdown("## AI Analysis")
|
| 130 |
+
|
| 131 |
+
# Get analysis from dedicated insights endpoint
|
| 132 |
+
analysis = DisputeAPIClient.get_insights(dispute_id)
|
| 133 |
+
|
| 134 |
+
if analysis:
|
| 135 |
+
ai_insights_panel(analysis, dispute.get("priority", 3))
|
| 136 |
+
|
| 137 |
+
# If followup questions exist
|
| 138 |
+
if analysis.get("followup_questions"):
|
| 139 |
+
st.markdown("### Follow-up Interactions")
|
| 140 |
+
display_followup_questions(
|
| 141 |
+
questions=analysis["followup_questions"],
|
| 142 |
+
dispute_id=dispute_id,
|
| 143 |
+
)
|
| 144 |
+
|
| 145 |
+
else:
|
| 146 |
+
st.warning("No stored analysis found")
|
| 147 |
+
if st.button("Generate New Analysis", type="primary"):
|
| 148 |
+
with st.spinner("Analyzing..."):
|
| 149 |
+
new_analysis = DisputeAPIClient.analyze_dispute(dispute_id)
|
| 150 |
+
if new_analysis and DisputeAPIClient.save_insights(
|
| 151 |
+
dispute_id, new_analysis
|
| 152 |
+
):
|
| 153 |
+
st.rerun()
|
| 154 |
+
|
| 155 |
+
with tab3:
|
| 156 |
+
# Resolution Actions
|
| 157 |
+
st.markdown("## Resolution Actions")
|
| 158 |
+
|
| 159 |
+
# Different action buttons based on current status
|
| 160 |
+
if status in ["Open", "Under Review", "Info Requested"]:
|
| 161 |
+
col1, col2, col3 = st.columns(3)
|
| 162 |
+
with col1:
|
| 163 |
+
if st.button(
|
| 164 |
+
"✅ Approve Dispute", use_container_width=True, type="primary"
|
| 165 |
+
):
|
| 166 |
+
if DisputeAPIClient.update_dispute_status(dispute_id, "Approved"):
|
| 167 |
+
st.session_state.notifications.append(
|
| 168 |
+
{
|
| 169 |
+
"type": "success",
|
| 170 |
+
"message": "Dispute approved successfully!",
|
| 171 |
+
}
|
| 172 |
+
)
|
| 173 |
+
st.rerun()
|
| 174 |
+
with col2:
|
| 175 |
+
if st.button("❌ Reject Dispute", use_container_width=True):
|
| 176 |
+
if DisputeAPIClient.update_dispute_status(dispute_id, "Rejected"):
|
| 177 |
+
st.session_state.notifications.append(
|
| 178 |
+
{"type": "success", "message": "Dispute rejected"}
|
| 179 |
+
)
|
| 180 |
+
st.rerun()
|
| 181 |
+
with col3:
|
| 182 |
+
if st.button("📩 Request More Info", use_container_width=True):
|
| 183 |
+
if DisputeAPIClient.update_dispute_status(
|
| 184 |
+
dispute_id, "Info Requested"
|
| 185 |
+
):
|
| 186 |
+
st.session_state.notifications.append(
|
| 187 |
+
{"type": "success", "message": "Information request sent"}
|
| 188 |
+
)
|
| 189 |
+
st.rerun()
|
| 190 |
+
else:
|
| 191 |
+
st.info(f"This dispute is already {status}. No further actions available.")
|
| 192 |
+
|
| 193 |
+
# Option to reopen the dispute
|
| 194 |
+
if st.button("Reopen Dispute", use_container_width=True):
|
| 195 |
+
if DisputeAPIClient.update_dispute_status(dispute_id, "Open"):
|
| 196 |
+
st.session_state.notifications.append(
|
| 197 |
+
{"type": "success", "message": "Dispute reopened successfully!"}
|
| 198 |
+
)
|
| 199 |
+
st.rerun()
|
| 200 |
+
|
| 201 |
+
# Add notes section
|
| 202 |
+
st.markdown("### Add Resolution Notes")
|
| 203 |
+
with st.form("resolution_notes_form"):
|
| 204 |
+
notes = st.text_area(
|
| 205 |
+
"Notes",
|
| 206 |
+
height=100,
|
| 207 |
+
placeholder="Add additional resolution notes here...",
|
| 208 |
+
)
|
| 209 |
+
submitted = st.form_submit_button("Save Notes")
|
| 210 |
+
if submitted:
|
| 211 |
+
# Add logic to save notes using API
|
| 212 |
+
st.success("Notes saved successfully!")
|
| 213 |
+
|
| 214 |
+
# Dangerous Zone
|
| 215 |
+
with st.expander("Advanced Options"):
|
| 216 |
+
st.warning("Danger Zone: These actions cannot be undone!")
|
| 217 |
+
if st.button("Delete Dispute", use_container_width=True):
|
| 218 |
+
confirm = st.checkbox("I understand this action cannot be undone")
|
| 219 |
+
if confirm and DisputeAPIClient.delete_dispute(dispute_id):
|
| 220 |
+
st.session_state.notifications.append(
|
| 221 |
+
{"type": "success", "message": "Dispute deleted successfully!"}
|
| 222 |
+
)
|
| 223 |
+
st.query_params.update({"page": "dashboard"})
|
| 224 |
+
st.rerun()
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
# import streamlit as st
|
| 228 |
+
# from datetime import datetime
|
| 229 |
+
# from utils.api_client import DisputeAPIClient
|
| 230 |
+
# from components.insights_panel import ai_insights_panel
|
| 231 |
+
# from components.followup_questions import display_followup_questions
|
| 232 |
+
|
| 233 |
+
|
| 234 |
+
def display_dispute_details_page():
|
| 235 |
+
"""Detailed view of a single dispute"""
|
| 236 |
+
|
| 237 |
+
# Get all disputes
|
| 238 |
+
all_disputes = DisputeAPIClient.get_disputes()
|
| 239 |
+
|
| 240 |
+
# Initialize session state to store selected dispute if not already set
|
| 241 |
+
if "selected_dispute_id" not in st.session_state:
|
| 242 |
+
st.session_state.selected_dispute_id = None
|
| 243 |
+
|
| 244 |
+
# Header Section with return button
|
| 245 |
+
col1, col2, col3 = st.columns([1, 3, 1])
|
| 246 |
+
with col1:
|
| 247 |
+
st.button(
|
| 248 |
+
"← Back", on_click=lambda: st.query_params.update({"page": "dashboard"})
|
| 249 |
+
)
|
| 250 |
+
with col2:
|
| 251 |
+
st.title("Dispute Details")
|
| 252 |
+
with col3:
|
| 253 |
+
st.write("")
|
| 254 |
+
|
| 255 |
+
# Display all disputes section for selection
|
| 256 |
+
with st.container(border=True):
|
| 257 |
+
st.markdown("### Dispute List")
|
| 258 |
+
|
| 259 |
+
# Create a searchable filter
|
| 260 |
+
search_term = st.text_input(
|
| 261 |
+
"Search disputes by ID, customer_id, or merchant", ""
|
| 262 |
+
)
|
| 263 |
+
|
| 264 |
+
# Filter disputes based on search term if provided
|
| 265 |
+
filtered_disputes = all_disputes
|
| 266 |
+
if search_term:
|
| 267 |
+
filtered_disputes = [
|
| 268 |
+
d
|
| 269 |
+
for d in all_disputes
|
| 270 |
+
if (
|
| 271 |
+
search_term.lower() in str(d.get("id", "")).lower()
|
| 272 |
+
or search_term.lower() in get_customer_name(d).lower()
|
| 273 |
+
or search_term.lower() in str(d.get("merchant_name", "")).lower()
|
| 274 |
+
)
|
| 275 |
+
]
|
| 276 |
+
|
| 277 |
+
# Display disputes in a table
|
| 278 |
+
if filtered_disputes:
|
| 279 |
+
dispute_data = {
|
| 280 |
+
"ID": [d.get("id", "")[:6] for d in filtered_disputes],
|
| 281 |
+
"Customer": [get_customer_name(d) for d in filtered_disputes],
|
| 282 |
+
"Amount": [f"${d.get('amount', 0):.2f}" for d in filtered_disputes],
|
| 283 |
+
"Merchant": [
|
| 284 |
+
d.get("merchant_name", "Unknown") for d in filtered_disputes
|
| 285 |
+
],
|
| 286 |
+
"Status": [d.get("status", "Unknown") for d in filtered_disputes],
|
| 287 |
+
"Priority": [f"P{d.get('priority', 'N/A')}" for d in filtered_disputes],
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
dispute_df = st.dataframe(
|
| 291 |
+
dispute_data,
|
| 292 |
+
use_container_width=True,
|
| 293 |
+
hide_index=True,
|
| 294 |
+
)
|
| 295 |
+
|
| 296 |
+
# Add dropdown to select dispute
|
| 297 |
+
selected_idx = 0
|
| 298 |
+
if st.session_state.selected_dispute_id:
|
| 299 |
+
selected_idx = next(
|
| 300 |
+
(
|
| 301 |
+
i
|
| 302 |
+
for i, d in enumerate(filtered_disputes)
|
| 303 |
+
if d.get("id") == st.session_state.selected_dispute_id
|
| 304 |
+
),
|
| 305 |
+
0,
|
| 306 |
+
)
|
| 307 |
+
|
| 308 |
+
selected_dispute_id = st.selectbox(
|
| 309 |
+
"Select a dispute to view details",
|
| 310 |
+
options=[d.get("id") for d in filtered_disputes],
|
| 311 |
+
format_func=lambda x: f"#{x[:6]} - {next((d.get('merchant_name', 'Unknown') for d in filtered_disputes if d.get('id') == x), 'Unknown')}",
|
| 312 |
+
index=selected_idx,
|
| 313 |
+
)
|
| 314 |
+
|
| 315 |
+
# Button to select the dispute
|
| 316 |
+
if st.button(
|
| 317 |
+
"View Selected Dispute", type="primary", use_container_width=True
|
| 318 |
+
):
|
| 319 |
+
st.session_state.selected_dispute_id = selected_dispute_id
|
| 320 |
+
st.rerun()
|
| 321 |
+
else:
|
| 322 |
+
st.info("No disputes found matching your search criteria.")
|
| 323 |
+
|
| 324 |
+
# If no dispute is selected, show a prompt
|
| 325 |
+
if not st.session_state.selected_dispute_id:
|
| 326 |
+
st.info("Please select a dispute from the list above to view its details.")
|
| 327 |
+
return
|
| 328 |
+
|
| 329 |
+
# Load dispute data
|
| 330 |
+
dispute = DisputeAPIClient.get_dispute(st.session_state.selected_dispute_id)
|
| 331 |
+
if not dispute:
|
| 332 |
+
st.error("Dispute not found")
|
| 333 |
+
st.session_state.selected_dispute_id = (
|
| 334 |
+
None # Reset selection if dispute not found
|
| 335 |
+
)
|
| 336 |
+
st.rerun()
|
| 337 |
+
return
|
| 338 |
+
|
| 339 |
+
# Show the selected dispute title
|
| 340 |
+
st.markdown(f"## Dispute #{dispute['id'][:6]}")
|
| 341 |
+
|
| 342 |
+
# Status bar with styled colors
|
| 343 |
+
status_colors = {
|
| 344 |
+
"Open": "🔵",
|
| 345 |
+
"Under Review": "🟠",
|
| 346 |
+
"Info Requested": "🟡",
|
| 347 |
+
"Resolved": "🟢",
|
| 348 |
+
"Approved": "✅",
|
| 349 |
+
"Rejected": "❌",
|
| 350 |
+
}
|
| 351 |
+
|
| 352 |
+
status = dispute.get("status", "Unknown")
|
| 353 |
+
priority = dispute.get("priority", "N/A")
|
| 354 |
+
|
| 355 |
+
# Status and Priority Indicators
|
| 356 |
+
col1, col2 = st.columns(2)
|
| 357 |
+
with col1:
|
| 358 |
+
st.markdown(f"### Status: {status_colors.get(status, '⚪')} {status}")
|
| 359 |
+
with col2:
|
| 360 |
+
priority_color = (
|
| 361 |
+
"red"
|
| 362 |
+
if priority >= 4
|
| 363 |
+
else "orange" if priority == 3 else "blue" if priority == 2 else "green"
|
| 364 |
+
)
|
| 365 |
+
st.markdown(
|
| 366 |
+
f"### Priority: <span style='color:{priority_color};'>P{priority}</span>",
|
| 367 |
+
unsafe_allow_html=True,
|
| 368 |
+
)
|
| 369 |
+
|
| 370 |
+
# Main Content Tabs
|
| 371 |
+
tab1, tab2, tab3 = st.tabs(["Details", "AI Analysis", "Resolution"])
|
| 372 |
+
|
| 373 |
+
with tab1:
|
| 374 |
+
# Basic Info Columns
|
| 375 |
+
col1, col2 = st.columns(2)
|
| 376 |
+
with col1:
|
| 377 |
+
with st.container(border=True):
|
| 378 |
+
st.markdown("### Customer Information")
|
| 379 |
+
customer_info = dispute.get("customer_id")
|
| 380 |
+
|
| 381 |
+
# Check if customer_id is a dictionary or a string/other type
|
| 382 |
+
if isinstance(customer_info, dict):
|
| 383 |
+
st.markdown(f"**Name:** {customer_info.get('name', 'Unknown')}")
|
| 384 |
+
st.markdown(f"**Email:** {customer_info.get('email', 'Unknown')}")
|
| 385 |
+
st.markdown(
|
| 386 |
+
f"**Account Type:** {customer_info.get('account_type', 'Unknown')}"
|
| 387 |
+
)
|
| 388 |
+
st.markdown(
|
| 389 |
+
f"**Total Disputes:** {customer_info.get('dispute_count', 0)}"
|
| 390 |
+
)
|
| 391 |
+
|
| 392 |
+
# Customer details button
|
| 393 |
+
if st.button(
|
| 394 |
+
"View Customer Profile",
|
| 395 |
+
key="view_customer",
|
| 396 |
+
use_container_width=True,
|
| 397 |
+
):
|
| 398 |
+
# Update session state instead of query params
|
| 399 |
+
st.session_state.page = "customer_details"
|
| 400 |
+
st.session_state.selected_customer_id = customer_info["id"]
|
| 401 |
+
st.query_params.update({"page": "customer_details"})
|
| 402 |
+
st.rerun()
|
| 403 |
+
else:
|
| 404 |
+
st.markdown(f"**Customer ID:** {customer_info}")
|
| 405 |
+
st.markdown("_Detailed customer information not available_")
|
| 406 |
+
|
| 407 |
+
with col2:
|
| 408 |
+
with st.container(border=True):
|
| 409 |
+
st.markdown("### Transaction Information")
|
| 410 |
+
st.markdown(f"**Amount:** ${dispute.get('amount', 0):.2f}")
|
| 411 |
+
st.markdown(f"**Merchant:** {dispute.get('merchant_name', 'Unknown')}")
|
| 412 |
+
st.markdown(
|
| 413 |
+
f"**Transaction ID:** {dispute.get('transaction_id', 'Unknown')}"
|
| 414 |
+
)
|
| 415 |
+
st.markdown(f"**Category:** {dispute.get('category', 'Unknown')}")
|
| 416 |
+
st.markdown(f"**Created:** {dispute.get('created_at', 'Unknown')}")
|
| 417 |
+
if dispute.get("resolved_at"):
|
| 418 |
+
st.markdown(f"**Resolved:** {dispute.get('resolved_at')}")
|
| 419 |
+
|
| 420 |
+
# Description Section
|
| 421 |
+
st.markdown("### Dispute Description")
|
| 422 |
+
st.write(dispute.get("description", "No description provided"))
|
| 423 |
+
|
| 424 |
+
with tab2:
|
| 425 |
+
# AI Insights Section
|
| 426 |
+
st.markdown("## AI Analysis")
|
| 427 |
+
|
| 428 |
+
# Get analysis from dedicated insights endpoint
|
| 429 |
+
analysis = DisputeAPIClient.get_insights(st.session_state.selected_dispute_id)
|
| 430 |
+
|
| 431 |
+
if analysis:
|
| 432 |
+
ai_insights_panel(analysis, dispute.get("priority", 3))
|
| 433 |
+
|
| 434 |
+
# If followup questions exist
|
| 435 |
+
if analysis.get("followup_questions"):
|
| 436 |
+
st.markdown("### Follow-up Interactions")
|
| 437 |
+
display_followup_questions(
|
| 438 |
+
questions=analysis["followup_questions"],
|
| 439 |
+
dispute_id=st.session_state.selected_dispute_id,
|
| 440 |
+
)
|
| 441 |
+
|
| 442 |
+
else:
|
| 443 |
+
st.warning("No stored analysis found")
|
| 444 |
+
if st.button("Generate New Analysis", type="primary"):
|
| 445 |
+
with st.spinner("Analyzing..."):
|
| 446 |
+
new_analysis = DisputeAPIClient.analyze_dispute(
|
| 447 |
+
st.session_state.selected_dispute_id
|
| 448 |
+
)
|
| 449 |
+
if new_analysis and DisputeAPIClient.save_insights(
|
| 450 |
+
st.session_state.selected_dispute_id, new_analysis
|
| 451 |
+
):
|
| 452 |
+
st.rerun()
|
| 453 |
+
|
| 454 |
+
with tab3:
|
| 455 |
+
# Resolution Actions
|
| 456 |
+
st.markdown("## Resolution Actions")
|
| 457 |
+
|
| 458 |
+
# Different action buttons based on current status
|
| 459 |
+
if status in ["Open", "Under Review", "Info Requested"]:
|
| 460 |
+
col1, col2, col3 = st.columns(3)
|
| 461 |
+
with col1:
|
| 462 |
+
if st.button(
|
| 463 |
+
"✅ Approve Dispute", use_container_width=True, type="primary"
|
| 464 |
+
):
|
| 465 |
+
if DisputeAPIClient.update_dispute_status(
|
| 466 |
+
st.session_state.selected_dispute_id, "Approved"
|
| 467 |
+
):
|
| 468 |
+
st.session_state.notifications.append(
|
| 469 |
+
{
|
| 470 |
+
"type": "success",
|
| 471 |
+
"message": "Dispute approved successfully!",
|
| 472 |
+
}
|
| 473 |
+
)
|
| 474 |
+
st.rerun()
|
| 475 |
+
with col2:
|
| 476 |
+
if st.button("❌ Reject Dispute", use_container_width=True):
|
| 477 |
+
if DisputeAPIClient.update_dispute_status(
|
| 478 |
+
st.session_state.selected_dispute_id, "Rejected"
|
| 479 |
+
):
|
| 480 |
+
st.session_state.notifications.append(
|
| 481 |
+
{"type": "success", "message": "Dispute rejected"}
|
| 482 |
+
)
|
| 483 |
+
st.rerun()
|
| 484 |
+
with col3:
|
| 485 |
+
if st.button("📩 Request More Info", use_container_width=True):
|
| 486 |
+
if DisputeAPIClient.update_dispute_status(
|
| 487 |
+
st.session_state.selected_dispute_id, "Info Requested"
|
| 488 |
+
):
|
| 489 |
+
st.session_state.notifications.append(
|
| 490 |
+
{"type": "success", "message": "Information request sent"}
|
| 491 |
+
)
|
| 492 |
+
st.rerun()
|
| 493 |
+
else:
|
| 494 |
+
st.info(f"This dispute is already {status}. No further actions available.")
|
| 495 |
+
|
| 496 |
+
# Option to reopen the dispute
|
| 497 |
+
if st.button("Reopen Dispute", use_container_width=True):
|
| 498 |
+
if DisputeAPIClient.update_dispute_status(
|
| 499 |
+
st.session_state.selected_dispute_id, "Open"
|
| 500 |
+
):
|
| 501 |
+
st.session_state.notifications.append(
|
| 502 |
+
{"type": "success", "message": "Dispute reopened successfully!"}
|
| 503 |
+
)
|
| 504 |
+
st.rerun()
|
| 505 |
+
|
| 506 |
+
# Add notes section
|
| 507 |
+
st.markdown("### Add Resolution Notes")
|
| 508 |
+
with st.form("resolution_notes_form"):
|
| 509 |
+
notes = st.text_area(
|
| 510 |
+
"Notes",
|
| 511 |
+
height=100,
|
| 512 |
+
placeholder="Add additional resolution notes here...",
|
| 513 |
+
)
|
| 514 |
+
submitted = st.form_submit_button("Save Notes")
|
| 515 |
+
if submitted:
|
| 516 |
+
# Add logic to save notes using API
|
| 517 |
+
st.success("Notes saved successfully!")
|
| 518 |
+
|
| 519 |
+
# Dangerous Zone
|
| 520 |
+
with st.expander("Advanced Options"):
|
| 521 |
+
st.warning("Danger Zone: These actions cannot be undone!")
|
| 522 |
+
if st.button("Delete Dispute", use_container_width=True):
|
| 523 |
+
confirm = st.checkbox("I understand this action cannot be undone")
|
| 524 |
+
if confirm and DisputeAPIClient.delete_dispute(
|
| 525 |
+
st.session_state.selected_dispute_id
|
| 526 |
+
):
|
| 527 |
+
st.session_state.notifications.append(
|
| 528 |
+
{"type": "success", "message": "Dispute deleted successfully!"}
|
| 529 |
+
)
|
| 530 |
+
st.session_state.selected_dispute_id = None
|
| 531 |
+
st.query_params.update({"page": "dashboard"})
|
| 532 |
+
st.rerun()
|
| 533 |
+
|
| 534 |
+
|
| 535 |
+
# Helper function to safely get customer name from dispute object
|
| 536 |
+
def get_customer_name(dispute):
|
| 537 |
+
customer_id = dispute.get("customer_id")
|
| 538 |
+
if isinstance(customer_id, dict):
|
| 539 |
+
return customer_id.get("name", "Unknown")
|
| 540 |
+
return "Unknown"
|
| 541 |
+
|
| 542 |
+
|
| 543 |
+
if __name__ == "__main__":
|
| 544 |
+
display_dispute_details_page()
|
app/frontend/pages/dispute_form.py
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app/frontend/pages/dispute_form.py
|
| 2 |
+
import streamlit as st
|
| 3 |
+
import uuid
|
| 4 |
+
from datetime import datetime
|
| 5 |
+
from utils.api_client import DisputeAPIClient
|
| 6 |
+
from components.insights_panel import ai_insights_panel
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def display_dispute_form():
|
| 10 |
+
st.title("New Dispute Form")
|
| 11 |
+
response = None
|
| 12 |
+
analysis = None
|
| 13 |
+
# Load customers for dropdown
|
| 14 |
+
customers = []
|
| 15 |
+
try:
|
| 16 |
+
customers = DisputeAPIClient.get_customers()
|
| 17 |
+
except Exception as e:
|
| 18 |
+
st.error(f"Failed to load customers: {str(e)}")
|
| 19 |
+
|
| 20 |
+
# Initialize variables
|
| 21 |
+
customer_id = st.session_state.get("prefill_customer_id", "")
|
| 22 |
+
customer_name = ""
|
| 23 |
+
customer_email = ""
|
| 24 |
+
|
| 25 |
+
col1, col2 = st.columns([3, 1])
|
| 26 |
+
with col2:
|
| 27 |
+
st.button(
|
| 28 |
+
"Back to Dashboard",
|
| 29 |
+
on_click=lambda: st.query_params.update({"page": "dashboard"}),
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
with st.form("dispute_form"):
|
| 33 |
+
# Customer Section
|
| 34 |
+
st.subheader("Customer Information")
|
| 35 |
+
|
| 36 |
+
# Customer selection method
|
| 37 |
+
customer_method = st.radio(
|
| 38 |
+
"Select Customer Method",
|
| 39 |
+
["Existing Customer", "New Customer"],
|
| 40 |
+
horizontal=True,
|
| 41 |
+
index=0 if customer_id else 1,
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
if customer_method == "Existing Customer" and customers:
|
| 45 |
+
customer_options = {
|
| 46 |
+
f"{c['name']} ({c['email']})": c["id"] for c in customers
|
| 47 |
+
}
|
| 48 |
+
selected_customer = st.selectbox(
|
| 49 |
+
"Select Customer",
|
| 50 |
+
options=list(customer_options.keys()),
|
| 51 |
+
index=(
|
| 52 |
+
next(
|
| 53 |
+
(
|
| 54 |
+
i
|
| 55 |
+
for i, c_id in enumerate(customer_options.values())
|
| 56 |
+
if c_id == customer_id
|
| 57 |
+
),
|
| 58 |
+
0,
|
| 59 |
+
)
|
| 60 |
+
if customer_id
|
| 61 |
+
else 0
|
| 62 |
+
),
|
| 63 |
+
)
|
| 64 |
+
if selected_customer:
|
| 65 |
+
customer_id = customer_options[selected_customer]
|
| 66 |
+
# Find customer details for display
|
| 67 |
+
customer = next((c for c in customers if c["id"] == customer_id), None)
|
| 68 |
+
if customer:
|
| 69 |
+
st.write(f"Account Type: {customer['account_type']}")
|
| 70 |
+
st.write(f"Previous Disputes: {customer.get('dispute_count', 0)}")
|
| 71 |
+
else:
|
| 72 |
+
customer_name = st.text_input("Customer Name")
|
| 73 |
+
customer_email = st.text_input("Customer Email")
|
| 74 |
+
account_type = st.selectbox(
|
| 75 |
+
"Account Type", options=["Individual", "Business", "Premium", "VIP"]
|
| 76 |
+
)
|
| 77 |
+
|
| 78 |
+
# Transaction Details
|
| 79 |
+
st.subheader("Transaction Details")
|
| 80 |
+
col1, col2 = st.columns(2)
|
| 81 |
+
with col1:
|
| 82 |
+
transaction_id = st.text_input(
|
| 83 |
+
"Transaction ID - Auto Generated",
|
| 84 |
+
value=f"TXN-{uuid.uuid4().hex[:10].upper()}",
|
| 85 |
+
disabled=True,
|
| 86 |
+
)
|
| 87 |
+
amount = st.number_input(
|
| 88 |
+
"Amount ($)", min_value=0.01, step=1.0, value=100.00
|
| 89 |
+
)
|
| 90 |
+
with col2:
|
| 91 |
+
merchant_name = st.text_input("Merchant Name")
|
| 92 |
+
transaction_date = st.date_input("Transaction Date")
|
| 93 |
+
|
| 94 |
+
# Dispute Details
|
| 95 |
+
st.subheader("Dispute Information")
|
| 96 |
+
category = st.selectbox(
|
| 97 |
+
"Category",
|
| 98 |
+
[
|
| 99 |
+
"Unauthorized",
|
| 100 |
+
"Fraud",
|
| 101 |
+
"Service Not Rendered",
|
| 102 |
+
"Duplicate",
|
| 103 |
+
"Product Quality",
|
| 104 |
+
"Billing Error",
|
| 105 |
+
"Other",
|
| 106 |
+
],
|
| 107 |
+
)
|
| 108 |
+
description = st.text_area(
|
| 109 |
+
"Description",
|
| 110 |
+
placeholder="Please provide detailed information about the dispute...",
|
| 111 |
+
height=150,
|
| 112 |
+
)
|
| 113 |
+
|
| 114 |
+
# Additional Information
|
| 115 |
+
with st.expander("Additional Information (Optional)"):
|
| 116 |
+
attach_evidence = st.checkbox("I have evidence to attach")
|
| 117 |
+
if attach_evidence:
|
| 118 |
+
st.file_uploader("Upload Evidence", type=["pdf", "jpg", "png"])
|
| 119 |
+
urgency = st.slider(
|
| 120 |
+
"Urgency Level",
|
| 121 |
+
1,
|
| 122 |
+
5,
|
| 123 |
+
2,
|
| 124 |
+
help="Higher urgency may affect prioritization",
|
| 125 |
+
)
|
| 126 |
+
|
| 127 |
+
# Add a hidden field to store action after form submission
|
| 128 |
+
view_details = st.checkbox(
|
| 129 |
+
"View complete details after submission", value=True, key="view_details"
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
+
# Form Submission
|
| 133 |
+
submitted = st.form_submit_button("Submit Dispute", use_container_width=True)
|
| 134 |
+
|
| 135 |
+
if submitted:
|
| 136 |
+
if customer_method == "New Customer" and (
|
| 137 |
+
not customer_name or not customer_email
|
| 138 |
+
):
|
| 139 |
+
st.error("Please enter customer name and email")
|
| 140 |
+
return
|
| 141 |
+
|
| 142 |
+
if not merchant_name or not description:
|
| 143 |
+
st.error("Please fill in all required fields")
|
| 144 |
+
return
|
| 145 |
+
|
| 146 |
+
# If new customer, create customer first
|
| 147 |
+
if customer_method == "New Customer":
|
| 148 |
+
customer_data = {
|
| 149 |
+
"name": customer_name,
|
| 150 |
+
"email": customer_email,
|
| 151 |
+
"account_type": account_type,
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
try:
|
| 155 |
+
with st.spinner("Creating new customer..."):
|
| 156 |
+
new_customer = DisputeAPIClient.create_customer(customer_data)
|
| 157 |
+
if new_customer:
|
| 158 |
+
customer_id = new_customer["id"]
|
| 159 |
+
else:
|
| 160 |
+
st.error("Failed to create customer: No data returned from API")
|
| 161 |
+
return
|
| 162 |
+
except Exception as e:
|
| 163 |
+
st.error(f"Failed to create customer: {str(e)}")
|
| 164 |
+
return
|
| 165 |
+
|
| 166 |
+
# Now create the dispute
|
| 167 |
+
dispute_data = {
|
| 168 |
+
"customer_id": customer_id,
|
| 169 |
+
"transaction_id": transaction_id,
|
| 170 |
+
"merchant_name": merchant_name,
|
| 171 |
+
"amount": float(amount),
|
| 172 |
+
"description": description,
|
| 173 |
+
"category": category,
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
with st.spinner("Creating dispute and analyzing using AI 🤖..."):
|
| 177 |
+
try:
|
| 178 |
+
# Create dispute
|
| 179 |
+
response = DisputeAPIClient.create_dispute(dispute_data)
|
| 180 |
+
if not response:
|
| 181 |
+
st.error("Failed to create dispute")
|
| 182 |
+
return
|
| 183 |
+
|
| 184 |
+
# Generate and save analysis
|
| 185 |
+
# If analysis already present, load it
|
| 186 |
+
# analysis = DisputeAPIClient.get_dispute(response["id"])
|
| 187 |
+
|
| 188 |
+
# if not analysis:
|
| 189 |
+
# analysis = DisputeAPIClient.analyze_dispute(response["id"])
|
| 190 |
+
|
| 191 |
+
# if analysis and DisputeAPIClient.save_insights(response["id"], analysis):
|
| 192 |
+
# # Store in session state to maintain UI state
|
| 193 |
+
# st.session_state.new_dispute_analysis = {
|
| 194 |
+
# "dispute_id": response["id"],
|
| 195 |
+
# "analysis": analysis
|
| 196 |
+
# }
|
| 197 |
+
# st.success("Dispute created successfully!")
|
| 198 |
+
# else:
|
| 199 |
+
# st.error("Dispute created but analysis failed")
|
| 200 |
+
|
| 201 |
+
analysis = DisputeAPIClient.analyze_dispute(response["id"])
|
| 202 |
+
st.session_state.new_dispute_analysis = {
|
| 203 |
+
"dispute_id": response["id"],
|
| 204 |
+
"analysis": analysis,
|
| 205 |
+
}
|
| 206 |
+
st.success("Dispute created successfully!")
|
| 207 |
+
|
| 208 |
+
except Exception as e:
|
| 209 |
+
st.error(f"Error: {str(e)}")
|
| 210 |
+
return
|
| 211 |
+
|
| 212 |
+
# After form submission, show analysis if available
|
| 213 |
+
if "new_dispute_analysis" in st.session_state:
|
| 214 |
+
st.divider()
|
| 215 |
+
st.subheader("Initial AI Analysis Results")
|
| 216 |
+
print(st.session_state.new_dispute_analysis)
|
| 217 |
+
# Display the analysis panel
|
| 218 |
+
ai_insights_panel(
|
| 219 |
+
st.session_state.new_dispute_analysis["analysis"]["analysis"],
|
| 220 |
+
priority=st.session_state.new_dispute_analysis["analysis"]["analysis"][
|
| 221 |
+
"priority"
|
| 222 |
+
],
|
| 223 |
+
)
|
| 224 |
+
|
| 225 |
+
# Add action buttons
|
| 226 |
+
col1, col2 = st.columns(2)
|
| 227 |
+
with col1:
|
| 228 |
+
if st.button("🔄 Regenerate Analysis"):
|
| 229 |
+
with st.spinner("Re-analyzing..."):
|
| 230 |
+
new_analysis = DisputeAPIClient.analyze_dispute(
|
| 231 |
+
st.session_state.new_dispute_analysis["dispute_id"],
|
| 232 |
+
)
|
| 233 |
+
if new_analysis and DisputeAPIClient.save_insights(
|
| 234 |
+
st.session_state.new_dispute_analysis["dispute_id"],
|
| 235 |
+
new_analysis,
|
| 236 |
+
):
|
| 237 |
+
st.session_state.new_dispute_analysis["analysis"] = new_analysis
|
| 238 |
+
st.rerun()
|
| 239 |
+
|
| 240 |
+
with col2:
|
| 241 |
+
if st.button("📋 View Full Details"):
|
| 242 |
+
st.query_params.update(
|
| 243 |
+
{
|
| 244 |
+
"page": "dispute_details",
|
| 245 |
+
"id": st.session_state.new_dispute_analysis["dispute_id"],
|
| 246 |
+
}
|
| 247 |
+
)
|
| 248 |
+
del st.session_state.new_dispute_analysis # Cleanup
|
| 249 |
+
st.rerun()
|
| 250 |
+
|
| 251 |
+
# Add option to create another dispute
|
| 252 |
+
if st.button("➕ Create Another Dispute"):
|
| 253 |
+
del st.session_state.new_dispute_analysis
|
| 254 |
+
st.query_params.update({"page": "new_dispute"})
|
| 255 |
+
st.rerun()
|
| 256 |
+
|
| 257 |
+
|
| 258 |
+
if __name__ == "__main__":
|
| 259 |
+
display_dispute_form()
|
app/frontend/streamlit_app.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app/frontend/streamlit_app.py
|
| 2 |
+
import streamlit as st
|
| 3 |
+
from components.sidebar import sidebar
|
| 4 |
+
from pages.dashboard import display_dashboard
|
| 5 |
+
from pages.dispute_details import display_dispute_details
|
| 6 |
+
from pages.dispute_form import display_dispute_form
|
| 7 |
+
from pages.admin import display_admin
|
| 8 |
+
from pages.api_docs import display_api_docs
|
| 9 |
+
from pages.customer_details import display_customer_details
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def main():
|
| 13 |
+
st.set_page_config(
|
| 14 |
+
page_title="Banking Dispute Resolution",
|
| 15 |
+
page_icon="⚖️",
|
| 16 |
+
layout="wide",
|
| 17 |
+
initial_sidebar_state="expanded",
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
# Initialize session state
|
| 21 |
+
if "current_page" not in st.session_state:
|
| 22 |
+
st.session_state.current_page = "dashboard"
|
| 23 |
+
|
| 24 |
+
if "notifications" not in st.session_state:
|
| 25 |
+
st.session_state.notifications = []
|
| 26 |
+
|
| 27 |
+
# Display sidebar
|
| 28 |
+
sidebar()
|
| 29 |
+
|
| 30 |
+
# Handle notifications if any
|
| 31 |
+
if st.session_state.notifications:
|
| 32 |
+
for notification in st.session_state.notifications:
|
| 33 |
+
if notification["type"] == "success":
|
| 34 |
+
st.success(notification["message"])
|
| 35 |
+
elif notification["type"] == "error":
|
| 36 |
+
st.error(notification["message"])
|
| 37 |
+
elif notification["type"] == "warning":
|
| 38 |
+
st.warning(notification["message"])
|
| 39 |
+
elif notification["type"] == "info":
|
| 40 |
+
st.info(notification["message"])
|
| 41 |
+
st.session_state.notifications = []
|
| 42 |
+
|
| 43 |
+
# Page routing
|
| 44 |
+
query_params = st.query_params
|
| 45 |
+
page = query_params.get("page", "dashboard")
|
| 46 |
+
|
| 47 |
+
if page == "dashboard":
|
| 48 |
+
display_dashboard()
|
| 49 |
+
elif page == "dispute_details":
|
| 50 |
+
display_dispute_details()
|
| 51 |
+
elif page == "new_dispute":
|
| 52 |
+
display_dispute_form()
|
| 53 |
+
elif page == "admin":
|
| 54 |
+
display_admin()
|
| 55 |
+
elif page == "api_docs":
|
| 56 |
+
display_api_docs()
|
| 57 |
+
elif page == "customer_details":
|
| 58 |
+
display_customer_details()
|
| 59 |
+
else:
|
| 60 |
+
st.error("Invalid page specified")
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
if __name__ == "__main__":
|
| 64 |
+
main()
|