┌─────────────────────────────────────────────────────────────────┐
│ Client Application │
└────────────────────────┬────────────────────────────────────────┘
│
│ ZaiPayment.webhooks.list()
│ ZaiPayment.webhooks.create(...)
│ ZaiPayment.webhooks.update(...)
│ ZaiPayment.webhooks.delete(...)
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ ZaiPayment (Module) │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ config() - Configuration singleton │ │
│ │ auth() - TokenProvider singleton │ │
│ │ webhooks() - Webhook resource singleton │ │
│ └───────────────────────────────────────────────────────────┘ │
└────────────────────────┬────────────────────────────────────────┘
│
┌───────────────┴───────────────┐
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────────┐
│ Config │ │ Auth::TokenProvider │
│ ───────────── │ │ ────────────────── │
│ - environment │◄─────────│ Uses config │
│ - client_id │ │ - bearer_token() │
│ - client_secret │ │ - refresh_token() │
│ - scope │ │ - clear_token() │
│ - endpoints() │ │ │
└──────────────────┘ └──────────────────────┘
│
│
▼
┌──────────────────────┐
│ TokenStore │
│ ────────────────── │
│ (MemoryStore) │
│ - fetch() │
│ - write() │
│ - clear() │
└──────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Resources::Webhook (Resource Layer) │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ list(limit:, offset:) │ │
│ │ show(webhook_id) │ │
│ │ create(url:, object_type:, enabled:, description:) │ │
│ │ update(webhook_id, ...) │ │
│ │ delete(webhook_id) │ │
│ │ │ │
│ │ Private validation methods: │ │
│ │ - validate_id!() │ │
│ │ - validate_presence!() │ │
│ │ - validate_url!() │ │
│ └───────────────────────────────────────────────────────────┘ │
└────────────────────────┬────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Client (HTTP Layer) │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ get(path, params:) │ │
│ │ post(path, body:) │ │
│ │ patch(path, body:) │ │
│ │ delete(path) │ │
│ │ │ │
│ │ Private: │ │
│ │ - connection() - Faraday with auth headers │ │
│ │ - handle_faraday_error() │ │
│ └───────────────────────────────────────────────────────────┘ │
└────────────────────────┬────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Faraday (HTTP Client) │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ Authorization: Bearer <token> │ │
│ │ Content-Type: application/json │ │
│ │ Accept: application/json │ │
│ └───────────────────────────────────────────────────────────┘ │
└────────────────────────┬────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Zai API │
│ sandbox.au-0000.api.assemblypay.com/webhooks │
└────────────────────────┬────────────────────────────────────────┘
│
│ HTTP Response
▼
┌─────────────────────────────────────────────────────────────────┐
│ Response (Wrapper) │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ status - HTTP status code │ │
│ │ body - Raw response body │ │
│ │ headers - Response headers │ │
│ │ data() - Extracted data │ │
│ │ meta() - Pagination metadata │ │
│ │ success?() - 2xx status check │ │
│ │ client_error?()- 4xx status check │ │
│ │ server_error?()- 5xx status check │ │
│ │ │ │
│ │ Private: │ │
│ │ - check_for_errors!() - Raises specific errors │ │
│ └───────────────────────────────────────────────────────────┘ │
└────────────────────────┬────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Error Hierarchy │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ Error (Base) │ │
│ │ ├── AuthError │ │
│ │ ├── ConfigurationError │ │
│ │ ├── ApiError │ │
│ │ │ ├── BadRequestError (400) │ │
│ │ │ ├── UnauthorizedError (401) │ │
│ │ │ ├── ForbiddenError (403) │ │
│ │ │ ├── NotFoundError (404) │ │
│ │ │ ├── ValidationError (422) │ │
│ │ │ ├── RateLimitError (429) │ │
│ │ │ └── ServerError (5xx) │ │
│ │ ├── TimeoutError │ │
│ │ └── ConnectionError │ │
│ └───────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
- Client calls
ZaiPayment.webhooks.list() - Module returns singleton
Resources::Webhookinstance - Webhook resource validates input and calls
client.get('/webhooks', params: {...}) - Client prepares HTTP request with authentication
- TokenProvider provides valid bearer token (auto-refresh if expired)
- Faraday makes HTTP request to Zai API
- Response wraps Faraday response
- Response checks status and raises error if needed
- Response returns to client with
data()andmeta()methods - Client application receives response and processes data
ZaiPayment.webhooks # Always returns same instance- Reduces object creation overhead
- Consistent configuration across application
- Easy to use in any context
Webhook.new(client: custom_client)- Testable (can inject mock client)
- Flexible (can use different configs)
- Follows SOLID principles
response = webhooks.list
response.success? # Boolean check
response.data # Extracted data
response.meta # Pagination info- Consistent interface across all resources
- Rich API for checking status
- Automatic error handling
validate_url!(url) # Before API call- Catches errors early
- Better error messages
- Reduces unnecessary API calls
lib/zai_payment/resources/
├── webhook.rb
├── user.rb # Future
└── item.rb # Future- Easy to extend
- Clear separation of concerns
- Follows REST principles
- ✅ TokenProvider: Uses Mutex for thread-safe token refresh
- ✅ MemoryStore: Thread-safe token storage
- ✅ Client: Creates new Faraday connection per instance
- ✅ Webhook: Stateless, no shared mutable state
Add new resources by following the same pattern:
# lib/zai_payment/resources/user.rb
module ZaiPayment
module Resources
class User
def initialize(client: nil)
@client = client || Client.new
end
def list
client.get('/users')
end
def show(user_id)
client.get("/users/#{user_id}")
end
end
end
end
# lib/zai_payment.rb
def users
@users ||= Resources::User.new
end