winofy-backend/FRONTEND_GUIDE.md
Ali Asadi 85598d347a Switch media uploads to presigned MinIO, matching the rest of Winsoo
The prior commit wired MinIO as Django's default storage backend but
kept plain ImageField multipart uploads through the Django backend and
public-bucket direct URLs — not how campaign/advertising/promotions do
it. Rework to match: image fields (ProductCategory.icon, Product.image,
StoreCategory.icon, Store.logo, Store.cover_image) now store an opaque
MinIO object_key (CharField) instead of a Django-managed file.

- utils/clients/minio_client.py — raw Minio SDK client, same shape as
  the other services.
- apps/core/views/media.py — POST /api/media/presign/ mints a 30-minute
  presigned PUT URL + object_key; the client uploads bytes directly to
  MinIO, then submits the object_key on the owning resource.
- apps/core/media.py — presigned_media_url() helper; every serializer
  with an image field now also exposes a `<field>_url` computed from a
  fresh 1-hour presigned GET, rather than trusting a stored/direct URL.
- FRONTEND_GUIDE.md updated to document the new upload flow and field
  shapes (object_key vs `_url`), replacing the stale "no upload
  endpoint yet" note.

Verified against a live local MinIO container: presign → direct PUT
upload → object_key stored on the model → serializer mints a working
presigned GET URL → URL fetches the uploaded bytes. Also exercised the
actual DRF view (POST /api/media/presign/) end-to-end, including the
401 on an unauthenticated request.
2026-08-18 10:42:44 +03:30

29 KiB
Raw Blame History

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:

{
  "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
{ "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 <field>_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 در انتظار بررسی, در حال پردازش, واریز شده, رد شده

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/<uuid>.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/<uuid>.jpg", ...} on PATCH /api/v1/seller/store/).

On read, every serializer that has an image field also returns a <field>_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. | 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:

{
  "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,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):

{
  "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:

{
  "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):

{
  "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}/):

{
  "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:

{
  "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). To set logo/cover_image, run the presign flow (§4.7) first and include the resulting object_keys in this JSON body — e.g. add "logo": "uploads/<uuid>.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 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:

{
  "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 (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.