winofy-backend/FRONTEND_GUIDE.md
Ali Asadi 52adc732fa Initial Winofy backend: marketplace core (no payment integration)
Django 6 + DRF resource server against the Gooyal accounts OAuth2 service,
matching the Winsoo ecosystem's conventions. Covers locations, stores,
catalog, cart, checkout/orders (with the multi-store-cart split and the
status stepper), reviews, and notifications, plus a demo-data seed command.

Payment integration (wallet debits, online gateway, seller payouts) is
intentionally left out here — see feature/payment.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-10 14:03:15 +03:30

443 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Winofy API — Frontend Integration Guide
This document is written for whoever (human or AI assistant) is building the two Winofy clients — **اپ مشتری** (customer app) and **پنل فروشنده** (seller panel) — against this backend. It covers auth, conventions, every endpoint, and the end-to-end flows that tie them together. Read §3 (Authentication) and §4 (Conventions) first — everything else assumes them.
Backend repo: `winofy-backend` (Django 6 + DRF). Swagger UI: `GET /api/swagger/swagger-ui/`. Raw OpenAPI schema: `GET /api/swagger/schema/`.
> **Branch note:** this guide describes `main`, which does **not** yet include payment integration (wallet debits, the online gateway, seller payouts/withdrawals) — that's on `feature/payment`. On `main`, `checkout` still accepts a `payment_method` and creates orders normally, but `OrderGroup.payment_status` just stays `pending` regardless of method, there's no `payment` key in the checkout response, and the `Seller · Wallet` endpoints (§7) don't exist yet. Everything else in this doc applies to both branches.
## 1. Base URL
All endpoints below are relative to `/api/`. Domain apps live under `/api/v1/`; health check and OAuth2 live directly under `/api/`.
```
{API_BASE_URL}/api/v1/...
```
## 2. Two clients, one API, one role model
There is no separate "seller API" service — it's the same backend, same auth, same `User`. Whether a logged-in user is a "customer" or a "seller" is determined entirely by **whether they own a `Store`**:
- Any authenticated user can call the customer-facing endpoints (browse, cart, checkout, etc).
- `POST /api/v1/seller/store/` lets **any** authenticated user become a seller by creating their store (this is the "ایجاد فروشگاه" screen). One user → at most one store.
- Every endpoint under `seller/...` requires the user to already own a store (`403`/`404` otherwise — see §4.3).
So: the seller panel app should call `GET /api/v1/seller/store/` right after login; a `404` means "show the create-store onboarding flow," anything else means "show the dashboard."
## 3. Authentication
**This backend does not implement login, OTP, or signup endpoints.** Like every other Winsoo/Gooyal service, it's an OAuth2 *resource server*: your client authenticates directly against the central **Gooyal accounts** service and sends the resulting bearer token to Winofy.
```
Authorization: Bearer <access_token issued by Gooyal accounts>
```
- The phone-number + OTP screens in the Figma file (C01/C02 for customers, S01/S02 for sellers) are a UI over Gooyal's own `/oauth2/token/` endpoint (password/OTP grant) — **not** a Winofy endpoint. Get the exact request shape and base URL from whoever owns the Gooyal accounts service integration for this app.
- Winofy will lazily create a local shadow user (keyed by the same UUID Gooyal uses) the first time it sees a valid token for that user — the frontend doesn't need to do anything to "register" a user with Winofy.
- There is no refresh-token handling documented here; follow the standard OAuth2 refresh flow against Gooyal accounts.
### 3.1 Local development without real Gooyal credentials
If you're integrating against a local instance that doesn't have real Gooyal OAuth2 credentials wired up yet, the two workarounds are:
1. **Django session auth** (also enabled): log into `/api/admin/` with a superuser account in a browser, then a browser-based dev client sharing cookies can call the API using that session. Not viable for a mobile app or a separate origin without CORS+CSRF setup.
2. Ask backend/DevOps for a real Gooyal `client_id`/`client_secret` and a test phone number — this is the only realistic path for end-to-end testing outside a browser.
There is **no dev-mode bypass endpoint** in this API — don't build against one that doesn't exist.
## 4. Conventions
### 4.1 Success responses — plain, not wrapped
Successful responses are the serializer's JSON directly. There is **no `{success: true, data: ...}` envelope** — that only applies to errors (§4.2). Don't unwrap a `data` key that isn't there.
### 4.2 Error responses — always wrapped, Persian messages
Every non-2xx response has this shape:
```json
{
"success": false,
"status_code": 400,
"status_message": "Bad Request",
"details": {
"message": { "field_name": ["پیام خطا به فارسی"] },
"error": "invalid",
"timestamp": "2026-08-09T12:00:00.000000+00:00"
}
}
```
- `details.message` is either a string, or an object keyed by field name (validation errors) — check which shape you got before rendering.
- All user-facing error text is in **Persian** — show `details.message` directly to the user, don't re-translate.
- Common status codes you'll see: `400` (validation), `401` (missing/invalid token), `403` (authenticated but not allowed — e.g. non-seller hitting a seller endpoint's permission check), `404` (not found / not yours), `409` (conflict — e.g. re-creating a store, invalid status transition), `503` (an upstream Gooyal/payment-gateway call failed).
### 4.3 Seller-endpoint permission errors
`seller/*` endpoints use two different failure modes depending on the view — be ready for either:
- `403 Forbidden` (from the `IsStoreOwner` permission check) on most `seller/*` list/action endpoints.
- `404 Not Found` specifically from `GET/PATCH /seller/store/` before a store exists (it does a plain `get_object_or_404`, not a permission check, since "you don't have a store yet" is the expected first-run state).
### 4.4 Pagination
List endpoints use limit/offset pagination:
```
GET /api/v1/stores/?limit=20&offset=40
```
```json
{ "count": 132, "next": "http://.../stores/?limit=20&offset=60", "previous": "http://.../stores/?limit=20&offset=20", "results": [ ... ] }
```
Default page size is 50 if `limit` is omitted.
### 4.5 Identifiers, money, dates
- Every object's primary key is a **UUID string** (field name `uuid`), not an integer. Use it in URLs: `/api/v1/products/{uuid}/`.
- All money fields (`price`, `delivery_fee`, `items_subtotal`, `total_amount`, `amount`, etc.) are **integers in Toman**, no decimals, no currency string.
- All timestamps are ISO 8601 with timezone (`created_at`, `placed_at`, `scheduled_at`, ...).
- Image fields (`image`, `logo`, `cover_image`, `icon`) are either `null` or an absolute/relative media URL — never assume they're set.
### 4.6 Enums
| Field | Values | Persian label |
|---|---|---|
| `Address.label` | `home`, `work`, `other` | خانه, محل کار, سایر |
| `Product.unit_type` | `gram`, `ml`, `piece` | گرم, میلی‌لیتر, عدد |
| `Store.status` | `pending`, `approved`, `suspended` | در انتظار تایید, تایید شده, معلق شده |
| `StoreWorkingHours.weekday` | `0`–`6` | شنبه=0 … جمعه=6 (Iranian week, **not** Sun-Sat) |
| `OrderGroup.delivery_type` | `express`, `scheduled` | ارسال فوری, زمان‌بندی شده |
| `OrderGroup.payment_method` | `wallet`, `online`, `cash_on_delivery` | کیف پول وینسو, درگاه بانکی, پرداخت در محل |
| `OrderGroup.payment_status` | `pending`, `paid`, `failed` | در انتظار پرداخت, پرداخت شده, ناموفق |
| `Order.status` | `placed`, `preparing`, `ready_to_ship`, `handed_to_courier`, `delivered`, `cancelled` | سفارش ثبت شد → در حال آماده‌سازی → آماده ارسال → تحویل سفیر شد → تحویل داده شد (+ لغو شده) |
| `SellerWithdrawalRequest.status` | `pending`, `processing`, `paid`, `rejected` | در انتظار بررسی, در حال پردازش, واریز شده, رد شده |
| `WalletTransactionRef.direction` | `debit`, `credit` | برداشت, واریز |
| `Notification.type` | `new_order`, `settlement_done`, `new_review`, `order_cancelled` | سفارش جدید, تسویه حساب, نظر جدید, لغو سفارش |
`Order.status` only moves forward through that exact sequence (or to `cancelled` from `placed`/`preparing` only) — see §6.3.
## 5. Customer app — endpoint reference
Auth column: **Public** = no token needed, **Auth** = any logged-in user, all scoped to the requester unless noted.
### Locations (`Locations` / `Addresses` in swagger)
| Method | Path | Auth | Notes |
|---|---|---|---|
| GET | `/api/v1/cities/` | Public | List active cities. |
| GET | `/api/v1/neighborhoods/?city={uuid}` | Public | List neighborhoods, optionally filtered by city. |
| GET | `/api/v1/addresses/` | Auth | The caller's own saved addresses. |
| POST | `/api/v1/addresses/` | Auth | Create an address (see body below). |
| GET/PATCH/DELETE | `/api/v1/addresses/{uuid}/` | Auth | |
**Address create/update body:**
```json
{
"label": "home",
"title": "",
"full_address": "خیابان ولیعصر، کوچه ۱۲",
"plaque": "15", "floor": "2", "unit": "3",
"recipient_name": "سارا احمدی", "recipient_phone": "09123456789",
"city_uuid": "<city uuid>",
"neighborhood_uuid": "<neighborhood uuid or omit>",
"latitude": 35.7595, "longitude": 51.4088,
"is_default": true
}
```
Response mirrors this but with `city`/`neighborhood` as nested objects (not `*_uuid`), plus `uuid` and `created_at`.
### Stores (`Stores` tag)
| Method | Path | Auth | Notes |
|---|---|---|---|
| GET | `/api/v1/store-categories/` | Public | سوپرمارکت / کافه / رستوران / ... |
| GET | `/api/v1/stores/?neighborhood={uuid}&category={uuid}&search=text&lat=..&lng=..` | Public | Home feed / search. `lat`+`lng` sorts by distance (15 km radius). Only `status=approved` stores are ever returned. |
| GET | `/api/v1/stores/{uuid}/` | Public | Store page — includes `working_hours`, `accepts_wallet/online/cash_on_delivery`, `description`, `address`, `phone_number` (fields the list endpoint omits). |
Store list item shape: `uuid, name, category{uuid,name,icon,order}, logo, cover_image, rating_avg, rating_count, min_order_amount, delivery_fee, free_delivery_threshold, is_open`.
### Catalog (`Catalog` tag)
| Method | Path | Auth | Notes |
|---|---|---|---|
| GET | `/api/v1/product-categories/` | Public | Flat shared taxonomy (`parent` nullable for subcategories). |
| GET | `/api/v1/products/?store={uuid}&category={uuid}&search=text` | Public | Store page's product grid / search. Only active products of approved stores. |
| GET | `/api/v1/products/{uuid}/` | Public | Product detail — adds `description`, `category`, `store` (a `StoreListSerializer`). |
Product list item shape: `uuid, name, image, price, unit_type, unit_value, is_active, is_out_of_stock`.
### Cart (`Cart` tag) — Auth required for all
| Method | Path | Notes |
|---|---|---|
| GET | `/api/v1/cart/` | Returns the cart **grouped by store** — see shape below. |
| DELETE | `/api/v1/cart/` | Empties the whole cart. |
| POST | `/api/v1/cart/items/` | Add a product; if it's already in the cart, **quantities add together** (not replaced). Body: `{"product_uuid": "...", "quantity": 2}`. |
| PATCH | `/api/v1/cart/items/{item_uuid}/` | Set an absolute quantity: `{"quantity": 5}`. |
| DELETE | `/api/v1/cart/items/{item_uuid}/` | Remove one line. |
**`GET /api/v1/cart/` response** — this is the shape behind the multi-store-cart UI (C07):
```json
{
"groups": [
{
"store": { "uuid": "...", "name": "سوپرمارکت پارسیان", "...": "..." },
"items": [
{ "uuid": "<cart item uuid>", "product": { "uuid": "...", "name": "شیر کاله", "price": 28500, "...": "..." }, "quantity": 2, "line_total": 57000 }
],
"items_subtotal": 57000,
"delivery_fee": 15000,
"total": 72000,
"meets_minimum_order": true
}
],
"grand_total": 72000
}
```
- `meets_minimum_order: false` means this store's slice is below `store.min_order_amount` — checkout will reject it (§6.2). Surface this in the cart UI per-store, same as the mock.
- `delivery_fee` is already `0` here if the store's `free_delivery_threshold` was met — don't recompute it client-side.
### Checkout (`Checkout` tag) — Auth required
| Method | Path |
|---|---|
| POST | `/api/v1/checkout/` |
Body:
```json
{
"address_uuid": "<address uuid>",
"delivery_type": "express",
"scheduled_at": null,
"payment_method": "wallet",
"notes": ""
}
```
- `scheduled_at` is **required** (ISO datetime) when `delivery_type` is `"scheduled"` — omitting it is a `400`.
- `address_uuid` must belong to the caller, or `400`.
- The cart is split into **one `Order` per store** and cleared on success. See §6.1–§6.2 for the full flow including coverage checks and payment-method branching.
Response = an `OrderGroup` object (§5, Orders below):
```json
{
"uuid": "<order group uuid>",
"recipient_name": "...", "recipient_phone": "...", "full_address": "...",
"delivery_type": "express", "scheduled_at": null,
"payment_method": "online", "payment_status": "pending",
"notes": "", "total_amount": 178500,
"orders": [ /* array of Order objects, one per store — see Orders below */ ],
"created_at": "..."
}
```
**On `main`**, that's it — `payment_status` stays `"pending"` no matter which `payment_method` you send; nothing external is contacted. **On `feature/payment`**, the response also includes a `payment` key and `payment_status` actually reflects what happened:
- `wallet`: `payment` = `{"method": "wallet", "status": "paid"}` — synchronous, done.
- `online`: `payment` = `{"method": "online", "status": "pending", "payment_url": "..."}` — **redirect/open a webview at `payment_url`**; the gateway calls Winofy back and payment_status updates asynchronously (poll `GET /api/v1/order-groups/{uuid}/` or `GET /api/v1/orders/{uuid}/` to see it flip to `paid`).
- `cash_on_delivery`: `payment` = `{"method": "cash_on_delivery", "status": "pending"}` — nothing to do, collected on delivery.
### Orders (`Orders` tag) — Auth required, scoped to the caller
| Method | Path | Notes |
|---|---|---|
| GET | `/api/v1/order-groups/` | Order history — each entry may bundle several stores' orders from one checkout. |
| GET | `/api/v1/order-groups/{uuid}/` | |
| GET | `/api/v1/orders/{uuid}/` | Single-order tracking (C09) — one store's slice. |
| POST | `/api/v1/orders/{uuid}/cancel/` | Body: `{"reason": ""}` (optional). Only works while `is_cancellable` is true. |
**`Order` object shape** (nested inside `OrderGroup.orders`, or standalone from `GET /orders/{uuid}/`):
```json
{
"uuid": "...", "store": { "...": "StoreListSerializer" }, "status": "preparing",
"items": [
{ "uuid": "...", "product": "<product uuid>", "product_name_snapshot": "شیر کاله", "unit_price_snapshot": 28500, "quantity": 2, "line_total": 57000 }
],
"items_subtotal": 57000, "delivery_fee": 15000,
"commission_amount": 2280, "seller_payout_amount": 54720,
"total_amount": 72000,
"cancel_reason": "", "courier_name": "", "courier_phone": "",
"placed_at": "...", "delivered_at": null,
"status_logs": [ { "from_status": "", "to_status": "placed", "note": "", "created_at": "..." } ],
"is_cancellable": true
}
```
- `items[].product` is just the product UUID (not expanded) — use `product_name_snapshot`/`unit_price_snapshot` for display; they're frozen at order time and won't change even if the product is later edited or deleted.
- Drive the order-tracking stepper UI off `status_logs` (ordered oldest→newest) or just off `status` directly — both are provided.
- `commission_amount`/`seller_payout_amount` are platform-internal — fine to show in a seller-facing context, not normally shown to the customer.
### Reviews (`Reviews` tag)
| Method | Path | Auth | Notes |
|---|---|---|---|
| GET | `/api/v1/reviews/?store={uuid}` | Public | Reviews for a store's page. |
| POST | `/api/v1/reviews/` | Auth | Body: `{"order_uuid": "...", "rating": 5, "comment": "..."}`. Only allowed once `order.status == "delivered"`, and only once per order (`400` otherwise — see error messages in §4.2). |
Response adds `customer_name`, `store` (uuid), `product` (uuid, nullable), `seller_reply` (null until the seller replies).
### Notifications (`Notifications` tag) — Auth required, shared by both apps
| Method | Path | Notes |
|---|---|---|
| GET | `/api/v1/notifications/` | The caller's notifications (works the same whether they're a customer or a seller — it's just "notifications for this user"). |
| GET | `/api/v1/notifications/{uuid}/` | |
| POST | `/api/v1/notifications/{uuid}/mark-read/` | |
| POST | `/api/v1/notifications/mark-all-read/` | Returns `204`. |
Shape: `uuid, type, title, body, related_order (uuid, nullable), is_read, created_at`. All fields are read-only from the client's perspective (server-generated).
## 6. Key flows
### 6.1 Location & delivery-coverage flow (the neighborhood-confirmation-sheet)
Stores don't deliver everywhere — each `Store` has a `service_neighborhoods` list. The Figma "محله‌ت رو انتخاب کن" sheet exists because of this. The API enforces it **at checkout time**, not earlier — the client's job is to react to the failure, not to pre-validate:
1. Customer picks/has an `Address` (with a `neighborhood_uuid`) — via the address list, or the map picker + `POST /api/v1/addresses/`.
2. Customer browses/adds to cart freely — `GET /api/v1/stores/` and cart endpoints **do not check coverage**.
3. On `POST /api/v1/checkout/`, if any store in the cart doesn't serve the address's neighborhood, the whole checkout fails with `400` and a message like *"فروشگاه «X» به محله انتخابی شما ارسال ندارد."* — show the confirmation sheet / prompt the user to pick a different address at that point.
If you want to warn the customer earlier (e.g. on the store page), you'd need to compare the store's `service_neighborhoods` (not currently exposed on the customer-facing store serializer — ask backend to add it if you need this pre-check) against the customer's selected/default address neighborhood.
### 6.2 Multi-store cart → checkout
1. `GET /api/v1/cart/` → render one card per `groups[]` entry (matches C07).
2. Block the "ادامه و ثبت سفارش" button per-store (or overall) if any group has `meets_minimum_order: false`.
3. `POST /api/v1/checkout/` once. The backend, inside one transaction:
- re-validates minimum order, stock, and neighborhood coverage per store (can still fail here even if the cart screen looked fine, e.g. a race with another customer depleting stock) — handle the `400` gracefully;
- creates one `OrderGroup` + one `Order` per store, snapshots the address into `recipient_name/recipient_phone/full_address` on the group (so later address edits don't retroactively change past orders);
- decrements `Product.stock_quantity` and clears the cart;
- branches on `payment_method` (§5 Checkout).
4. Route based on `payment.method`:
- `wallet`/`cash_on_delivery` → go straight to an order-confirmation screen.
- `online` → open `payment.payment_url`, then poll or listen for the order group to reach `payment_status: "paid"`.
### 6.3 Order status stepper (seller side drives it, customer side reads it)
The seller panel is the only side that advances status. Valid transitions:
```
placed → preparing → ready_to_ship → handed_to_courier → delivered
placed → cancelled
preparing → cancelled
```
No other transition is allowed — the backend returns `409` for anything else (e.g. trying to jump from `placed` straight to `delivered`, or cancelling after `handed_to_courier`). See §7 for the seller-side action endpoints. The customer app's `POST /orders/{uuid}/cancel/` uses the same rule (only while `placed`/`preparing`).
Commission (`Order.commission_amount`) is fixed at checkout time from the store's rate (4% by default) applied to `items_subtotal` only — `delivery_fee` is excluded from the split and goes to the seller, on both branches. **On `feature/payment` only**, the `delivered` transition additionally credits the seller's Gooyal wallet and fires a `settlement_done` notification — none of this needs client involvement either way. **On `main`**, `delivered` just marks the order delivered; no money moves yet.
### 6.4 Payment methods, in full (`feature/payment` only)
`main` accepts and stores `payment_method` on checkout but doesn't act on it — `payment_status` stays `pending` regardless. The behavior below is what `feature/payment` adds:
| `payment_method` | What happens | Client follow-up |
|---|---|---|
| `wallet` | Debits the customer's Gooyal wallet synchronously during checkout. | None — `payment_status` is already `paid` in the checkout response. |
| `online` | Opens a charge request on the internal `ipg` gateway. | Redirect to `payment.payment_url`; the gateway calls Winofy back asynchronously. Poll the order/order-group for `payment_status`. |
| `cash_on_delivery` | Nothing charged now. | Show "پرداخت در محل" confirmation; money changes hands at delivery, outside the API. |
## 7. Seller panel — endpoint reference
Every endpoint below requires the caller to own a `Store` (`IsStoreOwner`), except store creation itself.
### Seller · Store
| Method | Path | Notes |
|---|---|---|
| GET | `/api/v1/seller/store/` | `404` if the user hasn't created a store yet → show onboarding. |
| POST | `/api/v1/seller/store/` | Creates the store (one per user, `409` on a second attempt). Body below. |
| PATCH | `/api/v1/seller/store/` | Partial update, same body shape. |
| GET | `/api/v1/seller/store/working-hours/` | Returns array of `{weekday, opens_at, closes_at, is_closed}`. |
| PUT | `/api/v1/seller/store/working-hours/` | **Full replace** — send all 7 days each time; body is a bare array (or `{"working_hours": [...]}`). |
Store create/update body:
```json
{
"name": "سوپرمارکت پارسیان",
"category_uuid": "<store category uuid>",
"description": "...",
"phone_number": "09166352131",
"city_uuid": "<city uuid>",
"address": "خیابان آزادی، نبش کوچه مریم",
"latitude": 35.71, "longitude": 51.35,
"service_neighborhood_uuids": ["<neighborhood uuid>", "..."],
"delivery_radius_km": 5,
"min_order_amount": 50000, "delivery_fee": 25000, "free_delivery_threshold": null,
"accepts_wallet": true, "accepts_online": true, "accepts_cash_on_delivery": true,
"is_open": true
}
```
`rating_avg`, `rating_count`, `status` are read-only (server-computed; `status` starts `pending` — there's no admin-approval endpoint in this API yet, that's a manual/admin-panel step today). `logo`/`cover_image` aren't settable through this JSON body — that needs a multipart upload endpoint, which doesn't exist yet; flag this to backend if the store-branding screen needs it.
### Seller · Products
| Method | Path | Notes |
|---|---|---|
| GET | `/api/v1/seller/products/` | Only the caller's own products. |
| POST | `/api/v1/seller/products/` | Create (S07 "افزودن محصول"). |
| GET/PATCH/DELETE | `/api/v1/seller/products/{uuid}/` | |
| PATCH | `/api/v1/seller/products/{uuid}/stock/` | Inventory-only quick update (S08): `{"stock_quantity": 12}`. |
Product body: `name, description, image, category_uuid, price, unit_type, unit_value, stock_quantity, low_stock_threshold, is_active`. Response adds `sold_count` (read-only), `is_out_of_stock`, `is_low_stock` (both computed: `stock_quantity <= 0`, and `0 < stock_quantity <= low_stock_threshold`).
### Seller · Orders
| Method | Path | Notes |
|---|---|---|
| GET | `/api/v1/seller/orders/?status=placed` | `status` filter matches the S09 tabs; omit for all. |
| GET | `/api/v1/seller/orders/{uuid}/` | S10 detail. |
| POST | `/api/v1/seller/orders/{uuid}/confirm/` | `placed → preparing`. |
| POST | `/api/v1/seller/orders/{uuid}/mark-ready/` | `preparing → ready_to_ship`. |
| POST | `/api/v1/seller/orders/{uuid}/mark-shipped/` | `ready_to_ship → handed_to_courier`. Body may include `courier_name`/`courier_phone`. |
| POST | `/api/v1/seller/orders/{uuid}/mark-delivered/` | `handed_to_courier → delivered`. Triggers settlement (§6.3). |
| POST | `/api/v1/seller/orders/{uuid}/cancel/` | Only from `placed`/`preparing`. Body: `{"reason": ""}`. |
All the action endpoints accept an optional body `{"note": "", "courier_name": "", "courier_phone": ""}` and return the updated `Order` object (§5 shape). A `409` means the transition isn't legal from the order's current status (§6.3) — don't just retry, refetch the order and re-render the correct available actions.
### Seller · Wallet (`feature/payment` only — not on `main`)
| Method | Path | Notes |
|---|---|---|
| GET | `/api/v1/seller/wallet/` | `{"withdrawable_balance": 12500000, "pending_settlement": 350000, "recent_transactions": [...]}` (S11). |
| GET | `/api/v1/seller/wallet/withdrawals/` | The seller's withdrawal request history. |
| POST | `/api/v1/seller/wallet/withdrawals/` | S12 form: `{"amount": 500000, "iban": "IR120120000000012345678", "account_holder_name": "سارا احمدی"}`. `iban` must match `^IR\d{24}$` or `400`. |
`recent_transactions[]` items: `uuid, direction ("debit"/"credit"), amount, purpose, status, created_at`.
### Seller · Analytics
| Method | Path | Notes |
|---|---|---|
| GET | `/api/v1/seller/analytics/?period=today` | `period` ∈ `today`/`week`/`month` (default `today`). S17 dashboard. |
Response:
```json
{
"period": "today",
"order_count": 12, "total_sales": 2450000,
"cancellation_rate": 4.5, "average_order_value": 204000,
"chart": [ { "bucket": "2026-08-09T09:00:00Z", "sales": 320000 } ],
"top_products": [ { "name": "شیر کم چرب کاله 1 لیتری", "units_sold": 44, "revenue": 1680000 } ]
}
```
`chart[].bucket` is hourly when `period=today`, daily otherwise. `top_products` is capped at 5, sorted by revenue.
### Seller · Reviews
| Method | Path | Notes |
|---|---|---|
| GET | `/api/v1/seller/reviews/` | Reviews on the caller's store. |
| POST | `/api/v1/seller/reviews/{uuid}/reply/` | `{"seller_reply": "ممنون از خرید شما"}`. |
### Payments webhook (`feature/payment` only — not on `main`, and not called by either client app)
`GET`/`POST /api/v1/payments/ipg/callback/` is hit by the `ipg` payment gateway service itself after a customer completes an online payment, not by the frontend. Don't call it directly; it's documented here only so you know it exists and isn't a client-facing endpoint.
## 8. Local development / testing
- The backend ships a management command that seeds a realistic dataset: 3 cities, 13 neighborhoods, ~19 stores across 6 categories, ~370 products, 30 customers with addresses, 150+ historical orders in every status (placed/preparing/delivered/cancelled), and reviews. Ask backend to run `python manage.py seed_demo_data` (or `--flush` to reset it) against your dev environment before you start wiring up screens — there's no need to hand-create fixtures.
- Swagger UI (`/api/swagger/swagger-ui/`) is grouped into the same sections as this doc (Locations, Stores, Catalog, Cart, Checkout, Orders, Reviews, Notifications, and the `Seller · *` groups — `Seller · Wallet` and `Payments` only appear on `feature/payment`) — use it to try requests once you have a token.
- `GET /api/health/` needs no auth and is useful as a "is the backend even up" smoke check.
## 9. Known gaps to flag back to backend if you hit them
- No multipart/image-upload endpoint yet for store logo/cover or product images (JSON `PATCH` bodies can't set binary fields).
- No store-approval endpoint — new stores sit in `status: "pending"` until changed via Django admin.
- No admin/public endpoint to see a store's `service_neighborhoods` from the customer-facing store serializer (needed if you want to pre-warn about delivery coverage before checkout — see §6.1).
- The `ipg` online-payment callback contract (`/api/v1/payments/ipg/callback/`) is a best-effort integration pending confirmation against a live `ipg` environment — if online payments seem to hang in `pending`, that's the first place to check.