yookassarb 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +359 -0
- data/lib/yookassa/client.rb +73 -0
- data/lib/yookassa/configuration.rb +77 -0
- data/lib/yookassa/entities/base.rb +83 -0
- data/lib/yookassa/entities/collection.rb +57 -0
- data/lib/yookassa/entities/deal.rb +20 -0
- data/lib/yookassa/entities/payment.rb +42 -0
- data/lib/yookassa/entities/payout.rb +20 -0
- data/lib/yookassa/entities/receipt.rb +15 -0
- data/lib/yookassa/entities/refund.rb +20 -0
- data/lib/yookassa/entities/webhook_obj.rb +11 -0
- data/lib/yookassa/errors.rb +81 -0
- data/lib/yookassa/middleware/error_handler.rb +61 -0
- data/lib/yookassa/middleware/idempotency.rb +32 -0
- data/lib/yookassa/middleware/retry.rb +56 -0
- data/lib/yookassa/resources/base.rb +155 -0
- data/lib/yookassa/resources/deal.rb +15 -0
- data/lib/yookassa/resources/invoice.rb +31 -0
- data/lib/yookassa/resources/payment.rb +64 -0
- data/lib/yookassa/resources/payout.rb +15 -0
- data/lib/yookassa/resources/receipt.rb +15 -0
- data/lib/yookassa/resources/refund.rb +15 -0
- data/lib/yookassa/resources/settings.rb +19 -0
- data/lib/yookassa/resources/webhook.rb +45 -0
- data/lib/yookassa/version.rb +7 -0
- data/lib/yookassa/webhook/event_types.rb +36 -0
- data/lib/yookassa/webhook/ip_checker.rb +44 -0
- data/lib/yookassa/webhook/notification.rb +63 -0
- data/lib/yookassarb.rb +254 -0
- metadata +93 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 1af798c77da6146b2481f9d7cff67c0fc98b1f00a07c4b4f2631453bbf752ffe
|
|
4
|
+
data.tar.gz: a7356f8dc360faa0bc250ac2b4ac3b4f83263b072aca73b64e34a204a09a86cd
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: ef838595dd52364e9af5459164d8ba31f3a37f0740bc21cad3817ff577816d73b553c0abfd54c6c727d20a02e18d8bbce93d59beba9577c92877ca1ce615732e
|
|
7
|
+
data.tar.gz: 8e5368ffe8f4cafeccde9f7eeddd8eac0377aa625ca030ab39bcb23a5f797c1ee19ce4820070d481766a8e84a1b995df7ae18dbb34dcac65ac8733fd60cceabc
|
data/README.md
ADDED
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
# YooKassa Ruby SDK
|
|
2
|
+
|
|
3
|
+
[](https://rubygems.org/gems/yookassarb)
|
|
4
|
+
[](https://www.ruby-lang.org)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
Удобная Ruby-библиотека для работы с [YooKassa API v3](https://yookassa.ru/developers/api). Платежи, возвраты, чеки, выплаты, сделки, вебхуки — всё через простой и идиоматичный Ruby-интерфейс.
|
|
8
|
+
|
|
9
|
+
## Установка
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
# Gemfile
|
|
13
|
+
gem "yookassarb"
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
bundle install
|
|
18
|
+
|
|
19
|
+
# или напрямую
|
|
20
|
+
gem install yookassarb
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Быстрый старт
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
require "yookassarb"
|
|
27
|
+
|
|
28
|
+
Yookassa.configure do |config|
|
|
29
|
+
config.shop_id = "your_shop_id"
|
|
30
|
+
config.api_key = "your_secret_key"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Создать платёж
|
|
34
|
+
payment = Yookassa::Payment.create(
|
|
35
|
+
amount: { value: "100.00", currency: "RUB" },
|
|
36
|
+
confirmation: { type: "redirect", return_url: "https://example.com/thanks" },
|
|
37
|
+
description: "Заказ #72"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# Перенаправить пользователя на оплату
|
|
41
|
+
redirect_to payment.confirmation_url
|
|
42
|
+
|
|
43
|
+
# Проверить статус
|
|
44
|
+
payment = Yookassa::Payment.find(payment.id)
|
|
45
|
+
payment.succeeded? # => true/false
|
|
46
|
+
payment.waiting_for_capture? # => true/false
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Использование
|
|
50
|
+
|
|
51
|
+
### Платежи
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
# Создание
|
|
55
|
+
payment = Yookassa::Payment.create(
|
|
56
|
+
amount: { value: "500.00", currency: "RUB" },
|
|
57
|
+
payment_method_data: { type: "bank_card" },
|
|
58
|
+
confirmation: { type: "redirect", return_url: "https://example.com/return" },
|
|
59
|
+
description: "Подписка на месяц",
|
|
60
|
+
metadata: { order_id: "42" }
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
payment.id # => "2a7834f6-0001-5000-a000-1da326e5e123"
|
|
64
|
+
payment.status # => "pending"
|
|
65
|
+
payment.confirmation_url # => "https://yoomoney.ru/checkout/..."
|
|
66
|
+
payment.amount.value # => "500.00"
|
|
67
|
+
payment.amount.currency # => "RUB"
|
|
68
|
+
|
|
69
|
+
# Подтверждение (capture) двухстадийного платежа
|
|
70
|
+
captured = Yookassa::Payment.capture(payment.id,
|
|
71
|
+
amount: { value: "500.00", currency: "RUB" }
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Отмена
|
|
75
|
+
canceled = Yookassa::Payment.cancel(payment.id)
|
|
76
|
+
|
|
77
|
+
# Получение по ID
|
|
78
|
+
payment = Yookassa::Payment.find("2a7834f6-0001-5000-a000-1da326e5e123")
|
|
79
|
+
|
|
80
|
+
# Список с фильтрами
|
|
81
|
+
payments = Yookassa::Payment.list(
|
|
82
|
+
created_at_gte: "2024-01-01T00:00:00Z",
|
|
83
|
+
status: "succeeded",
|
|
84
|
+
limit: 10
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
payments.each { |p| puts "#{p.id}: #{p.amount.value} #{p.amount.currency}" }
|
|
88
|
+
payments.has_next? # => true/false — есть ли следующая страница
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**Хелперы статусов:**
|
|
92
|
+
|
|
93
|
+
```ruby
|
|
94
|
+
payment.pending? # => true
|
|
95
|
+
payment.waiting_for_capture? # => true
|
|
96
|
+
payment.succeeded? # => true
|
|
97
|
+
payment.canceled? # => true
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Возвраты
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
refund = Yookassa::Refund.create(
|
|
104
|
+
payment_id: "2a7834f6-0001-5000-a000-1da326e5e123",
|
|
105
|
+
amount: { value: "50.00", currency: "RUB" }
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
refund = Yookassa::Refund.find(refund.id)
|
|
109
|
+
refunds = Yookassa::Refund.list(payment_id: "2a7834f6-...")
|
|
110
|
+
|
|
111
|
+
refund.succeeded? # => true
|
|
112
|
+
refund.canceled? # => true
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Чеки (54-ФЗ)
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
receipt = Yookassa::Receipt.create(
|
|
119
|
+
customer: { email: "user@example.com" },
|
|
120
|
+
items: [
|
|
121
|
+
{
|
|
122
|
+
description: "Подписка",
|
|
123
|
+
amount: { value: "500.00", currency: "RUB" },
|
|
124
|
+
vat_code: 1,
|
|
125
|
+
quantity: "1"
|
|
126
|
+
}
|
|
127
|
+
],
|
|
128
|
+
payment_id: "2a7834f6-...",
|
|
129
|
+
type: "payment"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
receipt = Yookassa::Receipt.find(receipt.id)
|
|
133
|
+
receipts = Yookassa::Receipt.list(payment_id: "2a7834f6-...")
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Выплаты
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
payout = Yookassa::Payout.create(
|
|
140
|
+
amount: { value: "1000.00", currency: "RUB" },
|
|
141
|
+
payout_destination_data: { type: "bank_card", card: { number: "5555555555554444" } },
|
|
142
|
+
description: "Выплата продавцу"
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
payout = Yookassa::Payout.find(payout.id)
|
|
146
|
+
payouts = Yookassa::Payout.list(status: "succeeded")
|
|
147
|
+
|
|
148
|
+
payout.succeeded? # => true
|
|
149
|
+
payout.canceled? # => true
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Сделки
|
|
153
|
+
|
|
154
|
+
```ruby
|
|
155
|
+
deal = Yookassa::Deal.create(
|
|
156
|
+
type: "safe_deal",
|
|
157
|
+
fee_moment: "payment_succeeded",
|
|
158
|
+
description: "Безопасная сделка"
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
deal = Yookassa::Deal.find(deal.id)
|
|
162
|
+
deals = Yookassa::Deal.list(status: "opened")
|
|
163
|
+
|
|
164
|
+
deal.opened? # => true
|
|
165
|
+
deal.closed? # => true
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Вебхуки (управление подписками)
|
|
169
|
+
|
|
170
|
+
```ruby
|
|
171
|
+
client = Yookassa::Client.new(shop_id: "id", api_key: "key")
|
|
172
|
+
|
|
173
|
+
# Подписаться на событие
|
|
174
|
+
webhook = client.webhooks.create(
|
|
175
|
+
event: "payment.succeeded",
|
|
176
|
+
url: "https://example.com/webhooks/yookassa"
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# Список подписок
|
|
180
|
+
client.webhooks.list.each { |wh| puts "#{wh.id}: #{wh.event}" }
|
|
181
|
+
|
|
182
|
+
# Удалить подписку
|
|
183
|
+
client.webhooks.delete(webhook.id)
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Счета
|
|
187
|
+
|
|
188
|
+
```ruby
|
|
189
|
+
client = Yookassa::Client.new(shop_id: "id", api_key: "key")
|
|
190
|
+
|
|
191
|
+
invoice = client.invoices.create(
|
|
192
|
+
payment_data: {
|
|
193
|
+
amount: { value: "1000.00", currency: "RUB" }
|
|
194
|
+
},
|
|
195
|
+
cart: [{ description: "Товар", amount: { value: "1000.00", currency: "RUB" }, quantity: 1 }],
|
|
196
|
+
delivery_method_data: { type: "self" }
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
invoice = client.invoices.find(invoice.id)
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Настройки магазина
|
|
203
|
+
|
|
204
|
+
```ruby
|
|
205
|
+
client = Yookassa::Client.new(shop_id: "id", api_key: "key")
|
|
206
|
+
|
|
207
|
+
settings = client.settings.retrieve
|
|
208
|
+
settings.account_id # => "123456"
|
|
209
|
+
settings.status # => "enabled"
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Мульти-тенантность
|
|
213
|
+
|
|
214
|
+
Для работы с несколькими магазинами используйте инстансы `Client`:
|
|
215
|
+
|
|
216
|
+
```ruby
|
|
217
|
+
shop_a = Yookassa::Client.new(shop_id: "shop_a", api_key: "key_a")
|
|
218
|
+
shop_b = Yookassa::Client.new(shop_id: "shop_b", api_key: "key_b")
|
|
219
|
+
|
|
220
|
+
payment = shop_a.payments.create(amount: { value: "100.00", currency: "RUB" }, ...)
|
|
221
|
+
payout = shop_b.payouts.find("payout_id")
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Также поддерживается OAuth2-авторизация:
|
|
225
|
+
|
|
226
|
+
```ruby
|
|
227
|
+
client = Yookassa::Client.new(auth_token: "your_oauth_token")
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## Обработка входящих вебхуков
|
|
231
|
+
|
|
232
|
+
```ruby
|
|
233
|
+
# В контроллере (Rails, Sinatra, etc.)
|
|
234
|
+
class WebhooksController < ApplicationController
|
|
235
|
+
skip_before_action :verify_authenticity_token
|
|
236
|
+
|
|
237
|
+
def create
|
|
238
|
+
# Проверить IP отправителя
|
|
239
|
+
unless Yookassa::Webhook::IpChecker.trusted?(request.remote_ip)
|
|
240
|
+
head :forbidden
|
|
241
|
+
return
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Распарсить уведомление
|
|
245
|
+
notification = Yookassa::Webhook::Notification.parse(request.raw_post)
|
|
246
|
+
|
|
247
|
+
case notification.event
|
|
248
|
+
when Yookassa::Webhook::EventTypes::PAYMENT_SUCCEEDED
|
|
249
|
+
handle_payment(notification.object)
|
|
250
|
+
when Yookassa::Webhook::EventTypes::PAYMENT_CANCELED
|
|
251
|
+
handle_cancellation(notification.object)
|
|
252
|
+
when Yookassa::Webhook::EventTypes::REFUND_SUCCEEDED
|
|
253
|
+
handle_refund(notification.object)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
head :ok
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
private
|
|
260
|
+
|
|
261
|
+
def handle_payment(payment)
|
|
262
|
+
order = Order.find_by(payment_id: payment.id)
|
|
263
|
+
order.complete! if payment.succeeded?
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
**Типы событий:**
|
|
269
|
+
|
|
270
|
+
| Константа | Значение |
|
|
271
|
+
|-----------|----------|
|
|
272
|
+
| `PAYMENT_WAITING_FOR_CAPTURE` | `payment.waiting_for_capture` |
|
|
273
|
+
| `PAYMENT_SUCCEEDED` | `payment.succeeded` |
|
|
274
|
+
| `PAYMENT_CANCELED` | `payment.canceled` |
|
|
275
|
+
| `REFUND_SUCCEEDED` | `refund.succeeded` |
|
|
276
|
+
| `PAYOUT_SUCCEEDED` | `payout.succeeded` |
|
|
277
|
+
| `PAYOUT_CANCELED` | `payout.canceled` |
|
|
278
|
+
| `DEAL_CLOSED` | `deal.closed` |
|
|
279
|
+
|
|
280
|
+
## Обработка ошибок
|
|
281
|
+
|
|
282
|
+
```ruby
|
|
283
|
+
begin
|
|
284
|
+
Yookassa::Payment.create(amount: { value: "-1", currency: "RUB" })
|
|
285
|
+
rescue Yookassa::BadRequestError => e
|
|
286
|
+
e.http_code # => 400
|
|
287
|
+
e.code # => "invalid_request"
|
|
288
|
+
e.description # => "Невалидное значение параметра amount"
|
|
289
|
+
e.parameter # => "amount"
|
|
290
|
+
rescue Yookassa::UnauthorizedError
|
|
291
|
+
# Неверные креденшлы (401)
|
|
292
|
+
rescue Yookassa::ForbiddenError
|
|
293
|
+
# Нет доступа (403)
|
|
294
|
+
rescue Yookassa::NotFoundError
|
|
295
|
+
# Объект не найден (404)
|
|
296
|
+
rescue Yookassa::TooManyRequestsError
|
|
297
|
+
# Превышен лимит запросов (429)
|
|
298
|
+
rescue Yookassa::InternalServerError
|
|
299
|
+
# Ошибка на стороне YooKassa (500)
|
|
300
|
+
rescue Yookassa::ConnectionError
|
|
301
|
+
# Проблемы с сетью
|
|
302
|
+
rescue Yookassa::TimeoutError
|
|
303
|
+
# Таймаут запроса
|
|
304
|
+
end
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
## Конфигурация
|
|
308
|
+
|
|
309
|
+
```ruby
|
|
310
|
+
Yookassa.configure do |config|
|
|
311
|
+
# Аутентификация (один из двух вариантов)
|
|
312
|
+
config.shop_id = "your_shop_id" # ID магазина
|
|
313
|
+
config.api_key = "your_secret_key" # Секретный ключ
|
|
314
|
+
# или
|
|
315
|
+
config.auth_token = "oauth_token" # OAuth2-токен
|
|
316
|
+
|
|
317
|
+
# Настройки HTTP
|
|
318
|
+
config.timeout = 30 # таймаут запроса в секундах (по умолчанию: 30)
|
|
319
|
+
config.max_retries = 3 # макс. количество повторов (по умолчанию: 3)
|
|
320
|
+
config.retry_delay = 1.8 # базовая задержка между повторами в секундах (по умолчанию: 1.8)
|
|
321
|
+
config.logger = Rails.logger # логгер (по умолчанию: nil)
|
|
322
|
+
end
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
## Встроенные механизмы
|
|
326
|
+
|
|
327
|
+
### Идемпотентность
|
|
328
|
+
|
|
329
|
+
POST и DELETE запросы автоматически получают заголовок `Idempotence-Key` (UUID v4). Можно передать свой:
|
|
330
|
+
|
|
331
|
+
```ruby
|
|
332
|
+
Yookassa::Payment.create(params, idempotency_key: "my-unique-key-123")
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
### Автоматические повторы
|
|
336
|
+
|
|
337
|
+
SDK автоматически повторяет запросы при HTTP 202 (Accepted), 500 и сетевых ошибках. Задержка растёт линейно: `retry_delay * attempt`.
|
|
338
|
+
|
|
339
|
+
### Обработка ошибок API
|
|
340
|
+
|
|
341
|
+
HTTP-ошибки автоматически преобразуются в типизированные исключения с полным контекстом (код, описание, параметр, тело ответа).
|
|
342
|
+
|
|
343
|
+
## Совместимость
|
|
344
|
+
|
|
345
|
+
- Ruby >= 3.1
|
|
346
|
+
- Faraday 1.x или 2.x
|
|
347
|
+
|
|
348
|
+
## Разработка
|
|
349
|
+
|
|
350
|
+
```bash
|
|
351
|
+
bundle install
|
|
352
|
+
bundle exec rspec # запуск тестов
|
|
353
|
+
bundle exec rubocop # линтинг
|
|
354
|
+
gem build yookassarb.gemspec # сборка гема
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
## Лицензия
|
|
358
|
+
|
|
359
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yookassa
|
|
4
|
+
# API client that holds configuration and provides access to resource endpoints.
|
|
5
|
+
#
|
|
6
|
+
# You can use the global client via {Yookassa.default_client} or create
|
|
7
|
+
# an isolated instance for multi-shop setups.
|
|
8
|
+
#
|
|
9
|
+
# @example Standalone client
|
|
10
|
+
# client = Yookassa::Client.new(shop_id: "123", api_key: "key_...")
|
|
11
|
+
# payment = client.payments.create(amount: { value: "100.00", currency: "RUB" })
|
|
12
|
+
class Client
|
|
13
|
+
# @return [Configuration] the client's configuration
|
|
14
|
+
attr_reader :config
|
|
15
|
+
|
|
16
|
+
# Creates a new API client.
|
|
17
|
+
#
|
|
18
|
+
# @param shop_id [String, nil] YooKassa shop identifier
|
|
19
|
+
# @param api_key [String, nil] secret API key
|
|
20
|
+
# @param auth_token [String, nil] OAuth token (alternative to shop_id/api_key)
|
|
21
|
+
# @param options [Hash] additional options (:timeout, :max_retries, :retry_delay, :logger)
|
|
22
|
+
def initialize(shop_id: nil, api_key: nil, auth_token: nil, **options)
|
|
23
|
+
@config = Configuration.new
|
|
24
|
+
@config.shop_id = shop_id
|
|
25
|
+
@config.api_key = api_key
|
|
26
|
+
@config.auth_token = auth_token
|
|
27
|
+
@config.timeout = options[:timeout] if options.key?(:timeout)
|
|
28
|
+
@config.max_retries = options[:max_retries] if options.key?(:max_retries)
|
|
29
|
+
@config.retry_delay = options[:retry_delay] if options.key?(:retry_delay)
|
|
30
|
+
@config.logger = options[:logger] if options.key?(:logger)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @return [Resources::Payment] payment resource endpoint
|
|
34
|
+
def payments
|
|
35
|
+
@payments ||= Resources::Payment.new(self)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @return [Resources::Refund] refund resource endpoint
|
|
39
|
+
def refunds
|
|
40
|
+
@refunds ||= Resources::Refund.new(self)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# @return [Resources::Receipt] receipt resource endpoint
|
|
44
|
+
def receipts
|
|
45
|
+
@receipts ||= Resources::Receipt.new(self)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @return [Resources::Payout] payout resource endpoint
|
|
49
|
+
def payouts
|
|
50
|
+
@payouts ||= Resources::Payout.new(self)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @return [Resources::Deal] deal resource endpoint
|
|
54
|
+
def deals
|
|
55
|
+
@deals ||= Resources::Deal.new(self)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# @return [Resources::Webhook] webhook resource endpoint
|
|
59
|
+
def webhooks
|
|
60
|
+
@webhooks ||= Resources::Webhook.new(self)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# @return [Resources::Invoice] invoice resource endpoint
|
|
64
|
+
def invoices
|
|
65
|
+
@invoices ||= Resources::Invoice.new(self)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# @return [Resources::Settings] shop settings resource endpoint
|
|
69
|
+
def settings
|
|
70
|
+
@settings ||= Resources::Settings.new(self)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yookassa
|
|
4
|
+
# Holds API credentials and connection settings for the YooKassa API.
|
|
5
|
+
#
|
|
6
|
+
# @example Using shop_id + api_key
|
|
7
|
+
# Yookassa.configure do |config|
|
|
8
|
+
# config.shop_id = "123456"
|
|
9
|
+
# config.api_key = "live_..."
|
|
10
|
+
# end
|
|
11
|
+
#
|
|
12
|
+
# @example Using OAuth token
|
|
13
|
+
# Yookassa.configure do |config|
|
|
14
|
+
# config.auth_token = "oauth_token_..."
|
|
15
|
+
# end
|
|
16
|
+
class Configuration
|
|
17
|
+
# @return [String, nil] YooKassa shop (merchant) identifier
|
|
18
|
+
attr_accessor :shop_id
|
|
19
|
+
|
|
20
|
+
# @return [String, nil] secret API key for Basic auth
|
|
21
|
+
attr_accessor :api_key
|
|
22
|
+
|
|
23
|
+
# @return [String, nil] OAuth token (used instead of shop_id/api_key when set)
|
|
24
|
+
attr_accessor :auth_token
|
|
25
|
+
|
|
26
|
+
# @return [Integer] HTTP request timeout in seconds (default: 30)
|
|
27
|
+
attr_accessor :timeout
|
|
28
|
+
|
|
29
|
+
# @return [Integer] maximum number of retries on failure (default: 3)
|
|
30
|
+
attr_accessor :max_retries
|
|
31
|
+
|
|
32
|
+
# @return [Float] base delay between retries in seconds (default: 1.8)
|
|
33
|
+
attr_accessor :retry_delay
|
|
34
|
+
|
|
35
|
+
# @return [Logger, nil] optional logger for request/response debugging
|
|
36
|
+
attr_accessor :logger
|
|
37
|
+
|
|
38
|
+
def initialize
|
|
39
|
+
@timeout = 30
|
|
40
|
+
@max_retries = 3
|
|
41
|
+
@retry_delay = 1.8
|
|
42
|
+
@logger = nil
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Checks whether credentials are present (non-raising).
|
|
46
|
+
#
|
|
47
|
+
# @return [Boolean] +true+ if auth_token is set, or both shop_id and api_key are set
|
|
48
|
+
def validate
|
|
49
|
+
return true unless auth_token.to_s.empty?
|
|
50
|
+
return false if shop_id.to_s.empty?
|
|
51
|
+
return false if api_key.to_s.empty?
|
|
52
|
+
|
|
53
|
+
true
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Validates that credentials are present, raising on failure.
|
|
57
|
+
#
|
|
58
|
+
# @raise [Yookassa::Error] if required credentials are missing
|
|
59
|
+
# @return [void]
|
|
60
|
+
def validate!
|
|
61
|
+
return unless auth_token.to_s.empty?
|
|
62
|
+
raise Yookassa::Error, "shop_id is required (or provide auth_token)" if shop_id.to_s.empty?
|
|
63
|
+
raise Yookassa::Error, "api_key is required (or provide auth_token)" if api_key.to_s.empty?
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Returns the active credential set for HTTP authentication.
|
|
67
|
+
#
|
|
68
|
+
# @return [Hash] either +{ auth_token: ... }+ or +{ shop_id: ..., api_key: ... }+
|
|
69
|
+
def credentials
|
|
70
|
+
if auth_token && !auth_token.to_s.empty?
|
|
71
|
+
{ auth_token: auth_token }
|
|
72
|
+
else
|
|
73
|
+
{ shop_id: shop_id, api_key: api_key }
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yookassa
|
|
4
|
+
module Entities
|
|
5
|
+
# Base entity class that wraps API response hashes with dynamic attribute access.
|
|
6
|
+
#
|
|
7
|
+
# Attributes returned by the API are accessible as methods via +method_missing+.
|
|
8
|
+
# Nested hashes are automatically wrapped into entity instances, and arrays of
|
|
9
|
+
# hashes become arrays of entities.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# entity = Yookassa::Entities::Base.new(id: "abc", amount: { value: "100", currency: "RUB" })
|
|
13
|
+
# entity.id # => "abc"
|
|
14
|
+
# entity.amount.value # => "100"
|
|
15
|
+
# entity[:id] # => "abc"
|
|
16
|
+
# entity.to_h # => { "id" => "abc", "amount" => { "value" => "100", "currency" => "RUB" } }
|
|
17
|
+
class Base
|
|
18
|
+
# @return [Hash<String, Object>] normalized attribute hash (string keys)
|
|
19
|
+
attr_reader :attributes
|
|
20
|
+
|
|
21
|
+
# @param data [Hash, #to_h] raw API response data
|
|
22
|
+
def initialize(data)
|
|
23
|
+
@attributes = self.class.send(:normalize, data)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Hash-style attribute access.
|
|
27
|
+
#
|
|
28
|
+
# @param key [String, Symbol] attribute name
|
|
29
|
+
# @return [Object, nil]
|
|
30
|
+
def [](key)
|
|
31
|
+
@attributes[key.to_s]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Returns the raw attribute hash.
|
|
35
|
+
#
|
|
36
|
+
# @return [Hash<String, Object>]
|
|
37
|
+
def to_h
|
|
38
|
+
@attributes
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
42
|
+
@attributes.key?(method_name.to_s) || super
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def method_missing(method_name, *args)
|
|
48
|
+
key = method_name.to_s
|
|
49
|
+
if @attributes.key?(key)
|
|
50
|
+
wrap_value(@attributes[key])
|
|
51
|
+
else
|
|
52
|
+
super
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def wrap_value(value)
|
|
57
|
+
klass = self.class
|
|
58
|
+
case value
|
|
59
|
+
when Hash
|
|
60
|
+
klass.new(value)
|
|
61
|
+
when Array
|
|
62
|
+
value.map { |item| item.is_a?(Hash) ? klass.new(item) : item }
|
|
63
|
+
else
|
|
64
|
+
value
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def self.normalize(data)
|
|
69
|
+
case data
|
|
70
|
+
when Hash
|
|
71
|
+
data.transform_keys(&:to_s)
|
|
72
|
+
else
|
|
73
|
+
begin
|
|
74
|
+
data.to_h.transform_keys(&:to_s)
|
|
75
|
+
rescue StandardError
|
|
76
|
+
{}
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
private_class_method :normalize
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yookassa
|
|
4
|
+
module Entities
|
|
5
|
+
# Paginated collection of entities with cursor-based navigation.
|
|
6
|
+
#
|
|
7
|
+
# Includes +Enumerable+ so standard methods (+map+, +select+, +first+, etc.)
|
|
8
|
+
# work out of the box.
|
|
9
|
+
#
|
|
10
|
+
# @example Iterating over payments
|
|
11
|
+
# payments = client.payments.list(limit: 10)
|
|
12
|
+
# payments.each { |p| puts p.id }
|
|
13
|
+
# payments.has_next? # => true
|
|
14
|
+
# payments.next_cursor # => "cursor_abc"
|
|
15
|
+
class Collection
|
|
16
|
+
include Enumerable
|
|
17
|
+
|
|
18
|
+
# @return [Array<Entities::Base>] wrapped entity objects
|
|
19
|
+
attr_reader :items
|
|
20
|
+
|
|
21
|
+
# @return [String, nil] cursor for fetching the next page
|
|
22
|
+
attr_reader :next_cursor
|
|
23
|
+
|
|
24
|
+
# @param items [Array<Hash>] raw item hashes from API response
|
|
25
|
+
# @param next_cursor [String, nil] pagination cursor for the next page
|
|
26
|
+
# @param entity_class [Class] entity class to wrap each item (default: {Entities::Base})
|
|
27
|
+
def initialize(items:, next_cursor: nil, entity_class: Entities::Base)
|
|
28
|
+
@items = items.map { |item| entity_class.new(item) }
|
|
29
|
+
@next_cursor = next_cursor
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Yields each entity in the collection.
|
|
33
|
+
#
|
|
34
|
+
# @yield [entity] each wrapped entity
|
|
35
|
+
# @yieldparam entity [Entities::Base]
|
|
36
|
+
def each(&)
|
|
37
|
+
@items.each(&)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @return [Integer] number of items in this page
|
|
41
|
+
def size
|
|
42
|
+
@items.size
|
|
43
|
+
end
|
|
44
|
+
alias length size
|
|
45
|
+
|
|
46
|
+
# @return [Boolean] +true+ if the collection has no items
|
|
47
|
+
def empty?
|
|
48
|
+
@items.empty?
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# @return [Boolean] +true+ if more pages are available
|
|
52
|
+
def has_next?
|
|
53
|
+
!@next_cursor.to_s.empty?
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yookassa
|
|
4
|
+
module Entities
|
|
5
|
+
# Deal (Safe Deal) entity with status helpers.
|
|
6
|
+
#
|
|
7
|
+
# @see https://yookassa.ru/developers/api#deal_object Deal object reference
|
|
8
|
+
class Deal < Base
|
|
9
|
+
# @return [Boolean] +true+ if the deal is currently open
|
|
10
|
+
def opened?
|
|
11
|
+
status == "opened"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# @return [Boolean] +true+ if the deal has been closed
|
|
15
|
+
def closed?
|
|
16
|
+
status == "closed"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|