winofy-backend/CLAUDE.md
Ali Asadi c4f15d9c80 Integrate MinIO for media storage
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.
2026-08-17 18:01:29 +03:30

5.4 KiB

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

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

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 Orders 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}.