LogicGoInfotechSpaces commited on
Commit
1b002e5
·
verified ·
1 Parent(s): 0887c48

Update app/services.py

Browse files
Files changed (1) hide show
  1. app/services.py +184 -28
app/services.py CHANGED
@@ -22,11 +22,12 @@ CURRENCY_SYMBOLS: Dict[str, str] = {
22
 
23
  @dataclass(frozen=True)
24
  class MonthlySummary:
25
- """Aggregate spending for a single month and category."""
26
 
27
  year: int
28
  month: int
29
  category: str
 
30
  total: float
31
 
32
  @property
@@ -35,15 +36,15 @@ class MonthlySummary:
35
 
36
 
37
  def _bucket_transactions(transactions: Iterable[Transaction]) -> List[MonthlySummary]:
38
- """Aggregate transactions per month and category."""
39
- buckets: Dict[Tuple[int, int, str], float] = defaultdict(float)
40
  for txn in transactions:
41
- key = (txn.timestamp.year, txn.timestamp.month, txn.category)
42
  buckets[key] += txn.amount
43
 
44
  summaries = [
45
- MonthlySummary(year=year, month=month, category=category, total=round(total, 2))
46
- for (year, month, category), total in buckets.items()
47
  ]
48
  logger.debug("Created %d monthly summaries", len(summaries))
49
  return summaries
@@ -73,9 +74,10 @@ def generate_insights(transactions: Iterable[Transaction]) -> Tuple[List[Insight
73
  summaries.sort(key=_month_key, reverse=True)
74
  recent_months = summaries[:12] # guardrail, though we only surface up to 3 months
75
 
76
- grouped: Dict[str, List[MonthlySummary]] = defaultdict(list)
 
77
  for summary in recent_months:
78
- grouped[summary.category].append(summary)
79
 
80
  latest_month = max(recent_months, key=_month_key)
81
  latest_month_dt = _month_key(latest_month)
@@ -86,9 +88,7 @@ def generate_insights(transactions: Iterable[Transaction]) -> Tuple[List[Insight
86
  evaluated_months.append((latest_month_dt - relativedelta(months=offset)).strftime("%Y-%m"))
87
  evaluated_months = sorted(set(evaluated_months))
88
 
89
- currency = txns[0].currency
90
-
91
- for category, entries in grouped.items():
92
  entries.sort(key=_month_key, reverse=True)
93
  current = entries[0]
94
  history = entries[1:3]
@@ -116,26 +116,30 @@ def generate_insights(transactions: Iterable[Transaction]) -> Tuple[List[Insight
116
  )
117
  )
118
 
119
- # Additional highlight: biggest month-over-month change regardless of category
120
- monthly_totals: Dict[str, float] = defaultdict(float)
 
121
  for summary in summaries:
122
- monthly_totals[summary.iso_month] += summary.total
123
-
124
- sorted_months = sorted(monthly_totals.items(), reverse=True)
125
- if len(sorted_months) >= 2:
126
- latest_label, latest_total = sorted_months[0]
127
- prev_label, prev_total = sorted_months[1]
128
- delta = latest_total - prev_total
129
- if abs(delta) >= 1:
130
- descriptor = "more" if delta > 0 else "less"
131
- insights.append(
132
- Insight(
133
- message=(
134
- f"You spent {_format_currency(abs(delta), currency)} {descriptor} in {latest_label} compared "
135
- f"to {prev_label}. Consider reviewing large outliers."
 
 
 
 
136
  )
137
  )
138
- )
139
 
140
  if not insights:
141
  insights.append(
@@ -145,3 +149,155 @@ def generate_insights(transactions: Iterable[Transaction]) -> Tuple[List[Insight
145
  logger.info("Generated %d insights", len(insights))
146
  return insights, evaluated_months
147
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
23
  @dataclass(frozen=True)
24
  class MonthlySummary:
25
+ """Aggregate spending for a single month, category, and currency."""
26
 
27
  year: int
28
  month: int
29
  category: str
30
+ currency: str
31
  total: float
32
 
33
  @property
 
36
 
37
 
38
  def _bucket_transactions(transactions: Iterable[Transaction]) -> List[MonthlySummary]:
39
+ """Aggregate transactions per month, category, and currency."""
40
+ buckets: Dict[Tuple[int, int, str, str], float] = defaultdict(float)
41
  for txn in transactions:
42
+ key = (txn.timestamp.year, txn.timestamp.month, txn.category, txn.currency)
43
  buckets[key] += txn.amount
44
 
45
  summaries = [
46
+ MonthlySummary(year=year, month=month, category=category, currency=currency, total=round(total, 2))
47
+ for (year, month, category, currency), total in buckets.items()
48
  ]
49
  logger.debug("Created %d monthly summaries", len(summaries))
50
  return summaries
 
74
  summaries.sort(key=_month_key, reverse=True)
75
  recent_months = summaries[:12] # guardrail, though we only surface up to 3 months
76
 
77
+ # Group by category and currency to handle multi-currency scenarios
78
+ grouped: Dict[Tuple[str, str], List[MonthlySummary]] = defaultdict(list)
79
  for summary in recent_months:
80
+ grouped[(summary.category, summary.currency)].append(summary)
81
 
82
  latest_month = max(recent_months, key=_month_key)
83
  latest_month_dt = _month_key(latest_month)
 
88
  evaluated_months.append((latest_month_dt - relativedelta(months=offset)).strftime("%Y-%m"))
89
  evaluated_months = sorted(set(evaluated_months))
90
 
91
+ for (category, currency), entries in grouped.items():
 
 
92
  entries.sort(key=_month_key, reverse=True)
93
  current = entries[0]
94
  history = entries[1:3]
 
116
  )
117
  )
118
 
119
+ # Additional highlight: biggest month-over-month change per currency
120
+ # Group monthly totals by currency to avoid mixing currencies
121
+ monthly_totals_by_currency: Dict[str, Dict[str, float]] = defaultdict(lambda: defaultdict(float))
122
  for summary in summaries:
123
+ monthly_totals_by_currency[summary.currency][summary.iso_month] += summary.total
124
+
125
+ # Generate insights per currency
126
+ for currency, monthly_totals in monthly_totals_by_currency.items():
127
+ sorted_months = sorted(monthly_totals.items(), reverse=True)
128
+ if len(sorted_months) >= 2:
129
+ latest_label, latest_total = sorted_months[0]
130
+ prev_label, prev_total = sorted_months[1]
131
+ delta = latest_total - prev_total
132
+ if abs(delta) >= 1:
133
+ descriptor = "more" if delta > 0 else "less"
134
+ currency_note = f" (in {currency})" if len(monthly_totals_by_currency) > 1 else ""
135
+ insights.append(
136
+ Insight(
137
+ message=(
138
+ f"You spent {_format_currency(abs(delta), currency)} {descriptor} in {latest_label} compared "
139
+ f"to {prev_label}{currency_note}. Consider reviewing large outliers."
140
+ )
141
  )
142
  )
 
143
 
144
  if not insights:
145
  insights.append(
 
149
  logger.info("Generated %d insights", len(insights))
150
  return insights, evaluated_months
151
 
152
+
153
+
154
+
155
+
156
+
157
+ # from __future__ import annotations
158
+
159
+ # import logging
160
+ # from collections import defaultdict
161
+ # from dataclasses import dataclass
162
+ # from datetime import datetime
163
+ # from typing import Dict, Iterable, List, Tuple
164
+
165
+ # from dateutil.relativedelta import relativedelta
166
+
167
+ # from .schemas import Insight, Transaction
168
+
169
+ # logger = logging.getLogger(__name__)
170
+
171
+ # CURRENCY_SYMBOLS: Dict[str, str] = {
172
+ # "INR": "₹",
173
+ # "USD": "$",
174
+ # "EUR": "€",
175
+ # "GBP": "£",
176
+ # }
177
+
178
+
179
+ # @dataclass(frozen=True)
180
+ # class MonthlySummary:
181
+ # """Aggregate spending for a single month and category."""
182
+
183
+ # year: int
184
+ # month: int
185
+ # category: str
186
+ # total: float
187
+
188
+ # @property
189
+ # def iso_month(self) -> str:
190
+ # return f"{self.year:04d}-{self.month:02d}"
191
+
192
+
193
+ # def _bucket_transactions(transactions: Iterable[Transaction]) -> List[MonthlySummary]:
194
+ # """Aggregate transactions per month and category."""
195
+ # buckets: Dict[Tuple[int, int, str], float] = defaultdict(float)
196
+ # for txn in transactions:
197
+ # key = (txn.timestamp.year, txn.timestamp.month, txn.category)
198
+ # buckets[key] += txn.amount
199
+
200
+ # summaries = [
201
+ # MonthlySummary(year=year, month=month, category=category, total=round(total, 2))
202
+ # for (year, month, category), total in buckets.items()
203
+ # ]
204
+ # logger.debug("Created %d monthly summaries", len(summaries))
205
+ # return summaries
206
+
207
+
208
+ # def _format_currency(amount: float, currency: str) -> str:
209
+ # symbol = CURRENCY_SYMBOLS.get(currency.upper(), "")
210
+ # formatted_amount = f"{amount:,.2f}"
211
+ # return f"{symbol}{formatted_amount}" if symbol else f"{formatted_amount} {currency.upper()}"
212
+
213
+
214
+ # def _month_key(summary: MonthlySummary) -> datetime:
215
+ # return datetime(summary.year, summary.month, 1)
216
+
217
+
218
+ # def generate_insights(transactions: Iterable[Transaction]) -> Tuple[List[Insight], List[str]]:
219
+ # """Create AI-inspired insights from historical spending."""
220
+ # txns = list(transactions)
221
+ # if not txns:
222
+ # logger.info("No transactions provided, returning onboarding insight")
223
+ # return [Insight(message="Add a few expenses to start seeing personalized insights.")], []
224
+
225
+ # summaries = _bucket_transactions(txns)
226
+ # if not summaries:
227
+ # return [Insight(message="No spending data yet. Track expenses to unlock insights.")], []
228
+
229
+ # summaries.sort(key=_month_key, reverse=True)
230
+ # recent_months = summaries[:12] # guardrail, though we only surface up to 3 months
231
+
232
+ # grouped: Dict[str, List[MonthlySummary]] = defaultdict(list)
233
+ # for summary in recent_months:
234
+ # grouped[summary.category].append(summary)
235
+
236
+ # latest_month = max(recent_months, key=_month_key)
237
+ # latest_month_dt = _month_key(latest_month)
238
+ # evaluated_months = []
239
+
240
+ # insights: List[Insight] = []
241
+ # for offset in range(3):
242
+ # evaluated_months.append((latest_month_dt - relativedelta(months=offset)).strftime("%Y-%m"))
243
+ # evaluated_months = sorted(set(evaluated_months))
244
+
245
+ # currency = txns[0].currency
246
+
247
+ # for category, entries in grouped.items():
248
+ # entries.sort(key=_month_key, reverse=True)
249
+ # current = entries[0]
250
+ # history = entries[1:3]
251
+ # if not history:
252
+ # continue
253
+
254
+ # history_avg = sum(e.total for e in history) / len(history)
255
+ # if history_avg == 0:
256
+ # continue
257
+
258
+ # change_pct = ((current.total - history_avg) / history_avg) * 100
259
+ # diff_amount = current.total - history_avg
260
+
261
+ # if abs(change_pct) < 10:
262
+ # continue
263
+
264
+ # trend = "increased" if change_pct > 0 else "decreased"
265
+ # insights.append(
266
+ # Insight(
267
+ # category=category,
268
+ # message=(
269
+ # f"Your {category.lower()} spending {trend} by {abs(change_pct):.0f}% this month "
270
+ # f"versus your prior average, about {_format_currency(abs(diff_amount), currency)} difference."
271
+ # ),
272
+ # )
273
+ # )
274
+
275
+ # # Additional highlight: biggest month-over-month change regardless of category
276
+ # monthly_totals: Dict[str, float] = defaultdict(float)
277
+ # for summary in summaries:
278
+ # monthly_totals[summary.iso_month] += summary.total
279
+
280
+ # sorted_months = sorted(monthly_totals.items(), reverse=True)
281
+ # if len(sorted_months) >= 2:
282
+ # latest_label, latest_total = sorted_months[0]
283
+ # prev_label, prev_total = sorted_months[1]
284
+ # delta = latest_total - prev_total
285
+ # if abs(delta) >= 1:
286
+ # descriptor = "more" if delta > 0 else "less"
287
+ # insights.append(
288
+ # Insight(
289
+ # message=(
290
+ # f"You spent {_format_currency(abs(delta), currency)} {descriptor} in {latest_label} compared "
291
+ # f"to {prev_label}. Consider reviewing large outliers."
292
+ # )
293
+ # )
294
+ # )
295
+
296
+ # if not insights:
297
+ # insights.append(
298
+ # Insight(message="Spending is stable across tracked months. Keep up the steady habits!")
299
+ # )
300
+
301
+ # logger.info("Generated %d insights", len(insights))
302
+ # return insights, evaluated_months
303
+