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

Плагины для GGsel

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

Содержание

Плагин для GGsel работает с магазином продавца через API Lovsel: читает историю продаж, отвечает покупателям, меняет товары и реагирует на события магазина. Плагин не хранит и не запрашивает API-ключ GGsel — Lovsel уже подключил магазин и сам выполняет запросы Seller API.

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

Манифест

Плагин без поля marketplaces считается плагином GGsel — старые плагины работают как раньше. Явно:

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

Разрешения

РазрешениеМетоды ctx.ggsel
orders.readorders(), order(), currency()
chats.readmessages()
chats.writesend_message()
products.readproducts(), product()
products.writeset_price(), update_product(), set_active()
reviews.readreviews()
balance.readbalance()

Без нужного разрешения метод выбрасывает PermissionDenied с понятным текстом.

API ctx.ggsel

python
orders = ctx.ggsel.orders()                 # история продаж (orders.read)
info = ctx.ggsel.order("49916703")          # purchase/info заказа (orders.read)
msgs = ctx.ggsel.messages(49916703)          # переписка заказа (chats.read)
ctx.ggsel.send_message(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)
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                         # название магазина
МетодОбращается к GGsel
orders(), currency()нет — история продаж хранится в Lovsel с момента подключения магазина
order(), messages(), send_message()да
products(), product(), set_price(), update_product(), set_active()да
reviews(), balance()да

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

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

Поля заказа

Поля заказа из 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" в marketplaces. Обработчик должен работать быстро — долгую работу запускайте операцией.

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

  • Нет подтверждения, отмены и возврата заказа (полного и частичного) — это делает продавец в кабинете 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"],
    "marketplaces": ["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».