# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Stack Django 6 + Django REST Framework on Python 3.13, with GeoDjango (PostGIS) for location/coverage data. Part of the **Winsoo** ecosystem (`~/Projects/Winsoo/`) — same conventions as `campaign`, `wallet`, `advertising`, `promotions`, `settlement`. Winofy is a hyperlocal multi-vendor marketplace serving two clients: the customer app (اپ مشتری) and the seller panel (پنل فروشنده). ## Local setup ```bash python3.13 -m venv .venv source .venv/bin/activate pip install -r requirements.txt cp .env.example .env # then set SECRET_KEY and DB_USER (postgres role with CREATEDB/superuser or ownership of the DB below) createdb winofy_dev && psql -d winofy_dev -c "CREATE EXTENSION postgis;" python manage.py migrate python manage.py runserver ``` On macOS, uncomment `GDAL_LIBRARY_PATH` / `GEOS_LIBRARY_PATH` in `.env` (Homebrew paths) — GDAL isn't on the default library search path there. ## Common commands ```bash python manage.py runserver python manage.py startapp # then move it under apps/ and fix apps.py's `name` python manage.py makemigrations python manage.py migrate python manage.py test python manage.py shell ``` ## Settings Single file: `config/settings.py`. Config comes from `.env` via `django-environ` (`env(...)`) with a few OAuth2 vars still read through `python-decouple`'s `config()` — matching the mixed style already in use across the ecosystem. `LOGGING` is built in `config/other_settings/logging.py` and imported at the very **bottom** of `config/settings.py` (it reads `BASE_DIR` back off `django.conf.settings`, which only works once `BASE_DIR` has already been assigned earlier in the same module — don't move that import up). Key `.env` variables (see `.env.example`): - `SECRET_KEY` — required - `DB_NAME` / `DB_USER` / `DB_PASSWORD` / `DB_HOST` / `DB_PORT` — Postgres+PostGIS connection - `REDIS_URL` — used for caching outbound OAuth2 client-credentials tokens - `ALLOWED_HOSTS`, `CORS_ALLOWED_ORIGINS`, `CSRF_TRUSTED_ORIGINS` — comma-separated lists - `OAUTH2_PROVIDER_*` — Gooyal accounts OAuth2 resource-server + this app's own client credentials (see below) - `DEFAULT_COMMISSION_PERCENT` — platform commission on delivered orders (overridable per `Store`) - `MINIO_*` — MinIO object storage for media (`STORAGES['default']`), same pattern as the rest of the Winsoo ecosystem; defaults target the local `minio` container in `docker-compose.yml` ## Authentication — Gooyal accounts, resource-server pattern This service does **not** implement login/OTP/password endpoints. Every Winsoo service is an OAuth2 *resource server*: client apps authenticate directly against the central Gooyal accounts service's `/oauth2/token/` and pass the resulting bearer token to every microservice, including this one. `apps/gooyal_oauth2` (copied verbatim from the ecosystem) introspects incoming tokens against Gooyal's `/introspect/` endpoint and lazily creates a local shadow `apps.users.User` row (UUID PK = the Gooyal user's UUID) — see `apps/gooyal_oauth2/validators.py::OAuth2Validator`. For calls this service makes *outward* to other Gooyal services (currently just accounts), it authenticates itself via its own `client_credentials` grant, cached in Redis — see `utils/accounts_client.py`. > **No payment integration on this branch.** Wallet debits, online-gateway charges, seller payouts/withdrawals, and the `apps.payments` app all live on `feature/payment`, not here. `Order.commission_amount`/`seller_payout_amount` are still computed at checkout time (pure numbers, no external call), but nothing actually moves money yet — `OrderGroup.payment_status` just stays `pending` regardless of the chosen `payment_method`. See `feature/payment` for `utils/wallet_client.py`, `utils/ipg_client.py`, and the vendored `utils/clients/gooyal_wallet_client/` SDK. ## App layout Domain apps live under `apps/`; `apps.py`'s `name` must be the dotted path including the `apps.` prefix (e.g. `apps.stores`) to match `INSTALLED_APPS`. Each app follows: `models.py`, `admin.py`, `serializers.py`, `views.py` (`GenericViewSet` + explicit actions, not `ModelViewSet`), `urls.py` (`DefaultRouter`), `tests/`. All domain models inherit `utils.models.BaseModel` (UUID PK + `created_at`/`updated_at`). `apps/orders` is intentionally the biggest app — `Cart`/`CartItem` and `Notification` live there too rather than in their own apps. Cart is just pre-order state (it's cleared into `Order`s by `services.checkout()`), and every `Notification.type` this app fires (`new_order`, `order_cancelled`, `settlement_done`, `new_review`) is order-triggered, so both would've been single-model apps whose only real dependency was `orders` anyway. `reviews` stays separate since it touches `orders`/`stores`/`catalog` about equally and doesn't belong to any one of them. `payments` (wallet + ipg integration) lives on `feature/payment` only, for the same "earns its own boundary" reasoning — see above. Seller-panel-only endpoints are gated with `apps.core.permissions.IsStoreOwner` (checks `request.user.store` exists — one seller owns exactly one `Store`). ## Errors `utils.exceptions.exception_handler` (wired as `REST_FRAMEWORK['EXCEPTION_HANDLER']`) plus `utils.exceptions.ErrorMiddleware` for non-DRF 500s. User-facing error strings are in **Persian**; code/comments stay in English. Wraps every response as `{success, status_code, status_message, details}`.