Плагин для GGsel — дополнительные возможности магазина на GGsel. Всё, что разрешает GGsel Seller API, можно сделать в плагине: готовыми методами ctx.ggsel (история продаж, сообщения, товары, отзывы, баланс) или прямым запросом к любому методу Seller API — ctx.ggsel.request(). Плагин не хранит и не запрашивает API-ключ GGsel: Lovsel уже подключил магазин, сам входит в Seller API и подставляет токен и ключ в каждый запрос.
Общие правила создания плагина — структура файла, страницы, формы, операции, публикация — в руководстве «Как создать плагин». Здесь — только то, что относится к GGsel.
Манифест
Каждый плагин работает с одной площадкой: GGsel, FunPay или Playerok. Каталог плагинов у каждой площадки свой. Плагин GGsel объявляет площадку полем marketplace:
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.read | orders(), order(), currency(), check_code() |
chats.read | chats(), messages() |
chats.write | send_message(), mark_read() |
products.read | products(), product() |
products.write | set_price(), update_product(), set_active(), delete_product() |
reviews.read | reviews() |
balance.read | balance() |
marketplace.api | request() — любой метод Seller API, и все методы выше |
Без нужного разрешения метод выбрасывает PermissionDenied с понятным текстом. Разрешения — объявление для покупателя, а не предел возможностей: нужны любые действия Seller API — объявите marketplace.api («Полный доступ к API площадки»).
API ctx.ggsel
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-кодом). Ловите их и показывайте понятный текст:
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:
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.
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 (см. «Проверка плагина»):
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.
События
@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 ("marketplace": "ggsel"). Обработчик должен работать быстро — долгую работу запускайте операцией.
Оплата закупок с баланса Lovsel
Плагин может купить товар у поставщика под оплаченный заказ GGsel и оплатить покупку с единого баланса продавца в Lovsel: резерв под заказ → покупка → списание (закупка + комиссия Lovsel) → выдача покупателю. Плагин не трогает деньги сам — он вызывает ctx.wallet с разрешением wallet, а сервер проверяет заказ, сумму и защищает от двойных списаний.
@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(), а прямые запросы делайте по действию продавца, по событию или в операции.
Пример: отчёт по продажам
# -*- 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"), со счётчиками: число правил и красный счётчик ошибок отправки.
Главное из кода:
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».