Update app.py
Browse files
app.py
CHANGED
|
@@ -146,6 +146,112 @@ from utils.api_client import RewardPilotClient
|
|
| 146 |
from utils.llm_explainer import get_llm_explainer
|
| 147 |
import config
|
| 148 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
|
| 150 |
def get_recommendation_with_agent(user_id, merchant, category, amount):
|
| 151 |
import httpx
|
|
@@ -210,60 +316,81 @@ def get_recommendation_with_agent(user_id, merchant, category, amount):
|
|
| 210 |
}
|
| 211 |
card_name = card_name_map.get(card_id, card_id.replace('c_', '').replace('_', ' ').title())
|
| 212 |
|
| 213 |
-
# ========== CARD DATABASE (FALLBACK) ==========
|
| 214 |
-
# If API doesn't provide card_details, use this database
|
| 215 |
-
CARD_DATABASE = {
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
}
|
| 252 |
-
|
| 253 |
-
# ========== GET CARD DETAILS (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 254 |
card_details = result.get('card_details', {})
|
| 255 |
|
| 256 |
if not card_details or not card_details.get('reward_rate'):
|
| 257 |
-
#
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
'
|
| 265 |
-
|
| 266 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 267 |
|
| 268 |
reward_rate_value = card_details.get('reward_rate', 1.0)
|
| 269 |
monthly_cap = card_details.get('monthly_cap', None)
|
|
|
|
| 146 |
from utils.llm_explainer import get_llm_explainer
|
| 147 |
import config
|
| 148 |
|
| 149 |
+
# ===================== CARD DATABASE LOADER =====================
|
| 150 |
+
import json
|
| 151 |
+
import os
|
| 152 |
+
|
| 153 |
+
# Path to cards.json
|
| 154 |
+
CARDS_FILE = os.path.join(os.path.dirname(__file__), "data", "cards.json")
|
| 155 |
+
|
| 156 |
+
def load_card_database() -> dict:
|
| 157 |
+
"""Load card database from local cards.json"""
|
| 158 |
+
try:
|
| 159 |
+
with open(CARDS_FILE, 'r') as f:
|
| 160 |
+
cards = json.load(f)
|
| 161 |
+
print(f"✅ Loaded {len(cards)} cards from database")
|
| 162 |
+
return cards
|
| 163 |
+
except FileNotFoundError:
|
| 164 |
+
print(f"⚠️ cards.json not found at {CARDS_FILE}")
|
| 165 |
+
return {}
|
| 166 |
+
except json.JSONDecodeError as e:
|
| 167 |
+
print(f"❌ Error parsing cards.json: {e}")
|
| 168 |
+
return {}
|
| 169 |
+
|
| 170 |
+
# Load at startup
|
| 171 |
+
CARD_DATABASE = load_card_database()
|
| 172 |
+
|
| 173 |
+
def get_card_details(card_id: str, mcc: str = None) -> dict:
|
| 174 |
+
"""
|
| 175 |
+
Get card details from database
|
| 176 |
+
|
| 177 |
+
Args:
|
| 178 |
+
card_id: Card identifier (e.g., "c_citi_custom_cash")
|
| 179 |
+
mcc: Optional MCC code to get specific reward rate
|
| 180 |
+
|
| 181 |
+
Returns:
|
| 182 |
+
dict: Card details including name, reward rate, caps, etc.
|
| 183 |
+
"""
|
| 184 |
+
if card_id not in CARD_DATABASE:
|
| 185 |
+
print(f"⚠️ Card {card_id} not found in database, using fallback")
|
| 186 |
+
return {
|
| 187 |
+
"name": card_id.replace("c_", "").replace("_", " ").title(),
|
| 188 |
+
"issuer": "Unknown",
|
| 189 |
+
"reward_rate": 1.0,
|
| 190 |
+
"annual_fee": 0,
|
| 191 |
+
"spending_caps": {},
|
| 192 |
+
"benefits": []
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
card = CARD_DATABASE[card_id]
|
| 196 |
+
|
| 197 |
+
# Get reward rate for specific MCC (if provided)
|
| 198 |
+
reward_rate = 1.0
|
| 199 |
+
if mcc and "reward_structure" in card:
|
| 200 |
+
reward_structure = card["reward_structure"]
|
| 201 |
+
|
| 202 |
+
# Check exact MCC match
|
| 203 |
+
if mcc in reward_structure:
|
| 204 |
+
reward_rate = reward_structure[mcc]
|
| 205 |
+
else:
|
| 206 |
+
# Check MCC ranges (e.g., "3001-3999")
|
| 207 |
+
try:
|
| 208 |
+
mcc_int = int(mcc)
|
| 209 |
+
for key, rate in reward_structure.items():
|
| 210 |
+
if "-" in str(key):
|
| 211 |
+
start, end = str(key).split("-")
|
| 212 |
+
if int(start) <= mcc_int <= int(end):
|
| 213 |
+
reward_rate = rate
|
| 214 |
+
break
|
| 215 |
+
except (ValueError, AttributeError):
|
| 216 |
+
pass
|
| 217 |
+
|
| 218 |
+
# Use default if no match found
|
| 219 |
+
if reward_rate == 1.0 and "default" in reward_structure:
|
| 220 |
+
reward_rate = reward_structure["default"]
|
| 221 |
+
|
| 222 |
+
# Extract spending cap info
|
| 223 |
+
spending_caps = card.get("spending_caps", {})
|
| 224 |
+
cap_info = {}
|
| 225 |
+
|
| 226 |
+
if "monthly_bonus" in spending_caps:
|
| 227 |
+
cap_info = {
|
| 228 |
+
"type": "monthly",
|
| 229 |
+
"limit": spending_caps["monthly_bonus"],
|
| 230 |
+
"display": f"${spending_caps['monthly_bonus']}/month"
|
| 231 |
+
}
|
| 232 |
+
elif "quarterly_bonus" in spending_caps:
|
| 233 |
+
cap_info = {
|
| 234 |
+
"type": "quarterly",
|
| 235 |
+
"limit": spending_caps["quarterly_bonus"],
|
| 236 |
+
"display": f"${spending_caps['quarterly_bonus']}/quarter"
|
| 237 |
+
}
|
| 238 |
+
elif "annual_bonus" in spending_caps:
|
| 239 |
+
cap_info = {
|
| 240 |
+
"type": "annual",
|
| 241 |
+
"limit": spending_caps["annual_bonus"],
|
| 242 |
+
"display": f"${spending_caps['annual_bonus']}/year"
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
return {
|
| 246 |
+
"name": card.get("name", "Unknown Card"),
|
| 247 |
+
"issuer": card.get("issuer", "Unknown"),
|
| 248 |
+
"reward_rate": reward_rate,
|
| 249 |
+
"annual_fee": card.get("annual_fee", 0),
|
| 250 |
+
"spending_caps": cap_info,
|
| 251 |
+
"benefits": card.get("benefits", [])
|
| 252 |
+
}
|
| 253 |
+
# ===================== END CARD DATABASE =====================
|
| 254 |
+
|
| 255 |
|
| 256 |
def get_recommendation_with_agent(user_id, merchant, category, amount):
|
| 257 |
import httpx
|
|
|
|
| 316 |
}
|
| 317 |
card_name = card_name_map.get(card_id, card_id.replace('c_', '').replace('_', ' ').title())
|
| 318 |
|
| 319 |
+
# # ========== CARD DATABASE (FALLBACK) ==========
|
| 320 |
+
# # If API doesn't provide card_details, use this database
|
| 321 |
+
# CARD_DATABASE = {
|
| 322 |
+
# 'c_citi_custom_cash': {
|
| 323 |
+
# 'reward_rate': 5.0,
|
| 324 |
+
# 'monthly_cap': 500,
|
| 325 |
+
# 'base_rate': 1.0,
|
| 326 |
+
# 'annual_fee': 0,
|
| 327 |
+
# 'cap_type': 'monthly'
|
| 328 |
+
# },
|
| 329 |
+
# 'c_amex_gold': {
|
| 330 |
+
# 'reward_rate': 4.0,
|
| 331 |
+
# 'annual_cap': 25000,
|
| 332 |
+
# 'base_rate': 1.0,
|
| 333 |
+
# 'annual_fee': 250,
|
| 334 |
+
# 'cap_type': 'annual'
|
| 335 |
+
# },
|
| 336 |
+
# 'c_chase_sapphire_reserve': {
|
| 337 |
+
# 'reward_rate': 3.0,
|
| 338 |
+
# 'monthly_cap': None,
|
| 339 |
+
# 'base_rate': 3.0,
|
| 340 |
+
# 'annual_fee': 550,
|
| 341 |
+
# 'cap_type': 'none'
|
| 342 |
+
# },
|
| 343 |
+
# 'c_chase_freedom_unlimited': {
|
| 344 |
+
# 'reward_rate': 1.5,
|
| 345 |
+
# 'monthly_cap': None,
|
| 346 |
+
# 'base_rate': 1.5,
|
| 347 |
+
# 'annual_fee': 0,
|
| 348 |
+
# 'cap_type': 'none'
|
| 349 |
+
# },
|
| 350 |
+
# 'c_chase_sapphire_preferred': {
|
| 351 |
+
# 'reward_rate': 2.0,
|
| 352 |
+
# 'monthly_cap': None,
|
| 353 |
+
# 'base_rate': 2.0,
|
| 354 |
+
# 'annual_fee': 95,
|
| 355 |
+
# 'cap_type': 'none'
|
| 356 |
+
# }
|
| 357 |
+
# }
|
| 358 |
+
|
| 359 |
+
# ========== GET CARD DETAILS (FROM cards.json) ==========
|
| 360 |
+
# Get MCC from transaction
|
| 361 |
+
transaction_mcc = result.get('mcc', MCC_CATEGORIES.get(category, "5999"))
|
| 362 |
+
|
| 363 |
+
# Load card details from our database
|
| 364 |
+
card_details_from_db = get_card_details(card_id, transaction_mcc)
|
| 365 |
+
|
| 366 |
+
# Use API card_details if available, otherwise use our database
|
| 367 |
card_details = result.get('card_details', {})
|
| 368 |
|
| 369 |
if not card_details or not card_details.get('reward_rate'):
|
| 370 |
+
# Convert our database format to the format expected by the rest of the code
|
| 371 |
+
reward_structure = CARD_DATABASE.get(card_id, {}).get('reward_structure', {})
|
| 372 |
+
|
| 373 |
+
# Get reward rate for this MCC
|
| 374 |
+
if transaction_mcc in reward_structure:
|
| 375 |
+
reward_rate_value = reward_structure[transaction_mcc]
|
| 376 |
+
else:
|
| 377 |
+
reward_rate_value = reward_structure.get('default', 1.0)
|
| 378 |
+
|
| 379 |
+
# Get spending caps
|
| 380 |
+
spending_caps_db = CARD_DATABASE.get(card_id, {}).get('spending_caps', {})
|
| 381 |
+
|
| 382 |
+
card_details = {
|
| 383 |
+
'reward_rate': reward_rate_value,
|
| 384 |
+
'monthly_cap': spending_caps_db.get('monthly_bonus'),
|
| 385 |
+
'annual_cap': spending_caps_db.get('annual_bonus'),
|
| 386 |
+
'quarterly_cap': spending_caps_db.get('quarterly_bonus'),
|
| 387 |
+
'base_rate': reward_structure.get('default', 1.0),
|
| 388 |
+
'annual_fee': CARD_DATABASE.get(card_id, {}).get('annual_fee', 0),
|
| 389 |
+
'cap_type': 'monthly' if 'monthly_bonus' in spending_caps_db else
|
| 390 |
+
'annual' if 'annual_bonus' in spending_caps_db else
|
| 391 |
+
'quarterly' if 'quarterly_bonus' in spending_caps_db else 'none'
|
| 392 |
+
}
|
| 393 |
+
print(f"✅ Using cards.json details for {card_id}")
|
| 394 |
|
| 395 |
reward_rate_value = card_details.get('reward_rate', 1.0)
|
| 396 |
monthly_cap = card_details.get('monthly_cap', None)
|