Перейти к содержанию

Lemon Squeezy — международные платежи картой

Lemon Squeezy (LS) — платёжная платформа с моделью Merchant of Record: они выступают продавцом от своего имени, сами собирают налоги (VAT/GST) по всему миру и принимают Visa/Mastercard от иностранных пользователей.

Почему не Тинькофф для иностранцев? С марта 2022 года Visa и Mastercard прекратили обработку транзакций у российских эквайеров — иностранные карты получают DECLINED независимо от валюты счёта. Tinkoff физически не может принять оплату с зарубежной карты.


Архитектура интеграции

Пользователь нажимает "Pay with card" в дашборде
    → POST /api/billing/init-ls {tariffId}
    → BillingController строит checkout URL напрямую (без API call к LS):
        https://<storeSlug>.lemonsqueezy.com/checkout/buy/<variantId>
            ?checkout[email]=...&checkout[custom][user_id]=...
    → Браузер открывает URL и переходит на страницу оплаты LS
    → Оплата проходит
    → LS шлёт webhook на POST /api/billing/notify-ls
        X-Signature: HMAC-SHA256(rawBody, signing_secret)
        event: subscription_created | subscription_payment_success
    → BillingController добавляет 30 дней в user_tariffs
    → Пользователь возвращается на /auth/protected?ls_success=1

Почему URL, а не API? Cloudflare блокирует server-side запросы с datacenter/VPS IP к api.lemonsqueezy.com. URL-подход (браузер открывает checkout напрямую) обходит это ограничение без изменений в логике приёма платежей.

Типы событий подписки:

Событие Когда Действие бэкенда
subscription_created Первый платёж при оформлении +30 дней в пул
subscription_payment_success Ежемесячное автосписание +30 дней в пул
subscription_cancelled Пользователь отменил подписку Только лог; дни истекают сами

Идемпотентность: payments.ls_order_id = уникальный ID события (ID подписки или ID инвойса) — дублирующий webhook просто вернёт 200 already processed.


Часть 1 — Настройка аккаунта в Lemon Squeezy

1.1 Регистрация

  1. Зайди на app.lemonsqueezy.com
  2. Регистрируйся — поддерживаются физлица из РФ и других стран
  3. После входа попадёшь в Dashboard

1.2 Заполнить профиль продавца (обязательно для вывода денег)

Settings → Your profile: - Имя, адрес, ИНН/Tax ID (для физлиц из РФ — ИНН) - Способ вывода: банковская карта или счёт (Wise, PayPal и т.д.)

1.3 Подключить платёжный метод для биллинга платформы

LS берёт комиссию с продаж (~5% + $0.50). Нужна карта в Settings → Billing.


Часть 2 — Создание магазина

2.1 Создать магазин

Settings → Stores → Add store: - Name: dix.su (или любое) - Slug: dixsu (используется в checkout URL) - Currency: USD (рекомендуется для международной аудитории)

После создания в строке магазина будет Store ID (#12345) — запиши его.


Часть 3 — Создание продуктов (Subscription)

Нужно три продукта — по одному на каждый тариф. Для каждого:

Products → Add product → Subscription

Настройки продукта

Тариф Name Price Billing period
VIP VIP — 30 days $4.99 Monthly
PRO PRO — 30 days $7.99 Monthly
PERS PERS — 30 days $12.99 Monthly

Trial period — оставить пустым (платный с первого дня).

Checkout passthrough / Custom data — НЕ заполнять здесь; user_id и tariff_id передаются программно при создании checkout через API.

Получить Variant ID

После сохранения продукта: Products → [название] → Variants. У каждого варианта справа будет его ID (число, например 823456). Записать ID для VIP, PRO и PERS.


Часть 4 — API Key

Settings → API → Add API key: - Название: dixsu-prod - Нажать Create - Скопировать ключ немедленно — показывается только один раз


Часть 5 — Webhook

Settings → Webhooks → Add webhook:

Callback URL:

https://dix.su/api/billing/notify-ls

Signing secret — придумываешь сам (произвольная строка ≥ 32 символа). Сгенерировать случайную:

openssl rand -hex 32

Events — отметить три: - ✅ subscription_created - ✅ subscription_payment_success - ✅ subscription_cancelled

Сохранить. Signing secret больше нигде не показывается — запиши его.


Часть 6 — Переменные окружения на VPS

ssh -p 1022 root@217.114.43.49
nano /root/dixsu/.env

Добавить в конец файла:

# === Lemon Squeezy ===
LS_STORE_SLUG=dixsu                         # Slug магазина из шага 2.1 (Settings → Stores → Slug)
LS_API_KEY=eyJ0eXAiOiJKV1QiLCJhbGci...   # API Key из шага 4
LS_STORE_ID=12345                           # Store ID из шага 2.1
LS_SIGN_SECRET=a1b2c3d4e5f6...             # Signing secret из шага 5
LS_VARIANT_VIP_30D=823456                   # Variant ID VIP из шага 3
LS_VARIANT_PRO_30D=823457                   # Variant ID PRO
LS_VARIANT_PERS_30D=823458                  # Variant ID PERS

Сохранить (Ctrl+O, Ctrl+X), затем перезапустить:

cd /root/dixsu && docker compose up -d modulauth


Часть 7 — Проверка

7.1 Убедиться что кнопка появилась

Зайти на https://dix.su/auth/protected → раздел Тарифы. У каждого платного тарифа должна появиться кнопка "💳 Pay with card $X.XX" рядом с кнопкой Тинькофф (или вместо неё, если Тинькофф не настроен).

7.2 Протестировать webhook без реальной оплаты

В LS Dashboard:

Settings → Webhooks → [твой webhook] → кнопка "Send test"

Выбрать subscription_created → нажать Send. Проверить логи modulauth:

docker logs dixsu-modulauth-1 --tail=30 | grep '\[ls\]'

Ожидаемое в логах:

[ls] subscription_created userId=null tariff=null eventId=test-...
[ls] webhook missing required fields: ...

Тестовый ивент не содержит custom_data — это нормально. Главное что вебхук дошёл и подпись прошла (если был бы invalid signature — значит LS_SIGN_SECRET неверный).

7.3 Тестовая оплата

LS имеет тестовый режим (Test mode в шапке Dashboard). В тестовом режиме checkout принимает тестовые карты:

Карта Результат
4242 4242 4242 4242 Успешная оплата
4000 0000 0000 0002 Отказ

Дата — любая в будущем, CVV — любые 3 цифры.

Провести тестовую оплату → проверить что в истории платежей появилась запись, а в user_tariffs добавились 30 дней:

docker exec dixsu-postgres-1 psql -U dixsu -c \
  "SELECT role, remaining_days, status FROM user_tariffs WHERE user_id = <ID> ORDER BY id DESC LIMIT 5;"


Часть 8 — Производственный режим

После успешного теста переключить магазин в production: Settings → Stores → [магазин] → переключатель Test mode → OFF

Variant IDs в production могут отличаться от тестовых — пересмотреть и обновить LS_VARIANT_* в .env если нужно.


Диагностика

Webhook не доходит

# Проверить доступность endpoint извне
curl -s -o /dev/null -w "%{http_code}" -X POST https://dix.su/api/billing/notify-ls \
  -H "Content-Type: application/json" -d '{}'
# Должен вернуть 401 (подпись не совпадает) — значит endpoint доступен

Если 404 — modulauth не задеплоен или SecurityConfig не включил путь.

Подпись не совпадает (401 в логах)

docker logs dixsu-modulauth-1 | grep 'invalid webhook signature'

Причина: LS_SIGN_SECRET в .env не совпадает с Signing Secret в LS Dashboard. Проверить пробелы и переводы строк при копировании.

Дни не начислились

docker logs dixsu-modulauth-1 | grep '\[ls\]'

Смотреть строки вида [ls] subscription_created userId=... tariff=.... Если userId=nullcustom_data не пришёл от LS. Убедиться что checkout создаётся через /api/billing/init-ls, а не через прямую ссылку на продукт.

Дублирующий webhook (начислено дважды)

Не должно произойти — защищено по ls_order_id. Проверить:

docker exec dixsu-postgres-1 psql -U dixsu -c \
  "SELECT ls_order_id, COUNT(*) FROM payments WHERE provider='lemonsqueezy'
   GROUP BY ls_order_id HAVING COUNT(*) > 1;"


Изменение цен

Цены управляются в LS Dashboard (Products → Variants → Edit price). В коде цены для отображения на кнопке хранятся в LemonSqueezyService.USD_CENTS — обновить при изменении, пересобрать modulauth:

// LemonSqueezyService.java
public static final Map<String, Integer> USD_CENTS = Map.of(
    "vip_30d",  499,   // ← изменить
    "pro_30d",  799,
    "pers_30d", 1299
);


Чеки НПД для платежей через LS

Платежи через Lemon Squeezy не создают НПД-чеки — LS сам выступает продавцом и выдаёт свой инвойс клиенту. Это корректно: дважды выставлять чек за одну продажу нельзя. Если бизнес-форма сменится на ИП с онлайн-кассой, LS-платежи также не требуют фискализации через российскую кассу — продажа юридически совершена от имени LS (ирландская юрисдикция).


Справочник env-переменных

Переменная Откуда взять Пример
LS_STORE_SLUG Settings → Stores → поле Slug под названием dixsu
LS_API_KEY Settings → API → ключ (показывается один раз) eyJ0eXAiOiJKV1...
LS_STORE_ID Settings → Stores → #XXXXX 12345
LS_SIGN_SECRET Settings → Webhooks → Signing Secret a1b2c3d4...
LS_VARIANT_VIP_30D Products → VIP → Variants → ID 823456
LS_VARIANT_PRO_30D Products → PRO → Variants → ID 823457
LS_VARIANT_PERS_30D Products → PERS → Variants → ID 823458

Где смотреть код

Файл Что делает
modulauth/.../billing/LemonSqueezyService.java Создание checkout, верификация подписи webhook
modulauth/.../billing/BillingController.java POST /api/billing/init-ls, POST /api/billing/notify-ls
modulauth/.../config/SecurityConfig.java /api/billing/notify-ls разрешён без авторизации
modulauth/.../db/migration/V45__Add_ls_payments.sql provider, currency, ls_order_id в таблице payments
templates/protected-page.html Кнопка "💳 Pay with card", баннер ?ls_success=1