sammy786 commited on
Commit
112cabd
·
verified ·
1 Parent(s): 4d93085

Update utils/api_client.py

Browse files
Files changed (1) hide show
  1. utils/api_client.py +324 -151
utils/api_client.py CHANGED
@@ -1,185 +1,358 @@
1
- """API client for RewardPilot Orchestrator"""
 
 
2
 
3
- import httpx
4
- from typing import Dict, Optional
5
- from config import ORCHESTRATOR_URL
6
- from typing import Any, Dict, List, Optional, Tuple
7
  import logging
8
 
9
- # Create logger
10
  logger = logging.getLogger(__name__)
11
 
 
12
  class RewardPilotClient:
13
- """Client for interacting with RewardPilot Orchestrator"""
14
 
15
- def __init__(self, base_url: str = ORCHESTRATOR_URL):
16
- self.base_url = base_url.rstrip('/')
17
- self.timeout = 30.0
 
 
 
 
 
 
18
 
19
- async def get_recommendation(
20
  self,
21
  user_id: str,
22
  merchant: str,
23
- mcc: str,
24
- amount_usd: float,
25
- transaction_date: Optional[str] = None
26
  ) -> Dict:
27
- """Get card recommendation from orchestrator"""
28
- async with httpx.AsyncClient(timeout=self.timeout) as client:
29
- try:
30
- payload = {
31
- "user_id": user_id,
32
- "merchant": merchant,
33
- "mcc": mcc,
34
- "amount_usd": amount_usd
35
- }
36
- if transaction_date:
37
- payload["transaction_date"] = transaction_date
38
-
39
- response = await client.post(
40
- f"{self.base_url}/recommend",
41
- json=payload
42
- )
43
- response.raise_for_status()
44
- return response.json()
45
- except httpx.HTTPError as e:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  return {
47
- "error": True,
48
- "message": f"API Error: {str(e)}"
49
  }
50
- except Exception as e:
 
51
  return {
52
- "error": True,
53
- "message": f"Unexpected error: {str(e)}"
54
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
 
56
- def get_recommendation_sync(
57
- self,
58
- user_id: str,
59
- merchant: str,
60
- mcc: str,
61
- amount_usd: float,
62
- transaction_date: Optional[str] = None
63
- ) -> Dict:
64
- """Synchronous version for Gradio"""
65
- import asyncio
66
- try:
67
- loop = asyncio.get_event_loop()
68
- except RuntimeError:
69
- loop = asyncio.new_event_loop()
70
- asyncio.set_event_loop(loop)
71
-
72
- return loop.run_until_complete(
73
- self.get_recommendation(
74
- user_id, merchant, mcc, amount_usd, transaction_date
75
- )
76
- )
77
-
78
- def get_user_analytics(self, user_id: str) -> Dict[str, Any]:
79
  """
80
- Fetch user analytics including spending patterns and optimization metrics
81
 
82
  Args:
83
  user_id: User identifier
84
 
85
  Returns:
86
- Dictionary containing analytics data
87
  """
 
88
  try:
89
- # Option 1: If you have a dedicated analytics endpoint
90
- response = httpx.get(
91
  f"{self.orchestrator_url}/analytics/{user_id}",
92
- timeout=30.0
93
  )
94
- response.raise_for_status()
95
- return response.json()
96
 
97
- except Exception as e:
98
- logger.error(f"Analytics fetch failed: {e}")
99
- # Return mock data as fallback
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  return self._get_mock_analytics(user_id)
 
 
 
 
 
 
101
 
102
- def _get_mock_analytics(self, user_id: str) -> Dict[str, Any]:
103
- """Generate mock analytics data for demo purposes"""
104
- import random
105
-
106
- # Different data per user for variety
107
- user_multipliers = {
108
- "u_alice": 1.2,
109
- "u_bob": 0.9,
110
- "u_charlie": 1.5,
 
 
 
 
111
  }
112
- multiplier = user_multipliers.get(user_id, 1.0)
113
 
114
- return {
115
- "user_id": user_id,
116
- "annual_savings": round(342 * multiplier, 2),
117
- "rate_increase": round(23 * multiplier, 1),
118
- "optimized_transactions": int(156 * multiplier),
119
- "optimization_score": min(100, int(87 * multiplier)),
120
- "category_breakdown": [
121
- {
122
- "category": "🛒 Groceries",
123
- "monthly_spend": round(450 * multiplier, 2),
124
- "best_card": "Amex Gold",
125
- "rewards": round(27 * multiplier, 2),
126
- "rate": "6%"
127
- },
128
- {
129
- "category": "🍽️ Restaurants",
130
- "monthly_spend": round(320 * multiplier, 2),
131
- "best_card": "Amex Gold",
132
- "rewards": round(12.8 * multiplier, 2),
133
- "rate": "4%"
134
- },
135
- {
136
- "category": "⛽ Gas",
137
- "monthly_spend": round(180 * multiplier, 2),
138
- "best_card": "Costco Visa",
139
- "rewards": round(7.2 * multiplier, 2),
140
- "rate": "4%"
141
- },
142
- {
143
- "category": "✈️ Travel",
144
- "monthly_spend": round(850 * multiplier, 2),
145
- "best_card": "Sapphire Reserve",
146
- "rewards": round(42.5 * multiplier, 2),
147
- "rate": "5%"
148
- },
149
- {
150
- "category": "🎬 Entertainment",
151
- "monthly_spend": round(125 * multiplier, 2),
152
- "best_card": "Freedom Unlimited",
153
- "rewards": round(1.88 * multiplier, 2),
154
- "rate": "1.5%"
155
- },
156
- {
157
- "category": "🏪 Online Shopping",
158
- "monthly_spend": round(280 * multiplier, 2),
159
- "best_card": "Amazon Prime",
160
- "rewards": round(16.8 * multiplier, 2),
161
- "rate": "6%"
162
  }
163
- ],
164
- "total_monthly_spend": round(2205 * multiplier, 2),
165
- "total_monthly_rewards": round(108.18 * multiplier, 2),
166
- "average_rate": round(4.9, 1),
167
- "top_categories": [
168
- {"name": "✈️ Travel", "amount": round(850 * multiplier, 2), "change": "+45%"},
169
- {"name": "🛒 Groceries", "amount": round(450 * multiplier, 2), "change": "+12%"},
170
- {"name": "🍽️ Restaurants", "amount": round(320 * multiplier, 2), "change": "-5%"}
171
- ],
172
- "ytd_rewards": round(1298.16 * multiplier, 2),
173
- "ytd_potential": round(1640 * multiplier, 2),
174
- "money_left": round(341.84 * multiplier, 2),
175
- "forecast": {
176
- "next_month_spend": round(2350 * multiplier, 2),
177
- "next_month_rewards": round(115.25 * multiplier, 2),
178
- "cards_to_watch": ["Amex Gold (dining cap)", "Freedom (quarterly bonus)"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
  },
180
- "recommendations": [
181
- "💳 Use Chase Freedom for groceries in Q4 (5% back)",
182
- "⚠️ Monitor Amex Gold dining spend (cap at $2,000)",
183
- "🎯 Book holiday travel with Sapphire Reserve for 5x points"
184
- ]
185
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ API Client for RewardPilot Orchestrator Service
3
+ """
4
 
5
+ import requests
6
+ from typing import Dict, Optional, List
 
 
7
  import logging
8
 
9
+ logging.basicConfig(level=logging.INFO)
10
  logger = logging.getLogger(__name__)
11
 
12
+
13
  class RewardPilotClient:
14
+ """Client for interacting with RewardPilot microservices"""
15
 
16
+ def __init__(self, orchestrator_url: str = "http://localhost:8000"):
17
+ """
18
+ Initialize API client
19
+
20
+ Args:
21
+ orchestrator_url: Base URL for orchestrator service
22
+ """
23
+ self.orchestrator_url = orchestrator_url.rstrip('/')
24
+ self.timeout = 10 # seconds
25
 
26
+ def get_recommendation(
27
  self,
28
  user_id: str,
29
  merchant: str,
30
+ category: str,
31
+ amount: float,
32
+ mcc: Optional[str] = None
33
  ) -> Dict:
34
+ """
35
+ Get card recommendation for a transaction
36
+
37
+ Args:
38
+ user_id: User identifier
39
+ merchant: Merchant name
40
+ category: Transaction category (e.g., "Groceries", "Dining")
41
+ amount: Transaction amount in USD
42
+ mcc: Optional Merchant Category Code
43
+
44
+ Returns:
45
+ Dictionary with recommendation data
46
+ """
47
+
48
+ # Map category to MCC if not provided
49
+ if not mcc:
50
+ mcc = self._category_to_mcc(category)
51
+
52
+ payload = {
53
+ "user_id": user_id,
54
+ "merchant": merchant,
55
+ "category": category,
56
+ "mcc": mcc,
57
+ "amount_usd": amount
58
+ }
59
+
60
+ try:
61
+ response = requests.post(
62
+ f"{self.orchestrator_url}/recommend",
63
+ json=payload,
64
+ timeout=self.timeout
65
+ )
66
+
67
+ if response.status_code == 200:
68
+ data = response.json()
69
+ logger.info(f"✅ Got recommendation for {merchant}: {data.get('recommended_card', 'N/A')}")
70
  return {
71
+ "success": True,
72
+ "data": data
73
  }
74
+ else:
75
+ logger.error(f"❌ API error: {response.status_code}")
76
  return {
77
+ "success": False,
78
+ "error": f"API returned status {response.status_code}"
79
  }
80
+
81
+ except requests.exceptions.Timeout:
82
+ logger.error("❌ Request timeout")
83
+ return {
84
+ "success": False,
85
+ "error": "Request timed out. Service may be unavailable."
86
+ }
87
+ except requests.exceptions.ConnectionError:
88
+ logger.error("❌ Connection error")
89
+ # Return mock data for development/demo
90
+ return self._get_mock_recommendation(user_id, merchant, category, amount)
91
+ except Exception as e:
92
+ logger.error(f"❌ Unexpected error: {e}")
93
+ return {
94
+ "success": False,
95
+ "error": str(e)
96
+ }
97
 
98
+ def get_user_analytics(self, user_id: str) -> Dict:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  """
100
+ Get analytics for a user
101
 
102
  Args:
103
  user_id: User identifier
104
 
105
  Returns:
106
+ Dictionary with analytics data
107
  """
108
+
109
  try:
110
+ response = requests.get(
 
111
  f"{self.orchestrator_url}/analytics/{user_id}",
112
+ timeout=self.timeout
113
  )
 
 
114
 
115
+ if response.status_code == 200:
116
+ data = response.json()
117
+ logger.info(f"✅ Got analytics for {user_id}")
118
+ return {
119
+ "success": True,
120
+ "data": data
121
+ }
122
+ else:
123
+ logger.error(f"❌ API error: {response.status_code}")
124
+ return {
125
+ "success": False,
126
+ "error": f"API returned status {response.status_code}"
127
+ }
128
+
129
+ except requests.exceptions.ConnectionError:
130
+ logger.error("❌ Connection error")
131
+ # Return mock data for development/demo
132
  return self._get_mock_analytics(user_id)
133
+ except Exception as e:
134
+ logger.error(f"❌ Unexpected error: {e}")
135
+ return {
136
+ "success": False,
137
+ "error": str(e)
138
+ }
139
 
140
+ def compare_cards(self, card_ids: List[str]) -> Dict:
141
+ """
142
+ Compare multiple credit cards
143
+
144
+ Args:
145
+ card_ids: List of card identifiers
146
+
147
+ Returns:
148
+ Dictionary with comparison data
149
+ """
150
+
151
+ payload = {
152
+ "card_ids": card_ids
153
  }
 
154
 
155
+ try:
156
+ response = requests.post(
157
+ f"{self.orchestrator_url}/compare",
158
+ json=payload,
159
+ timeout=self.timeout
160
+ )
161
+
162
+ if response.status_code == 200:
163
+ return {
164
+ "success": True,
165
+ "data": response.json()
166
+ }
167
+ else:
168
+ return {
169
+ "success": False,
170
+ "error": f"API returned status {response.status_code}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
  }
172
+
173
+ except Exception as e:
174
+ logger.error(f" Error comparing cards: {e}")
175
+ return {
176
+ "success": False,
177
+ "error": str(e)
178
+ }
179
+
180
+ def _category_to_mcc(self, category: str) -> str:
181
+ """Map category name to MCC code"""
182
+
183
+ category_map = {
184
+ "Groceries": "5411",
185
+ "Dining": "5812",
186
+ "Travel": "4511",
187
+ "Gas": "5541",
188
+ "Online Shopping": "5999",
189
+ "Entertainment": "7832",
190
+ "Pharmacy": "5912",
191
+ "Department Store": "5311",
192
+ "Home Improvement": "5211",
193
+ "Utilities": "4900"
194
+ }
195
+
196
+ return category_map.get(category, "0000")
197
+
198
+ def _get_mock_recommendation(
199
+ self,
200
+ user_id: str,
201
+ merchant: str,
202
+ category: str,
203
+ amount: float
204
+ ) -> Dict:
205
+ """
206
+ Generate mock recommendation for demo/development
207
+
208
+ This is used when the orchestrator service is unavailable
209
+ """
210
+
211
+ # Simple rule-based mock logic
212
+ card_rules = {
213
+ "Groceries": {
214
+ "card": "Amex Gold",
215
+ "rate": "4x points",
216
+ "multiplier": 4.0
217
+ },
218
+ "Dining": {
219
+ "card": "Capital One Savor",
220
+ "rate": "4% cashback",
221
+ "multiplier": 4.0
222
  },
223
+ "Travel": {
224
+ "card": "Chase Sapphire Reserve",
225
+ "rate": "3x points",
226
+ "multiplier": 3.0
227
+ },
228
+ "Gas": {
229
+ "card": "Costco Visa",
230
+ "rate": "4% cashback",
231
+ "multiplier": 4.0
232
+ },
233
+ "Online Shopping": {
234
+ "card": "Amazon Prime Card",
235
+ "rate": "5% cashback",
236
+ "multiplier": 5.0
237
+ }
238
+ }
239
+
240
+ rule = card_rules.get(category, {
241
+ "card": "Citi Double Cash",
242
+ "rate": "2% cashback",
243
+ "multiplier": 2.0
244
+ })
245
+
246
+ rewards_earned = amount * (rule["multiplier"] / 100)
247
+ annual_potential = rewards_earned * 12 # Rough estimate
248
+
249
+ logger.warning(f"⚠️ Using mock data for {merchant}")
250
+
251
+ return {
252
+ "success": True,
253
+ "data": {
254
+ "recommended_card": rule["card"],
255
+ "rewards_earned": round(rewards_earned, 2),
256
+ "rewards_rate": rule["rate"],
257
+ "merchant": merchant,
258
+ "category": category,
259
+ "amount": amount,
260
+ "annual_potential": round(annual_potential, 2),
261
+ "optimization_score": 85,
262
+ "reasoning": f"Best card for {category.lower()} purchases",
263
+ "warnings": [],
264
+ "alternatives": [
265
+ {
266
+ "card": "Citi Double Cash",
267
+ "rewards": round(amount * 0.02, 2),
268
+ "rate": "2% cashback"
269
+ }
270
+ ],
271
+ "mock_data": True # Flag to indicate this is mock data
272
+ }
273
+ }
274
+
275
+ def _get_mock_analytics(self, user_id: str) -> Dict:
276
+ """Generate mock analytics for demo/development"""
277
+
278
+ logger.warning(f"⚠️ Using mock analytics for {user_id}")
279
+
280
+ return {
281
+ "success": True,
282
+ "data": {
283
+ "user_id": user_id,
284
+ "total_spending": 3450.75,
285
+ "total_rewards": 142.50,
286
+ "potential_savings": 425.00,
287
+ "optimization_score": 87,
288
+ "optimized_count": 156,
289
+ "total_transactions": 180,
290
+ "top_category": "Groceries",
291
+ "cards": ["Amex Gold", "Chase Sapphire Reserve", "Citi Double Cash"],
292
+ "category_breakdown": [
293
+ {
294
+ "category": "Groceries",
295
+ "spending": 1250.00,
296
+ "rewards": 50.00,
297
+ "transactions": 45
298
+ },
299
+ {
300
+ "category": "Dining",
301
+ "spending": 890.50,
302
+ "rewards": 35.62,
303
+ "transactions": 38
304
+ },
305
+ {
306
+ "category": "Travel",
307
+ "spending": 750.25,
308
+ "rewards": 22.51,
309
+ "transactions": 12
310
+ },
311
+ {
312
+ "category": "Gas",
313
+ "spending": 350.00,
314
+ "rewards": 14.00,
315
+ "transactions": 24
316
+ },
317
+ {
318
+ "category": "Online Shopping",
319
+ "spending": 210.00,
320
+ "rewards": 10.50,
321
+ "transactions": 18
322
+ }
323
+ ],
324
+ "monthly_trends": [
325
+ {"month": "Jan", "spending": 2800, "rewards": 115},
326
+ {"month": "Feb", "spending": 3100, "rewards": 128},
327
+ {"month": "Mar", "spending": 3450, "rewards": 142}
328
+ ],
329
+ "card_performance": [
330
+ {"card": "Amex Gold", "rewards": 65.00, "usage_pct": 45},
331
+ {"card": "Chase Sapphire Reserve", "rewards": 48.50, "usage_pct": 30},
332
+ {"card": "Citi Double Cash", "rewards": 29.00, "usage_pct": 25}
333
+ ],
334
+ "mock_data": True
335
+ }
336
+ }
337
+
338
+ def health_check(self) -> bool:
339
+ """
340
+ Check if orchestrator service is available
341
+
342
+ Returns:
343
+ True if service is healthy, False otherwise
344
+ """
345
+ try:
346
+ response = requests.get(
347
+ f"{self.orchestrator_url}/health",
348
+ timeout=5
349
+ )
350
+ return response.status_code == 200
351
+ except:
352
+ return False
353
+
354
+
355
+ # Convenience function
356
+ def create_client(orchestrator_url: str = "http://localhost:8000") -> RewardPilotClient:
357
+ """Create and return a RewardPilotClient instance"""
358
+ return RewardPilotClient(orchestrator_url)