""" PawCare Data Models All sensitive fields (phone, address, payment info) are AES-256 encrypted at rest using django-encrypted-model-fields (Fernet / cryptography library). """ import uuid from django.db import models from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, BaseUserManager from django.utils import timezone from encrypted_model_fields.fields import EncryptedCharField, EncryptedTextField # ───────────────────────────────────────────────────────────────────────────── # Role Constants # ───────────────────────────────────────────────────────────────────────────── class Role(models.TextChoices): ADMIN = 'admin', 'Admin' USER = 'user', 'User' CAREGIVER = 'caregiver', 'Caregiver' # ───────────────────────────────────────────────────────────────────────────── # Custom User Manager # ───────────────────────────────────────────────────────────────────────────── class UserManager(BaseUserManager): def create_user(self, email, password=None, **extra_fields): if not email: raise ValueError('Email is required.') email = self.normalize_email(email) extra_fields.setdefault('role', Role.USER) user = self.model(email=email, **extra_fields) user.set_password(password) user.save(using=self._db) return user def create_superuser(self, email, password=None, **extra_fields): extra_fields.setdefault('is_staff', True) extra_fields.setdefault('is_superuser', True) extra_fields.setdefault('role', Role.ADMIN) return self.create_user(email, password, **extra_fields) # ───────────────────────────────────────────────────────────────────────────── # User # ───────────────────────────────────────────────────────────────────────────── class User(AbstractBaseUser, PermissionsMixin): """Custom user model — email-based auth with RBAC.""" id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) email = models.EmailField(unique=True, db_index=True) first_name = models.CharField(max_length=100) last_name = models.CharField(max_length=100) role = models.CharField(max_length=20, choices=Role.choices, default=Role.USER) avatar_url = models.URLField(blank=True) # Encrypted PII phone = EncryptedCharField(max_length=255, blank=True) address = EncryptedTextField(blank=True) is_active = models.BooleanField(default=True) is_staff = models.BooleanField(default=False) date_joined = models.DateTimeField(default=timezone.now) last_login = models.DateTimeField(null=True, blank=True) objects = UserManager() USERNAME_FIELD = 'email' REQUIRED_FIELDS = ['first_name', 'last_name'] class Meta: db_table = 'users' verbose_name = 'User' verbose_name_plural = 'Users' ordering = ['-date_joined'] def __str__(self): return f'{self.first_name} {self.last_name} <{self.email}>' @property def full_name(self): return f'{self.first_name} {self.last_name}'.strip() @property def is_admin(self): return self.role == Role.ADMIN @property def is_caregiver(self): return self.role == Role.CAREGIVER # ───────────────────────────────────────────────────────────────────────────── # Service # ───────────────────────────────────────────────────────────────────────────── class Service(models.Model): """Pet care service types offered on the platform.""" id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) name = models.CharField(max_length=100) description = models.TextField(blank=True) icon = models.CharField(max_length=50, blank=True) # emoji / icon name base_price = models.DecimalField(max_digits=8, decimal_places=2) duration_minutes = models.PositiveIntegerField(default=60) is_active = models.BooleanField(default=True) created_at = models.DateTimeField(auto_now_add=True) class Meta: db_table = 'services' ordering = ['name'] def __str__(self): return self.name # ───────────────────────────────────────────────────────────────────────────── # Caregiver # ───────────────────────────────────────────────────────────────────────────── class Caregiver(models.Model): """ Caregiver profile — created by admin only via Django Admin. The linked User account has role = 'caregiver'. """ id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) user = models.OneToOneField( User, on_delete=models.CASCADE, related_name='caregiver_profile' ) # Bio & Professional Info bio = models.TextField(blank=True) years_of_experience = models.PositiveSmallIntegerField(default=0) specializations = models.JSONField(default=list) # ['dogs', 'cats', …] certifications = models.JSONField(default=list) # ['First Aid', …] languages = models.JSONField(default=list, blank=True) # Services & Pricing services = models.ManyToManyField(Service, blank=True, through='CaregiverService') # Location latitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True) longitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True) city = models.CharField(max_length=100, blank=True) country = models.CharField(max_length=100, default='Nepal') # Media — stored on Hugging Face Hub; these are public CDN URLs profile_image_url = models.URLField(blank=True) gallery_images = models.JSONField(default=list) # list of HF Hub URLs # Stats (computed periodically) rating = models.DecimalField(max_digits=3, decimal_places=2, default=0.00) total_reviews = models.PositiveIntegerField(default=0) total_bookings = models.PositiveIntegerField(default=0) # Availability & Status is_available = models.BooleanField(default=True) is_verified = models.BooleanField(default=False) is_featured = models.BooleanField(default=False) background_check_passed = models.BooleanField(default=False) # Encrypted sensitive contact info emergency_contact = EncryptedCharField(max_length=255, blank=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: db_table = 'caregivers' ordering = ['-rating', '-total_bookings'] def __str__(self): return f'{self.user.full_name} ({self.rating}★)' class CaregiverService(models.Model): """Through table — caregiver-specific pricing per service.""" caregiver = models.ForeignKey(Caregiver, on_delete=models.CASCADE) service = models.ForeignKey(Service, on_delete=models.CASCADE) price_per_hour = models.DecimalField(max_digits=8, decimal_places=2) is_active = models.BooleanField(default=True) class Meta: db_table = 'caregiver_services' unique_together = ('caregiver', 'service') def __str__(self): return f'{self.caregiver} — {self.service} @ {self.price_per_hour}/hr' # ───────────────────────────────────────────────────────────────────────────── # Pet # ───────────────────────────────────────────────────────────────────────────── class PetType(models.TextChoices): DOG = 'dog', 'Dog' CAT = 'cat', 'Cat' BIRD = 'bird', 'Bird' FISH = 'fish', 'Fish' RABBIT = 'rabbit', 'Rabbit' OTHER = 'other', 'Other' class Pet(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name='pets') name = models.CharField(max_length=100) pet_type = models.CharField(max_length=20, choices=PetType.choices) breed = models.CharField(max_length=100, blank=True) age_years = models.PositiveSmallIntegerField(null=True, blank=True) weight_kg = models.DecimalField(max_digits=5, decimal_places=2, null=True, blank=True) color = models.CharField(max_length=100, blank=True) image_url = models.URLField(blank=True) # Medical info is_vaccinated = models.BooleanField(default=False) is_neutered = models.BooleanField(default=False) medical_notes = EncryptedTextField(blank=True) # encrypted special_needs = models.TextField(blank=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: db_table = 'pets' ordering = ['name'] def __str__(self): return f'{self.name} ({self.pet_type}) — {self.owner.full_name}' # ───────────────────────────────────────────────────────────────────────────── # Booking # ───────────────────────────────────────────────────────────────────────────── class BookingStatus(models.TextChoices): PENDING = 'pending', 'Pending' CONFIRMED = 'confirmed', 'Confirmed' IN_PROGRESS = 'in_progress', 'In Progress' COMPLETED = 'completed', 'Completed' CANCELLED = 'cancelled', 'Cancelled' DISPUTED = 'disputed', 'Disputed' class Booking(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) user = models.ForeignKey(User, on_delete=models.PROTECT, related_name='bookings') caregiver = models.ForeignKey(Caregiver, on_delete=models.PROTECT, related_name='bookings') pet = models.ForeignKey(Pet, on_delete=models.PROTECT, related_name='bookings') service = models.ForeignKey(Service, on_delete=models.PROTECT) status = models.CharField( max_length=20, choices=BookingStatus.choices, default=BookingStatus.PENDING ) scheduled_start = models.DateTimeField() scheduled_end = models.DateTimeField() actual_start = models.DateTimeField(null=True, blank=True) actual_end = models.DateTimeField(null=True, blank=True) # Pricing price_subtotal = models.DecimalField(max_digits=10, decimal_places=2) price_fees = models.DecimalField(max_digits=10, decimal_places=2, default=0) price_total = models.DecimalField(max_digits=10, decimal_places=2) # Encrypted payment reference payment_reference = EncryptedCharField(max_length=255, blank=True) payment_status = models.CharField(max_length=20, default='pending') # Location snapshot at booking time service_address = EncryptedTextField(blank=True) notes = models.TextField(blank=True) cancellation_reason = models.TextField(blank=True) # Review rating = models.PositiveSmallIntegerField(null=True, blank=True) review = models.TextField(blank=True) reviewed_at = models.DateTimeField(null=True, blank=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: db_table = 'bookings' ordering = ['-created_at'] def __str__(self): return f'Booking #{str(self.id)[:8]} — {self.user.full_name} + {self.caregiver}' # ───────────────────────────────────────────────────────────────────────────── # Messaging # ───────────────────────────────────────────────────────────────────────────── class Conversation(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) participants = models.ManyToManyField(User, related_name='conversations') booking = models.OneToOneField( Booking, on_delete=models.SET_NULL, null=True, blank=True, related_name='conversation' ) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: db_table = 'conversations' ordering = ['-updated_at'] def __str__(self): names = ', '.join(p.full_name for p in self.participants.all()[:2]) return f'Conv [{names}]' class Message(models.Model): class MessageType(models.TextChoices): TEXT = 'text', 'Text' IMAGE = 'image', 'Image' SYSTEM = 'system', 'System' id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) conversation = models.ForeignKey( Conversation, on_delete=models.CASCADE, related_name='messages' ) sender = models.ForeignKey(User, on_delete=models.SET_NULL, null=True) message_type = models.CharField(max_length=10, choices=MessageType.choices, default=MessageType.TEXT) # Content is encrypted at rest content = EncryptedTextField() image_url = models.URLField(blank=True) is_read = models.BooleanField(default=False) read_at = models.DateTimeField(null=True, blank=True) created_at = models.DateTimeField(auto_now_add=True) class Meta: db_table = 'messages' ordering = ['created_at'] def __str__(self): return f'Msg from {self.sender} in {self.conversation}'