Содержание
Плагин для GGsel работает с магазином продавца через API Lovsel: читает историю продаж, отвечает покупателям, меняет товары и реагирует на события магазина. Плагин не хранит и не запрашивает API-ключ GGsel — Lovsel уже подключил магазин и сам выполняет запросы Seller API.
Общие правила создания плагина — структура файла, страницы, формы, операции, публикация — в руководстве «Как создать плагин». Здесь — только то, что относится к GGsel.
Манифест
Плагин без поля marketplaces считается плагином GGsel — старые плагины работают как раньше. Явно:
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.read | orders(), order(), currency() |
chats.read | messages() |
chats.write | send_message() |
products.read | products(), product() |
products.write | set_price(), update_product(), set_active() |
reviews.read | reviews() |
balance.read | balance() |
Без нужного разрешения метод выбрасывает PermissionDenied с понятным текстом.
API ctx.ggsel
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 приходят исключениями. Ловите их и показывайте понятный текст:
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.
События
@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_order | order — новый заказ. |
order_status | order, old_status — заказ сменил статус. |
new_chat | chat_id, customer_name — покупатель впервые написал. |
new_message | chat_id, message (id, text, author). |
init | Магазин запущен (получают все плагины). |
В каждом событии есть marketplace: "ggsel". События GGsel приходят только плагинам с "ggsel" в marketplaces. Обработчик должен работать быстро — долгую работу запускайте операцией.
Ограничения Seller API
- Нет подтверждения, отмены и возврата заказа (полного и частичного) — это делает продавец в кабинете GGsel.
- Нет изменения скидочной цены.
- Баланс возвращается в WMT (эквивалент доллара США).
- Изменения товаров выполняются асинхронно: GGsel принимает задачу и применяет её через несколько секунд.
- Не опрашивайте GGsel в цикле без паузы: для заказов используйте историю
ctx.ggsel.orders().
Пример: отчёт по продажам
# -*- 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"), со счётчиками: число правил и красный счётчик ошибок отправки.
Главное из кода:
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"),
]Проверить без сервера:
python -m lovsel_sdk.testing docs/examples/ggsel_thanks.pyfrom 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».