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