Spaces:
Sleeping
Sleeping
| """ | |
| 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}>' | |
| def full_name(self): | |
| return f'{self.first_name} {self.last_name}'.strip() | |
| def is_admin(self): | |
| return self.role == Role.ADMIN | |
| 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}' | |