zai_payment 1.0.1 → 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 +66 -4
- 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 +13 -7
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