chore: condense all docs and markdown files

This commit is contained in:
2026-03-14 15:11:40 +03:00
parent f3811b7520
commit 8b626a940e
24 changed files with 483 additions and 1346 deletions
+26 -101
View File
@@ -1,120 +1,45 @@
# Arabic Localization Readiness (ar-sa First)
# Arabic Localization Foundations
This ExecPlan is a living document. The sections `Progress`, `Surprises & Discoveries`, `Decision Log`, and `Outcomes & Retrospective` must be kept up to date as work proceeds.
The requirements for ExecPlans live in `PLANS.md` at the repository root. This document must be maintained in accordance with that file.
This ExecPlan follows `docs/PLANS.md`.
## Purpose / Big Picture
After this change, the codebase has localization foundations in place: locale selection, right-to-left layout support, and a user language preference in the backend. Arabic is treated as a first-class locale for structure and behavior, but full translation coverage is intentionally deferred until core backend flows stabilize. You can see it working by starting the backend and frontend, switching the language to Arabic, and observing RTL layout, the page `dir="rtl"`, and the API responding with the correct `Content-Language` when sending `Accept-Language: ar-sa`.
Ship i18n plumbing for `ar-sa` first (locale selection, RTL behavior, user preference), with full translation breadth deferred.
## Progress
- [x] (2026-02-27 00:00Z) Created initial ExecPlan for Arabic localization readiness.
- [x] (2026-02-28 12:00Z) Added backend locale settings, LocaleMiddleware, user language preference, and user locale middleware.
- [x] (2026-02-28 12:10Z) Wrapped backend user-facing strings for future translation (no full catalog yet).
- [x] (2026-02-28 12:20Z) Added frontend i18n, RTL support, language persistence, and minimal seed translations.
- [x] (2026-02-28 12:30Z) Added targeted backend and frontend tests for locale selection and RTL behavior.
- [x] (2026-02-28 12:40Z) Updated documentation and risks for staged localization.
- [x] (2026-02-27 00:00Z) Plan created.
- [x] (2026-02-28 12:20Z) Backend locale middleware + user preference landed.
- [x] (2026-02-28 12:30Z) Frontend i18n + RTL switching landed.
- [x] (2026-02-28 12:40Z) Basic tests/docs updated.
- [ ] Complete translation coverage and broader RTL QA as features expand.
## Surprises & Discoveries
- Observation: None yet.
Evidence: No implementation work has started.
- Observation: none critical after implementation.
Evidence: targeted locale/RTL tests passed for initial scope.
## Decision Log
- Decision: Use `ar-sa` as the default locale with English as a fallback.
Rationale: The product is KSA-focused and Arabic should be primary while keeping English for mixed audiences.
Date/Author: 2026-02-27, Codex
- Decision: Implement localization foundations now and defer full translation coverage.
Rationale: Early i18n plumbing avoids future refactors, while delaying full translation prevents churn as features evolve.
Date/Author: 2026-02-28, Codex
- Decision: Use lower-case `ar-sa` for locale identifiers in code and storage.
Rationale: Django language codes are lower-case; standardizing avoids mismatches between backend and frontend.
Date/Author: 2026-02-28, Codex
- Decision: Persist user language preference on the `User` model and fall back to `Accept-Language` for anonymous requests.
Rationale: This provides consistent localized behavior for logged-in users while respecting browser preferences for guests.
Date/Author: 2026-02-27, Codex
- Decision: Localize both backend API messages and frontend UI strings.
Rationale: A partial localization would create mismatched language experiences and confuse users.
Date/Author: 2026-02-27, Codex
- Decision: Use `i18next` + `react-i18next` with a small custom locale selection helper rather than a detection plugin.
Rationale: The project is small and can avoid extra dependencies while still meeting locale selection requirements.
Date/Author: 2026-02-27, Codex
- Decision: `ar-sa` default locale with `en` fallback.
Rationale: KSA-first product target.
Date/Author: 2026-02-27/Codex
- Decision: deliver foundations first, defer full string coverage.
Rationale: reduce churn while core product flows are still evolving.
Date/Author: 2026-02-28/Codex
## Outcomes & Retrospective
Localization foundations are now in place across backend and frontend, with user preference support, RTL layout, minimal Arabic strings, and basic tests. Full translation coverage and broader RTL QA remain as future work once core flows stabilize.
Foundations complete; translation completeness remains open.
## Context and Orientation
The backend is a Django + DRF app in `backend/` with settings in `backend/salon_api/settings.py`. The frontend is a Vite + React app in `frontend/` with the entrypoint at `frontend/src/main.jsx` and global styles in `frontend/src/styles.css`. Localization foundations now exist: Django `LocaleMiddleware` is configured and `apps/accounts/middleware.py` applies user preferences, while the frontend initializes `i18next` in `frontend/src/i18n/index.js` and sets `lang`/`dir` on the root element. User-facing strings have begun to be wrapped for translation, but full Arabic translation coverage remains pending.
- Backend locale settings/middleware: `backend/salon_api/settings.py`, `backend/apps/accounts/middleware.py`
- Frontend i18n/RTL: `frontend/src/i18n/`, `frontend/src/main.jsx`, `frontend/src/styles.css`
## Plan of Work
First, add Django locale support. Update `backend/salon_api/settings.py` to define `LANGUAGE_CODE="ar-sa"`, `LANGUAGES` with Arabic and English, `LOCALE_PATHS` pointing to `backend/locale`, and add `django.middleware.locale.LocaleMiddleware` to `MIDDLEWARE` after `SessionMiddleware`. Create `backend/apps/accounts/middleware.py` with `UserLocaleMiddleware` that activates `request.user.preferred_language` after `AuthenticationMiddleware` and sets the response `Content-Language` header. Add a `preferred_language` field to `backend/apps/accounts/models.py` and expose it via `backend/apps/accounts/serializers.py` so `/api/auth/me/` can read and update it.
Next, wrap all user-facing backend strings in translation wrappers. Use `from django.utils.translation import gettext_lazy as _` in serializers and models, and `gettext` in runtime view responses. Cover custom messages in `apps/accounts`, `apps/bookings`, `apps/payments`, and `apps/salons`. Do not translate the backend catalog yet; full Arabic API messages are a later milestone once core flows stabilize. Update or add tests that confirm language selection by user preference and `Accept-Language` headers.
Then, add frontend localization. Introduce an `frontend/src/i18n/` module that sets up `i18next` with `en` and `ar-sa` resource files. Update `frontend/src/main.jsx` to initialize i18n before rendering `App`, set `document.documentElement.lang` and `dir` whenever language changes, and persist the selected locale to local storage. Update `frontend/src/api/client.js` to include the `Accept-Language` header using the active locale. Replace hard-coded UI strings in `frontend/src/App.jsx` with `t(...)` keys and add minimal Arabic translations for the current UI.
Finally, make the UI RTL-safe. Update `frontend/src/styles.css` to use logical properties (`margin-inline`, `padding-inline`, `text-align: start`) where relevant, add `:dir(rtl)` overrides for layout if needed, and add an Arabic-capable font such as `Noto Sans Arabic` to the font stack. Validate end-to-end behavior by running the backend and frontend, switching language, and confirming the UI renders RTL and API responses match the selected locale. Full translation coverage remains a later milestone.
## Concrete Steps
Run these commands from the repository root (`/home/kopernikus/kshkool/Salon`).
1. Add backend locale middleware, settings, and `preferred_language` field, then create a migration.
- Update `backend/salon_api/settings.py`, `backend/apps/accounts/models.py`, and add `backend/apps/accounts/middleware.py`.
- Run:
python3 backend/manage.py makemigrations accounts
2. (Deferred) Generate and compile Arabic translations for the backend when full translation coverage is ready.
- Run:
python3 backend/manage.py makemessages -l ar --ignore frontend --ignore node_modules
python3 backend/manage.py compilemessages
- Edit `backend/locale/ar/LC_MESSAGES/django.po` to translate the newly wrapped strings.
3. Add frontend i18n resources and wire them into the app.
- Update `frontend/package.json`, `frontend/src/main.jsx`, `frontend/src/api/client.js`, `frontend/src/App.jsx`, and create `frontend/src/i18n/index.js` plus translation JSON files.
4. Run tests and verify behavior.
- Backend:
python3 -m pytest
- Frontend:
cd frontend
npm run test
Next phase: expand string coverage and run end-to-end RTL checks on all added routes/components.
## Validation and Acceptance
Backend acceptance is achieved when `Accept-Language` and user preference change the response language header. For example, an OTP error should carry `Content-Language: ar-sa` even if the message text remains English until translations are added:
$ curl -s -H "Accept-Language: ar-sa" -X POST http://localhost:8000/api/auth/otp/request/ -H "Content-Type: application/json" -d '{"phone_number":"123","channel":"sms"}'
{"phone_number":["Phone number is required"]}
Frontend acceptance is achieved when the page renders Arabic text, the root element uses `dir="rtl"`, and the UI remains readable. You should be able to toggle language, reload, and still see Arabic due to stored preference. Running `npm run dev` and visiting the page should show Arabic UI strings when the selected locale is `ar-sa`.
- Backend: locale header behavior matches user preference/`Accept-Language`.
- Frontend: language toggle sets `lang` + `dir`, persists across refresh.
- Commands:
- `cd backend && python3 -m pytest`
- `cd frontend && npm run test`
## Idempotence and Recovery
The locale settings and middleware changes are safe to apply multiple times. Translation commands can be rerun; `makemessages` updates catalogs and `compilemessages` rebuilds `.mo` files. If a translation file is corrupted, re-run `makemessages` and re-apply translations. The migration adding `preferred_language` is additive and reversible via standard Django migration rollback.
## Artifacts and Notes
Expected header behavior after implementing locale selection:
Content-Language: ar-sa
Example local storage entry for the frontend:
localStorage["locale"] = "ar-sa"
## Interfaces and Dependencies
Add backend localization dependencies by using Djangos built-in translation system and middleware (`django.middleware.locale.LocaleMiddleware`) and a new `apps.accounts.middleware.UserLocaleMiddleware` to enforce user preference. The `User` model gains a language preference field:
preferred_language = models.CharField(max_length=10, choices=settings.LANGUAGES, default=settings.LANGUAGE_CODE)
Frontend dependencies must include `i18next` and `react-i18next`. The i18n setup should live in `frontend/src/i18n/index.js`, exporting an initialized i18n instance. The API client in `frontend/src/api/client.js` must attach `Accept-Language` to every request based on the active locale.
Plan Maintenance Note: Initial plan created on 2026-02-27 to scope Arabic localization readiness across backend and frontend. Updated on 2026-02-28 to stage localization work (foundations now, full translations later).
Locale config is additive and safe to reapply; translation catalogs can be regenerated.
+35 -21
View File
@@ -1,34 +1,48 @@
# Phone-first Auth Hardening
This ExecPlan is a living document. It stays synchronized with `docs/PLANS.md` (see the "Queued Next Review Focus" section there) and tracks everything needed to bring the authentication API to a consolidated, phone-first contract with a pre-verified lifecycle and consistent display paths. The remaining work must be test-driven: one sub-flow defines specs/tests, another implements against those specs, and every commit must pass the relevant backend suite.
This ExecPlan follows `docs/PLANS.md`.
## Purpose / Big Picture
Users must be able to log in via phone OTP without a password, the backend must keep phone numbers as the canonical identifier, and every surface that mentions a person should fall back to the phone number when email is absent. The new contract documents the public login/auth endpoints and ensures that the pre-verification lifecycle is deterministic, rate limits stay sensible, and audits display clear phone-first names. The deliverables include updated documentation, new guards/tests against regressions, and polished serializers/models that no longer assume `user.email` exists.
## Milestones
1. Spec & Test Subagent: Formalize and implement the missing specs around pre-verification, OTP purpose safety, rate-limit exposure, and display fallbacks. This milestone produces new pytest modules covering the pre-verification promise, the OTP contract (auth vs verify), and the fallback names used across staff, availability, and reviews. Success is measured by the new tests failing before implementation changes and passing afterward.
2. Implementation Subagent: Update serializers, models, and docs to satisfy the specs. This includes reinforcing the user lifecycle (pre-verify), documenting the intended login surface (phone OTP as source-of-truth, register/token deprecated), tuning rate-limit metadata in responses, and ensuring every display path prefers phone numbers. Implementation is validated by rerunning the pytest suite (`python3 -m pytest backend/apps/accounts/tests backend/apps/salons/tests`).
Keep phone OTP as canonical login surface, preserve phone-first identity across serializers/admin/UI-facing strings, and lock regression tests around this contract.
## Progress
- [x] (2026-03-14 12:00 UTC) Capture the auth gaps in a dedicated ExecPlan and outline the test-first flow for the missing invariants.
- [x] (2026-03-14 13:55 UTC) Added specs/tests for display-name fallbacks, phone auth 404 handling, and serializer coverage so the new contract fails before implementation.
- [x] (2026-03-14 14:30 UTC) Implemented `User.display_name`, updated serializers/models/admin, documented the canonical phone OTP surfaces, and confirmed the specs pass via `python3 -m pytest backend/apps/accounts/tests backend/apps/salons/tests`.
- [x] (2026-03-14 12:00 UTC) Plan created with test-first scope.
- [x] (2026-03-14 13:55 UTC) Added tests for display fallback + phone auth error contracts.
- [x] (2026-03-14 14:30 UTC) Implemented `User.display_name`, serializer/admin updates, and docs updates.
- [ ] Expand tests for OAuth linking policy and remaining phone-first invariants.
## Surprises & Discoveries
- Pytest reports `jwt.api_jwt.InsecureKeyLengthWarning` because the test signing key is 8 bytes long.
Evidence: the two warnings emitted during `python3 -m pytest backend/apps/accounts/tests backend/apps/salons/tests` (see the console output).
- Observation: JWT test key warning appears in suite.
Evidence: `InsecureKeyLengthWarning` during accounts/salons pytest runs.
## Decision Log
- (2026-03-14 12:00 UTC) Committed to the pre-verified user lifecycle: `PhoneAuthRequestView` creates the user (if missing) before sending an auth OTP, and `PhoneAuthVerifyView` marks `is_phone_verified` true immediately upon successful verification.
- (2026-03-14 12:00 UTC) Deferred OAuth linking and non-KSA normalization until after the current auth reliability milestone, per the user request.
- (2026-03-14 14:05 UTC) Added `User.display_name` so every read path has a phone-first fallback and reused it in serializers/models to keep staff/review/booking strings readable for phone-only accounts.
- (2026-03-14 14:07 UTC) Reordered the Django admin list and add forms to highlight `phone_number` so admin workflows no longer depend on email-centric defaults.
- Decision: Pre-create user on phone request; verify on phone verify.
Rationale: deterministic onboarding lifecycle.
Date/Author: 2026-03-14/Codex
- Decision: Add `User.display_name` and reuse everywhere.
Rationale: stable fallback for phone-only accounts.
Date/Author: 2026-03-14/Codex
## Outcomes & Retrospective
Core phone-first hardening landed and tests pass for implemented scope. Remaining work is mainly policy (OAuth linking/conflict) plus extra invariants coverage.
- Phone-first auth now pre-creates customers before OTP sends, marks them verified on `/api/auth/phone/verify/`, and treats passwords as deprecated. Serializers and models no longer fall back to `user.email`; they use `User.display_name` so phone-only accounts always show a meaningful label. Django admin and README/risks docs document the canonical login surface, and the targeted pytest bundle passes with the existing JWT warnings noted above.
## Context and Orientation
- Auth endpoints: `backend/apps/accounts/views.py`
- User model/admin: `backend/apps/accounts/models.py`, `backend/apps/accounts/admin.py`
- Cross-app display paths: `backend/apps/salons/`, `backend/apps/bookings/`
## Plan of Work
1. Keep adding invariant tests first.
2. Finalize OAuth linking/conflict policy and enforce in auth services.
3. Update docs/runbooks/risks with final contract.
## Validation and Acceptance
From `backend/`:
- `python3 -m pytest backend/apps/accounts/tests backend/apps/salons/tests`
Acceptance:
- Phone auth endpoints remain canonical and stable.
- Display paths show phone-first labels when email absent.
- New invariant tests pass.
## Idempotence and Recovery
Auth hardening changes are additive and test-gated. Roll back by app-level revert if a contract regression is detected.
+26 -102
View File
@@ -1,121 +1,45 @@
# Booking Integrity (Availability, Schedules, Overlap Prevention)
# Booking Integrity
This ExecPlan is a living document. The sections `Progress`, `Surprises & Discoveries`, `Decision Log`, and `Outcomes & Retrospective` must be kept up to date as work proceeds.
The requirements for ExecPlans live in `PLANS.md` at the repository root. This document must be maintained in accordance with that file.
This ExecPlan follows `docs/PLANS.md`.
## Purpose / Big Picture
After this change, bookings cannot be created in invalid time windows or in ways that double-book staff. A manager can rely on the system to prevent overlapping appointments and to enforce staff working hours. You can see it working by attempting to create a booking outside a staff members availability window or that overlaps an existing confirmed booking and receiving a clear validation error; creating a booking that fits availability and does not overlap should succeed.
Reject invalid booking windows and staff double-booking; accept only schedule-valid requests.
## Progress
- [x] (2026-02-28 13:05Z) Created ExecPlan for booking integrity (availability, schedules, overlap prevention).
- [x] (2026-02-28 13:25Z) Added staff availability model, admin registration, and manual migration.
- [x] (2026-02-28 13:30Z) Introduced booking validation service for duration, schedule, and overlap checks.
- [x] (2026-02-28 13:32Z) Updated booking create serializer to require staff and enforce validation rules.
- [x] (2026-02-28 13:45Z) Added backend tests covering overlap prevention, availability windows, and duration validation.
- [x] (2026-02-28 13:50Z) Updated `docs/risks.md` to reflect closed booking-integrity gaps.
- [x] (2026-02-28 13:05Z) Plan created.
- [x] (2026-02-28 13:30Z) Validation service implemented (duration/availability/overlap).
- [x] (2026-02-28 13:45Z) Tests added.
- [x] (2026-02-28 13:50Z) Risks updated.
## Surprises & Discoveries
- Observation: Django is not installed in the environment, so `makemigrations` could not run.
Evidence: `ImportError: Couldn't import Django` when running `python3 backend/manage.py makemigrations salons`.
- Observation: `makemigrations` unavailable in one environment due missing Django.
Evidence: import error during initial migration step.
## Decision Log
- Decision: Require `staff` on booking creation to enforce schedule and overlap rules deterministically.
Rationale: Without an assigned staff member, the system cannot guarantee schedule integrity.
Date/Author: 2026-02-28, Codex
- Decision: Treat staff availability as open-ended if no availability records exist for that staff member.
Rationale: This avoids breaking existing workflows while enabling explicit schedule enforcement when configured.
Date/Author: 2026-02-28, Codex
- Decision: Enforce that `end_time - start_time` matches the service duration in minutes.
Rationale: Prevents inconsistent bookings and ensures predictable slot lengths.
Date/Author: 2026-02-28, Codex
- Decision: Add the `StaffAvailability` migration manually instead of using `makemigrations`.
Rationale: Django was unavailable in the environment; a manual migration keeps schema changes explicit and reviewable.
Date/Author: 2026-02-28, Codex
- Decision: require `staff` on booking creation.
Rationale: no deterministic schedule validation without staff.
Date/Author: 2026-02-28/Codex
- Decision: no-availability-records => open schedule.
Rationale: backward compatibility while enabling stricter config when data exists.
Date/Author: 2026-02-28/Codex
## Outcomes & Retrospective
Booking integrity is now enforced via staff availability checks, duration validation, and overlap prevention, with test coverage for each rule. This closes the highest-risk booking integrity gap in `docs/risks.md`, while timezone and business-hours enforcement remain future work.
Completed. Booking integrity checks are active with tests for core failure modes.
## Context and Orientation
Booking creation is implemented in `backend/apps/bookings/serializers.py` (`BookingCreateSerializer`) and routed via `backend/apps/bookings/views.py` in a DRF `ModelViewSet`. The booking model lives in `backend/apps/bookings/models.py`, while staff information is in `backend/apps/salons/models.py` as `StaffProfile`. There is no current scheduling model and no overlap validation. This plan introduces a staff availability model and a dedicated booking validation service to keep business logic out of views, in line with project standards.
- Validation entrypoint: `backend/apps/bookings/services.py`
- Create serializer: `backend/apps/bookings/serializers.py`
- Staff availability model: `backend/apps/salons/models.py`
## Plan of Work
First, add a staff availability model in `backend/apps/salons/models.py`. Create a `StaffAvailability` model with a foreign key to `StaffProfile`, a day-of-week integer (0-6), and start/end times (as `TimeField`). Use an `is_active` boolean to allow disabling entries without deleting them. Register the model in `backend/apps/salons/admin.py` for basic management. Create and apply a migration in the salons app.
Next, add a booking validation service in `backend/apps/bookings/services.py`. The service should expose a function like `validate_booking_request(service, staff, start_time, end_time)` that raises `serializers.ValidationError` or a custom domain error translated into DRF validation errors. It should check:
- `staff` is required and belongs to the same salon as the service.
- `start_time < end_time` and duration matches `service.duration_minutes`.
- Staff availability: if availability records exist for the staff and day-of-week, ensure the booking window is fully inside one availability window with `is_active=True`.
- Overlap: prevent any booking for the same staff with status in `pending` or `confirmed` that overlaps the requested window; `cancelled` and `completed` bookings should not block.
Then, update `BookingCreateSerializer` in `backend/apps/bookings/serializers.py` to call the validation service and to require `staff`. Keep `create` unchanged beyond relying on validated data.
Finally, add tests in `backend/apps/bookings/tests/test_booking_integrity.py`. Cover these cases:
- Reject bookings with no staff assigned.
- Reject bookings where `end_time` precedes `start_time`.
- Reject bookings where duration does not match `service.duration_minutes`.
- Reject bookings outside staff availability when availability records exist.
- Allow bookings when no availability records exist.
- Reject overlapping bookings for the same staff with `pending` or `confirmed` status; allow overlaps with `cancelled` or `completed` bookings.
Update `docs/risks.md` to mark booking integrity gaps as addressed once tests pass.
## Concrete Steps
Run these commands from the repository root (`/home/m7md/kshkool/Salon`).
1. Add staff availability model and migration.
- Edit `backend/apps/salons/models.py` and `backend/apps/salons/admin.py`.
- Run:
python3 backend/manage.py makemigrations salons
2. Add booking validation service and update serializer.
- Create `backend/apps/bookings/services.py` and update `backend/apps/bookings/serializers.py`.
3. Add tests.
- Create `backend/apps/bookings/tests/test_booking_integrity.py`.
4. Run tests.
- Backend:
python3 -m pytest
Completed; remaining future policy work is timezone/business-hours specifics.
## Validation and Acceptance
- Attempting to create a booking without a staff member returns HTTP 400 with a clear validation error.
- Creating a booking outside availability returns HTTP 400 with a clear validation error.
- Creating a booking overlapping an existing pending/confirmed booking for the same staff returns HTTP 400.
- Creating a booking within an availability window and without overlap returns HTTP 201.
- Running `python3 -m pytest` passes, and the new booking-integrity tests fail before the changes and pass after.
From `backend/`:
- `python3 -m pytest backend/apps/bookings/tests`
Acceptance:
- Invalid duration/availability/overlap cases return 400.
- Valid windows return 201.
## Idempotence and Recovery
Model and serializer changes are additive and safe to reapply. If a migration needs to be re-run, it can be rolled back using standard Django migration rollback and re-applied. The validation service is pure and can be iterated without impacting data. If availability rules are too strict, disabling availability entries will effectively remove the constraint without deleting data.
## Artifacts and Notes
Example overlap query used in validation:
Booking.objects.filter(
staff=staff,
status__in=[BookingStatus.PENDING, BookingStatus.CONFIRMED],
start_time__lt=end_time,
end_time__gt=start_time,
)
## Interfaces and Dependencies
- `backend/apps/salons/models.py` must define a new `StaffAvailability` model with fields: `staff` (FK), `day_of_week` (0-6), `start_time`, `end_time`, `is_active`.
- `backend/apps/bookings/services.py` must define `validate_booking_request(service, staff, start_time, end_time)`.
- `backend/apps/bookings/serializers.py` must call the validation service and require `staff` on create.
Plan Maintenance Note: Created on 2026-02-28 to implement booking integrity (availability, schedules, overlap prevention) as the next Phase 1 reliability step.
Plan Maintenance Note (Update): Marked milestones complete and recorded the manual migration decision after implementing booking integrity and tests on 2026-02-28.
Validation is stateless; schema change is additive and reversible by standard migrations.
+24 -85
View File
@@ -1,104 +1,43 @@
# Booking Lifecycle Notifications (SMS/WhatsApp)
# Booking Lifecycle Notifications
This ExecPlan is a living document. The sections `Progress`, `Surprises & Discoveries`, `Decision Log`, and `Outcomes & Retrospective` must be kept up to date as work proceeds.
The requirements for ExecPlans live in `PLANS.md` at the repository root. This document must be maintained in accordance with that file.
This ExecPlan follows `docs/PLANS.md`.
## Purpose / Big Picture
After this change, a booking will automatically notify the customer and the assigned staff member when it is created, confirmed, or cancelled. You can see it working by creating a booking and observing two notification records (customer + staff), then changing the booking status to confirmed or cancelled and seeing two more notification records for that event. In the console provider, the messages are logged, giving an immediate, user-visible trace of the booking lifecycle.
Notify customer and staff on booking created/confirmed/cancelled with auditable notification rows.
## Progress
- [x] (2026-02-28 17:05Z) Created ExecPlan for booking lifecycle notifications and reviewed bookings + notifications gaps.
- [x] (2026-02-28 17:30Z) Implemented notifications app with audit-friendly model, providers, and booking message templates.
- [x] (2026-02-28 17:40Z) Connected booking create/update flows to notification dispatch with idempotent event handling.
- [x] (2026-02-28 17:55Z) Allowed booking status updates with role checks to enable confirmation/cancellation.
- [x] (2026-02-28 18:05Z) Added tests for booking notifications (create, status change, no duplicate sends).
- [x] (2026-02-28 18:10Z) Updated `docs/risks.md` and validated tests (`python3 -m pytest`).
- [x] (2026-02-28 17:05Z) Plan + scope finalized.
- [x] (2026-02-28 17:40Z) Notification app + providers + booking wiring implemented.
- [x] (2026-02-28 18:05Z) Tests added for create/status-change/no-duplicate behavior.
- [x] (2026-02-28 18:10Z) Risks/docs updated.
## Surprises & Discoveries
- Observation: Booking status updates were blocked because `status` was read-only on the default booking serializer.
Evidence: `PATCH /api/bookings/<id>` returned HTTP 400 when attempting to confirm.
- Observation: status field had been read-only for update flow.
Evidence: `PATCH /api/bookings/<id>` validation failure before serializer update.
## Decision Log
- Decision: Store every booking notification in a dedicated `Notification` model for auditability, even when skipped.
Rationale: Lifecycle messages are user-facing and must be traceable for support and compliance.
Date/Author: 2026-02-28, Codex
- Decision: Reuse existing OTP provider adapters for SMS/WhatsApp delivery, with a new `NOTIFICATION_PROVIDER` setting.
Rationale: Avoid duplicate integration code while still allowing independent provider configuration.
Date/Author: 2026-02-28, Codex
- Decision: Default to SMS for booking notifications and use the recipients preferred language when formatting messages.
Rationale: SMS is the most reliable baseline in KSA, and language preference is already captured on the user.
Date/Author: 2026-02-28, Codex
- Decision: Allow booking status changes via `BookingSerializer` with role-based validation.
Rationale: Confirmation/cancellation must be reachable through the existing API, but should still respect basic role boundaries.
Date/Author: 2026-02-28, Codex
- Decision: persist notification records for every lifecycle send attempt.
Rationale: auditability + support traceability.
Date/Author: 2026-02-28/Codex
- Decision: reuse OTP provider abstraction for notification channels.
Rationale: avoid duplicated provider integration.
Date/Author: 2026-02-28/Codex
## Outcomes & Retrospective
Booking lifecycle notifications are now implemented with audit-friendly records and idempotent sending. Booking creation and status changes (confirmed/cancelled) trigger SMS/WhatsApp notifications for both customer and staff, and role-based validation now governs status updates. Provider adapters remain scaffolds, so production delivery still requires real SMS/WhatsApp wiring.
Completed. Booking lifecycle notifications are live with idempotent records and test coverage.
## Context and Orientation
Booking creation and updates are handled in `backend/apps/bookings/views.py` via a DRF `ModelViewSet`. The booking model is in `backend/apps/bookings/models.py`, with `status` indicating lifecycle state. There is currently no notification system beyond OTP scaffolding in `backend/apps/accounts/services/otp.py`. This plan adds a new Django app at `backend/apps/notifications/` to store notification records, format booking lifecycle messages, and dispatch them via SMS or WhatsApp providers.
A “notification” in this repository means a user-facing message (SMS or WhatsApp) that is stored for auditability in a `Notification` database row. A “lifecycle event” is a booking change that should inform the customer and staff: booking created, confirmed, or cancelled.
- Notification domain: `backend/apps/notifications/`
- Booking integration points: `backend/apps/bookings/views.py`
## Plan of Work
First, create a `notifications` Django app with models and admin registration. Define `Notification`, `NotificationEvent`, `NotificationStatus`, and `NotificationChannel` in `backend/apps/notifications/models.py`. The model must capture booking, recipient, phone number, event, channel, status, provider, message, and send timestamps, and it must be idempotent by preventing duplicates for the same booking + recipient + event + channel. Register the model in `backend/apps/notifications/admin.py` and add `apps.notifications` to `INSTALLED_APPS` in `backend/salon_api/settings.py`.
Next, implement notification dispatch in `backend/apps/notifications/services.py`. Reuse OTP provider adapters from `apps.accounts.services.otp` with a new `NOTIFICATION_PROVIDER` setting (default to `OTP_PROVIDER`). Add a `NOTIFICATION_DEFAULT_CHANNEL` setting (default `sms`). Implement `send_booking_notification(booking, recipient, event)` to build localized message text using the recipients preferred language, send via the provider, and update the notification status. Implement `notify_booking_lifecycle(booking, event)` for initial sends and `notify_on_status_change(booking, previous_status)` to trigger only on status transitions. If the recipient lacks a phone number, record the notification as `skipped` with a reason.
Then, wire booking lifecycle events in `backend/apps/bookings/views.py`. On `perform_create`, call `notify_booking_lifecycle(..., booking_created)` so both customer and staff receive a message. On `perform_update`, compare the previous status to the new status and call `notify_on_status_change` for confirmed or cancelled transitions. Avoid sending notifications if the status does not change.
Finally, add tests in `backend/apps/notifications/tests/test_booking_notifications.py`. Cover booking creation (two notifications), status change to confirmed (two notifications), and a repeat status update that should not create duplicates. Ensure tests use phone numbers on users to avoid skipped notifications. Update `docs/risks.md` to mark “No notifications (email/SMS) beyond OTP scaffolding” as addressed once tests pass.
## Concrete Steps
Run these commands from the repository root (`/home/m7md/kshkool/Salon`).
1. Add notifications app code and migrations.
- Create `backend/apps/notifications/` with `apps.py`, `models.py`, `services.py`, `admin.py`, and a migration `0001_initial.py`.
- Update `backend/salon_api/settings.py` to include `apps.notifications` and notification settings.
2. Wire booking lifecycle events.
- Update `backend/apps/bookings/views.py` to call notification services on create and status changes.
3. Add tests.
- Create `backend/apps/notifications/tests/test_booking_notifications.py`.
4. Run backend tests.
- From `backend/` with the venv active:
python3 -m pytest
Completed; future work is provider hardening/monitoring only.
## Validation and Acceptance
- Creating a booking returns HTTP 201 and creates two notification records (customer + staff) with event `booking_created`.
- Updating a bookings status to `confirmed` creates two notification records with event `booking_confirmed`.
- Repeating the same status update does not create duplicate notifications (records remain at two for that event).
- `python3 -m pytest` passes, and the new tests fail before the change and pass after.
From `backend/`:
- `python3 -m pytest backend/apps/notifications/tests`
Acceptance:
- Create/confirm/cancel each emit customer+staff notification rows once per event/channel.
## Idempotence and Recovery
Notification creation is idempotent by a uniqueness constraint on booking + recipient + event + channel. Re-running the send logic will update a pending or failed notification rather than creating duplicates. If a migration needs to be reverted, use standard Django migration rollback and re-apply. If a notification provider is misconfigured, notifications will be marked failed and can be retried after fixing settings.
## Artifacts and Notes
Expected console-provider log example when creating a booking:
INFO OTP SMS to 0500000002: Your booking request is received for Haircut at Main Salon on 2026-03-01 10:00.
INFO OTP SMS to 0500000003: Your booking request is received for Haircut at Main Salon on 2026-03-01 10:00.
## Interfaces and Dependencies
- `backend/apps/notifications/models.py` must define `Notification`, `NotificationEvent`, `NotificationStatus`, `NotificationChannel`.
- `backend/apps/notifications/services.py` must expose `send_booking_notification`, `notify_booking_lifecycle`, and `notify_on_status_change`.
- `backend/apps/bookings/views.py` must call notification services in `perform_create` and `perform_update`.
- `backend/salon_api/settings.py` must define `NOTIFICATION_PROVIDER` and `NOTIFICATION_DEFAULT_CHANNEL` settings.
Plan Maintenance Note: Created on 2026-02-28 to implement booking lifecycle notifications as the next Phase 1 reliability milestone.
Plan Maintenance Note (Update): Marked milestones complete, recorded the booking status update discovery, and documented role-based status validation after implementing notifications and tests on 2026-02-28.
Uniqueness constraints prevent duplicate event/channel sends per recipient/booking; retries update existing rows safely.
+27 -119
View File
@@ -1,138 +1,46 @@
# Payments Integration (Moyasar, Webhooks, Idempotency)
# Payments Integration (Moyasar)
This ExecPlan is a living document. The sections `Progress`, `Surprises & Discoveries`, `Decision Log`, and `Outcomes & Retrospective` must be kept up to date as work proceeds.
The requirements for ExecPlans live in `PLANS.md` at the repository root. This document must be maintained in accordance with that file.
This ExecPlan follows `docs/PLANS.md`.
## Purpose / Big Picture
After this change, the backend can create Moyasar payments, track their state transitions, and reconcile them via webhooks in an idempotent and auditable way. A user can create a booking payment and see it progress from initiated to paid or failed. You can see it working by creating a payment, receiving a webhook callback that marks it as paid, and observing the payment record transition with a recorded provider reference and idempotency key.
Create payments idempotently, reconcile webhook states safely, and keep payment history auditable.
## Progress
- [x] (2026-02-28 14:35Z) Created ExecPlan for payments integration (Moyasar + webhooks + idempotency).
- [x] (2026-02-28 15:05Z) Inspected payments models/endpoints and aligned naming with Moyasar scaffolding.
- [x] (2026-02-28 15:20Z) Defined payment state model extensions and idempotency tracking fields.
- [x] (2026-02-28 15:40Z) Implemented payment creation service and API endpoint with provider gateway.
- [x] (2026-02-28 15:55Z) Implemented webhook endpoint with secret verification and status mapping.
- [x] (2026-02-28 16:10Z) Added tests for creation, idempotency, and webhook reconciliation.
- [x] (2026-02-28 16:20Z) Updated `docs/risks.md` to close payment integration gaps once tested.
- [x] (2026-02-28 14:35Z) Plan created.
- [x] (2026-02-28 15:55Z) Payment creation + webhook processing implemented.
- [x] (2026-02-28 16:10Z) Tests for create/idempotency/webhooks implemented.
- [x] (2026-02-28 16:20Z) Risks updated.
## Surprises & Discoveries
- Observation: The payments gateway needed an HTTP client dependency, so `requests` was added to backend requirements.
Evidence: `ModuleNotFoundError: No module named 'requests'` when running migrations after adding gateway calls.
- Observation: missing `requests` dependency blocked gateway calls initially.
Evidence: `ModuleNotFoundError: requests`.
## Decision Log
- Decision: Model payment state transitions as explicit status changes with audit-friendly timestamps.
Rationale: Payment flows must be auditable and deterministic under retries.
Date/Author: 2026-02-28, Codex
- Decision: Require idempotency keys on payment creation requests.
Rationale: Prevents duplicate charges when clients retry.
Date/Author: 2026-02-28, Codex
- Decision: Use a dedicated webhook endpoint with signature verification.
Rationale: Ensures authenticity of provider callbacks and protects state integrity.
Date/Author: 2026-02-28, Codex
- Decision: Store provider payloads and webhook payloads on the payment record for auditability.
Rationale: Helps trace payment transitions without introducing a separate event table yet.
Date/Author: 2026-02-28, Codex
- Decision: enforce request-level idempotency key for payment create.
Rationale: prevent duplicate charges on retries.
Date/Author: 2026-02-28/Codex
- Decision: persist provider + webhook payloads.
Rationale: payment auditability/debuggability.
Date/Author: 2026-02-28/Codex
## Outcomes & Retrospective
Payment creation, idempotency handling, and webhook reconciliation are implemented for Moyasar. Tests cover creation, idempotency, and webhook status transitions, reducing the largest Phase 1 reliability gap. Refund/capture operations remain future work if required.
Completed for MVP core flow (create + webhook). Remaining work: richer operational monitoring and optional admin capture/refund APIs.
## Context and Orientation
Payments live in `backend/apps/payments/` with current models and API endpoints. The system currently stores payment records but does not integrate with Moyasar or reconcile webhooks. Booking flows live in `backend/apps/bookings/` and should link to payments. The project standards require business logic in services and predictable error responses.
- Payment model/services/views: `backend/apps/payments/`
- Booking dependency: `backend/apps/bookings/`
## Plan of Work
First, review existing payment models and endpoints to avoid breaking field names. Identify whether `Payment` includes a reference to `Booking`, a `provider_reference`, and a status field. If any are missing, add them along with timestamps for `initiated_at`, `paid_at`, and `failed_at`. Create a migration for the new fields. Ensure status choices include at least `initiated`, `pending`, `paid`, `failed`, and `refunded` if refunds are in scope.
Next, introduce idempotency tracking. Add a `idempotency_key` field to the payment model (unique, indexed) and validate that payment creation requests require it. If a request repeats with the same key, return the existing payment without creating a new provider charge.
Then, implement the Moyasar payment creation service in `backend/apps/payments/services.py`. The service should build the provider request using amount, currency, description, and return URLs, and persist the `provider_reference` (payment id returned by Moyasar). Store the full provider response in a JSON field for audit if available.
Add a dedicated API endpoint for payment creation in `backend/apps/payments/views.py` and `backend/apps/payments/urls.py`. It should:
- Require authentication.
- Validate booking ownership and amount.
- Require `idempotency_key`.
- Call the service to create the provider payment.
- Return the payment record plus any provider redirect URL if applicable.
Then, implement the webhook endpoint (`/api/payments/webhook/`) with signature verification using Moyasars secret. It should parse the event, locate the payment by `provider_reference`, apply an idempotent state transition, and record timestamps. Unknown events should be logged but return 200 to avoid retries if possible.
Finally, add tests in `backend/apps/payments/tests/`:
- Creating a payment succeeds and stores provider reference.
- Creating with the same idempotency key returns the original record.
- Webhook for `paid` updates status and timestamp.
- Webhook with invalid signature is rejected.
- Webhook is idempotent (replay does not change state or duplicate logs).
Update `docs/risks.md` to mark payment integration gaps as addressed.
## Concrete Steps
Run these commands from the repository root (`/home/m7md/kshkool/Salon`).
1. Inspect payments models and endpoints.
- Read `backend/apps/payments/models.py`, `backend/apps/payments/views.py`, and `backend/apps/payments/serializers.py`.
2. Add fields for provider reference, status timestamps, and idempotency.
- Update `backend/apps/payments/models.py` and create a migration.
- Run:
python3 backend/manage.py makemigrations payments
3. Implement services and endpoints.
- Add `backend/apps/payments/services.py`.
- Update serializers and views accordingly.
4. Add webhook endpoint and signature verification.
- Update `backend/apps/payments/urls.py` and `backend/apps/payments/views.py`.
5. Add tests.
- Create `backend/apps/payments/tests/test_payments_flow.py`.
6. Run tests.
- Backend:
source venv/bin/activate
cd backend
python3 -m pytest
Completed for current scope.
## Validation and Acceptance
- Creating a payment with a new idempotency key returns HTTP 201 and a provider reference.
- Creating the same payment with the same idempotency key returns HTTP 200/201 with the original payment (no new provider request).
- A valid webhook updates the payment status to `paid` and sets `paid_at`.
- An invalid webhook signature returns HTTP 400/401 and does not mutate data.
- `python3 -m pytest` passes with the new payments tests.
From `backend/`:
- `python3 -m pytest backend/apps/payments/tests`
Acceptance:
- New key => new payment.
- Same key => existing payment reused.
- Valid webhook transitions state idempotently.
- Invalid webhook auth rejected.
## Idempotence and Recovery
Payment creation is safe to retry with idempotency keys. Webhook processing is idempotent and can be replayed safely. If a payment status change is applied incorrectly, it can be corrected manually via admin and will be documented in audit fields.
## Artifacts and Notes
Example idempotency pattern:
existing = Payment.objects.filter(idempotency_key=key).first()
if existing:
return existing
Example overlap-safe webhook logic:
if payment.status == PaymentStatus.PAID:
return
payment.mark_paid()
## Interfaces and Dependencies
- `backend/apps/payments/models.py` must include fields: `provider_reference`, `idempotency_key`, `status`, `initiated_at`, `paid_at`, `failed_at`, and (optionally) `provider_payload` (JSON).
- `backend/apps/payments/services.py` must define `create_payment_for_booking(booking, idempotency_key, request_data)` and `verify_webhook_signature(request)`.
- `backend/apps/payments/views.py` must expose `PaymentCreateAPIView` and `payment_webhook` with signature verification.
Plan Maintenance Note: Created on 2026-02-28 to implement Moyasar payments with idempotency and webhook reconciliation as the next Phase 1 reliability milestone.
Plan Maintenance Note (Update): Marked steps complete and recorded dependency and audit decisions after implementing payments and tests on 2026-02-28.
Idempotency key guards create path; webhook replay is safe.