notify-utils 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
notify_utils/models.py ADDED
@@ -0,0 +1,532 @@
1
+ """
2
+ Modelos de dados para produtos e preços.
3
+ """
4
+
5
+ from dataclasses import dataclass
6
+ from datetime import datetime
7
+ from itertools import product
8
+ from typing import List, Optional
9
+ from enum import Enum
10
+
11
+
12
+ class PriceComparisonStatus(Enum):
13
+ """Status da comparação entre preços."""
14
+ DECREASED = "decreased" # Preço caiu
15
+ INCREASED = "increased" # Preço subiu
16
+ EQUAL = "equal" # Preço igual
17
+ FIRST_PRICE = "first_price" # Primeiro preço (histórico vazio)
18
+
19
+
20
+ class PriceAction(Enum):
21
+ """Ação realizada ao adicionar preço."""
22
+ ADDED = "added" # Novo preço adicionado
23
+ UPDATED = "updated" # Preço existente atualizado (ex: data)
24
+ REJECTED = "rejected" # Não foi aceito
25
+ NONE = "none" # Nenhuma ação (apenas comparação)
26
+
27
+
28
+ class PriceAdditionStrategy(Enum):
29
+ """Estratégia para adição de preços ao histórico."""
30
+ ALWAYS = "always" # Sempre adiciona
31
+ ONLY_DECREASE = "only_decrease" # Só adiciona se preço caiu
32
+ SMART = "smart" # Inteligente (default)
33
+ UPDATE_ON_EQUAL = "update_on_equal" # Atualiza data se preço igual
34
+
35
+
36
+ @dataclass
37
+ class Product:
38
+ """
39
+ Representa um produto com informações básicas.
40
+
41
+ Attributes:
42
+ product_id: Identificador único do produto
43
+ name: Nome do produto
44
+ url: URL do produto (opcional, para link no embed)
45
+ image_url: URL da imagem do produto (opcional, para thumbnail)
46
+ """
47
+ product_id: str
48
+ name: str
49
+ url: Optional[str] = None
50
+ image_url: Optional[str] = None
51
+
52
+
53
+ @dataclass
54
+ class Price:
55
+ """
56
+ Representa um preço em um determinado momento.
57
+
58
+ Attributes:
59
+ value: Valor do preço (sempre em formato decimal, ex: 1299.90)
60
+ date: Data e hora do preço
61
+ currency: Código da moeda (ex: "BRL", "USD")
62
+ source: Fonte do preço (ex: "mercadolivre", "amazon")
63
+ """
64
+ value: float
65
+ date: datetime
66
+ currency: str = "BRL"
67
+ source: Optional[str] = None
68
+
69
+ def __post_init__(self):
70
+ if self.value < 0:
71
+ raise ValueError("Preço não pode ser negativo")
72
+ if not isinstance(self.date, datetime):
73
+ raise TypeError("date deve ser um objeto datetime")
74
+
75
+
76
+ @dataclass
77
+ class ProductWithCurrentPriceAndOldPrice:
78
+ """
79
+ Combina um produto com seu preço atual e antigo.
80
+ Será usado usualmente para ser a entrada e conversões de API.
81
+ """
82
+ product: Product
83
+ current_price: Price
84
+ old_price: Optional[Price] = None
85
+
86
+
87
+ @dataclass
88
+ class ProductWithPriceHistory:
89
+ """
90
+ Combina um produto com seu histórico de preços.
91
+ Usado para análises baseadas em histórico.
92
+
93
+ Attributes:
94
+ product: Objeto Product
95
+ price_history: Objeto PriceHistory
96
+ """
97
+ product: Product
98
+ price_history: 'PriceHistory'
99
+
100
+ @dataclass
101
+ class PriceHistory:
102
+ """
103
+ Representa o histórico de preços de um produto.
104
+
105
+ Attributes:
106
+ product_id: Identificador do produto
107
+ prices: Lista de preços ordenada por data (mais recente primeiro)
108
+ """
109
+ product_id: str
110
+ prices: List[Price]
111
+
112
+ def __post_init__(self):
113
+ # Cria cópia para não modificar a lista original do usuário
114
+ self.prices = self.prices.copy()
115
+ # Ordena preços por data (mais recente primeiro)
116
+ self.prices.sort(key=lambda p: p.date, reverse=True)
117
+
118
+ def get_current_price(self) -> Optional[Price]:
119
+ """Retorna o preço mais recente."""
120
+ return self.prices[0] if self.prices else None
121
+
122
+ def get_previous_price(self) -> Optional[Price]:
123
+ """Retorna o penúltimo preço."""
124
+ return self.prices[1] if len(self.prices) > 1 else None
125
+
126
+ def compare_price(
127
+ self,
128
+ new_price: 'Price',
129
+ min_hours_for_increase: int = 24
130
+ ) -> 'PriceComparisonResult':
131
+ """
132
+ Compara novo preço com histórico SEM modificar.
133
+
134
+ Útil para análise antes de decidir se adiciona.
135
+ Não altera self.prices.
136
+
137
+ Args:
138
+ new_price: Preço a ser comparado
139
+ min_hours_for_increase: Horas mínimas para aceitar aumento
140
+
141
+ Returns:
142
+ PriceComparisonResult com action=NONE
143
+ """
144
+ current = self.get_current_price()
145
+
146
+ # Caso 1: Histórico vazio
147
+ if not current:
148
+ return PriceComparisonResult(
149
+ status=PriceComparisonStatus.FIRST_PRICE,
150
+ current_price=None,
151
+ new_price=new_price,
152
+ value_difference=0.0,
153
+ percentage_difference=0.0,
154
+ time_difference_hours=None,
155
+ should_add=True,
156
+ reason="first price in history",
157
+ action=PriceAction.NONE
158
+ )
159
+
160
+ # Calcular diferenças
161
+ value_diff = new_price.value - current.value
162
+ pct_diff = (value_diff / current.value) * 100 if current.value > 0 else 0.0
163
+ time_diff = (new_price.date - current.date).total_seconds() / 3600 # horas
164
+
165
+ # Determinar status
166
+ if new_price.value < current.value:
167
+ status = PriceComparisonStatus.DECREASED
168
+ should_add = True
169
+ reason = "price decreased"
170
+ elif new_price.value > current.value:
171
+ status = PriceComparisonStatus.INCREASED
172
+ if time_diff >= min_hours_for_increase:
173
+ should_add = True
174
+ reason = f"price increased after {time_diff:.1f} hours"
175
+ else:
176
+ should_add = False
177
+ reason = f"recent increase (only {time_diff:.1f} hours, need {min_hours_for_increase})"
178
+ else: # igual
179
+ status = PriceComparisonStatus.EQUAL
180
+ should_add = False
181
+ reason = "price unchanged"
182
+
183
+ return PriceComparisonResult(
184
+ status=status,
185
+ current_price=current,
186
+ new_price=new_price,
187
+ value_difference=round(value_diff, 2),
188
+ percentage_difference=round(pct_diff, 2),
189
+ time_difference_hours=round(time_diff, 2),
190
+ should_add=should_add,
191
+ reason=reason,
192
+ action=PriceAction.NONE
193
+ )
194
+
195
+ def add_price(
196
+ self,
197
+ price: 'Price',
198
+ strategy: 'PriceAdditionStrategy' = PriceAdditionStrategy.SMART,
199
+ min_hours_for_increase: int = 24
200
+ ) -> 'PriceComparisonResult':
201
+ """
202
+ Adiciona preço ao histórico baseado na estratégia.
203
+
204
+ Args:
205
+ price: Preço a adicionar
206
+ strategy: Estratégia de adição (padrão: SMART)
207
+ min_hours_for_increase: Horas mínimas para aceitar aumento
208
+
209
+ Returns:
210
+ PriceComparisonResult com action apropriada (ADDED, UPDATED, REJECTED)
211
+ """
212
+ # Primeiro compara
213
+ result = self.compare_price(price, min_hours_for_increase)
214
+
215
+ # Aplica estratégia
216
+ if strategy == PriceAdditionStrategy.ALWAYS:
217
+ # Sempre adiciona - cria nova lista
218
+ new_prices = self.prices.copy()
219
+ new_prices.append(price)
220
+ new_prices.sort(key=lambda p: p.date, reverse=True)
221
+ self.prices = new_prices
222
+ result.action = PriceAction.ADDED
223
+ result.affected_price = price
224
+ result.reason = "always strategy"
225
+
226
+ elif strategy == PriceAdditionStrategy.ONLY_DECREASE:
227
+ # Só adiciona se caiu
228
+ if result.status == PriceComparisonStatus.DECREASED or result.status == PriceComparisonStatus.FIRST_PRICE:
229
+ new_prices = self.prices.copy()
230
+ new_prices.append(price)
231
+ new_prices.sort(key=lambda p: p.date, reverse=True)
232
+ self.prices = new_prices
233
+ result.action = PriceAction.ADDED
234
+ result.affected_price = price
235
+ else:
236
+ result.action = PriceAction.REJECTED
237
+ result.reason = "only_decrease strategy: price did not decrease"
238
+
239
+ elif strategy == PriceAdditionStrategy.UPDATE_ON_EQUAL:
240
+ # Se igual: atualiza data do último
241
+ if result.status == PriceComparisonStatus.EQUAL:
242
+ result.previous_price = self.prices[0]
243
+ new_prices = self.prices.copy()
244
+ new_prices[0] = price # Substitui (atualiza data)
245
+ self.prices = new_prices
246
+ result.action = PriceAction.UPDATED
247
+ result.affected_price = price
248
+ result.reason = "price equal, date updated"
249
+ elif result.should_add:
250
+ new_prices = self.prices.copy()
251
+ new_prices.append(price)
252
+ new_prices.sort(key=lambda p: p.date, reverse=True)
253
+ self.prices = new_prices
254
+ result.action = PriceAction.ADDED
255
+ result.affected_price = price
256
+ else:
257
+ result.action = PriceAction.REJECTED
258
+
259
+ else: # SMART (default)
260
+ # Adiciona se should_add=True (queda ou aumento após tempo)
261
+ if result.should_add:
262
+ new_prices = self.prices.copy()
263
+ new_prices.append(price)
264
+ new_prices.sort(key=lambda p: p.date, reverse=True)
265
+ self.prices = new_prices
266
+ result.action = PriceAction.ADDED
267
+ result.affected_price = price
268
+ else:
269
+ result.action = PriceAction.REJECTED
270
+
271
+ return result
272
+
273
+ def update_latest_price_date(self, new_date: datetime) -> bool:
274
+ """
275
+ Atualiza data do preço mais recente sem mudar valor.
276
+
277
+ Útil quando preço está igual mas quer timestamp atualizado.
278
+
279
+ Args:
280
+ new_date: Nova data para o preço mais recente
281
+
282
+ Returns:
283
+ True se atualizado, False se histórico vazio
284
+ """
285
+ if not self.prices:
286
+ return False
287
+
288
+ # Cria novo Price com valor antigo e data nova
289
+ old_price = self.prices[0]
290
+ updated_price = Price(
291
+ value=old_price.value,
292
+ date=new_date,
293
+ currency=old_price.currency,
294
+ source=old_price.source
295
+ )
296
+
297
+ # Cria nova lista ao invés de modificar
298
+ new_prices = self.prices.copy()
299
+ new_prices[0] = updated_price
300
+ self.prices = new_prices
301
+ return True
302
+
303
+ def get_prices_by_currency(self, currency: str) -> List[Price]:
304
+ """Retorna apenas preços de uma determinada moeda."""
305
+ return [p for p in self.prices if p.currency == currency]
306
+
307
+
308
+ @dataclass
309
+ class PriceComparisonResult:
310
+ """
311
+ Resultado completo da comparação de preços.
312
+
313
+ Usado para:
314
+ - Análise de mudança de preço
315
+ - Decisão de adicionar ao histórico
316
+ - Integração com banco de dados
317
+ - Logging e notificações
318
+
319
+ Attributes:
320
+ status: Status da comparação (DECREASED, INCREASED, EQUAL, FIRST_PRICE)
321
+ current_price: Preço atual no histórico (None se vazio)
322
+ new_price: Novo preço sendo comparado
323
+ value_difference: Diferença em valor absoluto (R$)
324
+ percentage_difference: Diferença percentual (%)
325
+ time_difference_hours: Horas desde último preço (None se histórico vazio)
326
+ should_add: Sugestão se deve adicionar ao histórico
327
+ reason: Motivo da decisão (ex: "price decreased", "recent increase")
328
+ action: Ação realizada (ADDED, UPDATED, REJECTED, NONE)
329
+ affected_price: Objeto Price afetado (para persistência no BD)
330
+ previous_price: Estado anterior do preço (se action=UPDATED)
331
+ """
332
+ status: PriceComparisonStatus
333
+ current_price: Optional['Price']
334
+ new_price: 'Price'
335
+ value_difference: float
336
+ percentage_difference: float
337
+ time_difference_hours: Optional[float]
338
+ should_add: bool
339
+ reason: str
340
+ action: PriceAction
341
+ affected_price: Optional['Price'] = None
342
+ previous_price: Optional['Price'] = None
343
+
344
+ def to_dict(self) -> dict:
345
+ """Converte para dicionário (útil para logs/JSON)."""
346
+ return {
347
+ 'status': self.status.value,
348
+ 'current_price_value': self.current_price.value if self.current_price else None,
349
+ 'new_price_value': self.new_price.value,
350
+ 'value_difference': self.value_difference,
351
+ 'percentage_difference': self.percentage_difference,
352
+ 'time_difference_hours': self.time_difference_hours,
353
+ 'should_add': self.should_add,
354
+ 'reason': self.reason,
355
+ 'action': self.action.value,
356
+ 'affected_price_value': self.affected_price.value if self.affected_price else None,
357
+ }
358
+
359
+
360
+ @dataclass
361
+ class DiscountResult:
362
+ """
363
+ Resultado básico de cálculo de desconto.
364
+
365
+ Attributes:
366
+ current_price: Preço atual do produto
367
+ reference_price: Preço de referência usado para comparação
368
+ discount_percentage: Percentual de desconto (positivo = desconto, negativo = aumento)
369
+ discount_absolute: Valor absoluto do desconto
370
+ is_real_discount: Se o desconto é considerado real (percentual > 0)
371
+ """
372
+ current_price: float
373
+ reference_price: float
374
+ discount_percentage: float
375
+ discount_absolute: float
376
+ is_real_discount: bool
377
+
378
+ def to_dict(self) -> dict:
379
+ """Converte para dicionário (compatibilidade)."""
380
+ return {
381
+ 'current_price': self.current_price,
382
+ 'reference_price': self.reference_price,
383
+ 'discount_percentage': self.discount_percentage,
384
+ 'discount_absolute': self.discount_absolute,
385
+ 'is_real_discount': self.is_real_discount
386
+ }
387
+
388
+
389
+ @dataclass
390
+ class HistoryDiscountResult(DiscountResult):
391
+ """
392
+ Resultado de desconto calculado baseado em histórico de preços.
393
+
394
+ Extends DiscountResult com informações sobre o cálculo histórico.
395
+
396
+ Attributes:
397
+ calculation_method: Método usado ('mean' ou 'median')
398
+ period_days: Período em dias usado para cálculo (None = todos os preços)
399
+ samples_count: Quantidade de amostras (preços) usadas no cálculo
400
+ """
401
+ calculation_method: str # 'mean' ou 'median'
402
+ period_days: Optional[int] = None
403
+ samples_count: int = 0
404
+
405
+ def to_dict(self) -> dict:
406
+ """Converte para dicionário (compatibilidade)."""
407
+ base = super().to_dict()
408
+ base.update({
409
+ 'calculation_method': self.calculation_method,
410
+ 'period_days': self.period_days,
411
+ 'samples_count': self.samples_count
412
+ })
413
+ return base
414
+
415
+
416
+ @dataclass
417
+ class DiscountInfo:
418
+ """
419
+ Informações completas de desconto com estratégia de cálculo.
420
+
421
+ Usado pela função get_discount_info() que escolhe automaticamente
422
+ entre usar histórico ou preço anunciado.
423
+
424
+ Attributes:
425
+ current_price: Preço atual do produto
426
+ reference_price: Preço de referência usado (média histórica ou advertised)
427
+ discount_percentage: Percentual de desconto
428
+ discount_absolute: Valor absoluto do desconto
429
+ is_real_discount: Se o desconto é considerado real
430
+ strategy: Estratégia usada ('history' ou 'advertised')
431
+ has_history: Se havia histórico disponível
432
+ calculation_method: Método de cálculo ('mean', 'median' ou 'advertised')
433
+ period_days: Período usado (apenas se strategy='history')
434
+ samples_count: Quantidade de amostras (apenas se strategy='history')
435
+ adjusted_period_days: Período ajustado automaticamente (se auto_adjust_period=True)
436
+ days_since_most_recent: Dias desde o preço mais recente até hoje
437
+ skip_recent_days: Dias mais recentes ignorados (se > 0)
438
+ """
439
+ current_price: float
440
+ reference_price: float
441
+ discount_percentage: float
442
+ discount_absolute: float
443
+ is_real_discount: bool
444
+ strategy: str # 'history' ou 'advertised'
445
+ has_history: bool
446
+ calculation_method: str # 'mean', 'median' ou 'advertised'
447
+ period_days: Optional[int] = None
448
+ samples_count: Optional[int] = None
449
+ adjusted_period_days: Optional[int] = None
450
+ days_since_most_recent: Optional[int] = None
451
+ skip_recent_days: Optional[int] = None
452
+
453
+ def to_dict(self) -> dict:
454
+ """Converte para dicionário (compatibilidade)."""
455
+ return {
456
+ 'current_price': self.current_price,
457
+ 'reference_price': self.reference_price,
458
+ 'discount_percentage': self.discount_percentage,
459
+ 'discount_absolute': self.discount_absolute,
460
+ 'is_real_discount': self.is_real_discount,
461
+ 'strategy': self.strategy,
462
+ 'has_history': self.has_history,
463
+ 'calculation_method': self.calculation_method,
464
+ 'period_days': self.period_days,
465
+ 'samples_count': self.samples_count,
466
+ 'adjusted_period_days': self.adjusted_period_days,
467
+ 'days_since_most_recent': self.days_since_most_recent,
468
+ 'skip_recent_days': self.skip_recent_days
469
+ }
470
+
471
+
472
+ @dataclass
473
+ class PriceTrend:
474
+ """
475
+ Análise detalhada de tendência de preços ao longo do tempo.
476
+
477
+ Compara períodos de tempo para detectar direção, magnitude e
478
+ confiabilidade da tendência.
479
+
480
+ Attributes:
481
+ direction: Direção da tendência ('increasing', 'decreasing', 'stable')
482
+ change_percentage: Percentual de mudança entre períodos (positivo = aumento)
483
+ recent_avg: Preço médio do período recente
484
+ previous_avg: Preço médio do período anterior
485
+ volatility: Volatilidade (desvio padrão) dos preços
486
+ confidence: Nível de confiança ('high', 'medium', 'low') baseado em amostras
487
+ samples_recent: Quantidade de amostras do período recente
488
+ samples_previous: Quantidade de amostras do período anterior
489
+ is_accelerating: Se a tendência está acelerando (mudança > 10%)
490
+ analysis_period_days: Período em dias usado para análise
491
+ """
492
+ direction: str # 'increasing', 'decreasing', 'stable'
493
+ change_percentage: float
494
+ recent_avg: float
495
+ previous_avg: float
496
+ volatility: float
497
+ confidence: str # 'high', 'medium', 'low'
498
+ samples_recent: int
499
+ samples_previous: int
500
+ is_accelerating: bool
501
+ analysis_period_days: int
502
+
503
+ def to_dict(self) -> dict:
504
+ """Converte para dicionário (compatibilidade)."""
505
+ return {
506
+ 'direction': self.direction,
507
+ 'change_percentage': self.change_percentage,
508
+ 'recent_avg': self.recent_avg,
509
+ 'previous_avg': self.previous_avg,
510
+ 'volatility': self.volatility,
511
+ 'confidence': self.confidence,
512
+ 'samples_recent': self.samples_recent,
513
+ 'samples_previous': self.samples_previous,
514
+ 'is_accelerating': self.is_accelerating,
515
+ 'analysis_period_days': self.analysis_period_days
516
+ }
517
+
518
+ def is_stable(self) -> bool:
519
+ """Retorna True se a tendência é estável."""
520
+ return self.direction == 'stable'
521
+
522
+ def is_increasing(self) -> bool:
523
+ """Retorna True se a tendência é de aumento."""
524
+ return self.direction == 'increasing'
525
+
526
+ def is_decreasing(self) -> bool:
527
+ """Retorna True se a tendência é de queda."""
528
+ return self.direction == 'decreasing'
529
+
530
+ def has_high_confidence(self) -> bool:
531
+ """Retorna True se a análise tem alta confiança."""
532
+ return self.confidence == 'high'
@@ -0,0 +1,13 @@
1
+ """
2
+ Notificadores para alertas de preço.
3
+ """
4
+
5
+ from .discord_notifier import DiscordEmbedBuilder
6
+ from .formatters import format_price, format_price_history, get_unique_prices
7
+
8
+ __all__ = [
9
+ "DiscordEmbedBuilder",
10
+ "format_price",
11
+ "format_price_history",
12
+ "get_unique_prices",
13
+ ]
@@ -0,0 +1,127 @@
1
+ """
2
+ Notificador para Discord usando webhooks.
3
+ """
4
+
5
+ from typing import List, Optional, TYPE_CHECKING
6
+ from datetime import datetime
7
+
8
+ try:
9
+ from discord_webhook import DiscordWebhook, DiscordEmbed
10
+ DISCORD_AVAILABLE = True
11
+ except ImportError:
12
+ DISCORD_AVAILABLE = False
13
+ if TYPE_CHECKING:
14
+ from discord_webhook import DiscordEmbed
15
+
16
+ from ..models import Product, Price, DiscountInfo
17
+ from .formatters import format_price, format_price_history
18
+
19
+
20
+ class DiscordEmbedBuilder:
21
+
22
+ def build_embed(
23
+ self,
24
+ product: Product,
25
+ discount_info: DiscountInfo,
26
+ price_history: List[Price]
27
+ ) -> "DiscordEmbed":
28
+ """
29
+ Constrói o embed Discord com informações do alerta.
30
+
31
+ Args:
32
+ product: Produto
33
+ discount_info: Informações de desconto
34
+ price_history: Histórico de preços
35
+
36
+ Returns:
37
+ DiscordEmbed configurado
38
+ """
39
+ # Título
40
+ title = f"{discount_info.discount_percentage:.2f}% - {product.name}"
41
+
42
+ # Cor baseada no desconto
43
+ color = self._get_embed_color(discount_info.discount_percentage)
44
+
45
+ # Cria embed
46
+ embed = DiscordEmbed(
47
+ title=title,
48
+ # color=color
49
+ )
50
+
51
+ # Adiciona URL se disponível
52
+ if product.url:
53
+ embed.url = product.url
54
+
55
+ # Campo: Preço Atual
56
+ embed.add_embed_field(
57
+ name="Preco Atual",
58
+ value=format_price(discount_info.current_price),
59
+ inline=True
60
+ )
61
+
62
+ # Campo: Preço de Referência
63
+ ref_label = "mediana" if discount_info.strategy == "history" else "preco de"
64
+ embed.add_embed_field(
65
+ name="Preco de Referencia",
66
+ value=f"{format_price(discount_info.reference_price)} ({ref_label})",
67
+ inline=True
68
+ )
69
+
70
+ # Campo: Desconto (R$)
71
+ embed.add_embed_field(
72
+ name="Desconto (R$)",
73
+ value=format_price(abs(discount_info.discount_absolute)),
74
+ inline=True
75
+ )
76
+
77
+ # Campo: Desconto (%)
78
+ embed.add_embed_field(
79
+ name="Desconto (%)",
80
+ value=f"{discount_info.discount_percentage:.2f}%",
81
+ inline=True
82
+ )
83
+
84
+ # Campo: Histórico (últimos 5)
85
+ embed.add_embed_field(
86
+ name="Historico (ultimos 5)",
87
+ value=format_price_history(price_history, limit=5),
88
+ inline=False
89
+ )
90
+
91
+ # Thumbnail (imagem do produto)
92
+ if product.image_url:
93
+ embed.set_thumbnail(url=product.image_url)
94
+
95
+ # Footer com data/hora
96
+ # embed.set_footer(text=f"Notificacao gerada em {datetime.now().strftime('%d/%m/%Y as %H:%M')}")
97
+
98
+ # Timestamp
99
+ embed.set_timestamp()
100
+
101
+ return embed
102
+
103
+ def _get_embed_color(self, discount_percentage: float) -> int:
104
+ """
105
+ Retorna cor do embed baseado no percentual de desconto.
106
+
107
+ Args:
108
+ discount_percentage: Percentual de desconto (positivo = desconto, negativo = aumento)
109
+
110
+ Returns:
111
+ Cor em formato hexadecimal (int)
112
+ """
113
+ if discount_percentage >= 20.0:
114
+ # Verde: desconto >= 20%
115
+ return 0x00FF00
116
+ elif discount_percentage >= 10.0:
117
+ # Amarelo: desconto entre 10-19%
118
+ return 0xFFFF00
119
+ elif discount_percentage >= 1.0:
120
+ # Azul: desconto entre 1-9%
121
+ return 0x0099FF
122
+ elif discount_percentage < 0:
123
+ # Vermelho: aumento de preço
124
+ return 0xFF0000
125
+ else:
126
+ # Cinza: estável (< 1%)
127
+ return 0x808080