Summary of what I changed and added:
Added initial migrations for accounts, salons, bookings, payments. 0001_initial.py 0001_initial.py 0001_initial.py 0001_initial.py Added demo seeding command. seed_demo.py OTP provider abstraction now includes Twilio + Unifonic adapter scaffolds (not implemented yet, they raise). otp.py Moyasar gateway scaffold added (not implemented yet, raises). gateway.py Added .env loading in settings and expanded .env.example for OTP and Moyasar. settings.py .env.example Captured current gaps/risks in a tracked doc as requested. risks.md Updated README with seeding guidance and risk doc pointer. README.md
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
from datetime import timedelta
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.utils import timezone
|
||||
|
||||
from apps.bookings.models import Booking, BookingStatus
|
||||
from apps.payments.models import Payment, PaymentProvider, PaymentStatus
|
||||
from apps.salons.models import Review, Salon, SalonPhoto, Service, StaffProfile
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Seed demo data for local development"
|
||||
|
||||
def handle(self, *args, **options):
|
||||
User = get_user_model()
|
||||
|
||||
admin, _ = User.objects.get_or_create(
|
||||
email="admin@example.com",
|
||||
defaults={"role": "admin", "is_staff": True, "is_superuser": True},
|
||||
)
|
||||
if not admin.has_usable_password():
|
||||
admin.set_password("Admin123!")
|
||||
admin.save(update_fields=["password"])
|
||||
|
||||
manager, _ = User.objects.get_or_create(
|
||||
email="manager@example.com",
|
||||
defaults={"role": "manager", "first_name": "Rania", "last_name": "Mansour"},
|
||||
)
|
||||
if not manager.has_usable_password():
|
||||
manager.set_password("Manager123!")
|
||||
manager.save(update_fields=["password"])
|
||||
|
||||
staff_user, _ = User.objects.get_or_create(
|
||||
email="stylist@example.com",
|
||||
defaults={"role": "staff", "first_name": "Lina", "last_name": "Hassan"},
|
||||
)
|
||||
if not staff_user.has_usable_password():
|
||||
staff_user.set_password("Staff123!")
|
||||
staff_user.save(update_fields=["password"])
|
||||
|
||||
customer, _ = User.objects.get_or_create(
|
||||
email="customer@example.com",
|
||||
defaults={"role": "customer", "first_name": "Yara", "last_name": "Saleh"},
|
||||
)
|
||||
if not customer.has_usable_password():
|
||||
customer.set_password("Customer123!")
|
||||
customer.save(update_fields=["password"])
|
||||
|
||||
salon, _ = Salon.objects.get_or_create(
|
||||
owner=manager,
|
||||
name="Luxe Riyadh Studio",
|
||||
defaults={
|
||||
"description": "Premium styling and beauty services in the heart of Riyadh.",
|
||||
"address": "Olaya Street",
|
||||
"city": "Riyadh",
|
||||
"phone_number": "+966500000000",
|
||||
"email": "hello@luxeriayadh.example",
|
||||
"website": "https://luxeriayadh.example",
|
||||
},
|
||||
)
|
||||
|
||||
SalonPhoto.objects.get_or_create(
|
||||
salon=salon,
|
||||
image_url="https://images.unsplash.com/photo-1522335789203-aabd1fc54bc9",
|
||||
defaults={"alt_text": "Salon interior", "sort_order": 1},
|
||||
)
|
||||
|
||||
service_blowout, _ = Service.objects.get_or_create(
|
||||
salon=salon,
|
||||
name="Signature Blowout",
|
||||
defaults={
|
||||
"description": "45-minute styling session with wash and blowdry.",
|
||||
"duration_minutes": 45,
|
||||
"price_amount": 180,
|
||||
"currency": "SAR",
|
||||
},
|
||||
)
|
||||
|
||||
service_color, _ = Service.objects.get_or_create(
|
||||
salon=salon,
|
||||
name="Color Refresh",
|
||||
defaults={
|
||||
"description": "Full color refresh with toning and gloss.",
|
||||
"duration_minutes": 90,
|
||||
"price_amount": 420,
|
||||
"currency": "SAR",
|
||||
},
|
||||
)
|
||||
|
||||
staff_profile, _ = StaffProfile.objects.get_or_create(
|
||||
user=staff_user,
|
||||
salon=salon,
|
||||
defaults={"title": "Senior Stylist", "bio": "Specialist in modern cuts and color."},
|
||||
)
|
||||
|
||||
Review.objects.get_or_create(
|
||||
salon=salon,
|
||||
customer=customer,
|
||||
defaults={"rating": 5, "comment": "Loved the service and attention to detail."},
|
||||
)
|
||||
|
||||
salon.rating_avg = 5
|
||||
salon.rating_count = 1
|
||||
salon.save(update_fields=["rating_avg", "rating_count"])
|
||||
|
||||
start_time = timezone.now() + timedelta(days=1)
|
||||
end_time = start_time + timedelta(minutes=service_blowout.duration_minutes)
|
||||
|
||||
booking, _ = Booking.objects.get_or_create(
|
||||
salon=salon,
|
||||
customer=customer,
|
||||
service=service_blowout,
|
||||
staff=staff_profile,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
defaults={
|
||||
"status": BookingStatus.CONFIRMED,
|
||||
"price_amount": service_blowout.price_amount,
|
||||
"currency": service_blowout.currency,
|
||||
"notes": "Prefers natural volume.",
|
||||
},
|
||||
)
|
||||
|
||||
Payment.objects.get_or_create(
|
||||
booking=booking,
|
||||
provider=PaymentProvider.MOYASAR,
|
||||
defaults={
|
||||
"status": PaymentStatus.CREATED,
|
||||
"amount": booking.price_amount,
|
||||
"currency": booking.currency,
|
||||
"external_id": "",
|
||||
"metadata": {"note": "Demo payment record"},
|
||||
},
|
||||
)
|
||||
|
||||
self.stdout.write(self.style.SUCCESS("Demo data seeded."))
|
||||
@@ -0,0 +1,99 @@
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Salon",
|
||||
fields=[
|
||||
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||
("name", models.CharField(max_length=200)),
|
||||
("description", models.TextField(blank=True)),
|
||||
("address", models.CharField(max_length=255)),
|
||||
("city", models.CharField(max_length=100)),
|
||||
("latitude", models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True)),
|
||||
("longitude", models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True)),
|
||||
("phone_number", models.CharField(blank=True, max_length=20)),
|
||||
("email", models.EmailField(blank=True, max_length=254)),
|
||||
("website", models.URLField(blank=True)),
|
||||
("rating_avg", models.DecimalField(decimal_places=2, default=0, max_digits=3)),
|
||||
("rating_count", models.PositiveIntegerField(default=0)),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
(
|
||||
"owner",
|
||||
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name="owned_salons", to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="SalonPhoto",
|
||||
fields=[
|
||||
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||
("image_url", models.URLField()),
|
||||
("alt_text", models.CharField(blank=True, max_length=200)),
|
||||
("sort_order", models.PositiveIntegerField(default=0)),
|
||||
(
|
||||
"salon",
|
||||
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name="photos", to="salons.salon"),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Service",
|
||||
fields=[
|
||||
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||
("name", models.CharField(max_length=200)),
|
||||
("description", models.TextField(blank=True)),
|
||||
("duration_minutes", models.PositiveIntegerField()),
|
||||
("price_amount", models.DecimalField(decimal_places=2, max_digits=10)),
|
||||
("currency", models.CharField(default="SAR", max_length=10)),
|
||||
("is_active", models.BooleanField(default=True)),
|
||||
(
|
||||
"salon",
|
||||
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name="services", to="salons.salon"),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="StaffProfile",
|
||||
fields=[
|
||||
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||
("title", models.CharField(blank=True, max_length=200)),
|
||||
("bio", models.TextField(blank=True)),
|
||||
("is_active", models.BooleanField(default=True)),
|
||||
(
|
||||
"salon",
|
||||
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name="staff", to="salons.salon"),
|
||||
),
|
||||
(
|
||||
"user",
|
||||
models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Review",
|
||||
fields=[
|
||||
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||
("rating", models.PositiveSmallIntegerField()),
|
||||
("comment", models.TextField(blank=True)),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
(
|
||||
"customer",
|
||||
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name="reviews", to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
(
|
||||
"salon",
|
||||
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name="reviews", to="salons.salon"),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
Reference in New Issue
Block a user