diff --git a/backend/apps/accounts/migrations/0004_alter_user_groups_alter_user_phone_number_and_more.py b/backend/apps/accounts/migrations/0004_alter_user_groups_alter_user_phone_number_and_more.py new file mode 100644 index 0000000..f2215d8 --- /dev/null +++ b/backend/apps/accounts/migrations/0004_alter_user_groups_alter_user_phone_number_and_more.py @@ -0,0 +1,25 @@ +# Generated by Django 6.0.3 on 2026-03-13 20:53 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("accounts", "0003_preferred_language"), + ] + + operations = [ + migrations.AlterField( + model_name="user", + name="phone_number", + field=models.CharField(max_length=20, unique=True), + ), + migrations.AddConstraint( + model_name="user", + constraint=models.CheckConstraint( + condition=models.Q(("phone_number__regex", r"^\+[1-9][0-9]{7,14}$")), + name="accounts_user_phone_e164_format", + ), + ), + ] diff --git a/backend/apps/accounts/models.py b/backend/apps/accounts/models.py index efc4673..a659b7a 100644 --- a/backend/apps/accounts/models.py +++ b/backend/apps/accounts/models.py @@ -4,6 +4,7 @@ from datetime import timedelta from django.conf import settings from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, BaseUserManager from django.db import models +from django.db.models import Q from django.utils import timezone @@ -42,7 +43,7 @@ class UserManager(BaseUserManager): class User(AbstractBaseUser, PermissionsMixin): email = models.EmailField(unique=True, null=True, blank=True) - phone_number = models.CharField(max_length=20, unique=True, null=True, blank=True) + phone_number = models.CharField(max_length=20, unique=True) role = models.CharField(max_length=20, choices=UserRole.choices, default=UserRole.CUSTOMER) first_name = models.CharField(max_length=150, blank=True) last_name = models.CharField(max_length=150, blank=True) @@ -62,6 +63,14 @@ class User(AbstractBaseUser, PermissionsMixin): USERNAME_FIELD = "phone_number" REQUIRED_FIELDS = [] # email is optional; phone_number is the identifier + class Meta: + constraints = [ + models.CheckConstraint( + name="accounts_user_phone_e164_format", + condition=Q(phone_number__regex=r"^\+[1-9][0-9]{7,14}$"), + ), + ] + def __str__(self): return self.email or self.phone_number or str(self.id) diff --git a/backend/apps/accounts/tests/test_phone_first_enforcement.py b/backend/apps/accounts/tests/test_phone_first_enforcement.py index 09e394a..c2f991e 100644 --- a/backend/apps/accounts/tests/test_phone_first_enforcement.py +++ b/backend/apps/accounts/tests/test_phone_first_enforcement.py @@ -1,6 +1,7 @@ from unittest.mock import patch import pytest +from django.db import IntegrityError from django.urls import reverse from apps.accounts.models import OtpPurpose, PhoneOTP, User @@ -69,3 +70,24 @@ def test_otp_verify_rejects_auth_purpose_otp(client): ) assert response.status_code == 400 + + +@pytest.mark.django_db +def test_db_rejects_null_phone_number(): + # DB invariant: phone_number is mandatory. + with pytest.raises(IntegrityError): + User.objects.create(email="null-phone@example.com") + + +@pytest.mark.django_db +def test_db_rejects_non_e164_phone_number(): + # DB invariant: store normalized E.164 only. + with pytest.raises(IntegrityError): + User.objects.create_user(phone_number="0512345678") + + +@pytest.mark.django_db +def test_db_rejects_duplicate_phone_number(): + User.objects.create_user(phone_number="+966512345678") + with pytest.raises(IntegrityError): + User.objects.create_user(phone_number="+966512345678") diff --git a/backend/apps/notifications/tests/test_booking_notifications.py b/backend/apps/notifications/tests/test_booking_notifications.py index ab9a206..cd89f86 100644 --- a/backend/apps/notifications/tests/test_booking_notifications.py +++ b/backend/apps/notifications/tests/test_booking_notifications.py @@ -17,18 +17,18 @@ def booking_payload(): email="owner@example.com", password="pass", role=UserRole.MANAGER, - phone_number="0500000001", + phone_number="+966500000001", ) customer = User.objects.create_user( email="customer@example.com", password="pass", - phone_number="0500000002", + phone_number="+966500000002", ) staff_user = User.objects.create_user( email="staff@example.com", password="pass", role=UserRole.STAFF, - phone_number="0500000003", + phone_number="+966500000003", ) salon = Salon.objects.create(