Debopam Param commited on
Commit
44af237
·
1 Parent(s): 0f291df

first commit

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .dockerignore +93 -0
  2. .env.example +7 -0
  3. .gitignore +178 -0
  4. Dockerfile +12 -0
  5. README.md +33 -11
  6. Space.yaml +6 -0
  7. app/ai/__init__.py +0 -0
  8. app/ai/dispute_analyzer.py +151 -0
  9. app/ai/gemini_client.py +0 -0
  10. app/ai/langchain_service.py +132 -0
  11. app/ai/prompts/__init__.py +0 -0
  12. app/ai/prompts/dispute_insights.py +29 -0
  13. app/ai/prompts/dispute_priority.py +30 -0
  14. app/ai/prompts/followup_questions.py +20 -0
  15. app/ai/schemas/__init__.py +0 -0
  16. app/ai/schemas/insights_schema.py +30 -0
  17. app/ai/schemas/priority_schema.py +12 -0
  18. app/api/__init__.py +0 -0
  19. app/api/database.py +140 -0
  20. app/api/models.py +171 -0
  21. app/api/routes/__init__.py +0 -0
  22. app/api/routes/customers.py +181 -0
  23. app/api/routes/disputes.py +505 -0
  24. app/api/services/__init__.py +0 -0
  25. app/api/services/ai_service.py +0 -0
  26. app/api/services/database_service.py +331 -0
  27. app/api/services/priority_service.py +40 -0
  28. app/api/services/recommendation_service.py +25 -0
  29. app/core/__init__.py +0 -0
  30. app/core/ai_config.py +20 -0
  31. app/core/config.py +20 -0
  32. app/data/__init__.py +0 -0
  33. app/data/dispute_categories.json +14 -0
  34. app/data/sample_disputes.json +26 -0
  35. app/entrypoint.py +34 -0
  36. app/frontend/__init__.py +0 -0
  37. app/frontend/components/__init__.py +0 -0
  38. app/frontend/components/api_popover.py +0 -0
  39. app/frontend/components/dispute_card.py +52 -0
  40. app/frontend/components/followup_questions.py +33 -0
  41. app/frontend/components/insights_panel.py +109 -0
  42. app/frontend/components/sidebar.py +37 -0
  43. app/frontend/pages/__init__.py +0 -0
  44. app/frontend/pages/admin.py +43 -0
  45. app/frontend/pages/api_docs.py +400 -0
  46. app/frontend/pages/customer_details.py +416 -0
  47. app/frontend/pages/dashboard.py +203 -0
  48. app/frontend/pages/dispute_details.py +544 -0
  49. app/frontend/pages/dispute_form.py +259 -0
  50. 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
- title: AI Financial Dispute Automation
3
- emoji: 👁
4
- colorFrom: red
5
- colorTo: yellow
6
- sdk: docker
7
- pinned: false
8
- license: mit
9
- ---
10
-
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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()