# 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. Jump to §10 for a one-glance "which screen calls which endpoint" table. 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 `master`, which does **not** yet include payment integration (wallet debits, the online gateway, seller payouts/withdrawals) — that's on `feature/payment`. On `master`, `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 ``` - 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`) hold an opaque MinIO **object key** (or `null`), not a URL — read the sibling `_url` (e.g. `logo_url`) for a fetchable, presigned, time-limited URL. See §4.7. ### 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. `SellerWithdrawalRequest`/`WalletTransactionRef` only exist on `feature/payment`. ### 4.7 Image uploads (presigned MinIO) Every image field (`ProductCategory.icon`, `Product.image`, `StoreCategory.icon`, `Store.logo`, `Store.cover_image`) is a **MinIO object key**, not a Django-served file. Uploading is a two-step flow — the backend never receives the file bytes: 1. `POST /api/media/presign/` (auth required) with `{"filename": "photo.jpg"}` → `201 {"upload_url": "...", "object_key": "uploads/.jpg"}`. `upload_url` is a presigned MinIO `PUT` URL valid for 30 minutes. 2. Upload the raw file bytes directly to `upload_url` (a plain `PUT`, no auth header, no JSON body — just the file as the request body). 3. Send the `object_key` from step 1 as the field's value in the resource's create/update body (e.g. `{"logo": "uploads/.jpg", ...}` on `PATCH /api/v1/seller/store/`). On read, every serializer that has an image field also returns a `_url` sibling (`icon_url`, `image_url`, `logo_url`, `cover_image_url`) — a freshly presigned `GET` URL valid for 1 hour. Always re-fetch the resource (or re-request) rather than caching these URLs past an hour; they expire. `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": "", "neighborhood_uuid": "", "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,icon_url,order}, logo, logo_url, cover_image, cover_image_url, rating_avg, rating_count, min_order_amount, delivery_fee, free_delivery_threshold, is_open`. See §4.7 for `icon`/`logo`/`cover_image` vs. their `_url` siblings. ### 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": "", "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": "
", "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": "", "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 `master`**, 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_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 `master`**, `delivered` just marks the order delivered; no money moves yet. ### 6.4 Payment methods, in full (`feature/payment` only) `master` 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": "", "description": "...", "phone_number": "09166352131", "city_uuid": "", "address": "خیابان آزادی، نبش کوچه مریم", "latitude": 35.71, "longitude": 51.35, "service_neighborhood_uuids": ["", "..."], "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). To set `logo`/`cover_image`, run the presign flow (§4.7) first and include the resulting `object_key`s in this JSON body — e.g. add `"logo": "uploads/.png"`. ### 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`. `image` is a MinIO object key from the presign flow (§4.7), not a file upload. Response adds `image_url` (presigned, §4.7), `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 `master`) | 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 `master`, 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 (Media, 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 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. ## 10. Quick reference — screen → endpoint The same information as §5/§7, but organized by Figma screen code instead of by resource, for wiring up one screen at a time. "Auth" endpoints need a bearer token (§3); "Public" ones don't. ### اپ مشتری (Customer app) | Screen | Endpoint(s) | |---|---| | C01/C02 — Login, OTP | *None* — Gooyal accounts OAuth2 handles this entirely; see §3. | | C03 / guest-home — Home feed | `GET /api/v1/cities/`, `GET /api/v1/neighborhoods/?city=`, `GET /api/v1/store-categories/`, `GET /api/v1/stores/?neighborhood=&category=&search=&lat=&lng=` | | Address list / map picker / address form | `GET/POST /api/v1/addresses/`, `GET/PUT/PATCH/DELETE /api/v1/addresses/{uuid}/` | | C04 — Store page | `GET /api/v1/stores/{uuid}/`, `GET /api/v1/product-categories/`, `GET /api/v1/products/?store={uuid}&category=` | | C05 — Product detail | `GET /api/v1/products/{uuid}/` | | C06 — Search | `GET /api/v1/products/?search=`, `GET /api/v1/stores/?search=` | | C07 — Cart | `GET/DELETE /api/v1/cart/`, `POST /api/v1/cart/items/`, `PATCH/DELETE /api/v1/cart/items/{item_uuid}/` | | C08 — Checkout | `POST /api/v1/checkout/` (§6.2, §6.4) | | C09 — Order tracking / history | `GET /api/v1/order-groups/` (history), `GET /api/v1/order-groups/{uuid}/`, `GET /api/v1/orders/{uuid}/`, `POST /api/v1/orders/{uuid}/cancel/` | | C10 — Post-delivery / reviews | `POST /api/v1/reviews/` (after `delivered`), `GET /api/v1/reviews/?store=` (store page's review list) | | Notifications (bell icon) | `GET /api/v1/notifications/`, `POST /api/v1/notifications/{uuid}/mark-read/`, `POST /api/v1/notifications/mark-all-read/` | ### پنل فروشنده (Seller panel) | Screen | Endpoint(s) | |---|---| | S01/S02 — Seller login, OTP | *None* — Gooyal accounts OAuth2; see §3. | | S04 — Create store | `POST /api/v1/seller/store/` | | S05 — Dashboard | `GET /api/v1/seller/store/`, `GET /api/v1/seller/analytics/?period=today`, `GET /api/v1/seller/orders/?status=placed` | | S06 — Product management (list) | `GET /api/v1/seller/products/` | | S07 — Add product | `POST /api/media/presign/` (§4.7) → upload the file → `POST /api/v1/seller/products/` with `image` = the returned `object_key` | | S08 — Inventory | `GET /api/v1/seller/products/`, `PATCH /api/v1/seller/products/{uuid}/stock/` | | S09 — Order management (status tabs) | `GET /api/v1/seller/orders/?status=` | | S10 — Order detail + stepper | `GET /api/v1/seller/orders/{uuid}/`, then `POST .../confirm/`, `.../mark-ready/`, `.../mark-shipped/`, `.../mark-delivered/`, `.../cancel/` | | S11/S12 — Wallet, withdrawal | **Not on `master`** — `feature/payment` only (§7 Seller · Wallet). | | S14 — Store settings | `GET/PATCH /api/v1/seller/store/` (logo/cover uploads use the same presign flow as S07), `GET/PUT /api/v1/seller/store/working-hours/` | | S15 — Seller profile | `GET /api/v1/seller/store/`, `GET /api/v1/seller/reviews/` (rating summary) | | S16 — Notifications | Same shared endpoints as the customer app, above. | | S17 — Analytics | `GET /api/v1/seller/analytics/?period=today\|week\|month` | | Reviews & replies | `GET /api/v1/seller/reviews/`, `POST /api/v1/seller/reviews/{uuid}/reply/` |