Платежи
Провайдерские клиенты (YooKassa, Tinkoff) и построение чеков 54-ФЗ живут в отдельном,
секретонезависимом пакете @launchkit/payments — стартер не дублирует эту логику, а только
оркестрирует вызовы (src/lib/payments/).
Выбор провайдера
# PAYMENT_PROVIDER: mock | yookassa | tinkoff
PAYMENT_PROVIDER="mock"
YOOKASSA_SHOP_ID=""
YOOKASSA_SECRET_KEY=""
TINKOFF_TERMINAL_KEY=""
TINKOFF_PASSWORD=""
По умолчанию — mock: полный цикл checkout работает без каких-либо ключей, что удобно
для локальной разработки и демо. src/lib/payments/config.ts резолвит провайдера из env и
падает с понятной ошибкой, если выбран yookassa/tinkoff, а ключи не заданы:
export function resolvePaymentProvider(env: Env): ResolvedPaymentProvider {
switch (env.PAYMENT_PROVIDER) {
case 'yookassa':
if (!env.YOOKASSA_SHOP_ID || !env.YOOKASSA_SECRET_KEY) {
throw new Error('PAYMENT_PROVIDER=yookassa requires YOOKASSA_SHOP_ID and YOOKASSA_SECRET_KEY');
}
// ...
Как получить ключи
YooKassa — yookassa.ru/joinups: после подключения магазина в
личном кабинете доступны shopId и secretKey (Настройки → API).
Tinkoff Касса — tinkoff.ru/kassa: после подписания договора
в личном кабинете эквайринга выдаются TerminalKey и Password.
Оба провайдера имеют тестовый (sandbox) режим — используйте его, пока не готовы принимать реальные платежи.
Чек 54-ФЗ
Продукт работает по налоговому режиму ИП на УСН, 54-ФЗ (онлайн-касса). Основной канал фискализации — PSP-embedded: и YooKassa, и Tinkoff выпускают фискальный чек как часть самого вызова оплаты/возврата, отдельная касса/ОФД не нужны.
// src/lib/payments/receipt.ts
export function buildLicenseReceipt(order: LicenseOrder): ReceiptRequest {
return buildReceipt(
{
items: [{
description: `Лицензия LaunchKit — тариф «${order.plan}»`,
quantity: 1,
amountKopecks: order.amountKopecks,
vatCode: 'none', // без НДС — ИП на УСН
subject: 'service',
}],
totalAmountKopecks: order.amountKopecks,
customer: { email: order.customerEmail },
taxMode: 'ip_usn_54fz',
fiscalRail: 'psp_embedded',
},
'full_payment',
'income',
);
}
vatCode и subject (признак предмета расчёта) в этом примере — значения, подтверждённые
бухгалтером для лицензии на код (услуга/результат интеллектуальной деятельности). Если у вас
другой вид товара/услуги — сверьте эти поля со своим бухгалтером, это не техническое решение.
Все денежные суммы передаются в копейках (amountKopecks) — так исключается класс ошибок
округления с плавающей точкой на денежных суммах.
Создание платежа (checkout)
// src/lib/payments/checkout.ts
export async function createLicenseCheckout(params: CreateCheckoutParams) {
const receipt = buildLicenseReceipt(params.order);
return params.provider.create(params.creds, {
orderId: params.orderId,
amountKopecks: params.order.amountKopecks,
currency: 'RUB',
description: `Оплата тарифа «${params.order.plan}»`,
returnUrl: params.returnUrl,
idempotenceKey: params.idempotenceKey,
receipt,
notificationUrl: params.notificationUrl, // нужен только Tinkoff — YooKassa настраивает webhook в кабинете
});
}
Роут POST /api/checkout резолвит провайдера, вызывает createLicenseCheckout и возвращает
confirmationUrl — на неё редиректится покупатель для оплаты. idempotenceKey обязателен для
обоих провайдеров: он не даёт создать два платежа при повторном сабмите формы или при ретрае
после сетевой ошибки.
Возврат (возврат прихода)
По 54-ФЗ возврат денег требует выпуска отдельного фискального документа «возврат прихода», а
не просто отмены оплаты у провайдера. buildLicenseRefundReceipt строит такой чек, и он
передаётся вместе с вызовом refund:
export async function refundLicense(params: RefundParams) {
const receipt = buildLicenseRefundReceipt(params.order, params.refundAmountKopecks);
return params.provider.refund(params.creds, {
providerPaymentId: params.providerPaymentId,
amountKopecks: params.refundAmountKopecks ?? params.order.amountKopecks,
idempotenceKey: params.idempotenceKey,
receipt,
});
}
На основном (psp_embedded) рельсе провайдер выпускает возврат-прихода атомарно вместе с самим
возвратом денег в одном вызове API — отдельного похода в кассу/ОФД не требуется. Поддерживается
частичный возврат: refundAmountKopecks можно указать меньше полной суммы заказа.
Тестовый провайдер (mock)
createMockPaymentProvider() реализует тот же интерфейс PaymentProvider, что и боевые клиенты,
но без сетевых вызовов — платёж мгновенно переходит в succeeded. Используйте его в тестах и
для локальной разработки без реальных ключей; переключение на боевой провайдер — это только
смена PAYMENT_PROVIDER в .env, код checkout/refund не меняется.