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()
|