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>
443 lines
28 KiB
Markdown
443 lines
28 KiB
Markdown
# 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.
|