zai_payment 1.0.2 → 1.2.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 +4 -4
- data/CHANGELOG.md +81 -1
- data/IMPLEMENTATION.md +201 -0
- data/README.md +81 -13
- data/docs/ARCHITECTURE.md +232 -0
- data/docs/AUTHENTICATION.md +647 -0
- data/docs/README.md +81 -0
- data/docs/WEBHOOKS.md +417 -0
- data/docs/WEBHOOK_SECURITY_QUICKSTART.md +141 -0
- data/docs/WEBHOOK_SIGNATURE.md +244 -0
- data/examples/webhooks.md +635 -0
- data/lib/zai_payment/client.rb +116 -0
- data/lib/zai_payment/config.rb +2 -0
- data/lib/zai_payment/errors.rb +19 -0
- data/lib/zai_payment/resources/webhook.rb +331 -0
- data/lib/zai_payment/response.rb +77 -0
- data/lib/zai_payment/version.rb +1 -1
- data/lib/zai_payment.rb +10 -0
- metadata +40 -1
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
# Authentication Guide
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The Zai Payment gem implements **OAuth2 Client Credentials flow** for secure authentication with the Zai API. The gem intelligently manages your authentication tokens behind the scenes with automatic caching and refresh.
|
|
6
|
+
|
|
7
|
+
### Key Features
|
|
8
|
+
|
|
9
|
+
✅ **Automatic token management** - Tokens are cached and reused
|
|
10
|
+
✅ **Smart refresh** - Tokens refresh automatically before expiration
|
|
11
|
+
✅ **Thread-safe** - Safe for concurrent requests
|
|
12
|
+
✅ **Zero maintenance** - Set it once, forget about it
|
|
13
|
+
✅ **60-minute token lifetime** - Handled automatically by the gem
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Configuration
|
|
18
|
+
|
|
19
|
+
Before authentication, configure your Zai credentials:
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
# config/initializers/zai_payment.rb
|
|
23
|
+
ZaiPayment.configure do |config|
|
|
24
|
+
config.environment = :prelive # or :production
|
|
25
|
+
config.client_id = ENV.fetch('ZAI_CLIENT_ID')
|
|
26
|
+
config.client_secret = ENV.fetch('ZAI_CLIENT_SECRET')
|
|
27
|
+
config.scope = ENV.fetch('ZAI_OAUTH_SCOPE')
|
|
28
|
+
|
|
29
|
+
# Optional: Configure timeouts
|
|
30
|
+
config.timeout = 30 # Request timeout in seconds (default: 60)
|
|
31
|
+
config.open_timeout = 10 # Connection timeout in seconds (default: 60)
|
|
32
|
+
end
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Environment Variables
|
|
36
|
+
|
|
37
|
+
Store your credentials securely in environment variables:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# .env
|
|
41
|
+
ZAI_CLIENT_ID=your_client_id
|
|
42
|
+
ZAI_CLIENT_SECRET=your_client_secret
|
|
43
|
+
ZAI_OAUTH_SCOPE=your_scope
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
⚠️ **Never commit credentials to version control!**
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Getting Tokens: Two Approaches
|
|
51
|
+
|
|
52
|
+
### Approach 1: Short Way (Recommended) ⭐
|
|
53
|
+
|
|
54
|
+
The simplest way to get an authenticated token - perfect for most use cases:
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
# Get a token with automatic management
|
|
58
|
+
token = ZaiPayment.token
|
|
59
|
+
|
|
60
|
+
# Returns: "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**When to use:**
|
|
64
|
+
- ✅ Most common use case
|
|
65
|
+
- ✅ When you just need a token quickly
|
|
66
|
+
- ✅ When using the gem's built-in resources (webhooks, etc.)
|
|
67
|
+
- ✅ For simple integrations
|
|
68
|
+
|
|
69
|
+
**Benefits:**
|
|
70
|
+
- One-liner simplicity
|
|
71
|
+
- Uses global configuration
|
|
72
|
+
- Automatic token management
|
|
73
|
+
- Thread-safe
|
|
74
|
+
|
|
75
|
+
### Approach 2: Long Way (Advanced)
|
|
76
|
+
|
|
77
|
+
For advanced use cases where you need more control:
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
# Create your own configuration
|
|
81
|
+
config = ZaiPayment::Config.new
|
|
82
|
+
config.environment = :prelive
|
|
83
|
+
config.client_id = 'your_client_id'
|
|
84
|
+
config.client_secret = 'your_client_secret'
|
|
85
|
+
config.scope = 'your_scope'
|
|
86
|
+
|
|
87
|
+
# Create a token provider instance
|
|
88
|
+
token_provider = ZaiPayment::Auth::TokenProvider.new(config: config)
|
|
89
|
+
|
|
90
|
+
# Get the bearer token
|
|
91
|
+
token = token_provider.bearer_token
|
|
92
|
+
|
|
93
|
+
# Returns: "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**When to use:**
|
|
97
|
+
- ✅ Multiple Zai accounts/configurations
|
|
98
|
+
- ✅ Custom token stores (e.g., Redis)
|
|
99
|
+
- ✅ Testing with different configurations
|
|
100
|
+
- ✅ Advanced authentication scenarios
|
|
101
|
+
|
|
102
|
+
**Benefits:**
|
|
103
|
+
- Full control over configuration
|
|
104
|
+
- Can create multiple instances
|
|
105
|
+
- Custom token storage
|
|
106
|
+
- Useful for testing
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## How Token Management Works
|
|
111
|
+
|
|
112
|
+
### Automatic Caching
|
|
113
|
+
|
|
114
|
+
The gem automatically caches tokens to avoid unnecessary API calls:
|
|
115
|
+
|
|
116
|
+
```ruby
|
|
117
|
+
# First call - fetches from Zai API
|
|
118
|
+
token1 = ZaiPayment.token
|
|
119
|
+
# => Makes API call to get token
|
|
120
|
+
|
|
121
|
+
# Subsequent calls - uses cached token
|
|
122
|
+
token2 = ZaiPayment.token
|
|
123
|
+
# => Returns cached token (no API call)
|
|
124
|
+
|
|
125
|
+
token1 == token2 # => true
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Automatic Refresh
|
|
129
|
+
|
|
130
|
+
Tokens expire after 60 minutes. The gem monitors expiration and refreshes automatically:
|
|
131
|
+
|
|
132
|
+
```ruby
|
|
133
|
+
# Token expires in 60 minutes
|
|
134
|
+
token = ZaiPayment.token
|
|
135
|
+
|
|
136
|
+
# ... 59 minutes later ...
|
|
137
|
+
same_token = ZaiPayment.token # Still cached
|
|
138
|
+
|
|
139
|
+
# ... 61 minutes later ...
|
|
140
|
+
new_token = ZaiPayment.token # Automatically refreshed!
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Token Lifecycle
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
┌─────────────────────────────────────────────────────────┐
|
|
147
|
+
│ 1. Request Token │
|
|
148
|
+
│ ZaiPayment.token │
|
|
149
|
+
└────────────────────┬────────────────────────────────────┘
|
|
150
|
+
│
|
|
151
|
+
▼
|
|
152
|
+
┌─────────────────────────────────────────────────────────┐
|
|
153
|
+
│ 2. Check Cache │
|
|
154
|
+
│ • Token exists? → Check if expired │
|
|
155
|
+
│ • Token expired? → Fetch new token │
|
|
156
|
+
│ • No token? → Fetch new token │
|
|
157
|
+
└────────────────────┬────────────────────────────────────┘
|
|
158
|
+
│
|
|
159
|
+
▼
|
|
160
|
+
┌─────────────────────────────────────────────────────────┐
|
|
161
|
+
│ 3. Fetch Token (if needed) │
|
|
162
|
+
│ POST https://auth.api.hellozai.com/oauth/token │
|
|
163
|
+
│ • Grant type: client_credentials │
|
|
164
|
+
│ • Credentials: client_id + client_secret │
|
|
165
|
+
└────────────────────┬────────────────────────────────────┘
|
|
166
|
+
│
|
|
167
|
+
▼
|
|
168
|
+
┌─────────────────────────────────────────────────────────┐
|
|
169
|
+
│ 4. Cache Token │
|
|
170
|
+
│ • Store token in memory │
|
|
171
|
+
│ • Store expiration time (expires_in - buffer) │
|
|
172
|
+
│ • Thread-safe storage with Mutex │
|
|
173
|
+
└────────────────────┬────────────────────────────────────┘
|
|
174
|
+
│
|
|
175
|
+
▼
|
|
176
|
+
┌─────────────────────────────────────────────────────────┐
|
|
177
|
+
│ 5. Return Token │
|
|
178
|
+
│ "Bearer eyJhbGc..." │
|
|
179
|
+
└─────────────────────────────────────────────────────────┘
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Usage Examples
|
|
185
|
+
|
|
186
|
+
### Basic Usage
|
|
187
|
+
|
|
188
|
+
```ruby
|
|
189
|
+
# Simple token retrieval
|
|
190
|
+
token = ZaiPayment.token
|
|
191
|
+
puts token
|
|
192
|
+
# => "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
|
193
|
+
|
|
194
|
+
# Use with your own HTTP requests
|
|
195
|
+
require 'faraday'
|
|
196
|
+
|
|
197
|
+
connection = Faraday.new(url: 'https://api.hellozai.com') do |f|
|
|
198
|
+
f.request :json
|
|
199
|
+
f.response :json
|
|
200
|
+
f.adapter Faraday.default_adapter
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
response = connection.get('/some-endpoint') do |req|
|
|
204
|
+
req.headers['Authorization'] = ZaiPayment.token
|
|
205
|
+
req.headers['Content-Type'] = 'application/json'
|
|
206
|
+
end
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Using Built-in Resources
|
|
210
|
+
|
|
211
|
+
The gem's resources automatically handle authentication:
|
|
212
|
+
|
|
213
|
+
```ruby
|
|
214
|
+
# No need to manually get tokens!
|
|
215
|
+
# The gem handles it automatically
|
|
216
|
+
|
|
217
|
+
response = ZaiPayment.webhooks.list
|
|
218
|
+
# Internally uses ZaiPayment.token
|
|
219
|
+
|
|
220
|
+
response = ZaiPayment.webhooks.create(
|
|
221
|
+
url: 'https://example.com/webhook',
|
|
222
|
+
object_type: 'transactions'
|
|
223
|
+
)
|
|
224
|
+
# Authentication handled automatically
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Multiple Configurations
|
|
228
|
+
|
|
229
|
+
For managing multiple Zai accounts:
|
|
230
|
+
|
|
231
|
+
```ruby
|
|
232
|
+
# Account 1 (Production)
|
|
233
|
+
prod_config = ZaiPayment::Config.new
|
|
234
|
+
prod_config.environment = :production
|
|
235
|
+
prod_config.client_id = ENV['ZAI_PROD_CLIENT_ID']
|
|
236
|
+
prod_config.client_secret = ENV['ZAI_PROD_CLIENT_SECRET']
|
|
237
|
+
prod_config.scope = ENV['ZAI_PROD_SCOPE']
|
|
238
|
+
|
|
239
|
+
prod_token_provider = ZaiPayment::Auth::TokenProvider.new(config: prod_config)
|
|
240
|
+
prod_client = ZaiPayment::Client.new(
|
|
241
|
+
config: prod_config,
|
|
242
|
+
token_provider: prod_token_provider
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
# Account 2 (Prelive/Testing)
|
|
246
|
+
prelive_config = ZaiPayment::Config.new
|
|
247
|
+
prelive_config.environment = :prelive
|
|
248
|
+
prelive_config.client_id = ENV['ZAI_PRELIVE_CLIENT_ID']
|
|
249
|
+
prelive_config.client_secret = ENV['ZAI_PRELIVE_CLIENT_SECRET']
|
|
250
|
+
prelive_config.scope = ENV['ZAI_PRELIVE_SCOPE']
|
|
251
|
+
|
|
252
|
+
prelive_token_provider = ZaiPayment::Auth::TokenProvider.new(config: prelive_config)
|
|
253
|
+
prelive_client = ZaiPayment::Client.new(
|
|
254
|
+
config: prelive_config,
|
|
255
|
+
token_provider: prelive_token_provider
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
# Use different clients for different accounts
|
|
259
|
+
prod_webhooks = ZaiPayment::Resources::Webhook.new(client: prod_client)
|
|
260
|
+
prelive_webhooks = ZaiPayment::Resources::Webhook.new(client: prelive_client)
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### Rails Controller Example
|
|
264
|
+
|
|
265
|
+
```ruby
|
|
266
|
+
class ZaiController < ApplicationController
|
|
267
|
+
before_action :ensure_authenticated
|
|
268
|
+
|
|
269
|
+
def index
|
|
270
|
+
# Token is already validated
|
|
271
|
+
response = ZaiPayment.webhooks.list
|
|
272
|
+
render json: response.data
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
private
|
|
276
|
+
|
|
277
|
+
def ensure_authenticated
|
|
278
|
+
begin
|
|
279
|
+
# This will raise an error if authentication fails
|
|
280
|
+
ZaiPayment.token
|
|
281
|
+
rescue ZaiPayment::Errors::UnauthorizedError => e
|
|
282
|
+
render json: { error: 'Authentication failed' }, status: :unauthorized
|
|
283
|
+
rescue ZaiPayment::Errors::ApiError => e
|
|
284
|
+
render json: { error: 'API error' }, status: :service_unavailable
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## Testing
|
|
293
|
+
|
|
294
|
+
### RSpec Setup
|
|
295
|
+
|
|
296
|
+
```ruby
|
|
297
|
+
# spec/spec_helper.rb
|
|
298
|
+
RSpec.configure do |config|
|
|
299
|
+
config.before(:suite) do
|
|
300
|
+
# Configure for testing
|
|
301
|
+
ZaiPayment.configure do |c|
|
|
302
|
+
c.environment = :prelive
|
|
303
|
+
c.client_id = 'test_client_id'
|
|
304
|
+
c.client_secret = 'test_client_secret'
|
|
305
|
+
c.scope = 'test_scope'
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### Mocking Authentication
|
|
312
|
+
|
|
313
|
+
```ruby
|
|
314
|
+
# spec/support/zai_payment_helpers.rb
|
|
315
|
+
module ZaiPaymentHelpers
|
|
316
|
+
def mock_zai_authentication
|
|
317
|
+
allow(ZaiPayment).to receive(:token).and_return('Bearer mock_token')
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def mock_token_provider
|
|
321
|
+
token_provider = instance_double(
|
|
322
|
+
ZaiPayment::Auth::TokenProvider,
|
|
323
|
+
bearer_token: 'Bearer test_token'
|
|
324
|
+
)
|
|
325
|
+
allow(ZaiPayment::Auth::TokenProvider).to receive(:new).and_return(token_provider)
|
|
326
|
+
token_provider
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
RSpec.configure do |config|
|
|
331
|
+
config.include ZaiPaymentHelpers
|
|
332
|
+
end
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
### Test Example
|
|
336
|
+
|
|
337
|
+
```ruby
|
|
338
|
+
require 'rails_helper'
|
|
339
|
+
|
|
340
|
+
RSpec.describe 'Zai Authentication' do
|
|
341
|
+
describe 'token retrieval' do
|
|
342
|
+
it 'returns a valid bearer token' do
|
|
343
|
+
mock_zai_authentication
|
|
344
|
+
|
|
345
|
+
token = ZaiPayment.token
|
|
346
|
+
expect(token).to start_with('Bearer ')
|
|
347
|
+
expect(token.length).to be > 20
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
describe 'with custom configuration' do
|
|
352
|
+
it 'uses custom credentials' do
|
|
353
|
+
config = ZaiPayment::Config.new
|
|
354
|
+
config.environment = :prelive
|
|
355
|
+
config.client_id = 'custom_id'
|
|
356
|
+
config.client_secret = 'custom_secret'
|
|
357
|
+
config.scope = 'custom_scope'
|
|
358
|
+
|
|
359
|
+
token_provider = ZaiPayment::Auth::TokenProvider.new(config: config)
|
|
360
|
+
|
|
361
|
+
# Mock the HTTP request
|
|
362
|
+
allow(token_provider).to receive(:bearer_token).and_return('Bearer custom_token')
|
|
363
|
+
|
|
364
|
+
expect(token_provider.bearer_token).to eq('Bearer custom_token')
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
---
|
|
371
|
+
|
|
372
|
+
## Advanced Topics
|
|
373
|
+
|
|
374
|
+
### Thread Safety
|
|
375
|
+
|
|
376
|
+
The gem uses a Mutex to ensure thread-safe token storage:
|
|
377
|
+
|
|
378
|
+
```ruby
|
|
379
|
+
# Safe for concurrent requests
|
|
380
|
+
threads = 10.times.map do
|
|
381
|
+
Thread.new do
|
|
382
|
+
token = ZaiPayment.token
|
|
383
|
+
# All threads share the same cached token
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
threads.each(&:join)
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
### Token Store
|
|
391
|
+
|
|
392
|
+
The default token store is in-memory. For production systems with multiple servers, consider implementing a shared store:
|
|
393
|
+
|
|
394
|
+
```ruby
|
|
395
|
+
# Future: Custom token store (Redis example)
|
|
396
|
+
class RedisTokenStore
|
|
397
|
+
def initialize(redis_client)
|
|
398
|
+
@redis = redis_client
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
def get(key)
|
|
402
|
+
@redis.get(key)
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
def set(key, value, expires_in:)
|
|
406
|
+
@redis.setex(key, expires_in, value)
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
# This is a planned feature
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
### Timeout Configuration
|
|
414
|
+
|
|
415
|
+
Configure timeouts for authentication requests:
|
|
416
|
+
|
|
417
|
+
```ruby
|
|
418
|
+
ZaiPayment.configure do |config|
|
|
419
|
+
config.environment = :production
|
|
420
|
+
config.client_id = ENV['ZAI_CLIENT_ID']
|
|
421
|
+
config.client_secret = ENV['ZAI_CLIENT_SECRET']
|
|
422
|
+
config.scope = ENV['ZAI_OAUTH_SCOPE']
|
|
423
|
+
|
|
424
|
+
# Set timeouts (in seconds)
|
|
425
|
+
config.timeout = 30 # Total request timeout
|
|
426
|
+
config.open_timeout = 10 # Connection establishment timeout
|
|
427
|
+
end
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
---
|
|
431
|
+
|
|
432
|
+
## Error Handling
|
|
433
|
+
|
|
434
|
+
### Common Errors
|
|
435
|
+
|
|
436
|
+
```ruby
|
|
437
|
+
begin
|
|
438
|
+
token = ZaiPayment.token
|
|
439
|
+
rescue ZaiPayment::Errors::UnauthorizedError => e
|
|
440
|
+
# Invalid credentials (401)
|
|
441
|
+
puts "Authentication failed: #{e.message}"
|
|
442
|
+
# Check your client_id and client_secret
|
|
443
|
+
|
|
444
|
+
rescue ZaiPayment::Errors::TimeoutError => e
|
|
445
|
+
# Request timed out
|
|
446
|
+
puts "Request timeout: #{e.message}"
|
|
447
|
+
# Consider increasing timeout values
|
|
448
|
+
|
|
449
|
+
rescue ZaiPayment::Errors::ConnectionError => e
|
|
450
|
+
# Network connection failed
|
|
451
|
+
puts "Connection error: #{e.message}"
|
|
452
|
+
# Check network connectivity
|
|
453
|
+
|
|
454
|
+
rescue ZaiPayment::Errors::ApiError => e
|
|
455
|
+
# Other API errors
|
|
456
|
+
puts "API error: #{e.message}"
|
|
457
|
+
end
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
### Handling Authentication Failures
|
|
461
|
+
|
|
462
|
+
```ruby
|
|
463
|
+
def safely_get_token
|
|
464
|
+
retries = 0
|
|
465
|
+
max_retries = 3
|
|
466
|
+
|
|
467
|
+
begin
|
|
468
|
+
ZaiPayment.token
|
|
469
|
+
rescue ZaiPayment::Errors::TimeoutError => e
|
|
470
|
+
retries += 1
|
|
471
|
+
if retries < max_retries
|
|
472
|
+
sleep(2 ** retries) # Exponential backoff
|
|
473
|
+
retry
|
|
474
|
+
else
|
|
475
|
+
raise
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
end
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
---
|
|
482
|
+
|
|
483
|
+
## Best Practices
|
|
484
|
+
|
|
485
|
+
### ✅ Do's
|
|
486
|
+
|
|
487
|
+
✅ **Store credentials in environment variables**
|
|
488
|
+
```ruby
|
|
489
|
+
config.client_id = ENV.fetch('ZAI_CLIENT_ID')
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
✅ **Use the short way for simple cases**
|
|
493
|
+
```ruby
|
|
494
|
+
token = ZaiPayment.token
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
✅ **Configure once, use everywhere**
|
|
498
|
+
```ruby
|
|
499
|
+
# config/initializers/zai_payment.rb
|
|
500
|
+
ZaiPayment.configure { |c| ... }
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
✅ **Let the gem handle token refresh**
|
|
504
|
+
```ruby
|
|
505
|
+
# Don't manually refresh - it's automatic!
|
|
506
|
+
token = ZaiPayment.token
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
✅ **Use built-in resources**
|
|
510
|
+
```ruby
|
|
511
|
+
ZaiPayment.webhooks.list # Authentication automatic
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
### ❌ Don'ts
|
|
515
|
+
|
|
516
|
+
❌ **Don't hardcode credentials**
|
|
517
|
+
```ruby
|
|
518
|
+
# BAD!
|
|
519
|
+
config.client_id = 'abc123'
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
❌ **Don't manually manage tokens**
|
|
523
|
+
```ruby
|
|
524
|
+
# BAD! The gem does this for you
|
|
525
|
+
if token_expired?
|
|
526
|
+
fetch_new_token
|
|
527
|
+
end
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
❌ **Don't create new providers unnecessarily**
|
|
531
|
+
```ruby
|
|
532
|
+
# BAD! Use the global instance
|
|
533
|
+
100.times { ZaiPayment::Auth::TokenProvider.new }
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
❌ **Don't commit credentials to git**
|
|
537
|
+
```bash
|
|
538
|
+
# BAD!
|
|
539
|
+
git add .env
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
---
|
|
543
|
+
|
|
544
|
+
## Troubleshooting
|
|
545
|
+
|
|
546
|
+
### "Invalid client credentials"
|
|
547
|
+
|
|
548
|
+
**Problem:** Authentication returns 401 Unauthorized
|
|
549
|
+
|
|
550
|
+
**Solutions:**
|
|
551
|
+
1. Verify `client_id` and `client_secret` are correct
|
|
552
|
+
2. Check that credentials match the environment (prelive vs production)
|
|
553
|
+
3. Ensure scope is valid for your account
|
|
554
|
+
4. Confirm credentials are active in Zai dashboard
|
|
555
|
+
|
|
556
|
+
### "Token expired" errors
|
|
557
|
+
|
|
558
|
+
**Problem:** Getting errors about expired tokens
|
|
559
|
+
|
|
560
|
+
**Solution:** This shouldn't happen! The gem auto-refreshes. If you see this:
|
|
561
|
+
1. Check if you're caching tokens manually (don't do this)
|
|
562
|
+
2. Ensure you're using `ZaiPayment.token` correctly
|
|
563
|
+
3. Report as a bug if the issue persists
|
|
564
|
+
|
|
565
|
+
### Connection timeouts
|
|
566
|
+
|
|
567
|
+
**Problem:** Requests timing out during authentication
|
|
568
|
+
|
|
569
|
+
**Solutions:**
|
|
570
|
+
1. Increase timeout values in config
|
|
571
|
+
2. Check network connectivity
|
|
572
|
+
3. Verify firewall isn't blocking requests
|
|
573
|
+
4. Test network latency to Zai API
|
|
574
|
+
|
|
575
|
+
### Multiple configurations not working
|
|
576
|
+
|
|
577
|
+
**Problem:** Different configs getting mixed up
|
|
578
|
+
|
|
579
|
+
**Solution:** Ensure you're creating separate instances:
|
|
580
|
+
```ruby
|
|
581
|
+
# Good - separate instances
|
|
582
|
+
provider1 = ZaiPayment::Auth::TokenProvider.new(config: config1)
|
|
583
|
+
provider2 = ZaiPayment::Auth::TokenProvider.new(config: config2)
|
|
584
|
+
|
|
585
|
+
# Bad - sharing global config
|
|
586
|
+
ZaiPayment.configure { |c| config1 }
|
|
587
|
+
token1 = ZaiPayment.token
|
|
588
|
+
ZaiPayment.configure { |c| config2 }
|
|
589
|
+
token2 = ZaiPayment.token # Will overwrite first config!
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
---
|
|
593
|
+
|
|
594
|
+
## API Reference
|
|
595
|
+
|
|
596
|
+
### Configuration
|
|
597
|
+
|
|
598
|
+
```ruby
|
|
599
|
+
ZaiPayment.configure do |config|
|
|
600
|
+
config.environment # :prelive or :production (required)
|
|
601
|
+
config.client_id # String (required)
|
|
602
|
+
config.client_secret # String (required)
|
|
603
|
+
config.scope # String (required)
|
|
604
|
+
config.timeout # Integer, seconds (optional, default: 60)
|
|
605
|
+
config.open_timeout # Integer, seconds (optional, default: 60)
|
|
606
|
+
end
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
### Methods
|
|
610
|
+
|
|
611
|
+
#### `ZaiPayment.token`
|
|
612
|
+
Returns a bearer token string.
|
|
613
|
+
|
|
614
|
+
**Returns:** `String` - Bearer token (e.g., "Bearer eyJhbG...")
|
|
615
|
+
**Raises:** `UnauthorizedError`, `TimeoutError`, `ConnectionError`, `ApiError`
|
|
616
|
+
|
|
617
|
+
#### `ZaiPayment::Auth::TokenProvider.new(config:)`
|
|
618
|
+
Creates a new token provider instance.
|
|
619
|
+
|
|
620
|
+
**Parameters:**
|
|
621
|
+
- `config` - `ZaiPayment::Config` instance
|
|
622
|
+
|
|
623
|
+
**Returns:** `TokenProvider` instance
|
|
624
|
+
|
|
625
|
+
#### `TokenProvider#bearer_token`
|
|
626
|
+
Gets or refreshes the bearer token.
|
|
627
|
+
|
|
628
|
+
**Returns:** `String` - Bearer token
|
|
629
|
+
**Raises:** `UnauthorizedError`, `TimeoutError`, `ConnectionError`, `ApiError`
|
|
630
|
+
|
|
631
|
+
---
|
|
632
|
+
|
|
633
|
+
## External Resources
|
|
634
|
+
|
|
635
|
+
- [Zai OAuth2 Documentation](https://developer.hellozai.com/reference/overview#authentication)
|
|
636
|
+
- [OAuth2 Client Credentials Flow](https://oauth.net/2/grant-types/client-credentials/)
|
|
637
|
+
- [Zai API Reference](https://developer.hellozai.com/reference)
|
|
638
|
+
|
|
639
|
+
---
|
|
640
|
+
|
|
641
|
+
## Next Steps
|
|
642
|
+
|
|
643
|
+
- ✅ Authentication configured and working
|
|
644
|
+
- 📖 Read [Webhook Guide](WEBHOOKS.md) to start using webhooks
|
|
645
|
+
- 💡 Check [Examples](../examples/webhooks.md) for complete code samples
|
|
646
|
+
- 🔒 Set up [Webhook Security](WEBHOOK_SECURITY_QUICKSTART.md)
|
|
647
|
+
|