import uuid from decimal import Decimal from django.db import models from django.contrib.auth.models import User from django.utils import timezone class ChatRoom(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) name = models.CharField(max_length=100) description = models.TextField(blank=True) invite_code = models.CharField(max_length=12, unique=True, blank=True) created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name='created_rooms') created_at = models.DateTimeField(auto_now_add=True) def save(self, *args, **kwargs): if not self.invite_code: self.invite_code = uuid.uuid4().hex[:8].upper() super().save(*args, **kwargs) def __str__(self): return self.name @property def member_count(self): return self.memberships.filter(is_active=True).count() class Membership(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='memberships') room = models.ForeignKey(ChatRoom, on_delete=models.CASCADE, related_name='memberships') joined_at = models.DateTimeField(auto_now_add=True) is_active = models.BooleanField(default=True) is_admin = models.BooleanField(default=False) class Meta: unique_together = ('user', 'room') def __str__(self): return f"{self.user.username} in {self.room.name}" class Message(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) room = models.ForeignKey(ChatRoom, on_delete=models.CASCADE, related_name='messages') sender = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name='messages') content = models.TextField() msg_type = models.CharField(max_length=20, default='chat', choices=[('chat', 'Chat'), ('system', 'System'), ('event', 'Event'), ('expense', 'Expense')]) created_at = models.DateTimeField(auto_now_add=True) class Meta: ordering = ['created_at'] def __str__(self): return f"{self.sender}: {self.content[:50]}" class Event(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) room = models.ForeignKey(ChatRoom, on_delete=models.CASCADE, related_name='events') created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name='created_events') title = models.CharField(max_length=200) description = models.TextField(blank=True) location = models.CharField(max_length=200, blank=True) event_date = models.DateTimeField(null=True, blank=True) created_at = models.DateTimeField(auto_now_add=True) is_settled = models.BooleanField(default=False) class Meta: ordering = ['-created_at'] def __str__(self): return self.title @property def total_amount(self): return self.expenses.aggregate(total=models.Sum('amount'))['total'] or Decimal('0') def compute_balances(self): """Returns dict: {user_id: net_balance} positive=owed, negative=owes""" balances = {} for expense in self.expenses.prefetch_related('splits'): payer_id = expense.paid_by_id balances[payer_id] = balances.get(payer_id, Decimal('0')) + expense.amount for split in expense.splits.all(): uid = split.user_id balances[uid] = balances.get(uid, Decimal('0')) - split.amount return balances def compute_settlements(self): """Simplify debts: returns list of (debtor_user, creditor_user, amount)""" balances = self.compute_balances() # Fetch users user_ids = list(balances.keys()) users = {u.id: u for u in User.objects.filter(id__in=user_ids)} debtors = sorted( [(uid, -bal) for uid, bal in balances.items() if bal < -Decimal('0.01')], key=lambda x: -x[1] ) creditors = sorted( [(uid, bal) for uid, bal in balances.items() if bal > Decimal('0.01')], key=lambda x: -x[1] ) settlements = [] i, j = 0, 0 debtors = [[uid, amt] for uid, amt in debtors] creditors = [[uid, amt] for uid, amt in creditors] while i < len(debtors) and j < len(creditors): debtor_id, debt = debtors[i] creditor_id, credit = creditors[j] amount = min(debt, credit) if amount > Decimal('0.01'): settlements.append((users.get(debtor_id), users.get(creditor_id), round(amount, 2))) debtors[i][1] -= amount creditors[j][1] -= amount if debtors[i][1] < Decimal('0.01'): i += 1 if creditors[j][1] < Decimal('0.01'): j += 1 return settlements class Expense(models.Model): SPLIT_EQUAL = 'equal' SPLIT_CUSTOM = 'custom' SPLIT_CHOICES = [(SPLIT_EQUAL, 'Equal'), (SPLIT_CUSTOM, 'Custom')] id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name='expenses') description = models.CharField(max_length=200) amount = models.DecimalField(max_digits=10, decimal_places=2) paid_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name='paid_expenses') split_type = models.CharField(max_length=10, choices=SPLIT_CHOICES, default=SPLIT_EQUAL) created_at = models.DateTimeField(auto_now_add=True) class Meta: ordering = ['-created_at'] def __str__(self): return f"{self.description}: ${self.amount}" class ExpenseSplit(models.Model): expense = models.ForeignKey(Expense, on_delete=models.CASCADE, related_name='splits') user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='expense_splits') amount = models.DecimalField(max_digits=10, decimal_places=2) class Meta: unique_together = ('expense', 'user') def __str__(self): return f"{self.user.username}: ${self.amount}" class UserProfile(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile') display_name = models.CharField(max_length=100, blank=True) avatar_color = models.CharField(max_length=7, default='#6366f1') def __str__(self): return f"Profile({self.user.username})" @property def name(self): return self.display_name or self.user.get_full_name() or self.user.username def get_avatar_initials(self): name = self.name parts = name.split() if len(parts) >= 2: return (parts[0][0] + parts[-1][0]).upper() return name[:2].upper()