petcare-api / api /models.py
Sameer669
Initial PawCare Django backend with JWT auth, RBAC, audit logging, and HF storage
4f01198
"""
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}'