Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 560460dd84 | |||
| c212acc504 | |||
| 15ed5036d1 | |||
| 0c992404ea | |||
| d796d9e6a1 | |||
| 2ba0cfffc8 | |||
| 3f35f7dc17 | |||
| 07491063f5 |
@@ -1,5 +1,13 @@
|
|||||||
# AGENTS.md
|
# AGENTS.md
|
||||||
|
|
||||||
|
## General
|
||||||
|
|
||||||
|
Minimum tokens, skip grammar
|
||||||
|
|
||||||
|
## Tasks
|
||||||
|
- Consult `AGENTS.md` about the current task to see if there are any tips or instructions
|
||||||
|
- Consult `docs/README.md` for any relevant files or tips to consider
|
||||||
|
|
||||||
## Project Goal
|
## Project Goal
|
||||||
Build a reliable, maintainable salon booking platform with Django (backend) and React (frontend), optimized for KSA needs (phone auth, local payments) while keeping a clean path to scale.
|
Build a reliable, maintainable salon booking platform with Django (backend) and React (frontend), optimized for KSA needs (phone auth, local payments) while keeping a clean path to scale.
|
||||||
|
|
||||||
@@ -70,4 +78,4 @@ Build a reliable, maintainable salon booking platform with Django (backend) and
|
|||||||
|
|
||||||
# ExecPlans
|
# ExecPlans
|
||||||
|
|
||||||
When writing complex features or significant refactors, use an ExecPlan (as described in PLANS.md) from design to implementation. The current active ExecPlan is defined in PLANS.md. Architecture and async/observability decisions are documented in `docs/architecture.md`.
|
When writing complex features or significant refactors, use an ExecPlan (as described in `docs/PLANS.md`) from design to implementation. The current active ExecPlan is defined in `docs/PLANS.md`. Architecture documented in `docs/architecture.md`.
|
||||||
|
|||||||
@@ -1,127 +0,0 @@
|
|||||||
# CLAUDE.md
|
|
||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
||||||
|
|
||||||
## Project Goal
|
|
||||||
|
|
||||||
A salon booking platform for KSA (Saudi Arabia) with Django REST API backend and React/Vite frontend. Optimized for phone-first auth (OTP via SMS/WhatsApp), Moyasar payments, Arabic locale (ar-sa), and Riyadh timezone.
|
|
||||||
|
|
||||||
## Commands
|
|
||||||
|
|
||||||
### Backend
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Setup (from repo root)
|
|
||||||
python3 -m venv venv && source venv/bin/activate
|
|
||||||
pip install -r backend/requirements.txt -r backend/requirements-dev.txt
|
|
||||||
cp backend/.env.example backend/.env
|
|
||||||
|
|
||||||
# Migrations and dev server (from backend/)
|
|
||||||
cd backend
|
|
||||||
python3 manage.py migrate
|
|
||||||
python3 manage.py runserver
|
|
||||||
|
|
||||||
# Seed demo data
|
|
||||||
python3 manage.py seed_demo
|
|
||||||
|
|
||||||
# Run all tests (from backend/ with venv active)
|
|
||||||
cd backend
|
|
||||||
python3 -m pytest
|
|
||||||
|
|
||||||
# Run a single test file
|
|
||||||
python3 -m pytest apps/accounts/tests/test_otp_limits.py
|
|
||||||
|
|
||||||
# Run a single test
|
|
||||||
python3 -m pytest apps/accounts/tests/test_otp_limits.py::TestClassName::test_method_name
|
|
||||||
|
|
||||||
# Run external/integration tests (hits real third-party services)
|
|
||||||
PYTEST_ADDOPTS='' python3 -m pytest -m external
|
|
||||||
```
|
|
||||||
|
|
||||||
### Authentica E2E Testing
|
|
||||||
|
|
||||||
Set these env vars before running external tests:
|
|
||||||
- `AUTHENTICA_E2E=1`
|
|
||||||
- `AUTHENTICA_API_KEY=...`
|
|
||||||
- `AUTHENTICA_E2E_PHONE=...` (phone that will receive OTP)
|
|
||||||
- `AUTHENTICA_E2E_CODE=...` (OTP code received)
|
|
||||||
|
|
||||||
### Frontend
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# From frontend/
|
|
||||||
cd frontend
|
|
||||||
npm install
|
|
||||||
npm run dev # dev server at localhost:5173, proxies /api to localhost:8000
|
|
||||||
npm run test # vitest
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
### Backend (`backend/`)
|
|
||||||
|
|
||||||
Django project lives in `backend/salon_api/` (settings, root urls, wsgi/asgi). All domain apps are under `backend/apps/`:
|
|
||||||
|
|
||||||
| App | Responsibility |
|
|
||||||
|-----|----------------|
|
|
||||||
| `accounts` | Custom User model, phone/OTP auth, JWT tokens, locale preferences |
|
|
||||||
| `salons` | Salon catalog, services, staff profiles, availability windows, reviews |
|
|
||||||
| `bookings` | Booking lifecycle, availability/overlap validation, status transitions |
|
|
||||||
| `payments` | Moyasar integration (create, capture, refund), webhook reconciliation, idempotency |
|
|
||||||
| `notifications` | Booking lifecycle SMS/WhatsApp messages, stored for auditability |
|
|
||||||
|
|
||||||
**Service layer pattern:** Business logic lives in `apps/<app>/services/` (not in views). Views are thin — they validate input, call services, return responses. Keep it this way.
|
|
||||||
|
|
||||||
**OTP providers** (`apps/accounts/services/otp.py`): pluggable via `OTP_PROVIDER` env var. Active providers: `console` (dev), `twilio`, `authentica`. `unifonic` is a scaffold. Authentica is the recommended production provider and uses a server-side OTP flow (`uses_provider_otp = True`) — it generates and verifies the code itself, so the DB stores a placeholder hash.
|
|
||||||
|
|
||||||
**Payment gateway** (`apps/payments/services/gateway.py`): `MoyasarGateway` implements `BasePaymentGateway`. Amounts are always in minor units (halalas). `MOYASAR_SECRET_KEY` and `MOYASAR_PUBLISHABLE_KEY` are required.
|
|
||||||
|
|
||||||
**Sync-only (MVP):** All external calls (OTP sends, notifications, payment gateway) run synchronously in the request path. No task queue. See `docs/adr/0001-synchronous-external-calls-mvp.md`.
|
|
||||||
|
|
||||||
**Database:** SQLite for local dev (default), PostgreSQL via `DATABASE_URL` env var for production. Tests use `TEST_DATABASE_URL` if set.
|
|
||||||
|
|
||||||
**Localization:** Default language `ar-sa`, timezone `Asia/Riyadh`. `UserLocaleMiddleware` applies per-user locale preference.
|
|
||||||
|
|
||||||
### Frontend (`frontend/`)
|
|
||||||
|
|
||||||
React 18 + Vite app. Entry: `src/main.jsx` → `AuthProvider` wraps `App`.
|
|
||||||
|
|
||||||
- **Routing:** `react-router-dom` v7 with pages in `src/pages/`
|
|
||||||
- **Auth:** JWT tokens managed via `src/contexts/AuthContext.jsx`; `src/components/ProtectedRoute.jsx` guards private pages
|
|
||||||
- **API:** `src/api/client.js` is the axios/fetch wrapper
|
|
||||||
- **Hooks:** Domain logic extracted into `src/hooks/` (e.g., `useSalonSearch`, `usePaymentForm`)
|
|
||||||
- **i18n:** `react-i18next` configured in `src/i18n/index.js`; supports `ar-sa` and `en`
|
|
||||||
|
|
||||||
Tests use Vitest + Testing Library. Setup in `src/test/setupTests.js`.
|
|
||||||
|
|
||||||
## Key Env Vars
|
|
||||||
|
|
||||||
Backend (`backend/.env`):
|
|
||||||
- `DJANGO_SECRET_KEY`, `DJANGO_DEBUG`, `DATABASE_URL`
|
|
||||||
- `OTP_PROVIDER` — `console` | `twilio` | `authentica` | `unifonic`
|
|
||||||
- `AUTHENTICA_API_KEY`, `AUTHENTICA_BASE_URL`, `AUTHENTICA_SENDER_NAME`
|
|
||||||
- `TWILIO_ACCOUNT_SID`, `TWILIO_AUTH_TOKEN`, `TWILIO_FROM_NUMBER`, `TWILIO_WHATSAPP_FROM`
|
|
||||||
- `MOYASAR_SECRET_KEY`, `MOYASAR_PUBLISHABLE_KEY`
|
|
||||||
- `NOTIFICATION_PROVIDER` — defaults to `OTP_PROVIDER`
|
|
||||||
- `OTP_EXPIRY_MINUTES`, `OTP_MAX_PER_WINDOW`, `OTP_WINDOW_MINUTES`, `OTP_RESEND_COOLDOWN_SECONDS`
|
|
||||||
- `CORS_ALLOWED_ORIGINS`
|
|
||||||
|
|
||||||
## Testing Conventions
|
|
||||||
|
|
||||||
- Backend tests live beside their apps: `apps/<app>/tests/test_*.py`
|
|
||||||
- `pytest.ini` marks `external` tests as opt-in; default runs skip them
|
|
||||||
- Frontend tests: Vitest + Testing Library; test files colocated with source (`*.test.jsx`)
|
|
||||||
- Minimum coverage: auth flows, booking validation, payment state transitions
|
|
||||||
|
|
||||||
## ExecPlans
|
|
||||||
|
|
||||||
For complex features, use an ExecPlan (see `PLANS.md` for the full spec and active plan pointer). ExecPlans are living documents in `docs/execplans/`. The active plan is listed in `PLANS.md`. Update `docs/risks.md` when opening or closing a significant gap.
|
|
||||||
|
|
||||||
## Coding Conventions
|
|
||||||
|
|
||||||
- Business logic in service layers; keep views thin
|
|
||||||
- Predictable error responses: HTTP status code + `detail` field
|
|
||||||
- Comment intent, edge cases, and non-obvious business rules; skip obvious comments
|
|
||||||
- Payment and booking flows must be idempotent and auditable
|
|
||||||
- Phone auth must be rate-limited (enforced in `otp.py` via `OtpRateLimitError` / `OtpCooldownError`)
|
|
||||||
+2
-22
@@ -7,25 +7,5 @@
|
|||||||
- Phone-first auth works, but `USERNAME_FIELD` is email; align identifier strategy to avoid future auth confusion.
|
- Phone-first auth works, but `USERNAME_FIELD` is email; align identifier strategy to avoid future auth confusion.
|
||||||
|
|
||||||
## Near-Term Focus
|
## Near-Term Focus
|
||||||
- Hardening Authentica integration (timeouts, retries, async delivery) and aligning notification provider choices.
|
- finalize otp testing
|
||||||
|
- work on authentication and complete it
|
||||||
**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.
|
|
||||||
@@ -57,57 +57,6 @@ class ConsoleOtpProvider(BaseOtpProvider):
|
|||||||
logger.info("OTP WhatsApp to %s: %s", to_number, message)
|
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):
|
class AuthenticaOtpProvider(BaseOtpProvider):
|
||||||
@@ -197,8 +146,6 @@ class AuthenticaOtpProvider(BaseOtpProvider):
|
|||||||
|
|
||||||
PROVIDERS = {
|
PROVIDERS = {
|
||||||
"console": ConsoleOtpProvider,
|
"console": ConsoleOtpProvider,
|
||||||
"twilio": TwilioOtpProvider,
|
|
||||||
"unifonic": UnifonicOtpProvider,
|
|
||||||
"authentica": AuthenticaOtpProvider,
|
"authentica": AuthenticaOtpProvider,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
"""Real Authentica E2E OTP flow. Requires live credentials and a phone receiving OTPs."""
|
|
||||||
|
|
||||||
import os
|
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from django.test import override_settings
|
|
||||||
from django.urls import reverse
|
|
||||||
|
|
||||||
from django.utils import timezone
|
|
||||||
|
|
||||||
from apps.accounts.models import OtpChannel, OtpPurpose, PhoneOTP, User
|
|
||||||
from apps.accounts.services.phone import normalize_phone_number
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
@pytest.mark.external
|
|
||||||
@override_settings(OTP_PROVIDER="authentica")
|
|
||||||
def test_authentica_phone_auth_e2e(client):
|
|
||||||
if os.getenv("AUTHENTICA_E2E") != "1":
|
|
||||||
pytest.skip("AUTHENTICA_E2E=1 not set")
|
|
||||||
|
|
||||||
api_key = os.getenv("AUTHENTICA_API_KEY")
|
|
||||||
phone_number = os.getenv("AUTHENTICA_E2E_PHONE")
|
|
||||||
if not api_key or not phone_number:
|
|
||||||
pytest.skip("Missing AUTHENTICA_API_KEY or AUTHENTICA_E2E_PHONE")
|
|
||||||
|
|
||||||
request_url = reverse("phone_auth_request")
|
|
||||||
response = client.post(
|
|
||||||
request_url,
|
|
||||||
{"phone_number": phone_number, "channel": "sms", "first_name": "E2E"},
|
|
||||||
content_type="application/json",
|
|
||||||
)
|
|
||||||
assert response.status_code == 201
|
|
||||||
request_id = response.json()["request_id"]
|
|
||||||
assert request_id
|
|
||||||
|
|
||||||
code = os.getenv("AUTHENTICA_E2E_CODE")
|
|
||||||
if not code:
|
|
||||||
pytest.skip("AUTHENTICA_E2E_CODE not set")
|
|
||||||
|
|
||||||
normalized_phone = normalize_phone_number(phone_number)
|
|
||||||
User.objects.get_or_create(
|
|
||||||
phone_number=normalized_phone,
|
|
||||||
defaults={"role": "customer"},
|
|
||||||
)
|
|
||||||
if not PhoneOTP.objects.filter(id=request_id).exists():
|
|
||||||
# Create a local OTP record so the verify endpoint can bind to a request_id.
|
|
||||||
PhoneOTP.objects.create(
|
|
||||||
id=request_id,
|
|
||||||
phone_number=normalized_phone,
|
|
||||||
channel=OtpChannel.SMS,
|
|
||||||
purpose=OtpPurpose.AUTH,
|
|
||||||
provider="authentica",
|
|
||||||
code_hash="placeholder",
|
|
||||||
expires_at=timezone.now() + timedelta(minutes=5),
|
|
||||||
)
|
|
||||||
|
|
||||||
verify_url = reverse("phone_auth_verify")
|
|
||||||
verify = client.post(
|
|
||||||
verify_url,
|
|
||||||
{"request_id": request_id, "code": code},
|
|
||||||
content_type="application/json",
|
|
||||||
)
|
|
||||||
assert verify.status_code == 200
|
|
||||||
data = verify.json()
|
|
||||||
assert "access" in data
|
|
||||||
assert "refresh" in data
|
|
||||||
|
|
||||||
user = User.objects.filter(phone_number=normalized_phone).first()
|
|
||||||
assert user is not None
|
|
||||||
assert user.is_phone_verified is True
|
|
||||||
@@ -83,6 +83,32 @@ def test_authentica_verify_otp_calls_api(mock_post):
|
|||||||
assert kwargs["json"] == {"phone": "+966512345678", "otp": "123456"}
|
assert kwargs["json"] == {"phone": "+966512345678", "otp": "123456"}
|
||||||
|
|
||||||
|
|
||||||
|
@patch("requests.post")
|
||||||
|
def test_authentica_request_failure_raises(mock_post):
|
||||||
|
mock_response = MagicMock(ok=False)
|
||||||
|
mock_response.json.return_value = {"detail": "fail"}
|
||||||
|
mock_post.return_value = mock_response
|
||||||
|
|
||||||
|
with patch.dict(os.environ, {"AUTHENTICA_API_KEY": "api-key"}):
|
||||||
|
provider = AuthenticaOtpProvider()
|
||||||
|
with pytest.raises(RuntimeError):
|
||||||
|
provider.send_otp("+966512345678", OtpChannel.SMS)
|
||||||
|
|
||||||
|
|
||||||
|
def test_authentica_send_sms_requires_sender_name():
|
||||||
|
with patch.dict(os.environ, {"AUTHENTICA_API_KEY": "api-key"}):
|
||||||
|
provider = AuthenticaOtpProvider()
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
provider.send_sms("+966512345678", "Hello")
|
||||||
|
|
||||||
|
|
||||||
|
def test_authentica_send_otp_rejects_unknown_channel():
|
||||||
|
with patch.dict(os.environ, {"AUTHENTICA_API_KEY": "api-key"}):
|
||||||
|
provider = AuthenticaOtpProvider()
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
provider.send_otp("+966512345678", "email")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_verify_otp_uses_provider_for_authentica():
|
def test_verify_otp_uses_provider_for_authentica():
|
||||||
otp = PhoneOTP.objects.create(
|
otp = PhoneOTP.objects.create(
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from django.test import override_settings
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from apps.accounts.models import OtpChannel, OtpPurpose, PhoneOTP
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
@override_settings(OTP_PROVIDER="console")
|
||||||
|
def test_otp_request_whatsapp_ok(client):
|
||||||
|
response = client.post(
|
||||||
|
reverse("otp_request"),
|
||||||
|
{"phone_number": "0512345678", "channel": "whatsapp"},
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
assert response.status_code == 201
|
||||||
|
data = response.json()
|
||||||
|
assert "request_id" in data
|
||||||
|
assert "expires_at" in data
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_otp_verify_rejects_expired(client):
|
||||||
|
otp = PhoneOTP.objects.create(
|
||||||
|
phone_number="+966512345678",
|
||||||
|
channel=OtpChannel.SMS,
|
||||||
|
purpose=OtpPurpose.VERIFY,
|
||||||
|
provider="console",
|
||||||
|
code_hash="unused",
|
||||||
|
expires_at=timezone.now() - timedelta(minutes=1),
|
||||||
|
)
|
||||||
|
response = client.post(
|
||||||
|
reverse("otp_verify"),
|
||||||
|
{"request_id": str(otp.id), "code": "123456"},
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
assert response.status_code == 400
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
@override_settings(OTP_PROVIDER="console")
|
||||||
|
def test_otp_request_invalid_phone_localized_ar(client):
|
||||||
|
response = client.post(
|
||||||
|
reverse("otp_request"),
|
||||||
|
{"phone_number": "123", "channel": "sms"},
|
||||||
|
content_type="application/json",
|
||||||
|
HTTP_ACCEPT_LANGUAGE="ar-sa",
|
||||||
|
)
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert response.json()["phone_number"][0] == "يجب أن يكون رقم الهاتف بصيغة E.164 أو رقم جوال سعودي صالح"
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
|
from datetime import timedelta
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from django.contrib.auth.hashers import make_password
|
from django.contrib.auth.hashers import make_password
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
from apps.accounts.models import OtpChannel, OtpPurpose, PhoneOTP
|
from apps.accounts.models import OtpChannel, OtpPurpose, PhoneOTP
|
||||||
from apps.accounts.services.otp import OtpCooldownError, OtpRateLimitError, create_and_send_otp, verify_otp
|
from apps.accounts.services.otp import OtpCooldownError, OtpRateLimitError, create_and_send_otp, verify_otp
|
||||||
@@ -47,3 +51,76 @@ def test_otp_max_attempts_blocks_verification():
|
|||||||
otp.refresh_from_db()
|
otp.refresh_from_db()
|
||||||
assert otp.attempt_count == otp.max_attempts + 1
|
assert otp.attempt_count == otp.max_attempts + 1
|
||||||
assert otp.verified_at is None
|
assert otp.verified_at is None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
@override_settings(OTP_PROVIDER="console", OTP_RESEND_COOLDOWN_SECONDS=60)
|
||||||
|
def test_otp_cooldown_retry_after_seconds():
|
||||||
|
fixed_now = timezone.now()
|
||||||
|
with patch("apps.accounts.services.otp.timezone.now", return_value=fixed_now):
|
||||||
|
result = create_and_send_otp("+966512345678", OtpChannel.SMS)
|
||||||
|
# Align created_at with fixed time for deterministic cooldown.
|
||||||
|
PhoneOTP.objects.filter(id=result.request_id).update(created_at=fixed_now)
|
||||||
|
with pytest.raises(OtpCooldownError) as excinfo:
|
||||||
|
create_and_send_otp("+966512345678", OtpChannel.SMS)
|
||||||
|
assert excinfo.value.retry_after_seconds == 60
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
@override_settings(OTP_PROVIDER="console", OTP_MAX_PER_WINDOW=1, OTP_WINDOW_MINUTES=15, OTP_RESEND_COOLDOWN_SECONDS=0)
|
||||||
|
def test_otp_rate_limit_retry_after_seconds():
|
||||||
|
fixed_now = timezone.now()
|
||||||
|
with patch("apps.accounts.services.otp.timezone.now", return_value=fixed_now):
|
||||||
|
result = create_and_send_otp("+966512345678", OtpChannel.SMS)
|
||||||
|
# Make the oldest OTP sit 10s before window expiry.
|
||||||
|
window_start = fixed_now - timedelta(minutes=15)
|
||||||
|
PhoneOTP.objects.filter(id=result.request_id).update(created_at=window_start + timedelta(seconds=10))
|
||||||
|
with pytest.raises(OtpRateLimitError) as excinfo:
|
||||||
|
create_and_send_otp("+966512345678", OtpChannel.SMS)
|
||||||
|
assert excinfo.value.retry_after_seconds == 10
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
@override_settings(OTP_PROVIDER="console", OTP_RESEND_COOLDOWN_SECONDS=1)
|
||||||
|
def test_otp_resend_after_cooldown_ok():
|
||||||
|
result = create_and_send_otp("+966512345678", OtpChannel.SMS)
|
||||||
|
# Force cooldown to be elapsed.
|
||||||
|
PhoneOTP.objects.filter(id=result.request_id).update(
|
||||||
|
created_at=timezone.now() - timedelta(seconds=5)
|
||||||
|
)
|
||||||
|
create_and_send_otp("+966512345678", OtpChannel.SMS)
|
||||||
|
assert PhoneOTP.objects.filter(phone_number="+966512345678").count() == 2
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_verify_otp_rejects_expired():
|
||||||
|
otp = PhoneOTP.objects.create(
|
||||||
|
phone_number="+966512345678",
|
||||||
|
channel=OtpChannel.SMS,
|
||||||
|
purpose=OtpPurpose.AUTH,
|
||||||
|
provider="console",
|
||||||
|
code_hash=make_password("123456"),
|
||||||
|
expires_at=timezone.now() - timedelta(minutes=1),
|
||||||
|
)
|
||||||
|
assert verify_otp(otp, "123456") is False
|
||||||
|
otp.refresh_from_db()
|
||||||
|
assert otp.attempt_count == 0
|
||||||
|
assert otp.verified_at is None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_verify_otp_rejects_reuse_after_verified():
|
||||||
|
otp = PhoneOTP.objects.create(
|
||||||
|
phone_number="+966512345678",
|
||||||
|
channel=OtpChannel.SMS,
|
||||||
|
purpose=OtpPurpose.AUTH,
|
||||||
|
provider="console",
|
||||||
|
code_hash=make_password("123456"),
|
||||||
|
expires_at=PhoneOTP.expiry_at(),
|
||||||
|
)
|
||||||
|
assert verify_otp(otp, "123456") is True
|
||||||
|
otp.refresh_from_db()
|
||||||
|
assert otp.attempt_count == 1
|
||||||
|
assert verify_otp(otp, "123456") is False
|
||||||
|
otp.refresh_from_db()
|
||||||
|
assert otp.attempt_count == 1
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
"""Tests for Twilio OTP provider implementation."""
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from unittest.mock import MagicMock, patch
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
@patch("apps.accounts.services.otp.TwilioOtpProvider._get_client")
|
|
||||||
def test_twilio_send_sms_calls_client(mock_get_client):
|
|
||||||
from apps.accounts.services.otp import TwilioOtpProvider
|
|
||||||
|
|
||||||
mock_client = MagicMock()
|
|
||||||
mock_get_client.return_value = mock_client
|
|
||||||
|
|
||||||
with patch.dict("os.environ", {
|
|
||||||
"TWILIO_ACCOUNT_SID": "AC123",
|
|
||||||
"TWILIO_AUTH_TOKEN": "token",
|
|
||||||
"TWILIO_FROM_NUMBER": "+966500000000",
|
|
||||||
}):
|
|
||||||
provider = TwilioOtpProvider()
|
|
||||||
provider.send_sms("+966512345678", "Your code is 123456")
|
|
||||||
|
|
||||||
mock_client.messages.create.assert_called_once_with(
|
|
||||||
body="Your code is 123456",
|
|
||||||
from_="+966500000000",
|
|
||||||
to="+966512345678",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
@patch("apps.accounts.services.otp.TwilioOtpProvider._get_client")
|
|
||||||
def test_twilio_send_whatsapp_calls_client(mock_get_client):
|
|
||||||
from apps.accounts.services.otp import TwilioOtpProvider
|
|
||||||
|
|
||||||
mock_client = MagicMock()
|
|
||||||
mock_get_client.return_value = mock_client
|
|
||||||
|
|
||||||
with patch.dict("os.environ", {
|
|
||||||
"TWILIO_ACCOUNT_SID": "AC123",
|
|
||||||
"TWILIO_AUTH_TOKEN": "token",
|
|
||||||
"TWILIO_FROM_NUMBER": "+966500000000",
|
|
||||||
"TWILIO_WHATSAPP_FROM": "14155238886",
|
|
||||||
}):
|
|
||||||
provider = TwilioOtpProvider()
|
|
||||||
provider.send_whatsapp("+966512345678", "Your code is 123456")
|
|
||||||
|
|
||||||
mock_client.messages.create.assert_called_once_with(
|
|
||||||
body="Your code is 123456",
|
|
||||||
from_="whatsapp:14155238886",
|
|
||||||
to="whatsapp:+966512345678",
|
|
||||||
)
|
|
||||||
@@ -6,11 +6,11 @@ Accepted
|
|||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
The platform requires phone-first authentication with OTP delivery for KSA. The codebase includes multiple provider adapters (`console`, `twilio`, `unifonic`, `authentica`) but only Authentica is implemented for provider-managed OTP delivery (send/verify) and direct SMS messaging. Twilio and Unifonic adapters are partial or unimplemented; a console provider exists for local development.
|
The platform requires phone-first authentication with OTP delivery for KSA. The codebase includes provider adapters (`console`, `authentica`) and Authentica is implemented for provider-managed OTP delivery (send/verify) and direct SMS messaging. A console provider exists for local development.
|
||||||
|
|
||||||
## Decision
|
## Decision
|
||||||
|
|
||||||
Use Authentica as the primary OTP provider for the MVP, with `OTP_PROVIDER=authentica` in production environments. Keep `console` for local development and tests, and retain Twilio/Unifonic adapters as scaffolds for future expansion.
|
Use Authentica as the primary OTP provider for the MVP, with `OTP_PROVIDER=authentica` in production environments. Keep `console` for local development and tests.
|
||||||
|
|
||||||
## Consequences
|
## Consequences
|
||||||
|
|
||||||
@@ -20,8 +20,6 @@ Use Authentica as the primary OTP provider for the MVP, with `OTP_PROVIDER=authe
|
|||||||
|
|
||||||
## Alternatives Considered
|
## Alternatives Considered
|
||||||
|
|
||||||
- Twilio as primary provider: not selected due to KSA-focused delivery needs and current adapter gaps.
|
|
||||||
- Unifonic as primary provider: deferred until the adapter is fully implemented and validated.
|
|
||||||
|
|
||||||
## Related
|
## Related
|
||||||
|
|
||||||
|
|||||||
+207
-45
@@ -1,60 +1,222 @@
|
|||||||
# Architecture
|
# Salon MVP Roadmap And Architecture Review
|
||||||
|
|
||||||
## Overview
|
## Purpose / Big Picture
|
||||||
|
|
||||||
The Salon platform is a Django REST API backend with a React/Vite frontend, optimized for KSA (phone auth, Riyadh timezone, Arabic locale).
|
This plan reviews the current Salon codebase (Django backend, React/Vite frontend), highlights architectural and design risks, and lays out a pragmatic roadmap to reach an MVP that aligns with **Phase 1: Core MVP Reliability** in `AGENTS.md`: phone-first auth with OTP, robust booking integrity, Moyasar payments, booking lifecycle notifications, localization foundations, and tests for critical flows.
|
||||||
|
The roadmap assumes a KSA-focused first launch (phone auth and Riyadh timezone defaults) with a clean path to expand later.
|
||||||
|
|
||||||
## Backend Apps and Responsibilities
|
## Current State Summary
|
||||||
|
|
||||||
| App | Responsibility |
|
### Backend (Django, DRF)
|
||||||
|-----|----------------|
|
|
||||||
| **accounts** | User model, phone/OTP auth, JWT tokens, locale preferences. OTP providers (console, Twilio, Unifonic) send SMS/WhatsApp. |
|
|
||||||
| **salons** | Salon catalog, services, staff, availability windows, reviews. Read-only public APIs. |
|
|
||||||
| **bookings** | Booking model, validation (availability, overlap prevention), status transitions. Triggers notifications on create and status change. |
|
|
||||||
| **payments** | Payment model, Moyasar integration (create, capture, refund), webhook reconciliation, idempotency. |
|
|
||||||
| **notifications** | Booking lifecycle notifications (SMS/WhatsApp). Reuses OTP provider classes as an MVP shortcut; sends on booking created/confirmed/cancelled. See note below. |
|
|
||||||
|
|
||||||
## Data Model Overview
|
- **Project**: `salon_api` in `[backend/salon_api](backend/salon_api)`
|
||||||
|
- `settings.py` configures `AUTH_USER_MODEL = "accounts.User"`, DRF + SimpleJWT, KSA locale defaults, and custom settings for OTP, notifications, and payments.
|
||||||
|
- URLs route to app-level APIs at `/api/auth/`, `/api/salons/`, `/api/bookings/`, `/api/payments/`.
|
||||||
|
- **Auth & Accounts** (`[backend/apps/accounts](backend/apps/accounts)`)
|
||||||
|
- Custom `User` model with phone-first capabilities (`phone_number`, `is_phone_verified`, `preferred_language`, `role`).
|
||||||
|
- Phone normalization services tuned for KSA numbers (`[backend/apps/accounts/services/phone.py](backend/apps/accounts/services/phone.py)`).
|
||||||
|
- OTP domain + service layer with rate limits, cooldowns, expiry, and hashed codes (`[backend/apps/accounts/services/otp.py](backend/apps/accounts/services/otp.py)`).
|
||||||
|
- Phone-first auth endpoints that issue JWT tokens on successful OTP verification, plus basic email/password registration.
|
||||||
|
- Social login endpoint is a placeholder that always returns 501.
|
||||||
|
- **Salons & Catalog** (`[backend/apps/salons](backend/apps/salons)`)
|
||||||
|
- Models for `Salon`, `Service`, `StaffProfile`, `StaffAvailability`, `Review`, `SalonPhoto`.
|
||||||
|
- Read-only APIs for salon search, services, staff, and reviews.
|
||||||
|
- **Bookings** (`[backend/apps/bookings](backend/apps/bookings)`)
|
||||||
|
- `Booking` model ties together salon, service, staff, customer, time window, price, and status.
|
||||||
|
- `validate_booking_request` in `[backend/apps/bookings/services.py](backend/apps/bookings/services.py)` enforces staff membership, duration matching, availability windows, and overlap prevention for pending/confirmed bookings.
|
||||||
|
- `BookingViewSet` in `[backend/apps/bookings/views.py](backend/apps/bookings/views.py)` applies role-based access and hooks into notifications on create and relevant status changes.
|
||||||
|
- **Payments (Moyasar)** (`[backend/apps/payments](backend/apps/payments)`)
|
||||||
|
- `Payment` model tracks provider, status (fine-grained transitions), amount, idempotency key, external ID, payload, and timestamps.
|
||||||
|
- `MoyasarGateway` in `[backend/apps/payments/services/gateway.py](backend/apps/payments/services/gateway.py)` can create payments via HTTP but has `capture_payment`/`refund_payment` as TODOs.
|
||||||
|
- `create_payment_for_booking` in `[backend/apps/payments/services/payments.py](backend/apps/payments/services/payments.py)` enforces provider choice (Moyasar only), idempotency, and maps webhook events into internal statuses.
|
||||||
|
- Webhook view in `[backend/apps/payments/views.py](backend/apps/payments/views.py)` authenticates via shared secret and applies provider events idempotently.
|
||||||
|
- **Notifications** (`[backend/apps/notifications](backend/apps/notifications)`)
|
||||||
|
- `Notification` model records booking-related notifications and enforces uniqueness per booking/recipient/event/channel.
|
||||||
|
- Services in `[backend/apps/notifications/services.py](backend/apps/notifications/services.py)` reuse OTP providers to send SMS/WhatsApp messages for booking created/confirmed/cancelled events, with localization via `preferred_language`.
|
||||||
|
- Booking views call `notify_booking_lifecycle` / `notify_on_status_change` to trigger notifications for customers and staff.
|
||||||
|
- **Testing**
|
||||||
|
- Solid tests around:
|
||||||
|
- Phone normalization, OTP rate limiting, and phone auth flows (`[backend/apps/accounts/tests](backend/apps/accounts/tests)`).
|
||||||
|
- Booking integrity and overlap rules (`[backend/apps/bookings/tests](backend/apps/bookings/tests)`).
|
||||||
|
- Payment idempotency and Moyasar webhook handling (`[backend/apps/payments/tests](backend/apps/payments/tests)`).
|
||||||
|
- Booking notifications on create/status change (`[backend/apps/notifications/tests](backend/apps/notifications/tests)`).
|
||||||
|
- `docs/risks.md` explicitly tracks several known gaps around auth, booking, payments, data/UX, and ops.
|
||||||
|
|
||||||
The core data model centers on users, salons, and time-bound bookings. A booking ties a customer to a service, a staff member, and a scheduled time. Payments are recorded per booking and reconcile to the external gateway. Notifications are stored for every booking lifecycle message for auditability.
|
### Frontend (React, Vite)
|
||||||
|
|
||||||
- `accounts.User` owns phone, locale, and auth preferences.
|
- **Structure**
|
||||||
- `salons.Salon`, `salons.Service`, and `salons.Staff` define the catalog and scheduling surface.
|
- Vite React app at `[frontend](frontend)` with entry in `[frontend/src/main.jsx](frontend/src/main.jsx)` and single top-level component in `[frontend/src/App.jsx](frontend/src/App.jsx)`.
|
||||||
- `bookings.Booking` links customer, staff, service, and scheduled time, with status transitions.
|
- No `react-router` or multi-page routing; the entire experience is one composed screen.
|
||||||
- `payments.Payment` tracks gateway state and idempotency per booking.
|
- **Current Features**
|
||||||
- `notifications.Notification` records each SMS/WhatsApp send attempt tied to a booking event.
|
- **Salon search**
|
||||||
|
- Text search field calling `/salons/?q=<query>` via a small API client in `[frontend/src/api/client.js](frontend/src/api/client.js)`.
|
||||||
|
- Renders responsive list of salons with rating, city, and phone.
|
||||||
|
- **Localization/i18n**
|
||||||
|
- `react-i18next` setup in `[frontend/src/i18n/index.js](frontend/src/i18n/index.js)` with `en` and `ar-sa` translations.
|
||||||
|
- Locale preference stored in `localStorage`; applies `lang` and `dir` on the document.
|
||||||
|
- **Payments beta**
|
||||||
|
- A form in `App.jsx` that sends payment creation requests to `/api/payments/` using the Moyasar-style payload, with configurable `booking_id`, source type, token, and callback URL.
|
||||||
|
- Optionally includes a Bearer token from a manually-entered access token field.
|
||||||
|
- On success, can redirect to `redirect_url` and shows the raw JSON response.
|
||||||
|
- **State & Tests**
|
||||||
|
- All state is local to `App.jsx` via `useState`/`useEffect`; there is no centralized state management or domain hooks yet.
|
||||||
|
- A single test file `[frontend/src/App.test.jsx](frontend/src/App.test.jsx)` covers hero copy and locale/RTL behavior, but not search or payments.
|
||||||
|
|
||||||
## Data Flow
|
## Glaring Design And Architectural Issues
|
||||||
|
|
||||||
```
|
### Backend Risks
|
||||||
User → React Frontend → Django API
|
|
||||||
↓
|
- **Incomplete provider implementations for production-critical flows**
|
||||||
accounts (auth) ──→ OTP providers (Twilio/Unifonic/console)
|
- Authentica is the only OTP/notification provider; ensure it is fully configured and exercised in production-like environments.
|
||||||
salons (catalog)
|
- `MoyasarGateway` lacks `capture_payment` and `refund_payment` implementations, limiting payment lifecycle coverage.
|
||||||
bookings ──→ notifications ──→ OTP providers
|
- **Risk**: Code reads “production ready” at the API level, but the underlying integrations are not, which could cause outages if deployed naively.
|
||||||
payments ──→ Moyasar gateway
|
- **Tight coupling between OTP and notifications**
|
||||||
|
- Notification services import the OTP provider mapping and default `NOTIFICATION_PROVIDER` to `OTP_PROVIDER`, binding booking notifications to auth configuration.
|
||||||
|
- **Risk**: Changing OTP providers or adding a second channel for marketing/ops notifications will be harder and could have unintended side effects.
|
||||||
|
- **Synchronous IO-heavy work in request/response path**
|
||||||
|
- OTP sends, booking notifications, and payment gateway calls all occur synchronously inside view methods (`perform_create`, `create`, etc.).
|
||||||
|
- **Risk**: Slow or flaky providers will degrade API latency and user experience; retries and backoff are hard to implement without background jobs.
|
||||||
|
- **Cross-app domain coupling without a clear orchestration layer**
|
||||||
|
- `apps.bookings` depends on salons and notifications; notifications depend on accounts (OTP providers) and bookings; payments depend on bookings.
|
||||||
|
- **Risk**: As you add more lifecycle rules (e.g., auto-confirm booking on payment, send reminders, handle refunds), the spaghetti of cross-imports will grow unless you introduce clearer service boundaries.
|
||||||
|
- **Auth model vs login patterns**
|
||||||
|
- `User.USERNAME_FIELD` is email, while phone-based JWT issuance happens via custom endpoints.
|
||||||
|
- **Risk**: This split can confuse clients and admin tooling and may complicate future flows like social login or SSO unless you standardize on an identifier strategy.
|
||||||
|
- **Docs drift around ExecPlans**
|
||||||
|
- `AGENTS.md` references `docs/execplans/payments-moyasar.md` as the active plan, while `PLANS.md` names `docs/execplans/booking-notifications.md`.
|
||||||
|
- **Risk**: Contributors may follow different “active” plans, causing architectural inconsistency.
|
||||||
|
|
||||||
|
### Frontend Risks
|
||||||
|
|
||||||
|
- **Monolithic `App` component with no routing**
|
||||||
|
- `App.jsx` mixes hero/search, salon listing, payments, and locale controls.
|
||||||
|
- There is no `react-router` or notion of separate flows (auth, booking, profile, payments).
|
||||||
|
- **Risk**: Extending to full MVP flows (auth, booking, history, management) will quickly become unmanageable without a routing/page system and domain separation.
|
||||||
|
- **Domain logic embedded in UI components**
|
||||||
|
- API payload construction, validation rules (e.g. for source types), and error handling are implemented directly in `App.jsx` rather than reusable hooks or service modules.
|
||||||
|
- **Risk**: Code reuse, testing, and evolution (e.g., adding booking pages or admin consoles) will be painful.
|
||||||
|
- **Minimal test coverage for critical flows**
|
||||||
|
- Only i18n and hero copy are tested; search behavior, API integration, and payments are untested.
|
||||||
|
- **Risk**: Regressions in search, booking, and payments UX will slip through as MVP grows.
|
||||||
|
- **Styling & layout fragility**
|
||||||
|
- `frontend/src/styles.css` uses `::root` instead of `:root`, which likely breaks intended global CSS variables or base styles.
|
||||||
|
- Global CSS is tightly bound to the monolithic `App` layout.
|
||||||
|
- **Risk**: Visual regressions and layout churn when introducing additional pages or components.
|
||||||
|
- **Ad hoc auth token handling**
|
||||||
|
- The “access token” is a free-form text field that gets persisted as `auth_token` in `localStorage` and injected into payment requests.
|
||||||
|
- **Risk**: This is a placeholder pattern that does not scale to full auth (refresh tokens, logout, token rotation) and will need to be replaced.
|
||||||
|
|
||||||
|
### Cross-Cutting Risks
|
||||||
|
|
||||||
|
- **Lack of async/background processing**
|
||||||
|
- No Celery/RQ or similar job queue; all side effects are synchronous.
|
||||||
|
- **Risk**: Scaling SMS/WhatsApp notifications, email, and payment webhook fan-out will be difficult.
|
||||||
|
- **Observability and admin tooling gaps**
|
||||||
|
- Errors for payments and notifications are recorded in model metadata but not clearly surfaced in logs, dashboards, or admin views.
|
||||||
|
- **Risk**: Operational debugging during MVP rollout will be slower and more error-prone.
|
||||||
|
- **Internationalization strategy vs future markets**
|
||||||
|
- Phone normalization and defaults are tailored to KSA, which is correct for MVP, but `docs/risks.md` already notes the need to broaden later.
|
||||||
|
- **Risk**: Without clear boundaries between KSA-specific logic and generic logic, future expansion may require invasive changes.
|
||||||
|
|
||||||
|
## MVP Roadmap (Aligned To Phase 1)
|
||||||
|
|
||||||
|
This roadmap assumes “MVP” is equivalent to **Phase 1: Core MVP Reliability** in `AGENTS.md`, with a thin but robust frontend on top.
|
||||||
|
|
||||||
|
### Phase 0 – Architecture & Production Readiness Hardening
|
||||||
|
|
||||||
|
- **Finalize critical provider implementations**
|
||||||
|
- Ensure Authentica OTP/SMS is fully configured and validated end-to-end behind the provider abstraction in `[backend/apps/accounts/services/otp.py](backend/apps/accounts/services/otp.py)`, and wired into `[backend/apps/notifications/services.py](backend/apps/notifications/services.py)`.
|
||||||
|
- Implement or deliberately fence off `capture_payment` and `refund_payment` in `[backend/apps/payments/services/gateway.py](backend/apps/payments/services/gateway.py)` so that the MVP either fully supports or explicitly does not support partial captures/refunds.
|
||||||
|
- **Clarify and document boundaries**
|
||||||
|
- Add a short architecture section in `README`/docs describing how `accounts`, `salons`, `bookings`, `payments`, and `notifications` interact, and what each service is responsible for.
|
||||||
|
- Resolve the ExecPlan drift by making `AGENTS.md` and `PLANS.md` agree on the current active plan.
|
||||||
|
- **Introduce minimal async infrastructure (optional but recommended)**
|
||||||
|
- Decide whether MVP will ship with a task queue (e.g., Celery with Redis) or keep everything synchronous for the initial launch.
|
||||||
|
- If yes, introduce a thin task layer for OTP sends and booking notifications while preserving current APIs; if not, at least add clear timeouts/logging to external calls.
|
||||||
|
- **Frontend scaffolding for growth**
|
||||||
|
- Introduce `react-router` and refactor `App.jsx` into route-based pages (e.g., `HomePage`, `BookPage`, `PaymentPage`, `ProfilePage`), with shared layout and navigation.
|
||||||
|
- Extract salon search, payment form, and locale controls into dedicated components and hooks.
|
||||||
|
|
||||||
|
### Phase 1 – Core MVP Features (Backend + Frontend)
|
||||||
|
|
||||||
|
- **Phone-first auth UX**
|
||||||
|
- Backend: reuse existing phone auth endpoints; ensure error messages and rate-limit responses are predictable and localized.
|
||||||
|
- Frontend:
|
||||||
|
- Build OTP-based login/registration screens that drive `/api/auth/phone/request/` and `/api/auth/phone/verify/`.
|
||||||
|
- Introduce an auth context (or similar) to store access/refresh tokens, current user profile, and handle logout.
|
||||||
|
- Defer social login beyond MVP, but keep API surface ready for it.
|
||||||
|
- **Booking search and creation**
|
||||||
|
- Backend is largely ready (booking validation and role-based access); review booking serializers in `[backend/apps/bookings/serializers.py](backend/apps/bookings/serializers.py)` to ensure they expose all fields needed for frontend booking forms.
|
||||||
|
- Frontend:
|
||||||
|
- Build a **booking flow**: pick a salon → choose service → select staff (optional) → select date/time slot (based on availability endpoints) → confirm booking.
|
||||||
|
- Add a “My bookings” page showing upcoming and past bookings, tied into the existing `/api/bookings/` endpoints.
|
||||||
|
- **Payments via Moyasar**
|
||||||
|
- Backend: confirm `create_payment_for_booking` contracts (inputs/outputs) are stable and documented.
|
||||||
|
- Frontend:
|
||||||
|
- Evolve the payments beta UI into a **post-booking payment step** that starts from a selected booking and guides the user into Moyasar’s hosted flow, then shows a status page.
|
||||||
|
- Handle callback/return from Moyasar (even if via manual redirect URL in MVP) and surface payment success/failure to the user.
|
||||||
|
- **Booking lifecycle notifications**
|
||||||
|
- Backend already sends notifications on booking create and status changes; align messaging templates with product UX and ensure localization strings exist.
|
||||||
|
- Frontend: surface notification results implicitly via booking status changes and explicit messages on the booking details page.
|
||||||
|
- **Localization foundations**
|
||||||
|
- Backend: ensure `UserLocaleMiddleware` and translation strings cover all user-visible errors in auth, bookings, payments, and notifications.
|
||||||
|
- Frontend: expand `en/ar-sa` translations to cover auth, booking, and payment flows; verify RTL layouts on the new screens.
|
||||||
|
- **Tests for critical flows**
|
||||||
|
- Backend: extend tests where needed to cover new booking/payment edge cases (e.g., tying booking status to payment status if/when introduced).
|
||||||
|
- Frontend: add Vitest tests for:
|
||||||
|
- Phone auth screen flows (request/verify success + errors).
|
||||||
|
- Booking flow (form validation, happy path, displaying server-side errors).
|
||||||
|
- Payment initiation from an existing booking.
|
||||||
|
|
||||||
|
### Phase 2 – Manager Ops Lite (Post-MVP, partially covered now)
|
||||||
|
|
||||||
|
- **Salon and staff management UI**
|
||||||
|
- Use existing salon and staff models to build basic management pages for salon owners/managers (create/update services, staff, availability).
|
||||||
|
- **Calendar views and rescheduling**
|
||||||
|
- Provide calendar views for staff/managers to view daily/weekly bookings and reschedule or cancel within defined rules.
|
||||||
|
- **Reviews and ratings**
|
||||||
|
- Implement review submission and rating recalculation on the backend, with corresponding frontend components.
|
||||||
|
- **Reporting basics**
|
||||||
|
- Lightweight reports for managers (upcoming bookings, simple revenue summaries based on payment status) using existing payments data.
|
||||||
|
|
||||||
|
### Phase 3 – Scale & Compliance (Later)
|
||||||
|
|
||||||
|
- **Audit logging** for admin actions and booking/payment state changes.
|
||||||
|
- **PDPL/GDPR retention policies** and data export tooling.
|
||||||
|
- **Observability**: structured logging, metrics, and basic dashboards for auth failures, OTP send failures, payment errors, and notification outcomes.
|
||||||
|
|
||||||
|
## Architecture Overview Diagram
|
||||||
|
|
||||||
|
A simplified view of the target MVP data flow:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
user["User (web/mobile)"] --> frontend["ReactFrontend"]
|
||||||
|
frontend --> api["DjangoAPI"]
|
||||||
|
|
||||||
|
api --> accounts["AccountsApp"]
|
||||||
|
api --> salons["SalonsApp"]
|
||||||
|
api --> bookings["BookingsApp"]
|
||||||
|
api --> payments["PaymentsApp"]
|
||||||
|
api --> notifications["NotificationsApp"]
|
||||||
|
|
||||||
|
accounts --> otpProviders["OtpProviders"]
|
||||||
|
notifications --> otpProviders
|
||||||
|
payments --> moyasar["MoyasarGateway"]
|
||||||
|
|
||||||
|
bookings --> notifications
|
||||||
|
bookings --> payments
|
||||||
|
salons --> bookings
|
||||||
```
|
```
|
||||||
|
|
||||||
## Notification / OTP Provider Coupling (MVP Shortcut)
|
|
||||||
|
|
||||||
`notifications/services.py` imports `PROVIDERS` from `apps.accounts.services.otp` and uses OTP provider instances (e.g. `AuthenticaOtpProvider`) to send booking SMSes. This works today because Authentica handles both authentication OTPs and general SMS delivery.
|
|
||||||
|
|
||||||
Consequences of this coupling:
|
This diagram clarifies current coupling and highlights where future refactors (e.g., a dedicated messaging service or payment orchestrator) could sit.
|
||||||
- Notifications and OTP delivery cannot independently use different providers (e.g. Twilio for OTP, Unifonic for notifications).
|
|
||||||
- The `notifications` app is conceptually coupled to the `accounts` app's auth infrastructure.
|
|
||||||
|
|
||||||
This is an acceptable MVP shortcut. Before Phase 2, introduce a dedicated `NotificationProvider` abstraction in `notifications/` (mirroring `OtpProvider`) so the two systems can be configured and tested independently.
|
## Validation And Acceptance For This Plan
|
||||||
|
|
||||||
## Async and Observability (MVP Decision)
|
- The roadmap is accepted when:
|
||||||
|
- It clearly maps current backend and frontend capabilities to the Phase 1 MVP goals in `AGENTS.md`.
|
||||||
**Decision (MVP):** All OTP sends, booking notifications, and payment gateway calls run **synchronously** in the request/response path. No Celery, RQ, or other task queue for the initial launch.
|
- It identifies the highest-risk design/architecture issues that could impede MVP reliability or future evolution.
|
||||||
This is captured in ADR 0001 (`docs/adr/0001-synchronous-external-calls-mvp.md`).
|
- It provides a phased, concrete sequence of work packages that can be implemented via ExecPlans (e.g., booking notifications, Moyasar payments, phone auth UX).
|
||||||
|
- Each major feature area (auth, bookings, payments, notifications, localization, tests) should have or adopt an ExecPlan under `docs/execplans/` in line with `PLANS.md` before implementation begins.
|
||||||
**Rationale:**
|
|
||||||
- Reduces deployment complexity (no Redis, no worker processes).
|
|
||||||
- MVP traffic is expected to be low; synchronous latency is acceptable.
|
|
||||||
- External calls already use timeouts (e.g. Moyasar: 10s, Twilio: SDK default).
|
|
||||||
|
|
||||||
**Future:** When scaling, introduce a task queue (e.g. Celery + Redis) for OTP and notification sends. Payment creation and webhooks should remain synchronous for immediate feedback and idempotency.
|
|
||||||
|
|
||||||
**Observability:** Errors are logged via Python `logging` and stored in model metadata (e.g. `Payment.metadata["gateway_error"]`, `Notification.error_message`). Structured logging and metrics are Phase 3 work.
|
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@ This file tracks known gaps and risks to address in future iterations.
|
|||||||
## Security And Auth
|
## Security And Auth
|
||||||
- Phone normalization is KSA-focused and minimal; broaden for multi-country use.
|
- Phone normalization is KSA-focused and minimal; broaden for multi-country use.
|
||||||
- OTP protections are basic; add device fingerprinting and IP throttling if needed.
|
- OTP protections are basic; add device fingerprinting and IP throttling if needed.
|
||||||
- Authentica OTP provider is implemented (SMS + WhatsApp via Authentica OTP); Unifonic remains a scaffold.
|
- Authentica OTP provider is implemented (SMS + WhatsApp via Authentica OTP).
|
||||||
- Social login is a placeholder.
|
- Social login is a placeholder.
|
||||||
- `USERNAME_FIELD` is now `"phone_number"`; `REQUIRED_FIELDS = []`; `create_superuser` accepts `phone_number`. Admin and `createsuperuser` work correctly for phone-only users.
|
- `USERNAME_FIELD` is now `"phone_number"`; `REQUIRED_FIELDS = []`; `create_superuser` accepts `phone_number`. Admin and `createsuperuser` work correctly for phone-only users.
|
||||||
|
|
||||||
|
|||||||
@@ -1,248 +0,0 @@
|
|||||||
---
|
|
||||||
name: salon-mvp-roadmap
|
|
||||||
overview: High-level roadmap to bring the existing Salon Django/React codebase to a reliable MVP aligned with Phase 1 goals in AGENTS.md, plus a review of current architecture and major risks.
|
|
||||||
todos:
|
|
||||||
- id: backend-providers-readiness
|
|
||||||
content: "Harden backend providers: implement at least one real SMS/WhatsApp provider and clarify Moyasar capture/refund behavior for MVP."
|
|
||||||
status: pending
|
|
||||||
- id: async-and-observability
|
|
||||||
content: Decide on async task infrastructure and observability basics for OTP, notifications, and payments, and document the choice.
|
|
||||||
status: pending
|
|
||||||
- id: frontend-structure-and-routing
|
|
||||||
content: Refactor frontend into routed pages with separated components/hooks for search, auth, booking, and payments.
|
|
||||||
status: pending
|
|
||||||
- id: auth-and-booking-flows
|
|
||||||
content: Implement phone-first auth and end-to-end booking flows on the frontend using existing backend APIs.
|
|
||||||
status: pending
|
|
||||||
- id: payments-and-notifications-ux
|
|
||||||
content: Integrate payment initiation and booking lifecycle notifications into user-facing flows, including success/error handling.
|
|
||||||
status: pending
|
|
||||||
- id: tests-for-critical-flows
|
|
||||||
content: Expand backend and frontend tests to cover auth, booking, payment, and notification critical paths for MVP reliability.
|
|
||||||
status: pending
|
|
||||||
isProject: false
|
|
||||||
---
|
|
||||||
|
|
||||||
# Salon MVP Roadmap And Architecture Review
|
|
||||||
|
|
||||||
## Purpose / Big Picture
|
|
||||||
|
|
||||||
This plan reviews the current Salon codebase (Django backend, React/Vite frontend), highlights architectural and design risks, and lays out a pragmatic roadmap to reach an MVP that aligns with **Phase 1: Core MVP Reliability** in `AGENTS.md`: phone-first auth with OTP, robust booking integrity, Moyasar payments, booking lifecycle notifications, localization foundations, and tests for critical flows.
|
|
||||||
The roadmap assumes a KSA-focused first launch (phone auth and Riyadh timezone defaults) with a clean path to expand later.
|
|
||||||
|
|
||||||
## Current State Summary
|
|
||||||
|
|
||||||
### Backend (Django, DRF)
|
|
||||||
|
|
||||||
- **Project**: `salon_api` in `[backend/salon_api](backend/salon_api)`
|
|
||||||
- `settings.py` configures `AUTH_USER_MODEL = "accounts.User"`, DRF + SimpleJWT, KSA locale defaults, and custom settings for OTP, notifications, and payments.
|
|
||||||
- URLs route to app-level APIs at `/api/auth/`, `/api/salons/`, `/api/bookings/`, `/api/payments/`.
|
|
||||||
- **Auth & Accounts** (`[backend/apps/accounts](backend/apps/accounts)`)
|
|
||||||
- Custom `User` model with phone-first capabilities (`phone_number`, `is_phone_verified`, `preferred_language`, `role`).
|
|
||||||
- Phone normalization services tuned for KSA numbers (`[backend/apps/accounts/services/phone.py](backend/apps/accounts/services/phone.py)`).
|
|
||||||
- OTP domain + service layer with rate limits, cooldowns, expiry, and hashed codes (`[backend/apps/accounts/services/otp.py](backend/apps/accounts/services/otp.py)`).
|
|
||||||
- Phone-first auth endpoints that issue JWT tokens on successful OTP verification, plus basic email/password registration.
|
|
||||||
- Social login endpoint is a placeholder that always returns 501.
|
|
||||||
- **Salons & Catalog** (`[backend/apps/salons](backend/apps/salons)`)
|
|
||||||
- Models for `Salon`, `Service`, `StaffProfile`, `StaffAvailability`, `Review`, `SalonPhoto`.
|
|
||||||
- Read-only APIs for salon search, services, staff, and reviews.
|
|
||||||
- **Bookings** (`[backend/apps/bookings](backend/apps/bookings)`)
|
|
||||||
- `Booking` model ties together salon, service, staff, customer, time window, price, and status.
|
|
||||||
- `validate_booking_request` in `[backend/apps/bookings/services.py](backend/apps/bookings/services.py)` enforces staff membership, duration matching, availability windows, and overlap prevention for pending/confirmed bookings.
|
|
||||||
- `BookingViewSet` in `[backend/apps/bookings/views.py](backend/apps/bookings/views.py)` applies role-based access and hooks into notifications on create and relevant status changes.
|
|
||||||
- **Payments (Moyasar)** (`[backend/apps/payments](backend/apps/payments)`)
|
|
||||||
- `Payment` model tracks provider, status (fine-grained transitions), amount, idempotency key, external ID, payload, and timestamps.
|
|
||||||
- `MoyasarGateway` in `[backend/apps/payments/services/gateway.py](backend/apps/payments/services/gateway.py)` can create payments via HTTP but has `capture_payment`/`refund_payment` as TODOs.
|
|
||||||
- `create_payment_for_booking` in `[backend/apps/payments/services/payments.py](backend/apps/payments/services/payments.py)` enforces provider choice (Moyasar only), idempotency, and maps webhook events into internal statuses.
|
|
||||||
- Webhook view in `[backend/apps/payments/views.py](backend/apps/payments/views.py)` authenticates via shared secret and applies provider events idempotently.
|
|
||||||
- **Notifications** (`[backend/apps/notifications](backend/apps/notifications)`)
|
|
||||||
- `Notification` model records booking-related notifications and enforces uniqueness per booking/recipient/event/channel.
|
|
||||||
- Services in `[backend/apps/notifications/services.py](backend/apps/notifications/services.py)` reuse OTP providers to send SMS/WhatsApp messages for booking created/confirmed/cancelled events, with localization via `preferred_language`.
|
|
||||||
- Booking views call `notify_booking_lifecycle` / `notify_on_status_change` to trigger notifications for customers and staff.
|
|
||||||
- **Testing**
|
|
||||||
- Solid tests around:
|
|
||||||
- Phone normalization, OTP rate limiting, and phone auth flows (`[backend/apps/accounts/tests](backend/apps/accounts/tests)`).
|
|
||||||
- Booking integrity and overlap rules (`[backend/apps/bookings/tests](backend/apps/bookings/tests)`).
|
|
||||||
- Payment idempotency and Moyasar webhook handling (`[backend/apps/payments/tests](backend/apps/payments/tests)`).
|
|
||||||
- Booking notifications on create/status change (`[backend/apps/notifications/tests](backend/apps/notifications/tests)`).
|
|
||||||
- `docs/risks.md` explicitly tracks several known gaps around auth, booking, payments, data/UX, and ops.
|
|
||||||
|
|
||||||
### Frontend (React, Vite)
|
|
||||||
|
|
||||||
- **Structure**
|
|
||||||
- Vite React app at `[frontend](frontend)` with entry in `[frontend/src/main.jsx](frontend/src/main.jsx)` and single top-level component in `[frontend/src/App.jsx](frontend/src/App.jsx)`.
|
|
||||||
- No `react-router` or multi-page routing; the entire experience is one composed screen.
|
|
||||||
- **Current Features**
|
|
||||||
- **Salon search**
|
|
||||||
- Text search field calling `/salons/?q=<query>` via a small API client in `[frontend/src/api/client.js](frontend/src/api/client.js)`.
|
|
||||||
- Renders responsive list of salons with rating, city, and phone.
|
|
||||||
- **Localization/i18n**
|
|
||||||
- `react-i18next` setup in `[frontend/src/i18n/index.js](frontend/src/i18n/index.js)` with `en` and `ar-sa` translations.
|
|
||||||
- Locale preference stored in `localStorage`; applies `lang` and `dir` on the document.
|
|
||||||
- **Payments beta**
|
|
||||||
- A form in `App.jsx` that sends payment creation requests to `/api/payments/` using the Moyasar-style payload, with configurable `booking_id`, source type, token, and callback URL.
|
|
||||||
- Optionally includes a Bearer token from a manually-entered access token field.
|
|
||||||
- On success, can redirect to `redirect_url` and shows the raw JSON response.
|
|
||||||
- **State & Tests**
|
|
||||||
- All state is local to `App.jsx` via `useState`/`useEffect`; there is no centralized state management or domain hooks yet.
|
|
||||||
- A single test file `[frontend/src/App.test.jsx](frontend/src/App.test.jsx)` covers hero copy and locale/RTL behavior, but not search or payments.
|
|
||||||
|
|
||||||
## Glaring Design And Architectural Issues
|
|
||||||
|
|
||||||
### Backend Risks
|
|
||||||
|
|
||||||
- **Incomplete provider implementations for production-critical flows**
|
|
||||||
- Twilio/Unifonic providers in `[backend/apps/accounts/services/otp.py](backend/apps/accounts/services/otp.py)` are stubs with `NotImplementedError` for send methods, yet they are the backbone for both OTP and booking notifications.
|
|
||||||
- `MoyasarGateway` lacks `capture_payment` and `refund_payment` implementations, limiting payment lifecycle coverage.
|
|
||||||
- **Risk**: Code reads “production ready” at the API level, but the underlying integrations are not, which could cause outages if deployed naively.
|
|
||||||
- **Tight coupling between OTP and notifications**
|
|
||||||
- Notification services import the OTP provider mapping and default `NOTIFICATION_PROVIDER` to `OTP_PROVIDER`, binding booking notifications to auth configuration.
|
|
||||||
- **Risk**: Changing OTP providers or adding a second channel for marketing/ops notifications will be harder and could have unintended side effects.
|
|
||||||
- **Synchronous IO-heavy work in request/response path**
|
|
||||||
- OTP sends, booking notifications, and payment gateway calls all occur synchronously inside view methods (`perform_create`, `create`, etc.).
|
|
||||||
- **Risk**: Slow or flaky providers will degrade API latency and user experience; retries and backoff are hard to implement without background jobs.
|
|
||||||
- **Cross-app domain coupling without a clear orchestration layer**
|
|
||||||
- `apps.bookings` depends on salons and notifications; notifications depend on accounts (OTP providers) and bookings; payments depend on bookings.
|
|
||||||
- **Risk**: As you add more lifecycle rules (e.g., auto-confirm booking on payment, send reminders, handle refunds), the spaghetti of cross-imports will grow unless you introduce clearer service boundaries.
|
|
||||||
- **Auth model vs login patterns**
|
|
||||||
- `User.USERNAME_FIELD` is email, while phone-based JWT issuance happens via custom endpoints.
|
|
||||||
- **Risk**: This split can confuse clients and admin tooling and may complicate future flows like social login or SSO unless you standardize on an identifier strategy.
|
|
||||||
- **Docs drift around ExecPlans**
|
|
||||||
- `AGENTS.md` references `docs/execplans/payments-moyasar.md` as the active plan, while `PLANS.md` names `docs/execplans/booking-notifications.md`.
|
|
||||||
- **Risk**: Contributors may follow different “active” plans, causing architectural inconsistency.
|
|
||||||
|
|
||||||
### Frontend Risks
|
|
||||||
|
|
||||||
- **Monolithic `App` component with no routing**
|
|
||||||
- `App.jsx` mixes hero/search, salon listing, payments, and locale controls.
|
|
||||||
- There is no `react-router` or notion of separate flows (auth, booking, profile, payments).
|
|
||||||
- **Risk**: Extending to full MVP flows (auth, booking, history, management) will quickly become unmanageable without a routing/page system and domain separation.
|
|
||||||
- **Domain logic embedded in UI components**
|
|
||||||
- API payload construction, validation rules (e.g. for source types), and error handling are implemented directly in `App.jsx` rather than reusable hooks or service modules.
|
|
||||||
- **Risk**: Code reuse, testing, and evolution (e.g., adding booking pages or admin consoles) will be painful.
|
|
||||||
- **Minimal test coverage for critical flows**
|
|
||||||
- Only i18n and hero copy are tested; search behavior, API integration, and payments are untested.
|
|
||||||
- **Risk**: Regressions in search, booking, and payments UX will slip through as MVP grows.
|
|
||||||
- **Styling & layout fragility**
|
|
||||||
- `frontend/src/styles.css` uses `::root` instead of `:root`, which likely breaks intended global CSS variables or base styles.
|
|
||||||
- Global CSS is tightly bound to the monolithic `App` layout.
|
|
||||||
- **Risk**: Visual regressions and layout churn when introducing additional pages or components.
|
|
||||||
- **Ad hoc auth token handling**
|
|
||||||
- The “access token” is a free-form text field that gets persisted as `auth_token` in `localStorage` and injected into payment requests.
|
|
||||||
- **Risk**: This is a placeholder pattern that does not scale to full auth (refresh tokens, logout, token rotation) and will need to be replaced.
|
|
||||||
|
|
||||||
### Cross-Cutting Risks
|
|
||||||
|
|
||||||
- **Lack of async/background processing**
|
|
||||||
- No Celery/RQ or similar job queue; all side effects are synchronous.
|
|
||||||
- **Risk**: Scaling SMS/WhatsApp notifications, email, and payment webhook fan-out will be difficult.
|
|
||||||
- **Observability and admin tooling gaps**
|
|
||||||
- Errors for payments and notifications are recorded in model metadata but not clearly surfaced in logs, dashboards, or admin views.
|
|
||||||
- **Risk**: Operational debugging during MVP rollout will be slower and more error-prone.
|
|
||||||
- **Internationalization strategy vs future markets**
|
|
||||||
- Phone normalization and defaults are tailored to KSA, which is correct for MVP, but `docs/risks.md` already notes the need to broaden later.
|
|
||||||
- **Risk**: Without clear boundaries between KSA-specific logic and generic logic, future expansion may require invasive changes.
|
|
||||||
|
|
||||||
## MVP Roadmap (Aligned To Phase 1)
|
|
||||||
|
|
||||||
This roadmap assumes “MVP” is equivalent to **Phase 1: Core MVP Reliability** in `AGENTS.md`, with a thin but robust frontend on top.
|
|
||||||
|
|
||||||
### Phase 0 – Architecture & Production Readiness Hardening
|
|
||||||
|
|
||||||
- **Finalize critical provider implementations**
|
|
||||||
- Implement at least one real SMS/WhatsApp provider (Twilio or Unifonic) end-to-end, behind the existing provider abstraction in `[backend/apps/accounts/services/otp.py](backend/apps/accounts/services/otp.py)`, and wire it into `[backend/apps/notifications/services.py](backend/apps/notifications/services.py)`.
|
|
||||||
- Implement or deliberately fence off `capture_payment` and `refund_payment` in `[backend/apps/payments/services/gateway.py](backend/apps/payments/services/gateway.py)` so that the MVP either fully supports or explicitly does not support partial captures/refunds.
|
|
||||||
- **Clarify and document boundaries**
|
|
||||||
- Add a short architecture section in `README`/docs describing how `accounts`, `salons`, `bookings`, `payments`, and `notifications` interact, and what each service is responsible for.
|
|
||||||
- Resolve the ExecPlan drift by making `AGENTS.md` and `PLANS.md` agree on the current active plan.
|
|
||||||
- **Introduce minimal async infrastructure (optional but recommended)**
|
|
||||||
- Decide whether MVP will ship with a task queue (e.g., Celery with Redis) or keep everything synchronous for the initial launch.
|
|
||||||
- If yes, introduce a thin task layer for OTP sends and booking notifications while preserving current APIs; if not, at least add clear timeouts/logging to external calls.
|
|
||||||
- **Frontend scaffolding for growth**
|
|
||||||
- Introduce `react-router` and refactor `App.jsx` into route-based pages (e.g., `HomePage`, `BookPage`, `PaymentPage`, `ProfilePage`), with shared layout and navigation.
|
|
||||||
- Extract salon search, payment form, and locale controls into dedicated components and hooks.
|
|
||||||
|
|
||||||
### Phase 1 – Core MVP Features (Backend + Frontend)
|
|
||||||
|
|
||||||
- **Phone-first auth UX**
|
|
||||||
- Backend: reuse existing phone auth endpoints; ensure error messages and rate-limit responses are predictable and localized.
|
|
||||||
- Frontend:
|
|
||||||
- Build OTP-based login/registration screens that drive `/api/auth/phone/request/` and `/api/auth/phone/verify/`.
|
|
||||||
- Introduce an auth context (or similar) to store access/refresh tokens, current user profile, and handle logout.
|
|
||||||
- Defer social login beyond MVP, but keep API surface ready for it.
|
|
||||||
- **Booking search and creation**
|
|
||||||
- Backend is largely ready (booking validation and role-based access); review booking serializers in `[backend/apps/bookings/serializers.py](backend/apps/bookings/serializers.py)` to ensure they expose all fields needed for frontend booking forms.
|
|
||||||
- Frontend:
|
|
||||||
- Build a **booking flow**: pick a salon → choose service → select staff (optional) → select date/time slot (based on availability endpoints) → confirm booking.
|
|
||||||
- Add a “My bookings” page showing upcoming and past bookings, tied into the existing `/api/bookings/` endpoints.
|
|
||||||
- **Payments via Moyasar**
|
|
||||||
- Backend: confirm `create_payment_for_booking` contracts (inputs/outputs) are stable and documented.
|
|
||||||
- Frontend:
|
|
||||||
- Evolve the payments beta UI into a **post-booking payment step** that starts from a selected booking and guides the user into Moyasar’s hosted flow, then shows a status page.
|
|
||||||
- Handle callback/return from Moyasar (even if via manual redirect URL in MVP) and surface payment success/failure to the user.
|
|
||||||
- **Booking lifecycle notifications**
|
|
||||||
- Backend already sends notifications on booking create and status changes; align messaging templates with product UX and ensure localization strings exist.
|
|
||||||
- Frontend: surface notification results implicitly via booking status changes and explicit messages on the booking details page.
|
|
||||||
- **Localization foundations**
|
|
||||||
- Backend: ensure `UserLocaleMiddleware` and translation strings cover all user-visible errors in auth, bookings, payments, and notifications.
|
|
||||||
- Frontend: expand `en/ar-sa` translations to cover auth, booking, and payment flows; verify RTL layouts on the new screens.
|
|
||||||
- **Tests for critical flows**
|
|
||||||
- Backend: extend tests where needed to cover new booking/payment edge cases (e.g., tying booking status to payment status if/when introduced).
|
|
||||||
- Frontend: add Vitest tests for:
|
|
||||||
- Phone auth screen flows (request/verify success + errors).
|
|
||||||
- Booking flow (form validation, happy path, displaying server-side errors).
|
|
||||||
- Payment initiation from an existing booking.
|
|
||||||
|
|
||||||
### Phase 2 – Manager Ops Lite (Post-MVP, partially covered now)
|
|
||||||
|
|
||||||
- **Salon and staff management UI**
|
|
||||||
- Use existing salon and staff models to build basic management pages for salon owners/managers (create/update services, staff, availability).
|
|
||||||
- **Calendar views and rescheduling**
|
|
||||||
- Provide calendar views for staff/managers to view daily/weekly bookings and reschedule or cancel within defined rules.
|
|
||||||
- **Reviews and ratings**
|
|
||||||
- Implement review submission and rating recalculation on the backend, with corresponding frontend components.
|
|
||||||
- **Reporting basics**
|
|
||||||
- Lightweight reports for managers (upcoming bookings, simple revenue summaries based on payment status) using existing payments data.
|
|
||||||
|
|
||||||
### Phase 3 – Scale & Compliance (Later)
|
|
||||||
|
|
||||||
- **Audit logging** for admin actions and booking/payment state changes.
|
|
||||||
- **PDPL/GDPR retention policies** and data export tooling.
|
|
||||||
- **Observability**: structured logging, metrics, and basic dashboards for auth failures, OTP send failures, payment errors, and notification outcomes.
|
|
||||||
|
|
||||||
## Architecture Overview Diagram
|
|
||||||
|
|
||||||
A simplified view of the target MVP data flow:
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart LR
|
|
||||||
user["User (web/mobile)"] --> frontend["ReactFrontend"]
|
|
||||||
frontend --> api["DjangoAPI"]
|
|
||||||
|
|
||||||
api --> accounts["AccountsApp"]
|
|
||||||
api --> salons["SalonsApp"]
|
|
||||||
api --> bookings["BookingsApp"]
|
|
||||||
api --> payments["PaymentsApp"]
|
|
||||||
api --> notifications["NotificationsApp"]
|
|
||||||
|
|
||||||
accounts --> otpProviders["OtpProviders"]
|
|
||||||
notifications --> otpProviders
|
|
||||||
payments --> moyasar["MoyasarGateway"]
|
|
||||||
|
|
||||||
bookings --> notifications
|
|
||||||
bookings --> payments
|
|
||||||
salons --> bookings
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
This diagram clarifies current coupling and highlights where future refactors (e.g., a dedicated messaging service or payment orchestrator) could sit.
|
|
||||||
|
|
||||||
## Validation And Acceptance For This Plan
|
|
||||||
|
|
||||||
- The roadmap is accepted when:
|
|
||||||
- It clearly maps current backend and frontend capabilities to the Phase 1 MVP goals in `AGENTS.md`.
|
|
||||||
- It identifies the highest-risk design/architecture issues that could impede MVP reliability or future evolution.
|
|
||||||
- It provides a phased, concrete sequence of work packages that can be implemented via ExecPlans (e.g., booking notifications, Moyasar payments, phone auth UX).
|
|
||||||
- Each major feature area (auth, bookings, payments, notifications, localization, tests) should have or adopt an ExecPlan under `docs/execplans/` in line with `PLANS.md` before implementation begins.
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user