From db36551211bba5d7f60dc97d8dfe704807320f0f Mon Sep 17 00:00:00 2001 From: mohammad Date: Sat, 28 Feb 2026 13:28:58 +0300 Subject: [PATCH] Document payments sanity check and fix demo seed --- .../salons/management/commands/seed_demo.py | 4 +- docs/runbooks/payments_sanity_check.md | 136 ++++++++++++++++++ 2 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 docs/runbooks/payments_sanity_check.md diff --git a/backend/apps/salons/management/commands/seed_demo.py b/backend/apps/salons/management/commands/seed_demo.py index bd70522..2bc68bb 100644 --- a/backend/apps/salons/management/commands/seed_demo.py +++ b/backend/apps/salons/management/commands/seed_demo.py @@ -126,10 +126,10 @@ class Command(BaseCommand): booking=booking, provider=PaymentProvider.MOYASAR, defaults={ - "status": PaymentStatus.CREATED, + "status": PaymentStatus.INITIATED, "amount": booking.price_amount, "currency": booking.currency, - "external_id": "", + "external_id": None, "metadata": {"note": "Demo payment record"}, }, ) diff --git a/docs/runbooks/payments_sanity_check.md b/docs/runbooks/payments_sanity_check.md new file mode 100644 index 0000000..cb3c37b --- /dev/null +++ b/docs/runbooks/payments_sanity_check.md @@ -0,0 +1,136 @@ +# Payments Sanity Check (Moyasar Mock + Demo Data) + +This runbook documents the end-to-end sanity check for the Moyasar payments flow using demo data and a local mock provider. It is intended for developers and agents validating payment creation + webhook reconciliation before merging to `main`. + +## Purpose + +Verify that the payment creation endpoint and webhook processing work end-to-end in a local environment without hitting Moyasar. + +## Preconditions + +- Backend dependencies installed in the Python venv. +- Frontend is not required for this check. +- `backend/` database is migrated and uses SQLite for local dev. + +## High-level Flow + +1. Start a local mock Moyasar server (HTTP) that emulates `/v1/payments` responses. +2. Run migrations and seed demo data. +3. Start Django with a local payment configuration pointing to the mock server. +4. Obtain a JWT access token for the demo customer. +5. Create a payment for an existing booking. +6. Send a webhook payload to mark it as paid. +7. Verify the payment status updates. + +## Steps + +### 1) Start the mock Moyasar server + +The mock server responds to `POST /v1/payments` with a static `id` and `transaction_url`. + +Create the mock server at `/tmp/moyasar_mock.py` and run it: + + python3 /tmp/moyasar_mock.py + +Expected: the process stays running, listening on `http://127.0.0.1:8001`. + +### 2) Run migrations and seed demo data + + source venv/bin/activate + cd backend + python3 manage.py migrate + python3 manage.py seed_demo + +Expected: `Demo data seeded.` + +### 3) Start Django with the mock provider + +Run the backend with environment variables pointing to the mock server: + + DJANGO_DEBUG=1 \ + MOYASAR_SECRET_KEY=sk_test \ + MOYASAR_PUBLISHABLE_KEY=pk_test \ + MOYASAR_BASE_URL=http://127.0.0.1:8001 \ + MOYASAR_WEBHOOK_SECRET=whsec \ + python3 manage.py runserver 8000 + +Expected: server starts at `http://127.0.0.1:8000/`. + +### 4) Obtain a JWT access token + +The demo customer is: + +- `customer@example.com` +- `Customer123!` + +Fetch the access token: + + curl -s -X POST http://127.0.0.1:8000/api/auth/token/ \ + -H "Content-Type: application/json" \ + -d '{"email":"customer@example.com","password":"Customer123!"}' + +Expected: JSON containing `access` and `refresh` tokens. + +### 5) Create a payment + +Pick a booking (demo data creates bookings; you can list them): + + curl -s -H "Authorization: Bearer " http://127.0.0.1:8000/api/bookings/ + +Then create a payment (example uses booking id `3`): + + curl -s -X POST http://127.0.0.1:8000/api/payments/ \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{ + "booking_id": 3, + "provider": "moyasar", + "idempotency_key": "", + "source": {"type": "stcpay", "mobile": "0500000000"} + }' + +Expected: response includes: + +- `status: initiated` +- `external_id: pay_mock_123` +- `redirect_url: https://moyasar.example/tx/mock` + +### 6) Send webhook for paid state + + curl -s -X POST http://127.0.0.1:8000/api/payments/webhook/ \ + -H "Content-Type: application/json" \ + -d '{"type":"payment_paid","secret_token":"whsec","data":{"id":"pay_mock_123"}}' + +Expected: `{ "detail": "Webhook processed" }` + +### 7) Verify payment state + + curl -s -H "Authorization: Bearer " http://127.0.0.1:8000/api/payments/ + +Expected: payment record shows: + +- `status: paid` +- `paid_at` set +- `metadata.last_webhook` populated + +## Considerations and Edge Cases + +- **Webhook secret**: `MOYASAR_WEBHOOK_SECRET` must be set. Requests missing or mismatching `secret_token` return `401`. +- **Idempotency**: reuse the same `idempotency_key` to verify the API returns the existing payment without creating another provider charge. +- **Unsupported sources**: `creditcard` is rejected by the backend. Use `stcpay`, `token`, or `applepay`. +- **Callback URL**: required for `token` payments; otherwise validation fails. +- **Demo data**: `seed_demo` creates a payment with `external_id=None` (not empty string) to avoid violating unique constraints. +- **Debug mode**: `DJANGO_DEBUG=1` is required for local `runserver` if `ALLOWED_HOSTS` is not set. +- **JWT warnings**: short JWT secret keys can trigger warnings in logs; this is acceptable for local sanity checks but should be hardened in production. + +## What to Look For + +- Payment creation returns `external_id` from the mock server. +- Webhook transitions the payment to `paid` and populates `paid_at`. +- `metadata.last_webhook` persists the payload for audit. + +## Cleanup + +- Stop the Django server (`Ctrl+C`). +- Stop the mock server (`Ctrl+C`). +- Optionally delete `/tmp/moyasar_mock.py`.