{"_id":"@besales/robokassa-sdk","_rev":"5-a7aa4ee6898844fff12ee7574d028f08","name":"@besales/robokassa-sdk","dist-tags":{"latest":"0.2.0"},"versions":{"0.1.0":{"name":"@besales/robokassa-sdk","version":"0.1.0","author":{"name":"Vlad Petlyuk"},"license":"ISC","_id":"@besales/robokassa-sdk@0.1.0","maintainers":[{"name":"peresvets","email":"nick.soloview@gmail.com"}],"dist":{"shasum":"74b85887c37707210762b7319aa90c3b15d6b2ed","tarball":"https://registry.npmjs.org/@besales/robokassa-sdk/-/robokassa-sdk-0.1.0.tgz","fileCount":163,"integrity":"sha512-KgNmouZZe3xHgmX/blP6L71rCSdM9TCYnPZoTogXSuUpwFkvOSxQkgpaOXuBLaVKr01utC9WuNPqdqepztE1vQ==","signatures":[{"sig":"MEQCIEARnRo8EUffRVupMOFEQEpKnVri/WU6Ojy10CsC8HtYAiA93KCHImmz4UKBZQ0bIkRITtZwPwEEoj1/uR72Ha8Q6A==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":232635},"main":"./dist/cjs/index.js","types":"./dist/cjs/index.d.ts","module":"./dist/esm/index.js","engines":{"node":">=18.0.0"},"exports":{".":{"types":"./dist/cjs/index.d.ts","import":"./dist/esm/index.js","require":"./dist/cjs/index.js"},"./webhooks":{"types":"./dist/cjs/webhooks/index.d.ts","import":"./dist/esm/webhooks/index.js","require":"./dist/cjs/webhooks/index.js"}},"gitHead":"727f88cbe47009cd7e4c030dae57e47d0f4212b4","scripts":{"test":"jest","build":"yarn build:cjs && yarn build:esm","clean":"rm -rf dist","build:cjs":"tsc -p tsconfig.cjs.json","build:esm":"tsc -p tsconfig.esm.json && echo '{\"type\":\"module\"}' > dist/esm/package.json","prepublishOnly":"yarn clean && yarn build"},"_npmUser":{"name":"peresvets","email":"nick.soloview@gmail.com"},"_npmVersion":"11.11.0","description":"SDK клиент для Robokassa Payment Microservice (server-side only)","directories":{},"_nodeVersion":"25.8.0","_hasShrinkwrap":false,"devDependencies":{"jest":"^29.7.0","ts-jest":"^29.4.6","typescript":"^5.3.3","@types/jest":"^29.5.0","@types/node":"^20.11.20"},"_npmOperationalInternal":{"tmp":"tmp/robokassa-sdk_0.1.0_1773151657094_0.3699561770579589","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@besales/robokassa-sdk","version":"0.1.1","author":{"name":"Vlad Petlyuk"},"license":"ISC","_id":"@besales/robokassa-sdk@0.1.1","maintainers":[{"name":"peresvets","email":"nick.soloview@gmail.com"}],"dist":{"shasum":"37cf18c7094a1c442f7d8f83be4408a84d62e3c4","tarball":"https://registry.npmjs.org/@besales/robokassa-sdk/-/robokassa-sdk-0.1.1.tgz","fileCount":163,"integrity":"sha512-v1M5IBOy+DIgyHaOiMWEzSLXVSd8neGlMjT31YMx5dx3pfUxgQqKejnm4NoI4tL4zYbTeWW824QgsZGipAX13w==","signatures":[{"sig":"MEUCIQDnTFenuuiPUocFZczlbuwi9KyU2kcQOCx2dVXNQ7iPEgIgClDWx5aqW3cWXMDwRC4P/oVPkIQFW06iwpAEK2X/TOw=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":232635},"main":"./dist/cjs/index.js","types":"./dist/cjs/index.d.ts","module":"./dist/esm/index.js","engines":{"node":">=18.0.0"},"exports":{".":{"types":"./dist/cjs/index.d.ts","import":"./dist/esm/index.js","require":"./dist/cjs/index.js"},"./webhooks":{"types":"./dist/cjs/webhooks/index.d.ts","import":"./dist/esm/webhooks/index.js","require":"./dist/cjs/webhooks/index.js"}},"gitHead":"86fe7f3ae3fc967b5a9ab09f5bbc8dab250dd6c6","scripts":{"test":"jest","build":"yarn build:cjs && yarn build:esm","clean":"rm -rf dist","build:cjs":"tsc -p tsconfig.cjs.json","build:esm":"tsc -p tsconfig.esm.json && echo '{\"type\":\"module\"}' > dist/esm/package.json","prepublishOnly":"yarn clean && yarn build"},"_npmUser":{"name":"peresvets","email":"nick.soloview@gmail.com"},"_npmVersion":"11.11.0","description":"SDK клиент для Robokassa Payment Microservice (server-side only)","directories":{},"_nodeVersion":"25.8.0","_hasShrinkwrap":false,"devDependencies":{"jest":"^29.7.0","ts-jest":"^29.4.6","typescript":"^5.3.3","@types/jest":"^29.5.0","@types/node":"^20.11.20"},"_npmOperationalInternal":{"tmp":"tmp/robokassa-sdk_0.1.1_1773317465386_0.530232893879174","host":"s3://npm-registry-packages-npm-production"}},"0.1.2":{"name":"@besales/robokassa-sdk","version":"0.1.2","author":{"name":"Vlad Petlyuk"},"license":"ISC","_id":"@besales/robokassa-sdk@0.1.2","maintainers":[{"name":"peresvets","email":"nick.soloview@gmail.com"}],"dist":{"shasum":"d8594a8b0c8895139f973d77c75945575bcce3b6","tarball":"https://registry.npmjs.org/@besales/robokassa-sdk/-/robokassa-sdk-0.1.2.tgz","fileCount":163,"integrity":"sha512-7FxUM6mv9lCtfp6DtNlrEzCG2/1IX743pv6XCzAnw2XqjqBlFYNK1rrT6ps6X4iGQMOsnOIolw5hJxqq2J+sQg==","signatures":[{"sig":"MEQCIDJ1vRuobcJfJXr/3aDugaXig5nSdZ2QxAUB7y7Lw9qGAiBUlZDUD7bj6OAQR25RevKGyq05+a3NWx8nYYECFmf7Yg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":244224},"main":"./dist/cjs/index.js","types":"./dist/cjs/index.d.ts","module":"./dist/esm/index.js","engines":{"node":">=18.0.0"},"exports":{".":{"types":"./dist/cjs/index.d.ts","import":"./dist/esm/index.js","require":"./dist/cjs/index.js"},"./webhooks":{"types":"./dist/cjs/webhooks/index.d.ts","import":"./dist/esm/webhooks/index.js","require":"./dist/cjs/webhooks/index.js"}},"gitHead":"353e12dda1d8f52a99d27a8d9cdf2254b037572c","scripts":{"test":"jest","build":"yarn build:cjs && yarn build:esm","clean":"rm -rf dist","build:cjs":"tsc -p tsconfig.cjs.json","build:esm":"tsc -p tsconfig.esm.json && echo '{\"type\":\"module\"}' > dist/esm/package.json","prepublishOnly":"yarn clean && yarn build"},"_npmUser":{"name":"peresvets","email":"nick.soloview@gmail.com"},"_npmVersion":"11.11.0","description":"SDK клиент для Robokassa Payment Microservice (server-side only)","directories":{},"_nodeVersion":"25.8.0","_hasShrinkwrap":false,"devDependencies":{"jest":"^29.7.0","ts-jest":"^29.4.6","typescript":"^5.3.3","@types/jest":"^29.5.0","@types/node":"^20.11.20"},"_npmOperationalInternal":{"tmp":"tmp/robokassa-sdk_0.1.2_1775118638464_0.42045053646968","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@besales/robokassa-sdk","version":"0.2.0","description":"SDK клиент для Robokassa Payment Microservice (server-side only)","author":{"name":"Vlad Petlyuk"},"license":"ISC","main":"./dist/cjs/index.js","module":"./dist/esm/index.js","types":"./dist/cjs/index.d.ts","exports":{".":{"require":"./dist/cjs/index.js","import":"./dist/esm/index.js","types":"./dist/cjs/index.d.ts"},"./webhooks":{"require":"./dist/cjs/webhooks/index.js","import":"./dist/esm/webhooks/index.js","types":"./dist/cjs/webhooks/index.d.ts"}},"engines":{"node":">=18.0.0"},"scripts":{"build":"yarn build:cjs && yarn build:esm","build:cjs":"tsc -p tsconfig.cjs.json","build:esm":"tsc -p tsconfig.esm.json && echo '{\"type\":\"module\"}' > dist/esm/package.json","clean":"rm -rf dist","prepublishOnly":"yarn clean && yarn build","test":"jest"},"devDependencies":{"@types/jest":"^29.5.0","@types/node":"^20.11.20","jest":"^29.7.0","ts-jest":"^29.4.6","typescript":"^5.3.3"},"_id":"@besales/robokassa-sdk@0.2.0","gitHead":"27a2af8f9da579d586a78eadb1e8340d4769e74d","_nodeVersion":"22.22.2","_npmVersion":"10.9.7","dist":{"integrity":"sha512-3DWUKhD2Wb4z1z06M+uy33ieUjQZkSK4e3D+UYQJPnNbfWmQ+khJBwbpc4YAgxIyVPUQPhU3cETERtzyNlarxQ==","shasum":"2b6fee704406443b1de6b902ac2f26d5802ce15e","tarball":"https://registry.npmjs.org/@besales/robokassa-sdk/-/robokassa-sdk-0.2.0.tgz","fileCount":163,"unpackedSize":251197,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIHksi0l9DC7HGYcQbZ7fKutA+hfTWaTXWjkUhAal59qVAiBQ8eewgI/8h5zNs2QukxBrHSJv5P+W6yf00KpLj8KbVw=="}]},"_npmUser":{"name":"peresvets","email":"nick.soloview@gmail.com"},"directories":{},"maintainers":[{"name":"peresvets","email":"nick.soloview@gmail.com"},{"name":"solgalovk","email":"kirillsolgalov@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/robokassa-sdk_0.2.0_1780082758225_0.27320095944794254"},"_hasShrinkwrap":false}},"time":{"created":"2026-03-10T14:07:36.960Z","modified":"2026-05-29T19:25:58.516Z","0.1.0":"2026-03-10T14:07:37.230Z","0.1.1":"2026-03-12T12:11:05.540Z","0.1.2":"2026-04-02T08:30:38.622Z","0.2.0":"2026-05-29T19:25:58.368Z"},"author":{"name":"Vlad Petlyuk"},"license":"ISC","description":"SDK клиент для Robokassa Payment Microservice (server-side only)","maintainers":[{"name":"peresvets","email":"nick.soloview@gmail.com"},{"name":"solgalovk","email":"kirillsolgalov@gmail.com"}],"readme":"# @besales/robokassa-sdk\n\nTypeScript SDK для интеграции с микросервисом обработки платежей через Robokassa.\n\n> **SERVER-SIDE ONLY** — не использовать в браузере. SDK работает с API-ключами, которые нельзя раскрывать на клиенте.\n\n## Установка\n\n```bash\nyarn add @besales/robokassa-sdk\n# или\nnpm install @besales/robokassa-sdk\n```\n\n**Требования:** Node.js >= 18.0.0 (используется нативный `fetch`)\n\n## Быстрый старт\n\n```typescript\nimport { RobokassaPaymentClient } from '@besales/robokassa-sdk';\n\nconst client = new RobokassaPaymentClient({\n  baseUrl: 'https://payments.example.com',\n  apiKey: process.env.PAYMENT_SERVICE_API_KEY!,       // глобальный ключ\n  clientApiKey: process.env.PAYMENT_SERVICE_CLIENT_API_KEY!, // ключ клиента\n});\n\n// Одноразовый платёж\nconst { paymentLink, paymentId } = await client.payments.create({\n  userId: 'user-uuid',\n  productId: 'product-uuid',\n});\n// → redirect пользователя на paymentLink\n\n// Подписка\nconst result = await client.subscriptions.create({\n  userId: 'user-uuid',\n  planId: 'plan-uuid',\n});\nif (result.requiresPayment) {\n  // → redirect на result.paymentLink\n}\n```\n\n## Двойная аутентификация\n\nSDK использует два API-ключа:\n\n| Ключ | Откуда взять | Для чего |\n|------|-------------|----------|\n| `apiKey` | Переменная окружения `API_KEY` на сервере платежей | CRUD клиентов, продуктов, планов; создание платежей и подписок; списки; статистика |\n| `clientApiKey` | Поле `apiKey` из таблицы Client (создаётся в админке) | Получение/отмена/пауза/возобновление конкретных подписок |\n\n```typescript\n// Методы, требующие Global API Key (apiKey)\nclient.clients.create(...)     client.products.list()\nclient.plans.create(...)       client.payments.create(...)\nclient.subscriptions.create(...)\nclient.subscriptions.list(...)\n\n// Методы, требующие Client API Key (clientApiKey)\nclient.subscriptions.getById(id)\nclient.subscriptions.cancel(id, userId)\nclient.subscriptions.pause(id, userId)\nclient.subscriptions.resume(id, userId)\nclient.subscriptions.changePlan(id, userId, input)\nclient.subscriptions.clearScheduledPlanChange(id, userId)\n```\n\nЕсли `clientApiKey` не передан в конструктор, вызов методов с `auth: 'client'` бросит ошибку.\n\n## Мультиклиентская Модель\n\nОдин payment service может хранить данные многих клиентов одновременно.\n\nЭто значит:\n- `Client` — это внешний сервис;\n- `Product` и `SubscriptionPlan` принадлежат конкретному `clientName`;\n- наличие тарифов у клиента `A` ничего не говорит о наличии тарифов у клиента `B`.\n\nПрактический вывод:\n- если у вашего backend `PAYMENT_SERVICE_CLIENT_NAME=beprompt`, то вам нужны планы именно для `beprompt`;\n- планы клиента `12astro` не будут “общими” и не закроют каталог для `beprompt`.\n\nРекомендуемый сценарий:\n1. Создать `Client` для внешнего сервиса.\n2. Создать `Product` и/или `SubscriptionPlan` с тем же `clientName`.\n3. Сохранить remote `planId` во внешнем сервисе.\n\nЕсли вы строите каталог подписок, фильтруйте планы по `clientName`:\n\n```typescript\nconst { data: plans } = await client.plans.list({\n  clientName: process.env.PAYMENT_SERVICE_CLIENT_NAME!,\n  isActive: true,\n});\n```\n\n## Модули\n\n### Клиенты (clients)\n\n```typescript\n// Список всех клиентов\nconst clients = await client.clients.list();\n\n// Создать клиента\nconst newClient = await client.clients.create({\n  name: 'my_bot',\n  apiUrl: 'https://my-bot.example.com',\n  apiKey: 'webhook-secret-key',\n});\n\n// Получить по ID\nconst bot = await client.clients.getById('client-id');\n\n// Обновить\nawait client.clients.update('client-id', { apiUrl: 'https://new-url.com' });\n\n// Удалить (ошибка если есть привязанные продукты)\nawait client.clients.delete('client-id');\n```\n\n### Продукты (products)\n\nЦена продуктов указывается в **рублях**.\n\n```typescript\n// Все продукты\nconst products = await client.products.list();\n\n// Продукты конкретного клиента\nconst botProducts = await client.products.listByClient('my_bot');\n\n// Создать\nconst product = await client.products.create({\n  name: 'Пакет 100 генераций',\n  price: 999,            // 999 рублей\n  generations: 100,\n  clientName: 'my_bot',\n});\n```\n\n### Тарифные планы (plans)\n\nЦена планов указывается в **копейках** (99900 = 999 рублей).\n\n```typescript\n// Список с пагинацией\nconst { data: plans, total, page, limit } = await client.plans.list({\n  clientName: 'my_bot',\n  isActive: true,\n  page: 1,\n  limit: 20,\n});\n\n// Создать план\nconst plan = await client.plans.create({\n  name: 'Pro подписка',\n  price: 99900,           // 999 рублей (в копейках!)\n  interval: 'MONTHLY',\n  generations: 1000,\n  clientName: 'my_bot',\n  trialDays: 7,           // 7 дней бесплатного триала\n});\n\n// Обновить (PATCH, не PUT)\nawait client.plans.update('plan-id', { price: 149900 });\n\n// Удалить/деактивировать\nconst result = await client.plans.delete('plan-id');\n// result.deleted или result.deactivated\n```\n\nУдаление работает так:\n- если у плана нет подписок, он удаляется физически;\n- если у плана уже есть подписки, сервис не удаляет его физически, а деактивирует (`isActive=false`).\n\nЕсли нужно просто убрать тариф из каталога, обычно безопаснее сделать:\n\n```typescript\nawait client.plans.update('plan-id', { isActive: false });\n```\n\n### Платежи (payments)\n\n```typescript\n// Создать платёж\nconst { paymentLink, paymentId } = await client.payments.create({\n  userId: 'user-uuid',\n  productId: 'product-uuid',\n  language: 'ru',\n  robokassaConfigId: 'clp...', // опционально\n});\n// → redirect пользователя на paymentLink\n\n// Обновить данные платежа (до оплаты)\nawait client.payments.update(paymentId, { username: 'john_doe' });\n```\n\n### Подписки (subscriptions)\n\n```typescript\n// Создать подписку\nconst result = await client.subscriptions.create({\n  userId: 'user-uuid',\n  planId: 'plan-uuid',\n  language: 'ru',\n  robokassaConfigId: 'clp...', // опционально\n});\n\nif (result.requiresPayment) {\n  // Подписка без триала — нужна оплата\n  // redirect на result.paymentLink\n} else {\n  // Подписка с триалом — уже активна\n}\n\n// Список подписок\nconst { data: subs, count } = await client.subscriptions.list({\n  userId: 'user-uuid',\n  status: 'ACTIVE',\n  page: 1,\n  limit: 20,\n});\n\n// Получить по ID (требует clientApiKey)\nconst sub = await client.subscriptions.getById('sub-id');\n\n// Отменить (требует clientApiKey)\nawait client.subscriptions.cancel('sub-id', 'user-id');\n\n// Приостановить (требует clientApiKey)\nawait client.subscriptions.pause('sub-id', 'user-id', 2); // на 2 месяца\n\n// Возобновить (требует clientApiKey)\nawait client.subscriptions.resume('sub-id', 'user-id');\n\n// Немедленный апгрейд на более дорогой тариф (требует clientApiKey)\nconst upgrade = await client.subscriptions.changePlan('sub-id', 'user-id', {\n  newPlanId: 'plan-pro',\n  applyAt: 'immediately',\n});\n\nif (upgrade.requiresPayment) {\n  // Для активации апгрейда нужна доплата\n  // redirect на upgrade.paymentLink\n}\n\n// Запланированный даунгрейд на конец периода (требует clientApiKey)\nconst scheduled = await client.subscriptions.changePlan('sub-id', 'user-id', {\n  newPlanId: 'plan-basic',\n  applyAt: 'end_of_period',\n});\n\nif (scheduled.isScheduled) {\n  console.log(\n    'Переход запланирован на',\n    scheduled.subscription.scheduledPlanChangeAt\n  );\n}\n\n// Отменить запланированную смену тарифа (требует clientApiKey)\nawait client.subscriptions.clearScheduledPlanChange('sub-id', 'user-id');\n\n// Статистика\nconst stats = await client.subscriptions.getStats();\nconst cronStats = await client.subscriptions.getCronStats();\n```\n\nПравила `changePlan()`:\n- `applyAt: 'immediately'` поддерживает только переход на план с большей ценой и возвращает `paymentLink` для доплаты;\n- `applyAt: 'end_of_period'` поддерживает только переход на план с меньшей ценой и записывает `scheduledPlanId`/`scheduledPlanChangeAt`;\n- смена между разными `interval` разрешена, решение принимается только по абсолютной цене нового плана.\n\n### Health check\n\n```typescript\nconst health = await client.healthcheck();\n// { status: 'ok', service: 'payment-service', timestamp: '...' }\n\nconst subHealth = await client.subscriptions.health();\n```\n\n## Webhook-обработчики\n\nSDK предоставляет готовые обработчики webhook'ов с валидацией API-ключа.\n\nПлатёжный сервис отправляет два типа webhook'ов на ваш сервис:\n- `POST {apiUrl}/api/user/webhook` — уведомления о платежах\n- `POST {apiUrl}/api/user/subscription-webhook` — изменения статуса подписок\n\n### Express\n\n```typescript\nimport { createExpressWebhookMiddleware } from '@besales/robokassa-sdk/webhooks';\n\n// Webhook платежей\napp.post('/api/user/webhook', ...createExpressWebhookMiddleware({\n  apiKey: process.env.WEBHOOK_API_KEY!,\n  onPayment: async (payload) => {\n    console.log('Платёж:', payload.paymentId, payload.status);\n    console.log('Генерации:', payload.generationsAdded);\n    console.log('Мультикасса:', payload.robokassaCode);\n\n    if (payload.status === 'PAID') {\n      await addGenerations(payload.userId, payload.generationsAdded);\n    }\n  },\n}));\n\n// Webhook подписок\napp.post('/api/user/subscription-webhook', ...createExpressWebhookMiddleware({\n  apiKey: process.env.WEBHOOK_API_KEY!,\n  onSubscription: async (payload) => {\n    console.log('Подписка:', payload.subscriptionId, payload.status, payload.eventType);\n\n    if (payload.eventType === 'PLAN_CHANGED') {\n      await recalculateQuota(payload.userId, payload.planId, payload.previousPlanId);\n    }\n\n    if (payload.status === 'CANCELLED') {\n      await deactivateUser(payload.userId);\n    }\n  },\n}));\n```\n\n### Fastify\n\n```typescript\nimport { robokassaWebhookPlugin } from '@besales/robokassa-sdk/webhooks';\n\nfastify.register(robokassaWebhookPlugin, {\n  apiKey: process.env.WEBHOOK_API_KEY!,\n  onPayment: async (payload) => {\n    await addGenerations(payload.userId, payload.generationsAdded);\n  },\n  onSubscription: async (payload) => {\n    if (payload.status === 'CANCELLED') {\n      await deactivateUser(payload.userId);\n    }\n  },\n});\n```\n\n### Framework-agnostic (core)\n\n```typescript\nimport {\n  validateWebhookApiKey,\n  parsePaymentWebhook,\n  parseSubscriptionWebhook,\n} from '@besales/robokassa-sdk/webhooks';\n\n// В любом фреймворке\nfunction handleRequest(headers: Record<string, string>, body: unknown) {\n  if (!validateWebhookApiKey(headers, 'expected-key')) {\n    return { status: 401 };\n  }\n  const payload = parsePaymentWebhook(body);\n  // ... обработка\n}\n```\n\n## Обработка ошибок\n\nSDK предоставляет типизированные ошибки:\n\n```typescript\nimport {\n  RateLimitError,\n  NotFoundError,\n  ValidationError,\n  UnauthorizedError,\n  ConflictError,\n  OutcomeUnknownError,\n} from '@besales/robokassa-sdk';\n\ntry {\n  await client.payments.create({ userId: 'uuid', productId: 'uuid' });\n} catch (error) {\n  // API-ошибки (наследуются от PaymentApiError)\n  if (error instanceof RateLimitError) {\n    // 429 — превышен rate limit\n    console.log(`Повторите через ${error.retryAfterMs}ms`);\n  } else if (error instanceof NotFoundError) {\n    // 404 — продукт или пользователь не найден\n  } else if (error instanceof ValidationError) {\n    // 400 — невалидные входные данные\n  } else if (error instanceof UnauthorizedError) {\n    // 401 — неверный API-ключ\n  } else if (error instanceof ConflictError) {\n    // 409 — конфликт (дубликат подписки, имя занято)\n  }\n\n  // ⚠️ OutcomeUnknownError НЕ наследует PaymentApiError!\n  // Это отдельный тип — «результат запроса неизвестен», не «API вернул ошибку».\n  // catch (e instanceof PaymentApiError) его НЕ поймает.\n  if (error instanceof OutcomeUnknownError) {\n    // Сетевая ошибка на POST/PATCH/DELETE\n    // Запрос мог выполниться, а мог нет — проверьте через GET перед повтором!\n    console.log('Оригинальная ошибка:', error.originalError);\n  }\n}\n```\n\n## Retry-политика\n\n| Метод | Retry при сетевой ошибке | Retry при 429 | Почему |\n|-------|--------------------------|---------------|--------|\n| GET | Автоматически (2 попытки) | Автоматически | Безопасно, идемпотентно |\n| POST | Нет → `OutcomeUnknownError` | Нет → `RateLimitError` | Нет idempotency keys — может создать дубликат |\n| PATCH | Нет → `OutcomeUnknownError` | Нет → `RateLimitError` | Нет гарантий идемпотентности |\n| DELETE | Нет → `OutcomeUnknownError` | Нет → `RateLimitError` | Может выполниться дважды |\n\nПринудительный retry для mutating-запроса (только если уверены):\n\n```typescript\n// Opt-in retry для POST\nconst result = await client.someModule.someMethod(data, { retry: true });\n```\n\n## Конфигурация\n\n```typescript\nconst client = new RobokassaPaymentClient({\n  baseUrl: 'https://payments.example.com',  // Обязательный\n  apiKey: 'global-api-key',                  // Обязательный\n  clientApiKey: 'client-api-key',            // Для операций с подписками\n  language: 'ru',                            // Язык ответов: 'ru' | 'en'\n  timeout: 10_000,                           // Таймаут в мс (дефолт: 10000)\n  retries: 2,                                // Retry для GET (дефолт: 2)\n});\n```\n\n## Webhook payload'ы\n\n### PaymentWebhookPayload\n\n| Поле | Тип | Описание |\n|------|-----|----------|\n| `paymentId` | `string` | ID платежа |\n| `userId` | `string \\| null` | ID пользователя |\n| `status` | `string` | Статус: `PAID`, `FAILED` |\n| `amount` | `number \\| null` | Сумма в рублях |\n| `generationsAdded` | `number` | Количество начисленных генераций |\n| `productName` | `string` | Название продукта |\n| `robokassaCode` | `string` | Код Robokassa-конфигурации |\n| `isRecurring` | `boolean?` | Рекуррентный платёж? |\n| `subscriptionType` | `string?` | Тип подписки (MONTHLY и т.д.) |\n| `subscriptionDays` | `number?` | Дней подписки |\n\n### SubscriptionWebhookPayload\n\n| Поле | Тип | Описание |\n|------|-----|----------|\n| `subscriptionId` | `string` | ID подписки |\n| `userId` | `string` | ID пользователя |\n| `status` | `string` | Статус: `ACTIVE`, `CANCELLED`, `PAUSED`, `EXPIRED` |\n| `planId` | `string` | ID тарифного плана |\n| `scheduledPlanId` | `string \\| null` | ID плана, на который запланирован переход |\n| `scheduledPlanChangeAt` | `string \\| null` | Дата автопереключения плана (ISO 8601) |\n| `previousPlanId` | `string \\| null` | Предыдущий план при `eventType = PLAN_CHANGED` |\n| `startDate` | `string` | Дата начала (ISO 8601) |\n| `endDate` | `string \\| null` | Дата окончания |\n| `nextBillingDate` | `string \\| null` | Следующее списание |\n| `isActive` | `boolean` | Активна ли подписка |\n| `autoRenew` | `boolean` | Автопродление |\n| `language` | `string` | Язык |\n| `eventType` | `'STATE_CHANGE' \\| 'UPCOMING_BILLING' \\| 'PLAN_CHANGED'` | Тип события webhook |\n| `robokassaCode` | `string` | Код Robokassa-конфигурации |\n\n`PLAN_CHANGED` приходит при фактическом применении смены тарифа:\n- сразу после успешной доплаты за `applyAt: 'immediately'`;\n- после успешного списания нового периода для `applyAt: 'end_of_period'`.\n\n## Лицензия\n\nПроприетарный. Только для внутреннего использования.\n","readmeFilename":"README.md"}