Swaps default media storage from local FileSystemStorage to MinIO via django-minio-backend, matching the rest of the Winsoo ecosystem (advertising, campaign, wallet). Points at a local MinIO container (added to docker-compose.yml) by default since no shared bucket is provisioned yet. Existing ImageFields pick up the new storage backend automatically — no model changes needed. Verified against a live local MinIO container: bucket auto-creates on first save, object lands under the expected key, URL resolves.
67 lines
5.4 KiB
Markdown
67 lines
5.4 KiB
Markdown
# 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 <name> # 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}`.
|