TheDeepDas commited on
Commit
c68b343
·
1 Parent(s): 185f6c1

Fix Django backend deployment on HF Spaces

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. Dockerfile +17 -0
  2. basho_backend/apps/__init__.py +0 -0
  3. basho_backend/apps/accounts/__init__.py +0 -0
  4. basho_backend/apps/accounts/admin.py +5 -0
  5. basho_backend/apps/accounts/apps.py +6 -0
  6. basho_backend/apps/accounts/managers.py +21 -0
  7. basho_backend/apps/accounts/migrations/0001_initial.py +45 -0
  8. basho_backend/apps/accounts/migrations/0002_user_profile_image.py +18 -0
  9. basho_backend/apps/accounts/migrations/0003_alter_user_profile_image.py +18 -0
  10. basho_backend/apps/accounts/migrations/0004_remove_user_profile_image_user_avatar.py +22 -0
  11. basho_backend/apps/accounts/migrations/__init__.py +0 -0
  12. basho_backend/apps/accounts/models.py +39 -0
  13. basho_backend/apps/accounts/otp.py +4 -0
  14. basho_backend/apps/accounts/services.py +107 -0
  15. basho_backend/apps/accounts/tests.py +3 -0
  16. basho_backend/apps/accounts/urls.py +38 -0
  17. basho_backend/apps/accounts/views.py +304 -0
  18. basho_backend/apps/corporate/__init__.py +0 -0
  19. basho_backend/apps/corporate/admin.py +49 -0
  20. basho_backend/apps/corporate/apps.py +5 -0
  21. basho_backend/apps/corporate/migrations/0001_initial.py +32 -0
  22. basho_backend/apps/corporate/migrations/0002_alter_corporateinquiry_options.py +17 -0
  23. basho_backend/apps/corporate/migrations/__init__.py +0 -0
  24. basho_backend/apps/corporate/models.py +39 -0
  25. basho_backend/apps/corporate/tests.py +3 -0
  26. basho_backend/apps/corporate/urls.py +6 -0
  27. basho_backend/apps/corporate/views.py +66 -0
  28. basho_backend/apps/experiences/__init__.py +0 -0
  29. basho_backend/apps/experiences/admin.py +116 -0
  30. basho_backend/apps/experiences/apps.py +5 -0
  31. basho_backend/apps/experiences/migrations/0001_initial.py +43 -0
  32. basho_backend/apps/experiences/migrations/0002_booking_payment_amount_booking_payment_status.py +23 -0
  33. basho_backend/apps/experiences/migrations/0003_studiobooking_upcomingevent.py +36 -0
  34. basho_backend/apps/experiences/migrations/0004_workshop_workshopslot_workshopregistration.py +69 -0
  35. basho_backend/apps/experiences/migrations/0005_alter_booking_options_alter_experience_options_and_more.py +210 -0
  36. basho_backend/apps/experiences/migrations/0006_alter_booking_options_alter_experience_options_and_more.py +201 -0
  37. basho_backend/apps/experiences/migrations/0007_booking_otp_booking_otp_expires_at_experienceslot_and_more.py +46 -0
  38. basho_backend/apps/experiences/migrations/0008_remove_booking_otp_remove_booking_otp_expires_at_and_more.py +62 -0
  39. basho_backend/apps/experiences/migrations/0009_rename_booked_participants_experienceslot_booked_slots_and_more.py +37 -0
  40. basho_backend/apps/experiences/migrations/0010_alter_experience_image.py +18 -0
  41. basho_backend/apps/experiences/migrations/__init__.py +0 -0
  42. basho_backend/apps/experiences/models.py +256 -0
  43. basho_backend/apps/experiences/serializers.py +186 -0
  44. basho_backend/apps/experiences/tests.py +3 -0
  45. basho_backend/apps/experiences/urls.py +67 -0
  46. basho_backend/apps/experiences/views.py +434 -0
  47. basho_backend/apps/main/__init__.py +0 -0
  48. basho_backend/apps/main/admin.py +3 -0
  49. basho_backend/apps/main/apps.py +6 -0
  50. basho_backend/apps/main/migrations/__init__.py +0 -0
Dockerfile ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ ENV PYTHONDONTWRITEBYTECODE=1
4
+ ENV PYTHONUNBUFFERED=1
5
+
6
+ WORKDIR /app
7
+
8
+ COPY requirements.txt .
9
+ RUN pip install --no-cache-dir -r requirements.txt
10
+
11
+ COPY . .
12
+
13
+ WORKDIR /app/kdon
14
+
15
+ EXPOSE 7860
16
+
17
+ CMD ["gunicorn", "basho_backend.basho_backend.wsgi:application", "--bind", "0.0.0.0:7860"]
basho_backend/apps/__init__.py ADDED
File without changes
basho_backend/apps/accounts/__init__.py ADDED
File without changes
basho_backend/apps/accounts/admin.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ from django.contrib import admin
2
+ from .models import User, EmailOTP
3
+
4
+ admin.site.register(User)
5
+ admin.site.register(EmailOTP)
basho_backend/apps/accounts/apps.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ from django.apps import AppConfig
2
+
3
+ class AccountsConfig(AppConfig):
4
+ default_auto_field = "django.db.models.BigAutoField"
5
+ name = "apps.accounts"
6
+ label = "accounts"
basho_backend/apps/accounts/managers.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from django.contrib.auth.models import BaseUserManager
2
+
3
+ class UserManager(BaseUserManager):
4
+ def create_user(self, username, email, password=None):
5
+ if not email:
6
+ raise ValueError("Email is required")
7
+
8
+ user = self.model(
9
+ username=username,
10
+ email=self.normalize_email(email),
11
+ )
12
+ user.set_password(password)
13
+ user.save(using=self._db)
14
+ return user
15
+
16
+ def create_superuser(self, username, email, password):
17
+ user = self.create_user(username, email, password)
18
+ user.is_staff = True
19
+ user.is_superuser = True
20
+ user.save(using=self._db)
21
+ return user
basho_backend/apps/accounts/migrations/0001_initial.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Generated by Django 6.0 on 2026-01-08 12:43
2
+
3
+ import django.utils.timezone
4
+ from django.db import migrations, models
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ initial = True
10
+
11
+ dependencies = [
12
+ ('auth', '0012_alter_user_first_name_max_length'),
13
+ ]
14
+
15
+ operations = [
16
+ migrations.CreateModel(
17
+ name='EmailOTP',
18
+ fields=[
19
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
20
+ ('email', models.EmailField(max_length=254)),
21
+ ('otp', models.CharField(max_length=6)),
22
+ ('created_at', models.DateTimeField(auto_now_add=True)),
23
+ ],
24
+ ),
25
+ migrations.CreateModel(
26
+ name='User',
27
+ fields=[
28
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
29
+ ('password', models.CharField(max_length=128, verbose_name='password')),
30
+ ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
31
+ ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
32
+ ('username', models.CharField(max_length=150, unique=True)),
33
+ ('email', models.EmailField(max_length=254, unique=True)),
34
+ ('is_email_verified', models.BooleanField(default=False)),
35
+ ('is_active', models.BooleanField(default=True)),
36
+ ('is_staff', models.BooleanField(default=False)),
37
+ ('date_joined', models.DateTimeField(default=django.utils.timezone.now)),
38
+ ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
39
+ ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
40
+ ],
41
+ options={
42
+ 'abstract': False,
43
+ },
44
+ ),
45
+ ]
basho_backend/apps/accounts/migrations/0002_user_profile_image.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Generated by Django 6.0 on 2026-01-10 13:04
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('accounts', '0001_initial'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AddField(
14
+ model_name='user',
15
+ name='profile_image',
16
+ field=models.ImageField(blank=True, null=True, upload_to='profile_pics/'),
17
+ ),
18
+ ]
basho_backend/apps/accounts/migrations/0003_alter_user_profile_image.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Generated by Django 6.0 on 2026-01-12 19:52
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('accounts', '0002_user_profile_image'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AlterField(
14
+ model_name='user',
15
+ name='profile_image',
16
+ field=models.ImageField(blank=True, default='profile_pics/default/default.png', upload_to='profile_pics/'),
17
+ ),
18
+ ]
basho_backend/apps/accounts/migrations/0004_remove_user_profile_image_user_avatar.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Generated by Django 6.0 on 2026-01-15 15:47
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('accounts', '0003_alter_user_profile_image'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.RemoveField(
14
+ model_name='user',
15
+ name='profile_image',
16
+ ),
17
+ migrations.AddField(
18
+ model_name='user',
19
+ name='avatar',
20
+ field=models.CharField(default='p1.png', max_length=50),
21
+ ),
22
+ ]
basho_backend/apps/accounts/migrations/__init__.py ADDED
File without changes
basho_backend/apps/accounts/models.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from django.db import models
2
+ from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin
3
+ from django.utils import timezone
4
+ from .managers import UserManager
5
+
6
+
7
+ class User(AbstractBaseUser, PermissionsMixin):
8
+ username = models.CharField(max_length=150, unique=True)
9
+ email = models.EmailField(unique=True)
10
+
11
+ avatar = models.CharField(
12
+ max_length=50,
13
+ default="p1.png"
14
+ )
15
+
16
+
17
+
18
+ is_email_verified = models.BooleanField(default=False)
19
+ is_active = models.BooleanField(default=True)
20
+ is_staff = models.BooleanField(default=False)
21
+
22
+ date_joined = models.DateTimeField(default=timezone.now)
23
+
24
+ objects = UserManager()
25
+
26
+ USERNAME_FIELD = "username"
27
+ REQUIRED_FIELDS = ["email"]
28
+
29
+ def __str__(self):
30
+ return self.username
31
+
32
+
33
+ class EmailOTP(models.Model):
34
+ email = models.EmailField()
35
+ otp = models.CharField(max_length=6)
36
+ created_at = models.DateTimeField(auto_now_add=True)
37
+
38
+ def __str__(self):
39
+ return f"{self.email} - {self.otp}"
basho_backend/apps/accounts/otp.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ import random
2
+
3
+ def generate_otp():
4
+ return str(random.randint(100000, 999999))
basho_backend/apps/accounts/services.py ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from django.core.mail import EmailMultiAlternatives
2
+ from django.conf import settings
3
+
4
+ def send_otp_email(email, otp):
5
+ subject = "Welcome to Basho byy Shivangi"
6
+
7
+ text_content = f"""
8
+ Welcome to Basho byy Shivangi!
9
+
10
+ We’re delighted to have you join us.
11
+
12
+ Your One-Time Password (OTP) is: {otp}
13
+
14
+ ⏳This OTP is valid for 5 minutes.
15
+ Do not share this OTP with anyone.
16
+
17
+ Warm regards,
18
+ Team Basho byy Shivangi
19
+ """
20
+
21
+ html_content = f"""
22
+ <html>
23
+ <body style="font-family: Arial, sans-serif; color: #333;">
24
+ <h2>Welcome to Basho byy Shivangi!</h2>
25
+
26
+ <p>We’re delighted to have you join us.</p>
27
+
28
+ <p>Your One-Time Password (OTP) is:</p>
29
+
30
+ <p style="
31
+ font-size: 32px;
32
+ font-weight: bold;
33
+ letter-spacing: 4px;
34
+ color: #652810;
35
+ margin: 16px 0;
36
+ ">
37
+ {otp}
38
+ </p>
39
+
40
+ <p>⏳ <strong>This OTP is valid for 5 minutes.</strong></p>
41
+ <p>Please do not share this OTP with anyone.</p>
42
+
43
+ <br />
44
+ <p>Warm regards,<br /><strong>Team Basho byy Shivangi</strong></p>
45
+ </body>
46
+ </html>
47
+ """
48
+
49
+ email_message = EmailMultiAlternatives(
50
+ subject=subject,
51
+ body=text_content,
52
+ from_email=settings.DEFAULT_FROM_EMAIL,
53
+ to=[email],
54
+ )
55
+
56
+ email_message.attach_alternative(html_content, "text/html")
57
+ email_message.send()
58
+
59
+
60
+ def send_welcome_email(email, username):
61
+ subject = "Welcome to Basho byy Shivangi "
62
+
63
+ text_content = f"""
64
+ Welcome to Basho byy Shivangi, {username}!
65
+
66
+ Your account has been successfully created.
67
+
68
+ We’re delighted to have you as part of our community.
69
+
70
+ Warm regards,
71
+ Team Basho byy Shivangi
72
+ """
73
+
74
+ html_content = f"""
75
+ <html>
76
+ <body style="font-family: Arial, sans-serif; color: #333;">
77
+ <h2>Welcome to Basho byy Shivangi </h2>
78
+
79
+ <p>Hi <strong>{username}</strong>,</p>
80
+
81
+ <p>
82
+ Your account has been <strong>successfully created</strong>.
83
+ We’re delighted to have you join our journey of handcrafted elegance.
84
+ </p>
85
+
86
+ <p>
87
+ You can now explore our collections, workshops, and custom creations.
88
+ </p>
89
+
90
+ <br />
91
+
92
+ <p style="color:#652810;">
93
+ Warm regards,<br /> <strong>Team Basho byy Shivangi!</strong>
94
+ </p>
95
+ </body>
96
+ </html>
97
+ """
98
+
99
+ email_message = EmailMultiAlternatives(
100
+ subject=subject,
101
+ body=text_content,
102
+ from_email=settings.DEFAULT_FROM_EMAIL,
103
+ to=[email],
104
+ )
105
+
106
+ email_message.attach_alternative(html_content, "text/html")
107
+ email_message.send()
basho_backend/apps/accounts/tests.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from django.test import TestCase
2
+
3
+ # Create your tests here.
basho_backend/apps/accounts/urls.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from django.urls import path
2
+ from .views import (
3
+ send_otp,
4
+ register_user,
5
+ login_user,
6
+ google_login,
7
+ google_register,
8
+ change_username,
9
+ me,
10
+ set_avatar,
11
+
12
+ )
13
+
14
+ urlpatterns = [
15
+ path("send-otp/", send_otp),
16
+ path("register/", register_user),
17
+ path("login/", login_user),
18
+ path("google-login/", google_login),
19
+ path("google-register/", google_register),
20
+ path("change-username/", change_username),
21
+ path("me/", me),
22
+ path("set-avatar/", set_avatar),
23
+ ]
24
+
25
+ from .views import (
26
+ forgot_password_send_otp,
27
+ forgot_password_verify_otp,
28
+ forgot_password_reset,
29
+ me
30
+ )
31
+
32
+ urlpatterns += [
33
+ path("forgot-password/send-otp/", forgot_password_send_otp),
34
+ path("forgot-password/verify-otp/", forgot_password_verify_otp),
35
+ path("forgot-password/reset-password/", forgot_password_reset),
36
+ path("me/", me),
37
+
38
+ ]
basho_backend/apps/accounts/views.py ADDED
@@ -0,0 +1,304 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from rest_framework.decorators import api_view, permission_classes, authentication_classes
2
+ from rest_framework.permissions import IsAuthenticated
3
+ from rest_framework.response import Response
4
+ from django.contrib.auth import authenticate
5
+ from rest_framework_simplejwt.tokens import RefreshToken
6
+ from rest_framework_simplejwt.authentication import JWTAuthentication
7
+ from django.utils import timezone
8
+ from datetime import timedelta
9
+ from django.views.decorators.csrf import csrf_exempt
10
+ from django.contrib.auth import get_user_model
11
+ from django.utils.crypto import get_random_string
12
+ import re
13
+
14
+ from .models import User, EmailOTP
15
+ from .otp import generate_otp
16
+ from .services import send_otp_email, send_welcome_email
17
+
18
+
19
+ import os
20
+ from django.core.files import File
21
+ from django.conf import settings
22
+ from django.views.decorators.csrf import csrf_exempt
23
+
24
+
25
+
26
+ # ---------------- HELPERS ----------------
27
+
28
+ def is_strong_password(password):
29
+ if len(password) < 8:
30
+ return False
31
+ if not re.search(r"[A-Z]", password):
32
+ return False
33
+ if not re.search(r"[^A-Za-z0-9]", password):
34
+ return False
35
+ return True
36
+
37
+
38
+ # ---------------- AUTH ----------------
39
+
40
+ @api_view(["POST"])
41
+ def send_otp(request):
42
+ email = request.data.get("email")
43
+ username = request.data.get("username")
44
+
45
+ if not email or not username:
46
+ return Response({"error": "Email and username required"}, status=400)
47
+
48
+ if User.objects.filter(username=username).exists():
49
+ return Response({"error": "Username already exists"}, status=400)
50
+
51
+ if User.objects.filter(email=email).exists():
52
+ return Response({"error": "Email already registered"}, status=400)
53
+
54
+ otp = generate_otp()
55
+
56
+ EmailOTP.objects.update_or_create(
57
+ email=email,
58
+ defaults={"otp": otp, "created_at": timezone.now()}
59
+ )
60
+
61
+ send_otp_email(email, otp)
62
+ return Response({"success": "OTP sent"})
63
+
64
+
65
+ @api_view(["POST"])
66
+ def register_user(request):
67
+ email = request.data.get("email")
68
+ username = request.data.get("username")
69
+ password = request.data.get("password")
70
+ otp = request.data.get("otp")
71
+
72
+ if not all([email, username, password, otp]):
73
+ return Response({"error": "All fields are required"}, status=400)
74
+
75
+ otp_obj = EmailOTP.objects.filter(email=email, otp=otp).first()
76
+ if not otp_obj:
77
+ return Response({"error": "Incorrect OTP"}, status=400)
78
+
79
+ if timezone.now() - otp_obj.created_at > timedelta(minutes=5):
80
+ otp_obj.delete()
81
+ return Response({"error": "OTP expired"}, status=400)
82
+
83
+ if not is_strong_password(password):
84
+ return Response(
85
+ {"error": "Password must be at least 8 characters, include 1 uppercase letter and 1 special character"},
86
+ status=400
87
+ )
88
+
89
+ user = User.objects.create_user(
90
+ username=username,
91
+ email=email,
92
+ password=password
93
+ )
94
+ user.is_email_verified = True
95
+ user.save()
96
+
97
+ send_welcome_email(email, username)
98
+ otp_obj.delete()
99
+
100
+ refresh = RefreshToken.for_user(user)
101
+
102
+ return Response({
103
+ "access": str(refresh.access_token),
104
+ "refresh": str(refresh),
105
+ "username": user.username,
106
+ })
107
+
108
+
109
+ @api_view(["POST"])
110
+ def login_user(request):
111
+ user = authenticate(
112
+ username=request.data.get("username"),
113
+ password=request.data.get("password")
114
+ )
115
+
116
+ if not user:
117
+ return Response({"error": "Incorrect username or password"}, status=400)
118
+
119
+ refresh = RefreshToken.for_user(user)
120
+
121
+ return Response({
122
+ "access": str(refresh.access_token),
123
+ "refresh": str(refresh),
124
+ "username": user.username,
125
+ "email": user.email,
126
+ "avatar": user.avatar,
127
+
128
+ })
129
+
130
+
131
+ @api_view(["POST"])
132
+ def google_login(request):
133
+ email = request.data.get("email")
134
+ user = User.objects.filter(email=email).first()
135
+
136
+ if not user:
137
+ return Response({"error": "Account not found. Please register first."}, status=400)
138
+
139
+ refresh = RefreshToken.for_user(user)
140
+
141
+ return Response({
142
+ "access": str(refresh.access_token),
143
+ "refresh": str(refresh),
144
+ "username": user.username,
145
+ "email": user.email,
146
+ "avatar": user.avatar,
147
+
148
+ })
149
+
150
+
151
+ @api_view(["POST"])
152
+ def google_register(request):
153
+ email = request.data.get("email")
154
+ username = request.data.get("username")
155
+
156
+ password = get_random_string(12)
157
+
158
+ user = User.objects.create_user(
159
+ username=username,
160
+ email=email,
161
+ password=password
162
+ )
163
+ user.is_email_verified = True
164
+ user.save()
165
+
166
+ refresh = RefreshToken.for_user(user)
167
+
168
+ return Response({
169
+ "access": str(refresh.access_token),
170
+ "refresh": str(refresh),
171
+ "username": user.username,
172
+ })
173
+
174
+
175
+ # ---------------- CHANGE USERNAME (UNCHANGED LOGIC) ----------------
176
+
177
+ @csrf_exempt
178
+ @api_view(["POST"])
179
+ @permission_classes([IsAuthenticated])
180
+ def change_username(request):
181
+ user = request.user
182
+ new_username = request.data.get("username", "").strip()
183
+
184
+ if not new_username:
185
+ return Response({"error": "Username cannot be empty"}, status=400)
186
+
187
+ if User.objects.filter(username=new_username).exclude(id=user.id).exists():
188
+ return Response({"error": "Username already taken"}, status=400)
189
+
190
+ user.username = new_username
191
+ user.save(update_fields=["username"])
192
+
193
+ return Response({"username": user.username})
194
+
195
+
196
+ # ---------------- PROFILE PICTURE ----------------
197
+
198
+ @api_view(["POST"])
199
+ @authentication_classes([JWTAuthentication])
200
+ @permission_classes([IsAuthenticated])
201
+ def set_avatar(request):
202
+ user = request.user
203
+ avatar = request.data.get("avatar")
204
+
205
+ if not avatar:
206
+ return Response({"error": "Avatar is required"}, status=400)
207
+
208
+ # optional safety check
209
+ allowed_avatars = [
210
+ "p1.png", "p2.png", "p3.png", "p4.png", "p5.png",
211
+ "p6.png", "p7.png", "p8.png", "p9.png", "p10.png",
212
+ "p11.png", "p12.png", "p13.png", "p14.png",
213
+ "p15.png", "p16.png", "p17.png",
214
+ ]
215
+
216
+ if avatar not in allowed_avatars:
217
+ return Response({"error": "Invalid avatar"}, status=400)
218
+
219
+ user.avatar = avatar
220
+ user.save(update_fields=["avatar"])
221
+
222
+ return Response({
223
+ "avatar": user.avatar
224
+ })
225
+
226
+
227
+ @api_view(["GET"])
228
+ @authentication_classes([JWTAuthentication])
229
+ @permission_classes([IsAuthenticated])
230
+ def me(request):
231
+ user = request.user
232
+ return Response({
233
+ "username": user.username,
234
+ "email": user.email,
235
+ "avatar": user.avatar,
236
+
237
+ })
238
+
239
+
240
+
241
+ # ---------------- FORGOT PASSWORD ----------------
242
+
243
+ @api_view(["POST"])
244
+ def forgot_password_send_otp(request):
245
+ email = request.data.get("email")
246
+ user = User.objects.filter(email=email).first()
247
+
248
+ if not user:
249
+ return Response({"error": "Incorrect email id entered"}, status=400)
250
+
251
+ otp = generate_otp()
252
+ EmailOTP.objects.update_or_create(
253
+ email=email,
254
+ defaults={"otp": otp, "created_at": timezone.now()}
255
+ )
256
+
257
+ send_otp_email(email, otp)
258
+ return Response({"success": "OTP sent"})
259
+
260
+
261
+ @api_view(["POST"])
262
+ def forgot_password_verify_otp(request):
263
+ email = request.data.get("email")
264
+ otp = request.data.get("otp")
265
+
266
+ otp_obj = EmailOTP.objects.filter(email=email, otp=otp).first()
267
+ if not otp_obj:
268
+ return Response({"error": "Incorrect OTP entered"}, status=400)
269
+
270
+ if timezone.now() - otp_obj.created_at > timedelta(minutes=5):
271
+ otp_obj.delete()
272
+ return Response({"error": "OTP expired"}, status=400)
273
+
274
+ return Response({"success": "OTP verified"})
275
+
276
+
277
+ @api_view(["POST"])
278
+ def forgot_password_reset(request):
279
+ email = request.data.get("email")
280
+ new_password = request.data.get("new_password")
281
+ confirm_password = request.data.get("confirm_password")
282
+
283
+ if new_password != confirm_password:
284
+ return Response({"error": "Passwords do not match"}, status=400)
285
+
286
+ if not is_strong_password(new_password):
287
+ return Response({"error": "Password must be strong"}, status=400)
288
+
289
+ user = User.objects.filter(email=email).first()
290
+ user.set_password(new_password)
291
+ user.save()
292
+
293
+ EmailOTP.objects.filter(email=email).delete()
294
+ return Response({"success": "Password reset successful"})
295
+
296
+ @api_view(["GET"])
297
+ @permission_classes([IsAuthenticated])
298
+ def me(request):
299
+ user = request.user
300
+ return Response({
301
+ "id": user.id,
302
+ "username": user.username,
303
+ "email": user.email,
304
+ })
basho_backend/apps/corporate/__init__.py ADDED
File without changes
basho_backend/apps/corporate/admin.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from django.contrib import admin
2
+ from .models import CorporateInquiry
3
+
4
+
5
+ @admin.register(CorporateInquiry)
6
+ class CorporateInquiryAdmin(admin.ModelAdmin):
7
+ """
8
+ Read-only Admin for Corporate Inquiries
9
+ """
10
+
11
+ # ✅ Show fields in list view
12
+ list_display = (
13
+ "company_name",
14
+ "contact_name",
15
+ "email",
16
+ "service_type",
17
+ "created_at",
18
+ )
19
+
20
+ # ✅ Allow searching
21
+ search_fields = (
22
+ "company_name",
23
+ "contact_name",
24
+ "email",
25
+ )
26
+
27
+ # ✅ Filters on right sidebar
28
+ list_filter = (
29
+ "service_type",
30
+ "created_at",
31
+ )
32
+
33
+ # 🔒 Make ALL fields read-only
34
+ readonly_fields = [field.name for field in CorporateInquiry._meta.fields]
35
+
36
+ # ❌ Disable ADD permission
37
+ def has_add_permission(self, request):
38
+ return False
39
+
40
+ # ❌ Disable DELETE permission
41
+ def has_delete_permission(self, request, obj=None):
42
+ return False
43
+
44
+ # ❌ Disable EDIT (save) permission
45
+ def has_change_permission(self, request, obj=None):
46
+ # Allow viewing but not editing
47
+ if request.method in ["GET", "HEAD"]:
48
+ return True
49
+ return False
basho_backend/apps/corporate/apps.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ from django.apps import AppConfig
2
+
3
+ class CorporateConfig(AppConfig):
4
+ default_auto_field = "django.db.models.BigAutoField"
5
+ name = "apps.corporate"
basho_backend/apps/corporate/migrations/0001_initial.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Generated by Django 6.0 on 2026-01-09 19:44
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ initial = True
9
+
10
+ dependencies = [
11
+ ]
12
+
13
+ operations = [
14
+ migrations.CreateModel(
15
+ name='CorporateInquiry',
16
+ fields=[
17
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
18
+ ('company_name', models.CharField(max_length=255)),
19
+ ('company_website', models.URLField(blank=True)),
20
+ ('contact_name', models.CharField(max_length=255)),
21
+ ('email', models.EmailField(max_length=254)),
22
+ ('phone', models.CharField(blank=True, max_length=20)),
23
+ ('service_type', models.CharField(choices=[('Corporate Gifting', 'Corporate Gifting'), ('Team Workshop', 'Team Workshop'), ('Brand Collaboration', 'Brand Collaboration')], max_length=50)),
24
+ ('details', models.JSONField(blank=True, default=dict)),
25
+ ('message', models.TextField(blank=True)),
26
+ ('budget', models.CharField(blank=True, max_length=100)),
27
+ ('timeline', models.CharField(blank=True, max_length=100)),
28
+ ('consent', models.BooleanField(default=False)),
29
+ ('created_at', models.DateTimeField(auto_now_add=True)),
30
+ ],
31
+ ),
32
+ ]
basho_backend/apps/corporate/migrations/0002_alter_corporateinquiry_options.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Generated by Django 6.0 on 2026-01-12 15:27
2
+
3
+ from django.db import migrations
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('corporate', '0001_initial'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AlterModelOptions(
14
+ name='corporateinquiry',
15
+ options={'ordering': ['-created_at']},
16
+ ),
17
+ ]
basho_backend/apps/corporate/migrations/__init__.py ADDED
File without changes
basho_backend/apps/corporate/models.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from django.db import models
2
+
3
+
4
+ class CorporateInquiry(models.Model):
5
+ SERVICE_CHOICES = [
6
+ ("Corporate Gifting", "Corporate Gifting"),
7
+ ("Team Workshop", "Team Workshop"),
8
+ ("Brand Collaboration", "Brand Collaboration"),
9
+ ]
10
+
11
+ company_name = models.CharField(max_length=255)
12
+ company_website = models.URLField(blank=True)
13
+
14
+ contact_name = models.CharField(max_length=255)
15
+ email = models.EmailField()
16
+ phone = models.CharField(max_length=20, blank=True)
17
+
18
+ service_type = models.CharField(
19
+ max_length=50,
20
+ choices=SERVICE_CHOICES
21
+ )
22
+
23
+ # dynamic form data stored safely
24
+ details = models.JSONField(default=dict, blank=True)
25
+
26
+ message = models.TextField(blank=True)
27
+ budget = models.CharField(max_length=100, blank=True)
28
+ timeline = models.CharField(max_length=100, blank=True)
29
+
30
+ consent = models.BooleanField(default=False)
31
+
32
+ created_at = models.DateTimeField(auto_now_add=True)
33
+
34
+ def __str__(self):
35
+ return f"{self.company_name} | {self.service_type}"
36
+
37
+ class Meta:
38
+ ordering = ["-created_at"]
39
+
basho_backend/apps/corporate/tests.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from django.test import TestCase
2
+
3
+ # Create your tests here.
basho_backend/apps/corporate/urls.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ from django.urls import path
2
+ from .views import corporate_inquiry
3
+
4
+ urlpatterns = [
5
+ path("corporate-inquiry/", corporate_inquiry),
6
+ ]
basho_backend/apps/corporate/views.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from rest_framework.decorators import api_view
2
+ from rest_framework.response import Response
3
+ from rest_framework import status
4
+
5
+ from .models import CorporateInquiry
6
+
7
+
8
+ @api_view(["POST"])
9
+ def corporate_inquiry(request):
10
+ data = request.data
11
+
12
+ # 🔒 REQUIRED FIELDS CHECK
13
+ required_fields = [
14
+ "companyName",
15
+ "contactName",
16
+ "email",
17
+ "serviceType",
18
+ "consent",
19
+ ]
20
+
21
+ for field in required_fields:
22
+ if not data.get(field):
23
+ return Response(
24
+ {"error": f"{field} is required"},
25
+ status=status.HTTP_400_BAD_REQUEST,
26
+ )
27
+
28
+ # ✅ Extract dynamic service-specific fields safely
29
+ details = data.copy()
30
+
31
+ for key in [
32
+ "companyName",
33
+ "companyWebsite",
34
+ "contactName",
35
+ "email",
36
+ "phone",
37
+ "serviceType",
38
+ "message",
39
+ "budget",
40
+ "timeline",
41
+ "consent",
42
+ ]:
43
+ details.pop(key, None)
44
+
45
+ inquiry = CorporateInquiry.objects.create(
46
+ company_name=data["companyName"],
47
+ company_website=data.get("companyWebsite", ""),
48
+ contact_name=data["contactName"],
49
+ email=data["email"],
50
+ phone=data.get("phone", ""),
51
+ service_type=data["serviceType"],
52
+ details=details,
53
+ message=data.get("message", ""),
54
+ budget=data.get("budget", ""),
55
+ timeline=data.get("timeline", ""),
56
+ consent=bool(data.get("consent")),
57
+ )
58
+
59
+ return Response(
60
+ {
61
+ "success": True,
62
+ "id": inquiry.id,
63
+ "message": "Inquiry submitted successfully",
64
+ },
65
+ status=status.HTTP_201_CREATED,
66
+ )
basho_backend/apps/experiences/__init__.py ADDED
File without changes
basho_backend/apps/experiences/admin.py ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from django.contrib import admin
2
+ from .models import (
3
+ Experience,
4
+ Booking,
5
+ StudioBooking,
6
+ UpcomingEvent,
7
+ Workshop,
8
+ WorkshopSlot,
9
+ WorkshopRegistration,
10
+ ExperienceSlot,
11
+ )
12
+
13
+ # ---------- BASIC MODELS ----------
14
+ @admin.register(Experience)
15
+ class ExperienceAdmin(admin.ModelAdmin):
16
+ list_display = ("title", "price", "is_active")
17
+ list_filter = ("is_active",)
18
+ search_fields = ("title",)
19
+
20
+ @admin.register(ExperienceSlot)
21
+ class ExperienceSlotAdmin(admin.ModelAdmin):
22
+ list_display = (
23
+ "experience",
24
+ "date",
25
+ "start_time",
26
+ "end_time",
27
+ "total_slots",
28
+ "booked_slots",
29
+ "is_active",
30
+ )
31
+
32
+ list_filter = ("experience", "date", "is_active")
33
+ search_fields = ("experience__title",)
34
+ ordering = ("date", "start_time")
35
+
36
+ readonly_fields = ("booked_slots",)
37
+
38
+
39
+ @admin.register(Booking)
40
+ class BookingAdmin(admin.ModelAdmin):
41
+ list_display = (
42
+ "id",
43
+ "full_name",
44
+ "experience",
45
+ "status",
46
+ "payment_status_display",
47
+ "created_at",
48
+ )
49
+
50
+ list_filter = ("status",)
51
+
52
+ def payment_status_display(self, obj):
53
+ if obj.payment_order:
54
+ return obj.payment_order.status
55
+ return "NO PAYMENT"
56
+
57
+ payment_status_display.short_description = "Payment Status"
58
+
59
+
60
+ @admin.register(StudioBooking)
61
+ class StudioBookingAdmin(admin.ModelAdmin):
62
+ list_display = ("full_name", "email", "phone", "visit_date", "time_slot", "created_at")
63
+ search_fields = ("full_name", "email", "phone")
64
+
65
+
66
+ @admin.register(UpcomingEvent)
67
+ class UpcomingEventAdmin(admin.ModelAdmin):
68
+ list_display = ("title", "date", "location")
69
+ search_fields = ("title",)
70
+
71
+
72
+ # ---------- WORKSHOPS ----------
73
+ @admin.register(Workshop)
74
+ class WorkshopAdmin(admin.ModelAdmin):
75
+ list_display = (
76
+ "name",
77
+ "type",
78
+ "level",
79
+ "price",
80
+ "featured",
81
+ "is_active",
82
+ )
83
+ list_filter = ("type", "level", "featured", "is_active")
84
+ search_fields = ("name", "description", "instructor")
85
+
86
+
87
+ @admin.register(WorkshopSlot)
88
+ class WorkshopSlotAdmin(admin.ModelAdmin):
89
+ list_display = (
90
+ "workshop",
91
+ "date",
92
+ "start_time",
93
+ "end_time",
94
+ "available_spots",
95
+ "is_available",
96
+ )
97
+ list_filter = ("date", "is_available")
98
+
99
+
100
+ @admin.register(WorkshopRegistration)
101
+ class WorkshopRegistrationAdmin(admin.ModelAdmin):
102
+ list_display = (
103
+ "id",
104
+ "name",
105
+ "workshop",
106
+ "status",
107
+ "payment_status_display",
108
+ "created_at",
109
+ )
110
+
111
+ def payment_status_display(self, obj):
112
+ if obj.payment_order:
113
+ return obj.payment_order.status
114
+ return "NO PAYMENT"
115
+
116
+ payment_status_display.short_description = "Payment Status"
basho_backend/apps/experiences/apps.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class ExperiencesConfig(AppConfig):
5
+ name = "apps.experiences"
basho_backend/apps/experiences/migrations/0001_initial.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Generated by Django 6.0 on 2026-01-07 15:56
2
+
3
+ import django.db.models.deletion
4
+ from django.db import migrations, models
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ initial = True
10
+
11
+ dependencies = [
12
+ ]
13
+
14
+ operations = [
15
+ migrations.CreateModel(
16
+ name='Experience',
17
+ fields=[
18
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
19
+ ('title', models.CharField(max_length=200)),
20
+ ('tagline', models.CharField(max_length=200)),
21
+ ('description', models.TextField()),
22
+ ('duration', models.CharField(max_length=50)),
23
+ ('people', models.CharField(max_length=50)),
24
+ ('price', models.IntegerField()),
25
+ ('image', models.ImageField(upload_to='experiences/')),
26
+ ('is_active', models.BooleanField(default=True)),
27
+ ],
28
+ ),
29
+ migrations.CreateModel(
30
+ name='Booking',
31
+ fields=[
32
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
33
+ ('full_name', models.CharField(max_length=100)),
34
+ ('phone', models.CharField(max_length=15)),
35
+ ('email', models.EmailField(max_length=254)),
36
+ ('booking_date', models.DateField()),
37
+ ('number_of_people', models.IntegerField(default=2)),
38
+ ('status', models.CharField(choices=[('pending', 'Pending'), ('confirmed', 'Confirmed'), ('cancelled', 'Cancelled')], default='pending', max_length=20)),
39
+ ('created_at', models.DateTimeField(auto_now_add=True)),
40
+ ('experience', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bookings', to='experiences.experience')),
41
+ ],
42
+ ),
43
+ ]
basho_backend/apps/experiences/migrations/0002_booking_payment_amount_booking_payment_status.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Generated by Django 6.0 on 2026-01-07 21:07
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('experiences', '0001_initial'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AddField(
14
+ model_name='booking',
15
+ name='payment_amount',
16
+ field=models.IntegerField(default=0),
17
+ ),
18
+ migrations.AddField(
19
+ model_name='booking',
20
+ name='payment_status',
21
+ field=models.CharField(choices=[('pending', 'Pending'), ('paid', 'Paid'), ('failed', 'Failed')], default='pending', max_length=20),
22
+ ),
23
+ ]
basho_backend/apps/experiences/migrations/0003_studiobooking_upcomingevent.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Generated by Django 6.0 on 2026-01-07 21:58
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('experiences', '0002_booking_payment_amount_booking_payment_status'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.CreateModel(
14
+ name='StudioBooking',
15
+ fields=[
16
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
17
+ ('full_name', models.CharField(max_length=100)),
18
+ ('phone', models.CharField(max_length=15)),
19
+ ('email', models.EmailField(max_length=254)),
20
+ ('visit_date', models.DateField()),
21
+ ('time_slot', models.CharField(max_length=50)),
22
+ ('created_at', models.DateTimeField(auto_now_add=True)),
23
+ ],
24
+ ),
25
+ migrations.CreateModel(
26
+ name='UpcomingEvent',
27
+ fields=[
28
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
29
+ ('title', models.CharField(max_length=200)),
30
+ ('date', models.CharField(max_length=50)),
31
+ ('location', models.CharField(max_length=200)),
32
+ ('description', models.TextField()),
33
+ ('badge', models.CharField(default='✨ Upcoming', max_length=50)),
34
+ ],
35
+ ),
36
+ ]
basho_backend/apps/experiences/migrations/0004_workshop_workshopslot_workshopregistration.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Generated by Django 6.0 on 2026-01-08 18:20
2
+
3
+ import django.db.models.deletion
4
+ from django.db import migrations, models
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ dependencies = [
10
+ ('experiences', '0003_studiobooking_upcomingevent'),
11
+ ]
12
+
13
+ operations = [
14
+ migrations.CreateModel(
15
+ name='Workshop',
16
+ fields=[
17
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
18
+ ('name', models.CharField(max_length=200)),
19
+ ('type', models.CharField(choices=[('group', 'Group'), ('private', 'Private'), ('experience', 'Experience')], max_length=20)),
20
+ ('level', models.CharField(choices=[('beginner', 'Beginner'), ('intermediate', 'Intermediate'), ('advanced', 'Advanced')], max_length=20)),
21
+ ('experience_type', models.CharField(blank=True, choices=[('couples_date', 'Couple’s Date'), ('birthday_party', 'Birthday Party'), ('corporate', 'Corporate'), ('masterclass', 'Masterclass')], max_length=30, null=True)),
22
+ ('description', models.TextField()),
23
+ ('long_description', models.TextField()),
24
+ ('images', models.JSONField(default=list)),
25
+ ('duration', models.CharField(max_length=50)),
26
+ ('min_participants', models.PositiveIntegerField()),
27
+ ('max_participants', models.PositiveIntegerField()),
28
+ ('price', models.PositiveIntegerField()),
29
+ ('price_per_person', models.BooleanField(default=True)),
30
+ ('includes', models.JSONField(default=list)),
31
+ ('requirements', models.JSONField(blank=True, null=True)),
32
+ ('provided_materials', models.JSONField(default=list)),
33
+ ('location', models.CharField(max_length=200)),
34
+ ('instructor', models.CharField(max_length=100)),
35
+ ('take_home', models.CharField(max_length=200)),
36
+ ('certificate', models.BooleanField(default=False)),
37
+ ('lunch_included', models.BooleanField(default=False)),
38
+ ('featured', models.BooleanField(default=False)),
39
+ ('is_active', models.BooleanField(default=True)),
40
+ ],
41
+ ),
42
+ migrations.CreateModel(
43
+ name='WorkshopSlot',
44
+ fields=[
45
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
46
+ ('date', models.DateField()),
47
+ ('start_time', models.TimeField()),
48
+ ('end_time', models.TimeField()),
49
+ ('available_spots', models.PositiveIntegerField()),
50
+ ('is_available', models.BooleanField(default=True)),
51
+ ('workshop', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='slots', to='experiences.workshop')),
52
+ ],
53
+ ),
54
+ migrations.CreateModel(
55
+ name='WorkshopRegistration',
56
+ fields=[
57
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
58
+ ('name', models.CharField(max_length=100)),
59
+ ('email', models.EmailField(max_length=254)),
60
+ ('phone', models.CharField(max_length=15)),
61
+ ('number_of_participants', models.PositiveIntegerField()),
62
+ ('special_requests', models.TextField(blank=True, null=True)),
63
+ ('gst_number', models.CharField(blank=True, max_length=50, null=True)),
64
+ ('created_at', models.DateTimeField(auto_now_add=True)),
65
+ ('workshop', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='registrations', to='experiences.workshop')),
66
+ ('slot', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='registrations', to='experiences.workshopslot')),
67
+ ],
68
+ ),
69
+ ]
basho_backend/apps/experiences/migrations/0005_alter_booking_options_alter_experience_options_and_more.py ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Generated by Django 6.0 on 2026-01-10 10:20
2
+
3
+ import django.core.validators
4
+ import django.utils.timezone
5
+ from django.db import migrations, models
6
+
7
+
8
+ class Migration(migrations.Migration):
9
+
10
+ dependencies = [
11
+ ('experiences', '0004_workshop_workshopslot_workshopregistration'),
12
+ ]
13
+
14
+ operations = [
15
+ migrations.AlterModelOptions(
16
+ name='booking',
17
+ options={'ordering': ['-created_at']},
18
+ ),
19
+ migrations.AlterModelOptions(
20
+ name='experience',
21
+ options={'verbose_name_plural': 'Experiences'},
22
+ ),
23
+ migrations.AlterModelOptions(
24
+ name='studiobooking',
25
+ options={'ordering': ['visit_date', 'time_slot'], 'verbose_name_plural': 'Studio Bookings'},
26
+ ),
27
+ migrations.AlterModelOptions(
28
+ name='upcomingevent',
29
+ options={'ordering': ['date']},
30
+ ),
31
+ migrations.AlterModelOptions(
32
+ name='workshop',
33
+ options={'ordering': ['name']},
34
+ ),
35
+ migrations.AlterModelOptions(
36
+ name='workshopregistration',
37
+ options={'ordering': ['-created_at']},
38
+ ),
39
+ migrations.AlterModelOptions(
40
+ name='workshopslot',
41
+ options={'ordering': ['date', 'start_time']},
42
+ ),
43
+ migrations.AddField(
44
+ model_name='workshop',
45
+ name='created_at',
46
+ field=models.DateTimeField(default=django.utils.timezone.now),
47
+ ),
48
+ migrations.AddField(
49
+ model_name='workshop',
50
+ name='updated_at',
51
+ field=models.DateTimeField(default=django.utils.timezone.now),
52
+ ),
53
+ migrations.AddField(
54
+ model_name='workshopregistration',
55
+ name='payment_amount',
56
+ field=models.PositiveIntegerField(default=0),
57
+ ),
58
+ migrations.AddField(
59
+ model_name='workshopregistration',
60
+ name='payment_status',
61
+ field=models.CharField(choices=[('pending', 'Pending'), ('paid', 'Paid'), ('failed', 'Failed')], default='pending', max_length=20),
62
+ ),
63
+ migrations.AddField(
64
+ model_name='workshopregistration',
65
+ name='updated_at',
66
+ field=models.DateTimeField(default=django.utils.timezone.now),
67
+ ),
68
+ migrations.AddField(
69
+ model_name='workshopslot',
70
+ name='created_at',
71
+ field=models.DateTimeField(default=django.utils.timezone.now),
72
+ ),
73
+ migrations.AddField(
74
+ model_name='workshopslot',
75
+ name='updated_at',
76
+ field=models.DateTimeField(default=django.utils.timezone.now),
77
+ ),
78
+ migrations.AlterField(
79
+ model_name='booking',
80
+ name='created_at',
81
+ field=models.DateTimeField(default=django.utils.timezone.now),
82
+ ),
83
+ migrations.AlterField(
84
+ model_name='workshop',
85
+ name='certificate',
86
+ field=models.BooleanField(default=False, help_text='Is certificate provided?'),
87
+ ),
88
+ migrations.AlterField(
89
+ model_name='workshop',
90
+ name='description',
91
+ field=models.TextField(help_text='Short description for cards'),
92
+ ),
93
+ migrations.AlterField(
94
+ model_name='workshop',
95
+ name='duration',
96
+ field=models.CharField(help_text="e.g., '3 hours', '2-3 hours'", max_length=50),
97
+ ),
98
+ migrations.AlterField(
99
+ model_name='workshop',
100
+ name='experience_type',
101
+ field=models.CharField(blank=True, choices=[('couples_date', "Couple's Date"), ('birthday_party', 'Birthday Party'), ('corporate', 'Corporate'), ('masterclass', 'Masterclass')], max_length=30, null=True),
102
+ ),
103
+ migrations.AlterField(
104
+ model_name='workshop',
105
+ name='featured',
106
+ field=models.BooleanField(default=False, help_text='Show as featured workshop?'),
107
+ ),
108
+ migrations.AlterField(
109
+ model_name='workshop',
110
+ name='images',
111
+ field=models.JSONField(default=list, help_text='List of image URLs/paths'),
112
+ ),
113
+ migrations.AlterField(
114
+ model_name='workshop',
115
+ name='includes',
116
+ field=models.JSONField(default=list, help_text="List of what's included (materials, tools, etc.)"),
117
+ ),
118
+ migrations.AlterField(
119
+ model_name='workshop',
120
+ name='instructor',
121
+ field=models.CharField(default='Shivangi', max_length=100),
122
+ ),
123
+ migrations.AlterField(
124
+ model_name='workshop',
125
+ name='is_active',
126
+ field=models.BooleanField(default=True, help_text='Is workshop available for booking?'),
127
+ ),
128
+ migrations.AlterField(
129
+ model_name='workshop',
130
+ name='location',
131
+ field=models.CharField(default='Basho Studio', max_length=200),
132
+ ),
133
+ migrations.AlterField(
134
+ model_name='workshop',
135
+ name='long_description',
136
+ field=models.TextField(help_text='Detailed description for workshop page'),
137
+ ),
138
+ migrations.AlterField(
139
+ model_name='workshop',
140
+ name='lunch_included',
141
+ field=models.BooleanField(default=False, help_text='Is lunch included?'),
142
+ ),
143
+ migrations.AlterField(
144
+ model_name='workshop',
145
+ name='max_participants',
146
+ field=models.PositiveIntegerField(validators=[django.core.validators.MinValueValidator(1)]),
147
+ ),
148
+ migrations.AlterField(
149
+ model_name='workshop',
150
+ name='min_participants',
151
+ field=models.PositiveIntegerField(validators=[django.core.validators.MinValueValidator(1)]),
152
+ ),
153
+ migrations.AlterField(
154
+ model_name='workshop',
155
+ name='price',
156
+ field=models.PositiveIntegerField(help_text='Price in rupees'),
157
+ ),
158
+ migrations.AlterField(
159
+ model_name='workshop',
160
+ name='price_per_person',
161
+ field=models.BooleanField(default=True, help_text='If True, price is per person. If False, price is for the group.'),
162
+ ),
163
+ migrations.AlterField(
164
+ model_name='workshop',
165
+ name='provided_materials',
166
+ field=models.JSONField(default=list, help_text='Materials provided by studio'),
167
+ ),
168
+ migrations.AlterField(
169
+ model_name='workshop',
170
+ name='requirements',
171
+ field=models.JSONField(blank=True, help_text='What participants need to bring/know', null=True),
172
+ ),
173
+ migrations.AlterField(
174
+ model_name='workshop',
175
+ name='take_home',
176
+ field=models.CharField(help_text='What participants can take home', max_length=200),
177
+ ),
178
+ migrations.AlterField(
179
+ model_name='workshopregistration',
180
+ name='created_at',
181
+ field=models.DateTimeField(default=django.utils.timezone.now),
182
+ ),
183
+ migrations.AlterField(
184
+ model_name='workshopregistration',
185
+ name='number_of_participants',
186
+ field=models.PositiveIntegerField(validators=[django.core.validators.MinValueValidator(1)]),
187
+ ),
188
+ migrations.AlterField(
189
+ model_name='workshopslot',
190
+ name='available_spots',
191
+ field=models.PositiveIntegerField(validators=[django.core.validators.MinValueValidator(0)]),
192
+ ),
193
+ migrations.AlterField(
194
+ model_name='workshopslot',
195
+ name='is_available',
196
+ field=models.BooleanField(default=True, help_text='Is this slot open for bookings?'),
197
+ ),
198
+ migrations.AlterUniqueTogether(
199
+ name='workshopslot',
200
+ unique_together={('workshop', 'date', 'start_time')},
201
+ ),
202
+ migrations.AddIndex(
203
+ model_name='workshopregistration',
204
+ index=models.Index(fields=['email'], name='experiences_email_b6e066_idx'),
205
+ ),
206
+ migrations.AddIndex(
207
+ model_name='workshopregistration',
208
+ index=models.Index(fields=['created_at'], name='experiences_created_ce420a_idx'),
209
+ ),
210
+ ]
basho_backend/apps/experiences/migrations/0006_alter_booking_options_alter_experience_options_and_more.py ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Generated by Django 6.0 on 2026-01-10 11:04
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('experiences', '0005_alter_booking_options_alter_experience_options_and_more'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AlterModelOptions(
14
+ name='booking',
15
+ options={},
16
+ ),
17
+ migrations.AlterModelOptions(
18
+ name='experience',
19
+ options={},
20
+ ),
21
+ migrations.AlterModelOptions(
22
+ name='studiobooking',
23
+ options={},
24
+ ),
25
+ migrations.AlterModelOptions(
26
+ name='upcomingevent',
27
+ options={},
28
+ ),
29
+ migrations.AlterModelOptions(
30
+ name='workshop',
31
+ options={},
32
+ ),
33
+ migrations.AlterModelOptions(
34
+ name='workshopregistration',
35
+ options={},
36
+ ),
37
+ migrations.AlterModelOptions(
38
+ name='workshopslot',
39
+ options={},
40
+ ),
41
+ migrations.RemoveIndex(
42
+ model_name='workshopregistration',
43
+ name='experiences_email_b6e066_idx',
44
+ ),
45
+ migrations.RemoveIndex(
46
+ model_name='workshopregistration',
47
+ name='experiences_created_ce420a_idx',
48
+ ),
49
+ migrations.AlterUniqueTogether(
50
+ name='workshopslot',
51
+ unique_together=set(),
52
+ ),
53
+ migrations.RemoveField(
54
+ model_name='workshop',
55
+ name='created_at',
56
+ ),
57
+ migrations.RemoveField(
58
+ model_name='workshop',
59
+ name='updated_at',
60
+ ),
61
+ migrations.AlterField(
62
+ model_name='booking',
63
+ name='created_at',
64
+ field=models.DateTimeField(auto_now_add=True),
65
+ ),
66
+ migrations.AlterField(
67
+ model_name='workshop',
68
+ name='certificate',
69
+ field=models.BooleanField(default=False),
70
+ ),
71
+ migrations.AlterField(
72
+ model_name='workshop',
73
+ name='description',
74
+ field=models.TextField(),
75
+ ),
76
+ migrations.AlterField(
77
+ model_name='workshop',
78
+ name='duration',
79
+ field=models.CharField(max_length=50),
80
+ ),
81
+ migrations.AlterField(
82
+ model_name='workshop',
83
+ name='experience_type',
84
+ field=models.CharField(blank=True, choices=[('couples_date', 'Couple’s Date'), ('birthday_party', 'Birthday Party'), ('corporate', 'Corporate'), ('masterclass', 'Masterclass')], max_length=30, null=True),
85
+ ),
86
+ migrations.AlterField(
87
+ model_name='workshop',
88
+ name='featured',
89
+ field=models.BooleanField(default=False),
90
+ ),
91
+ migrations.AlterField(
92
+ model_name='workshop',
93
+ name='images',
94
+ field=models.JSONField(default=list),
95
+ ),
96
+ migrations.AlterField(
97
+ model_name='workshop',
98
+ name='includes',
99
+ field=models.JSONField(default=list),
100
+ ),
101
+ migrations.AlterField(
102
+ model_name='workshop',
103
+ name='instructor',
104
+ field=models.CharField(max_length=100),
105
+ ),
106
+ migrations.AlterField(
107
+ model_name='workshop',
108
+ name='is_active',
109
+ field=models.BooleanField(default=True),
110
+ ),
111
+ migrations.AlterField(
112
+ model_name='workshop',
113
+ name='location',
114
+ field=models.CharField(max_length=200),
115
+ ),
116
+ migrations.AlterField(
117
+ model_name='workshop',
118
+ name='long_description',
119
+ field=models.TextField(),
120
+ ),
121
+ migrations.AlterField(
122
+ model_name='workshop',
123
+ name='lunch_included',
124
+ field=models.BooleanField(default=False),
125
+ ),
126
+ migrations.AlterField(
127
+ model_name='workshop',
128
+ name='max_participants',
129
+ field=models.PositiveIntegerField(),
130
+ ),
131
+ migrations.AlterField(
132
+ model_name='workshop',
133
+ name='min_participants',
134
+ field=models.PositiveIntegerField(),
135
+ ),
136
+ migrations.AlterField(
137
+ model_name='workshop',
138
+ name='price',
139
+ field=models.PositiveIntegerField(),
140
+ ),
141
+ migrations.AlterField(
142
+ model_name='workshop',
143
+ name='price_per_person',
144
+ field=models.BooleanField(default=True),
145
+ ),
146
+ migrations.AlterField(
147
+ model_name='workshop',
148
+ name='provided_materials',
149
+ field=models.JSONField(default=list),
150
+ ),
151
+ migrations.AlterField(
152
+ model_name='workshop',
153
+ name='requirements',
154
+ field=models.JSONField(blank=True, null=True),
155
+ ),
156
+ migrations.AlterField(
157
+ model_name='workshop',
158
+ name='take_home',
159
+ field=models.CharField(max_length=200),
160
+ ),
161
+ migrations.AlterField(
162
+ model_name='workshopregistration',
163
+ name='created_at',
164
+ field=models.DateTimeField(auto_now_add=True),
165
+ ),
166
+ migrations.AlterField(
167
+ model_name='workshopregistration',
168
+ name='number_of_participants',
169
+ field=models.PositiveIntegerField(),
170
+ ),
171
+ migrations.AlterField(
172
+ model_name='workshopslot',
173
+ name='available_spots',
174
+ field=models.PositiveIntegerField(),
175
+ ),
176
+ migrations.AlterField(
177
+ model_name='workshopslot',
178
+ name='is_available',
179
+ field=models.BooleanField(default=True),
180
+ ),
181
+ migrations.RemoveField(
182
+ model_name='workshopregistration',
183
+ name='payment_amount',
184
+ ),
185
+ migrations.RemoveField(
186
+ model_name='workshopregistration',
187
+ name='payment_status',
188
+ ),
189
+ migrations.RemoveField(
190
+ model_name='workshopregistration',
191
+ name='updated_at',
192
+ ),
193
+ migrations.RemoveField(
194
+ model_name='workshopslot',
195
+ name='created_at',
196
+ ),
197
+ migrations.RemoveField(
198
+ model_name='workshopslot',
199
+ name='updated_at',
200
+ ),
201
+ ]
basho_backend/apps/experiences/migrations/0007_booking_otp_booking_otp_expires_at_experienceslot_and_more.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Generated by Django 6.0 on 2026-01-10 19:09
2
+
3
+ import django.db.models.deletion
4
+ from django.db import migrations, models
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ dependencies = [
10
+ ('experiences', '0006_alter_booking_options_alter_experience_options_and_more'),
11
+ ]
12
+
13
+ operations = [
14
+ migrations.AddField(
15
+ model_name='booking',
16
+ name='otp',
17
+ field=models.CharField(blank=True, max_length=6, null=True),
18
+ ),
19
+ migrations.AddField(
20
+ model_name='booking',
21
+ name='otp_expires_at',
22
+ field=models.DateTimeField(blank=True, null=True),
23
+ ),
24
+ migrations.CreateModel(
25
+ name='ExperienceSlot',
26
+ fields=[
27
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
28
+ ('date', models.DateField()),
29
+ ('start_time', models.TimeField()),
30
+ ('end_time', models.TimeField()),
31
+ ('min_participants', models.PositiveIntegerField()),
32
+ ('max_participants', models.PositiveIntegerField()),
33
+ ('booked_participants', models.PositiveIntegerField(default=0)),
34
+ ('is_active', models.BooleanField(default=True)),
35
+ ('experience', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='slots', to='experiences.experience')),
36
+ ],
37
+ options={
38
+ 'ordering': ['date', 'start_time'],
39
+ },
40
+ ),
41
+ migrations.AddField(
42
+ model_name='booking',
43
+ name='slot',
44
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='bookings', to='experiences.experienceslot'),
45
+ ),
46
+ ]
basho_backend/apps/experiences/migrations/0008_remove_booking_otp_remove_booking_otp_expires_at_and_more.py ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Generated by Django 6.0 on 2026-01-12 18:51
2
+
3
+ import django.db.models.deletion
4
+ from django.db import migrations, models
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ dependencies = [
10
+ ('experiences', '0007_booking_otp_booking_otp_expires_at_experienceslot_and_more'),
11
+ ('orders', '0001_initial'),
12
+ ]
13
+
14
+ operations = [
15
+ migrations.RemoveField(
16
+ model_name='booking',
17
+ name='otp',
18
+ ),
19
+ migrations.RemoveField(
20
+ model_name='booking',
21
+ name='otp_expires_at',
22
+ ),
23
+ migrations.RemoveField(
24
+ model_name='booking',
25
+ name='payment_status',
26
+ ),
27
+ migrations.AddField(
28
+ model_name='booking',
29
+ name='payment_order',
30
+ field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='experience_booking', to='orders.paymentorder'),
31
+ ),
32
+ migrations.AddField(
33
+ model_name='workshopregistration',
34
+ name='payment_order',
35
+ field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='workshop_registration', to='orders.paymentorder'),
36
+ ),
37
+ migrations.AddField(
38
+ model_name='workshopregistration',
39
+ name='status',
40
+ field=models.CharField(choices=[('pending', 'Pending'), ('confirmed', 'Confirmed'), ('failed', 'Failed')], default='pending', max_length=20),
41
+ ),
42
+ migrations.AlterField(
43
+ model_name='booking',
44
+ name='number_of_people',
45
+ field=models.PositiveIntegerField(),
46
+ ),
47
+ migrations.AlterField(
48
+ model_name='booking',
49
+ name='payment_amount',
50
+ field=models.PositiveIntegerField(),
51
+ ),
52
+ migrations.AlterField(
53
+ model_name='booking',
54
+ name='status',
55
+ field=models.CharField(choices=[('pending', 'Pending'), ('confirmed', 'Confirmed'), ('cancelled', 'Cancelled'), ('failed', 'Failed')], default='pending', max_length=20),
56
+ ),
57
+ migrations.AlterField(
58
+ model_name='workshopregistration',
59
+ name='slot',
60
+ field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='registrations', to='experiences.workshopslot'),
61
+ ),
62
+ ]
basho_backend/apps/experiences/migrations/0009_rename_booked_participants_experienceslot_booked_slots_and_more.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Generated by Django 6.0 on 2026-01-15 09:40
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('experiences', '0008_remove_booking_otp_remove_booking_otp_expires_at_and_more'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.RenameField(
14
+ model_name='experienceslot',
15
+ old_name='booked_participants',
16
+ new_name='booked_slots',
17
+ ),
18
+ migrations.RenameField(
19
+ model_name='experienceslot',
20
+ old_name='max_participants',
21
+ new_name='total_slots',
22
+ ),
23
+ migrations.RemoveField(
24
+ model_name='experienceslot',
25
+ name='min_participants',
26
+ ),
27
+ migrations.AddField(
28
+ model_name='experience',
29
+ name='max_participants',
30
+ field=models.PositiveIntegerField(blank=True, null=True),
31
+ ),
32
+ migrations.AddField(
33
+ model_name='experience',
34
+ name='min_participants',
35
+ field=models.PositiveIntegerField(blank=True, null=True),
36
+ ),
37
+ ]
basho_backend/apps/experiences/migrations/0010_alter_experience_image.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Generated by Django 6.0 on 2026-01-16 21:04
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('experiences', '0009_rename_booked_participants_experienceslot_booked_slots_and_more'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AlterField(
14
+ model_name='experience',
15
+ name='image',
16
+ field=models.JSONField(blank=True, default=list),
17
+ ),
18
+ ]
basho_backend/apps/experiences/migrations/__init__.py ADDED
File without changes
basho_backend/apps/experiences/models.py ADDED
@@ -0,0 +1,256 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from django.db import models
2
+ from django.core.exceptions import ValidationError
3
+
4
+ class Experience(models.Model):
5
+ title = models.CharField(max_length=200)
6
+ tagline = models.CharField(max_length=200)
7
+ description = models.TextField()
8
+ duration = models.CharField(max_length=50)
9
+ people = models.CharField(max_length=50)
10
+
11
+ min_participants = models.PositiveIntegerField(null=True, blank=True)
12
+ max_participants = models.PositiveIntegerField(null=True, blank=True)
13
+
14
+ price = models.IntegerField()
15
+ image = models.JSONField(default=list, blank=True)
16
+ is_active = models.BooleanField(default=True)
17
+
18
+
19
+ def __str__(self):
20
+ return self.title
21
+
22
+ class ExperienceSlot(models.Model):
23
+ experience = models.ForeignKey(
24
+ Experience,
25
+ on_delete=models.CASCADE,
26
+ related_name="slots"
27
+ )
28
+
29
+ date = models.DateField()
30
+ start_time = models.TimeField()
31
+ end_time = models.TimeField()
32
+ total_slots = models.PositiveIntegerField()
33
+ booked_slots = models.PositiveIntegerField(default=0)
34
+
35
+ is_active = models.BooleanField(default=True)
36
+
37
+ def clean(self):
38
+ if self.start_time >= self.end_time:
39
+ raise ValidationError("Start time must be before end time.")
40
+
41
+ if self.booked_slots > self.total_slots:
42
+ raise ValidationError("Booked slots cannot exceed total slots.")
43
+
44
+ exp = self.experience
45
+ if exp.min_participants and exp.max_participants:
46
+ if exp.min_participants > exp.max_participants:
47
+ raise ValidationError(
48
+ "Experience min participants cannot exceed max participants."
49
+ )
50
+
51
+ def save(self, *args, **kwargs):
52
+ self.full_clean()
53
+ super().save(*args, **kwargs)
54
+
55
+ def __str__(self):
56
+ return f"{self.experience.title} | {self.date} {self.start_time}-{self.end_time}"
57
+
58
+ class Meta:
59
+ ordering = ["date", "start_time"]
60
+
61
+ class Booking(models.Model):
62
+ STATUS_CHOICES = (
63
+ ("pending", "Pending"),
64
+ ("confirmed", "Confirmed"),
65
+ ("cancelled", "Cancelled"),
66
+ ("failed", "Failed"),
67
+ )
68
+
69
+ experience = models.ForeignKey(
70
+ Experience,
71
+ on_delete=models.CASCADE,
72
+ related_name="bookings"
73
+ )
74
+
75
+ slot = models.ForeignKey(
76
+ ExperienceSlot,
77
+ on_delete=models.PROTECT,
78
+ related_name="bookings",
79
+ null=True,
80
+ blank=True,
81
+ )
82
+
83
+
84
+ full_name = models.CharField(max_length=100)
85
+ phone = models.CharField(max_length=15)
86
+ email = models.EmailField()
87
+
88
+ booking_date = models.DateField()
89
+ number_of_people = models.PositiveIntegerField()
90
+
91
+ status = models.CharField(
92
+ max_length=20,
93
+ choices=STATUS_CHOICES,
94
+ default="pending"
95
+ )
96
+
97
+ payment_amount = models.PositiveIntegerField()
98
+
99
+ # 🔗 LINK TO ORDERS APP
100
+ payment_order = models.OneToOneField(
101
+ "orders.PaymentOrder",
102
+ on_delete=models.SET_NULL,
103
+ null=True,
104
+ blank=True,
105
+ related_name="experience_booking"
106
+ )
107
+
108
+ created_at = models.DateTimeField(auto_now_add=True)
109
+
110
+ def __str__(self):
111
+ return f"{self.full_name} - {self.experience.title}"
112
+
113
+ class StudioBooking(models.Model):
114
+ full_name = models.CharField(max_length=100)
115
+ phone = models.CharField(max_length=15)
116
+ email = models.EmailField()
117
+ visit_date = models.DateField()
118
+ time_slot = models.CharField(max_length=50)
119
+ created_at = models.DateTimeField(auto_now_add=True)
120
+
121
+ def __str__(self):
122
+ return f"{self.full_name} - {self.visit_date} ({self.time_slot})"
123
+
124
+ class UpcomingEvent(models.Model):
125
+ title = models.CharField(max_length=200)
126
+ date = models.CharField(max_length=50) # simple string for now
127
+ location = models.CharField(max_length=200)
128
+ description = models.TextField()
129
+ badge = models.CharField(max_length=50, default="✨ Upcoming")
130
+
131
+ def __str__(self):
132
+ return self.title
133
+
134
+ class Workshop(models.Model):
135
+ WORKSHOP_TYPE_CHOICES = [
136
+ ("group", "Group"),
137
+ ("private", "Private"),
138
+ ("experience", "Experience"),
139
+ ]
140
+
141
+ LEVEL_CHOICES = [
142
+ ("beginner", "Beginner"),
143
+ ("intermediate", "Intermediate"),
144
+ ("advanced", "Advanced"),
145
+ ]
146
+
147
+ EXPERIENCE_TYPE_CHOICES = [
148
+ ("couples_date", "Couple’s Date"),
149
+ ("birthday_party", "Birthday Party"),
150
+ ("corporate", "Corporate"),
151
+ ("masterclass", "Masterclass"),
152
+ ]
153
+
154
+ name = models.CharField(max_length=200)
155
+ type = models.CharField(max_length=20, choices=WORKSHOP_TYPE_CHOICES)
156
+ level = models.CharField(max_length=20, choices=LEVEL_CHOICES)
157
+ experience_type = models.CharField(
158
+ max_length=30,
159
+ choices=EXPERIENCE_TYPE_CHOICES,
160
+ blank=True,
161
+ null=True
162
+ )
163
+
164
+ description = models.TextField()
165
+ long_description = models.TextField()
166
+
167
+ images = models.JSONField(default=list)
168
+
169
+ duration = models.CharField(max_length=50)
170
+
171
+ min_participants = models.PositiveIntegerField()
172
+ max_participants = models.PositiveIntegerField()
173
+
174
+ price = models.PositiveIntegerField()
175
+ price_per_person = models.BooleanField(default=True)
176
+
177
+ includes = models.JSONField(default=list)
178
+ requirements = models.JSONField(blank=True, null=True)
179
+ provided_materials = models.JSONField(default=list)
180
+
181
+ location = models.CharField(max_length=200)
182
+ instructor = models.CharField(max_length=100)
183
+ take_home = models.CharField(max_length=200)
184
+
185
+ certificate = models.BooleanField(default=False)
186
+ lunch_included = models.BooleanField(default=False)
187
+ featured = models.BooleanField(default=False)
188
+
189
+ is_active = models.BooleanField(default=True)
190
+
191
+ def __str__(self):
192
+ return self.name
193
+
194
+ class WorkshopSlot(models.Model):
195
+ workshop = models.ForeignKey(
196
+ Workshop,
197
+ on_delete=models.CASCADE,
198
+ related_name="slots"
199
+ )
200
+
201
+ date = models.DateField()
202
+ start_time = models.TimeField()
203
+ end_time = models.TimeField()
204
+
205
+ available_spots = models.PositiveIntegerField()
206
+ is_available = models.BooleanField(default=True)
207
+
208
+ def __str__(self):
209
+ return f"{self.workshop.name} | {self.date} {self.start_time}"
210
+
211
+ class WorkshopRegistration(models.Model):
212
+ STATUS_CHOICES = (
213
+ ("pending", "Pending"),
214
+ ("confirmed", "Confirmed"),
215
+ ("failed", "Failed"),
216
+ )
217
+
218
+ workshop = models.ForeignKey(
219
+ Workshop,
220
+ on_delete=models.CASCADE,
221
+ related_name="registrations"
222
+ )
223
+
224
+ slot = models.ForeignKey(
225
+ WorkshopSlot,
226
+ on_delete=models.PROTECT,
227
+ related_name="registrations"
228
+ )
229
+
230
+ name = models.CharField(max_length=100)
231
+ email = models.EmailField()
232
+ phone = models.CharField(max_length=15)
233
+
234
+ number_of_participants = models.PositiveIntegerField()
235
+ special_requests = models.TextField(blank=True, null=True)
236
+ gst_number = models.CharField(max_length=50, blank=True, null=True)
237
+
238
+ status = models.CharField(
239
+ max_length=20,
240
+ choices=STATUS_CHOICES,
241
+ default="pending"
242
+ )
243
+
244
+ # 🔗 LINK TO ORDERS APP
245
+ payment_order = models.OneToOneField(
246
+ "orders.PaymentOrder",
247
+ on_delete=models.SET_NULL,
248
+ null=True,
249
+ blank=True,
250
+ related_name="workshop_registration"
251
+ )
252
+
253
+ created_at = models.DateTimeField(auto_now_add=True)
254
+
255
+ def __str__(self):
256
+ return f"{self.name} - {self.workshop.name}"
basho_backend/apps/experiences/serializers.py ADDED
@@ -0,0 +1,186 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from rest_framework import serializers
2
+ from .models import (
3
+ Booking,
4
+ Experience,
5
+ StudioBooking,
6
+ UpcomingEvent,
7
+ Workshop,
8
+ WorkshopSlot,
9
+ WorkshopRegistration,
10
+ ExperienceSlot)
11
+
12
+ class BookingSerializer(serializers.ModelSerializer):
13
+ class Meta:
14
+ model = Booking
15
+ fields = [
16
+ "id",
17
+ "experience",
18
+ "slot",
19
+ "full_name",
20
+ "phone",
21
+ "email",
22
+ "booking_date",
23
+ "number_of_people",
24
+ "status",
25
+ ]
26
+ read_only_fields = ["status"]
27
+
28
+ def validate(self, data):
29
+ slot = data.get("slot")
30
+ people = data.get("number_of_people")
31
+
32
+ if not slot:
33
+ raise serializers.ValidationError({
34
+ "slot": "Please select a valid time slot."
35
+ })
36
+
37
+ if not slot.is_active:
38
+ raise serializers.ValidationError({
39
+ "slot": "This slot is no longer available."
40
+ })
41
+
42
+ experience = slot.experience
43
+
44
+ # ✅ Booking-level rules (from Experience)
45
+ if experience.min_participants is not None:
46
+ if people < experience.min_participants:
47
+ raise serializers.ValidationError({
48
+ "number_of_people": f"Minimum {experience.min_participants} participants required."
49
+ })
50
+
51
+ if experience.max_participants is not None:
52
+ if people > experience.max_participants:
53
+ raise serializers.ValidationError({
54
+ "number_of_people": f"Maximum {experience.max_participants} participants allowed per booking."
55
+ })
56
+
57
+
58
+ # ✅ Slot capacity rule
59
+ available = slot.total_slots - slot.booked_slots
60
+ if people > available:
61
+ raise serializers.ValidationError({
62
+ "number_of_people": f"Only {available} slots left for this time."
63
+ })
64
+
65
+ return data
66
+
67
+ class ExperienceSlotSerializer(serializers.ModelSerializer):
68
+ startTime = serializers.TimeField(source="start_time")
69
+ endTime = serializers.TimeField(source="end_time")
70
+ availableSlots = serializers.SerializerMethodField()
71
+
72
+ class Meta:
73
+ model = ExperienceSlot
74
+ fields = [
75
+ "id",
76
+ "date",
77
+ "startTime",
78
+ "endTime",
79
+ "availableSlots",
80
+ ]
81
+
82
+ def get_availableSlots(self, obj):
83
+ return max(0, obj.total_slots - obj.booked_slots)
84
+
85
+ class StudioBookingSerializer(serializers.ModelSerializer):
86
+ class Meta:
87
+ model = StudioBooking
88
+ fields = "__all__"
89
+
90
+ class UpcomingEventSerializer(serializers.ModelSerializer):
91
+ class Meta:
92
+ model = UpcomingEvent
93
+ fields = "__all__"
94
+
95
+ class WorkshopSlotSerializer(serializers.ModelSerializer):
96
+ startTime = serializers.TimeField(source="start_time")
97
+ endTime = serializers.TimeField(source="end_time")
98
+ availableSpots = serializers.IntegerField(source="available_spots")
99
+ isAvailable = serializers.BooleanField(source="is_available")
100
+
101
+ class Meta:
102
+ model = WorkshopSlot
103
+ fields = [
104
+ "id",
105
+ "date",
106
+ "startTime",
107
+ "endTime",
108
+ "availableSpots",
109
+ "isAvailable",
110
+ ]
111
+
112
+ class WorkshopSerializer(serializers.ModelSerializer):
113
+ participants = serializers.SerializerMethodField()
114
+ schedule = WorkshopSlotSerializer(source="slots", many=True)
115
+
116
+ experienceType = serializers.CharField(source="experience_type", allow_null=True)
117
+ longDescription = serializers.CharField(source="long_description")
118
+ pricePerPerson = serializers.BooleanField(source="price_per_person")
119
+ takeHome = serializers.CharField(source="take_home")
120
+ providedMaterials = serializers.JSONField(source="provided_materials")
121
+ lunchIncluded = serializers.BooleanField(source="lunch_included")
122
+
123
+ class Meta:
124
+ model = Workshop
125
+ fields = [
126
+ "id",
127
+ "name",
128
+ "type",
129
+ "level",
130
+ "experienceType",
131
+ "description",
132
+ "longDescription",
133
+ "images",
134
+ "duration",
135
+ "participants",
136
+ "price",
137
+ "pricePerPerson",
138
+ "includes",
139
+ "requirements",
140
+ "location",
141
+ "instructor",
142
+ "takeHome",
143
+ "providedMaterials",
144
+ "certificate",
145
+ "lunchIncluded",
146
+ "featured",
147
+ "schedule",
148
+ ]
149
+
150
+ def get_participants(self, obj):
151
+ return {
152
+ "min": obj.min_participants,
153
+ "max": obj.max_participants,
154
+ }
155
+
156
+ class WorkshopRegistrationSerializer(serializers.ModelSerializer):
157
+ class Meta:
158
+ model = WorkshopRegistration
159
+ fields = "__all__"
160
+
161
+ class ExperienceSerializer(serializers.ModelSerializer):
162
+ slots = ExperienceSlotSerializer(many=True, read_only=True)
163
+ participants = serializers.SerializerMethodField()
164
+ image = serializers.JSONField()
165
+
166
+ class Meta:
167
+ model = Experience
168
+ fields = [
169
+ "id",
170
+ "title",
171
+ "tagline",
172
+ "description",
173
+ "duration",
174
+ "people",
175
+ "price",
176
+ "image",
177
+ "is_active",
178
+ "participants",
179
+ "slots",
180
+ ]
181
+
182
+ def get_participants(self, obj):
183
+ return {
184
+ "min": obj.min_participants,
185
+ "max": obj.max_participants,
186
+ }
basho_backend/apps/experiences/tests.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from django.test import TestCase
2
+
3
+ # Create your tests here.
basho_backend/apps/experiences/urls.py ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from django.urls import path
2
+ from .views import (
3
+ CreateBookingView,
4
+ CreateStudioBookingView,
5
+ ListUpcomingEventsView,
6
+ ListWorkshopsView,
7
+ ReleaseExperienceSlotView,
8
+ WorkshopDetailView,
9
+ ListWorkshopSlotsView,
10
+ CreateWorkshopRegistrationView,
11
+ ListExperienceSlotsView,
12
+ ListExperiencesView,
13
+ ListExperienceAvailableDatesView,
14
+ ListExperienceSlotsByDateView,
15
+ VerifyExperiencePaymentView,
16
+ my_workshops,
17
+ my_experiences
18
+ )
19
+
20
+
21
+
22
+ urlpatterns = [
23
+ # Experiences
24
+ path("book/", CreateBookingView.as_view(), name="create-booking"),
25
+ path("verify-payment/", VerifyExperiencePaymentView.as_view()),
26
+ #path("book/confirm/", ConfirmBookingView.as_view(), name="confirm-booking"),
27
+ path(
28
+ "<int:experience_id>/slots/",
29
+ ListExperienceSlotsView.as_view(),
30
+ name="experience-slots",
31
+ ),
32
+ path("", ListExperiencesView.as_view(), name="list-experiences"),
33
+ path("release-slot/", ReleaseExperienceSlotView.as_view()),
34
+ path("<int:experience_id>/available-dates/", ListExperienceAvailableDatesView.as_view(), name="experience-available-dates"),
35
+ path("<int:experience_id>/slots-by-date/", ListExperienceSlotsByDateView.as_view(), name="experience-slots-by-date"),
36
+ path(
37
+ "<int:experience_id>/available-dates/",
38
+ ListExperienceAvailableDatesView.as_view(),),
39
+
40
+ path(
41
+ "<int:experience_id>/slots-by-date/",
42
+ ListExperienceSlotsByDateView.as_view(),
43
+ ),
44
+ # Studio
45
+ path("studio-book/", CreateStudioBookingView.as_view(), name="create-studio-booking"),
46
+
47
+ # Events
48
+ path("events/", ListUpcomingEventsView.as_view(), name="list-upcoming-events"),
49
+
50
+ # Workshops (UNCHANGED)
51
+ path("workshops/", ListWorkshopsView.as_view(), name="list-workshops"),
52
+ path("workshops/<int:workshop_id>/", WorkshopDetailView.as_view(), name="workshop-detail"),
53
+ path(
54
+ "workshops/<int:workshop_id>/slots/",
55
+ ListWorkshopSlotsView.as_view(),
56
+ name="workshop-slots",
57
+ ),
58
+ path(
59
+ "workshops/register/",
60
+ CreateWorkshopRegistrationView.as_view(),
61
+ name="workshop-register",
62
+ ),
63
+ path("my-workshops/", my_workshops),
64
+ path("my-experiences/", my_experiences),
65
+
66
+
67
+ ]
basho_backend/apps/experiences/views.py ADDED
@@ -0,0 +1,434 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from django.shortcuts import get_object_or_404
2
+ from django.db import transaction
3
+ from rest_framework.exceptions import ValidationError
4
+ from rest_framework.views import APIView
5
+ from rest_framework.response import Response
6
+ from rest_framework import status
7
+ from django.http import JsonResponse
8
+ from django.core.mail import send_mail
9
+ from django.conf import settings
10
+ from rest_framework.permissions import IsAuthenticated
11
+ from rest_framework.decorators import api_view, permission_classes
12
+ import razorpay
13
+ from django.utils.decorators import method_decorator
14
+ from django.views.decorators.csrf import csrf_exempt
15
+ from django.conf import settings
16
+ from django.db.models import F
17
+
18
+
19
+
20
+ from apps.orders.models import PaymentOrder
21
+ from .models import (
22
+ Booking,
23
+ StudioBooking,
24
+ UpcomingEvent,
25
+ Workshop,
26
+ WorkshopSlot,
27
+ WorkshopRegistration,
28
+ Experience,
29
+ ExperienceSlot,
30
+ )
31
+ from .serializers import (
32
+ BookingSerializer,
33
+ StudioBookingSerializer,
34
+ UpcomingEventSerializer,
35
+ WorkshopSerializer,
36
+ WorkshopSlotSerializer,
37
+ WorkshopRegistrationSerializer,
38
+ ExperienceSlotSerializer,
39
+ ExperienceSerializer, # ✅ ADD THIS
40
+ )
41
+
42
+ razorpay_client = razorpay.Client(
43
+ auth=(settings.RAZORPAY_KEY_ID, settings.RAZORPAY_KEY_SECRET)
44
+ )
45
+
46
+ # =========================
47
+ # EXPERIENCE BOOKING (PAYMENT FIRST)
48
+ # =========================
49
+
50
+ class CreateBookingView(APIView):
51
+ def post(self, request):
52
+ serializer = BookingSerializer(data=request.data)
53
+ serializer.is_valid(raise_exception=True)
54
+
55
+ slot = serializer.validated_data["slot"]
56
+ people = serializer.validated_data["number_of_people"]
57
+ experience = serializer.validated_data["experience"]
58
+
59
+ # 🔒 ATOMIC BLOCK (prevents race conditions)
60
+ with transaction.atomic():
61
+ slot = ExperienceSlot.objects.select_for_update().get(id=slot.id)
62
+
63
+ available = slot.total_slots - slot.booked_slots
64
+
65
+ if people > available:
66
+ raise ValidationError({
67
+ "slot": f"Only {available} slots left for this time slot."
68
+ })
69
+
70
+ booking = serializer.save(
71
+ status="pending",
72
+ payment_amount=experience.price
73
+ )
74
+
75
+ # ✅ Reserve seats
76
+ slot.booked_slots += people
77
+ slot.save()
78
+
79
+
80
+ # ✅ CREATE RAZORPAY ORDER
81
+ razorpay_order = razorpay_client.order.create({
82
+ "amount": booking.payment_amount * 100,
83
+ "currency": "INR",
84
+ "payment_capture": 1
85
+ })
86
+
87
+ payment_order = PaymentOrder.objects.create(
88
+ user=request.user if request.user.is_authenticated else None,
89
+ order_type="EXPERIENCE",
90
+ linked_object_id=booking.id,
91
+ linked_app="experiences",
92
+ amount=booking.payment_amount,
93
+ razorpay_order_id=razorpay_order["id"],
94
+ )
95
+
96
+ booking.payment_order = payment_order
97
+ booking.save()
98
+
99
+ return Response({
100
+ "booking_id": booking.id,
101
+ "razorpay_order_id": payment_order.razorpay_order_id,
102
+ "amount": payment_order.amount,
103
+ }, status=status.HTTP_201_CREATED)
104
+
105
+
106
+
107
+ razorpay_client = razorpay.Client(
108
+ auth=(settings.RAZORPAY_KEY_ID, settings.RAZORPAY_KEY_SECRET)
109
+ )
110
+
111
+ class ReleaseExperienceSlotView(APIView):
112
+ def post(self, request):
113
+ booking_id = request.data.get("booking_id")
114
+
115
+ if not booking_id:
116
+ return Response(
117
+ {"error": "booking_id required"},
118
+ status=status.HTTP_400_BAD_REQUEST
119
+ )
120
+
121
+ try:
122
+ with transaction.atomic():
123
+ booking = Booking.objects.select_for_update().get(id=booking_id)
124
+
125
+ # Only release if not confirmed
126
+ if booking.status != "pending":
127
+ return Response(
128
+ {"message": "Booking already processed"},
129
+ status=status.HTTP_200_OK
130
+ )
131
+
132
+ slot = booking.slot
133
+ slot.booked_slots -= booking.number_of_people
134
+ slot.save()
135
+
136
+
137
+ booking.status = "failed"
138
+ booking.save()
139
+
140
+ return Response(
141
+ {"message": "Slot released successfully"},
142
+ status=status.HTTP_200_OK
143
+ )
144
+
145
+ except Booking.DoesNotExist:
146
+ return Response(
147
+ {"error": "Booking not found"},
148
+ status=status.HTTP_404_NOT_FOUND
149
+ )
150
+
151
+ class VerifyExperiencePaymentView(APIView):
152
+ def post(self, request):
153
+ data = request.data
154
+
155
+ try:
156
+ # 1️⃣ Verify signature
157
+ razorpay_client.utility.verify_payment_signature({
158
+ "razorpay_order_id": data["razorpay_order_id"],
159
+ "razorpay_payment_id": data["razorpay_payment_id"],
160
+ "razorpay_signature": data["razorpay_signature"],
161
+ })
162
+
163
+ # 2️⃣ Fetch payment order
164
+ payment_order = PaymentOrder.objects.get(
165
+ razorpay_order_id=data["razorpay_order_id"]
166
+ )
167
+
168
+ # 3️⃣ Mark payment as paid
169
+ payment_order.status = "PAID"
170
+ payment_order.razorpay_payment_id = data["razorpay_payment_id"]
171
+ payment_order.save()
172
+
173
+ # 4️⃣ Confirm booking
174
+ booking = Booking.objects.get(id=payment_order.linked_object_id)
175
+ booking.status = "confirmed"
176
+ booking.save()
177
+
178
+ # 🔍 TEMP DEBUG — add just above send_mail
179
+ print("Experience fields:")
180
+ print(booking.experience._meta.get_fields())
181
+
182
+ # 5️⃣ Send email
183
+ if booking.email:
184
+ send_mail(
185
+ subject="Your Experience Booking is Confirmed 🎉",
186
+ message=f"""
187
+ Hi {booking.full_name},
188
+
189
+ Your experience booking has been successfully confirmed!
190
+
191
+ 📅 Date: {booking.booking_date}
192
+ 🎨 Experience: {booking.experience.title}
193
+ 💰 Amount Paid: ₹{booking.payment_amount}
194
+
195
+ We look forward to welcoming you ✨
196
+
197
+ – Team Basho
198
+ """,
199
+ from_email=settings.DEFAULT_FROM_EMAIL,
200
+ recipient_list=[booking.email],
201
+ fail_silently=False,
202
+ )
203
+
204
+ return Response({
205
+ "message": "Successfully placed order / booked experience"
206
+ }, status=status.HTTP_200_OK)
207
+
208
+ except razorpay.errors.SignatureVerificationError:
209
+ return Response(
210
+ {"error": "Payment verification failed"},
211
+ status=status.HTTP_400_BAD_REQUEST
212
+ )
213
+
214
+ # =========================
215
+ # STUDIO BOOKING (NO PAYMENT)
216
+ # =========================
217
+ @method_decorator(csrf_exempt, name="dispatch")
218
+ class CreateStudioBookingView(APIView):
219
+ def post(self, request):
220
+ serializer = StudioBookingSerializer(data=request.data)
221
+ serializer.is_valid(raise_exception=True)
222
+
223
+ booking = serializer.save()
224
+
225
+ try:
226
+ send_mail(
227
+ subject="Your Studio Visit is Confirmed ✨",
228
+ message=(
229
+ f"Hi {booking.full_name},\n\n"
230
+ f"Your studio visit has been confirmed.\n\n"
231
+ f"Date: {booking.visit_date}\n"
232
+ f"Time Slot: {booking.time_slot}\n\n"
233
+ f"– Basho Studio"
234
+ ),
235
+ from_email=settings.DEFAULT_FROM_EMAIL,
236
+ recipient_list=[booking.email],
237
+ fail_silently=True, # 🔥 THIS IS THE KEY
238
+ )
239
+ except Exception as e:
240
+ print("⚠️ Studio email failed:", str(e))
241
+
242
+
243
+ return Response(
244
+ {"message": "Studio booking confirmed"},
245
+ status=status.HTTP_201_CREATED
246
+ )
247
+
248
+
249
+ # =========================
250
+ # LISTING VIEWS
251
+ # =========================
252
+
253
+ class ListUpcomingEventsView(APIView):
254
+ def get(self, request):
255
+ events = UpcomingEvent.objects.all()
256
+ serializer = UpcomingEventSerializer(events, many=True)
257
+ return Response(serializer.data)
258
+
259
+
260
+ class ListWorkshopsView(APIView):
261
+ def get(self, request):
262
+ workshops = Workshop.objects.filter(is_active=True)
263
+ serializer = WorkshopSerializer(workshops, many=True)
264
+ return Response(serializer.data, status=status.HTTP_200_OK)
265
+
266
+
267
+ class WorkshopDetailView(APIView):
268
+ def get(self, request, workshop_id):
269
+ workshop = get_object_or_404(Workshop, id=workshop_id, is_active=True)
270
+ serializer = WorkshopSerializer(workshop)
271
+ return Response(serializer.data, status=status.HTTP_200_OK)
272
+
273
+
274
+ class ListWorkshopSlotsView(APIView):
275
+ def get(self, request, workshop_id):
276
+ workshop = get_object_or_404(Workshop, id=workshop_id, is_active=True)
277
+ slots = WorkshopSlot.objects.filter(
278
+ workshop=workshop,
279
+ is_available=True
280
+ ).order_by("date", "start_time")
281
+
282
+ serializer = WorkshopSlotSerializer(slots, many=True)
283
+ return Response(serializer.data)
284
+
285
+
286
+ class ListExperienceSlotsView(APIView):
287
+ def get(self, request, experience_id):
288
+ experience = get_object_or_404(Experience, id=experience_id, is_active=True)
289
+ slots = ExperienceSlot.objects.filter(
290
+ experience=experience,
291
+ is_active=True
292
+ ).order_by("date", "start_time")
293
+
294
+ serializer = ExperienceSlotSerializer(slots, many=True)
295
+ return Response(serializer.data, status=status.HTTP_200_OK)
296
+
297
+ class ListExperiencesView(APIView):
298
+ def get(self, request):
299
+ experiences = Experience.objects.filter(is_active=True)
300
+ serializer = ExperienceSerializer(experiences, many=True)
301
+ return Response(serializer.data, status=status.HTTP_200_OK)
302
+
303
+ class ListExperienceAvailableDatesView(APIView):
304
+ def get(self, request, experience_id):
305
+ experience = get_object_or_404(
306
+ Experience,
307
+ id=experience_id,
308
+ is_active=True
309
+ )
310
+
311
+ # Slots that still have availability
312
+ slots = (
313
+ ExperienceSlot.objects
314
+ .filter(
315
+ experience=experience,
316
+ is_active=True,
317
+ total_slots__gt=F("booked_slots")
318
+ )
319
+ .values_list("date", flat=True)
320
+ .distinct()
321
+ .order_by("date")
322
+ )
323
+
324
+ # Convert dates to string (frontend-friendly)
325
+ dates = [d.isoformat() for d in slots]
326
+
327
+ return Response(dates, status=status.HTTP_200_OK)
328
+
329
+ class ListExperienceSlotsByDateView(APIView):
330
+ def get(self, request, experience_id):
331
+ date = request.query_params.get("date")
332
+
333
+ if not date:
334
+ return Response(
335
+ {"error": "date query param is required"},
336
+ status=status.HTTP_400_BAD_REQUEST
337
+ )
338
+
339
+ experience = get_object_or_404(
340
+ Experience,
341
+ id=experience_id,
342
+ is_active=True
343
+ )
344
+
345
+ slots = (
346
+ ExperienceSlot.objects
347
+ .filter(
348
+ experience=experience,
349
+ date=date,
350
+ is_active=True
351
+ )
352
+ .order_by("start_time")
353
+ )
354
+
355
+ serializer = ExperienceSlotSerializer(slots, many=True)
356
+ return Response(serializer.data, status=status.HTTP_200_OK)
357
+
358
+ # =========================
359
+ # WORKSHOP REGISTRATION (PAYMENT FIRST)
360
+ # =========================
361
+
362
+ class CreateWorkshopRegistrationView(APIView):
363
+ def post(self, request):
364
+ serializer = WorkshopRegistrationSerializer(data=request.data)
365
+ serializer.is_valid(raise_exception=True)
366
+
367
+ registration = serializer.save(status="pending")
368
+
369
+ amount = (
370
+ registration.workshop.price * registration.number_of_participants
371
+ if registration.workshop.price_per_person
372
+ else registration.workshop.price
373
+ )
374
+
375
+ razorpay_order = razorpay_client.order.create({
376
+ "amount": amount * 100, # paise
377
+ "currency": "INR",
378
+ "payment_capture": 1
379
+ })
380
+
381
+ payment_order = PaymentOrder.objects.create(
382
+ user=request.user if request.user.is_authenticated else None,
383
+ order_type="WORKSHOP",
384
+ linked_object_id=registration.id,
385
+ linked_app="experiences",
386
+ amount=amount,
387
+ razorpay_order_id=razorpay_order["id"],
388
+ )
389
+
390
+ registration.payment_order = payment_order
391
+ registration.save()
392
+
393
+ return Response(
394
+ {
395
+ "registration_id": registration.id,
396
+ "razorpay_order_id": payment_order.razorpay_order_id,
397
+ "amount": amount,
398
+ },
399
+ status=status.HTTP_201_CREATED
400
+ )
401
+ @api_view(["GET"])
402
+ @permission_classes([IsAuthenticated])
403
+ def my_workshops(request):
404
+ orders = PaymentOrder.objects.filter(
405
+ user=request.user,
406
+ order_type="WORKSHOP",
407
+ status="PAID"
408
+ ).order_by("-created_at")
409
+
410
+ data = [{
411
+ "id": o.id,
412
+ "amount": o.amount,
413
+ "date": o.created_at,
414
+ "linked_object_id": o.linked_object_id
415
+ } for o in orders]
416
+
417
+ return JsonResponse({"workshops": data})
418
+ @api_view(["GET"])
419
+ @permission_classes([IsAuthenticated])
420
+ def my_experiences(request):
421
+ orders = PaymentOrder.objects.filter(
422
+ user=request.user,
423
+ order_type="EXPERIENCE",
424
+ status="PAID"
425
+ ).order_by("-created_at")
426
+
427
+ data = [{
428
+ "id": o.id,
429
+ "amount": o.amount,
430
+ "date": o.created_at,
431
+ "linked_object_id": o.linked_object_id
432
+ } for o in orders]
433
+
434
+ return JsonResponse({"experiences": data})
basho_backend/apps/main/__init__.py ADDED
File without changes
basho_backend/apps/main/admin.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from django.contrib import admin
2
+
3
+ # Register your models here.
basho_backend/apps/main/apps.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class MainConfig(AppConfig):
5
+ default_auto_field = 'django.db.models.BigAutoField'
6
+ name = 'apps.main'
basho_backend/apps/main/migrations/__init__.py ADDED
File without changes