zai_payment 1.1.0 → 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.
@@ -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
+