zai_payment 1.0.2 → 1.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 +4 -4
- data/CHANGELOG.md +37 -1
- data/IMPLEMENTATION.md +201 -0
- data/README.md +65 -1
- data/docs/ARCHITECTURE.md +232 -0
- data/docs/WEBHOOKS.md +157 -0
- data/examples/webhooks.md +146 -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 +157 -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 +8 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e3dde823881d82243bdd2f57882c57cfe06ba21d20ab5402b58a876c7c752e8c
|
|
4
|
+
data.tar.gz: 37ce743767afba2f834354a81102cc445c544151c26601e59a0f82eb6b967a92
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0b96c6511bc25c2dcbfe44af58957b160c3bd7ac1fb6aa94fb5708ced6a6e2ff6a209f188f664eb6c857ebe4177f5260191eb62aa1e64924adc57d695815ed38
|
|
7
|
+
data.tar.gz: 6e2410984fe931c17097f145de9a23e7c6048af308c068e7f590efab9991a21c838aed6158bb1be56863fa9e86090f46599eaffec1cf415e2610c46861de06e1
|
data/CHANGELOG.md
CHANGED
|
@@ -1,7 +1,43 @@
|
|
|
1
|
-
## [
|
|
1
|
+
## [Released]
|
|
2
|
+
|
|
3
|
+
## [1.1.0] - 2025-10-22
|
|
4
|
+
### Added
|
|
5
|
+
- **Webhooks API**: Full CRUD operations for managing Zai webhooks
|
|
6
|
+
- `ZaiPayment.webhooks.list` - List all webhooks with pagination
|
|
7
|
+
- `ZaiPayment.webhooks.show(id)` - Get a specific webhook
|
|
8
|
+
- `ZaiPayment.webhooks.create(...)` - Create a new webhook
|
|
9
|
+
- `ZaiPayment.webhooks.update(id, ...)` - Update an existing webhook
|
|
10
|
+
- `ZaiPayment.webhooks.delete(id)` - Delete a webhook
|
|
11
|
+
- **Base API Client**: Reusable HTTP client for all API requests
|
|
12
|
+
- **Response Wrapper**: Standardized response handling with error management
|
|
13
|
+
- **Enhanced Error Handling**: New error classes for different API scenarios
|
|
14
|
+
- `ValidationError` (400, 422)
|
|
15
|
+
- `UnauthorizedError` (401)
|
|
16
|
+
- `ForbiddenError` (403)
|
|
17
|
+
- `NotFoundError` (404)
|
|
18
|
+
- `RateLimitError` (429)
|
|
19
|
+
- `ServerError` (5xx)
|
|
20
|
+
- `TimeoutError` and `ConnectionError` for network issues
|
|
21
|
+
- Comprehensive test suite for webhook functionality
|
|
22
|
+
- Example code in `examples/webhooks.rb`
|
|
23
|
+
|
|
24
|
+
**Full Changelog**: https://github.com/Sentia/zai-payment/compare/v1.0.2...v1.1.0
|
|
25
|
+
|
|
26
|
+
## [1.0.2] - 2025-10-22
|
|
27
|
+
- Update gemspec files and readme
|
|
28
|
+
|
|
29
|
+
**Full Changelog**: https://github.com/Sentia/zai-payment/releases/tag/v1.0.2
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
##[1.0.1] - 2025-10-21
|
|
33
|
+
- Update readme and versions
|
|
34
|
+
|
|
35
|
+
**Full Changelog**: https://github.com/Sentia/zai-payment/releases/tag/v1.0.1
|
|
36
|
+
|
|
2
37
|
|
|
3
38
|
## [1.0.0] - 2025-10-21
|
|
4
39
|
|
|
5
40
|
- Initial release: token auth client with in-memory caching (`ZaiPayment.token`, `refresh_token!`, `clear_token!`, `token_type`, `token_expiry`)
|
|
6
41
|
|
|
7
42
|
**Full Changelog**: https://github.com/Sentia/zai-payment/commits/v1.0.0
|
|
43
|
+
|
data/IMPLEMENTATION.md
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# Implementation Summary: Zai Payment Webhooks
|
|
2
|
+
|
|
3
|
+
## ✅ What Was Implemented
|
|
4
|
+
|
|
5
|
+
### 1. Core Infrastructure (New Files)
|
|
6
|
+
|
|
7
|
+
#### `/lib/zai_payment/client.rb`
|
|
8
|
+
- Base HTTP client for all API requests
|
|
9
|
+
- Handles authentication automatically
|
|
10
|
+
- Supports GET, POST, PATCH, DELETE methods
|
|
11
|
+
- Proper error handling and connection management
|
|
12
|
+
- Thread-safe and reusable
|
|
13
|
+
|
|
14
|
+
#### `/lib/zai_payment/response.rb`
|
|
15
|
+
- Response wrapper class
|
|
16
|
+
- Convenience methods: `success?`, `client_error?`, `server_error?`
|
|
17
|
+
- Automatic error raising based on HTTP status
|
|
18
|
+
- Clean data extraction from response body
|
|
19
|
+
|
|
20
|
+
#### `/lib/zai_payment/resources/webhook.rb`
|
|
21
|
+
- Complete CRUD operations for webhooks:
|
|
22
|
+
- `list(limit:, offset:)` - List all webhooks with pagination
|
|
23
|
+
- `show(webhook_id)` - Get specific webhook details
|
|
24
|
+
- `create(url:, object_type:, enabled:, description:)` - Create new webhook
|
|
25
|
+
- `update(webhook_id, ...)` - Update existing webhook
|
|
26
|
+
- `delete(webhook_id)` - Delete webhook
|
|
27
|
+
- Full input validation
|
|
28
|
+
- URL format validation
|
|
29
|
+
- Comprehensive error messages
|
|
30
|
+
|
|
31
|
+
### 2. Enhanced Error Handling
|
|
32
|
+
|
|
33
|
+
#### `/lib/zai_payment/errors.rb` (Updated)
|
|
34
|
+
Added new error classes:
|
|
35
|
+
- `ApiError` - Base API error
|
|
36
|
+
- `BadRequestError` (400)
|
|
37
|
+
- `UnauthorizedError` (401)
|
|
38
|
+
- `ForbiddenError` (403)
|
|
39
|
+
- `NotFoundError` (404)
|
|
40
|
+
- `ValidationError` (422)
|
|
41
|
+
- `RateLimitError` (429)
|
|
42
|
+
- `ServerError` (5xx)
|
|
43
|
+
- `TimeoutError` - Network timeout
|
|
44
|
+
- `ConnectionError` - Connection failed
|
|
45
|
+
|
|
46
|
+
### 3. Main Module Integration
|
|
47
|
+
|
|
48
|
+
#### `/lib/zai_payment.rb` (Updated)
|
|
49
|
+
- Added `require` statements for new components
|
|
50
|
+
- Added `webhooks` method that returns a singleton instance
|
|
51
|
+
- Usage: `ZaiPayment.webhooks.list`
|
|
52
|
+
|
|
53
|
+
### 4. Testing
|
|
54
|
+
|
|
55
|
+
#### `/spec/zai_payment/resources/webhook_spec.rb` (New)
|
|
56
|
+
Comprehensive test suite covering:
|
|
57
|
+
- List webhooks (success, pagination, unauthorized)
|
|
58
|
+
- Show webhook (success, not found, validation)
|
|
59
|
+
- Create webhook (success, validation errors, API errors)
|
|
60
|
+
- Update webhook (success, not found, validation)
|
|
61
|
+
- Delete webhook (success, not found, validation)
|
|
62
|
+
- Edge cases and error scenarios
|
|
63
|
+
|
|
64
|
+
### 5. Documentation
|
|
65
|
+
|
|
66
|
+
#### `/examples/webhooks.rb` (New)
|
|
67
|
+
- Complete usage examples
|
|
68
|
+
- All CRUD operations
|
|
69
|
+
- Error handling patterns
|
|
70
|
+
- Pagination examples
|
|
71
|
+
- Custom client instances
|
|
72
|
+
|
|
73
|
+
#### `/docs/WEBHOOKS.md` (New)
|
|
74
|
+
- Architecture overview
|
|
75
|
+
- API method documentation
|
|
76
|
+
- Error handling guide
|
|
77
|
+
- Best practices
|
|
78
|
+
- Testing instructions
|
|
79
|
+
- Future enhancements
|
|
80
|
+
|
|
81
|
+
#### `/README.md` (Updated)
|
|
82
|
+
- Added webhook usage section
|
|
83
|
+
- Error handling examples
|
|
84
|
+
- Updated roadmap (Webhooks: Done ✅)
|
|
85
|
+
|
|
86
|
+
#### `/CHANGELOG.md` (Updated)
|
|
87
|
+
- Added v1.1.0 release notes
|
|
88
|
+
- Documented all new features
|
|
89
|
+
- Listed all new error classes
|
|
90
|
+
|
|
91
|
+
### 6. Version
|
|
92
|
+
|
|
93
|
+
#### `/lib/zai_payment/version.rb` (Updated)
|
|
94
|
+
- Bumped version to 1.1.0
|
|
95
|
+
|
|
96
|
+
## 📁 File Structure
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
lib/
|
|
100
|
+
├── zai_payment/
|
|
101
|
+
│ ├── auth/ # Authentication (existing)
|
|
102
|
+
│ ├── client.rb # ✨ NEW: Base HTTP client
|
|
103
|
+
│ ├── response.rb # ✨ NEW: Response wrapper
|
|
104
|
+
│ ├── resources/
|
|
105
|
+
│ │ └── webhook.rb # ✨ NEW: Webhook CRUD operations
|
|
106
|
+
│ ├── config.rb # (existing)
|
|
107
|
+
│ ├── errors.rb # ✅ UPDATED: Added API error classes
|
|
108
|
+
│ └── version.rb # ✅ UPDATED: v1.1.0
|
|
109
|
+
└── zai_payment.rb # ✅ UPDATED: Added webhooks accessor
|
|
110
|
+
|
|
111
|
+
spec/
|
|
112
|
+
└── zai_payment/
|
|
113
|
+
└── resources/
|
|
114
|
+
└── webhook_spec.rb # ✨ NEW: Comprehensive tests
|
|
115
|
+
|
|
116
|
+
examples/
|
|
117
|
+
└── webhooks.rb # ✨ NEW: Usage examples
|
|
118
|
+
|
|
119
|
+
docs/
|
|
120
|
+
└── WEBHOOKS.md # ✨ NEW: Complete documentation
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## 🎯 Key Features
|
|
124
|
+
|
|
125
|
+
1. **Clean API**: `ZaiPayment.webhooks.list`, `.show`, `.create`, `.update`, `.delete`
|
|
126
|
+
2. **Automatic Authentication**: Uses existing TokenProvider
|
|
127
|
+
3. **Comprehensive Validation**: URL format, required fields, etc.
|
|
128
|
+
4. **Rich Error Handling**: Specific errors for each scenario
|
|
129
|
+
5. **Pagination Support**: Built-in pagination for list operations
|
|
130
|
+
6. **Thread-Safe**: Reuses existing thread-safe authentication
|
|
131
|
+
7. **Well-Tested**: Full RSpec test coverage
|
|
132
|
+
8. **Documented**: Inline docs, examples, and guides
|
|
133
|
+
|
|
134
|
+
## 🚀 Usage
|
|
135
|
+
|
|
136
|
+
```ruby
|
|
137
|
+
# Configure once
|
|
138
|
+
ZaiPayment.configure do |config|
|
|
139
|
+
config.environment = :prelive
|
|
140
|
+
config.client_id = ENV['ZAI_CLIENT_ID']
|
|
141
|
+
config.client_secret = ENV['ZAI_CLIENT_SECRET']
|
|
142
|
+
config.scope = ENV['ZAI_SCOPE']
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Use webhooks
|
|
146
|
+
response = ZaiPayment.webhooks.list
|
|
147
|
+
webhooks = response.data
|
|
148
|
+
|
|
149
|
+
response = ZaiPayment.webhooks.create(
|
|
150
|
+
url: 'https://example.com/webhook',
|
|
151
|
+
object_type: 'transactions',
|
|
152
|
+
enabled: true
|
|
153
|
+
)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## ✨ Best Practices Applied
|
|
157
|
+
|
|
158
|
+
1. **Single Responsibility Principle**: Each class has one clear purpose
|
|
159
|
+
2. **DRY**: Reusable Client and Response classes
|
|
160
|
+
3. **Open/Closed**: Easy to extend for new resources (Users, Items, etc.)
|
|
161
|
+
4. **Dependency Injection**: Client accepts custom config and token provider
|
|
162
|
+
5. **Fail Fast**: Validation before API calls
|
|
163
|
+
6. **Clear Error Messages**: Descriptive validation errors
|
|
164
|
+
7. **RESTful Design**: Standard HTTP methods and status codes
|
|
165
|
+
8. **Comprehensive Testing**: Unit tests for all scenarios
|
|
166
|
+
9. **Documentation**: Examples, inline docs, and guides
|
|
167
|
+
10. **Version Control**: Semantic versioning with changelog
|
|
168
|
+
|
|
169
|
+
## 🔄 Ready for Extension
|
|
170
|
+
|
|
171
|
+
The infrastructure is now in place to easily add more resources:
|
|
172
|
+
|
|
173
|
+
```ruby
|
|
174
|
+
# Future resources can follow the same pattern:
|
|
175
|
+
lib/zai_payment/resources/
|
|
176
|
+
├── webhook.rb # ✅ Done
|
|
177
|
+
├── user.rb # Coming soon
|
|
178
|
+
├── item.rb # Coming soon
|
|
179
|
+
├── transaction.rb # Coming soon
|
|
180
|
+
└── wallet.rb # Coming soon
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Each resource can reuse:
|
|
184
|
+
- `ZaiPayment::Client` for HTTP requests
|
|
185
|
+
- `ZaiPayment::Response` for response handling
|
|
186
|
+
- Error classes for consistent error handling
|
|
187
|
+
- Same authentication mechanism
|
|
188
|
+
- Same configuration
|
|
189
|
+
- Same testing patterns
|
|
190
|
+
|
|
191
|
+
## 🎉 Summary
|
|
192
|
+
|
|
193
|
+
Successfully implemented a complete, production-ready webhook management system for the Zai Payment gem with:
|
|
194
|
+
- ✅ Full CRUD operations
|
|
195
|
+
- ✅ Comprehensive testing
|
|
196
|
+
- ✅ Rich error handling
|
|
197
|
+
- ✅ Complete documentation
|
|
198
|
+
- ✅ Clean, maintainable code
|
|
199
|
+
- ✅ Following Ruby and Rails best practices
|
|
200
|
+
- ✅ Ready for production use
|
|
201
|
+
|
data/README.md
CHANGED
|
@@ -67,14 +67,78 @@ Or, more easily, you can get a token with the convenience one-liner:
|
|
|
67
67
|
ZaiPayment.token
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
+
## 🚀 Usage
|
|
71
|
+
|
|
72
|
+
### Webhooks
|
|
73
|
+
|
|
74
|
+
The gem provides a comprehensive interface for managing Zai webhooks:
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
# List all webhooks
|
|
78
|
+
response = ZaiPayment.webhooks.list
|
|
79
|
+
webhooks = response.data
|
|
80
|
+
|
|
81
|
+
# List with pagination
|
|
82
|
+
response = ZaiPayment.webhooks.list(limit: 20, offset: 10)
|
|
83
|
+
|
|
84
|
+
# Get a specific webhook
|
|
85
|
+
response = ZaiPayment.webhooks.show('webhook_id')
|
|
86
|
+
webhook = response.data
|
|
87
|
+
|
|
88
|
+
# Create a webhook
|
|
89
|
+
response = ZaiPayment.webhooks.create(
|
|
90
|
+
url: 'https://example.com/webhooks/zai',
|
|
91
|
+
object_type: 'transactions',
|
|
92
|
+
enabled: true,
|
|
93
|
+
description: 'Production webhook for transactions'
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# Update a webhook
|
|
97
|
+
response = ZaiPayment.webhooks.update(
|
|
98
|
+
'webhook_id',
|
|
99
|
+
enabled: false,
|
|
100
|
+
description: 'Temporarily disabled'
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# Delete a webhook
|
|
104
|
+
response = ZaiPayment.webhooks.delete('webhook_id')
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
For more examples, see [examples/webhooks.md](examples/webhooks.md).
|
|
108
|
+
|
|
109
|
+
### Error Handling
|
|
110
|
+
|
|
111
|
+
The gem provides specific error classes for different scenarios:
|
|
112
|
+
|
|
113
|
+
```ruby
|
|
114
|
+
begin
|
|
115
|
+
response = ZaiPayment.webhooks.create(
|
|
116
|
+
url: 'https://example.com/webhook',
|
|
117
|
+
object_type: 'transactions'
|
|
118
|
+
)
|
|
119
|
+
rescue ZaiPayment::Errors::ValidationError => e
|
|
120
|
+
# Handle validation errors (400, 422)
|
|
121
|
+
puts "Validation error: #{e.message}"
|
|
122
|
+
rescue ZaiPayment::Errors::UnauthorizedError => e
|
|
123
|
+
# Handle authentication errors (401)
|
|
124
|
+
puts "Authentication failed: #{e.message}"
|
|
125
|
+
rescue ZaiPayment::Errors::NotFoundError => e
|
|
126
|
+
# Handle not found errors (404)
|
|
127
|
+
puts "Resource not found: #{e.message}"
|
|
128
|
+
rescue ZaiPayment::Errors::ApiError => e
|
|
129
|
+
# Handle other API errors
|
|
130
|
+
puts "API error: #{e.message}"
|
|
131
|
+
end
|
|
132
|
+
```
|
|
133
|
+
|
|
70
134
|
## 🧩 Roadmap
|
|
71
135
|
|
|
72
136
|
| Area | Description | Status |
|
|
73
137
|
| ------------------------------- | --------------------------------- | -------------- |
|
|
74
138
|
| ✅ Authentication | OAuth2 Client Credentials flow | Done |
|
|
139
|
+
| ✅ Webhooks | CRUD for webhook endpoints | Done |
|
|
75
140
|
| 💳 Payments | Single and recurring payments | 🚧 In progress |
|
|
76
141
|
| 🏦 Virtual Accounts (VA / PIPU) | Manage virtual accounts & PayTo | ⏳ Planned |
|
|
77
|
-
| 🧾 Webhooks | CRUD for webhook endpoints | ⏳ Planned |
|
|
78
142
|
| 👤 Users | Manage PayIn / PayOut users | ⏳ Planned |
|
|
79
143
|
| 💼 Wallets | Create and manage wallet accounts | ⏳ Planned |
|
|
80
144
|
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# Zai Payment Webhook Architecture
|
|
2
|
+
|
|
3
|
+
```
|
|
4
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
5
|
+
│ Client Application │
|
|
6
|
+
└────────────────────────┬────────────────────────────────────────┘
|
|
7
|
+
│
|
|
8
|
+
│ ZaiPayment.webhooks.list()
|
|
9
|
+
│ ZaiPayment.webhooks.create(...)
|
|
10
|
+
│ ZaiPayment.webhooks.update(...)
|
|
11
|
+
│ ZaiPayment.webhooks.delete(...)
|
|
12
|
+
│
|
|
13
|
+
▼
|
|
14
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
15
|
+
│ ZaiPayment (Module) │
|
|
16
|
+
│ ┌───────────────────────────────────────────────────────────┐ │
|
|
17
|
+
│ │ config() - Configuration singleton │ │
|
|
18
|
+
│ │ auth() - TokenProvider singleton │ │
|
|
19
|
+
│ │ webhooks() - Webhook resource singleton │ │
|
|
20
|
+
│ └───────────────────────────────────────────────────────────┘ │
|
|
21
|
+
└────────────────────────┬────────────────────────────────────────┘
|
|
22
|
+
│
|
|
23
|
+
┌───────────────┴───────────────┐
|
|
24
|
+
│ │
|
|
25
|
+
▼ ▼
|
|
26
|
+
┌──────────────────┐ ┌──────────────────────┐
|
|
27
|
+
│ Config │ │ Auth::TokenProvider │
|
|
28
|
+
│ ───────────── │ │ ────────────────── │
|
|
29
|
+
│ - environment │◄─────────│ Uses config │
|
|
30
|
+
│ - client_id │ │ - bearer_token() │
|
|
31
|
+
│ - client_secret │ │ - refresh_token() │
|
|
32
|
+
│ - scope │ │ - clear_token() │
|
|
33
|
+
│ - endpoints() │ │ │
|
|
34
|
+
└──────────────────┘ └──────────────────────┘
|
|
35
|
+
│
|
|
36
|
+
│
|
|
37
|
+
▼
|
|
38
|
+
┌──────────────────────┐
|
|
39
|
+
│ TokenStore │
|
|
40
|
+
│ ────────────────── │
|
|
41
|
+
│ (MemoryStore) │
|
|
42
|
+
│ - fetch() │
|
|
43
|
+
│ - write() │
|
|
44
|
+
│ - clear() │
|
|
45
|
+
└──────────────────────┘
|
|
46
|
+
│
|
|
47
|
+
▼
|
|
48
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
49
|
+
│ Resources::Webhook (Resource Layer) │
|
|
50
|
+
│ ┌───────────────────────────────────────────────────────────┐ │
|
|
51
|
+
│ │ list(limit:, offset:) │ │
|
|
52
|
+
│ │ show(webhook_id) │ │
|
|
53
|
+
│ │ create(url:, object_type:, enabled:, description:) │ │
|
|
54
|
+
│ │ update(webhook_id, ...) │ │
|
|
55
|
+
│ │ delete(webhook_id) │ │
|
|
56
|
+
│ │ │ │
|
|
57
|
+
│ │ Private validation methods: │ │
|
|
58
|
+
│ │ - validate_id!() │ │
|
|
59
|
+
│ │ - validate_presence!() │ │
|
|
60
|
+
│ │ - validate_url!() │ │
|
|
61
|
+
│ └───────────────────────────────────────────────────────────┘ │
|
|
62
|
+
└────────────────────────┬────────────────────────────────────────┘
|
|
63
|
+
│
|
|
64
|
+
▼
|
|
65
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
66
|
+
│ Client (HTTP Layer) │
|
|
67
|
+
│ ┌───────────────────────────────────────────────────────────┐ │
|
|
68
|
+
│ │ get(path, params:) │ │
|
|
69
|
+
│ │ post(path, body:) │ │
|
|
70
|
+
│ │ patch(path, body:) │ │
|
|
71
|
+
│ │ delete(path) │ │
|
|
72
|
+
│ │ │ │
|
|
73
|
+
│ │ Private: │ │
|
|
74
|
+
│ │ - connection() - Faraday with auth headers │ │
|
|
75
|
+
│ │ - handle_faraday_error() │ │
|
|
76
|
+
│ └───────────────────────────────────────────────────────────┘ │
|
|
77
|
+
└────────────────────────┬────────────────────────────────────────┘
|
|
78
|
+
│
|
|
79
|
+
▼
|
|
80
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
81
|
+
│ Faraday (HTTP Client) │
|
|
82
|
+
│ ┌───────────────────────────────────────────────────────────┐ │
|
|
83
|
+
│ │ Authorization: Bearer <token> │ │
|
|
84
|
+
│ │ Content-Type: application/json │ │
|
|
85
|
+
│ │ Accept: application/json │ │
|
|
86
|
+
│ └───────────────────────────────────────────────────────────┘ │
|
|
87
|
+
└────────────────────────┬────────────────────────────────────────┘
|
|
88
|
+
│
|
|
89
|
+
▼
|
|
90
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
91
|
+
│ Zai API │
|
|
92
|
+
│ sandbox.au-0000.api.assemblypay.com/webhooks │
|
|
93
|
+
└────────────────────────┬────────────────────────────────────────┘
|
|
94
|
+
│
|
|
95
|
+
│ HTTP Response
|
|
96
|
+
▼
|
|
97
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
98
|
+
│ Response (Wrapper) │
|
|
99
|
+
│ ┌───────────────────────────────────────────────────────────┐ │
|
|
100
|
+
│ │ status - HTTP status code │ │
|
|
101
|
+
│ │ body - Raw response body │ │
|
|
102
|
+
│ │ headers - Response headers │ │
|
|
103
|
+
│ │ data() - Extracted data │ │
|
|
104
|
+
│ │ meta() - Pagination metadata │ │
|
|
105
|
+
│ │ success?() - 2xx status check │ │
|
|
106
|
+
│ │ client_error?()- 4xx status check │ │
|
|
107
|
+
│ │ server_error?()- 5xx status check │ │
|
|
108
|
+
│ │ │ │
|
|
109
|
+
│ │ Private: │ │
|
|
110
|
+
│ │ - check_for_errors!() - Raises specific errors │ │
|
|
111
|
+
│ └───────────────────────────────────────────────────────────┘ │
|
|
112
|
+
└────────────────────────┬────────────────────────────────────────┘
|
|
113
|
+
│
|
|
114
|
+
▼
|
|
115
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
116
|
+
│ Error Hierarchy │
|
|
117
|
+
│ ┌───────────────────────────────────────────────────────────┐ │
|
|
118
|
+
│ │ Error (Base) │ │
|
|
119
|
+
│ │ ├── AuthError │ │
|
|
120
|
+
│ │ ├── ConfigurationError │ │
|
|
121
|
+
│ │ ├── ApiError │ │
|
|
122
|
+
│ │ │ ├── BadRequestError (400) │ │
|
|
123
|
+
│ │ │ ├── UnauthorizedError (401) │ │
|
|
124
|
+
│ │ │ ├── ForbiddenError (403) │ │
|
|
125
|
+
│ │ │ ├── NotFoundError (404) │ │
|
|
126
|
+
│ │ │ ├── ValidationError (422) │ │
|
|
127
|
+
│ │ │ ├── RateLimitError (429) │ │
|
|
128
|
+
│ │ │ └── ServerError (5xx) │ │
|
|
129
|
+
│ │ ├── TimeoutError │ │
|
|
130
|
+
│ │ └── ConnectionError │ │
|
|
131
|
+
│ └───────────────────────────────────────────────────────────┘ │
|
|
132
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Request Flow
|
|
136
|
+
|
|
137
|
+
1. **Client calls** `ZaiPayment.webhooks.list()`
|
|
138
|
+
2. **Module** returns singleton `Resources::Webhook` instance
|
|
139
|
+
3. **Webhook resource** validates input and calls `client.get('/webhooks', params: {...})`
|
|
140
|
+
4. **Client** prepares HTTP request with authentication
|
|
141
|
+
5. **TokenProvider** provides valid bearer token (auto-refresh if expired)
|
|
142
|
+
6. **Faraday** makes HTTP request to Zai API
|
|
143
|
+
7. **Response** wraps Faraday response
|
|
144
|
+
8. **Response** checks status and raises error if needed
|
|
145
|
+
9. **Response** returns to client with `data()` and `meta()` methods
|
|
146
|
+
10. **Client application** receives response and processes data
|
|
147
|
+
|
|
148
|
+
## Key Design Decisions
|
|
149
|
+
|
|
150
|
+
### 1. Singleton Pattern for Resources
|
|
151
|
+
```ruby
|
|
152
|
+
ZaiPayment.webhooks # Always returns same instance
|
|
153
|
+
```
|
|
154
|
+
- Reduces object creation overhead
|
|
155
|
+
- Consistent configuration across application
|
|
156
|
+
- Easy to use in any context
|
|
157
|
+
|
|
158
|
+
### 2. Dependency Injection
|
|
159
|
+
```ruby
|
|
160
|
+
Webhook.new(client: custom_client)
|
|
161
|
+
```
|
|
162
|
+
- Testable (can inject mock client)
|
|
163
|
+
- Flexible (can use different configs)
|
|
164
|
+
- Follows SOLID principles
|
|
165
|
+
|
|
166
|
+
### 3. Response Wrapper
|
|
167
|
+
```ruby
|
|
168
|
+
response = webhooks.list
|
|
169
|
+
response.success? # Boolean check
|
|
170
|
+
response.data # Extracted data
|
|
171
|
+
response.meta # Pagination info
|
|
172
|
+
```
|
|
173
|
+
- Consistent interface across all resources
|
|
174
|
+
- Rich API for checking status
|
|
175
|
+
- Automatic error handling
|
|
176
|
+
|
|
177
|
+
### 4. Fail Fast Validation
|
|
178
|
+
```ruby
|
|
179
|
+
validate_url!(url) # Before API call
|
|
180
|
+
```
|
|
181
|
+
- Catches errors early
|
|
182
|
+
- Better error messages
|
|
183
|
+
- Reduces unnecessary API calls
|
|
184
|
+
|
|
185
|
+
### 5. Resource-Based Organization
|
|
186
|
+
```ruby
|
|
187
|
+
lib/zai_payment/resources/
|
|
188
|
+
├── webhook.rb
|
|
189
|
+
├── user.rb # Future
|
|
190
|
+
└── item.rb # Future
|
|
191
|
+
```
|
|
192
|
+
- Easy to extend
|
|
193
|
+
- Clear separation of concerns
|
|
194
|
+
- Follows REST principles
|
|
195
|
+
|
|
196
|
+
## Thread Safety
|
|
197
|
+
|
|
198
|
+
- ✅ **TokenProvider**: Uses Mutex for thread-safe token refresh
|
|
199
|
+
- ✅ **MemoryStore**: Thread-safe token storage
|
|
200
|
+
- ✅ **Client**: Creates new Faraday connection per instance
|
|
201
|
+
- ✅ **Webhook**: Stateless, no shared mutable state
|
|
202
|
+
|
|
203
|
+
## Extension Points
|
|
204
|
+
|
|
205
|
+
Add new resources by following the same pattern:
|
|
206
|
+
|
|
207
|
+
```ruby
|
|
208
|
+
# lib/zai_payment/resources/user.rb
|
|
209
|
+
module ZaiPayment
|
|
210
|
+
module Resources
|
|
211
|
+
class User
|
|
212
|
+
def initialize(client: nil)
|
|
213
|
+
@client = client || Client.new
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def list
|
|
217
|
+
client.get('/users')
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def show(user_id)
|
|
221
|
+
client.get("/users/#{user_id}")
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# lib/zai_payment.rb
|
|
228
|
+
def users
|
|
229
|
+
@users ||= Resources::User.new
|
|
230
|
+
end
|
|
231
|
+
```
|
|
232
|
+
|
data/docs/WEBHOOKS.md
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# Zai Payment Webhook Implementation
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
This document provides a summary of the webhook implementation in the zai_payment gem.
|
|
5
|
+
|
|
6
|
+
## Architecture
|
|
7
|
+
|
|
8
|
+
### Core Components
|
|
9
|
+
|
|
10
|
+
1. **Client** (`lib/zai_payment/client.rb`)
|
|
11
|
+
- Base HTTP client for making API requests
|
|
12
|
+
- Handles authentication automatically via TokenProvider
|
|
13
|
+
- Supports GET, POST, PATCH, DELETE methods
|
|
14
|
+
- Manages connection with proper headers and JSON encoding/decoding
|
|
15
|
+
|
|
16
|
+
2. **Response** (`lib/zai_payment/response.rb`)
|
|
17
|
+
- Wraps Faraday responses
|
|
18
|
+
- Provides convenient methods: `success?`, `client_error?`, `server_error?`
|
|
19
|
+
- Automatically raises appropriate errors based on HTTP status
|
|
20
|
+
- Extracts data and metadata from response body
|
|
21
|
+
|
|
22
|
+
3. **Webhook Resource** (`lib/zai_payment/resources/webhook.rb`)
|
|
23
|
+
- Implements all CRUD operations for webhooks
|
|
24
|
+
- Full input validation
|
|
25
|
+
- Clean, documented API
|
|
26
|
+
|
|
27
|
+
4. **Enhanced Error Handling** (`lib/zai_payment/errors.rb`)
|
|
28
|
+
- Specific error classes for different scenarios
|
|
29
|
+
- Makes debugging and error handling easier
|
|
30
|
+
|
|
31
|
+
## API Methods
|
|
32
|
+
|
|
33
|
+
### List Webhooks
|
|
34
|
+
```ruby
|
|
35
|
+
ZaiPayment.webhooks.list(limit: 10, offset: 0)
|
|
36
|
+
```
|
|
37
|
+
- Returns paginated list of webhooks
|
|
38
|
+
- Response includes `data` (array of webhooks) and `meta` (pagination info)
|
|
39
|
+
|
|
40
|
+
### Show Webhook
|
|
41
|
+
```ruby
|
|
42
|
+
ZaiPayment.webhooks.show(webhook_id)
|
|
43
|
+
```
|
|
44
|
+
- Returns details of a specific webhook
|
|
45
|
+
- Raises `NotFoundError` if webhook doesn't exist
|
|
46
|
+
|
|
47
|
+
### Create Webhook
|
|
48
|
+
```ruby
|
|
49
|
+
ZaiPayment.webhooks.create(
|
|
50
|
+
url: 'https://example.com/webhook',
|
|
51
|
+
object_type: 'transactions',
|
|
52
|
+
enabled: true,
|
|
53
|
+
description: 'Optional description'
|
|
54
|
+
)
|
|
55
|
+
```
|
|
56
|
+
- Validates URL format
|
|
57
|
+
- Validates required fields
|
|
58
|
+
- Returns created webhook with ID
|
|
59
|
+
|
|
60
|
+
### Update Webhook
|
|
61
|
+
```ruby
|
|
62
|
+
ZaiPayment.webhooks.update(
|
|
63
|
+
webhook_id,
|
|
64
|
+
url: 'https://example.com/new-webhook',
|
|
65
|
+
enabled: false
|
|
66
|
+
)
|
|
67
|
+
```
|
|
68
|
+
- All fields are optional
|
|
69
|
+
- Only updates provided fields
|
|
70
|
+
- Validates URL format if URL is provided
|
|
71
|
+
|
|
72
|
+
### Delete Webhook
|
|
73
|
+
```ruby
|
|
74
|
+
ZaiPayment.webhooks.delete(webhook_id)
|
|
75
|
+
```
|
|
76
|
+
- Permanently deletes the webhook
|
|
77
|
+
- Returns 204 No Content on success
|
|
78
|
+
|
|
79
|
+
## Error Handling
|
|
80
|
+
|
|
81
|
+
The gem provides specific error classes:
|
|
82
|
+
|
|
83
|
+
| Error Class | HTTP Status | Description |
|
|
84
|
+
|------------|-------------|-------------|
|
|
85
|
+
| `ValidationError` | 400, 422 | Invalid input data |
|
|
86
|
+
| `UnauthorizedError` | 401 | Authentication failed |
|
|
87
|
+
| `ForbiddenError` | 403 | Access denied |
|
|
88
|
+
| `NotFoundError` | 404 | Resource not found |
|
|
89
|
+
| `RateLimitError` | 429 | Too many requests |
|
|
90
|
+
| `ServerError` | 5xx | Server-side error |
|
|
91
|
+
| `TimeoutError` | - | Request timeout |
|
|
92
|
+
| `ConnectionError` | - | Connection failed |
|
|
93
|
+
|
|
94
|
+
Example:
|
|
95
|
+
```ruby
|
|
96
|
+
begin
|
|
97
|
+
response = ZaiPayment.webhooks.create(...)
|
|
98
|
+
rescue ZaiPayment::Errors::ValidationError => e
|
|
99
|
+
puts "Validation failed: #{e.message}"
|
|
100
|
+
rescue ZaiPayment::Errors::UnauthorizedError => e
|
|
101
|
+
puts "Authentication failed: #{e.message}"
|
|
102
|
+
end
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Best Practices Implemented
|
|
106
|
+
|
|
107
|
+
1. **Single Responsibility**: Each class has a clear, focused purpose
|
|
108
|
+
2. **DRY (Don't Repeat Yourself)**: Client and Response classes are reusable
|
|
109
|
+
3. **Error Handling**: Comprehensive error handling with specific error classes
|
|
110
|
+
4. **Input Validation**: All inputs are validated before making API calls
|
|
111
|
+
5. **Documentation**: Inline documentation with examples
|
|
112
|
+
6. **Testing**: Comprehensive test coverage using RSpec
|
|
113
|
+
7. **Thread Safety**: TokenProvider uses mutex for thread-safe token refresh
|
|
114
|
+
8. **Configuration**: Centralized configuration management
|
|
115
|
+
9. **RESTful Design**: Follows REST principles for resource management
|
|
116
|
+
10. **Response Wrapping**: Consistent response format across all methods
|
|
117
|
+
|
|
118
|
+
## Usage Examples
|
|
119
|
+
|
|
120
|
+
See `examples/webhooks.rb` for complete examples including:
|
|
121
|
+
- Basic CRUD operations
|
|
122
|
+
- Pagination
|
|
123
|
+
- Error handling
|
|
124
|
+
- Custom client instances
|
|
125
|
+
|
|
126
|
+
## Testing
|
|
127
|
+
|
|
128
|
+
Run the webhook tests:
|
|
129
|
+
```bash
|
|
130
|
+
bundle exec rspec spec/zai_payment/resources/webhook_spec.rb
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
The test suite covers:
|
|
134
|
+
- All CRUD operations
|
|
135
|
+
- Success and error scenarios
|
|
136
|
+
- Input validation
|
|
137
|
+
- Error handling
|
|
138
|
+
- Edge cases
|
|
139
|
+
|
|
140
|
+
## Future Enhancements
|
|
141
|
+
|
|
142
|
+
Potential improvements for future versions:
|
|
143
|
+
1. Webhook job management (list jobs, show job details)
|
|
144
|
+
2. Webhook signature verification
|
|
145
|
+
3. Webhook retry logic
|
|
146
|
+
4. Bulk operations
|
|
147
|
+
5. Async webhook operations
|
|
148
|
+
|
|
149
|
+
## API Reference
|
|
150
|
+
|
|
151
|
+
For the official Zai API documentation, see:
|
|
152
|
+
- [List Webhooks](https://developer.hellozai.com/reference/getallwebhooks)
|
|
153
|
+
- [Show Webhook](https://developer.hellozai.com/reference/getwebhookbyid)
|
|
154
|
+
- [Create Webhook](https://developer.hellozai.com/reference/createwebhook)
|
|
155
|
+
- [Update Webhook](https://developer.hellozai.com/reference/updatewebhook)
|
|
156
|
+
- [Delete Webhook](https://developer.hellozai.com/reference/deletewebhookbyid)
|
|
157
|
+
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# Webhook Examples
|
|
2
|
+
|
|
3
|
+
This file demonstrates how to use the ZaiPayment webhook functionality.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
require 'zai_payment'
|
|
9
|
+
|
|
10
|
+
# Configure the gem
|
|
11
|
+
ZaiPayment.configure do |config|
|
|
12
|
+
config.environment = :prelive # or :production
|
|
13
|
+
config.client_id = 'your_client_id'
|
|
14
|
+
config.client_secret = 'your_client_secret'
|
|
15
|
+
config.scope = 'your_scope'
|
|
16
|
+
end
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## List Webhooks
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
# Get all webhooks
|
|
23
|
+
response = ZaiPayment.webhooks.list
|
|
24
|
+
puts response.data # Array of webhooks
|
|
25
|
+
puts response.meta # Pagination metadata
|
|
26
|
+
|
|
27
|
+
# With pagination
|
|
28
|
+
response = ZaiPayment.webhooks.list(limit: 20, offset: 10)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Show a Specific Webhook
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
webhook_id = 'webhook_123'
|
|
35
|
+
response = ZaiPayment.webhooks.show(webhook_id)
|
|
36
|
+
|
|
37
|
+
webhook = response.data
|
|
38
|
+
puts webhook['id']
|
|
39
|
+
puts webhook['url']
|
|
40
|
+
puts webhook['object_type']
|
|
41
|
+
puts webhook['enabled']
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Create a Webhook
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
response = ZaiPayment.webhooks.create(
|
|
48
|
+
url: 'https://example.com/webhooks/zai',
|
|
49
|
+
object_type: 'transactions',
|
|
50
|
+
enabled: true,
|
|
51
|
+
description: 'Production webhook for transactions'
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
new_webhook = response.data
|
|
55
|
+
puts "Created webhook with ID: #{new_webhook['id']}"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Update a Webhook
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
webhook_id = 'webhook_123'
|
|
62
|
+
|
|
63
|
+
# Update specific fields
|
|
64
|
+
response = ZaiPayment.webhooks.update(
|
|
65
|
+
webhook_id,
|
|
66
|
+
enabled: false,
|
|
67
|
+
description: 'Temporarily disabled'
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Or update multiple fields
|
|
71
|
+
response = ZaiPayment.webhooks.update(
|
|
72
|
+
webhook_id,
|
|
73
|
+
url: 'https://example.com/webhooks/zai-v2',
|
|
74
|
+
object_type: 'items',
|
|
75
|
+
enabled: true
|
|
76
|
+
)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Delete a Webhook
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
webhook_id = 'webhook_123'
|
|
83
|
+
response = ZaiPayment.webhooks.delete(webhook_id)
|
|
84
|
+
|
|
85
|
+
if response.success?
|
|
86
|
+
puts "Webhook deleted successfully"
|
|
87
|
+
end
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Error Handling
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
begin
|
|
94
|
+
response = ZaiPayment.webhooks.create(
|
|
95
|
+
url: 'https://example.com/webhook',
|
|
96
|
+
object_type: 'transactions'
|
|
97
|
+
)
|
|
98
|
+
rescue ZaiPayment::Errors::ValidationError => e
|
|
99
|
+
puts "Validation error: #{e.message}"
|
|
100
|
+
rescue ZaiPayment::Errors::UnauthorizedError => e
|
|
101
|
+
puts "Authentication failed: #{e.message}"
|
|
102
|
+
rescue ZaiPayment::Errors::NotFoundError => e
|
|
103
|
+
puts "Resource not found: #{e.message}"
|
|
104
|
+
rescue ZaiPayment::Errors::ApiError => e
|
|
105
|
+
puts "API error: #{e.message}"
|
|
106
|
+
end
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Using Custom Client Instance
|
|
110
|
+
|
|
111
|
+
If you need more control, you can create your own client instance:
|
|
112
|
+
|
|
113
|
+
```ruby
|
|
114
|
+
config = ZaiPayment::Config.new
|
|
115
|
+
config.environment = :prelive
|
|
116
|
+
config.client_id = 'your_client_id'
|
|
117
|
+
config.client_secret = 'your_client_secret'
|
|
118
|
+
config.scope = 'your_scope'
|
|
119
|
+
|
|
120
|
+
token_provider = ZaiPayment::Auth::TokenProvider.new(config: config)
|
|
121
|
+
client = ZaiPayment::Client.new(config: config, token_provider: token_provider)
|
|
122
|
+
|
|
123
|
+
webhooks = ZaiPayment::Resources::Webhook.new(client: client)
|
|
124
|
+
response = webhooks.list
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Response Object
|
|
128
|
+
|
|
129
|
+
All webhook methods return a `ZaiPayment::Response` object with the following methods:
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
response = ZaiPayment.webhooks.list
|
|
133
|
+
|
|
134
|
+
# Check status
|
|
135
|
+
response.success? # => true/false (2xx status)
|
|
136
|
+
response.client_error? # => true/false (4xx status)
|
|
137
|
+
response.server_error? # => true/false (5xx status)
|
|
138
|
+
|
|
139
|
+
# Access data
|
|
140
|
+
response.data # => Main response data (array or hash)
|
|
141
|
+
response.meta # => Pagination metadata (if available)
|
|
142
|
+
response.body # => Raw response body
|
|
143
|
+
response.headers # => Response headers
|
|
144
|
+
response.status # => HTTP status code
|
|
145
|
+
```
|
|
146
|
+
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'faraday'
|
|
4
|
+
|
|
5
|
+
module ZaiPayment
|
|
6
|
+
# Base API client that handles HTTP requests to Zai API
|
|
7
|
+
class Client
|
|
8
|
+
attr_reader :config, :token_provider
|
|
9
|
+
|
|
10
|
+
def initialize(config: nil, token_provider: nil)
|
|
11
|
+
@config = config || ZaiPayment.config
|
|
12
|
+
@token_provider = token_provider || ZaiPayment.auth
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Perform a GET request
|
|
16
|
+
#
|
|
17
|
+
# @param path [String] the API endpoint path
|
|
18
|
+
# @param params [Hash] query parameters
|
|
19
|
+
# @return [Response] the API response
|
|
20
|
+
def get(path, params: {})
|
|
21
|
+
request(:get, path, params: params)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Perform a POST request
|
|
25
|
+
#
|
|
26
|
+
# @param path [String] the API endpoint path
|
|
27
|
+
# @param body [Hash] request body
|
|
28
|
+
# @return [Response] the API response
|
|
29
|
+
def post(path, body: {})
|
|
30
|
+
request(:post, path, body: body)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Perform a PATCH request
|
|
34
|
+
#
|
|
35
|
+
# @param path [String] the API endpoint path
|
|
36
|
+
# @param body [Hash] request body
|
|
37
|
+
# @return [Response] the API response
|
|
38
|
+
def patch(path, body: {})
|
|
39
|
+
request(:patch, path, body: body)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Perform a DELETE request
|
|
43
|
+
#
|
|
44
|
+
# @param path [String] the API endpoint path
|
|
45
|
+
# @return [Response] the API response
|
|
46
|
+
def delete(path)
|
|
47
|
+
request(:delete, path)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def request(method, path, params: {}, body: {})
|
|
53
|
+
response = connection.public_send(method) do |req|
|
|
54
|
+
req.url path
|
|
55
|
+
req.params = params if params.any?
|
|
56
|
+
req.body = body if body.any?
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
Response.new(response)
|
|
60
|
+
rescue Faraday::Error => e
|
|
61
|
+
handle_faraday_error(e)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def connection
|
|
65
|
+
@connection ||= build_connection
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def build_connection
|
|
69
|
+
Faraday.new do |faraday|
|
|
70
|
+
configure_connection(faraday)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def configure_connection(faraday)
|
|
75
|
+
faraday.url_prefix = base_url
|
|
76
|
+
apply_headers(faraday)
|
|
77
|
+
apply_middleware(faraday)
|
|
78
|
+
apply_timeouts(faraday)
|
|
79
|
+
faraday.adapter Faraday.default_adapter
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def apply_headers(faraday)
|
|
83
|
+
faraday.headers['Authorization'] = token_provider.bearer_token
|
|
84
|
+
faraday.headers['Content-Type'] = 'application/json'
|
|
85
|
+
faraday.headers['Accept'] = 'application/json'
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def apply_middleware(faraday)
|
|
89
|
+
faraday.request :json
|
|
90
|
+
faraday.response :json, content_type: /\bjson$/
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def apply_timeouts(faraday)
|
|
94
|
+
faraday.options.timeout = config.timeout if config.timeout
|
|
95
|
+
faraday.options.open_timeout = config.open_timeout if config.open_timeout
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def base_url
|
|
99
|
+
# Webhooks API uses va_base endpoint
|
|
100
|
+
config.endpoints[:va_base]
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def handle_faraday_error(error)
|
|
104
|
+
case error
|
|
105
|
+
when Faraday::TimeoutError
|
|
106
|
+
raise Errors::TimeoutError, "Request timed out: #{error.message}"
|
|
107
|
+
when Faraday::ConnectionFailed
|
|
108
|
+
raise Errors::ConnectionError, "Connection failed: #{error.message}"
|
|
109
|
+
when Faraday::ClientError
|
|
110
|
+
raise Errors::ApiError, "Client error: #{error.message}"
|
|
111
|
+
else
|
|
112
|
+
raise Errors::ApiError, "Request failed: #{error.message}"
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
data/lib/zai_payment/config.rb
CHANGED
data/lib/zai_payment/errors.rb
CHANGED
|
@@ -2,8 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
module ZaiPayment
|
|
4
4
|
module Errors
|
|
5
|
+
# Base error class
|
|
5
6
|
class Error < StandardError; end
|
|
7
|
+
|
|
8
|
+
# Authentication errors
|
|
6
9
|
class AuthError < Error; end
|
|
10
|
+
|
|
11
|
+
# Configuration errors
|
|
7
12
|
class ConfigurationError < Error; end
|
|
13
|
+
|
|
14
|
+
# API errors
|
|
15
|
+
class ApiError < Error; end
|
|
16
|
+
class BadRequestError < ApiError; end
|
|
17
|
+
class UnauthorizedError < ApiError; end
|
|
18
|
+
class ForbiddenError < ApiError; end
|
|
19
|
+
class NotFoundError < ApiError; end
|
|
20
|
+
class ValidationError < ApiError; end
|
|
21
|
+
class RateLimitError < ApiError; end
|
|
22
|
+
class ServerError < ApiError; end
|
|
23
|
+
|
|
24
|
+
# Network errors
|
|
25
|
+
class TimeoutError < Error; end
|
|
26
|
+
class ConnectionError < Error; end
|
|
8
27
|
end
|
|
9
28
|
end
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ZaiPayment
|
|
4
|
+
module Resources
|
|
5
|
+
# Webhook resource for managing Zai webhooks
|
|
6
|
+
#
|
|
7
|
+
# @see https://developer.hellozai.com/reference/getallwebhooks
|
|
8
|
+
class Webhook
|
|
9
|
+
attr_reader :client
|
|
10
|
+
|
|
11
|
+
def initialize(client: nil)
|
|
12
|
+
@client = client || Client.new
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# List all webhooks
|
|
16
|
+
#
|
|
17
|
+
# @param limit [Integer] number of records to return (default: 10)
|
|
18
|
+
# @param offset [Integer] number of records to skip (default: 0)
|
|
19
|
+
# @return [Response] the API response containing webhooks array
|
|
20
|
+
#
|
|
21
|
+
# @example
|
|
22
|
+
# webhooks = ZaiPayment::Resources::Webhook.new
|
|
23
|
+
# response = webhooks.list
|
|
24
|
+
# response.data # => [{"id" => "...", "url" => "..."}, ...]
|
|
25
|
+
#
|
|
26
|
+
# @see https://developer.hellozai.com/reference/getallwebhooks
|
|
27
|
+
def list(limit: 10, offset: 0)
|
|
28
|
+
params = {
|
|
29
|
+
limit: limit,
|
|
30
|
+
offset: offset
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
client.get('/webhooks', params: params)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Get a specific webhook by ID
|
|
37
|
+
#
|
|
38
|
+
# @param webhook_id [String] the webhook ID
|
|
39
|
+
# @return [Response] the API response containing webhook details
|
|
40
|
+
#
|
|
41
|
+
# @example
|
|
42
|
+
# webhooks = ZaiPayment::Resources::Webhook.new
|
|
43
|
+
# response = webhooks.show("webhook_id")
|
|
44
|
+
# response.data # => {"id" => "webhook_id", "url" => "...", ...}
|
|
45
|
+
#
|
|
46
|
+
# @see https://developer.hellozai.com/reference/getwebhookbyid
|
|
47
|
+
def show(webhook_id)
|
|
48
|
+
validate_id!(webhook_id, 'webhook_id')
|
|
49
|
+
client.get("/webhooks/#{webhook_id}")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Create a new webhook
|
|
53
|
+
#
|
|
54
|
+
# @param url [String] the webhook URL to receive notifications
|
|
55
|
+
# @param object_type [String] the type of object to watch (e.g., 'transactions', 'items')
|
|
56
|
+
# @param enabled [Boolean] whether the webhook is enabled (default: true)
|
|
57
|
+
# @param description [String] optional description of the webhook
|
|
58
|
+
# @return [Response] the API response containing created webhook
|
|
59
|
+
#
|
|
60
|
+
# @example
|
|
61
|
+
# webhooks = ZaiPayment::Resources::Webhook.new
|
|
62
|
+
# response = webhooks.create(
|
|
63
|
+
# url: "https://example.com/webhooks",
|
|
64
|
+
# object_type: "transactions",
|
|
65
|
+
# enabled: true
|
|
66
|
+
# )
|
|
67
|
+
#
|
|
68
|
+
# @see https://developer.hellozai.com/reference/createwebhook
|
|
69
|
+
def create(url: nil, object_type: nil, enabled: true, description: nil)
|
|
70
|
+
validate_presence!(url, 'url')
|
|
71
|
+
validate_presence!(object_type, 'object_type')
|
|
72
|
+
validate_url!(url)
|
|
73
|
+
|
|
74
|
+
body = {
|
|
75
|
+
url: url,
|
|
76
|
+
object_type: object_type,
|
|
77
|
+
enabled: enabled
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
body[:description] = description if description
|
|
81
|
+
|
|
82
|
+
client.post('/webhooks', body: body)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Update an existing webhook
|
|
86
|
+
#
|
|
87
|
+
# @param webhook_id [String] the webhook ID
|
|
88
|
+
# @param url [String] optional new webhook URL
|
|
89
|
+
# @param object_type [String] optional new object type
|
|
90
|
+
# @param enabled [Boolean] optional enabled status
|
|
91
|
+
# @param description [String] optional description
|
|
92
|
+
# @return [Response] the API response containing updated webhook
|
|
93
|
+
#
|
|
94
|
+
# @example
|
|
95
|
+
# webhooks = ZaiPayment::Resources::Webhook.new
|
|
96
|
+
# response = webhooks.update(
|
|
97
|
+
# "webhook_id",
|
|
98
|
+
# enabled: false
|
|
99
|
+
# )
|
|
100
|
+
#
|
|
101
|
+
# @see https://developer.hellozai.com/reference/updatewebhook
|
|
102
|
+
def update(webhook_id, url: nil, object_type: nil, enabled: nil, description: nil)
|
|
103
|
+
validate_id!(webhook_id, 'webhook_id')
|
|
104
|
+
|
|
105
|
+
body = {}
|
|
106
|
+
body[:url] = url if url
|
|
107
|
+
body[:object_type] = object_type if object_type
|
|
108
|
+
body[:enabled] = enabled unless enabled.nil?
|
|
109
|
+
body[:description] = description if description
|
|
110
|
+
|
|
111
|
+
validate_url!(url) if url
|
|
112
|
+
|
|
113
|
+
raise Errors::ValidationError, 'At least one attribute must be provided for update' if body.empty?
|
|
114
|
+
|
|
115
|
+
client.patch("/webhooks/#{webhook_id}", body: body)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Delete a webhook
|
|
119
|
+
#
|
|
120
|
+
# @param webhook_id [String] the webhook ID
|
|
121
|
+
# @return [Response] the API response
|
|
122
|
+
#
|
|
123
|
+
# @example
|
|
124
|
+
# webhooks = ZaiPayment::Resources::Webhook.new
|
|
125
|
+
# response = webhooks.delete("webhook_id")
|
|
126
|
+
#
|
|
127
|
+
# @see https://developer.hellozai.com/reference/deletewebhook
|
|
128
|
+
def delete(webhook_id)
|
|
129
|
+
validate_id!(webhook_id, 'webhook_id')
|
|
130
|
+
client.delete("/webhooks/#{webhook_id}")
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
private
|
|
134
|
+
|
|
135
|
+
def validate_id!(value, field_name)
|
|
136
|
+
return unless value.nil? || value.to_s.strip.empty?
|
|
137
|
+
|
|
138
|
+
raise Errors::ValidationError, "#{field_name} is required and cannot be blank"
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def validate_presence!(value, field_name)
|
|
142
|
+
return unless value.nil? || value.to_s.strip.empty?
|
|
143
|
+
|
|
144
|
+
raise Errors::ValidationError, "#{field_name} is required and cannot be blank"
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def validate_url!(url)
|
|
148
|
+
uri = URI.parse(url)
|
|
149
|
+
unless uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
|
|
150
|
+
raise Errors::ValidationError, 'url must be a valid HTTP or HTTPS URL'
|
|
151
|
+
end
|
|
152
|
+
rescue URI::InvalidURIError
|
|
153
|
+
raise Errors::ValidationError, 'url must be a valid URL'
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ZaiPayment
|
|
4
|
+
# Wrapper for API responses
|
|
5
|
+
class Response
|
|
6
|
+
attr_reader :status, :body, :headers, :raw_response
|
|
7
|
+
|
|
8
|
+
def initialize(faraday_response)
|
|
9
|
+
@raw_response = faraday_response
|
|
10
|
+
@status = faraday_response.status
|
|
11
|
+
@body = faraday_response.body
|
|
12
|
+
@headers = faraday_response.headers
|
|
13
|
+
|
|
14
|
+
check_for_errors!
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Check if the response was successful (2xx status)
|
|
18
|
+
def success?
|
|
19
|
+
(200..299).cover?(status)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Check if the response was a client error (4xx status)
|
|
23
|
+
def client_error?
|
|
24
|
+
(400..499).cover?(status)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Check if the response was a server error (5xx status)
|
|
28
|
+
def server_error?
|
|
29
|
+
(500..599).cover?(status)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Get the data from the response body
|
|
33
|
+
def data
|
|
34
|
+
body.is_a?(Hash) ? body['webhooks'] || body : body
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Get pagination or metadata info
|
|
38
|
+
def meta
|
|
39
|
+
body.is_a?(Hash) ? body['meta'] : nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
ERROR_STATUS_MAP = {
|
|
43
|
+
400 => Errors::BadRequestError,
|
|
44
|
+
401 => Errors::UnauthorizedError,
|
|
45
|
+
403 => Errors::ForbiddenError,
|
|
46
|
+
404 => Errors::NotFoundError,
|
|
47
|
+
422 => Errors::ValidationError,
|
|
48
|
+
429 => Errors::RateLimitError
|
|
49
|
+
}.merge((500..599).to_h { |code| [code, Errors::ServerError] }).freeze
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def check_for_errors!
|
|
54
|
+
return if success?
|
|
55
|
+
|
|
56
|
+
raise_appropriate_error
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def raise_appropriate_error
|
|
60
|
+
error_message = extract_error_message
|
|
61
|
+
error_class = error_class_for_status
|
|
62
|
+
raise error_class, error_message
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def error_class_for_status
|
|
66
|
+
ERROR_STATUS_MAP.fetch(status, Errors::ApiError)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def extract_error_message
|
|
70
|
+
if body.is_a?(Hash)
|
|
71
|
+
body['error'] || body['message'] || body['errors']&.join(', ') || "HTTP #{status}"
|
|
72
|
+
else
|
|
73
|
+
"HTTP #{status}: #{body}"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
data/lib/zai_payment/version.rb
CHANGED
data/lib/zai_payment.rb
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'faraday'
|
|
4
|
+
require 'uri'
|
|
4
5
|
require_relative 'zai_payment/version'
|
|
5
6
|
require_relative 'zai_payment/config'
|
|
6
7
|
require_relative 'zai_payment/errors'
|
|
7
8
|
require_relative 'zai_payment/auth/token_provider'
|
|
8
9
|
require_relative 'zai_payment/auth/token_store'
|
|
9
10
|
require_relative 'zai_payment/auth/token_stores/memory_store'
|
|
11
|
+
require_relative 'zai_payment/client'
|
|
12
|
+
require_relative 'zai_payment/response'
|
|
13
|
+
require_relative 'zai_payment/resources/webhook'
|
|
10
14
|
|
|
11
15
|
module ZaiPayment
|
|
12
16
|
class << self
|
|
@@ -29,5 +33,11 @@ module ZaiPayment
|
|
|
29
33
|
def clear_token! = auth.clear_token
|
|
30
34
|
def token_expiry = auth.token_expiry
|
|
31
35
|
def token_type = auth.token_type
|
|
36
|
+
|
|
37
|
+
# --- Resource accessors ---
|
|
38
|
+
# @return [ZaiPayment::Resources::Webhook] webhook resource instance
|
|
39
|
+
def webhooks
|
|
40
|
+
@webhooks ||= Resources::Webhook.new
|
|
41
|
+
end
|
|
32
42
|
end
|
|
33
43
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: zai_payment
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Eddy Jaga
|
|
@@ -32,15 +32,22 @@ extra_rdoc_files: []
|
|
|
32
32
|
files:
|
|
33
33
|
- CHANGELOG.md
|
|
34
34
|
- CODE_OF_CONDUCT.md
|
|
35
|
+
- IMPLEMENTATION.md
|
|
35
36
|
- LICENSE.txt
|
|
36
37
|
- README.md
|
|
37
38
|
- Rakefile
|
|
39
|
+
- docs/ARCHITECTURE.md
|
|
40
|
+
- docs/WEBHOOKS.md
|
|
41
|
+
- examples/webhooks.md
|
|
38
42
|
- lib/zai_payment.rb
|
|
39
43
|
- lib/zai_payment/auth/token_provider.rb
|
|
40
44
|
- lib/zai_payment/auth/token_store.rb
|
|
41
45
|
- lib/zai_payment/auth/token_stores/memory_store.rb
|
|
46
|
+
- lib/zai_payment/client.rb
|
|
42
47
|
- lib/zai_payment/config.rb
|
|
43
48
|
- lib/zai_payment/errors.rb
|
|
49
|
+
- lib/zai_payment/resources/webhook.rb
|
|
50
|
+
- lib/zai_payment/response.rb
|
|
44
51
|
- lib/zai_payment/version.rb
|
|
45
52
|
- sig/zai_payment.rbs
|
|
46
53
|
homepage: https://github.com/Sentia/zai-payment
|