diff --git a/backend/README.md b/backend/README.md index a8b7455..e75fb62 100644 --- a/backend/README.md +++ b/backend/README.md @@ -7,25 +7,5 @@ - Phone-first auth works, but `USERNAME_FIELD` is email; align identifier strategy to avoid future auth confusion. ## Near-Term Focus -- Hardening Authentica integration (timeouts, retries, async delivery) and aligning notification provider choices. - -**Authentica E2E** -Run the real Authentica OTP flow only when explicitly enabled. - -Env vars (in `backend/.env` or shell): -- `AUTHENTICA_E2E=1` -- `AUTHENTICA_API_KEY=...` -- `AUTHENTICA_E2E_PHONE=...` (must receive OTP) -- `AUTHENTICA_E2E_CODE=...` (required; no interactive prompt) - -Command: -```bash -cd backend -PYTEST_ADDOPTS='' python3 -m pytest apps/accounts/tests -m external -``` - -Suggested flow: -1. Trigger the E2E test to send the OTP, then set `AUTHENTICA_E2E_CODE` and re-run if needed. -- Decide and document payment lifecycle scope (capture/refund supported vs explicitly out of scope). -- Add timeouts/logging for external calls or introduce minimal async jobs for OTP/notifications. -- Keep booking, payment, and notification orchestration in service layers, not views. +- finalize otp testing +- work on authentication and complete it \ No newline at end of file diff --git a/backend/apps/accounts/services/otp.py b/backend/apps/accounts/services/otp.py index 75fe1db..fee893f 100644 --- a/backend/apps/accounts/services/otp.py +++ b/backend/apps/accounts/services/otp.py @@ -57,57 +57,6 @@ class ConsoleOtpProvider(BaseOtpProvider): logger.info("OTP WhatsApp to %s: %s", to_number, message) -class TwilioOtpProvider(BaseOtpProvider): - """Twilio provider for SMS and WhatsApp OTP delivery. Requires TWILIO_* env vars.""" - - def __init__(self) -> None: - self.account_sid = os.getenv("TWILIO_ACCOUNT_SID") - self.auth_token = os.getenv("TWILIO_AUTH_TOKEN") - self.from_number = os.getenv("TWILIO_FROM_NUMBER") - self.whatsapp_from = os.getenv("TWILIO_WHATSAPP_FROM") - - def _assert_config(self) -> None: - if not self.account_sid or not self.auth_token or not self.from_number: - raise ValueError(_("Twilio credentials are not configured")) - - def _get_client(self): - from twilio.rest import Client - self._assert_config() - return Client(self.account_sid, self.auth_token) - - def send_sms(self, to_number: str, message: str) -> None: - client = self._get_client() - client.messages.create(body=message, from_=self.from_number, to=to_number) - - def send_whatsapp(self, to_number: str, message: str) -> None: - self._assert_config() - if not self.whatsapp_from: - raise ValueError(_("Twilio WhatsApp sender is not configured")) - client = self._get_client() - from_ = f"whatsapp:{self.whatsapp_from}" - to = f"whatsapp:{to_number}" - client.messages.create(body=message, from_=from_, to=to) - - -class UnifonicOtpProvider(BaseOtpProvider): - def __init__(self) -> None: - self.app_sid = os.getenv("UNIFONIC_APP_SID") - self.sender_id = os.getenv("UNIFONIC_SENDER_ID") - self.whatsapp_sender = os.getenv("UNIFONIC_WHATSAPP_SENDER") - - def _assert_config(self) -> None: - if not self.app_sid or not self.sender_id: - raise ValueError(_("Unifonic credentials are not configured")) - - def send_sms(self, to_number: str, message: str) -> None: - self._assert_config() - raise NotImplementedError(_("Unifonic SMS adapter not implemented yet")) - - def send_whatsapp(self, to_number: str, message: str) -> None: - self._assert_config() - if not self.whatsapp_sender: - raise ValueError(_("Unifonic WhatsApp sender is not configured")) - raise NotImplementedError(_("Unifonic WhatsApp adapter not implemented yet")) class AuthenticaOtpProvider(BaseOtpProvider): @@ -197,8 +146,6 @@ class AuthenticaOtpProvider(BaseOtpProvider): PROVIDERS = { "console": ConsoleOtpProvider, - "twilio": TwilioOtpProvider, - "unifonic": UnifonicOtpProvider, "authentica": AuthenticaOtpProvider, } diff --git a/backend/apps/accounts/tests/test_authentica_e2e_mock.py b/backend/apps/accounts/tests/test_authentica_e2e_mock.py deleted file mode 100644 index 8fe1ba3..0000000 --- a/backend/apps/accounts/tests/test_authentica_e2e_mock.py +++ /dev/null @@ -1,72 +0,0 @@ -"""Mocked end-to-end phone auth flow using Authentica OTP provider.""" - -import os -from unittest.mock import MagicMock, patch - -import pytest -from django.test import override_settings -from django.urls import reverse - -from apps.accounts.models import User - - -@pytest.mark.django_db -@override_settings(OTP_PROVIDER="authentica") -@patch("requests.post") -def test_phone_auth_flow_with_authentica_mock(mock_post, client): - def make_response(payload, ok=True): - response = MagicMock() - response.ok = ok - response.json.return_value = payload - response.text = "" - return response - - def side_effect(url, headers=None, json=None, timeout=None): - assert headers and headers.get("X-Authorization") == "api-key" - assert timeout == 7.0 - if url.endswith("/api/v2/send-otp"): - assert json == {"method": "sms", "phone": "+966512345678"} - return make_response({"success": True}) - if url.endswith("/api/v2/verify-otp"): - if json == {"phone": "+966512345678", "otp": "123456"}: - return make_response({"verified": True}) - return make_response({"verified": False}) - raise AssertionError(f"Unexpected URL {url}") - - with patch.dict( - os.environ, - { - "AUTHENTICA_API_KEY": "api-key", - "AUTHENTICA_TIMEOUT_SECONDS": "7", - }, - ): - mock_post.side_effect = side_effect - - request_url = reverse("phone_auth_request") - verify_url = reverse("phone_auth_verify") - - response = client.post( - request_url, - {"phone_number": "0512345678", "channel": "sms", "first_name": "Sara"}, - content_type="application/json", - ) - assert response.status_code == 201 - request_id = response.json()["request_id"] - - bad = client.post( - verify_url, - {"request_id": request_id, "code": "000000"}, - content_type="application/json", - ) - assert bad.status_code == 400 - - good = client.post( - verify_url, - {"request_id": request_id, "code": "123456"}, - content_type="application/json", - ) - assert good.status_code == 200 - - user = User.objects.filter(phone_number="+966512345678").first() - assert user is not None - assert user.is_phone_verified is True diff --git a/PLANS.md b/docs/PLANS.md similarity index 100% rename from PLANS.md rename to docs/PLANS.md