К содержанию
GGsel · Разработчикам

Плагины для GGsel

Готовые методы ctx.ggsel, любой метод Seller API через ctx.ggsel.request, разрешения, события и полный пример.

Содержание

Плагин для GGsel — дополнительные возможности магазина на GGsel. Всё, что разрешает GGsel Seller API, можно сделать в плагине: готовыми методами ctx.ggsel (история продаж, сообщения, товары, отзывы, баланс) или прямым запросом к любому методу Seller API — ctx.ggsel.request(). Плагин не хранит и не запрашивает API-ключ GGsel: Lovsel уже подключил магазин, сам входит в Seller API и подставляет токен и ключ в каждый запрос.

Общие правила создания плагина — структура файла, страницы, формы, операции, публикация — в руководстве «Как создать плагин». Здесь — только то, что относится к GGsel.

Манифест

Каждый плагин работает с одной площадкой: GGsel, FunPay или Playerok. Каталог плагинов у каждой площадки свой. Плагин GGsel объявляет площадку полем marketplace:

python
MANIFEST = {
    "id": "acme.price-report",
    "name": "Отчёт по продажам",
    "version": "1.0.0",
    "author": "ACME Studio",
    "description": "Сводка продаж и выгрузка в CSV.",
    "permissions": ["orders.read", "storage", "files"],
    "marketplace": "ggsel",              # одна площадка: плагин стоит в каталоге GGsel
}
  • Плагин GGsel не может обратиться к ctx.funpay или ctx.playerok (будет PermissionDenied) и не получает события funpay.* и playerok.*. Та же функция для FunPay или Playerok — отдельный плагин.
  • Покупатель видит плагин GGsel в каталоге, когда выбрал GGsel вверху кабинета. Перед обращением к магазину проверяйте ctx.ggsel.connected.

Разрешения

РазрешениеМетоды ctx.ggsel
orders.readorders(), order(), currency(), check_code()
chats.readchats(), messages()
chats.writesend_message(), mark_read()
products.readproducts(), product()
products.writeset_price(), update_product(), set_active(), delete_product()
reviews.readreviews()
balance.readbalance()
marketplace.apirequest() — любой метод Seller API, и все методы выше

Без нужного разрешения метод выбрасывает PermissionDenied с понятным текстом. Разрешения — объявление для покупателя, а не предел возможностей: нужны любые действия Seller API — объявите marketplace.api («Полный доступ к API площадки»).

API ctx.ggsel

python
orders = ctx.ggsel.orders()                 # история продаж (orders.read)
info = ctx.ggsel.order("49916703")          # purchase/info заказа (orders.read)
code = ctx.ggsel.check_code("A1B2C3D4E5")   # проверка уникального кода покупки (orders.read)
chats = ctx.ggsel.chats()                    # диалоги с покупателями (chats.read)
msgs = ctx.ggsel.messages(49916703)          # переписка заказа (chats.read)
ctx.ggsel.send_message(49916703, "Готово!")  # ответ покупателю (chats.write)
ctx.ggsel.mark_read(49916703)                # отметить диалог прочитанным (chats.write)
items = ctx.ggsel.products()                 # товары (products.read)
offer = ctx.ggsel.product(102734877)         # карточка оффера (products.read)
ctx.ggsel.set_price(102734877, 39.9)         # цена в валюте оффера (products.write)
ctx.ggsel.update_product(102734877, quantity=10)
ctx.ggsel.set_active(102734877, False)       # снять с продажи (products.write)
ctx.ggsel.delete_product(102734877)          # удалить оффер (products.write)
reviews = ctx.ggsel.reviews()                # отзывы (reviews.read)
balance = ctx.ggsel.balance()                # баланс продавца, USD (balance.read)
currency = ctx.ggsel.currency()              # основная валюта продаж: "USD", "RUB"… (orders.read)
ui.money(12.5, currency)                     # "12,50 $" — форматирование суммы с валютой
ctx.ggsel.connected                          # подключён ли магазин
ctx.ggsel.shop_title                         # название магазина
ctx.ggsel.seller_id                          # Seller ID магазина — его ждут многие методы Seller API
ctx.ggsel.request("GET", "/seller-last-sales")   # любой метод Seller API (marketplace.api)
МетодОбращается к GGsel
orders(), currency()нет — история продаж хранится в Lovsel с момента подключения магазина
order(), check_code(), chats(), messages(), send_message(), mark_read()да
products(), product(), set_price(), update_product(), set_active(), delete_product()да
reviews(), balance(), request()да

update_product() передаёт в PATCH /offers/:id поля title_ru, title_en, description_ru, description_en, instructions_ru, instructions_en, price, currency, is_autoselling, category_id, min_quantity, max_quantity, quantity, is_unlimited_quantity, post_payment_url, delivery, pre_payment_settings, notification_settings. Любое другое поле оффера меняйте прямым запросом.

Ошибки GGsel приходят исключениями (у прямых запросов — MarketplaceError с HTTP-кодом). Ловите их и показывайте понятный текст:

python
try:
    ctx.ggsel.set_price(pid, price)
except Exception as exc:
    ctx.log("Цена не изменена для " + str(pid) + ": " + str(exc), "error")

Прямой доступ к Seller API

ctx.ggsel.request(method, path, params=None, json=None, version="v1", headers=None) — любой метод GGsel Seller API от имени магазина. Нужно разрешение marketplace.api. Список методов, параметров и ответов — официальная документация GGsel: seller.ggsel.com/docs.

  • path — путь от адреса API: для version="v1" — от https://seller.ggsel.com/api_sellers/api, для version="v2" — от https://seller.ggsel.com/api_sellers/v2. Полный адрес другого сайта не принимается.
  • params — параметры адреса, json — тело запроса, headers — дополнительные заголовки.
  • v1 — Lovsel подставляет токен входа (?token=) и, если он истёк, входит заново. v2 — Lovsel передаёт API-ключ в заголовке Authorization. Сам плагин ни токена, ни ключа не видит.
  • Возвращает разобранный JSON ответа (пустой ответ — {}). Ошибка GGsel или сети — MarketplaceError: status — HTTP-код, message — пояснение GGsel, body — начало ответа.
  • Запросы идут с сервера Lovsel — того же адреса, что внесён в белый список IP магазина в GGsel («Настройки API» → «IP-адреса»), поэтому отдельно ничего настраивать не нужно.

Методы, которыми пользуется сам Lovsel:

python
seller = ctx.ggsel.seller_id

# v1: последние продажи, подробности продажи, проверка уникального кода, баланс, отзывы
sales = ctx.ggsel.request("GET", "/seller-last-sales", params={"seller_id": seller, "top": 50, "group": "false"})
info = ctx.ggsel.request("GET", "/purchase/info/49916703")
code = ctx.ggsel.request("GET", "/purchases/unique-code/A1B2C3D4E5")
money = ctx.ggsel.request("GET", "/sellers/account/balance/info")
reviews = ctx.ggsel.request("GET", "/reviews", params={"seller_id": seller, "type": "all", "page": 1, "rows": 50})

# v1: товары продавца постранично и диалоги
goods = ctx.ggsel.request("POST", "/seller-goods", json={"id_seller": int(seller), "page": 1, "rows": 100,
                                                         "currency": "RUB", "lang": "ru-RU", "show_hidden": 1})
chats = ctx.ggsel.request("GET", "/debates/v2/chats", params={"pagesize": 100, "page": 1})
ctx.ggsel.request("POST", "/debates/v2", params={"id_i": 49916703}, json={"message": "Готово!"})

# v2: оффер — прочитать, изменить любые поля, включить, приостановить, удалить
offer = ctx.ggsel.request("GET", "/offers/102734877", version="v2")
ctx.ggsel.request("PATCH", "/offers/102734877", json={"quantity": 10, "is_autoselling": True}, version="v2")
ctx.ggsel.request("POST", "/offers/batch_activate", json={"offer_ids": [102734877]}, version="v2")
ctx.ggsel.request("POST", "/offers/batch_pause", json={"offer_ids": [102734877]}, version="v2")

Другие методы Seller API вызываются так же — по пути и параметрам из документации GGsel. Если GGsel добавит новый метод, плагин сможет вызвать его сразу, без обновления Lovsel.

python
from lovsel_sdk import MarketplaceError, ui

@app.action("restock", confirm="Поставить остаток 10 у выбранных офферов?")
def restock(ctx, data):
    done = 0
    for offer_id in data.get("ids") or []:
        try:
            ctx.ggsel.request("PATCH", "/offers/%s" % offer_id, json={"quantity": 10}, version="v2")
            done += 1
        except MarketplaceError as exc:
            ctx.log("Оффер %s: %s" % (offer_id, exc.message), "error")
    return ui.result("Остаток обновлён у %d офферов" % done)

На тестовом стенде прямые запросы отвечают из параметра api (см. «Проверка плагина»):

python
plugin = load_plugin("my_plugin.py", api={"PATCH /offers/101": {"data": {"id": 101, "quantity": 10}}})
plugin.action("restock", {"ids": [101]})
assert plugin.api_calls[0]["json"] == {"quantity": 10}

Поля заказа

Поля заказа из orders(): id, product_id, title, amount, profit, currency, status (код: paid, delivered, refunded…), status_label (подпись для людей: «Оплачен»), status_tone (цвет бейджа), is_refund, buyer, chat_id, created_at.

  • В интерфейсе показывайте status_label, а не код.
  • Валюта у каждого заказа своя (currency): не суммируйте заказы в разных валютах и не подставляйте «₽» по умолчанию. Для колонки таблицы используйте format="money", для чисел — ui.money(value, currency).
  • Товары из products(): id, title, price, currency, active.

События

python
@app.on("new_order")
def on_new_order(ctx, event):
    order = event["order"]      # id, product_id, chat_id, status, title, amount, currency, buyer
    if order["product_id"] in ctx.storage.get("watched", []):
        ctx.ggsel.send_message(order["chat_id"], "Спасибо за покупку! Готовлю заказ.")
СобытиеДанные
new_orderorder — новый заказ.
order_statusorder, old_status — заказ сменил статус.
new_chatchat_id, customer_name — покупатель впервые написал.
new_messagechat_id, message (id, text, author).
initМагазин запущен (получают все плагины).

В каждом событии есть marketplace: "ggsel". События GGsel приходят только плагинам GGsel ("marketplace": "ggsel"). Обработчик должен работать быстро — долгую работу запускайте операцией.

Оплата закупок с баланса Lovsel

Плагин может купить товар у поставщика под оплаченный заказ GGsel и оплатить покупку с единого баланса продавца в Lovsel: резерв под заказ → покупка → списание (закупка + комиссия Lovsel) → выдача покупателю. Плагин не трогает деньги сам — он вызывает ctx.wallet с разрешением wallet, а сервер проверяет заказ, сумму и защищает от двойных списаний.

python
@app.on("new_order")
def on_order(ctx, event):
    order = event["order"]
    res = ctx.wallet.purchase(order["id"], 100, marketplace="ggsel", buy=buy, check=check, title="Ключ")
    if res.ok:
        ctx.ggsel.send_message(order["chat_id"], "Ваш товар: " + res.result["goods"])
        res.operation.mark_delivered()

Подробно, с нехваткой денег, зависшими запросами и возвратами, — «Единый баланс Lovsel в плагинах».

Ограничения Seller API

Lovsel не ограничивает плагины: чего нет в Seller API, того нет и в плагине, а всё, что в нём есть, доступно через ctx.ggsel.request(). По документации Seller API, которой пользуется Lovsel:

  • нет подтверждения, отмены и возврата заказа (полного и частичного) — это делает продавец в кабинете GGsel;
  • нет изменения скидочной цены;
  • баланс возвращается в WMT (эквивалент доллара США);
  • изменения товаров выполняются асинхронно: GGsel принимает задачу и применяет её через несколько секунд.

Не опрашивайте GGsel в цикле без паузы: для заказов используйте историю ctx.ggsel.orders(), а прямые запросы делайте по действию продавца, по событию или в операции.

Пример: отчёт по продажам

python
# -*- coding: utf-8 -*-
import csv
import io

from lovsel_sdk import ValidationError, WebPlugin, ui

MANIFEST = {
    "id": "acme.price-report",
    "name": "Отчёт по продажам",
    "version": "1.0.0",
    "author": "ACME Studio",
    "description": "Сводка продаж и выгрузка в CSV.",
    "category": "analytics",
    "permissions": ["orders.read", "storage", "files", "notifications"],
    "capabilities": ["operations"],
    "marketplace": "ggsel",
}

app = WebPlugin(MANIFEST)

app.settings([
    ui.field.number("goal", "Цель выручки на месяц", min=0, default=1000, hint="В валюте магазина"),
    ui.field.switch("notify", "Сообщать о крупных заказах", default=True),
    ui.field.number("big", "Крупный заказ от", min=1, default=50, hint="В валюте магазина"),
])


@app.page("overview", "Обзор", icon="layout-dashboard")
def overview(ctx, params):
    currency = ctx.ggsel.currency()
    orders = [o for o in ctx.ggsel.orders() if (o["currency"] or currency) == currency]
    if not orders:
        return [ui.empty("Продаж пока нет", "Отчёт появится после первых заказов.")]
    revenue = sum(o["amount"] or 0 for o in orders)
    goal = ctx.settings["goal"] or 1
    return [
        ui.stats([
            ui.stat("Выручка", ui.money(revenue, currency), icon="money"),
            ui.stat("Заказов", len(orders), icon="cart"),
        ]),
        ui.card("Цель месяца", [ui.progress(min(revenue, goal), goal, label="Выполнение цели")]),
        ui.card("Выгрузка", [
            ui.text("CSV со всеми заказами из истории Lovsel."),
            ui.actions([ui.button("Скачать CSV", action="export", variant="primary", icon="download")]),
        ]),
    ]


@app.action("export")
def export(ctx, data):
    orders = ctx.ggsel.orders()
    if not orders:
        raise ValidationError("Нет заказов для выгрузки")
    op = ctx.operations.start("Выгрузка заказов", total=len(orders))

    def work(op):
        out = io.StringIO()
        writer = csv.writer(out, delimiter=";")
        writer.writerow(["Заказ", "Товар", "Сумма", "Статус"])
        for o in orders:
            if op.cancelled:
                return "Остановлено"
            writer.writerow([o["id"], o["title"], o["amount"], o["status_label"]])
            op.step(current="#" + str(o["id"]))
        op.finish("Готово", result={"download": ctx.download("orders.csv", "" + out.getvalue(), "text/csv")})

    op.run_in_background(work)
    return ui.result("Выгрузка запущена", operation=op)


@app.on("new_order")
def big_order(ctx, event):
    order = event["order"]
    if ctx.settings["notify"] and float(order.get("amount") or 0) >= ctx.settings["big"]:
        ctx.notify("Крупный заказ #%s" % order["id"], order.get("title") or "", order_id=order["id"])

Полный пример: благодарности покупателям

Файл docs/examples/ggsel_thanks.py (кнопка «Пример плагина» вверху статьи) — готовый плагин с интерфейсом, как у встроенных разделов Lovsel. После оплаты заказа он отправляет покупателю сообщение по правилу товара.

Что в нём есть:

СтраницаКомпоненты SDK
Обзорui.header с главной кнопкой, ui.stats с изменением к вчерашнему дню, «Требуют внимания» (ui.list_view с кнопкой «Повторить»), график ui.chart(kind="bar"), лента ui.timeline.
ПравилаТаблица с вкладками-фильтрами «Работают / Выключены», поиском, выбором строк и действиями над выбранными, меню «⋯», переход в правило по нажатию на строку, итог под таблицей.
Новое правилоСкрытая страница с мастером ui.wizard: товар из списка с поиском → текст с переменными и предпросмотром в чате GGsel → проверка. Шаг проверяется действием validate.
ПравилоШапка с «назад» и бейджем, переключатель ui.toggle, форма с field.template и кнопкой «Вернуть стандартный текст», история по товару, удаление в ui.details.
ИсторияПоиск и фильтр в ui.toolbar (параметры страницы), таблица, окно сбоку с перепиской ui.chat.
ИнструкцияПункт внизу меню: этапы ui.steps и ui.message_preview.

Меню плагина — слева (layout="sidebar"), со счётчиками: число правил и красный счётчик ошибок отправки.

Главное из кода:

python
app = WebPlugin(MANIFEST, layout="sidebar")

VARIABLES = [
    ui.var("buyer", "Имя покупателя", "Алексей"),
    ui.var("product", "Название товара", "Telegram · подписчики"),
    ui.var("order_id", "Номер заказа", "10842"),
    ui.var("amount", "Сумма заказа", "450 ₽"),
]


@app.on("new_order")
def on_new_order(ctx, event):
    order = event.get("order") or {}
    rule = next((r for r in _rules(ctx).values() if str(r["product_id"]) == str(order.get("product_id"))), None)
    if rule is None or not rule.get("enabled"):
        return
    sent = ctx.storage.get("sent_orders", []) or []
    if str(order.get("id")) in sent:
        return  # повторное событие по тому же заказу
    _send(ctx, rule, order)
    ctx.storage.set("sent_orders", (sent + [str(order.get("id"))])[-2000:])


@app.page("rule_new", "Новое правило", icon="plus", hidden=True)
def rule_new(ctx, params):
    options = [{"value": str(p["id"]), "label": p["title"],
                "hint": "#%s · %s" % (p["id"], ui.money(p.get("price"), p.get("currency")))}
               for p in _products(ctx)]
    return [
        ui.header("Новое правило", back=ui.button("Все правила", page="rules")),
        ui.wizard("new_rule", [
            ui.step("product", "Товар", [
                ui.field.select("product_id", "Товар GGsel", options, searchable=True, required=True)]),
            ui.step("message", "Сообщение", [
                ui.field.template("template", "Текст благодарности", variables=VARIABLES, required=True)]),
            ui.step("review", "Проверка", review=True),
        ], action="create_rule", submit="Создать правило", validate="check_rule"),
    ]

Проверить без сервера:

bash
python -m lovsel_sdk.testing docs/examples/ggsel_thanks.py
python
from lovsel_sdk.testing import load_plugin

plugin = load_plugin("docs/examples/ggsel_thanks.py",
                     products=[{"id": 101, "title": "Steam ключ Portal 2", "price": 199, "currency": "RUB"}])
plugin.action("create_rule", {"product_id": "101", "template": "Спасибо, {buyer}!", "enabled": True})
plugin.emit("new_order", {"order": {"id": "9001", "product_id": 101, "chat_id": 9001, "buyer": "anna"}})
assert plugin.ggsel_sent == [{"chat_id": "9001", "text": "Спасибо, anna!"}]

Как устроены компоненты и что ещё умеют таблицы, формы и мастера — в статье «Компоненты интерфейса SDK».