from decimal import Decimal, ROUND_HALF_UP from django.db import models class PriceWatcher(models.Model): INTERVAL_CHOICES = [ (6, 'Every 6 hours'), (12, 'Every 12 hours'), (24, 'Every day'), (48, 'Every 2 days'), (72, 'Every 3 days'), (168, 'Every 7 days'), ] name = models.CharField(max_length=200) url = models.URLField(max_length=2000) css_selector = models.CharField( max_length=500, help_text='CSS selector for the price element, e.g. span.price or .product-price', ) check_interval_hours = models.IntegerField(choices=INTERVAL_CHOICES, default=24) alert_price_threshold = models.DecimalField( max_digits=12, decimal_places=2, null=True, blank=True, help_text='Send an alert when the detected price is at or below this value. Leave blank to disable alerts.', ) recurring_notification = models.BooleanField( default=True, help_text=( 'Keep notifying on every check while the price meets the threshold. ' 'Disable to notify only once (until you dismiss the alert).' ), ) enabled = models.BooleanField(default=True) last_checked_at = models.DateTimeField(null=True, blank=True) initial_price = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True) last_price = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True) created_at = models.DateTimeField(auto_now_add=True) class Meta: ordering = ['name'] def __str__(self): return self.name @property def price_change_direction(self): if self.initial_price is None or self.last_price is None: return None if self.last_price > self.initial_price: return 'up' if self.last_price < self.initial_price: return 'down' return 'flat' @property def price_change_pct(self): if self.initial_price is None or self.last_price is None or self.initial_price == 0: return None pct = ((self.last_price - self.initial_price) / self.initial_price) * Decimal('100') return abs(pct).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP) @property def price_change_symbol(self): direction = self.price_change_direction if direction == 'up': return '↑' if direction == 'down': return '↓' if direction == 'flat': return '→' return '' class PriceSnapshot(models.Model): watcher = models.ForeignKey(PriceWatcher, on_delete=models.CASCADE, related_name='snapshots') price = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True) raw_text = models.CharField(max_length=500, blank=True) error = models.CharField(max_length=500, blank=True) checked_at = models.DateTimeField(auto_now_add=True) class Meta: ordering = ['-checked_at'] def __str__(self): return f'{self.watcher.name} @ {self.checked_at:%Y-%m-%d %H:%M}' class PriceAlert(models.Model): watcher = models.ForeignKey(PriceWatcher, on_delete=models.CASCADE, related_name='alerts') old_price = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True) new_price = models.DecimalField(max_digits=12, decimal_places=2) drop_pct = models.DecimalField(max_digits=5, decimal_places=2, default=0) created_at = models.DateTimeField(auto_now_add=True) dismissed = models.BooleanField(default=False) class Meta: ordering = ['-created_at'] def __str__(self): return f'{self.watcher.name}: ${self.old_price} → ${self.new_price}'